Merge pull request 'feat/core-function' (#12) from feat/core-function into main
Reviewed-on: eigen/fe-monorepo-template#12
This commit is contained in:
@@ -1,56 +1,56 @@
|
|||||||
# Auto-Update System
|
[← Back to Root](../../../README.md)
|
||||||
|
|
||||||
The Unified Update Lifecycle.
|
# Desktop Auto-Update System
|
||||||
|
|
||||||
> This document defines the strategic implementation of our cross-platform auto-update system. Powered by **electron-updater**, this architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
|
`apps/desktop` utilizes a unified update lifecycle powered by **electron-updater**. This architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## 🏗 Reactive Update Flow
|
||||||
|
|
||||||
- [Reactive Update Flow](#reactive-update-flow)
|
|
||||||
- [Current Provider: GitHub Releases](#current-provider-github-releases)
|
|
||||||
- [Release Workflow: The Deterministic Pipeline](#release-workflow-the-deterministic-pipeline)
|
|
||||||
- [CI/CD Environment Variables](#cicd-environment-variables)
|
|
||||||
- [Deployment Strategies](#deployment-strategies)
|
|
||||||
- [Code Signing: The Trust Boundary](#code-signing-the-trust-boundary)
|
|
||||||
- [Testing Updates in Development](#testing-updates-in-development)
|
|
||||||
- [Diagnostic Runbook](#diagnostic-runbook)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reactive Update Flow
|
|
||||||
|
|
||||||
The following diagram illustrates the **Reactive Update Flow**, bridging the Node.js Main Process with the React UI layer through a secure IPC event stream.
|
The following diagram illustrates the **Reactive Update Flow**, bridging the Node.js Main Process with the React UI layer through a secure IPC event stream.
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
graph TD
|
||||||
│ Main Process (src/main/index.ts) │
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
│ │
|
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
│ autoUpdater.checkForUpdatesAndNotify() │
|
classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||||
│ │ │
|
classDef ipcBridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
│ ├─→ 'checking-for-update' │
|
|
||||||
│ ├─→ 'update-available' → { version, releaseDate } │
|
%% ─── Subgraphs ───
|
||||||
│ ├─→ 'download-progress' → { percent, bytesPerSecond } │
|
subgraph MainProcess ["Main Process (src/main/index.ts)"]
|
||||||
│ ├─→ 'update-downloaded' → { version, releaseNotes } │
|
AUTO[autoUpdater.checkForUpdatesAndNotify]
|
||||||
│ └─→ 'error' → { message } │
|
EVENTS{{Update Events: progress, downloaded, error}}
|
||||||
│ │
|
SENDER[sendToRenderer]
|
||||||
│ sendToRenderer('updater:*', payload) │
|
end
|
||||||
├──────────────────────── IPC Bridge ──────────────────────────────┤
|
|
||||||
│ Preload (src/preload/index.ts) │
|
subgraph Preload ["IPC Bridge (src/preload/index.ts)"]
|
||||||
│ │
|
EXPOSE{contextBridge.exposeInMainWorld}
|
||||||
│ contextBridge.exposeInMainWorld('electronAPI', { │
|
end
|
||||||
│ onUpdateAvailable, onDownloadProgress, │
|
|
||||||
│ onUpdateDownloaded, onUpdateError, │
|
subgraph Renderer ["Renderer (React App - apps/web)"]
|
||||||
│ checkForUpdates, installUpdate │
|
HOOK([useElectronUpdater Hook])
|
||||||
│ }) │
|
ACTIONS[UI Actions: Install, Check]
|
||||||
├──────────────────────── Renderer ────────────────────────────────┤
|
end
|
||||||
│ React App (apps/web) │
|
|
||||||
│ │
|
%% ─── Flow & Relationships ───
|
||||||
│ useElectronUpdater() hook │
|
AUTO ---> EVENTS
|
||||||
│ → Reactive state: status, progress, updateInfo, errorMessage │
|
EVENTS ---> SENDER
|
||||||
│ → Actions: checkForUpdates(), installUpdate() │
|
SENDER ===>|'updater:*' Event Stream| EXPOSE
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
EXPOSE ===>|electronAPI window object| HOOK
|
||||||
|
HOOK -.->|Reactive State Status and Progress| ACTIONS
|
||||||
|
ACTIONS -.->|ipcRenderer.invoke| EXPOSE
|
||||||
|
EXPOSE -.->|Trigger Update or Install| AUTO
|
||||||
|
|
||||||
|
%% ─── Apply Styles ───
|
||||||
|
class AUTO,EVENTS,SENDER coreEntity;
|
||||||
|
class EXPOSE ipcBridge;
|
||||||
|
class HOOK,ACTIONS appEntity;
|
||||||
|
|
||||||
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||||
|
style MainProcess fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style Renderer fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lifecycle Sequence
|
### Lifecycle Sequence
|
||||||
@@ -63,7 +63,7 @@ The following diagram illustrates the **Reactive Update Flow**, bridging the Nod
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current Provider: GitHub Releases
|
## 🌐 Current Provider: GitHub Releases
|
||||||
|
|
||||||
The update provider is declared in `electron-builder.yml`:
|
The update provider is declared in `electron-builder.yml`:
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ publish:
|
|||||||
### Operational Mechanics
|
### Operational Mechanics
|
||||||
|
|
||||||
1. When `electron-builder --publish always` executes, it:
|
1. When `electron-builder --publish always` executes, it:
|
||||||
|
|
||||||
- Compiles the application for the target platform.
|
- Compiles the application for the target platform.
|
||||||
- Uploads the installer(s) to a **GitHub Release** tagged with the version from `package.json`.
|
- Uploads the installer(s) to a **GitHub Release** tagged with the version from `package.json`.
|
||||||
- Generates and uploads the platform-specific manifest: `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux).
|
- Generates and uploads the platform-specific manifest: `latest.yml` (Windows), `latest-mac.yml` (macOS), or `latest-linux.yml` (Linux).
|
||||||
@@ -88,7 +89,7 @@ publish:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Release Workflow: The Deterministic Pipeline
|
## 🚀 Release Workflow: The Deterministic Pipeline
|
||||||
|
|
||||||
To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries.
|
To maintain release integrity, follow this deterministic pipeline to synchronize web assets and native binaries.
|
||||||
|
|
||||||
@@ -109,8 +110,7 @@ pnpm run prebuild
|
|||||||
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml
|
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION] > **Treat `GH_TOKEN` as a critical secret.** It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault.
|
||||||
> **Treat `GH_TOKEN` as a critical secret.** It grants write access to your repository's release assets. Never commit it to version control, never log it in CI output, and always inject it via encrypted secrets or a vault.
|
|
||||||
|
|
||||||
### Automated Release (GitHub Actions)
|
### Automated Release (GitHub Actions)
|
||||||
|
|
||||||
@@ -155,18 +155,18 @@ jobs:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CI/CD Environment Variables
|
## 🔐 CI/CD Environment Variables
|
||||||
|
|
||||||
| Variable | Required | Platform | Description |
|
| Variable | Required | Platform | Description |
|
||||||
|---|---|---|---|
|
| ----------------------------- | ------------- | -------------- | --------------------------------------------------------------------------------------------- |
|
||||||
| `GH_TOKEN` | Yes | All | GitHub personal access token with `repo` scope. Authorizes release artifact uploads. |
|
| `GH_TOKEN` | Yes | All | GitHub personal access token with `repo` scope. Authorizes release artifact uploads. |
|
||||||
| `CSC_LINK` | macOS/Windows | macOS, Windows | Base64-encoded `.p12` code signing certificate. Generate with: `base64 -i cert.p12 \| pbcopy` |
|
| `CSC_LINK` | macOS/Windows | macOS, Windows | Base64-encoded `.p12` code signing certificate. Generate with: `base64 -i cert.p12 \| pbcopy` |
|
||||||
| `CSC_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Passphrase for the `.p12` certificate. |
|
| `CSC_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Passphrase for the `.p12` certificate. |
|
||||||
| `APPLE_ID` | macOS only | macOS | Apple ID email for notarization submission. |
|
| `APPLE_ID` | macOS only | macOS | Apple ID email for notarization submission. |
|
||||||
| `APPLE_APP_SPECIFIC_PASSWORD` | macOS only | macOS | App-specific password generated at [appleid.apple.com](https://appleid.apple.com). |
|
| `APPLE_APP_SPECIFIC_PASSWORD` | macOS only | macOS | App-specific password generated at [appleid.apple.com](https://appleid.apple.com). |
|
||||||
| `APPLE_TEAM_ID` | macOS only | macOS | Your Apple Developer Team ID. |
|
| `APPLE_TEAM_ID` | macOS only | macOS | Your Apple Developer Team ID. |
|
||||||
| `WIN_CSC_LINK` | Windows only | Windows | Separate Windows code signing certificate (if different from `CSC_LINK`). |
|
| `WIN_CSC_LINK` | Windows only | Windows | Separate Windows code signing certificate (if different from `CSC_LINK`). |
|
||||||
| `WIN_CSC_KEY_PASSWORD` | Windows only | Windows | Passphrase for the Windows certificate. |
|
| `WIN_CSC_KEY_PASSWORD` | Windows only | Windows | Passphrase for the Windows certificate. |
|
||||||
|
|
||||||
### Configuring Secrets
|
### Configuring Secrets
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ jobs:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment Strategies
|
## ☁️ Deployment Strategies
|
||||||
|
|
||||||
### AWS S3 (Private Infrastructure)
|
### AWS S3 (Private Infrastructure)
|
||||||
|
|
||||||
@@ -195,13 +195,14 @@ publish:
|
|||||||
|
|
||||||
**Additional environment variables:**
|
**Additional environment variables:**
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
| ----------------------- | -------------------------------------------------------------- |
|
||||||
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 `PutObject` and `GetObject` permissions |
|
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 `PutObject` and `GetObject` permissions |
|
||||||
| `AWS_SECRET_ACCESS_KEY` | IAM secret key |
|
| `AWS_SECRET_ACCESS_KEY` | IAM secret key |
|
||||||
|
|
||||||
**Bucket structure:**
|
**Bucket structure:**
|
||||||
```
|
|
||||||
|
```text
|
||||||
your-bucket/desktop-releases/
|
your-bucket/desktop-releases/
|
||||||
├── latest.yml (Windows manifest)
|
├── latest.yml (Windows manifest)
|
||||||
├── latest-mac.yml (macOS manifest)
|
├── latest-mac.yml (macOS manifest)
|
||||||
@@ -222,13 +223,12 @@ For self-hosted infrastructure (Nginx, Caddy, etc.):
|
|||||||
```yaml
|
```yaml
|
||||||
publish:
|
publish:
|
||||||
provider: generic
|
provider: generic
|
||||||
url: https://updates.your-domain.com/desktop
|
url: [https://updates.your-domain.com/desktop](https://updates.your-domain.com/desktop)
|
||||||
```
|
```
|
||||||
|
|
||||||
Your server must host the same directory structure as the S3 layout above.
|
Your server must host the same directory structure as the S3 layout above.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT] > **MIME Type Configuration**: Ensure your file server correctly serves `.yml` files with `text/yaml` and installer binaries with `application/octet-stream`. Incorrect MIME types will cause download corruption or silent update failures.
|
||||||
> **MIME Type Configuration**: Ensure your file server correctly serves `.yml` files with `text/yaml` and installer binaries with `application/octet-stream`. Incorrect MIME types will cause download corruption or silent update failures.
|
|
||||||
|
|
||||||
**Nginx reference:**
|
**Nginx reference:**
|
||||||
|
|
||||||
@@ -253,10 +253,9 @@ server {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Code Signing: The Trust Boundary
|
## 🛡️ Code Signing: The Trust Boundary
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING] > **Code signing is not merely a requirement — it is the Trust Boundary established by the operating system.** macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures, `electron-updater` will **reject update payloads entirely**.
|
||||||
> **Code signing is not merely a requirement — it is the Trust Boundary established by the operating system.** macOS Gatekeeper will explicitly terminate unsigned applications or refuse background updates to maintain system integrity. Windows SmartScreen will display alarming warnings to users. Without valid signatures, `electron-updater` will **reject update payloads entirely**.
|
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
@@ -273,7 +272,7 @@ server {
|
|||||||
```xml
|
```xml
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
"[http://www.apple.com/DTDs/PropertyList-1.0.dtd](http://www.apple.com/DTDs/PropertyList-1.0.dtd)">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
@@ -300,7 +299,7 @@ server {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing Updates in Development
|
## 🧪 Testing Updates in Development
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The auto-updater is **intentionally disabled** in development mode to prevent runtime crashes. Setting `forceDevUpdateConfig` requires a `dev-app-update.yml` file, which introduces unnecessary complexity during local development.
|
> The auto-updater is **intentionally disabled** in development mode to prevent runtime crashes. Setting `forceDevUpdateConfig` requires a `dev-app-update.yml` file, which introduces unnecessary complexity during local development.
|
||||||
@@ -317,6 +316,7 @@ if (IS_DEV) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
This means:
|
This means:
|
||||||
|
|
||||||
- No update check is performed on startup.
|
- No update check is performed on startup.
|
||||||
- No `electron-updater` events are emitted.
|
- No `electron-updater` events are emitted.
|
||||||
- The `useElectronUpdater()` hook will remain in `idle` status.
|
- The `useElectronUpdater()` hook will remain in `idle` status.
|
||||||
@@ -347,52 +347,34 @@ python3 -m http.server 8080 --directory .
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Diagnostic Runbook
|
## ⚠️ Diagnostic Runbook
|
||||||
|
|
||||||
### Issue: "Update check failed" on startup
|
### Issue: "Update check failed" on startup
|
||||||
|
|
||||||
| | |
|
| Symptom | Root Cause | Resolution |
|
||||||
|---|---|
|
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Symptom** | Console logs `[AutoUpdater] Startup check failed (possibly offline)` |
|
| Console logs `[AutoUpdater] Startup check failed (possibly offline)` | The machine is offline, or the update server (GitHub/S3/generic) is unreachable. | No action required. The error is caught in a `try/catch` block, logged to the console, and the application continues to function normally. The next check will occur on the next app launch. |
|
||||||
| **Root Cause** | The machine is offline, or the update server (GitHub/S3/generic) is unreachable. |
|
|
||||||
| **Resolution** | No action required. The error is caught in a `try/catch` block, logged to the console, and the application continues to function normally. The next check will occur on the next app launch. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Issue: `app-update.yml` not found in production build
|
### Issue: `app-update.yml` not found in production build
|
||||||
|
|
||||||
| | |
|
| Symptom | Root Cause | Resolution |
|
||||||
|---|---|
|
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Symptom** | `electron-updater` throws "Cannot find app-update.yml" immediately after launch. |
|
| `electron-updater` throws "Cannot find app-update.yml" immediately after launch. | The `publish` block in `electron-builder.yml` is missing or misconfigured. `electron-builder` generates `app-update.yml` only when a valid provider is declared. | Verify the `publish` block exists in `electron-builder.yml`. Run `electron-builder --publish never` and inspect `release/*/resources/app-update.yml` to confirm generation. |
|
||||||
| **Root Cause** | The `publish` block in `electron-builder.yml` is missing or misconfigured. `electron-builder` generates `app-update.yml` only when a valid provider is declared. |
|
|
||||||
| **Resolution** | Verify the `publish` block exists in `electron-builder.yml`. Run `electron-builder --publish never` and inspect `release/*/resources/app-update.yml` to confirm generation. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Issue: "Cannot update: code signature is invalid" (macOS)
|
### Issue: "Cannot update: code signature is invalid" (macOS)
|
||||||
|
|
||||||
| | |
|
| Symptom | Root Cause | Resolution |
|
||||||
|---|---|
|
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Symptom** | The updater downloads a new version but refuses to apply it, logging a signature validation error. |
|
| The updater downloads a new version but refuses to apply it, logging a signature validation error. | The application was not signed, or the signing certificate has expired / been revoked. | Ensure `CSC_LINK` and `CSC_KEY_PASSWORD` are correctly set in CI. Verify the packaged app with: `codesign --verify --deep --strict release/mac*/Desktop.app`. Re-sign and re-publish if the certificate was rotated. |
|
||||||
| **Root Cause** | The application was not signed, or the signing certificate has expired / been revoked. |
|
|
||||||
| **Resolution** | Ensure `CSC_LINK` and `CSC_KEY_PASSWORD` are correctly set in CI. Verify the packaged app with: `codesign --verify --deep --strict release/mac*/Desktop.app`. Re-sign and re-publish if the certificate was rotated. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Issue: Updates work on Windows/Linux but not macOS
|
### Issue: Updates work on Windows/Linux but not macOS
|
||||||
|
|
||||||
| | |
|
| Symptom | Root Cause | Resolution |
|
||||||
|---|---|
|
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Symptom** | Windows and Linux users receive updates, but macOS users see no update prompt. |
|
| Windows and Linux users receive updates, but macOS users see no update prompt. | macOS requires **both** a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload. | Provide all Apple credential environment variables (`APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`) and ensure `hardenedRuntime: true` is set in `electron-builder.yml`. Re-package and re-publish. |
|
||||||
| **Root Cause** | macOS requires **both** a valid code signature AND Apple notarization. Without notarization, Gatekeeper silently quarantines the update payload. |
|
|
||||||
| **Resolution** | Provide all Apple credential environment variables (`APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`) and ensure `hardenedRuntime: true` is set in `electron-builder.yml`. Re-package and re-publish. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Issue: S3/Generic provider returns corrupted downloads
|
### Issue: S3/Generic provider returns corrupted downloads
|
||||||
|
|
||||||
| | |
|
| Symptom | Root Cause | Resolution |
|
||||||
|---|---|
|
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| **Symptom** | Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. |
|
| Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. | The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale `latest*.yml` files. | Verify MIME types: `.yml` → `text/yaml`, `.exe`/`.dmg`/`.AppImage`/`.zip` → `application/octet-stream`. Add `Cache-Control: no-cache` headers to `latest*.yml` responses. Invalidate CDN cache after publishing a new release. |
|
||||||
| **Root Cause** | The file server is serving update manifests or binaries with incorrect MIME types, or a CDN is caching stale `latest*.yml` files. |
|
|
||||||
| **Resolution** | Verify MIME types: `.yml` → `text/yaml`, `.exe`/`.dmg`/`.AppImage`/`.zip` → `application/octet-stream`. Add `Cache-Control: no-cache` headers to `latest*.yml` responses. Invalidate CDN cache after publishing a new release. |
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# Configuration Guide
|
[← Back to Root](../../../README.md)
|
||||||
|
|
||||||
|
# Desktop Configuration Guide
|
||||||
|
|
||||||
The Blueprint for Runtime Control.
|
The Blueprint for Runtime Control.
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ The Blueprint for Runtime Control.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Target App Orchestration
|
## 🎯 Target App Orchestration
|
||||||
|
|
||||||
The Desktop Wrapper is architected to embed **any** web application within the monorepo ecosystem. The target application is resolved at build time through a declarative configuration surface in `apps/desktop/.env`.
|
The Desktop Wrapper is architected to embed **any** web application within the monorepo ecosystem. The target application is resolved at build time through a declarative configuration surface in `apps/desktop/.env`.
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ DESKTOP_DEV_SERVER_URL=http://localhost:5173
|
|||||||
To redirect the wrapper to a different application — for example, `apps/admin` — modify the configuration and re-execute the build pipeline:
|
To redirect the wrapper to a different application — for example, `apps/admin` — modify the configuration and re-execute the build pipeline:
|
||||||
|
|
||||||
1. **Update the `.env` declaration:**
|
1. **Update the `.env` declaration:**
|
||||||
|
|
||||||
```env
|
```env
|
||||||
DESKTOP_TARGET_APP=admin
|
DESKTOP_TARGET_APP=admin
|
||||||
DESKTOP_DEV_SERVER_URL=http://localhost:3001
|
DESKTOP_DEV_SERVER_URL=http://localhost:3001
|
||||||
@@ -51,61 +54,71 @@ To redirect the wrapper to a different application — for example, `apps/admin`
|
|||||||
|
|
||||||
### The Deployment Bridge
|
### The Deployment Bridge
|
||||||
|
|
||||||
The `prebuild` hook invokes `scripts/copy-web-dist.ts`, which serves as the **Deployment Bridge** between the web workspace and the native container. It reads `DESKTOP_TARGET_APP`, resolves the corresponding `apps/<target>/dist/` directory, and synchronizes the contents into `apps/desktop/web-dist/`. This bridge directory is then ingested by `electron-builder` via both the `files` and `extraResources` declarations in `electron-builder.yml`.
|
The `prebuild` hook invokes `scripts/copy-web-dist.ts`, which serves as the **Deployment Bridge** between the web workspace and the native container. It reads `DESKTOP_TARGET_APP`, resolves the corresponding `apps/<target>/dist/` directory, and synchronizes the contents into `apps/desktop/web-dist/`. This bridge directory is then ingested by `electron-builder`.
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
┌─────────────────────────────┐ Deployment Bridge ┌──────────────────────────┐
|
graph TD
|
||||||
│ apps/<target>/dist/ │ ─── copy-web-dist.ts ────────→ │ apps/desktop/web-dist/ │
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
│ (Vite build output) │ prebuild hook │ (Native container) │
|
classDef webApp fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
└─────────────────────────────┘ └──────────────────────────┘
|
classDef bridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
│
|
classDef electronApp fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||||
▼
|
classDef finalArtifact fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||||
electron-builder
|
|
||||||
files + extraResources
|
%% ─── Nodes ───
|
||||||
│
|
SOURCE[(apps/TARGET/dist/)]
|
||||||
▼
|
SCRIPT{copy-web-dist.ts}
|
||||||
┌──────────────────────┐
|
DEST[(apps/desktop/web-dist/)]
|
||||||
│ Packaged .app/.exe │
|
BUILDER(electron-builder)
|
||||||
│ resources/web-dist/ │
|
OUTPUT([Packaged .app / .exe])
|
||||||
└──────────────────────┘
|
|
||||||
|
%% ─── Flow ───
|
||||||
|
SOURCE ===>|Vite Build Output| SCRIPT
|
||||||
|
SCRIPT ===>|Deployment Bridge Prebuild Hook| DEST
|
||||||
|
DEST -.->|files and extraResources| BUILDER
|
||||||
|
BUILDER ===> OUTPUT
|
||||||
|
|
||||||
|
%% ─── Apply Styles ───
|
||||||
|
class SOURCE webApp;
|
||||||
|
class SCRIPT bridge;
|
||||||
|
class DEST,BUILDER electronApp;
|
||||||
|
class OUTPUT finalArtifact;
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Environment Variables Registry
|
## 🔐 Environment Variables Registry
|
||||||
|
|
||||||
| Variable | Default | Security Scope | Consumer | Description |
|
| Variable | Default | Security Scope | Consumer | Description |
|
||||||
|---|---|---|---|---|
|
| ----------------------------- | ----------------------- | -------------- | ------------------- | --------------------------------------------------------------- |
|
||||||
| `DESKTOP_TARGET_APP` | `web` | Build-time | `copy-web-dist.ts` | Workspace identifier of the web app to embed |
|
| `DESKTOP_TARGET_APP` | `web` | Build-time | `copy-web-dist.ts` | Workspace identifier of the web app to embed |
|
||||||
| `DESKTOP_DEV_SERVER_URL` | `http://localhost:5173` | Runtime (dev) | `src/main/index.ts` | Dev server URL loaded in the Electron window during development |
|
| `DESKTOP_DEV_SERVER_URL` | `http://localhost:5173` | Runtime (dev) | `src/main/index.ts` | Dev server URL loaded in the Electron window during development |
|
||||||
| `GH_TOKEN` | — | CI/CD | `electron-builder` | GitHub personal access token for publishing releases |
|
| `GH_TOKEN` | — | CI/CD | `electron-builder` | GitHub personal access token for publishing releases |
|
||||||
| `CSC_LINK` | — | CI/CD | `electron-builder` | Base64-encoded `.p12` code signing certificate |
|
| `CSC_LINK` | — | CI/CD | `electron-builder` | Base64-encoded `.p12` code signing certificate |
|
||||||
| `CSC_KEY_PASSWORD` | — | CI/CD | `electron-builder` | Passphrase for the `.p12` certificate |
|
| `CSC_KEY_PASSWORD` | — | CI/CD | `electron-builder` | Passphrase for the `.p12` certificate |
|
||||||
| `APPLE_ID` | — | CI/CD (macOS) | `electron-builder` | Apple ID email for notarization submission |
|
| `APPLE_ID` | — | CI/CD (macOS) | `electron-builder` | Apple ID email for notarization submission |
|
||||||
| `APPLE_APP_SPECIFIC_PASSWORD` | — | CI/CD (macOS) | `electron-builder` | App-specific password for notarization |
|
| `APPLE_APP_SPECIFIC_PASSWORD` | — | CI/CD (macOS) | `electron-builder` | App-specific password for notarization |
|
||||||
| `APPLE_TEAM_ID` | — | CI/CD (macOS) | `electron-builder` | Apple Developer Team ID |
|
| `APPLE_TEAM_ID` | — | CI/CD (macOS) | `electron-builder` | Apple Developer Team ID |
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT] > **Build-time** variables are consumed during the `prebuild` phase and baked into the artifact. **Runtime** variables are read by the Electron main process at launch. **CI/CD** variables are secrets injected exclusively in the deployment environment — they must never appear in source control or local `.env` files.
|
||||||
> **Build-time** variables are consumed during the `prebuild` phase and baked into the artifact. **Runtime** variables are read by the Electron main process at launch. **CI/CD** variables are secrets injected exclusively in the deployment environment — they must never appear in source control or local `.env` files.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deterministic Path Resolution
|
## 📂 Deterministic Path Resolution
|
||||||
|
|
||||||
The following matrix defines how the target app's static assets are resolved across every phase of the application lifecycle. Each path is **deterministic** — there is no runtime ambiguity.
|
The following matrix defines how the target app's static assets are resolved across every phase of the application lifecycle. Each path is **deterministic** — there is no runtime ambiguity.
|
||||||
|
|
||||||
| Phase | Resolution Strategy | Resolved Path | Context |
|
| Phase | Resolution Strategy | Resolved Path | Context |
|
||||||
|---|---|---|---|
|
| --------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------- |
|
||||||
| **Prebuild** | `copy-web-dist.ts` reads `DESKTOP_TARGET_APP` | `monorepo-root/apps/<target>/dist/` → `apps/desktop/web-dist/` | Deployment Bridge: build-time synchronization |
|
| **Prebuild** | `copy-web-dist.ts` reads `DESKTOP_TARGET_APP` | `monorepo-root/apps/<target>/dist/` → `apps/desktop/web-dist/` | Deployment Bridge: build-time synchronization |
|
||||||
| **Development** | `__dirname` relative traversal from `out/main/` | `apps/desktop/out/main/` → `../../` → `apps/` → `<target>/dist/` | Direct filesystem access to the web app's build output |
|
| **Development** | `__dirname` relative traversal from `out/main/` | `apps/desktop/out/main/` → `../../` → `apps/` → `<target>/dist/` | Direct filesystem access to the web app's build output |
|
||||||
| **Production** | `process.resourcesPath` | `Contents/Resources/web-dist/` (macOS) / `resources/web-dist/` (Windows/Linux) | OS-specific resource directory within the packaged binary |
|
| **Production** | `process.resourcesPath` | `Contents/Resources/web-dist/` (macOS) / `resources/web-dist/` (Windows/Linux) | OS-specific resource directory within the packaged binary |
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The development path relies on `__dirname` pointing to `apps/desktop/out/main/` at runtime. If electron-vite's output directory is ever reconfigured, this traversal must be updated in `getWebDistPath()` within `src/main/index.ts`.
|
> The development path relies on `__dirname` pointing to `apps/desktop/out/main/` at runtime. If electron-vite's output directory is ever reconfigured, this traversal must be updated in `getWebDistPath()` within `src/main/index.ts`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Production Routing: Overcoming Protocol Constraints
|
## 🚀 Production Routing: Overcoming Protocol Constraints
|
||||||
|
|
||||||
### The Constraint
|
### The Constraint
|
||||||
|
|
||||||
@@ -117,18 +130,34 @@ Electron's default `file://` protocol breaks this contract. Requesting `file:///
|
|||||||
|
|
||||||
The `app://` scheme is a **Privileged Virtual File System** that resolves SPA routing conflicts by implementing a **Heuristic Resource Loader**. It operates as follows:
|
The `app://` scheme is a **Privileged Virtual File System** that resolves SPA routing conflicts by implementing a **Heuristic Resource Loader**. It operates as follows:
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Request: app://-/settings/profile
|
graph TD
|
||||||
│
|
%% ─── Styling Definitions ───
|
||||||
├─ Decode URI → "settings/profile"
|
classDef request fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||||
│
|
classDef process fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||||
├─ Normalize + validate path (security boundary check)
|
classDef decision fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
│
|
classDef success fill:#3b82f6,stroke:#1d4ed8,stroke-width:2px,color:#ffffff
|
||||||
├─ Does web-dist/settings/profile exist as a file?
|
|
||||||
│ ├─ YES → Serve with correct MIME type + CSP headers
|
%% ─── Nodes ───
|
||||||
│ └─ NO → Heuristic Fallback: serve web-dist/index.html
|
REQ([Request: app://-/settings/profile])
|
||||||
│
|
DECODE[Decode URI and Normalize Path]
|
||||||
└─ React Router resolves /settings/profile client-side
|
CHECK{File exists in web-dist?}
|
||||||
|
SERVE_FILE[Serve Asset with MIME + CSP]
|
||||||
|
SERVE_FALLBACK[Heuristic Fallback: Serve index.html]
|
||||||
|
REACT([React Router Handles Route])
|
||||||
|
|
||||||
|
%% ─── Flow ───
|
||||||
|
REQ ---> DECODE
|
||||||
|
DECODE ---> CHECK
|
||||||
|
CHECK ===>|YES| SERVE_FILE
|
||||||
|
CHECK -.->|NO| SERVE_FALLBACK
|
||||||
|
SERVE_FALLBACK ---> REACT
|
||||||
|
|
||||||
|
%% ─── Apply Styles ───
|
||||||
|
class REQ request;
|
||||||
|
class DECODE process;
|
||||||
|
class CHECK decision;
|
||||||
|
class SERVE_FILE,SERVE_FALLBACK,REACT success;
|
||||||
```
|
```
|
||||||
|
|
||||||
If a requested URI does not map to a physical asset, the handler intelligently intercepts the request to serve the `index.html` entry point, allowing React Router to maintain stateful client-side navigation. This ensures that deep links, page refreshes, and direct URL entry all function without modification to the React app's routing configuration.
|
If a requested URI does not map to a physical asset, the handler intelligently intercepts the request to serve the `index.html` entry point, allowing React Router to maintain stateful client-side navigation. This ensures that deep links, page refreshes, and direct URL entry all function without modification to the React app's routing configuration.
|
||||||
@@ -142,11 +171,11 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
{
|
{
|
||||||
scheme: 'app',
|
scheme: 'app',
|
||||||
privileges: {
|
privileges: {
|
||||||
standard: true, // Enables URL parsing (host, path, query)
|
standard: true, // Enables URL parsing (host, path, query)
|
||||||
secure: true, // Treated as a secure origin (HTTPS equivalent)
|
secure: true, // Treated as a secure origin (HTTPS equivalent)
|
||||||
supportFetchAPI: true, // Allows fetch() from this scheme
|
supportFetchAPI: true, // Allows fetch() from this scheme
|
||||||
corsEnabled: true, // Enables CORS for cross-origin requests
|
corsEnabled: true, // Enables CORS for cross-origin requests
|
||||||
stream: true, // Supports streaming responses
|
stream: true, // Supports streaming responses
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -154,25 +183,24 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Defense-in-Depth: Multi-Layered Protection
|
## 🛡️ Defense-in-Depth: Multi-Layered Protection
|
||||||
|
|
||||||
The custom protocol handler enforces a **multi-layered defense perimeter** that goes beyond standard Electron security defaults.
|
The custom protocol handler enforces a **multi-layered defense perimeter** that goes beyond standard Electron security defaults.
|
||||||
|
|
||||||
| Layer | Technique | Implementation | Threat Mitigated |
|
| Layer | Technique | Implementation | Threat Mitigated |
|
||||||
|---|---|---|---|
|
| ------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
||||||
| **I/O Sanitization** | Path traversal guard | `normalize()` + `startsWith()` validation against `web-dist/` boundary | Directory traversal attacks (`../../etc/passwd`) → `403 Forbidden` |
|
| **I/O Sanitization** | Path traversal guard | `normalize()` + `startsWith()` validation against `web-dist/` boundary | Directory traversal attacks (`../../etc/passwd`) → `403 Forbidden` |
|
||||||
| **In-Flight Policy Injection** | CSP response headers | `Content-Security-Policy` injected as HTTP response headers on every HTML payload | XSS execution via script injection |
|
| **In-Flight Policy Injection** | CSP response headers | `Content-Security-Policy` injected as HTTP response headers on every HTML payload | XSS execution via script injection |
|
||||||
| **Cryptographic Isolation** | Privileged scheme registration | `app` scheme registered with `standard`, `secure`, `supportFetchAPI`, `corsEnabled` | Scheme downgrade attacks; the renderer treats `app://` identically to `https://` |
|
| **Cryptographic Isolation** | Privileged scheme registration | `app` scheme registered with `standard`, `secure`, `supportFetchAPI`, `corsEnabled` | Scheme downgrade attacks; the renderer treats `app://` identically to `https://` |
|
||||||
| **Resource Type Validation** | `statSync.isFile()` check | Only regular files are served; directories return the SPA fallback | Information disclosure via directory listing |
|
| **Resource Type Validation** | `statSync.isFile()` check | Only regular files are served; directories return the SPA fallback | Information disclosure via directory listing |
|
||||||
| **Origin Sanitization** | CORS bypass proxy | `webRequest.onBeforeSendHeaders` strips `app://` Origin headers on outgoing requests | Backend CORS rejection of non-standard origins |
|
| **Origin Sanitization** | CORS bypass proxy | `webRequest.onBeforeSendHeaders` strips `app://` Origin headers on outgoing requests | Backend CORS rejection of non-standard origins |
|
||||||
| **Navigation Confinement** | `will-navigate` guard | Blocks navigation to URLs outside `app://` and the authorized dev server | Phishing via in-app redirect to malicious sites |
|
| **Navigation Confinement** | `will-navigate` guard | Blocks navigation to URLs outside `app://` and the authorized dev server | Phishing via in-app redirect to malicious sites |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Break Glass Procedure: Disaster Recovery Protocol
|
## ⚠️ Break Glass Procedure: Disaster Recovery Protocol
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION] > **This is a formal Disaster Recovery Protocol.** Execute only if the custom `app://` protocol causes an irrecoverable failure — for example, a critical third-party library that refuses to operate under a non-standard URI scheme. This procedure requires coordinated changes across both the React application and the Electron main process. Estimated recovery time: **15 minutes**.
|
||||||
> **This is a formal Disaster Recovery Protocol.** Execute only if the custom `app://` protocol causes an irrecoverable failure — for example, a critical third-party library that refuses to operate under a non-standard URI scheme. This procedure requires coordinated changes across both the React application and the Electron main process. Estimated recovery time: **15 minutes**.
|
|
||||||
|
|
||||||
### Step 1: Switch the Router — React Application
|
### Step 1: Switch the Router — React Application
|
||||||
|
|
||||||
@@ -206,6 +234,7 @@ All routes transition to hash-based addressing: `#/app/dashboard`, `#/auth/login
|
|||||||
In `apps/desktop/src/main/index.ts`, execute the following surgical removals:
|
In `apps/desktop/src/main/index.ts`, execute the following surgical removals:
|
||||||
|
|
||||||
**a)** Remove the scheme registration block at the top of the file:
|
**a)** Remove the scheme registration block at the top of the file:
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
- protocol.registerSchemesAsPrivileged([ ... ]);
|
- protocol.registerSchemesAsPrivileged([ ... ]);
|
||||||
```
|
```
|
||||||
@@ -215,6 +244,7 @@ In `apps/desktop/src/main/index.ts`, execute the following surgical removals:
|
|||||||
**c)** Remove the `registerAppProtocol()` invocation inside `app.whenReady()`.
|
**c)** Remove the `registerAppProtocol()` invocation inside `app.whenReady()`.
|
||||||
|
|
||||||
**d)** Redirect production content loading in `createWindow()`:
|
**d)** Redirect production content loading in `createWindow()`:
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
if (IS_DEV) {
|
if (IS_DEV) {
|
||||||
mainWindow.loadURL(DEV_SERVER_URL);
|
mainWindow.loadURL(DEV_SERVER_URL);
|
||||||
@@ -227,23 +257,26 @@ In `apps/desktop/src/main/index.ts`, execute the following surgical removals:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**e)** Inject a CSP `<meta>` tag into the web app's `index.html`, since the In-Flight Policy Injection layer is no longer available:
|
**e)** Inject a CSP `<meta>` tag into the web app's `index.html`, since the In-Flight Policy Injection layer is no longer available:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' file:; script-src 'self' file:;
|
content="default-src 'self' file:; script-src 'self' file:;
|
||||||
style-src 'self' 'unsafe-inline' file:;
|
style-src 'self' 'unsafe-inline' file:;
|
||||||
connect-src 'self' https:;
|
connect-src 'self' https:;
|
||||||
img-src 'self' file: data: https:;
|
img-src 'self' file: data: https:;
|
||||||
font-src 'self' file: data:;" />
|
font-src 'self' file: data:;"
|
||||||
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Trade-off Analysis
|
### Trade-off Analysis
|
||||||
|
|
||||||
| Dimension | Custom `app://` Protocol | `file://` + HashRouter |
|
| Dimension | Custom `app://` Protocol | `file://` + HashRouter |
|
||||||
|---|---|---|
|
| --------------------------------- | -------------------------------------------------- | ----------------------------------------------------------- |
|
||||||
| **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` |
|
| **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` |
|
||||||
| **Router Compatibility** | `BrowserRouter` — zero changes required | Must migrate to `HashRouter` |
|
| **Router Compatibility** | `BrowserRouter` — zero changes required | Must migrate to `HashRouter` |
|
||||||
| **Deep Linking** | Full, native-style support | Hash-based only |
|
| **Deep Linking** | Full, native-style support | Hash-based only |
|
||||||
| **Protocol-Native Compatibility** | Rare edge cases with non-standard scheme detection | Maximum third-party compatibility |
|
| **Protocol-Native Compatibility** | Rare edge cases with non-standard scheme detection | Maximum third-party compatibility |
|
||||||
| **Security Delivery Vector** | CSP via response headers (strongest enforcement) | CSP via `<meta>` tag (bypassable by early script execution) |
|
| **Security Delivery Vector** | CSP via response headers (strongest enforcement) | CSP via `<meta>` tag (bypassable by early script execution) |
|
||||||
| **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) |
|
| **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) |
|
||||||
| **Recovery Time** | — | ~15 minutes, 2 files |
|
| **Recovery Time** | — | ~15 minutes, 2 files |
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
[← Back to Root](../../../README.md)
|
||||||
|
|
||||||
# IPC Architecture & Security Model
|
# IPC Architecture & Security Model
|
||||||
|
|
||||||
The Secure Communication Blueprint.
|
The Secure Communication Blueprint.
|
||||||
@@ -23,44 +25,56 @@ The desktop wrapper enforces a **strict privilege separation** between three exe
|
|||||||
|
|
||||||
### Trust Level Matrix
|
### Trust Level Matrix
|
||||||
|
|
||||||
| Context | Trust Level | Privilege Scope | Security Guarantee |
|
| Context | Trust Level | Privilege Scope | Security Guarantee |
|
||||||
|---|---|---|---|
|
| ------------------ | ------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
| **Main Process** | Fully Trusted | Unrestricted Node.js access: filesystem, network, printers, OS APIs, child processes | Only code authored by the engineering team executes here |
|
| **Main Process** | Fully Trusted | Unrestricted Node.js access: filesystem, network, printers, OS APIs, child processes | Only code authored by the engineering team executes here |
|
||||||
| **Preload Script** | Controlled | Restricted to `ipcRenderer.invoke()` and `ipcRenderer.send()` — no direct Node.js access | Executes in a **Hermetically Sealed Context** — isolated from both the Main Process globals and the Renderer's DOM |
|
| **Preload Script** | Controlled | Restricted to `ipcRenderer.invoke()` and `ipcRenderer.send()` — no direct Node.js access | Executes in a **Hermetically Sealed Context** — isolated from both the Main Process globals and the Renderer's DOM |
|
||||||
| **Renderer** | Untrusted | Standard browser sandbox — zero Node.js API surface | Designated as a **Zero-Trust Environment** — may execute third-party code, npm packages, or XSS payloads |
|
| **Renderer** | Untrusted | Standard browser sandbox — zero Node.js API surface | Designated as a **Zero-Trust Environment** — may execute third-party code, npm packages, or XSS payloads |
|
||||||
|
|
||||||
### Process Topology
|
### Process Topology
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
graph TD
|
||||||
│ MAIN PROCESS [Fully Trusted] │
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
│ │
|
classDef trustedLayer fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#0f172a
|
||||||
│ UNRESTRICTED PRIVILEGES │
|
classDef gatewayLayer fill:#f0fdf4,stroke:#10b981,stroke-width:2px,color:#064e3b
|
||||||
│ Filesystem · Network · Printers · Native APIs · Child Processes │
|
classDef untrustedLayer fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
|
||||||
│ Auto-Updater · OS Integration · System Notifications │
|
classDef functionNode fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||||
│ │
|
|
||||||
│ ipcMain.handle('channel', handler) ← Command handlers │
|
%% ─── Subgraphs ───
|
||||||
│ ipcMain.on('channel', handler) ← Event listeners │
|
subgraph Main ["MAIN PROCESS [Fully Trusted]"]
|
||||||
│ webContents.send('channel', data) ← Downstream push │
|
M_DESC["Unrestricted Node.js Privileges"]
|
||||||
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
|
IPC_MAIN_H[ipcMain.handle]
|
||||||
│ PRELOAD SCRIPT [Secure Gateway] │
|
IPC_MAIN_O[ipcMain.on]
|
||||||
│ │
|
end
|
||||||
│ HERMETICALLY SEALED CONTEXT │
|
|
||||||
│ Performs Interface Narrowing: transforms broad IPC capabilities │
|
subgraph Preload ["PRELOAD SCRIPT [Secure Gateway]"]
|
||||||
│ into a minimal, auditable API surface. Acts as the sole │
|
P_DESC["Hermetically Sealed Context (Interface Narrowing)"]
|
||||||
│ authorized mediator between trusted and untrusted contexts. │
|
CTX_BRIDGE{contextBridge.exposeInMainWorld}
|
||||||
│ │
|
end
|
||||||
│ contextBridge.exposeInMainWorld('electronAPI', { ... }) │
|
|
||||||
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
|
subgraph Renderer ["RENDERER [Zero-Trust Environment]"]
|
||||||
│ RENDERER [Zero-Trust Environment] │
|
R_DESC["Standard Browser Sandbox (No Node.js APIs)"]
|
||||||
│ │
|
E_API([window.electronAPI])
|
||||||
│ UNTRUSTED WEB CONTENT │
|
end
|
||||||
│ Standard browser sandbox. Zero access to: require, __dirname, │
|
|
||||||
│ process, fs, child_process, net, os, ipcRenderer. │
|
%% ─── Flow & Relationships ───
|
||||||
│ │
|
E_API ===>|Only Authorized Vector| CTX_BRIDGE
|
||||||
│ ONLY authorized interaction vector: │
|
CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_H
|
||||||
│ window.electronAPI.methodName(args) │
|
CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_O
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
|
||||||
|
%% ─── Apply Styles ───
|
||||||
|
class Main trustedLayer;
|
||||||
|
class Preload gatewayLayer;
|
||||||
|
class Renderer untrustedLayer;
|
||||||
|
class M_DESC,P_DESC,R_DESC,IPC_MAIN_H,IPC_MAIN_O functionNode;
|
||||||
|
class CTX_BRIDGE gatewayLayer;
|
||||||
|
class E_API untrustedLayer;
|
||||||
|
|
||||||
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||||
|
style Main fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style Preload fill:transparent,stroke:#10b981,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style Renderer fill:transparent,stroke:#ef4444,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
```
|
```
|
||||||
|
|
||||||
The Preload Script functions as a **Secure Gateway** that performs **Interface Narrowing** — it transforms the broad, unrestricted IPC capabilities of the Main Process into a deliberately narrow, type-safe API surface. The renderer communicates with native functionality **exclusively** through this gateway. There are no alternative paths, no escape hatches, and no backdoors.
|
The Preload Script functions as a **Secure Gateway** that performs **Interface Narrowing** — it transforms the broad, unrestricted IPC capabilities of the Main Process into a deliberately narrow, type-safe API surface. The renderer communicates with native functionality **exclusively** through this gateway. There are no alternative paths, no escape hatches, and no backdoors.
|
||||||
@@ -69,12 +83,12 @@ The Preload Script functions as a **Secure Gateway** that performs **Interface N
|
|||||||
|
|
||||||
These settings are declared in `BrowserWindow.webPreferences` and are **non-negotiable**:
|
These settings are declared in `BrowserWindow.webPreferences` and are **non-negotiable**:
|
||||||
|
|
||||||
| Setting | Value | Enforcement |
|
| Setting | Value | Enforcement |
|
||||||
|---|---|---|
|
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `contextIsolation` | `true` | The Preload executes in a hermetically sealed V8 context. The renderer **cannot** access `require()`, Node.js globals, or any variable from the preload's scope. |
|
| `contextIsolation` | `true` | The Preload executes in a hermetically sealed V8 context. The renderer **cannot** access `require()`, Node.js globals, or any variable from the preload's scope. |
|
||||||
| `nodeIntegration` | `false` | **Zero** Node.js API surface in the renderer. `fs`, `child_process`, `os`, `net`, and all built-in modules are completely unavailable. |
|
| `nodeIntegration` | `false` | **Zero** Node.js API surface in the renderer. `fs`, `child_process`, `os`, `net`, and all built-in modules are completely unavailable. |
|
||||||
| `sandbox` | `true` | The renderer process runs inside a **Chromium OS-level sandbox**, restricting system calls and file access at the kernel level. |
|
| `sandbox` | `true` | The renderer process runs inside a **Chromium OS-level sandbox**, restricting system calls and file access at the kernel level. |
|
||||||
| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. |
|
| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,8 +96,7 @@ These settings are declared in `BrowserWindow.webPreferences` and are **non-nego
|
|||||||
|
|
||||||
Every native feature in this architecture **must** follow the Three-Step Bridge — a Standard Operating Procedure (SOP) that ensures traceability, type-safety, and auditability across the entire IPC surface.
|
Every native feature in this architecture **must** follow the Three-Step Bridge — a Standard Operating Procedure (SOP) that ensures traceability, type-safety, and auditability across the entire IPC surface.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT] > **Deterministic Synchronization**: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is **mandatory**. A mismatch between any two of the three layers will result in either a **Type-Safety Gap** (silent failures in development) or a **Runtime Regression** (crashes in production).
|
||||||
> **Deterministic Synchronization**: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is **mandatory**. A mismatch between any two of the three layers will result in either a **Type-Safety Gap** (silent failures in development) or a **Runtime Regression** (crashes in production).
|
|
||||||
|
|
||||||
### Step 1: Register the Handler — Main Process
|
### Step 1: Register the Handler — Main Process
|
||||||
|
|
||||||
@@ -134,6 +147,7 @@ contextBridge.exposeInMainWorld('electronAPI', electronAPI);
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Non-negotiable rules:**
|
**Non-negotiable rules:**
|
||||||
|
|
||||||
- **Never** expose `ipcRenderer` directly — this is a **Catastrophic Failure** pattern.
|
- **Never** expose `ipcRenderer` directly — this is a **Catastrophic Failure** pattern.
|
||||||
- **Never** expose `ipcRenderer.on` without cleanup — use `createEventSubscription()`, which returns an unsubscribe function for React `useEffect` lifecycle management.
|
- **Never** expose `ipcRenderer.on` without cleanup — use `createEventSubscription()`, which returns an unsubscribe function for React `useEffect` lifecycle management.
|
||||||
- **Always** declare explicit TypeScript types for all function signatures.
|
- **Always** declare explicit TypeScript types for all function signatures.
|
||||||
@@ -160,17 +174,18 @@ The following is the **complete, authoritative registry** of all authorized IPC
|
|||||||
|
|
||||||
### Printer Subsystem
|
### Printer Subsystem
|
||||||
|
|
||||||
| Channel | Direction | Pattern | Payload | Access Control |
|
| Channel | Direction | Pattern | Payload | Access Control |
|
||||||
|---|---|---|---|---|
|
| ------------------ | -------------------------- | ------------------- | --------------------------------------------------------------------- | ------------------------------ |
|
||||||
| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` | Read-only hardware enumeration |
|
| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` | Read-only hardware enumeration |
|
||||||
| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Accepts `ElectronPrintOptions`, returns `{ success, failureReason? }` | Controlled hardware invocation |
|
| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Accepts `ElectronPrintOptions`, returns `{ success, failureReason? }` | Controlled hardware invocation |
|
||||||
|
|
||||||
**Main process handler:** `setupPrinterIPC()` in `src/main/index.ts`
|
**Main process handler:** `setupPrinterIPC()` in `src/main/index.ts`
|
||||||
|
|
||||||
**Preload Gateway surface:**
|
**Preload Gateway surface:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
getPrinters: () => ipcRenderer.invoke('printer:get-list')
|
getPrinters: () => ipcRenderer.invoke('printer:get-list');
|
||||||
print: (options?) => ipcRenderer.invoke('printer:print', options)
|
print: (options?) => ipcRenderer.invoke('printer:print', options);
|
||||||
```
|
```
|
||||||
|
|
||||||
**React consumption hook:** `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
|
**React consumption hook:** `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
|
||||||
@@ -179,16 +194,16 @@ print: (options?) => ipcRenderer.invoke('printer:print', options)
|
|||||||
|
|
||||||
### Auto-Updater Subsystem
|
### Auto-Updater Subsystem
|
||||||
|
|
||||||
| Channel | Direction | Pattern | Payload | Access Control |
|
| Channel | Direction | Pattern | Payload | Access Control |
|
||||||
|---|---|---|---|---|
|
| ----------------------- | --------------- | ------------------- | -------------------------------------------------------------- | ---------------------------------- |
|
||||||
| `updater:check` | Renderer → Main | `invoke` / `handle` | Returns update check result | Read-only version query |
|
| `updater:check` | Renderer → Main | `invoke` / `handle` | Returns update check result | Read-only version query |
|
||||||
| `updater:install` | Renderer → Main | `send` / `on` | No payload | Privileged: quits app and installs |
|
| `updater:install` | Renderer → Main | `send` / `on` | No payload | Privileged: quits app and installs |
|
||||||
| `updater:checking` | Main → Renderer | `send` | No payload | Status notification |
|
| `updater:checking` | Main → Renderer | `send` | No payload | Status notification |
|
||||||
| `updater:available` | Main → Renderer | `send` | `UpdateInfo { version, releaseDate, releaseNotes }` | Status notification |
|
| `updater:available` | Main → Renderer | `send` | `UpdateInfo { version, releaseDate, releaseNotes }` | Status notification |
|
||||||
| `updater:not-available` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
| `updater:not-available` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
||||||
| `updater:progress` | Main → Renderer | `send` | `ProgressInfo { percent, bytesPerSecond, transferred, total }` | Progress telemetry |
|
| `updater:progress` | Main → Renderer | `send` | `ProgressInfo { percent, bytesPerSecond, transferred, total }` | Progress telemetry |
|
||||||
| `updater:downloaded` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
| `updater:downloaded` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
||||||
| `updater:error` | Main → Renderer | `send` | Error message string | Error telemetry |
|
| `updater:error` | Main → Renderer | `send` | Error message string | Error telemetry |
|
||||||
|
|
||||||
**Main process handlers:** `setupAutoUpdaterIPC()` + `setupAutoUpdaterEvents()` in `src/main/index.ts`
|
**Main process handlers:** `setupAutoUpdaterIPC()` + `setupAutoUpdaterEvents()` in `src/main/index.ts`
|
||||||
|
|
||||||
@@ -283,7 +298,7 @@ contextBridge.exposeInMainWorld('ipc', ipcRenderer);
|
|||||||
contextBridge.exposeInMainWorld('require', require);
|
contextBridge.exposeInMainWorld('require', require);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Threat:** The renderer can `require('child_process').exec('rm -rf /')`. A single XSS vulnerability in *any* dependency — including transitive ones — escalates to **full filesystem access, credential theft, reverse shells, and data exfiltration**.
|
**Threat:** The renderer can `require('child_process').exec('rm -rf /')`. A single XSS vulnerability in _any_ dependency — including transitive ones — escalates to **full filesystem access, credential theft, reverse shells, and data exfiltration**.
|
||||||
|
|
||||||
**Classification:** **Total System Compromise**
|
**Classification:** **Total System Compromise**
|
||||||
|
|
||||||
@@ -294,7 +309,7 @@ contextBridge.exposeInMainWorld('require', require);
|
|||||||
```typescript
|
```typescript
|
||||||
// VIOLATION: Catastrophic Failure — Complete Boundary Collapse
|
// VIOLATION: Catastrophic Failure — Complete Boundary Collapse
|
||||||
new BrowserWindow({
|
new BrowserWindow({
|
||||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
webPreferences: { nodeIntegration: true, contextIsolation: false },
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_CMS_API_URL=http://localhost:8001/cms
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Type-safe Environment Wrapper for apps/landing.
|
||||||
|
*/
|
||||||
|
export const ENV = {
|
||||||
|
CMS_API_URL: import.meta.env.VITE_CMS_API_URL || 'http://localhost:8001/cms',
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
VITE_API_BASE_URL=http://localhost:8000/api
|
||||||
|
VITE_APP_ENV=development
|
||||||
|
VITE_COUCHDB_BASE_URL=http://202.146.229.134:7700
|
||||||
|
VITE_COUCHDB_USERNAME=root
|
||||||
|
VITE_COUCHDB_PASSWORD=password
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
"@repo/utils": "workspace:*",
|
"@repo/utils": "workspace:*",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
|
"events": "^3.3.0",
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
|
"lucide-react": "^1.17.0",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Table,
|
||||||
|
Badge,
|
||||||
|
} from '@repo/ui/components';
|
||||||
|
import { itemDB, posConfigDB } from '../../core/db';
|
||||||
|
import type { Item, POSConfiguration } from '../../core/db/types';
|
||||||
|
|
||||||
|
export default function PouchSample() {
|
||||||
|
const [configs, setConfigs] = useState<POSConfiguration[]>([]);
|
||||||
|
const [items, setItems] = useState<Item[]>([]);
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const allConfigs = await posConfigDB.find({ selector: {} });
|
||||||
|
setConfigs(allConfigs);
|
||||||
|
|
||||||
|
const allItems = await itemDB.find({ selector: {} });
|
||||||
|
setItems(allItems);
|
||||||
|
console.log({allConfigs, allItems})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load PouchDB data', err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 1. Initial Data Load
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// 2. Setup Real-Time Listeners
|
||||||
|
const unsubscribeItems = itemDB.onChange(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const unsubscribePos = posConfigDB.onChange(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. CRITICAL: Cleanup to prevent memory leaks on unmount
|
||||||
|
return () => {
|
||||||
|
unsubscribeItems();
|
||||||
|
unsubscribePos();
|
||||||
|
};
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
// ─── POS Configuration Handlers ─────────────────────────────────
|
||||||
|
|
||||||
|
const handleSeedConfig = async () => {
|
||||||
|
try {
|
||||||
|
const id = `pos-${Date.now()}`;
|
||||||
|
await posConfigDB.create({
|
||||||
|
_id: id,
|
||||||
|
pos_number: '1111111111666',
|
||||||
|
pos_name: 'Premium Test POS TESTING COUNCH',
|
||||||
|
items: items, // mapping current items
|
||||||
|
payment_methods: [{ id: 'cash', name: 'Cash' }],
|
||||||
|
});
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to seed config', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfig = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await posConfigDB.delete(id);
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete config', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Items Inventory Handlers ───────────────────────────────────
|
||||||
|
|
||||||
|
const handleAddItem = async () => {
|
||||||
|
try {
|
||||||
|
const id = `item-${Date.now()}`;
|
||||||
|
await itemDB.create({
|
||||||
|
_id: id,
|
||||||
|
name: 'PLAYGROUND ALL DAY TESTING POUCH',
|
||||||
|
base_price: '75000',
|
||||||
|
item_type: 'wahana',
|
||||||
|
usage_type: 'ticket',
|
||||||
|
item_category: [{ name: 'Entertainment' }],
|
||||||
|
item_rates: [
|
||||||
|
{ season_period: 'weekday', price: 50000 },
|
||||||
|
{ season_period: 'weekend', price: 75000 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to add item', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteItem = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await itemDB.delete(id);
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete item', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAll = async () => {
|
||||||
|
try {
|
||||||
|
await posConfigDB.cleanAllData();
|
||||||
|
await itemDB.cleanAllData();
|
||||||
|
loadData();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to clear data', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={2}>Enterprise PouchDB Sync</Title>
|
||||||
|
<Button color="error" variant="outline" onClick={handleClearAll}>
|
||||||
|
Clear All Local Data
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Items Inventory Table */}
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={4}>Items Database</Title>
|
||||||
|
<Button onClick={handleAddItem} color="success">
|
||||||
|
Inject Mock ERP Item
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||||
|
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Name</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Type</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Base Price</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Rates Count</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{items.length > 0 ? (
|
||||||
|
items.map((item) => (
|
||||||
|
<Table.Tr key={item._id}>
|
||||||
|
<Table.Td>{item._id}</Table.Td>
|
||||||
|
<Table.Td>{item.name}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge color="brand" variant="light">
|
||||||
|
{item.item_type}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>${Number(item.base_price).toFixed(2)}</Table.Td>
|
||||||
|
<Table.Td>{item.item_rates?.length || 0}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteItem(item._id)}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={6} align="center">
|
||||||
|
<Text c="dimmed">No items found.</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* POS Configuration Table */}
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<Group justify="space-between" mb="md">
|
||||||
|
<Title order={4}>POS Configurations</Title>
|
||||||
|
<Button onClick={handleSeedConfig} variant="light" color="brand">
|
||||||
|
Inject Mock POS Config
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||||
|
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Name</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Number</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Mapped Items</Table.Th>
|
||||||
|
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{configs.length > 0 ? (
|
||||||
|
configs.map((cfg) => (
|
||||||
|
<Table.Tr key={cfg._id}>
|
||||||
|
<Table.Td>{cfg._id}</Table.Td>
|
||||||
|
<Table.Td>{cfg.pos_name}</Table.Td>
|
||||||
|
<Table.Td>{cfg.pos_number}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge color="info" variant="outline">
|
||||||
|
{cfg.items?.length || 0} Items
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteConfig(cfg._id)}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={5} align="center">
|
||||||
|
<Text c="dimmed">No configurations found.</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -18,10 +19,15 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Badge,
|
Badge,
|
||||||
Divider,
|
Divider,
|
||||||
|
Tabs,
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
} from '@repo/ui/components';
|
} from '@repo/ui/components';
|
||||||
|
import { ShieldCheck, Database, Lock, Layout, Activity, Printer } from 'lucide-react';
|
||||||
import PrinterList from './printer-list';
|
import PrinterList from './printer-list';
|
||||||
import ExamplePage from './example/example.page';
|
import ExamplePage from './example/example.page';
|
||||||
import EventsDemoPage from './events-demo';
|
import EventsDemoPage from './events-demo';
|
||||||
|
import PouchSample from './pouch-sample';
|
||||||
|
|
||||||
interface ShowcaseViewProps {
|
interface ShowcaseViewProps {
|
||||||
colorScheme: ColorSchemeType;
|
colorScheme: ColorSchemeType;
|
||||||
@@ -31,6 +37,8 @@ interface ShowcaseViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) {
|
export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>('ui-components');
|
||||||
|
|
||||||
// Mock data for the table
|
// Mock data for the table
|
||||||
const tableData = [
|
const tableData = [
|
||||||
{ id: 'ORD-001', customer: 'John Doe', status: 'Shipped', total: '$120.00' },
|
{ id: 'ORD-001', customer: 'John Doe', status: 'Shipped', total: '$120.00' },
|
||||||
@@ -38,194 +46,245 @@ export default function ShowcaseView({ colorScheme, setColorScheme, density, set
|
|||||||
{ id: 'ORD-003', customer: 'Acme Corp', status: 'Delivered', total: '$1,250.00' },
|
{ id: 'ORD-003', customer: 'Acme Corp', status: 'Delivered', total: '$1,250.00' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const getSubtitle = () => {
|
||||||
|
switch (activeTab) {
|
||||||
|
case 'rbac': return 'Role-Based Access Control and Permissions';
|
||||||
|
case 'storage': return 'Offline-First PouchDB Synchronization';
|
||||||
|
case 'auth': return 'Authentication & Security Layers';
|
||||||
|
case 'ui-components': return 'Theme, Typography, Forms & Data Grids';
|
||||||
|
case 'events': return 'Global Event Bus Synchronization';
|
||||||
|
case 'hardware': return 'Hardware Integration & Printers';
|
||||||
|
default: return 'Architecture Showcase';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="lg" py="xl">
|
<Box className="min-h-screen" style={{ backgroundColor: 'var(--mantine-color-body)' }}>
|
||||||
<Stack gap="xl">
|
<Tabs
|
||||||
<Title order={1}>Super App UI Showcase</Title>
|
orientation="vertical"
|
||||||
|
placement="left"
|
||||||
|
value={activeTab}
|
||||||
|
onChange={setActiveTab}
|
||||||
|
variant="pills"
|
||||||
|
radius="md"
|
||||||
|
className="h-screen"
|
||||||
|
styles={{
|
||||||
|
root: { display: 'flex', height: '100vh', overflow: 'hidden' },
|
||||||
|
list: {
|
||||||
|
minWidth: 260,
|
||||||
|
padding: '1rem',
|
||||||
|
borderRight: '1px solid var(--mantine-color-default-border)',
|
||||||
|
backgroundColor: 'var(--mantine-color-default-element-bg)'
|
||||||
|
},
|
||||||
|
panel: { flex: 1, display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' },
|
||||||
|
tab: { justifyContent: 'flex-start' }, // Ensure the entire tab content aligns left
|
||||||
|
tabLabel: { textAlign: 'left', flex: 1 }, // Ensure the text pushes to fill and aligns left
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs.List>
|
||||||
|
<Box mb="xl" px="sm">
|
||||||
|
<Title order={3} c="brand.7">Eigen ERP</Title>
|
||||||
|
<Text size="xs" c="dimmed">Architecture Showcase</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* =========================================
|
<Tabs.Tab value="ui-components" leftSection={<Layout size={18} />}>
|
||||||
CONTROL PANEL
|
UI Components
|
||||||
========================================= */}
|
</Tabs.Tab>
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
<Tabs.Tab value="storage" leftSection={<Database size={18} />}>
|
||||||
<Title order={4} mb="md">
|
Offline Storage
|
||||||
Theme Controls
|
</Tabs.Tab>
|
||||||
</Title>
|
<Tabs.Tab value="rbac" leftSection={<ShieldCheck size={18} />}>
|
||||||
<Group grow align="flex-end">
|
RBAC Engine
|
||||||
<Select
|
</Tabs.Tab>
|
||||||
label="Color Scheme"
|
<Tabs.Tab value="auth" leftSection={<Lock size={18} />}>
|
||||||
value={colorScheme}
|
Auth & Security
|
||||||
onChange={(val: string | null) => setColorScheme((val as ColorSchemeType) || 'light')}
|
</Tabs.Tab>
|
||||||
data={[
|
<Tabs.Tab value="events" leftSection={<Activity size={18} />}>
|
||||||
{ value: 'light', label: 'Light' },
|
Events
|
||||||
{ value: 'dark', label: 'Dark' },
|
</Tabs.Tab>
|
||||||
]}
|
<Tabs.Tab value="hardware" leftSection={<Printer size={18} />}>
|
||||||
/>
|
Hardware
|
||||||
<Select
|
</Tabs.Tab>
|
||||||
label="Density (Spacing & Sizing)"
|
</Tabs.List>
|
||||||
value={density}
|
|
||||||
onChange={(val: string | null) => setDensity((val as DensityType) || 'standard')}
|
|
||||||
data={[
|
|
||||||
{ value: 'compact', label: 'Compact (ERP Mode)' },
|
|
||||||
{ value: 'standard', label: 'Standard (UI Mode)' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* =========================================
|
<Tabs.Panel value={activeTab as string}>
|
||||||
TAILWIND V4 BRIDGE TEST
|
{/* Header */}
|
||||||
========================================= */}
|
<Paper
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
p="md"
|
||||||
<Title order={4} mb="md">
|
radius={0}
|
||||||
Tailwind v4 Synchronization
|
withBorder
|
||||||
</Title>
|
style={{
|
||||||
{/* This div purely uses Tailwind classes to prove it inherits Mantine's variables */}
|
borderTop: 0,
|
||||||
<div className="bg-brand-500 text-brand-50 p-md rounded-md shadow-md text-base">
|
borderLeft: 0,
|
||||||
<span className="font-bold">Tailwind works!</span> The padding (p-md), border-radius (rounded-md), text size
|
borderRight: 0,
|
||||||
(text-base), and background color of this box are entirely controlled by the ThemeProvider's current state.
|
zIndex: 10,
|
||||||
</div>
|
flexShrink: 0
|
||||||
</Card>
|
}}
|
||||||
|
>
|
||||||
{/* =========================================
|
<Group justify="space-between">
|
||||||
TYPOGRAPHY & BUTTONS
|
<Stack gap={0}>
|
||||||
========================================= */}
|
<Title order={3}>Architecture Showcase</Title>
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
<Text size="sm" c="dimmed">{getSubtitle()}</Text>
|
||||||
<Stack gap="lg">
|
</Stack>
|
||||||
<div>
|
|
||||||
<Title order={4} mb="xs">
|
|
||||||
Typography & Badges
|
|
||||||
</Title>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
This is dimmed small text indicating a subtitle.
|
|
||||||
</Text>
|
|
||||||
<Text mb="md">
|
|
||||||
This is standard text describing the components below. Watch how the font changes when you switch
|
|
||||||
density.
|
|
||||||
</Text>
|
|
||||||
<Group>
|
|
||||||
<Badge color="brand">Brand Badge</Badge>
|
|
||||||
<Badge color="success" variant="light">
|
|
||||||
Success Status
|
|
||||||
</Badge>
|
|
||||||
<Badge color="error" variant="outline">
|
|
||||||
Error State
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Title order={4} mb="md">
|
|
||||||
Buttons
|
|
||||||
</Title>
|
|
||||||
<Group>
|
|
||||||
<Button variant="filled" color="brand">
|
|
||||||
Filled Button
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" color="brand">
|
|
||||||
Outline Button
|
|
||||||
</Button>
|
|
||||||
<Button variant="light" color="info">
|
|
||||||
Light Info
|
|
||||||
</Button>
|
|
||||||
<Button variant="subtle" color="error">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* =========================================
|
|
||||||
COMPLEX FORMS (ERP STYLE)
|
|
||||||
========================================= */}
|
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
|
||||||
<Title order={4} mb="md">
|
|
||||||
Form Elements
|
|
||||||
</Title>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Group grow align="flex-start">
|
|
||||||
<TextInput label="First Name" placeholder="Enter your first name" withAsterisk />
|
|
||||||
<TextInput label="Last Name" placeholder="Enter your last name" />
|
|
||||||
</Group>
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
<Group grow align="flex-start">
|
{/* Scrollable Content Area */}
|
||||||
<NumberInput label="Age" placeholder="25" min={0} max={100} />
|
<Box className="flex-1 overflow-y-auto p-6" style={{ height: 'calc(100vh - 80px)' }}>
|
||||||
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
|
<Container size="xl" m={0} p={0}>
|
||||||
</Group>
|
|
||||||
|
{/* --- UI COMPONENTS TAB --- */}
|
||||||
|
{activeTab === 'ui-components' && (
|
||||||
|
<Stack gap="xl">
|
||||||
|
{/* Control Panel */}
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<Title order={4} mb="md">Theme Controls</Title>
|
||||||
|
<Group grow align="flex-end">
|
||||||
|
<Select
|
||||||
|
label="Color Scheme"
|
||||||
|
value={colorScheme}
|
||||||
|
onChange={(val) => setColorScheme((val as ColorSchemeType) || 'light')}
|
||||||
|
data={[{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }]}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Density (Spacing & Sizing)"
|
||||||
|
value={density}
|
||||||
|
onChange={(val) => setDensity((val as DensityType) || 'standard')}
|
||||||
|
data={[{ value: 'compact', label: 'Compact (ERP Mode)' }, { value: 'standard', label: 'Standard (UI Mode)' }]}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Textarea label="Bio" placeholder="Tell us about yourself" minRows={3} />
|
{/* Typography & Buttons */}
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<Stack gap="lg">
|
||||||
|
<div>
|
||||||
|
<Title order={4} mb="xs">Typography & Badges</Title>
|
||||||
|
<Text size="sm" c="dimmed">This is dimmed small text indicating a subtitle.</Text>
|
||||||
|
<Group mt="md">
|
||||||
|
<Badge color="brand">Brand Badge</Badge>
|
||||||
|
<Badge color="success" variant="light">Success Status</Badge>
|
||||||
|
<Badge color="error" variant="outline">Error State</Badge>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div>
|
||||||
|
<Title order={4} mb="md">Buttons</Title>
|
||||||
|
<Group>
|
||||||
|
<Button variant="filled" color="brand">Filled Button</Button>
|
||||||
|
<Button variant="outline" color="brand">Outline Button</Button>
|
||||||
|
<Button variant="light" color="info">Light Info</Button>
|
||||||
|
<Button variant="subtle" color="error">Cancel</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Group mt="sm">
|
{/* Forms */}
|
||||||
<Checkbox label="I agree to the terms and conditions" defaultChecked />
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
<Switch label="Enable notifications" defaultChecked />
|
<Title order={4} mb="md">Form Elements</Title>
|
||||||
</Group>
|
<Stack gap="md">
|
||||||
|
<Group grow align="flex-start">
|
||||||
|
<TextInput label="First Name" placeholder="Enter your first name" withAsterisk />
|
||||||
|
<TextInput label="Last Name" placeholder="Enter your last name" />
|
||||||
|
</Group>
|
||||||
|
<Group grow align="flex-start">
|
||||||
|
<NumberInput label="Age" placeholder="25" min={0} max={100} />
|
||||||
|
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
|
||||||
|
</Group>
|
||||||
|
<Textarea label="Bio" placeholder="Tell us about yourself" minRows={3} />
|
||||||
|
<Group mt="sm">
|
||||||
|
<Checkbox label="I agree to the terms and conditions" defaultChecked />
|
||||||
|
<Switch label="Enable notifications" defaultChecked />
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Radio.Group name="favoriteFramework" label="Select your favorite framework" withAsterisk>
|
{/* Data Grid */}
|
||||||
<Group mt="xs">
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
<Radio value="react" label="React" />
|
<Title order={4} mb="md">Data Grid</Title>
|
||||||
<Radio value="svelte" label="Svelte" />
|
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||||
<Radio value="vue" label="Vue" />
|
<Table.Thead>
|
||||||
</Group>
|
<Table.Tr>
|
||||||
</Radio.Group>
|
<Table.Th>Order ID</Table.Th>
|
||||||
</Stack>
|
<Table.Th>Customer</Table.Th>
|
||||||
</Card>
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>Total</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{tableData.map((row) => (
|
||||||
|
<Table.Tr key={row.id}>
|
||||||
|
<Table.Td>{row.id}</Table.Td>
|
||||||
|
<Table.Td>{row.customer}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge size="sm" color={row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'}>
|
||||||
|
{row.status}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{row.total}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* =========================================
|
{/* --- STORAGE TAB --- */}
|
||||||
DATA GRID / TABLE
|
{activeTab === 'storage' && (
|
||||||
========================================= */}
|
<Stack gap="xl">
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
<PouchSample />
|
||||||
<Title order={4} mb="md">
|
</Stack>
|
||||||
Data Grid
|
)}
|
||||||
</Title>
|
|
||||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>Order ID</Table.Th>
|
|
||||||
<Table.Th>Customer</Table.Th>
|
|
||||||
<Table.Th>Status</Table.Th>
|
|
||||||
<Table.Th>Total Amount</Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
<Table.Tbody>
|
|
||||||
{tableData.map((row) => (
|
|
||||||
<Table.Tr key={row.id}>
|
|
||||||
<Table.Td>{row.id}</Table.Td>
|
|
||||||
<Table.Td>{row.customer}</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<Badge
|
|
||||||
size="sm"
|
|
||||||
color={row.status === 'Delivered' ? 'success' : row.status === 'Shipped' ? 'info' : 'warning'}
|
|
||||||
>
|
|
||||||
{row.status}
|
|
||||||
</Badge>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>{row.total}</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
|
||||||
<PrinterList />
|
|
||||||
</Card>
|
|
||||||
<Card withBorder shadow="sm" radius="md" p="md">
|
|
||||||
<Title order={4} mb="md">
|
|
||||||
Nested Showcase Example
|
|
||||||
</Title>
|
|
||||||
<Text>
|
|
||||||
This is an example of a nested showcase component. You can create multiple layers of showcases to organize
|
|
||||||
features by domain or complexity.
|
|
||||||
</Text>
|
|
||||||
<ExamplePage />
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* =========================================
|
{/* --- RBAC TAB --- */}
|
||||||
EVENT BUS SHOWCASE
|
{activeTab === 'rbac' && (
|
||||||
========================================= */}
|
<Stack gap="xl">
|
||||||
<EventsDemoPage />
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
</Stack>
|
<Title order={4} mb="md">RBAC Engine</Title>
|
||||||
</Container>
|
<Text c="dimmed">RBAC Demo Component Coming Soon...</Text>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* --- AUTH TAB --- */}
|
||||||
|
{activeTab === 'auth' && (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<Title order={4} mb="md">Auth & Security</Title>
|
||||||
|
<Text c="dimmed">Auth Demo Component Coming Soon...</Text>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* --- EVENTS TAB --- */}
|
||||||
|
{activeTab === 'events' && (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<Title order={4} mb="md">Nested Showcase Example</Title>
|
||||||
|
<Text mb="md">This is an example of a nested showcase component.</Text>
|
||||||
|
<ExamplePage />
|
||||||
|
</Card>
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<EventsDemoPage />
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* --- HARDWARE TAB --- */}
|
||||||
|
{activeTab === 'hardware' && (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Card withBorder shadow="sm" radius="md" p="md">
|
||||||
|
<PrinterList />
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Multi-Database PouchDB Configuration for apps/web.
|
||||||
|
*
|
||||||
|
* This module demonstrates the IoC pattern: the consuming app decides
|
||||||
|
* which databases to create and where they sync to. The core engine
|
||||||
|
* (`PouchDatabaseManager`) has zero knowledge of business domains.
|
||||||
|
*/
|
||||||
|
import { PouchDatabaseManager } from '@repo/core-storage';
|
||||||
|
import { ENV } from '../../environment/env';
|
||||||
|
import type { Item, POSConfiguration } from './types';
|
||||||
|
|
||||||
|
// ─── Manager Singleton ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export const dbManager = new PouchDatabaseManager();
|
||||||
|
|
||||||
|
// ─── Helper: Build Secure Remote URL ────────────────────────────
|
||||||
|
|
||||||
|
function buildRemoteUrl(dbName: string): string | undefined {
|
||||||
|
const { COUCHDB_BASE_URL, COUCHDB_USERNAME, COUCHDB_PASSWORD } = ENV;
|
||||||
|
|
||||||
|
if (!COUCHDB_BASE_URL || !COUCHDB_USERNAME || !COUCHDB_PASSWORD) {
|
||||||
|
console.warn(`[DB Config] CouchDB credentials missing — "${dbName}" will run in offline-only mode.`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Tambahkan http:// secara otomatis jika DevOps hanya mengisi IP Address di .env
|
||||||
|
const safeBaseUrl = COUCHDB_BASE_URL.startsWith('http')
|
||||||
|
? COUCHDB_BASE_URL
|
||||||
|
: `http://${COUCHDB_BASE_URL}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Gunakan URL parser yang aman dari karakter aneh pada password
|
||||||
|
const url = new URL(safeBaseUrl);
|
||||||
|
url.username = encodeURIComponent(COUCHDB_USERNAME);
|
||||||
|
url.password = encodeURIComponent(COUCHDB_PASSWORD);
|
||||||
|
url.pathname = `/${dbName}`;
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[DB Config] Invalid URL format for CouchDB:`, safeBaseUrl);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Register Application Databases ─────────────────────────────
|
||||||
|
|
||||||
|
/** POS Configuration database — stores device settings, theme, etc. */
|
||||||
|
export const posConfigDB = dbManager.register<POSConfiguration>({
|
||||||
|
localName: 'pos_configuration',
|
||||||
|
remoteUrl: buildRemoteUrl('pos_configuration'),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Items database — products available for sale in POS. */
|
||||||
|
export const itemDB = dbManager.register<Item>({
|
||||||
|
localName: 'item',
|
||||||
|
remoteUrl: buildRemoteUrl('item'),
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Enterprise ERP Data Domain Models
|
||||||
|
* These types reflect the actual schema of the underlying CouchDB instances.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ItemRate {
|
||||||
|
season_period?: string | null;
|
||||||
|
price: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemCategory {
|
||||||
|
_id?: string;
|
||||||
|
name?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a sellable product or service.
|
||||||
|
*/
|
||||||
|
export interface Item {
|
||||||
|
_id: string;
|
||||||
|
_rev?: string;
|
||||||
|
name: string;
|
||||||
|
base_price: string | number;
|
||||||
|
item_type: string;
|
||||||
|
usage_type?: string;
|
||||||
|
item_category?: ItemCategory[] | ItemCategory | string;
|
||||||
|
item_rates?: ItemRate[];
|
||||||
|
|
||||||
|
// Allow for other ERP-specific fields
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the configuration and assigned data for a specific Point of Sale terminal.
|
||||||
|
*/
|
||||||
|
export interface POSConfiguration {
|
||||||
|
_id: string;
|
||||||
|
_rev?: string;
|
||||||
|
pos_number: string;
|
||||||
|
pos_name: string;
|
||||||
|
items: Item[];
|
||||||
|
payment_methods?: any[];
|
||||||
|
|
||||||
|
// Allow for other ERP-specific fields
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Type-safe Environment Wrapper for apps/web.
|
||||||
|
* DO NOT use `import.meta.env` directly in components. Import this `ENV` object instead.
|
||||||
|
*/
|
||||||
|
export const ENV = {
|
||||||
|
API_BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
|
||||||
|
APP_ENV: (import.meta.env.VITE_APP_ENV || 'development') as 'development' | 'staging' | 'production',
|
||||||
|
IS_PROD: import.meta.env.VITE_APP_ENV === 'production',
|
||||||
|
|
||||||
|
// CouchDB Connection
|
||||||
|
COUCHDB_BASE_URL: import.meta.env.VITE_COUCHDB_BASE_URL || 'http://localhost:5984',
|
||||||
|
COUCHDB_USERNAME: import.meta.env.VITE_COUCHDB_USERNAME || '',
|
||||||
|
COUCHDB_PASSWORD: import.meta.env.VITE_COUCHDB_PASSWORD || '',
|
||||||
|
} as const;
|
||||||
@@ -8,4 +8,14 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true, // Fail if 5173 is in use. Electron NEEDS this exact port.
|
strictPort: true, // Fail if 5173 is in use. Electron NEEDS this exact port.
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
// Crucial for PouchDB to not crash in the browser
|
||||||
|
global: 'window',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// Force Vite to use the installed npm package for 'events'
|
||||||
|
events: 'events',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+40
-28
@@ -1,7 +1,7 @@
|
|||||||
# Enterprise API Engine (`@repo/core-api`)
|
|
||||||
|
|
||||||
[← Back to Root](../../README.md)
|
[← Back to Root](../../README.md)
|
||||||
|
|
||||||
|
# Enterprise API Engine (`@repo/core-api`)
|
||||||
|
|
||||||
The platform-agnostic API engine for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine.
|
The platform-agnostic API engine for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine.
|
||||||
|
|
||||||
**This package enforces App Autonomy (IoC).** The core provides the engine and interceptor pipelines, but the consuming applications (`apps/web`, `apps/landing`) inject their own specific configurations, authentication tokens, and error handling behaviors.
|
**This package enforces App Autonomy (IoC).** The core provides the engine and interceptor pipelines, but the consuming applications (`apps/web`, `apps/landing`) inject their own specific configurations, authentication tokens, and error handling behaviors.
|
||||||
@@ -9,13 +9,20 @@ The platform-agnostic API engine for the monorepo. Provides an isolated HTTP cli
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
|
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
|
classDef coreEngine fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||||
|
classDef dataService fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
|
classDef observability fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||||
|
classDef errorNode fill:#f43f5e,stroke:#be123c,stroke-width:2px,color:#ffffff
|
||||||
|
|
||||||
|
%% ─── Subgraphs ───
|
||||||
subgraph Apps ["apps/* (App Autonomy)"]
|
subgraph Apps ["apps/* (App Autonomy)"]
|
||||||
WEB[apps/web]
|
WEB([apps/web])
|
||||||
LAND[apps/landing]
|
LAND([apps/landing])
|
||||||
DESK[apps/desktop]
|
DESK([apps/desktop])
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Core ["@repo/core-api (Engine)"]
|
subgraph Core ["@repo/core-api (Engine)"]
|
||||||
@@ -34,28 +41,31 @@ graph TD
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
WEB & LAND & DESK -->|instantiates| FACTORY
|
%% ─── Flow & Relationships ───
|
||||||
WEB & LAND & DESK -->|extends| COMMON
|
WEB & LAND & DESK ===>|instantiates| FACTORY
|
||||||
COMMON -->|executes via| FACTORY
|
WEB & LAND & DESK ===>|extends| COMMON
|
||||||
|
|
||||||
|
COMMON --->|executes via| FACTORY
|
||||||
FACTORY -.->|reports via| FARO
|
FACTORY -.->|reports via| FARO
|
||||||
FACTORY -.->|throws| API_ERR
|
FACTORY -.->|throws| API_ERR
|
||||||
|
|
||||||
%% Styling Subgraphs (Backgrounds)
|
%% ─── Apply Styles ───
|
||||||
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
|
class WEB,LAND,DESK appEntity;
|
||||||
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
|
class FACTORY coreEngine;
|
||||||
|
class BASE,COMMON dataService;
|
||||||
|
class FARO observability;
|
||||||
|
class API_ERR errorNode;
|
||||||
|
|
||||||
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||||
|
style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
|
||||||
%% Styling Nodes (Apps - Blue)
|
%% Nested subgraphs also need transparent backgrounds to prevent glaring white boxes in dark mode
|
||||||
style WEB fill:#339af0,stroke:#1864ab,color:#fff
|
style HTTP fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||||
style LAND fill:#339af0,stroke:#1864ab,color:#fff
|
style OBS fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||||
style DESK fill:#339af0,stroke:#1864ab,color:#fff
|
style DATA fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||||
|
style ERRORS fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||||
%% Styling Nodes (Core Modules)
|
```
|
||||||
style FACTORY fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
style FARO fill:#fd7e14,stroke:#d9480f,color:#fff
|
|
||||||
style BASE fill:#20c997,stroke:#089981,color:#fff
|
|
||||||
style COMMON fill:#20c997,stroke:#089981,color:#fff
|
|
||||||
style API_ERR fill:#fa5252,stroke:#c92a2a,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
### Data Flow Lifecycle
|
### Data Flow Lifecycle
|
||||||
|
|
||||||
@@ -65,24 +75,26 @@ Every HTTP request flows through this precise interceptor pipeline:
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
autonumber
|
autonumber
|
||||||
|
|
||||||
box #e7f5ff App Layer (Consumers)
|
%% ─── Dark-Mode Friendly RGBA Boxes ───
|
||||||
|
box rgba(59, 130, 246, 0.1) App Layer (Consumers)
|
||||||
participant C as UI Component
|
participant C as UI Component
|
||||||
end
|
end
|
||||||
|
|
||||||
box #f8f9fa Core Engine (@repo/core-api)
|
box rgba(148, 163, 184, 0.1) Core Engine (@repo/core-api)
|
||||||
participant S as Data Service
|
participant S as Data Service
|
||||||
participant H as HTTP Client
|
participant H as HTTP Client
|
||||||
participant F as Faro Adapter
|
participant F as Faro Adapter
|
||||||
end
|
end
|
||||||
|
|
||||||
box #e7f5ff App Logic (IoC)
|
box rgba(16, 185, 129, 0.1) App Logic (IoC)
|
||||||
participant A as App Hooks
|
participant A as App Hooks
|
||||||
end
|
end
|
||||||
|
|
||||||
box #fff5f5 External
|
box rgba(245, 158, 11, 0.1) External
|
||||||
participant N as Network
|
participant N as Network
|
||||||
end
|
end
|
||||||
|
|
||||||
|
%% ─── Execution Flow ───
|
||||||
C->>S: getMany()
|
C->>S: getMany()
|
||||||
S->>H: request()
|
S->>H: request()
|
||||||
H->>F: onRequestStart() (Log + Span)
|
H->>F: onRequestStart() (Log + Span)
|
||||||
|
|||||||
@@ -1,74 +1,116 @@
|
|||||||
# @repo/core-events
|
|
||||||
|
|
||||||
[← Back to Root](../../README.md)
|
[← Back to Root](../../README.md)
|
||||||
|
|
||||||
## Overview
|
# Event Bus (`@repo/core-events`)
|
||||||
|
|
||||||
`@repo/core-events` is the **decoupled Nervous System** of the ERP. It provides a highly performant, strictly typed Event Bus powered by `mitt` and React hooks.
|
The Global Pub/Sub & Hardware Integration Blueprint.
|
||||||
|
|
||||||
**This package is a pure tool.** It ships zero application-specific events. Each consuming app (`apps/web`, `apps/landing`, etc.) registers its own events autonomously using TypeScript Declaration Merging — the same Inversion of Control pattern used by `@repo/core-api`'s `createHttpClient` factory.
|
> This module provides a strictly-typed, global event bus for the monorepo ecosystem. It decouples cross-component communication and manages real-time hardware signals (such as printers and POS peripherals), ensuring a reactive and memory-safe architecture across all applications.
|
||||||
|
|
||||||
By routing communication through a centralized event bus, we achieve:
|
|
||||||
- **App Autonomy**: The core defines the bus. The app defines the contract. No circular knowledge.
|
|
||||||
- **Zero Coupling**: Publishers and subscribers don't need to import or know about each other.
|
|
||||||
- **Extreme Performance**: Components can subscribe to high-frequency data streams (like WebSockets) and update their own local state *without* triggering massive React tree re-renders.
|
|
||||||
- **Memory Safety**: The provided `useAppEvent` hook automatically handles subscription cleanup on component unmount, preventing the most common source of memory leaks in SPA architectures.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
## 🧠 System Overview
|
||||||
|
|
||||||
|
`@repo/core-events` is the **decoupled Nervous System** of the ERP. It provides a highly performant, strictly-typed Event Bus powered by `mitt` and custom React hooks.
|
||||||
|
|
||||||
|
**This package is a pure tool.** It ships zero application-specific events. Each consuming app (`apps/web`, `apps/desktop`, etc.) registers its own events autonomously using **TypeScript Declaration Merging** — the exact same Inversion of Control (IoC) pattern utilized by our `@repo/core-api` factory and `@repo/core-storage` engine.
|
||||||
|
|
||||||
|
### Architectural Topology
|
||||||
|
|
||||||
|
### 1. Conceptual Topology: The Pub/Sub Data Flow
|
||||||
|
This diagram illustrates the high-level concept of our decoupled architecture, demonstrating how application-specific types merge into the core bus.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
|
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
|
classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||||
|
classDef busEntity fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
|
|
||||||
|
%% ─── Nodes ───
|
||||||
|
TYPES[[App-Specific Event Types]]
|
||||||
|
PUB([Publisher Component])
|
||||||
|
BUS{Global Event Bus 'mitt'}
|
||||||
|
SUB([Subscriber Component])
|
||||||
|
|
||||||
|
%% ─── Flow ───
|
||||||
|
TYPES -.->|Declaration Merging| BUS
|
||||||
|
PUB ===>|emit 'event', payload| BUS
|
||||||
|
BUS ===>|useAppEvent 'event'| SUB
|
||||||
|
|
||||||
|
%% ─── Apply Styles ───
|
||||||
|
class TYPES,PUB,SUB appEntity;
|
||||||
|
class BUS busEntity;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. System Architecture: Core Engine vs. App Autonomy
|
||||||
|
This detailed diagram shows the exact boundaries between the @repo/core-events engine and the consuming application, highlighting real-world publishers (e.g., Cashier UI) and subscribers.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
|
classDef appComponent fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
|
classDef injection fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||||
|
classDef registry fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
|
classDef coreBus fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||||
|
|
||||||
|
%% ─── Subgraphs ───
|
||||||
subgraph Core ["@repo/core-events (Pure Tool)"]
|
subgraph Core ["@repo/core-events (Pure Tool)"]
|
||||||
R["AppEventRegistry<br/>(empty interface)"]
|
R[AppEventRegistry Empty Interface]
|
||||||
T["AppEvents = mapped type"]
|
T[AppEvents Mapped Type]
|
||||||
E((Event Bus<br/>mitt))
|
E((Global Event Bus mitt))
|
||||||
H[useAppEvent / usePublishEvent]
|
H[Hooks: useAppEvent / usePublishEvent]
|
||||||
R --> T --> E
|
|
||||||
E --> H
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Apps ["apps/web (App Autonomy)"]
|
subgraph Apps ["apps/web (App Autonomy)"]
|
||||||
D["events.d.ts<br/>declare module augmentation"]
|
D[[events.d.ts Declaration Merging]]
|
||||||
A[Cashier UI]
|
|
||||||
B[Profile Settings]
|
%% Publishers
|
||||||
C[WebSocket Client]
|
A([Cashier UI])
|
||||||
X[Electron IPC Bridge]
|
B([Profile Settings])
|
||||||
Y[IndexedDB Sync]
|
C([WebSocket Client])
|
||||||
Z[Stock Grid Row]
|
|
||||||
|
%% Subscribers
|
||||||
|
X([Electron IPC Bridge])
|
||||||
|
Y([IndexedDB Sync])
|
||||||
|
Z([Stock Grid Row])
|
||||||
end
|
end
|
||||||
|
|
||||||
D -. "merges into" .-> R
|
%% ─── Flow & Relationships ───
|
||||||
|
D -.->|Augments| R
|
||||||
|
R ---> T ---> E
|
||||||
|
E ---> H
|
||||||
|
|
||||||
A -- "DEVICE:PRINT_RECEIPT" --> E
|
%% Emitting Events
|
||||||
B -- "AUTH:PROFILE_UPDATED" --> E
|
A ===>|DEVICE:PRINT_RECEIPT| E
|
||||||
C -- "WS:STOCK_UPDATE" --> E
|
B ===>|AUTH:PROFILE_UPDATED| E
|
||||||
|
C ===>|WS:STOCK_UPDATE| E
|
||||||
|
|
||||||
E -.-> X
|
%% Subscribing to Events
|
||||||
E -.-> Y
|
E -.->|Triggers| X
|
||||||
E -.-> Z
|
E -.->|Triggers| Y
|
||||||
|
E -.->|Triggers| Z
|
||||||
|
|
||||||
%% Styling Subgraphs (Backgrounds)
|
%% ─── Apply Styles ───
|
||||||
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
|
class A,B,C,X,Y,Z appComponent;
|
||||||
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
|
class D injection;
|
||||||
|
class R,T registry;
|
||||||
|
class E,H coreBus;
|
||||||
|
|
||||||
%% Styling Core Engine (Purple) & Contracts (Green)
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||||
style R fill:#20c997,stroke:#089981,color:#fff
|
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
style T fill:#20c997,stroke:#089981,color:#fff
|
style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
style E fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
style H fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
|
|
||||||
%% Styling App Injection (Orange) & Components (Blue)
|
|
||||||
style D fill:#fd7e14,stroke:#d9480f,color:#fff
|
|
||||||
style A fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
style B fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
style C fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
style X fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
style Y fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
style Z fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Core Value Proposition
|
||||||
|
|
||||||
|
By routing communication through this centralized event bus, we achieve:
|
||||||
|
|
||||||
|
* **App Autonomy**: The core defines the engine. The app defines the contract. There is zero circular dependency.
|
||||||
|
* **Zero Coupling**: Publishers and subscribers do not need to import, reference, or know about each other's existence.
|
||||||
|
* **Extreme Performance**: Components can subscribe to high-frequency data streams (like WebSockets or hardware signals) and update their own local state *without* triggering massive React tree re-renders.
|
||||||
|
* **Memory Safety**: The provided `useAppEvent` hook automatically handles subscription cleanup on component unmount, proactively preventing the most common source of memory leaks in Single Page Architectures (SPAs).
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Defining Events (Module Augmentation)
|
## Defining Events (Module Augmentation)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Enterprise i18n Architecture (`@repo/core-i18n`)
|
|
||||||
|
|
||||||
[← Back to Root](../../README.md)
|
[← Back to Root](../../README.md)
|
||||||
|
|
||||||
|
# i18n Architecture (`@repo/core-i18n`)
|
||||||
|
|
||||||
A highly decoupled, type-safe internationalization engine for the monorepo.
|
A highly decoupled, type-safe internationalization engine for the monorepo.
|
||||||
|
|
||||||
It uses a **Hybrid Namespace Strategy**:
|
It uses a **Hybrid Namespace Strategy**:
|
||||||
@@ -16,49 +16,50 @@ This architecture strictly adheres to **Inversion of Control (IoC)**. The core e
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
|
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
|
classDef coreEngine fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||||
|
classDef dataStore fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
|
classDef externalAPI fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||||
|
|
||||||
|
%% ─── Subgraphs ───
|
||||||
subgraph Apps ["apps/* (App Autonomy)"]
|
subgraph Apps ["apps/* (App Autonomy)"]
|
||||||
UI[React Components]
|
UI([React Components])
|
||||||
DICT[Feature Dictionaries<br/>e.g., booking.json]
|
DICT[[Feature Dictionaries: booking.json]]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Core ["@repo/core-i18n (Engine)"]
|
subgraph Core ["@repo/core-i18n (Engine)"]
|
||||||
I18N((i18next Instance))
|
I18N[i18next Instance]
|
||||||
STORE[(core-storage)]
|
STORE[(core-storage)]
|
||||||
COMMON[Common Vocabulary]
|
COMMON[Common Vocabulary]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Backend ["Backend API (External)"]
|
subgraph Backend ["Backend API (External)"]
|
||||||
SYNC[Language Sync Endpoint]
|
SYNC([Language Sync Endpoint])
|
||||||
TENANT[Tenant Config Endpoint]
|
TENANT([Tenant Config Endpoint])
|
||||||
end
|
end
|
||||||
|
|
||||||
UI -->|uses useTranslation| I18N
|
%% ─── Flow & Relationships ───
|
||||||
|
UI ===>|uses useTranslation| I18N
|
||||||
DICT -.->|lazy loads| I18N
|
DICT -.->|lazy loads| I18N
|
||||||
COMMON -->|preloads| I18N
|
COMMON --->|preloads| I18N
|
||||||
I18N <-->|reads/persists| STORE
|
I18N <===>|reads / persists| STORE
|
||||||
|
|
||||||
I18N -->|changeLanguage sync| SYNC
|
I18N --->|changeLanguage sync| SYNC
|
||||||
SYNC -.->|fails? rollback| I18N
|
SYNC -.->|fails? rollback| I18N
|
||||||
|
|
||||||
TENANT -.->|applyTenantOverrides| I18N
|
TENANT -.->|applyTenantOverrides| I18N
|
||||||
|
|
||||||
%% Styling Subgraphs (Backgrounds)
|
%% ─── Apply Styles ───
|
||||||
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
|
class UI,DICT appEntity;
|
||||||
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
|
class I18N coreEngine;
|
||||||
style Backend fill:#fff4e6,stroke:#ffd8a8,stroke-width:2px,color:#d9480f
|
class STORE,COMMON dataStore;
|
||||||
|
class SYNC,TENANT externalAPI;
|
||||||
|
|
||||||
%% Styling App Nodes (Blue)
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||||
style UI fill:#339af0,stroke:#1864ab,color:#fff
|
style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
style DICT fill:#339af0,stroke:#1864ab,color:#fff
|
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style Backend fill:transparent,stroke:#f59e0b,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
%% Styling Core Nodes (Purple Engine, Green Storage/Data)
|
|
||||||
style I18N fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
style STORE fill:#20c997,stroke:#089981,color:#fff
|
|
||||||
style COMMON fill:#20c997,stroke:#089981,color:#fff
|
|
||||||
|
|
||||||
%% Styling Backend Nodes (Orange/Network)
|
|
||||||
style SYNC fill:#fd7e14,stroke:#d9480f,color:#fff
|
|
||||||
style TENANT fill:#fd7e14,stroke:#d9480f,color:#fff
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+211
-130
@@ -1,168 +1,249 @@
|
|||||||
# Enterprise Storage Engine (`@repo/core-storage`)
|
|
||||||
|
|
||||||
[← Back to Root](../../README.md)
|
[← Back to Root](../../README.md)
|
||||||
|
|
||||||
The **Enterprise-grade storage engine** for the monorepo.
|
# Storage Engine (`@repo/core-storage`)
|
||||||
|
|
||||||
This package provides a unified, Factory-based interface for interacting with browser storage (`localStorage` and `IndexedDB`). It enforces strict type safety, **Runtime Validation**, App Autonomy (Inversion of Control), and automatically provides **AES encryption at rest** for sensitive payloads (like access tokens) using `@repo/utils`.
|
`@repo/core-storage` is the **Enterprise-grade, multi-tool storage engine** for the Eigen Monorepo.
|
||||||
|
|
||||||
|
It provides a unified set of strictly-typed, secure, and fault-tolerant storage mechanisms tailored for React applications. It enforces strict **Inversion of Control (IoC)**—the core engine knows absolutely nothing about your application's business domains; instead, the consuming apps inject their own configurations and types.
|
||||||
|
|
||||||
|
This package provides three primary storage solutions:
|
||||||
|
1. **Secure Local Storage** (Strict Key-Gatekeeping & AES encryption)
|
||||||
|
2. **Secure IndexedDB** (For larger key-value payloads)
|
||||||
|
3. **Offline-First PouchDB** (For document-oriented, bi-directional sync data)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture & Data Flow
|
## 🔒 Secure Key-Value Storage (LocalStorage & IndexedDB)
|
||||||
|
|
||||||
|
Browser storage is notoriously vulnerable to XSS attacks and pollution. The `LocalStorageService` and `IndexedDBService` implement a strict **Gatekeeper** pattern to solve this.
|
||||||
|
|
||||||
|
By forcing developers to register every key explicitly into either `plainTextKeys` or `encryptedKeys`, the engine guarantees:
|
||||||
|
1. No unapproved or rogue keys can ever be written or read (throws a `Security Exception`).
|
||||||
|
2. Highly sensitive tokens (e.g., JWTs) are automatically routed through the `@repo/utils` AES Encryption pipeline before touching the disk.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
|
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
|
classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||||
|
classDef gatekeeper fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
|
||||||
|
classDef encrypt fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||||
|
classDef error fill:#f43f5e,stroke:#be123c,stroke-width:2px,color:#ffffff
|
||||||
|
classDef database fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||||
|
|
||||||
|
%% ─── Subgraphs ───
|
||||||
subgraph Apps ["apps/* (App Autonomy)"]
|
subgraph Apps ["apps/* (App Autonomy)"]
|
||||||
REG[[AppStorageKey & App Registries]]
|
REG[[AppStorageKey Config]]
|
||||||
UI[React Components / API Interceptors]
|
UI([React Components / API])
|
||||||
INST{{Storage Instances}}
|
INST{{Storage Instances}}
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Core ["@repo/core-storage (Engine Factories)"]
|
subgraph Core ["@repo/core-storage"]
|
||||||
|
FAC[Factory: createStorage]
|
||||||
API[IStorageService API]
|
API[IStorageService API]
|
||||||
FAC[createLocalStorage / createIndexedDB]
|
|
||||||
VAL{Runtime Gatekeeper}
|
VAL{Runtime Gatekeeper}
|
||||||
|
ERR>Throws Security Exception]
|
||||||
ENC{{AES Encryption Pipeline}}
|
ENC{{AES Encryption Pipeline}}
|
||||||
LOCAL[LocalStorage Adapter]
|
LOCAL[(LocalStorage Adapter)]
|
||||||
IDB[IndexedDB Adapter]
|
IDB[(IndexedDB Adapter)]
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Browser ["Browser APIs (Native)"]
|
|
||||||
B_LOCAL[(localStorage)]
|
|
||||||
B_IDB[(IndexedDB)]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
%% ─── Flow & Relationships ───
|
||||||
|
%% 1. Initialization Flow
|
||||||
REG -.->|Injects Keys & Config| FAC
|
REG -.->|Injects Keys & Config| FAC
|
||||||
FAC --> INST
|
FAC -.->|Returns| INST
|
||||||
UI -->|getItem / setItem| INST
|
|
||||||
INST --> API
|
|
||||||
API --> VAL
|
|
||||||
|
|
||||||
VAL -.->|Valid Key?| ENC
|
%% 2. Runtime Execution Flow
|
||||||
VAL -.->|Invalid Key!| ERR[Throws Security Exception]
|
UI ===>|getItem / setItem| INST
|
||||||
|
INST ---> API
|
||||||
|
API ---> VAL
|
||||||
|
|
||||||
ENC -.->|Sensitive Key| LOCAL & IDB
|
%% 3. Gatekeeper Decision Tree
|
||||||
VAL -.->|Plain-text Key| LOCAL & IDB
|
VAL -.->|Invalid Key| ERR
|
||||||
|
VAL ===>|Sensitive Key| ENC
|
||||||
|
|
||||||
|
VAL --->|Plain-text Key| LOCAL
|
||||||
|
VAL --->|Plain-text Key| IDB
|
||||||
|
|
||||||
|
%% 4. Post-Encryption Storage
|
||||||
|
ENC ===>|Encrypted Data| LOCAL
|
||||||
|
ENC ===>|Encrypted Data| IDB
|
||||||
|
|
||||||
LOCAL <--> B_LOCAL
|
%% ─── Apply Styles ───
|
||||||
IDB <--> B_IDB
|
class REG,UI,INST appEntity;
|
||||||
|
class FAC,API coreEntity;
|
||||||
|
class VAL gatekeeper;
|
||||||
|
class ENC encrypt;
|
||||||
|
class ERR error;
|
||||||
|
class LOCAL,IDB database;
|
||||||
|
|
||||||
%% Styling Subgraphs
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||||
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
|
style Apps fill:transparent,stroke:#818cf8,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
|
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
style Browser fill:#f1f3f5,stroke:#ced4da,stroke-width:2px,color:#495057
|
|
||||||
|
|
||||||
%% Styling Nodes
|
|
||||||
style UI fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
style REG fill:#1864ab,stroke:#1864ab,color:#fff
|
|
||||||
style INST fill:#339af0,stroke:#1864ab,color:#fff
|
|
||||||
style API fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
style FAC fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
|
|
||||||
%% Gatekeeper is GREEN (Security Checkpoint), Error is RED
|
|
||||||
style VAL fill:#20c997,stroke:#089981,color:#fff
|
|
||||||
style ERR fill:#fa5252,stroke:#c92a2a,color:#fff
|
|
||||||
|
|
||||||
style LOCAL fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
style IDB fill:#845ef7,stroke:#5f3dc4,color:#fff
|
|
||||||
style ENC fill:#fab005,stroke:#e67700,color:#fff
|
|
||||||
style B_LOCAL fill:#868e96,stroke:#495057,color:#fff
|
|
||||||
style B_IDB fill:#868e96,stroke:#495057,color:#fff
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Usage & Implementation
|
||||||
|
|
||||||
## 🎯 Primary Goals & Architectural Principles
|
|
||||||
|
|
||||||
* **App Autonomy (Inversion of Control)**: The core storage engine does not know about your application's keys. Consuming applications define their own keys, their own `encryptedKeys` sets, and their own `plainTextKeys` sets, injecting them into the factory upon instantiation.
|
|
||||||
* **Runtime Gatekeeper (Defensive Programming)**: The engine validates every `setItem`, `getItem`, and `removeItem` operation. If an app attempts to access a key that wasn't explicitly registered in `encryptedKeys` or `plainTextKeys`, the engine will immediately throw a Security Exception to prevent rogue data access/injection.
|
|
||||||
* **Dual Backend Strategy**:
|
|
||||||
* `createLocalStorage`: Ideal for small, synchronous-like data (tokens, user preferences, settings).
|
|
||||||
* `createIndexedDB`: Built for large, asynchronous data (offline drafts, cached API responses, large arrays/blobs) bypassing the 5MB localStorage limit.
|
|
||||||
* **Security by Default**: Developers don't need to manually encrypt/decrypt data. If a key is passed in the `encryptedKeys` configuration, the engine handles AES encryption transparently.
|
|
||||||
* **Corrupt Data Resilience**: If parsing or decryption fails (e.g., tampered data or changed encryption keys), the corrupt entry is safely removed and returns `null`, preventing the app from crashing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 App-Level Setup & Usage
|
|
||||||
|
|
||||||
### 1. Define App Keys and Instantiate (Inversion of Control)
|
|
||||||
|
|
||||||
In your consuming application (e.g., `apps/web/src/core/storage/index.ts`), define your keys and use the factories to create your instances.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// apps/web/src/core/storage/index.ts
|
|
||||||
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
|
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
|
||||||
|
|
||||||
// 1. Define Keys
|
// 1. Define allowed keys (Strict Type Safety)
|
||||||
export const AppStorageKey = {
|
export type AppStorageKey = 'THEME' | 'ACCESS_TOKEN' | 'OFFLINE_CACHE';
|
||||||
USER_PROFILE: 'user_profile',
|
|
||||||
ACCESS_TOKEN: 'access_token',
|
|
||||||
LOCALE: 'app_locale',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
// 2. Instantiate Local Storage
|
||||||
|
export const appStorage = createLocalStorage<AppStorageKey>({
|
||||||
// 2. Classify Keys
|
plainTextKeys: new Set(['THEME']),
|
||||||
export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
|
encryptedKeys: new Set(['ACCESS_TOKEN']), // Auto AES encrypted
|
||||||
AppStorageKey.USER_PROFILE,
|
|
||||||
AppStorageKey.ACCESS_TOKEN,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
|
||||||
AppStorageKey.LOCALE,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 3. Instantiate Factories
|
|
||||||
export const secureStorage = createLocalStorage<AppStorageKeyValue>({
|
|
||||||
encryptedKeys: ENCRYPTED_KEYS,
|
|
||||||
plainTextKeys: PLAIN_KEYS
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const secureIndexedDB = createIndexedDB<AppStorageKeyValue>({
|
// 3. Usage
|
||||||
dbName: 'eigen_erp_db',
|
await appStorage.setItem('ACCESS_TOKEN', 'ey...'); // Encrypted on disk
|
||||||
storeName: 'web_store',
|
const theme = await appStorage.getItem('THEME'); // Plaintext on disk
|
||||||
encryptedKeys: ENCRYPTED_KEYS,
|
|
||||||
plainTextKeys: PLAIN_KEYS
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Usage in App Components
|
### ✅ Do's and ❌ Don'ts
|
||||||
|
|
||||||
Now, you can import your locally-created instances anywhere in your app.
|
* **✅ DO use TypeScript Literal Types** for your storage keys (`type Keys = 'A' | 'B'`) to get full IntelliSense.
|
||||||
|
* **✅ DO place Session/Auth tokens** exclusively inside the `encryptedKeys` Set.
|
||||||
```typescript
|
* **❌ DON'T use native `window.localStorage` directly** anywhere in your React components. It bypasses our encryption and gatekeeper logic.
|
||||||
import { secureStorage, AppStorageKey } from '@/core/storage';
|
* **❌ DON'T mix domain data.** Keep UI preferences (Theme, Sidebar state) in LocalStorage, and large datasets (Offline Caches) in IndexedDB.
|
||||||
import type { UserProfile } from '@/types';
|
|
||||||
|
|
||||||
// CREATE / UPDATE
|
|
||||||
// Since USER_PROFILE is in ENCRYPTED_KEYS, it is AES-encrypted automatically.
|
|
||||||
await secureStorage.setItem(AppStorageKey.USER_PROFILE, {
|
|
||||||
id: 1,
|
|
||||||
name: 'Firman',
|
|
||||||
role: 'admin'
|
|
||||||
});
|
|
||||||
|
|
||||||
// READ (Returns null if not found or if decryption fails)
|
|
||||||
const profile = await secureStorage.getItem<UserProfile>(AppStorageKey.USER_PROFILE);
|
|
||||||
if (profile) {
|
|
||||||
console.log('Welcome back,', profile.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE
|
|
||||||
await secureStorage.removeItem(AppStorageKey.USER_PROFILE);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. The Runtime Gatekeeper
|
|
||||||
|
|
||||||
If you try to access an unregistered key, the engine protects the app by throwing an error at runtime:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Throws Error: "[Storage Engine] Security Exception: Key 'rogue_key' is not registered..."
|
|
||||||
await secureStorage.setItem('rogue_key' as any, 'hacked');
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!WARNING]
|
## 🔄 Offline-First Document Storage (PouchDB & CouchDB)
|
||||||
> **Migration Hazard**: If you move an existing key from `plainTextKeys` to `encryptedKeys` (or vice versa), existing users will experience a parsing failure on their next session (because the library expects encrypted data but finds plain text). The resilient parser will catch this and gracefully clear the key, which may effectively log them out or reset their local preference.
|
|
||||||
|
For complex, document-oriented data that requires fault-tolerance, offline support, and bi-directional cloud synchronization, we use the `PouchDatabaseManager`.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||||
|
classDef appEntity fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
|
||||||
|
classDef coreEntity fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
|
||||||
|
classDef localDb fill:#6366f1,stroke:#4338ca,stroke-width:2px,color:#ffffff
|
||||||
|
classDef remoteDb fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
|
||||||
|
|
||||||
|
%% ─── Subgraphs ───
|
||||||
|
subgraph UI ["Consuming App (apps/*)"]
|
||||||
|
COMP([React Components / Forms])
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph CoreStorage ["@repo/core-storage Engine"]
|
||||||
|
MGR[PouchDatabaseManager Factory]
|
||||||
|
L_SALES[(Local PouchDB: Sales)]
|
||||||
|
L_INV[(Local PouchDB: Inventory)]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph RemoteServer ["CouchDB Cluster"]
|
||||||
|
R_SALES[(Remote CouchDB: sales_db)]
|
||||||
|
R_INV[(Remote CouchDB: inventory_db)]
|
||||||
|
end
|
||||||
|
|
||||||
|
%% ─── Flow & Relationships ───
|
||||||
|
COMP ===>|Read / Write| L_SALES
|
||||||
|
COMP ===>|Read / Write| L_INV
|
||||||
|
|
||||||
|
MGR -.->|Instantiates Multi-DB| L_SALES
|
||||||
|
MGR -.->|Instantiates Multi-DB| L_INV
|
||||||
|
|
||||||
|
L_SALES <===>|Native Sync Live and Retry| R_SALES
|
||||||
|
L_INV <===>|Native Sync Live and Retry| R_INV
|
||||||
|
|
||||||
|
%% ─── Apply Styles ───
|
||||||
|
class COMP appEntity;
|
||||||
|
class MGR coreEntity;
|
||||||
|
class L_SALES,L_INV localDb;
|
||||||
|
class R_SALES,R_INV remoteDb;
|
||||||
|
|
||||||
|
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||||
|
style UI fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style CoreStorage fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
style RemoteServer fill:transparent,stroke:#f59e0b,stroke-width:2px,stroke-dasharray: 5 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Initialization (IoC Factory)
|
||||||
|
|
||||||
|
The `PouchDatabaseManager` acts as a central singleton. It registers and manages all database instances. If a remote URL is provided, it automatically handles background synchronization.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PouchDatabaseManager } from '@repo/core-storage';
|
||||||
|
import type { Item } from './types';
|
||||||
|
|
||||||
|
export const dbManager = new PouchDatabaseManager();
|
||||||
|
|
||||||
|
export const itemDB = dbManager.register<Item>({
|
||||||
|
localName: 'items_db',
|
||||||
|
remoteUrl: 'http://admin:password@localhost:5984/items_db'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. CRUD & MongoDB-style Queries
|
||||||
|
|
||||||
|
The registered database returns a `PouchDatabaseWrapper`. This wrapper fully abstracts the raw PouchDB API into clean, Promise-based helpers, auto-handling `_rev` conflicts.
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|---|---|
|
||||||
|
| `create(data)` | Inserts a new document. Auto-generates `_id` if omitted. |
|
||||||
|
| `update(id, data)` | Auto-fetches the latest `_rev` to merge payloads cleanly. |
|
||||||
|
| `delete(id)` | Auto-fetches the latest `_rev` to safely remove the document. |
|
||||||
|
| `getAll()` | Retrieves all documents (filters out internal `_design/` docs). |
|
||||||
|
| `find(options)` | Queries using MongoDB-style selectors (via `pouchdb-find`). |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: Querying data using selectors
|
||||||
|
const expensiveItems = await itemDB.find({
|
||||||
|
selector: { price: { $gt: 100 }, category: 'electronics' }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Real-Time Reactivity (`onChange` Pub/Sub)
|
||||||
|
|
||||||
|
We implemented a **Publisher-Subscriber (Pub/Sub)** pattern inside the wrapper to handle real-time data changes efficiently. The wrapper maintains a *single* background connection to the changes feed and broadcasts events to all React subscribers.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useEffect, useCallback, useState } from 'react';
|
||||||
|
import { itemDB } from '../core/db';
|
||||||
|
|
||||||
|
export function InventoryList() {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
const data = await itemDB.getAll();
|
||||||
|
setItems(data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
// Subscribe to background sync mutations
|
||||||
|
const unsubscribe = itemDB.onChange(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// CRITICAL: Prevent memory leaks
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [loadData]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Do's and ❌ Don'ts for PouchDB
|
||||||
|
|
||||||
|
* **✅ DO use `.onChange()`** to make your UI reactive to background cloud syncs.
|
||||||
|
* **✅ DO return the `unsubscribe` function** in your `useEffect` cleanup block to prevent severe memory leaks.
|
||||||
|
* **❌ DON'T use `db.raw.changes()`** inside your React components. It creates zombie WebSocket connections and tightly couples your UI to PouchDB's specific API.
|
||||||
|
* **❌ DON'T pass the `_rev` property** manually when updating or deleting. The wrapper's `update()` and `delete()` methods handle revision fetching automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Troubleshooting
|
||||||
|
|
||||||
|
### CouchDB CORS Infinite Retries
|
||||||
|
By providing a `remoteUrl`, the engine runs bi-directional sync in the background (`live: true, retry: true`). Fault tolerance is guaranteed: if CouchDB crashes, local reads/writes continue uninterrupted.
|
||||||
|
|
||||||
|
However, if your browser blocks CouchDB sync with a **CORS error**, PouchDB will misinterpret this as a network failure and enter an infinite retry loop, flooding your Network tab.
|
||||||
|
|
||||||
|
> **DO NOT try to fix this in the frontend Vite config or proxy!**
|
||||||
|
> This is strictly a CouchDB server policy issue. You must enable CORS directly on the CouchDB cluster (editing its `local.ini` or via its dashboard) to allow `origins`, `credentials`, and `headers`.
|
||||||
@@ -13,12 +13,23 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/utils": "workspace:*"
|
"@repo/utils": "workspace:*",
|
||||||
|
"pouchdb-browser": "^9.0.0",
|
||||||
|
"pouchdb-find": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@repo/eslint-config": "workspace:*",
|
||||||
"@repo/typescript-config": "workspace:*",
|
"@repo/typescript-config": "workspace:*",
|
||||||
|
"@types/pouchdb": "^6.4.2",
|
||||||
|
"@types/pouchdb-adapter-memory": "^6.1.6",
|
||||||
|
"@types/pouchdb-browser": "^6.1.5",
|
||||||
|
"@types/pouchdb-core": "^7.0.15",
|
||||||
|
"@types/pouchdb-find": "^7.3.3",
|
||||||
|
"@types/pouchdb-mapreduce": "^6.1.10",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
|
"pouchdb-adapter-memory": "^9.0.0",
|
||||||
|
"pouchdb-core": "^9.0.0",
|
||||||
|
"pouchdb-mapreduce": "^9.0.0",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"vitest": "^4.0.17"
|
"vitest": "^4.0.17"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
export type { IStorageService } from './storage.interface';
|
export type { IStorageService } from './storage.interface';
|
||||||
export type { StorageOptions } from './local-storage.service';
|
export type { StorageOptions } from './local-storage.service';
|
||||||
export type { IndexedDBConfig } from './indexed-db.service';
|
export type { IndexedDBConfig } from './indexed-db.service';
|
||||||
|
export type { PouchConfig } from './pouch';
|
||||||
|
|
||||||
// ─── Service Classes ────────────────────────────────────────────
|
// ─── Service Classes ────────────────────────────────────────────
|
||||||
export { LocalStorageService, createLocalStorage } from './local-storage.service';
|
export { LocalStorageService, createLocalStorage } from './local-storage.service';
|
||||||
export { IndexedDBService, createIndexedDB } from './indexed-db.service';
|
export { IndexedDBService, createIndexedDB } from './indexed-db.service';
|
||||||
|
export { PouchDatabaseManager, PouchDatabaseWrapper } from './pouch';
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import PouchDB from 'pouchdb-browser';
|
||||||
|
import PouchDBFind from 'pouchdb-find';
|
||||||
|
|
||||||
|
// Register the find plugin globally
|
||||||
|
PouchDB.plugin(PouchDBFind);
|
||||||
|
|
||||||
|
// ─── Configuration Interface ────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for creating a new PouchDB database instance.
|
||||||
|
* Follows Inversion of Control — the consuming app decides names and remote URLs.
|
||||||
|
*/
|
||||||
|
export interface PouchConfig {
|
||||||
|
/** Name of the local PouchDB database (stored in IndexedDB by the browser). */
|
||||||
|
localName: string;
|
||||||
|
/**
|
||||||
|
* Optional remote CouchDB URL for bi-directional live sync.
|
||||||
|
* Should include credentials if authentication is required.
|
||||||
|
* Example: `http://user:password@host:port/db_name`
|
||||||
|
*/
|
||||||
|
remoteUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PouchDatabaseWrapper ───────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object-Oriented wrapper around a single PouchDB instance.
|
||||||
|
* Provides strictly-typed CRUD + query helpers so developers never interact
|
||||||
|
* with the raw PouchDB API or pass `dbName` repeatedly.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const itemDB = dbManager.register({ localName: 'items' });
|
||||||
|
* await itemDB.create({ _id: 'item-001', name: 'Widget', price: 9.99 });
|
||||||
|
* const results = await itemDB.find({ selector: { price: { $gt: 5 } } });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class PouchDatabaseWrapper<DefaultType extends object = any> {
|
||||||
|
/** The underlying raw PouchDB instance (escape hatch for advanced usage). */
|
||||||
|
readonly raw: PouchDB.Database;
|
||||||
|
|
||||||
|
private syncHandler: PouchDB.Replication.Sync<object> | null = null;
|
||||||
|
private listeners = new Set<() => void>();
|
||||||
|
private changesFeed: PouchDB.Core.Changes<object> | null = null;
|
||||||
|
|
||||||
|
constructor(config: PouchConfig) {
|
||||||
|
this.raw = new PouchDB(config.localName);
|
||||||
|
|
||||||
|
// Set up bi-directional live sync if a remote URL is provided
|
||||||
|
if (config.remoteUrl) {
|
||||||
|
this.syncHandler = this.raw.sync(config.remoteUrl, {
|
||||||
|
live: true,
|
||||||
|
retry: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fault-tolerant error handling — prevents app crashes when CouchDB is unreachable
|
||||||
|
this.syncHandler.on('error', (err: unknown) => {
|
||||||
|
console.warn(`[PouchDB Sync] Error on "${config.localName}":`, err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.syncHandler.on('paused', (info: unknown) => {
|
||||||
|
if (info) {
|
||||||
|
console.warn(`[PouchDB Sync] Paused on "${config.localName}":`, info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.syncHandler.on('denied', (err: unknown) => {
|
||||||
|
console.warn(`[PouchDB Sync] Denied on "${config.localName}":`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── CRUD Operations ────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new document. If `_id` is not provided in data, PouchDB generates one.
|
||||||
|
*/
|
||||||
|
async create<T extends object = DefaultType>(data: T): Promise<PouchDB.Core.Response> {
|
||||||
|
return this.raw.put(data as PouchDB.Core.Document<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing document by ID.
|
||||||
|
* Automatically fetches the latest `_rev` to prevent conflict errors.
|
||||||
|
*/
|
||||||
|
async update<T extends object = DefaultType>(id: string, data: Partial<T>): Promise<PouchDB.Core.Response> {
|
||||||
|
const existing = await this.raw.get(id);
|
||||||
|
const merged = { ...existing, ...data };
|
||||||
|
return this.raw.put(merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a document by ID.
|
||||||
|
* Automatically fetches the latest `_rev` before removal.
|
||||||
|
*/
|
||||||
|
async delete(id: string): Promise<PouchDB.Core.Response> {
|
||||||
|
const doc = await this.raw.get(id);
|
||||||
|
return this.raw.remove(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a single document by ID.
|
||||||
|
*/
|
||||||
|
async getOne<T = DefaultType>(id: string): Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta> {
|
||||||
|
return this.raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all documents from the database.
|
||||||
|
* Returns a clean array of document objects (excludes PouchDB design docs).
|
||||||
|
*/
|
||||||
|
async getAll<T = DefaultType>(): Promise<T[]> {
|
||||||
|
const result = await this.raw.allDocs({ include_docs: true });
|
||||||
|
return result.rows
|
||||||
|
.filter((row) => !row.id.startsWith('_design/'))
|
||||||
|
.map((row) => row.doc as unknown as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve multiple documents by their IDs.
|
||||||
|
* Returns a clean array of found documents (silently skips missing/errored entries).
|
||||||
|
*/
|
||||||
|
async getSome<T = DefaultType>(ids: string[]): Promise<T[]> {
|
||||||
|
const result = await this.raw.allDocs({ keys: ids, include_docs: true });
|
||||||
|
return result.rows
|
||||||
|
.filter((row): row is PouchDB.Core.AllDocsResponse<object>['rows'][number] => !('error' in row) && !!(row as any).doc)
|
||||||
|
.map((row) => (row as any).doc as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query documents using MongoDB-style selectors (powered by `pouchdb-find`).
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const electronics = await itemDB.find({ selector: { category: 'electronics' } });
|
||||||
|
* const expensive = await itemDB.find({ selector: { price: { $gt: 100 } }, limit: 10 });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async find<T extends object = DefaultType>(options: PouchDB.Find.FindRequest<T>): Promise<T[]> {
|
||||||
|
const result = await this.raw.find(options as PouchDB.Find.FindRequest<object>);
|
||||||
|
return result.docs as unknown as T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all documents from the database while keeping the database itself intact.
|
||||||
|
* Useful for "clear cache" or "reset local data" flows.
|
||||||
|
*/
|
||||||
|
async cleanAllData(): Promise<void> {
|
||||||
|
const result = await this.raw.allDocs();
|
||||||
|
const deletions = result.rows
|
||||||
|
.filter((row) => !row.id.startsWith('_design/'))
|
||||||
|
.map((row) => ({
|
||||||
|
_id: row.id,
|
||||||
|
_rev: row.value.rev,
|
||||||
|
_deleted: true as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (deletions.length > 0) {
|
||||||
|
await this.raw.bulkDocs(deletions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel any active sync and completely destroy the local database.
|
||||||
|
* After calling this, the wrapper instance should not be used again.
|
||||||
|
*/
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
if (this.syncHandler) {
|
||||||
|
this.syncHandler.cancel();
|
||||||
|
}
|
||||||
|
if (this.changesFeed) {
|
||||||
|
this.changesFeed.cancel();
|
||||||
|
}
|
||||||
|
this.listeners.clear();
|
||||||
|
await this.raw.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the live sync connection (if active) without destroying the database.
|
||||||
|
*/
|
||||||
|
cancelSync(): void {
|
||||||
|
if (this.syncHandler) {
|
||||||
|
this.syncHandler.cancel();
|
||||||
|
this.syncHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to real-time changes in the database.
|
||||||
|
* Returns an unsubscribe function.
|
||||||
|
*/
|
||||||
|
onChange(callback: () => void): () => void {
|
||||||
|
this.listeners.add(callback);
|
||||||
|
|
||||||
|
if (!this.changesFeed) {
|
||||||
|
this.changesFeed = this.raw.changes({
|
||||||
|
since: 'now',
|
||||||
|
live: true,
|
||||||
|
include_docs: true
|
||||||
|
}).on('change', () => {
|
||||||
|
this.listeners.forEach(cb => cb());
|
||||||
|
}).on('error', (err) => {
|
||||||
|
console.warn(`[PouchDB Listener Error]`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PouchDatabaseManager ───────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoC Factory for managing multiple PouchDB database instances with optional
|
||||||
|
* bi-directional CouchDB synchronization.
|
||||||
|
*
|
||||||
|
* `register()` returns a `PouchDatabaseWrapper` with full CRUD + query helpers,
|
||||||
|
* so developers never need to pass `dbName` into each operation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const dbManager = new PouchDatabaseManager();
|
||||||
|
*
|
||||||
|
* const itemDB = dbManager.register({ localName: 'items' });
|
||||||
|
* await itemDB.create({ _id: 'item-001', name: 'Widget', price: 9.99 });
|
||||||
|
*
|
||||||
|
* const results = await itemDB.find({ selector: { price: { $gt: 5 } } });
|
||||||
|
* console.log(results); // [{ _id: 'item-001', name: 'Widget', price: 9.99, ... }]
|
||||||
|
*
|
||||||
|
* const salesDB = dbManager.register({
|
||||||
|
* localName: 'sales',
|
||||||
|
* remoteUrl: 'http://admin:pass@localhost:5984/sales_db',
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class PouchDatabaseManager {
|
||||||
|
private databases = new Map<string, PouchDatabaseWrapper<any>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register and initialize a new PouchDB database.
|
||||||
|
* If a database with the same `localName` already exists, returns the existing wrapper.
|
||||||
|
*
|
||||||
|
* @param config - Configuration for the database instance.
|
||||||
|
* @returns A `PouchDatabaseWrapper` with CRUD + query helpers.
|
||||||
|
*/
|
||||||
|
register<T extends object = any>(config: PouchConfig): PouchDatabaseWrapper<T> {
|
||||||
|
if (this.databases.has(config.localName)) {
|
||||||
|
return this.databases.get(config.localName) as PouchDatabaseWrapper<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = new PouchDatabaseWrapper<T>(config);
|
||||||
|
this.databases.set(config.localName, wrapper);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a previously registered database wrapper by its local name.
|
||||||
|
*/
|
||||||
|
get<T extends object = any>(localName: string): PouchDatabaseWrapper<T> | undefined {
|
||||||
|
return this.databases.get(localName) as PouchDatabaseWrapper<T> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a specific registered database and remove it from the manager.
|
||||||
|
*/
|
||||||
|
async destroy(localName: string): Promise<void> {
|
||||||
|
const wrapper = this.databases.get(localName);
|
||||||
|
if (!wrapper) return;
|
||||||
|
await wrapper.destroy();
|
||||||
|
this.databases.delete(localName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all managed databases. Useful for logout/cleanup scenarios.
|
||||||
|
*/
|
||||||
|
async destroyAll(): Promise<void> {
|
||||||
|
const names = Array.from(this.databases.keys());
|
||||||
|
await Promise.all(names.map((name) => this.destroy(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all currently registered database names.
|
||||||
|
*/
|
||||||
|
listDatabases(): string[] {
|
||||||
|
return Array.from(this.databases.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,7 +145,7 @@ describe('LocalStorageService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns null for non-existent keys', async () => {
|
it('returns null for non-existent keys', async () => {
|
||||||
const result = await storage.getItem<string>('nonexistent' as TestStorageKeyValue);
|
const result = await storage.getItem<string>(TestStorageKey.THEME);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import PouchDB from 'pouchdb-core';
|
||||||
|
import PouchDBAdapterMemory from 'pouchdb-adapter-memory';
|
||||||
|
import PouchDBFind from 'pouchdb-find';
|
||||||
|
import PouchDBMapReduce from 'pouchdb-mapreduce';
|
||||||
|
|
||||||
|
// Build a minimal PouchDB for testing: core + memory adapter + find
|
||||||
|
PouchDB.plugin(PouchDBAdapterMemory);
|
||||||
|
PouchDB.plugin(PouchDBFind);
|
||||||
|
PouchDB.plugin(PouchDBMapReduce);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since the tests run in Node (not a browser), we cannot use PouchDatabaseManager
|
||||||
|
* directly because it imports `pouchdb-browser` which requires `self`.
|
||||||
|
* Instead, we test the CRUD logic by creating a lightweight test wrapper
|
||||||
|
* that mirrors PouchDatabaseWrapper's methods using the memory-backed PouchDB.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createTestDB(name: string) {
|
||||||
|
const raw = new PouchDB(name, { adapter: 'memory' });
|
||||||
|
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
|
||||||
|
async create<T extends object>(data: T) {
|
||||||
|
return raw.put(data as PouchDB.Core.Document<T>);
|
||||||
|
},
|
||||||
|
|
||||||
|
async update<T extends object>(id: string, data: Partial<T>) {
|
||||||
|
const existing = await raw.get(id);
|
||||||
|
const merged = { ...existing, ...data };
|
||||||
|
return raw.put(merged);
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
const doc = await raw.get(id);
|
||||||
|
return raw.remove(doc);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOne<T>(id: string) {
|
||||||
|
return raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAll<T>() {
|
||||||
|
const result = await raw.allDocs({ include_docs: true });
|
||||||
|
return result.rows
|
||||||
|
.filter((row) => !row.id.startsWith('_design/'))
|
||||||
|
.map((row) => row.doc as unknown as T);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSome<T>(ids: string[]) {
|
||||||
|
const result = await raw.allDocs({ keys: ids, include_docs: true });
|
||||||
|
return result.rows
|
||||||
|
.filter((row): row is any => !('error' in row) && !!(row as any).doc)
|
||||||
|
.map((row: any) => row.doc as T);
|
||||||
|
},
|
||||||
|
|
||||||
|
async find<T extends object>(options: PouchDB.Find.FindRequest<T>) {
|
||||||
|
const result = await raw.find(options as PouchDB.Find.FindRequest<object>);
|
||||||
|
return result.docs as unknown as T[];
|
||||||
|
},
|
||||||
|
|
||||||
|
async cleanAllData() {
|
||||||
|
const result = await raw.allDocs();
|
||||||
|
const deletions = result.rows
|
||||||
|
.filter((row) => !row.id.startsWith('_design/'))
|
||||||
|
.map((row) => ({
|
||||||
|
_id: row.id,
|
||||||
|
_rev: row.value.rev,
|
||||||
|
_deleted: true as const,
|
||||||
|
}));
|
||||||
|
if (deletions.length > 0) {
|
||||||
|
await raw.bulkDocs(deletions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
await raw.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PouchDatabaseWrapper CRUD Operations', () => {
|
||||||
|
let db: ReturnType<typeof createTestDB>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Use a unique name per test to avoid cross-contamination
|
||||||
|
db = createTestDB(`test_db_${Date.now()}_${Math.random().toString(36).slice(2)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
try {
|
||||||
|
await db.destroy();
|
||||||
|
} catch {
|
||||||
|
// Already destroyed in some tests
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── create ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should create a document with a given _id', async () => {
|
||||||
|
const res = await db.create({ _id: 'doc-001', name: 'Alice', age: 30 });
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
expect(res.id).toBe('doc-001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a conflict if creating with a duplicate _id', async () => {
|
||||||
|
await db.create({ _id: 'dup-001', name: 'First' });
|
||||||
|
await expect(db.create({ _id: 'dup-001', name: 'Second' })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getOne ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getOne', () => {
|
||||||
|
it('should retrieve a document by id', async () => {
|
||||||
|
await db.create({ _id: 'fetch-001', product: 'Widget', price: 9.99 });
|
||||||
|
|
||||||
|
const doc = await db.getOne<{ product: string; price: number }>('fetch-001');
|
||||||
|
expect(doc._id).toBe('fetch-001');
|
||||||
|
expect(doc.product).toBe('Widget');
|
||||||
|
expect(doc.price).toBe(9.99);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for a non-existent document', async () => {
|
||||||
|
await expect(db.getOne('non-existent')).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── update ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should merge new fields into an existing document', async () => {
|
||||||
|
await db.create({ _id: 'upd-001', name: 'Original', count: 1 });
|
||||||
|
|
||||||
|
const res = await db.update('upd-001', { count: 42, extra: 'field' });
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
const updated = await db.getOne<{ name: string; count: number; extra: string }>('upd-001');
|
||||||
|
expect(updated.name).toBe('Original'); // untouched
|
||||||
|
expect(updated.count).toBe(42); // updated
|
||||||
|
expect(updated.extra).toBe('field'); // newly added
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when updating a non-existent document', async () => {
|
||||||
|
await expect(db.update('ghost', { name: 'nope' })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── delete ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should remove a document by id', async () => {
|
||||||
|
await db.create({ _id: 'del-001', name: 'ToBeDeleted' });
|
||||||
|
|
||||||
|
const res = await db.delete('del-001');
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
|
||||||
|
await expect(db.getOne('del-001')).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getAll ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should return all documents as a flat array', async () => {
|
||||||
|
await db.create({ _id: 'a', val: 1 });
|
||||||
|
await db.create({ _id: 'b', val: 2 });
|
||||||
|
await db.create({ _id: 'c', val: 3 });
|
||||||
|
|
||||||
|
const all = await db.getAll<{ val: number }>();
|
||||||
|
expect(all).toHaveLength(3);
|
||||||
|
expect(all.map((d: any) => d.val).sort()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty database', async () => {
|
||||||
|
const all = await db.getAll();
|
||||||
|
expect(all).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── getSome ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('getSome', () => {
|
||||||
|
it('should return only the requested documents', async () => {
|
||||||
|
await db.create({ _id: 'x1', v: 10 });
|
||||||
|
await db.create({ _id: 'x2', v: 20 });
|
||||||
|
await db.create({ _id: 'x3', v: 30 });
|
||||||
|
|
||||||
|
const some = await db.getSome<{ v: number }>(['x1', 'x3']);
|
||||||
|
expect(some).toHaveLength(2);
|
||||||
|
expect(some.map((d: any) => d.v).sort()).toEqual([10, 30]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should silently skip missing ids', async () => {
|
||||||
|
await db.create({ _id: 'exists', v: 1 });
|
||||||
|
|
||||||
|
const some = await db.getSome<{ v: number }>(['exists', 'ghost']);
|
||||||
|
expect(some).toHaveLength(1);
|
||||||
|
expect((some[0] as any).v).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── find (pouchdb-find selectors) ────────────────────────────
|
||||||
|
|
||||||
|
describe('find', () => {
|
||||||
|
it('should filter documents using selectors', async () => {
|
||||||
|
await db.create({ _id: 'p1', category: 'electronics', price: 100 });
|
||||||
|
await db.create({ _id: 'p2', category: 'clothing', price: 50 });
|
||||||
|
await db.create({ _id: 'p3', category: 'electronics', price: 200 });
|
||||||
|
|
||||||
|
const results = await db.find<{ category: string; price: number }>({
|
||||||
|
selector: { category: 'electronics' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.every((r) => r.category === 'electronics')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support $gt comparisons', async () => {
|
||||||
|
await db.create({ _id: 'i1', price: 10 });
|
||||||
|
await db.create({ _id: 'i2', price: 50 });
|
||||||
|
await db.create({ _id: 'i3', price: 100 });
|
||||||
|
|
||||||
|
const results = await db.find<{ price: number }>({
|
||||||
|
selector: { price: { $gt: 40 } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.every((r) => r.price > 40)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── cleanAllData ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('cleanAllData', () => {
|
||||||
|
it('should remove all documents but keep the database intact', async () => {
|
||||||
|
await db.create({ _id: 'c1', name: 'One' });
|
||||||
|
await db.create({ _id: 'c2', name: 'Two' });
|
||||||
|
await db.create({ _id: 'c3', name: 'Three' });
|
||||||
|
|
||||||
|
let all = await db.getAll();
|
||||||
|
expect(all).toHaveLength(3);
|
||||||
|
|
||||||
|
await db.cleanAllData();
|
||||||
|
|
||||||
|
all = await db.getAll();
|
||||||
|
expect(all).toHaveLength(0);
|
||||||
|
|
||||||
|
// Database should still be functional after cleaning
|
||||||
|
await db.create({ _id: 'c4', name: 'Four' });
|
||||||
|
all = await db.getAll();
|
||||||
|
expect(all).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+561
-1
@@ -185,9 +185,15 @@ importers:
|
|||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.19
|
specifier: ^1.11.19
|
||||||
version: 1.11.19
|
version: 1.11.19
|
||||||
|
events:
|
||||||
|
specifier: ^3.3.0
|
||||||
|
version: 3.3.0
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^24.2.2
|
specifier: ^24.2.2
|
||||||
version: 24.2.3(typescript@5.5.4)
|
version: 24.2.3(typescript@5.5.4)
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^1.17.0
|
||||||
|
version: 1.17.0(react@19.2.3)
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.3
|
specifier: ^19.2.3
|
||||||
version: 19.2.3
|
version: 19.2.3
|
||||||
@@ -379,6 +385,12 @@ importers:
|
|||||||
'@repo/utils':
|
'@repo/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../utils
|
version: link:../utils
|
||||||
|
pouchdb-browser:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.0
|
||||||
|
pouchdb-find:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@repo/eslint-config':
|
'@repo/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -386,9 +398,36 @@ importers:
|
|||||||
'@repo/typescript-config':
|
'@repo/typescript-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../configs/typescript
|
version: link:../configs/typescript
|
||||||
|
'@types/pouchdb':
|
||||||
|
specifier: ^6.4.2
|
||||||
|
version: 6.4.2
|
||||||
|
'@types/pouchdb-adapter-memory':
|
||||||
|
specifier: ^6.1.6
|
||||||
|
version: 6.1.6
|
||||||
|
'@types/pouchdb-browser':
|
||||||
|
specifier: ^6.1.5
|
||||||
|
version: 6.1.5
|
||||||
|
'@types/pouchdb-core':
|
||||||
|
specifier: ^7.0.15
|
||||||
|
version: 7.0.15
|
||||||
|
'@types/pouchdb-find':
|
||||||
|
specifier: ^7.3.3
|
||||||
|
version: 7.3.3
|
||||||
|
'@types/pouchdb-mapreduce':
|
||||||
|
specifier: ^6.1.10
|
||||||
|
version: 6.1.10
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.1
|
specifier: ^8.57.1
|
||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
|
pouchdb-adapter-memory:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.0
|
||||||
|
pouchdb-core:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.0
|
||||||
|
pouchdb-mapreduce:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.5.4
|
specifier: 5.5.4
|
||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
@@ -3125,6 +3164,135 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-cordova-sqlite@1.0.4:
|
||||||
|
resolution: {integrity: sha512-1MGjmAMux3OIyJ+iXfhJ5hNIzS+KjGJ05O3bF5Gen5TiJUFNK1bOp3VVV9SxXgz+hGwnBruBAWdAqhbB6ZHhSA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-fruitdown@6.1.6:
|
||||||
|
resolution: {integrity: sha512-KaFB29hUI97eTtJI6pjv7EQcqhZ63qHWovKgyiE+HZF5fVmdrBbTmnIrbR87AJXcXKy47+oQFJ7rzxY8TalpLQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-http@6.1.6:
|
||||||
|
resolution: {integrity: sha512-DJur1mt07GJXwGb5K+MOILoCOSgoQpsi7hybcTzRLeR3IO8Y8eq7TnhTkftAJdx9VHJGOiOXFjO+8BYM69j5yA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-idb@6.1.7:
|
||||||
|
resolution: {integrity: sha512-KwjkJ4fTNz5wPXYu20bUoWud7ty0t7tgdo4oc0AJvG+fcURAH7mI7uFmpE4dZIT+hUq5G61xu96AVq9b2q4T3g==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-leveldb@6.1.6:
|
||||||
|
resolution: {integrity: sha512-mqeTpA2Ni2U4FA5ISRESy4WwhfUahXViUa3jQpXGdSpruaeHlhTLzZJPyz7/mGlvdAfAFv9Vd5d6ys3ASmMujw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-localstorage@6.1.6:
|
||||||
|
resolution: {integrity: sha512-+HQBCpD80XkKJE64r7uLwzkNRgkvMnhDI5rIFLx3USxdrRph/R3awcEubRFndcgtxzcUaL9iYw9KetgFMUqPrg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-memory@6.1.6:
|
||||||
|
resolution: {integrity: sha512-QCCtW561XuwFACzP/4zYySzs/a4em0EeuQdszen0YOaGV1/fRqJE0dOlmzh8do4sNJomLO6+MFtEzguGljnkgA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-node-websql@6.1.5:
|
||||||
|
resolution: {integrity: sha512-yi68syUvHs4OM3mzKlh4zfpov64KITIAnxi387zgdby6SEfAJzWPC0dfH77iEVRDGCrKb3cKTNkl/UGHnphaow==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-adapter-websql': 6.1.7
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-adapter-websql@6.1.7:
|
||||||
|
resolution: {integrity: sha512-9oNkP5ZCGMkQALO9KmtbHXlkBq8i2hoCEE6/gWzRicAvL1y+WIKjEQiIIEamMhj5u5tARvW3n2/r+JXwLCyYgw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-browser@6.1.5:
|
||||||
|
resolution: {integrity: sha512-f+HjxEjYFpgoYWXnMI9AQZZ+SIG8dBiBPrpfWWGsCl+48rumsP5BuBWHq/aXoB8SRKYO0XdP4TNvMBWM3UATCw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-adapter-http': 6.1.6
|
||||||
|
'@types/pouchdb-adapter-idb': 6.1.7
|
||||||
|
'@types/pouchdb-adapter-websql': 6.1.7
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
'@types/pouchdb-mapreduce': 6.1.10
|
||||||
|
'@types/pouchdb-replication': 6.4.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-core@7.0.15:
|
||||||
|
resolution: {integrity: sha512-gq1Qbqn9nCaAKRRv6fRHZ4/ER+QYEwSXBZlDQcxwdbPrtZO8EhIn2Bct0AlguaSEdFcABfbaxxyQwFINkNQ9dQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/debug': 4.1.12
|
||||||
|
'@types/pouchdb-find': 7.3.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-find@7.3.3:
|
||||||
|
resolution: {integrity: sha512-U7zXk67s9Ar+9Pwj5kSbuMnn8zif0AOOIPy4KRFeJ/S/Tk+mNS90soj+3OV21H8xyB7WTxjvS1JLablZC6C6ow==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-http@6.1.5:
|
||||||
|
resolution: {integrity: sha512-9jGCAl6DUsXIl1vjuPu8tzGykAr84549P4IS0zYdrOKq5eXzQRUb/tb2hEVTmmTcYKXu2P1N55ABsdDNZvzGGA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-adapter-http': 6.1.6
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-mapreduce@6.1.10:
|
||||||
|
resolution: {integrity: sha512-AgYVqCnaA5D7cWkWyzZVuk0137N4yZsmIQTD/i3DmuMxYYoFrtWUoQu0tbA52SpTRGdL8ubQ7JFQXzA13fA6IQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-node@6.1.7:
|
||||||
|
resolution: {integrity: sha512-hryc2eCtNB3GbLcHSwU8glLaY66gDMus1AYkcIYAAxufdnK2BAy1oxaRLmnwRn1A1vG41P/t0htFD161LUnfQw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-adapter-http': 6.1.6
|
||||||
|
'@types/pouchdb-adapter-leveldb': 6.1.6
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
'@types/pouchdb-mapreduce': 6.1.10
|
||||||
|
'@types/pouchdb-replication': 6.4.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb-replication@6.4.7:
|
||||||
|
resolution: {integrity: sha512-slB4zOwri3SAVHioFx/FWC/KqOzzb7nDFtV+qzaKzxkf+U5zTwCbK3uRHaj0d/XQk0DwVeajf1ni3Wiyq3j2OA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
'@types/pouchdb-find': 7.3.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/pouchdb@6.4.2:
|
||||||
|
resolution: {integrity: sha512-YsI47rASdtzR+3V3JE2UKY58snhm0AglHBpyckQBkRYoCbTvGagXHtV0x5n8nzN04jQmvTG+Sm85cIzKT3KXBA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/pouchdb-adapter-cordova-sqlite': 1.0.4
|
||||||
|
'@types/pouchdb-adapter-fruitdown': 6.1.6
|
||||||
|
'@types/pouchdb-adapter-http': 6.1.6
|
||||||
|
'@types/pouchdb-adapter-idb': 6.1.7
|
||||||
|
'@types/pouchdb-adapter-leveldb': 6.1.6
|
||||||
|
'@types/pouchdb-adapter-localstorage': 6.1.6
|
||||||
|
'@types/pouchdb-adapter-memory': 6.1.6
|
||||||
|
'@types/pouchdb-adapter-node-websql': 6.1.5
|
||||||
|
'@types/pouchdb-adapter-websql': 6.1.7
|
||||||
|
'@types/pouchdb-browser': 6.1.5
|
||||||
|
'@types/pouchdb-core': 7.0.15
|
||||||
|
'@types/pouchdb-http': 6.1.5
|
||||||
|
'@types/pouchdb-mapreduce': 6.1.10
|
||||||
|
'@types/pouchdb-node': 6.1.7
|
||||||
|
'@types/pouchdb-replication': 6.4.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/react-dom@19.2.3(@types/react@19.2.7):
|
/@types/react-dom@19.2.3(@types/react@19.2.7):
|
||||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3803,6 +3971,25 @@ packages:
|
|||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/abstract-leveldown@2.7.2:
|
||||||
|
resolution: {integrity: sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==}
|
||||||
|
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||||
|
dependencies:
|
||||||
|
xtend: 4.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/abstract-leveldown@6.2.3:
|
||||||
|
resolution: {integrity: sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||||
|
dependencies:
|
||||||
|
buffer: 5.7.1
|
||||||
|
immediate: 3.3.0
|
||||||
|
level-concat-iterator: 2.0.1
|
||||||
|
level-supports: 1.0.1
|
||||||
|
xtend: 4.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/acorn-import-attributes@1.9.5(acorn@8.15.0):
|
/acorn-import-attributes@1.9.5(acorn@8.15.0):
|
||||||
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
|
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4902,6 +5089,15 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/deferred-leveldown@5.3.0:
|
||||||
|
resolution: {integrity: sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||||
|
dependencies:
|
||||||
|
abstract-leveldown: 6.2.3
|
||||||
|
inherits: 2.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/define-data-property@1.1.4:
|
/define-data-property@1.1.4:
|
||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5053,6 +5249,10 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/double-ended-queue@2.1.0-0:
|
||||||
|
resolution: {integrity: sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/dunder-proto@1.0.1:
|
/dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5217,6 +5417,13 @@ packages:
|
|||||||
/err-code@2.0.3:
|
/err-code@2.0.3:
|
||||||
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
|
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
|
||||||
|
|
||||||
|
/errno@0.1.8:
|
||||||
|
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
prr: 1.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/error-ex@1.3.4:
|
/error-ex@1.3.4:
|
||||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5934,6 +6141,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
/events@3.3.0:
|
||||||
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
|
engines: {node: '>=0.8.x'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/execa@5.1.1:
|
/execa@5.1.1:
|
||||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -6025,6 +6237,12 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
/fetch-cookie@2.2.0:
|
||||||
|
resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==}
|
||||||
|
dependencies:
|
||||||
|
set-cookie-parser: 2.7.2
|
||||||
|
tough-cookie: 4.1.4
|
||||||
|
|
||||||
/file-entry-cache@6.0.1:
|
/file-entry-cache@6.0.1:
|
||||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
@@ -6175,6 +6393,10 @@ packages:
|
|||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/functional-red-black-tree@1.0.1:
|
||||||
|
resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/functions-have-names@1.2.3:
|
/functions-have-names@1.2.3:
|
||||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6561,6 +6783,10 @@ packages:
|
|||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/immediate@3.3.0:
|
||||||
|
resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/import-fresh@3.3.1:
|
/import-fresh@3.3.1:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -6911,6 +7137,10 @@ packages:
|
|||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/isarray@0.0.1:
|
||||||
|
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/isarray@1.0.0:
|
/isarray@1.0.0:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -7126,6 +7356,56 @@ packages:
|
|||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/level-codec@9.0.2:
|
||||||
|
resolution: {integrity: sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
deprecated: Superseded by level-transcoder (https://github.com/Level/community#faq)
|
||||||
|
dependencies:
|
||||||
|
buffer: 5.7.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/level-concat-iterator@2.0.1:
|
||||||
|
resolution: {integrity: sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/level-errors@2.0.1:
|
||||||
|
resolution: {integrity: sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||||
|
dependencies:
|
||||||
|
errno: 0.1.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/level-iterator-stream@4.0.2:
|
||||||
|
resolution: {integrity: sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
xtend: 4.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/level-supports@1.0.1:
|
||||||
|
resolution: {integrity: sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
xtend: 4.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/levelup@4.4.0:
|
||||||
|
resolution: {integrity: sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
deprecated: Superseded by abstract-level (https://github.com/Level/community#faq)
|
||||||
|
dependencies:
|
||||||
|
deferred-leveldown: 5.3.0
|
||||||
|
level-errors: 2.0.1
|
||||||
|
level-iterator-stream: 4.0.2
|
||||||
|
level-supports: 1.0.1
|
||||||
|
xtend: 4.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/levn@0.4.1:
|
/levn@0.4.1:
|
||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -7353,6 +7633,18 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ltgt@2.2.1:
|
||||||
|
resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/lucide-react@1.17.0(react@19.2.3):
|
||||||
|
resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lz-string@1.5.0:
|
/lz-string@1.5.0:
|
||||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -7515,6 +7807,18 @@ packages:
|
|||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/memdown@1.4.1:
|
||||||
|
resolution: {integrity: sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w==}
|
||||||
|
deprecated: Superseded by memory-level (https://github.com/Level/community#faq)
|
||||||
|
dependencies:
|
||||||
|
abstract-leveldown: 2.7.2
|
||||||
|
functional-red-black-tree: 1.0.1
|
||||||
|
immediate: 3.3.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
ltgt: 2.2.1
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/memoizerific@1.11.3:
|
/memoizerific@1.11.3:
|
||||||
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
|
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8002,6 +8306,17 @@ packages:
|
|||||||
semver: 7.7.1
|
semver: 7.7.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/node-fetch@2.6.9:
|
||||||
|
resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
encoding: ^0.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
encoding:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
/node-gyp@9.4.1:
|
/node-gyp@9.4.1:
|
||||||
resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==}
|
resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==}
|
||||||
engines: {node: ^12.13 || ^14.13 || >=16}
|
engines: {node: ^12.13 || ^14.13 || >=16}
|
||||||
@@ -8447,6 +8762,163 @@ packages:
|
|||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-abstract-mapreduce@9.0.0:
|
||||||
|
resolution: {integrity: sha512-SnTtqwAEiAa3uxKbc1J7LfiBViwEkKe2xkK92zxyTXPqWBvMnh4UU3GXxx7GrXTM4L9llsQ3lSjpbH4CNqG1Mw==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-binary-utils: 9.0.0
|
||||||
|
pouchdb-collate: 9.0.0
|
||||||
|
pouchdb-errors: 9.0.0
|
||||||
|
pouchdb-fetch: 9.0.0
|
||||||
|
pouchdb-mapreduce-utils: 9.0.0
|
||||||
|
pouchdb-md5: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
|
/pouchdb-adapter-leveldb-core@9.0.0:
|
||||||
|
resolution: {integrity: sha512-b3ZGPtVXyivGL5SK3AIDG7PrNsZdoDpGFkmTytDTtctkVhxOg71gnXXP+CrupENPqSNG/eGbKW4w+bbMpxy6aA==}
|
||||||
|
dependencies:
|
||||||
|
double-ended-queue: 2.1.0-0
|
||||||
|
levelup: 4.4.0
|
||||||
|
pouchdb-adapter-utils: 9.0.0
|
||||||
|
pouchdb-binary-utils: 9.0.0
|
||||||
|
pouchdb-core: 9.0.0
|
||||||
|
pouchdb-errors: 9.0.0
|
||||||
|
pouchdb-json: 9.0.0
|
||||||
|
pouchdb-md5: 9.0.0
|
||||||
|
pouchdb-merge: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
sublevel-pouchdb: 9.0.0
|
||||||
|
through2: 3.0.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-adapter-memory@9.0.0:
|
||||||
|
resolution: {integrity: sha512-XbCwJ5f5U9dGdkiDikzYjTebdPHuA6Ghylx1Pq0lDe4y6l8R9xhjDSUy56pJ8G2F4Z+8QdB5FBY9EQoFlFSXWQ==}
|
||||||
|
dependencies:
|
||||||
|
memdown: 1.4.1
|
||||||
|
pouchdb-adapter-leveldb-core: 9.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-adapter-utils@9.0.0:
|
||||||
|
resolution: {integrity: sha512-hmbm4ey0HL0vtoY1tRTPIt2FfYjvMh3DWoGGSxXDTS73qTFQ+Fhhi5I0AnN9PcD2omfKQAVXiYks4kkMvlAHqA==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-binary-utils: 9.0.0
|
||||||
|
pouchdb-errors: 9.0.0
|
||||||
|
pouchdb-md5: 9.0.0
|
||||||
|
pouchdb-merge: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-binary-utils@9.0.0:
|
||||||
|
resolution: {integrity: sha512-2OMtgDZi82vqs+zNDE0YiYjOaWkYCUcZJZKK3WkRr+XYRu+2B7umJrnygJFhUwoGedBbHSrlQBLhdNV3F1AX1A==}
|
||||||
|
|
||||||
|
/pouchdb-browser@9.0.0:
|
||||||
|
resolution: {integrity: sha512-0uKFWhsTtiVOF0+aGo7mvtCTP40f6dlsLNmJUvc/lwjsX1C3v+eBfVbvykyxpFl7UTAoJkXl+g/GOzNvyMtV1g==}
|
||||||
|
dependencies:
|
||||||
|
spark-md5: 3.0.2
|
||||||
|
uuid: 8.3.2
|
||||||
|
vuvuzela: 1.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/pouchdb-changes-filter@9.0.0:
|
||||||
|
resolution: {integrity: sha512-ig0fo0WLgIjAniFJ19Uw1Y+oxiypqC+Skhd8BCETRVXOhLBzueRwEQR4thffyo0UayYVqldJfSR5wHSDvEVk/A==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-errors: 9.0.0
|
||||||
|
pouchdb-selector-core: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-collate@9.0.0:
|
||||||
|
resolution: {integrity: sha512-TrnEDNZEmIIl+W3xKUO8h+geqVLQ90oZe5ujPkl8myUzpREULWXWQBnV5EzPXVEKDBpJlb8T3I6oy/zdWGQpdA==}
|
||||||
|
|
||||||
|
/pouchdb-core@9.0.0:
|
||||||
|
resolution: {integrity: sha512-98SJgs8bqXhr4gMGuOTR8yVeLlMYy797zlOtdlvlXIxIicvocyA8ColhVVhdBXPNOGxT2HwReIMywdIVAgibpg==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-changes-filter: 9.0.0
|
||||||
|
pouchdb-errors: 9.0.0
|
||||||
|
pouchdb-fetch: 9.0.0
|
||||||
|
pouchdb-merge: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
uuid: 8.3.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-errors@9.0.0:
|
||||||
|
resolution: {integrity: sha512-961PSMLhW0UqqdJ566g+CdLZ5pkBJRd6l4WWpCDdD0USvE4xYfYGzv43w7nZZBw1k3Xdy092yqPge7yX/tfnyw==}
|
||||||
|
|
||||||
|
/pouchdb-fetch@9.0.0:
|
||||||
|
resolution: {integrity: sha512-TbE3cUcAJQrwb9kr44tDP0X+NAbcqgjsTvcL30L4xzBNJeCPTIRjukYX80s154SHJUXBxcWRiPsMmNqpXsjfCA==}
|
||||||
|
dependencies:
|
||||||
|
fetch-cookie: 2.2.0
|
||||||
|
node-fetch: 2.6.9
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
|
/pouchdb-find@9.0.0:
|
||||||
|
resolution: {integrity: sha512-vvVhq4eEOmSkwSRwf2NBYtdhURB7ryJ7sUI4WDN00GuLUj2g8jAXBJuZIryVgdYt/5S5cfn70iRL6Eow+LFhpA==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-abstract-mapreduce: 9.0.0
|
||||||
|
pouchdb-collate: 9.0.0
|
||||||
|
pouchdb-errors: 9.0.0
|
||||||
|
pouchdb-fetch: 9.0.0
|
||||||
|
pouchdb-md5: 9.0.0
|
||||||
|
pouchdb-selector-core: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/pouchdb-json@9.0.0:
|
||||||
|
resolution: {integrity: sha512-aI41mYVyI195GXuT1Ys7mLIB/Mvrz11ihoTP6km6hYqVgSuaUxuZcFUozlyTJiZXr7H5kdhNgclhlVnjir4JAA==}
|
||||||
|
dependencies:
|
||||||
|
vuvuzela: 1.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-mapreduce-utils@9.0.0:
|
||||||
|
resolution: {integrity: sha512-Bjh8W6QXqp1j7MKmHhYYp5cYlcQsm5drD8Jd/F+ZlfNt18uiD2SQXWzGM5797+tiW/LszFGb8ttw0uHWjxufCQ==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
|
||||||
|
/pouchdb-mapreduce@9.0.0:
|
||||||
|
resolution: {integrity: sha512-ZD8PleQ9atzQAzT2LZWsvooUVEfsen5QGv/SDfci20IleCaFW2A2q7OERrqY0YWKDCCNRsWhPWPmsFvZC9K8DQ==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-abstract-mapreduce: 9.0.0
|
||||||
|
pouchdb-mapreduce-utils: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-md5@9.0.0:
|
||||||
|
resolution: {integrity: sha512-58xUYBvW3/s+aH0j4uOhhN8yCk0LQ254cxBzI/gbKA9PrfwHpe4zrr0L/ia5ml3A30oH1f8aTnuVMwWDkFcuww==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-binary-utils: 9.0.0
|
||||||
|
spark-md5: 3.0.2
|
||||||
|
|
||||||
|
/pouchdb-merge@9.0.0:
|
||||||
|
resolution: {integrity: sha512-Xh+TgOZCkGoZpI589btKf/cTiuQ5CsnPl9YpdW4h0cAPusniN6XNsR62F+/HbL9wirI6XTEPHUrk7MsQbk3S3A==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/pouchdb-selector-core@9.0.0:
|
||||||
|
resolution: {integrity: sha512-ZYHYsdoedwm8j5tYofz+3+uUSK8i+7tRCBb01T0OuqDQb17+w5mzjHF8Ppi160xdPUPaWCo1Un+nLWGJzkmA3g==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-collate: 9.0.0
|
||||||
|
pouchdb-utils: 9.0.0
|
||||||
|
|
||||||
|
/pouchdb-utils@9.0.0:
|
||||||
|
resolution: {integrity: sha512-xWZE5c+nAslgmLC8JBZbky8AYgdz7pKtv7KTSi6CD2tuQD0WyNKib0YnhZndeE84dksTeZlqlg56RQHsHoB2LQ==}
|
||||||
|
dependencies:
|
||||||
|
pouchdb-errors: 9.0.0
|
||||||
|
pouchdb-md5: 9.0.0
|
||||||
|
uuid: 8.3.2
|
||||||
|
|
||||||
/prelude-ls@1.2.1:
|
/prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -8543,6 +9015,15 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/prr@1.0.1:
|
||||||
|
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/psl@1.15.0:
|
||||||
|
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
/pump@3.0.4:
|
/pump@3.0.4:
|
||||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8554,6 +9035,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
/querystringify@2.2.0:
|
||||||
|
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||||
|
|
||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -8791,6 +9275,15 @@ packages:
|
|||||||
type-fest: 0.6.0
|
type-fest: 0.6.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/readable-stream@1.1.14:
|
||||||
|
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
|
||||||
|
dependencies:
|
||||||
|
core-util-is: 1.0.3
|
||||||
|
inherits: 2.0.4
|
||||||
|
isarray: 0.0.1
|
||||||
|
string_decoder: 0.10.31
|
||||||
|
dev: true
|
||||||
|
|
||||||
/readable-stream@2.3.8:
|
/readable-stream@2.3.8:
|
||||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8933,6 +9426,9 @@ packages:
|
|||||||
engines: {node: '>=0.10.5'}
|
engines: {node: '>=0.10.5'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/requires-port@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||||
|
|
||||||
/resedit@1.7.2:
|
/resedit@1.7.2:
|
||||||
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
|
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
@@ -9227,7 +9723,6 @@ packages:
|
|||||||
|
|
||||||
/set-cookie-parser@2.7.2:
|
/set-cookie-parser@2.7.2:
|
||||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/set-function-length@1.2.2:
|
/set-function-length@1.2.2:
|
||||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
@@ -9402,6 +9897,9 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/spark-md5@3.0.2:
|
||||||
|
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||||
|
|
||||||
/spdx-correct@3.2.0:
|
/spdx-correct@3.2.0:
|
||||||
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9571,6 +10069,10 @@ packages:
|
|||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/string_decoder@0.10.31:
|
||||||
|
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/string_decoder@1.1.1:
|
/string_decoder@1.1.1:
|
||||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9631,6 +10133,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/sublevel-pouchdb@9.0.0:
|
||||||
|
resolution: {integrity: sha512-pX4r8+F7wuts0C81kUJ341h4bl2aRe7qV572FE8X1FMz9VkKlmi2nPD1vfeiOJXz5Y09I4MHjGULAbqvTfQZEQ==}
|
||||||
|
dependencies:
|
||||||
|
level-codec: 9.0.2
|
||||||
|
ltgt: 2.2.1
|
||||||
|
readable-stream: 1.1.14
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sumchecker@3.0.1:
|
/sumchecker@3.0.1:
|
||||||
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
|
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
|
||||||
engines: {node: '>= 8.0'}
|
engines: {node: '>= 8.0'}
|
||||||
@@ -9729,6 +10239,13 @@ packages:
|
|||||||
/text-table@0.2.0:
|
/text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
|
/through2@3.0.2:
|
||||||
|
resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==}
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tiny-invariant@1.3.3:
|
/tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -9787,6 +10304,15 @@ packages:
|
|||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tough-cookie@4.1.4:
|
||||||
|
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
psl: 1.15.0
|
||||||
|
punycode: 2.3.1
|
||||||
|
universalify: 0.2.0
|
||||||
|
url-parse: 1.5.10
|
||||||
|
|
||||||
/tough-cookie@5.1.2:
|
/tough-cookie@5.1.2:
|
||||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -9794,6 +10320,9 @@ packages:
|
|||||||
tldts: 6.1.86
|
tldts: 6.1.86
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tr46@0.0.3:
|
||||||
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
/tr46@5.1.1:
|
/tr46@5.1.1:
|
||||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -10137,6 +10666,10 @@ packages:
|
|||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/universalify@0.2.0:
|
||||||
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
|
engines: {node: '>= 4.0.0'}
|
||||||
|
|
||||||
/universalify@2.0.1:
|
/universalify@2.0.1:
|
||||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@@ -10198,6 +10731,12 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
/url-parse@1.5.10:
|
||||||
|
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||||
|
dependencies:
|
||||||
|
querystringify: 2.2.0
|
||||||
|
requires-port: 1.0.0
|
||||||
|
|
||||||
/use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3):
|
/use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3):
|
||||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -10286,6 +10825,10 @@ packages:
|
|||||||
which-typed-array: 1.1.19
|
which-typed-array: 1.1.19
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/uuid@8.3.2:
|
||||||
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
/uuid@9.0.1:
|
/uuid@9.0.1:
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -10602,6 +11145,9 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/vuvuzela@1.0.3:
|
||||||
|
resolution: {integrity: sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ==}
|
||||||
|
|
||||||
/w3c-xmlserializer@5.0.0:
|
/w3c-xmlserializer@5.0.0:
|
||||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -10623,6 +11169,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
|
resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/webidl-conversions@3.0.1:
|
||||||
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
/webidl-conversions@7.0.0:
|
/webidl-conversions@7.0.0:
|
||||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -10653,6 +11202,12 @@ packages:
|
|||||||
webidl-conversions: 7.0.0
|
webidl-conversions: 7.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-url@5.0.0:
|
||||||
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
dependencies:
|
||||||
|
tr46: 0.0.3
|
||||||
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
/which-boxed-primitive@1.1.1:
|
/which-boxed-primitive@1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -10792,6 +11347,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/xtend@4.0.2:
|
||||||
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/y18n@5.0.8:
|
/y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|||||||
Reference in New Issue
Block a user