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:
2026-05-29 09:31:58 +00:00
23 changed files with 2385 additions and 662 deletions
+90 -108
View File
@@ -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. |
+110 -77
View File
@@ -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 |
+76 -61
View File
@@ -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 },
}); });
``` ```
+1
View File
@@ -0,0 +1 @@
VITE_CMS_API_URL=http://localhost:8001/cms
+6
View File
@@ -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;
+5
View File
@@ -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
+2
View File
@@ -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",
+237
View File
@@ -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>
);
}
+238 -179
View File
@@ -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>
); );
} }
+57
View File
@@ -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'),
});
+47
View File
@@ -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;
}
+14
View File
@@ -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;
+10
View File
@@ -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
View File
@@ -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)
+91 -49
View File
@@ -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)
+28 -27
View File
@@ -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
View File
@@ -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`.
+12 -1
View File
@@ -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
View File
@@ -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';
+289
View File
@@ -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());
}
}
+1 -1
View File
@@ -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();
}); });
+257
View File
@@ -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);
});
});
});
+561 -1
View File
@@ -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'}