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)
- [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
## πŸ— 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.
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Main Process (src/main/index.ts) β”‚
β”‚ β”‚
β”‚ autoUpdater.checkForUpdatesAndNotify() β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€β†’ 'checking-for-update' β”‚
β”‚ β”œβ”€β†’ 'update-available' β†’ { version, releaseDate } β”‚
β”‚ β”œβ”€β†’ 'download-progress' β†’ { percent, bytesPerSecond } β”‚
β”‚ β”œβ”€β†’ 'update-downloaded' β†’ { version, releaseNotes } β”‚
β”‚ └─→ 'error' β†’ { message } β”‚
β”‚ β”‚
β”‚ sendToRenderer('updater:*', payload) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ IPC Bridge ───────────────────────────────
β”‚ Preload (src/preload/index.ts) β”‚
β”‚ β”‚
β”‚ contextBridge.exposeInMainWorld('electronAPI', { β”‚
β”‚ onUpdateAvailable, onDownloadProgress, β”‚
β”‚ onUpdateDownloaded, onUpdateError, β”‚
β”‚ checkForUpdates, installUpdate β”‚
β”‚ }) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Renderer ─────────────────────────────────
β”‚ React App (apps/web) β”‚
β”‚ β”‚
β”‚ useElectronUpdater() hook β”‚
β”‚ β†’ Reactive state: status, progress, updateInfo, errorMessage β”‚
β”‚ β†’ Actions: checkForUpdates(), installUpdate() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```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 ipcBridge fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
%% ─── Subgraphs ───
subgraph MainProcess ["Main Process (src/main/index.ts)"]
AUTO[autoUpdater.checkForUpdatesAndNotify]
EVENTS{{Update Events: progress, downloaded, error}}
SENDER[sendToRenderer]
end
subgraph Preload ["IPC Bridge (src/preload/index.ts)"]
EXPOSE{contextBridge.exposeInMainWorld}
end
subgraph Renderer ["Renderer (React App - apps/web)"]
HOOK([useElectronUpdater Hook])
ACTIONS[UI Actions: Install, Check]
end
%% ─── Flow & Relationships ───
AUTO ---> EVENTS
EVENTS ---> SENDER
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
@@ -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`:
@@ -77,6 +77,7 @@ publish:
### Operational Mechanics
1. When `electron-builder --publish always` executes, it:
- Compiles the application for the target platform.
- 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).
@@ -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.
@@ -109,8 +110,7 @@ pnpm run prebuild
GH_TOKEN=your_token electron-builder --publish always --config electron-builder.yml
```
> [!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.
> [!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.
### Automated Release (GitHub Actions)
@@ -155,18 +155,18 @@ jobs:
---
## CI/CD Environment Variables
## πŸ” CI/CD Environment Variables
| Variable | Required | Platform | Description |
|---|---|---|---|
| `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_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Passphrase for the `.p12` certificate. |
| `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_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_KEY_PASSWORD` | Windows only | Windows | Passphrase for the Windows certificate. |
| Variable | Required | Platform | Description |
| ----------------------------- | ------------- | -------------- | --------------------------------------------------------------------------------------------- |
| `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_KEY_PASSWORD` | macOS/Windows | macOS, Windows | Passphrase for the `.p12` certificate. |
| `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_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_KEY_PASSWORD` | Windows only | Windows | Passphrase for the Windows certificate. |
### Configuring Secrets
@@ -176,7 +176,7 @@ jobs:
---
## Deployment Strategies
## ☁️ Deployment Strategies
### AWS S3 (Private Infrastructure)
@@ -195,13 +195,14 @@ publish:
**Additional environment variables:**
| Variable | Description |
|---|---|
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 `PutObject` and `GetObject` permissions |
| `AWS_SECRET_ACCESS_KEY` | IAM secret key |
| Variable | Description |
| ----------------------- | -------------------------------------------------------------- |
| `AWS_ACCESS_KEY_ID` | IAM access key with S3 `PutObject` and `GetObject` permissions |
| `AWS_SECRET_ACCESS_KEY` | IAM secret key |
**Bucket structure:**
```
```text
your-bucket/desktop-releases/
β”œβ”€β”€ latest.yml (Windows manifest)
β”œβ”€β”€ latest-mac.yml (macOS manifest)
@@ -222,13 +223,12 @@ For self-hosted infrastructure (Nginx, Caddy, etc.):
```yaml
publish:
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.
> [!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.
> [!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.
**Nginx reference:**
@@ -253,10 +253,9 @@ server {
---
## Code Signing: The Trust Boundary
## πŸ›‘οΈ Code Signing: The Trust Boundary
> [!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**.
> [!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**.
### macOS
@@ -273,7 +272,7 @@ server {
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!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">
<dict>
<key>com.apple.security.cs.allow-jit</key>
@@ -300,7 +299,7 @@ server {
---
## Testing Updates in Development
## πŸ§ͺ Testing Updates in Development
> [!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.
@@ -317,6 +316,7 @@ if (IS_DEV) {
```
This means:
- No update check is performed on startup.
- No `electron-updater` events are emitted.
- 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
| | |
|---|---|
| **Symptom** | Console logs `[AutoUpdater] Startup check failed (possibly offline)` |
| **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. |
---
| Symptom | Root Cause | Resolution |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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. |
### Issue: `app-update.yml` not found in production build
| | |
|---|---|
| **Symptom** | `electron-updater` throws "Cannot find app-update.yml" immediately after launch. |
| **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. |
---
| Symptom | Root Cause | Resolution |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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. |
### Issue: "Cannot update: code signature is invalid" (macOS)
| | |
|---|---|
| **Symptom** | The updater downloads a new version but refuses to apply it, logging a signature validation error. |
| **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. |
---
| Symptom | Root Cause | Resolution |
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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. |
### Issue: Updates work on Windows/Linux but not macOS
| | |
|---|---|
| **Symptom** | Windows and Linux users receive updates, but macOS users see no update prompt. |
| **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. |
---
| Symptom | Root Cause | Resolution |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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. |
### Issue: S3/Generic provider returns corrupted downloads
| | |
|---|---|
| **Symptom** | Users report that the update downloads but fails to install, or the downloaded file is 0 bytes. |
| **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. |
| Symptom | Root Cause | Resolution |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 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. |
+110 -77
View File
@@ -1,4 +1,6 @@
# Configuration Guide
[← Back to Root](../../../README.md)
# Desktop Configuration Guide
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`.
@@ -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:
1. **Update the `.env` declaration:**
```env
DESKTOP_TARGET_APP=admin
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 `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`.
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Deployment Bridge β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ apps/<target>/dist/ β”‚ ─── copy-web-dist.ts ────────→ β”‚ apps/desktop/web-dist/ β”‚
β”‚ (Vite build output) β”‚ prebuild hook β”‚ (Native container) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
electron-builder
files + extraResources
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Packaged .app/.exe β”‚
β”‚ resources/web-dist/ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```mermaid
graph TD
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
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
%% ─── Nodes ───
SOURCE[(apps/TARGET/dist/)]
SCRIPT{copy-web-dist.ts}
DEST[(apps/desktop/web-dist/)]
BUILDER(electron-builder)
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 |
|---|---|---|---|---|
| `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 |
| `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_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_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 |
| 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_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 |
| `CSC_LINK` | β€” | CI/CD | `electron-builder` | Base64-encoded `.p12` code signing 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_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 |
> [!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.
> [!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.
---
## 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.
| 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 |
| **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 |
| 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 |
| **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 |
> [!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`.
---
## Production Routing: Overcoming Protocol Constraints
## πŸš€ Production Routing: Overcoming Protocol Constraints
### 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:
```
Request: app://-/settings/profile
β”‚
β”œβ”€ Decode URI β†’ "settings/profile"
β”‚
β”œβ”€ Normalize + validate path (security boundary check)
β”‚
β”œβ”€ Does web-dist/settings/profile exist as a file?
β”‚ β”œβ”€ YES β†’ Serve with correct MIME type + CSP headers
β”‚ └─ NO β†’ Heuristic Fallback: serve web-dist/index.html
β”‚
└─ React Router resolves /settings/profile client-side
```mermaid
graph TD
%% ─── Styling Definitions ───
classDef request fill:#f59e0b,stroke:#b45309,stroke-width:2px,color:#ffffff
classDef process fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
classDef decision fill:#10b981,stroke:#047857,stroke-width:2px,color:#ffffff
classDef success fill:#3b82f6,stroke:#1d4ed8,stroke-width:2px,color:#ffffff
%% ─── Nodes ───
REQ([Request: app://-/settings/profile])
DECODE[Decode URI and Normalize Path]
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.
@@ -142,11 +171,11 @@ protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true, // Enables URL parsing (host, path, query)
secure: true, // Treated as a secure origin (HTTPS equivalent)
standard: true, // Enables URL parsing (host, path, query)
secure: true, // Treated as a secure origin (HTTPS equivalent)
supportFetchAPI: true, // Allows fetch() from this scheme
corsEnabled: true, // Enables CORS for cross-origin requests
stream: true, // Supports streaming responses
corsEnabled: true, // Enables CORS for cross-origin requests
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.
| 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` |
| **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://` |
| **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 |
| **Navigation Confinement** | `will-navigate` guard | Blocks navigation to URLs outside `app://` and the authorized dev server | Phishing via in-app redirect to malicious sites |
| 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` |
| **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://` |
| **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 |
| **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]
> **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**.
> [!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**.
### 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:
**a)** Remove the scheme registration block at the top of the file:
```diff
- 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()`.
**d)** Redirect production content loading in `createWindow()`:
```diff
if (IS_DEV) {
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:
```html
<meta http-equiv="Content-Security-Policy"
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' file:; script-src 'self' file:;
style-src 'self' 'unsafe-inline' file:;
connect-src 'self' https:;
img-src 'self' file: data: https:;
font-src 'self' file: data:;" />
font-src 'self' file: data:;"
/>
```
### Trade-off Analysis
| Dimension | Custom `app://` Protocol | `file://` + HashRouter |
|---|---|---|
| **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` |
| **Router Compatibility** | `BrowserRouter` β€” zero changes required | Must migrate to `HashRouter` |
| **Deep Linking** | Full, native-style support | Hash-based only |
| **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) |
| **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) |
| **Recovery Time** | β€” | ~15 minutes, 2 files |
| Dimension | Custom `app://` Protocol | `file://` + HashRouter |
| --------------------------------- | -------------------------------------------------- | ----------------------------------------------------------- |
| **Aesthetic Integrity** | Clean URLs: `/app/dashboard` | Hash prefix: `#/app/dashboard` |
| **Router Compatibility** | `BrowserRouter` β€” zero changes required | Must migrate to `HashRouter` |
| **Deep Linking** | Full, native-style support | Hash-based only |
| **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) |
| **Implementation Complexity** | Higher (custom protocol handler + security layers) | Lower (no custom protocol infrastructure) |
| **Recovery Time** | β€” | ~15 minutes, 2 files |
+76 -61
View File
@@ -1,3 +1,5 @@
[← Back to Root](../../../README.md)
# IPC Architecture & Security Model
The Secure Communication Blueprint.
@@ -23,44 +25,56 @@ The desktop wrapper enforces a **strict privilege separation** between three exe
### Trust Level Matrix
| 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 |
| **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 |
| 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 |
| **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 |
### Process Topology
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ MAIN PROCESS [Fully Trusted] β”‚
β”‚ β”‚
β”‚ UNRESTRICTED PRIVILEGES β”‚
β”‚ Filesystem Β· Network Β· Printers Β· Native APIs Β· Child Processes β”‚
β”‚ Auto-Updater Β· OS Integration Β· System Notifications β”‚
β”‚ β”‚
β”‚ ipcMain.handle('channel', handler) ← Command handlers β”‚
β”‚ ipcMain.on('channel', handler) ← Event listeners β”‚
β”‚ webContents.send('channel', data) ← Downstream push β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Non-Bypassable Isolation Boundary ──────────────────
β”‚ PRELOAD SCRIPT [Secure Gateway] β”‚
β”‚ β”‚
β”‚ HERMETICALLY SEALED CONTEXT β”‚
β”‚ Performs Interface Narrowing: transforms broad IPC capabilities β”‚
β”‚ into a minimal, auditable API surface. Acts as the sole β”‚
β”‚ authorized mediator between trusted and untrusted contexts. β”‚
β”‚ β”‚
β”‚ contextBridge.exposeInMainWorld('electronAPI', { ... }) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Non-Bypassable Isolation Boundary ──────────────────
β”‚ RENDERER [Zero-Trust Environment] β”‚
β”‚ β”‚
β”‚ UNTRUSTED WEB CONTENT β”‚
β”‚ Standard browser sandbox. Zero access to: require, __dirname, β”‚
β”‚ process, fs, child_process, net, os, ipcRenderer. β”‚
β”‚ β”‚
β”‚ ONLY authorized interaction vector: β”‚
β”‚ window.electronAPI.methodName(args) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```mermaid
graph TD
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
classDef trustedLayer fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#0f172a
classDef gatewayLayer fill:#f0fdf4,stroke:#10b981,stroke-width:2px,color:#064e3b
classDef untrustedLayer fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
classDef functionNode fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#0f172a
%% ─── Subgraphs ───
subgraph Main ["MAIN PROCESS [Fully Trusted]"]
M_DESC["Unrestricted Node.js Privileges"]
IPC_MAIN_H[ipcMain.handle]
IPC_MAIN_O[ipcMain.on]
end
subgraph Preload ["PRELOAD SCRIPT [Secure Gateway]"]
P_DESC["Hermetically Sealed Context (Interface Narrowing)"]
CTX_BRIDGE{contextBridge.exposeInMainWorld}
end
subgraph Renderer ["RENDERER [Zero-Trust Environment]"]
R_DESC["Standard Browser Sandbox (No Node.js APIs)"]
E_API([window.electronAPI])
end
%% ─── Flow & Relationships ───
E_API ===>|Only Authorized Vector| CTX_BRIDGE
CTX_BRIDGE --->|Transforms to narrow IPC calls| IPC_MAIN_H
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.
@@ -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**:
| 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. |
| `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. |
| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. |
| 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. |
| `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. |
| `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.
> [!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).
> [!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).
### Step 1: Register the Handler β€” Main Process
@@ -134,6 +147,7 @@ contextBridge.exposeInMainWorld('electronAPI', electronAPI);
```
**Non-negotiable rules:**
- **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.
- **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
| Channel | Direction | Pattern | Payload | Access Control |
|---|---|---|---|---|
| `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 |
| Channel | Direction | Pattern | Payload | Access Control |
| ------------------ | -------------------------- | ------------------- | --------------------------------------------------------------------- | ------------------------------ |
| `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 |
**Main process handler:** `setupPrinterIPC()` in `src/main/index.ts`
**Preload Gateway surface:**
```typescript
getPrinters: () => ipcRenderer.invoke('printer:get-list')
print: (options?) => ipcRenderer.invoke('printer:print', options)
getPrinters: () => ipcRenderer.invoke('printer:get-list');
print: (options?) => ipcRenderer.invoke('printer:print', options);
```
**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
| Channel | Direction | Pattern | Payload | Access Control |
|---|---|---|---|---|
| `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:checking` | Main β†’ Renderer | `send` | No payload | Status notification |
| `updater:available` | Main β†’ Renderer | `send` | `UpdateInfo { version, releaseDate, releaseNotes }` | Status notification |
| `updater:not-available` | Main β†’ Renderer | `send` | `UpdateInfo` | Status notification |
| `updater:progress` | Main β†’ Renderer | `send` | `ProgressInfo { percent, bytesPerSecond, transferred, total }` | Progress telemetry |
| `updater:downloaded` | Main β†’ Renderer | `send` | `UpdateInfo` | Status notification |
| `updater:error` | Main β†’ Renderer | `send` | Error message string | Error telemetry |
| Channel | Direction | Pattern | Payload | Access Control |
| ----------------------- | --------------- | ------------------- | -------------------------------------------------------------- | ---------------------------------- |
| `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:checking` | Main β†’ Renderer | `send` | No payload | Status notification |
| `updater:available` | Main β†’ Renderer | `send` | `UpdateInfo { version, releaseDate, releaseNotes }` | Status notification |
| `updater:not-available` | Main β†’ Renderer | `send` | `UpdateInfo` | Status notification |
| `updater:progress` | Main β†’ Renderer | `send` | `ProgressInfo { percent, bytesPerSecond, transferred, total }` | Progress telemetry |
| `updater:downloaded` | Main β†’ Renderer | `send` | `UpdateInfo` | Status notification |
| `updater:error` | Main β†’ Renderer | `send` | Error message string | Error telemetry |
**Main process handlers:** `setupAutoUpdaterIPC()` + `setupAutoUpdaterEvents()` in `src/main/index.ts`
@@ -283,7 +298,7 @@ contextBridge.exposeInMainWorld('ipc', ipcRenderer);
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**
@@ -294,7 +309,7 @@ contextBridge.exposeInMainWorld('require', require);
```typescript
// VIOLATION: Catastrophic Failure β€” Complete Boundary Collapse
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:*",
"@tailwindcss/vite": "^4.1.18",
"dayjs": "^1.11.19",
"events": "^3.3.0",
"i18next": "^24.2.2",
"lucide-react": "^1.17.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"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 {
Button,
@@ -18,10 +19,15 @@ import {
Table,
Badge,
Divider,
Tabs,
Box,
Paper,
} from '@repo/ui/components';
import { ShieldCheck, Database, Lock, Layout, Activity, Printer } from 'lucide-react';
import PrinterList from './printer-list';
import ExamplePage from './example/example.page';
import EventsDemoPage from './events-demo';
import PouchSample from './pouch-sample';
interface ShowcaseViewProps {
colorScheme: ColorSchemeType;
@@ -31,6 +37,8 @@ interface ShowcaseViewProps {
}
export default function ShowcaseView({ colorScheme, setColorScheme, density, setDensity }: ShowcaseViewProps) {
const [activeTab, setActiveTab] = useState<string | null>('ui-components');
// Mock data for the table
const tableData = [
{ 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' },
];
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 (
<Container size="lg" py="xl">
<Stack gap="xl">
<Title order={1}>Super App UI Showcase</Title>
<Box className="min-h-screen" style={{ backgroundColor: 'var(--mantine-color-body)' }}>
<Tabs
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>
{/* =========================================
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: string | null) => setColorScheme((val as ColorSchemeType) || 'light')}
data={[
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
]}
/>
<Select
label="Density (Spacing & Sizing)"
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.Tab value="ui-components" leftSection={<Layout size={18} />}>
UI Components
</Tabs.Tab>
<Tabs.Tab value="storage" leftSection={<Database size={18} />}>
Offline Storage
</Tabs.Tab>
<Tabs.Tab value="rbac" leftSection={<ShieldCheck size={18} />}>
RBAC Engine
</Tabs.Tab>
<Tabs.Tab value="auth" leftSection={<Lock size={18} />}>
Auth & Security
</Tabs.Tab>
<Tabs.Tab value="events" leftSection={<Activity size={18} />}>
Events
</Tabs.Tab>
<Tabs.Tab value="hardware" leftSection={<Printer size={18} />}>
Hardware
</Tabs.Tab>
</Tabs.List>
{/* =========================================
TAILWIND V4 BRIDGE TEST
========================================= */}
<Card withBorder shadow="sm" radius="md" p="md">
<Title order={4} mb="md">
Tailwind v4 Synchronization
</Title>
{/* This div purely uses Tailwind classes to prove it inherits Mantine's variables */}
<div className="bg-brand-500 text-brand-50 p-md rounded-md shadow-md text-base">
<span className="font-bold">Tailwind works!</span> The padding (p-md), border-radius (rounded-md), text size
(text-base), and background color of this box are entirely controlled by the ThemeProvider's current state.
</div>
</Card>
{/* =========================================
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>
<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" />
<Tabs.Panel value={activeTab as string}>
{/* Header */}
<Paper
p="md"
radius={0}
withBorder
style={{
borderTop: 0,
borderLeft: 0,
borderRight: 0,
zIndex: 10,
flexShrink: 0
}}
>
<Group justify="space-between">
<Stack gap={0}>
<Title order={3}>Architecture Showcase</Title>
<Text size="sm" c="dimmed">{getSubtitle()}</Text>
</Stack>
</Group>
</Paper>
<Group grow align="flex-start">
<NumberInput label="Age" placeholder="25" min={0} max={100} />
<PasswordInput label="Password" placeholder="Your secret password" withAsterisk />
</Group>
{/* Scrollable Content Area */}
<Box className="flex-1 overflow-y-auto p-6" style={{ height: 'calc(100vh - 80px)' }}>
<Container size="xl" m={0} p={0}>
{/* --- 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">
<Checkbox label="I agree to the terms and conditions" defaultChecked />
<Switch label="Enable notifications" defaultChecked />
</Group>
{/* Forms */}
<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 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>
<Group mt="xs">
<Radio value="react" label="React" />
<Radio value="svelte" label="Svelte" />
<Radio value="vue" label="Vue" />
</Group>
</Radio.Group>
</Stack>
</Card>
{/* Data Grid */}
<Card withBorder shadow="sm" radius="md" p="md">
<Title order={4} mb="md">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</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>
)}
{/* =========================================
DATA GRID / TABLE
========================================= */}
<Card withBorder shadow="sm" radius="md" p="md">
<Title order={4} mb="md">
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>
{/* --- STORAGE TAB --- */}
{activeTab === 'storage' && (
<Stack gap="xl">
<PouchSample />
</Stack>
)}
{/* =========================================
EVENT BUS SHOWCASE
========================================= */}
<EventsDemoPage />
</Stack>
</Container>
{/* --- RBAC TAB --- */}
{activeTab === 'rbac' && (
<Stack gap="xl">
<Card withBorder shadow="sm" radius="md" p="md">
<Title order={4} mb="md">RBAC Engine</Title>
<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,
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)
# 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.
**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
```mermaid
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)"]
WEB[apps/web]
LAND[apps/landing]
DESK[apps/desktop]
WEB([apps/web])
LAND([apps/landing])
DESK([apps/desktop])
end
subgraph Core ["@repo/core-api (Engine)"]
@@ -34,28 +41,31 @@ graph TD
end
end
WEB & LAND & DESK -->|instantiates| FACTORY
WEB & LAND & DESK -->|extends| COMMON
COMMON -->|executes via| FACTORY
%% ─── Flow & Relationships ───
WEB & LAND & DESK ===>|instantiates| FACTORY
WEB & LAND & DESK ===>|extends| COMMON
COMMON --->|executes via| FACTORY
FACTORY -.->|reports via| FARO
FACTORY -.->|throws| API_ERR
%% Styling Subgraphs (Backgrounds)
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
%% ─── Apply Styles ───
class WEB,LAND,DESK appEntity;
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)
style WEB fill:#339af0,stroke:#1864ab,color:#fff
style LAND fill:#339af0,stroke:#1864ab,color:#fff
style DESK fill:#339af0,stroke:#1864ab,color:#fff
%% 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
```
%% Nested subgraphs also need transparent backgrounds to prevent glaring white boxes in dark mode
style HTTP fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
style OBS fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
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
```
### Data Flow Lifecycle
@@ -65,24 +75,26 @@ Every HTTP request flows through this precise interceptor pipeline:
sequenceDiagram
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
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 H as HTTP Client
participant F as Faro Adapter
end
box #e7f5ff App Logic (IoC)
box rgba(16, 185, 129, 0.1) App Logic (IoC)
participant A as App Hooks
end
box #fff5f5 External
box rgba(245, 158, 11, 0.1) External
participant N as Network
end
%% ─── Execution Flow ───
C->>S: getMany()
S->>H: request()
H->>F: onRequestStart() (Log + Span)
+91 -49
View File
@@ -1,74 +1,116 @@
# @repo/core-events
[← 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.
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.
> 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.
---
## 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
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)"]
R["AppEventRegistry<br/>(empty interface)"]
T["AppEvents = mapped type"]
E((Event Bus<br/>mitt))
H[useAppEvent / usePublishEvent]
R --> T --> E
E --> H
R[AppEventRegistry Empty Interface]
T[AppEvents Mapped Type]
E((Global Event Bus mitt))
H[Hooks: useAppEvent / usePublishEvent]
end
subgraph Apps ["apps/web (App Autonomy)"]
D["events.d.ts<br/>declare module augmentation"]
A[Cashier UI]
B[Profile Settings]
C[WebSocket Client]
X[Electron IPC Bridge]
Y[IndexedDB Sync]
Z[Stock Grid Row]
D[[events.d.ts Declaration Merging]]
%% Publishers
A([Cashier UI])
B([Profile Settings])
C([WebSocket Client])
%% Subscribers
X([Electron IPC Bridge])
Y([IndexedDB Sync])
Z([Stock Grid Row])
end
D -. "merges into" .-> R
%% ─── Flow & Relationships ───
D -.->|Augments| R
R ---> T ---> E
E ---> H
A -- "DEVICE:PRINT_RECEIPT" --> E
B -- "AUTH:PROFILE_UPDATED" --> E
C -- "WS:STOCK_UPDATE" --> E
%% Emitting Events
A ===>|DEVICE:PRINT_RECEIPT| E
B ===>|AUTH:PROFILE_UPDATED| E
C ===>|WS:STOCK_UPDATE| E
E -.-> X
E -.-> Y
E -.-> Z
%% Subscribing to Events
E -.->|Triggers| X
E -.->|Triggers| Y
E -.->|Triggers| Z
%% Styling Subgraphs (Backgrounds)
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
%% ─── Apply Styles ───
class A,B,C,X,Y,Z appComponent;
class D injection;
class R,T registry;
class E,H coreBus;
%% Styling Core Engine (Purple) & Contracts (Green)
style R fill:#20c997,stroke:#089981,color:#fff
style T fill:#20c997,stroke:#089981,color:#fff
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
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
```
### 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)
+28 -27
View File
@@ -1,7 +1,7 @@
# Enterprise i18n Architecture (`@repo/core-i18n`)
[← Back to Root](../../README.md)
# i18n Architecture (`@repo/core-i18n`)
A highly decoupled, type-safe internationalization engine for the monorepo.
It uses a **Hybrid Namespace Strategy**:
@@ -16,49 +16,50 @@ This architecture strictly adheres to **Inversion of Control (IoC)**. The core e
```mermaid
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)"]
UI[React Components]
DICT[Feature Dictionaries<br/>e.g., booking.json]
UI([React Components])
DICT[[Feature Dictionaries: booking.json]]
end
subgraph Core ["@repo/core-i18n (Engine)"]
I18N((i18next Instance))
I18N[i18next Instance]
STORE[(core-storage)]
COMMON[Common Vocabulary]
end
subgraph Backend ["Backend API (External)"]
SYNC[Language Sync Endpoint]
TENANT[Tenant Config Endpoint]
SYNC([Language Sync Endpoint])
TENANT([Tenant Config Endpoint])
end
UI -->|uses useTranslation| I18N
%% ─── Flow & Relationships ───
UI ===>|uses useTranslation| I18N
DICT -.->|lazy loads| I18N
COMMON -->|preloads| I18N
I18N <-->|reads/persists| STORE
COMMON --->|preloads| I18N
I18N <===>|reads / persists| STORE
I18N -->|changeLanguage sync| SYNC
I18N --->|changeLanguage sync| SYNC
SYNC -.->|fails? rollback| I18N
TENANT -.->|applyTenantOverrides| I18N
%% Styling Subgraphs (Backgrounds)
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
style Backend fill:#fff4e6,stroke:#ffd8a8,stroke-width:2px,color:#d9480f
%% ─── Apply Styles ───
class UI,DICT appEntity;
class I18N coreEngine;
class STORE,COMMON dataStore;
class SYNC,TENANT externalAPI;
%% Styling App Nodes (Blue)
style UI fill:#339af0,stroke:#1864ab,color:#fff
style DICT fill:#339af0,stroke:#1864ab,color:#fff
%% 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
%% ─── 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
style Backend fill:transparent,stroke:#f59e0b,stroke-width:2px,stroke-dasharray: 5 5
```
---
+211 -130
View File
@@ -1,168 +1,249 @@
# Enterprise Storage Engine (`@repo/core-storage`)
[← 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
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)"]
REG[[AppStorageKey & App Registries]]
UI[React Components / API Interceptors]
REG[[AppStorageKey Config]]
UI([React Components / API])
INST{{Storage Instances}}
end
subgraph Core ["@repo/core-storage (Engine Factories)"]
subgraph Core ["@repo/core-storage"]
FAC[Factory: createStorage]
API[IStorageService API]
FAC[createLocalStorage / createIndexedDB]
VAL{Runtime Gatekeeper}
ERR>Throws Security Exception]
ENC{{AES Encryption Pipeline}}
LOCAL[LocalStorage Adapter]
IDB[IndexedDB Adapter]
end
subgraph Browser ["Browser APIs (Native)"]
B_LOCAL[(localStorage)]
B_IDB[(IndexedDB)]
LOCAL[(LocalStorage Adapter)]
IDB[(IndexedDB Adapter)]
end
%% ─── Flow & Relationships ───
%% 1. Initialization Flow
REG -.->|Injects Keys & Config| FAC
FAC --> INST
UI -->|getItem / setItem| INST
INST --> API
API --> VAL
FAC -.->|Returns| INST
VAL -.->|Valid Key?| ENC
VAL -.->|Invalid Key!| ERR[Throws Security Exception]
%% 2. Runtime Execution Flow
UI ===>|getItem / setItem| INST
INST ---> API
API ---> VAL
ENC -.->|Sensitive Key| LOCAL & IDB
VAL -.->|Plain-text Key| LOCAL & IDB
%% 3. Gatekeeper Decision Tree
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
IDB <--> B_IDB
%% ─── Apply Styles ───
class REG,UI,INST appEntity;
class FAC,API coreEntity;
class VAL gatekeeper;
class ENC encrypt;
class ERR error;
class LOCAL,IDB database;
%% Styling Subgraphs
style Apps fill:#e7f5ff,stroke:#74c0fc,stroke-width:2px,color:#1864ab
style Core fill:#f8f9fa,stroke:#ced4da,stroke-width:2px,color:#495057
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
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
style Apps fill:transparent,stroke:#818cf8,stroke-width:2px,stroke-dasharray: 5 5
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
```
---
## 🎯 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.
### Usage & Implementation
```typescript
// apps/web/src/core/storage/index.ts
import { createLocalStorage, createIndexedDB } from '@repo/core-storage';
// 1. Define Keys
export const AppStorageKey = {
USER_PROFILE: 'user_profile',
ACCESS_TOKEN: 'access_token',
LOCALE: 'app_locale',
} as const;
// 1. Define allowed keys (Strict Type Safety)
export type AppStorageKey = 'THEME' | 'ACCESS_TOKEN' | 'OFFLINE_CACHE';
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
// 2. Classify Keys
export const ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
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
// 2. Instantiate Local Storage
export const appStorage = createLocalStorage<AppStorageKey>({
plainTextKeys: new Set(['THEME']),
encryptedKeys: new Set(['ACCESS_TOKEN']), // Auto AES encrypted
});
export const secureIndexedDB = createIndexedDB<AppStorageKeyValue>({
dbName: 'eigen_erp_db',
storeName: 'web_store',
encryptedKeys: ENCRYPTED_KEYS,
plainTextKeys: PLAIN_KEYS
});
// 3. Usage
await appStorage.setItem('ACCESS_TOKEN', 'ey...'); // Encrypted on disk
const theme = await appStorage.getItem('THEME'); // Plaintext on disk
```
### 2. Usage in App Components
### βœ… Do's and ❌ Don'ts
Now, you can import your locally-created instances anywhere in your app.
```typescript
import { secureStorage, AppStorageKey } from '@/core/storage';
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');
```
* **βœ… 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.
* **❌ DON'T use native `window.localStorage` directly** anywhere in your React components. It bypasses our encryption and gatekeeper logic.
* **❌ DON'T mix domain data.** Keep UI preferences (Theme, Sidebar state) in LocalStorage, and large datasets (Offline Caches) in IndexedDB.
---
> [!WARNING]
> **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.
## πŸ”„ Offline-First Document Storage (PouchDB & CouchDB)
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"
},
"dependencies": {
"@repo/utils": "workspace:*"
"@repo/utils": "workspace:*",
"pouchdb-browser": "^9.0.0",
"pouchdb-find": "^9.0.0"
},
"devDependencies": {
"@repo/eslint-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",
"pouchdb-adapter-memory": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-mapreduce": "^9.0.0",
"typescript": "5.5.4",
"vitest": "^4.0.17"
}
+2
View File
@@ -2,7 +2,9 @@
export type { IStorageService } from './storage.interface';
export type { StorageOptions } from './local-storage.service';
export type { IndexedDBConfig } from './indexed-db.service';
export type { PouchConfig } from './pouch';
// ─── Service Classes ────────────────────────────────────────────
export { LocalStorageService, createLocalStorage } from './local-storage.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 () => {
const result = await storage.getItem<string>('nonexistent' as TestStorageKeyValue);
const result = await storage.getItem<string>(TestStorageKey.THEME);
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:
specifier: ^1.11.19
version: 1.11.19
events:
specifier: ^3.3.0
version: 3.3.0
i18next:
specifier: ^24.2.2
version: 24.2.3(typescript@5.5.4)
lucide-react:
specifier: ^1.17.0
version: 1.17.0(react@19.2.3)
react:
specifier: ^19.2.3
version: 19.2.3
@@ -379,6 +385,12 @@ importers:
'@repo/utils':
specifier: workspace:*
version: link:../utils
pouchdb-browser:
specifier: ^9.0.0
version: 9.0.0
pouchdb-find:
specifier: ^9.0.0
version: 9.0.0
devDependencies:
'@repo/eslint-config':
specifier: workspace:*
@@ -386,9 +398,36 @@ importers:
'@repo/typescript-config':
specifier: workspace:*
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:
specifier: ^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:
specifier: 5.5.4
version: 5.5.4
@@ -3125,6 +3164,135 @@ packages:
dev: 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):
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -3803,6 +3971,25 @@ packages:
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
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):
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
peerDependencies:
@@ -4902,6 +5089,15 @@ packages:
engines: {node: '>=10'}
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:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -5053,6 +5249,10 @@ packages:
engines: {node: '>=12'}
dev: true
/double-ended-queue@2.1.0-0:
resolution: {integrity: sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==}
dev: true
/dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -5217,6 +5417,13 @@ packages:
/err-code@2.0.3:
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:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
dependencies:
@@ -5934,6 +6141,11 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
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:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@@ -6025,6 +6237,12 @@ packages:
dependencies:
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:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -6175,6 +6393,10 @@ packages:
is-callable: 1.2.7
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:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: false
@@ -6561,6 +6783,10 @@ packages:
engines: {node: '>= 4'}
dev: false
/immediate@3.3.0:
resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==}
dev: true
/import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -6911,6 +7137,10 @@ packages:
is-docker: 2.2.1
dev: true
/isarray@0.0.1:
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
dev: true
/isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: true
@@ -7126,6 +7356,56 @@ packages:
readable-stream: 2.3.8
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:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -7353,6 +7633,18 @@ packages:
engines: {node: '>=12'}
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:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -7515,6 +7807,18 @@ packages:
'@types/mdast': 4.0.4
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:
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
dependencies:
@@ -8002,6 +8306,17 @@ packages:
semver: 7.7.1
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:
resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==}
engines: {node: ^12.13 || ^14.13 || >=16}
@@ -8447,6 +8762,163 @@ packages:
source-map-js: 1.2.1
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:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -8543,6 +9015,15 @@ packages:
engines: {node: '>=10'}
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:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
dependencies:
@@ -8554,6 +9035,9 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
/querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -8791,6 +9275,15 @@ packages:
type-fest: 0.6.0
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:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
@@ -8933,6 +9426,9 @@ packages:
engines: {node: '>=0.10.5'}
dev: false
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
/resedit@1.7.2:
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
engines: {node: '>=12', npm: '>=6'}
@@ -9227,7 +9723,6 @@ packages:
/set-cookie-parser@2.7.2:
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
dev: false
/set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
@@ -9402,6 +9897,9 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/spark-md5@3.0.2:
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
/spdx-correct@3.2.0:
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
dependencies:
@@ -9571,6 +10069,10 @@ packages:
es-object-atoms: 1.1.1
dev: false
/string_decoder@0.10.31:
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
dev: true
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
@@ -9631,6 +10133,14 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
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:
resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==}
engines: {node: '>= 8.0'}
@@ -9729,6 +10239,13 @@ packages:
/text-table@0.2.0:
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:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
dev: true
@@ -9787,6 +10304,15 @@ packages:
is-number: 7.0.0
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:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
@@ -9794,6 +10320,9 @@ packages:
tldts: 6.1.86
dev: true
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
/tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
@@ -10137,6 +10666,10 @@ packages:
engines: {node: '>= 4.0.0'}
dev: true
/universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
/universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -10198,6 +10731,12 @@ packages:
dependencies:
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):
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
@@ -10286,6 +10825,10 @@ packages:
which-typed-array: 1.1.19
dev: true
/uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
/uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@@ -10602,6 +11145,9 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/vuvuzela@1.0.3:
resolution: {integrity: sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ==}
/w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
@@ -10623,6 +11169,9 @@ packages:
resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
/webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -10653,6 +11202,12 @@ packages:
webidl-conversions: 7.0.0
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:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -10792,6 +11347,11 @@ packages:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
dev: true
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
dev: true
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}