Merge pull request 'feat/core-function' (#12) from feat/core-function into main
Reviewed-on: eigen/fe-monorepo-template#12
This commit is contained in:
@@ -1,56 +1,56 @@
|
||||
# Auto-Update System
|
||||
[β Back to Root](../../../README.md)
|
||||
|
||||
The Unified Update Lifecycle.
|
||||
# Desktop Auto-Update System
|
||||
|
||||
> This document defines the strategic implementation of our cross-platform auto-update system. Powered by **electron-updater**, this architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
|
||||
`apps/desktop` utilizes a unified update lifecycle powered by **electron-updater**. This architecture ensures seamless, background delivery of application patches, maintaining version parity across our global user base.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Reactive Update Flow](#reactive-update-flow)
|
||||
- [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. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
VITE_CMS_API_URL=http://localhost:8001/cms
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Type-safe Environment Wrapper for apps/landing.
|
||||
*/
|
||||
export const ENV = {
|
||||
CMS_API_URL: import.meta.env.VITE_CMS_API_URL || 'http://localhost:8001/cms',
|
||||
} as const;
|
||||
@@ -0,0 +1,5 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000/api
|
||||
VITE_APP_ENV=development
|
||||
VITE_COUCHDB_BASE_URL=http://202.146.229.134:7700
|
||||
VITE_COUCHDB_USERNAME=root
|
||||
VITE_COUCHDB_PASSWORD=password
|
||||
@@ -21,7 +21,9 @@
|
||||
"@repo/utils": "workspace:*",
|
||||
"@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",
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Title,
|
||||
Text,
|
||||
Table,
|
||||
Badge,
|
||||
} from '@repo/ui/components';
|
||||
import { itemDB, posConfigDB } from '../../core/db';
|
||||
import type { Item, POSConfiguration } from '../../core/db/types';
|
||||
|
||||
export default function PouchSample() {
|
||||
const [configs, setConfigs] = useState<POSConfiguration[]>([]);
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
|
||||
// Load initial data
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const allConfigs = await posConfigDB.find({ selector: {} });
|
||||
setConfigs(allConfigs);
|
||||
|
||||
const allItems = await itemDB.find({ selector: {} });
|
||||
setItems(allItems);
|
||||
console.log({allConfigs, allItems})
|
||||
} catch (err) {
|
||||
console.error('Failed to load PouchDB data', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Initial Data Load
|
||||
loadData();
|
||||
|
||||
// 2. Setup Real-Time Listeners
|
||||
const unsubscribeItems = itemDB.onChange(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const unsubscribePos = posConfigDB.onChange(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 3. CRITICAL: Cleanup to prevent memory leaks on unmount
|
||||
return () => {
|
||||
unsubscribeItems();
|
||||
unsubscribePos();
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
// βββ POS Configuration Handlers βββββββββββββββββββββββββββββββββ
|
||||
|
||||
const handleSeedConfig = async () => {
|
||||
try {
|
||||
const id = `pos-${Date.now()}`;
|
||||
await posConfigDB.create({
|
||||
_id: id,
|
||||
pos_number: '1111111111666',
|
||||
pos_name: 'Premium Test POS TESTING COUNCH',
|
||||
items: items, // mapping current items
|
||||
payment_methods: [{ id: 'cash', name: 'Cash' }],
|
||||
});
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to seed config', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfig = async (id: string) => {
|
||||
try {
|
||||
await posConfigDB.delete(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete config', err);
|
||||
}
|
||||
};
|
||||
|
||||
// βββ Items Inventory Handlers βββββββββββββββββββββββββββββββββββ
|
||||
|
||||
const handleAddItem = async () => {
|
||||
try {
|
||||
const id = `item-${Date.now()}`;
|
||||
await itemDB.create({
|
||||
_id: id,
|
||||
name: 'PLAYGROUND ALL DAY TESTING POUCH',
|
||||
base_price: '75000',
|
||||
item_type: 'wahana',
|
||||
usage_type: 'ticket',
|
||||
item_category: [{ name: 'Entertainment' }],
|
||||
item_rates: [
|
||||
{ season_period: 'weekday', price: 50000 },
|
||||
{ season_period: 'weekend', price: 75000 },
|
||||
],
|
||||
});
|
||||
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to add item', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (id: string) => {
|
||||
try {
|
||||
await itemDB.delete(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete item', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAll = async () => {
|
||||
try {
|
||||
await posConfigDB.cleanAllData();
|
||||
await itemDB.cleanAllData();
|
||||
loadData();
|
||||
} catch (err) {
|
||||
console.error('Failed to clear data', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group justify="space-between">
|
||||
<Title order={2}>Enterprise PouchDB Sync</Title>
|
||||
<Button color="error" variant="outline" onClick={handleClearAll}>
|
||||
Clear All Local Data
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Items Inventory Table */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Items Database</Title>
|
||||
<Button onClick={handleAddItem} color="success">
|
||||
Inject Mock ERP Item
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Name</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Type</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Base Price</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Rates Count</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<Table.Tr key={item._id}>
|
||||
<Table.Td>{item._id}</Table.Td>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="brand" variant="light">
|
||||
{item.item_type}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>${Number(item.base_price).toFixed(2)}</Table.Td>
|
||||
<Table.Td>{item.item_rates?.length || 0}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteItem(item._id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={6} align="center">
|
||||
<Text c="dimmed">No items found.</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* POS Configuration Table */}
|
||||
<Card withBorder shadow="sm" radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>POS Configurations</Title>
|
||||
<Button onClick={handleSeedConfig} variant="light" color="brand">
|
||||
Inject Mock POS Config
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto border border-gray-200 rounded-lg scrollbar-thin scrollbar-thumb-gray-300">
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">ID</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Name</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">POS Number</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Mapped Items</Table.Th>
|
||||
<Table.Th className="sticky top-0 bg-white z-10 shadow-sm">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{configs.length > 0 ? (
|
||||
configs.map((cfg) => (
|
||||
<Table.Tr key={cfg._id}>
|
||||
<Table.Td>{cfg._id}</Table.Td>
|
||||
<Table.Td>{cfg.pos_name}</Table.Td>
|
||||
<Table.Td>{cfg.pos_number}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="info" variant="outline">
|
||||
{cfg.items?.length || 0} Items
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Button size="xs" color="error" variant="subtle" onClick={() => handleDeleteConfig(cfg._id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5} align="center">
|
||||
<Text c="dimmed">No configurations found.</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { ColorSchemeType, DensityType } from '@repo/ui/provider';
|
||||
import {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Multi-Database PouchDB Configuration for apps/web.
|
||||
*
|
||||
* This module demonstrates the IoC pattern: the consuming app decides
|
||||
* which databases to create and where they sync to. The core engine
|
||||
* (`PouchDatabaseManager`) has zero knowledge of business domains.
|
||||
*/
|
||||
import { PouchDatabaseManager } from '@repo/core-storage';
|
||||
import { ENV } from '../../environment/env';
|
||||
import type { Item, POSConfiguration } from './types';
|
||||
|
||||
// βββ Manager Singleton ββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
export const dbManager = new PouchDatabaseManager();
|
||||
|
||||
// βββ Helper: Build Secure Remote URL ββββββββββββββββββββββββββββ
|
||||
|
||||
function buildRemoteUrl(dbName: string): string | undefined {
|
||||
const { COUCHDB_BASE_URL, COUCHDB_USERNAME, COUCHDB_PASSWORD } = ENV;
|
||||
|
||||
if (!COUCHDB_BASE_URL || !COUCHDB_USERNAME || !COUCHDB_PASSWORD) {
|
||||
console.warn(`[DB Config] CouchDB credentials missing β "${dbName}" will run in offline-only mode.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 1. Tambahkan http:// secara otomatis jika DevOps hanya mengisi IP Address di .env
|
||||
const safeBaseUrl = COUCHDB_BASE_URL.startsWith('http')
|
||||
? COUCHDB_BASE_URL
|
||||
: `http://${COUCHDB_BASE_URL}`;
|
||||
|
||||
try {
|
||||
// 2. Gunakan URL parser yang aman dari karakter aneh pada password
|
||||
const url = new URL(safeBaseUrl);
|
||||
url.username = encodeURIComponent(COUCHDB_USERNAME);
|
||||
url.password = encodeURIComponent(COUCHDB_PASSWORD);
|
||||
url.pathname = `/${dbName}`;
|
||||
|
||||
return url.toString();
|
||||
} catch (error) {
|
||||
console.error(`[DB Config] Invalid URL format for CouchDB:`, safeBaseUrl);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// βββ Register Application Databases βββββββββββββββββββββββββββββ
|
||||
|
||||
/** POS Configuration database β stores device settings, theme, etc. */
|
||||
export const posConfigDB = dbManager.register<POSConfiguration>({
|
||||
localName: 'pos_configuration',
|
||||
remoteUrl: buildRemoteUrl('pos_configuration'),
|
||||
});
|
||||
|
||||
/** Items database β products available for sale in POS. */
|
||||
export const itemDB = dbManager.register<Item>({
|
||||
localName: 'item',
|
||||
remoteUrl: buildRemoteUrl('item'),
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Enterprise ERP Data Domain Models
|
||||
* These types reflect the actual schema of the underlying CouchDB instances.
|
||||
*/
|
||||
|
||||
export interface ItemRate {
|
||||
season_period?: string | null;
|
||||
price: string | number;
|
||||
}
|
||||
|
||||
export interface ItemCategory {
|
||||
_id?: string;
|
||||
name?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a sellable product or service.
|
||||
*/
|
||||
export interface Item {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
name: string;
|
||||
base_price: string | number;
|
||||
item_type: string;
|
||||
usage_type?: string;
|
||||
item_category?: ItemCategory[] | ItemCategory | string;
|
||||
item_rates?: ItemRate[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the configuration and assigned data for a specific Point of Sale terminal.
|
||||
*/
|
||||
export interface POSConfiguration {
|
||||
_id: string;
|
||||
_rev?: string;
|
||||
pos_number: string;
|
||||
pos_name: string;
|
||||
items: Item[];
|
||||
payment_methods?: any[];
|
||||
|
||||
// Allow for other ERP-specific fields
|
||||
[key: string]: any;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Type-safe Environment Wrapper for apps/web.
|
||||
* DO NOT use `import.meta.env` directly in components. Import this `ENV` object instead.
|
||||
*/
|
||||
export const ENV = {
|
||||
API_BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
|
||||
APP_ENV: (import.meta.env.VITE_APP_ENV || 'development') as 'development' | 'staging' | 'production',
|
||||
IS_PROD: import.meta.env.VITE_APP_ENV === 'production',
|
||||
|
||||
// CouchDB Connection
|
||||
COUCHDB_BASE_URL: import.meta.env.VITE_COUCHDB_BASE_URL || 'http://localhost:5984',
|
||||
COUCHDB_USERNAME: import.meta.env.VITE_COUCHDB_USERNAME || '',
|
||||
COUCHDB_PASSWORD: import.meta.env.VITE_COUCHDB_PASSWORD || '',
|
||||
} as const;
|
||||
@@ -8,4 +8,14 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
strictPort: true, // Fail if 5173 is in use. Electron NEEDS this exact port.
|
||||
},
|
||||
define: {
|
||||
// Crucial for PouchDB to not crash in the browser
|
||||
global: 'window',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// Force Vite to use the installed npm package for 'events'
|
||||
events: 'events',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+40
-28
@@ -1,7 +1,7 @@
|
||||
# Enterprise API Engine (`@repo/core-api`)
|
||||
|
||||
[β Back to Root](../../README.md)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
@@ -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,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';
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import PouchDB from 'pouchdb-browser';
|
||||
import PouchDBFind from 'pouchdb-find';
|
||||
|
||||
// Register the find plugin globally
|
||||
PouchDB.plugin(PouchDBFind);
|
||||
|
||||
// βββ Configuration Interface ββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
/**
|
||||
* Configuration for creating a new PouchDB database instance.
|
||||
* Follows Inversion of Control β the consuming app decides names and remote URLs.
|
||||
*/
|
||||
export interface PouchConfig {
|
||||
/** Name of the local PouchDB database (stored in IndexedDB by the browser). */
|
||||
localName: string;
|
||||
/**
|
||||
* Optional remote CouchDB URL for bi-directional live sync.
|
||||
* Should include credentials if authentication is required.
|
||||
* Example: `http://user:password@host:port/db_name`
|
||||
*/
|
||||
remoteUrl?: string;
|
||||
}
|
||||
|
||||
// βββ PouchDatabaseWrapper βββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
/**
|
||||
* Object-Oriented wrapper around a single PouchDB instance.
|
||||
* Provides strictly-typed CRUD + query helpers so developers never interact
|
||||
* with the raw PouchDB API or pass `dbName` repeatedly.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const itemDB = dbManager.register({ localName: 'items' });
|
||||
* await itemDB.create({ _id: 'item-001', name: 'Widget', price: 9.99 });
|
||||
* const results = await itemDB.find({ selector: { price: { $gt: 5 } } });
|
||||
* ```
|
||||
*/
|
||||
export class PouchDatabaseWrapper<DefaultType extends object = any> {
|
||||
/** The underlying raw PouchDB instance (escape hatch for advanced usage). */
|
||||
readonly raw: PouchDB.Database;
|
||||
|
||||
private syncHandler: PouchDB.Replication.Sync<object> | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
private changesFeed: PouchDB.Core.Changes<object> | null = null;
|
||||
|
||||
constructor(config: PouchConfig) {
|
||||
this.raw = new PouchDB(config.localName);
|
||||
|
||||
// Set up bi-directional live sync if a remote URL is provided
|
||||
if (config.remoteUrl) {
|
||||
this.syncHandler = this.raw.sync(config.remoteUrl, {
|
||||
live: true,
|
||||
retry: true,
|
||||
});
|
||||
|
||||
// Fault-tolerant error handling β prevents app crashes when CouchDB is unreachable
|
||||
this.syncHandler.on('error', (err: unknown) => {
|
||||
console.warn(`[PouchDB Sync] Error on "${config.localName}":`, err);
|
||||
});
|
||||
|
||||
this.syncHandler.on('paused', (info: unknown) => {
|
||||
if (info) {
|
||||
console.warn(`[PouchDB Sync] Paused on "${config.localName}":`, info);
|
||||
}
|
||||
});
|
||||
|
||||
this.syncHandler.on('denied', (err: unknown) => {
|
||||
console.warn(`[PouchDB Sync] Denied on "${config.localName}":`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// βββ CRUD Operations ββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
/**
|
||||
* Create a new document. If `_id` is not provided in data, PouchDB generates one.
|
||||
*/
|
||||
async create<T extends object = DefaultType>(data: T): Promise<PouchDB.Core.Response> {
|
||||
return this.raw.put(data as PouchDB.Core.Document<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing document by ID.
|
||||
* Automatically fetches the latest `_rev` to prevent conflict errors.
|
||||
*/
|
||||
async update<T extends object = DefaultType>(id: string, data: Partial<T>): Promise<PouchDB.Core.Response> {
|
||||
const existing = await this.raw.get(id);
|
||||
const merged = { ...existing, ...data };
|
||||
return this.raw.put(merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document by ID.
|
||||
* Automatically fetches the latest `_rev` before removal.
|
||||
*/
|
||||
async delete(id: string): Promise<PouchDB.Core.Response> {
|
||||
const doc = await this.raw.get(id);
|
||||
return this.raw.remove(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single document by ID.
|
||||
*/
|
||||
async getOne<T = DefaultType>(id: string): Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta> {
|
||||
return this.raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all documents from the database.
|
||||
* Returns a clean array of document objects (excludes PouchDB design docs).
|
||||
*/
|
||||
async getAll<T = DefaultType>(): Promise<T[]> {
|
||||
const result = await this.raw.allDocs({ include_docs: true });
|
||||
return result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => row.doc as unknown as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve multiple documents by their IDs.
|
||||
* Returns a clean array of found documents (silently skips missing/errored entries).
|
||||
*/
|
||||
async getSome<T = DefaultType>(ids: string[]): Promise<T[]> {
|
||||
const result = await this.raw.allDocs({ keys: ids, include_docs: true });
|
||||
return result.rows
|
||||
.filter((row): row is PouchDB.Core.AllDocsResponse<object>['rows'][number] => !('error' in row) && !!(row as any).doc)
|
||||
.map((row) => (row as any).doc as T);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query documents using MongoDB-style selectors (powered by `pouchdb-find`).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const electronics = await itemDB.find({ selector: { category: 'electronics' } });
|
||||
* const expensive = await itemDB.find({ selector: { price: { $gt: 100 } }, limit: 10 });
|
||||
* ```
|
||||
*/
|
||||
async find<T extends object = DefaultType>(options: PouchDB.Find.FindRequest<T>): Promise<T[]> {
|
||||
const result = await this.raw.find(options as PouchDB.Find.FindRequest<object>);
|
||||
return result.docs as unknown as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all documents from the database while keeping the database itself intact.
|
||||
* Useful for "clear cache" or "reset local data" flows.
|
||||
*/
|
||||
async cleanAllData(): Promise<void> {
|
||||
const result = await this.raw.allDocs();
|
||||
const deletions = result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => ({
|
||||
_id: row.id,
|
||||
_rev: row.value.rev,
|
||||
_deleted: true as const,
|
||||
}));
|
||||
|
||||
if (deletions.length > 0) {
|
||||
await this.raw.bulkDocs(deletions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any active sync and completely destroy the local database.
|
||||
* After calling this, the wrapper instance should not be used again.
|
||||
*/
|
||||
async destroy(): Promise<void> {
|
||||
if (this.syncHandler) {
|
||||
this.syncHandler.cancel();
|
||||
}
|
||||
if (this.changesFeed) {
|
||||
this.changesFeed.cancel();
|
||||
}
|
||||
this.listeners.clear();
|
||||
await this.raw.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the live sync connection (if active) without destroying the database.
|
||||
*/
|
||||
cancelSync(): void {
|
||||
if (this.syncHandler) {
|
||||
this.syncHandler.cancel();
|
||||
this.syncHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to real-time changes in the database.
|
||||
* Returns an unsubscribe function.
|
||||
*/
|
||||
onChange(callback: () => void): () => void {
|
||||
this.listeners.add(callback);
|
||||
|
||||
if (!this.changesFeed) {
|
||||
this.changesFeed = this.raw.changes({
|
||||
since: 'now',
|
||||
live: true,
|
||||
include_docs: true
|
||||
}).on('change', () => {
|
||||
this.listeners.forEach(cb => cb());
|
||||
}).on('error', (err) => {
|
||||
console.warn(`[PouchDB Listener Error]`, err);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.listeners.delete(callback);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// βββ PouchDatabaseManager βββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
/**
|
||||
* IoC Factory for managing multiple PouchDB database instances with optional
|
||||
* bi-directional CouchDB synchronization.
|
||||
*
|
||||
* `register()` returns a `PouchDatabaseWrapper` with full CRUD + query helpers,
|
||||
* so developers never need to pass `dbName` into each operation.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const dbManager = new PouchDatabaseManager();
|
||||
*
|
||||
* const itemDB = dbManager.register({ localName: 'items' });
|
||||
* await itemDB.create({ _id: 'item-001', name: 'Widget', price: 9.99 });
|
||||
*
|
||||
* const results = await itemDB.find({ selector: { price: { $gt: 5 } } });
|
||||
* console.log(results); // [{ _id: 'item-001', name: 'Widget', price: 9.99, ... }]
|
||||
*
|
||||
* const salesDB = dbManager.register({
|
||||
* localName: 'sales',
|
||||
* remoteUrl: 'http://admin:pass@localhost:5984/sales_db',
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class PouchDatabaseManager {
|
||||
private databases = new Map<string, PouchDatabaseWrapper<any>>();
|
||||
|
||||
/**
|
||||
* Register and initialize a new PouchDB database.
|
||||
* If a database with the same `localName` already exists, returns the existing wrapper.
|
||||
*
|
||||
* @param config - Configuration for the database instance.
|
||||
* @returns A `PouchDatabaseWrapper` with CRUD + query helpers.
|
||||
*/
|
||||
register<T extends object = any>(config: PouchConfig): PouchDatabaseWrapper<T> {
|
||||
if (this.databases.has(config.localName)) {
|
||||
return this.databases.get(config.localName) as PouchDatabaseWrapper<T>;
|
||||
}
|
||||
|
||||
const wrapper = new PouchDatabaseWrapper<T>(config);
|
||||
this.databases.set(config.localName, wrapper);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a previously registered database wrapper by its local name.
|
||||
*/
|
||||
get<T extends object = any>(localName: string): PouchDatabaseWrapper<T> | undefined {
|
||||
return this.databases.get(localName) as PouchDatabaseWrapper<T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a specific registered database and remove it from the manager.
|
||||
*/
|
||||
async destroy(localName: string): Promise<void> {
|
||||
const wrapper = this.databases.get(localName);
|
||||
if (!wrapper) return;
|
||||
await wrapper.destroy();
|
||||
this.databases.delete(localName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all managed databases. Useful for logout/cleanup scenarios.
|
||||
*/
|
||||
async destroyAll(): Promise<void> {
|
||||
const names = Array.from(this.databases.keys());
|
||||
await Promise.all(names.map((name) => this.destroy(name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* List all currently registered database names.
|
||||
*/
|
||||
listDatabases(): string[] {
|
||||
return Array.from(this.databases.keys());
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ describe('LocalStorageService', () => {
|
||||
});
|
||||
|
||||
it('returns null for non-existent keys', async () => {
|
||||
const result = await storage.getItem<string>('nonexistent' as TestStorageKeyValue);
|
||||
const result = await storage.getItem<string>(TestStorageKey.THEME);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import PouchDB from 'pouchdb-core';
|
||||
import PouchDBAdapterMemory from 'pouchdb-adapter-memory';
|
||||
import PouchDBFind from 'pouchdb-find';
|
||||
import PouchDBMapReduce from 'pouchdb-mapreduce';
|
||||
|
||||
// Build a minimal PouchDB for testing: core + memory adapter + find
|
||||
PouchDB.plugin(PouchDBAdapterMemory);
|
||||
PouchDB.plugin(PouchDBFind);
|
||||
PouchDB.plugin(PouchDBMapReduce);
|
||||
|
||||
/**
|
||||
* Since the tests run in Node (not a browser), we cannot use PouchDatabaseManager
|
||||
* directly because it imports `pouchdb-browser` which requires `self`.
|
||||
* Instead, we test the CRUD logic by creating a lightweight test wrapper
|
||||
* that mirrors PouchDatabaseWrapper's methods using the memory-backed PouchDB.
|
||||
*/
|
||||
|
||||
function createTestDB(name: string) {
|
||||
const raw = new PouchDB(name, { adapter: 'memory' });
|
||||
|
||||
return {
|
||||
raw,
|
||||
|
||||
async create<T extends object>(data: T) {
|
||||
return raw.put(data as PouchDB.Core.Document<T>);
|
||||
},
|
||||
|
||||
async update<T extends object>(id: string, data: Partial<T>) {
|
||||
const existing = await raw.get(id);
|
||||
const merged = { ...existing, ...data };
|
||||
return raw.put(merged);
|
||||
},
|
||||
|
||||
async delete(id: string) {
|
||||
const doc = await raw.get(id);
|
||||
return raw.remove(doc);
|
||||
},
|
||||
|
||||
async getOne<T>(id: string) {
|
||||
return raw.get<T>(id) as Promise<T & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta>;
|
||||
},
|
||||
|
||||
async getAll<T>() {
|
||||
const result = await raw.allDocs({ include_docs: true });
|
||||
return result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => row.doc as unknown as T);
|
||||
},
|
||||
|
||||
async getSome<T>(ids: string[]) {
|
||||
const result = await raw.allDocs({ keys: ids, include_docs: true });
|
||||
return result.rows
|
||||
.filter((row): row is any => !('error' in row) && !!(row as any).doc)
|
||||
.map((row: any) => row.doc as T);
|
||||
},
|
||||
|
||||
async find<T extends object>(options: PouchDB.Find.FindRequest<T>) {
|
||||
const result = await raw.find(options as PouchDB.Find.FindRequest<object>);
|
||||
return result.docs as unknown as T[];
|
||||
},
|
||||
|
||||
async cleanAllData() {
|
||||
const result = await raw.allDocs();
|
||||
const deletions = result.rows
|
||||
.filter((row) => !row.id.startsWith('_design/'))
|
||||
.map((row) => ({
|
||||
_id: row.id,
|
||||
_rev: row.value.rev,
|
||||
_deleted: true as const,
|
||||
}));
|
||||
if (deletions.length > 0) {
|
||||
await raw.bulkDocs(deletions);
|
||||
}
|
||||
},
|
||||
|
||||
async destroy() {
|
||||
await raw.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('PouchDatabaseWrapper CRUD Operations', () => {
|
||||
let db: ReturnType<typeof createTestDB>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Use a unique name per test to avoid cross-contamination
|
||||
db = createTestDB(`test_db_${Date.now()}_${Math.random().toString(36).slice(2)}`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await db.destroy();
|
||||
} catch {
|
||||
// Already destroyed in some tests
|
||||
}
|
||||
});
|
||||
|
||||
// βββ create βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a document with a given _id', async () => {
|
||||
const res = await db.create({ _id: 'doc-001', name: 'Alice', age: 30 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.id).toBe('doc-001');
|
||||
});
|
||||
|
||||
it('should throw a conflict if creating with a duplicate _id', async () => {
|
||||
await db.create({ _id: 'dup-001', name: 'First' });
|
||||
await expect(db.create({ _id: 'dup-001', name: 'Second' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// βββ getOne βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
describe('getOne', () => {
|
||||
it('should retrieve a document by id', async () => {
|
||||
await db.create({ _id: 'fetch-001', product: 'Widget', price: 9.99 });
|
||||
|
||||
const doc = await db.getOne<{ product: string; price: number }>('fetch-001');
|
||||
expect(doc._id).toBe('fetch-001');
|
||||
expect(doc.product).toBe('Widget');
|
||||
expect(doc.price).toBe(9.99);
|
||||
});
|
||||
|
||||
it('should throw for a non-existent document', async () => {
|
||||
await expect(db.getOne('non-existent')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// βββ update βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
describe('update', () => {
|
||||
it('should merge new fields into an existing document', async () => {
|
||||
await db.create({ _id: 'upd-001', name: 'Original', count: 1 });
|
||||
|
||||
const res = await db.update('upd-001', { count: 42, extra: 'field' });
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const updated = await db.getOne<{ name: string; count: number; extra: string }>('upd-001');
|
||||
expect(updated.name).toBe('Original'); // untouched
|
||||
expect(updated.count).toBe(42); // updated
|
||||
expect(updated.extra).toBe('field'); // newly added
|
||||
});
|
||||
|
||||
it('should throw when updating a non-existent document', async () => {
|
||||
await expect(db.update('ghost', { name: 'nope' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// βββ delete βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
describe('delete', () => {
|
||||
it('should remove a document by id', async () => {
|
||||
await db.create({ _id: 'del-001', name: 'ToBeDeleted' });
|
||||
|
||||
const res = await db.delete('del-001');
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
await expect(db.getOne('del-001')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// βββ getAll βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all documents as a flat array', async () => {
|
||||
await db.create({ _id: 'a', val: 1 });
|
||||
await db.create({ _id: 'b', val: 2 });
|
||||
await db.create({ _id: 'c', val: 3 });
|
||||
|
||||
const all = await db.getAll<{ val: number }>();
|
||||
expect(all).toHaveLength(3);
|
||||
expect(all.map((d: any) => d.val).sort()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return empty array for empty database', async () => {
|
||||
const all = await db.getAll();
|
||||
expect(all).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// βββ getSome ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
describe('getSome', () => {
|
||||
it('should return only the requested documents', async () => {
|
||||
await db.create({ _id: 'x1', v: 10 });
|
||||
await db.create({ _id: 'x2', v: 20 });
|
||||
await db.create({ _id: 'x3', v: 30 });
|
||||
|
||||
const some = await db.getSome<{ v: number }>(['x1', 'x3']);
|
||||
expect(some).toHaveLength(2);
|
||||
expect(some.map((d: any) => d.v).sort()).toEqual([10, 30]);
|
||||
});
|
||||
|
||||
it('should silently skip missing ids', async () => {
|
||||
await db.create({ _id: 'exists', v: 1 });
|
||||
|
||||
const some = await db.getSome<{ v: number }>(['exists', 'ghost']);
|
||||
expect(some).toHaveLength(1);
|
||||
expect((some[0] as any).v).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// βββ find (pouchdb-find selectors) ββββββββββββββββββββββββββββ
|
||||
|
||||
describe('find', () => {
|
||||
it('should filter documents using selectors', async () => {
|
||||
await db.create({ _id: 'p1', category: 'electronics', price: 100 });
|
||||
await db.create({ _id: 'p2', category: 'clothing', price: 50 });
|
||||
await db.create({ _id: 'p3', category: 'electronics', price: 200 });
|
||||
|
||||
const results = await db.find<{ category: string; price: number }>({
|
||||
selector: { category: 'electronics' },
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.every((r) => r.category === 'electronics')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support $gt comparisons', async () => {
|
||||
await db.create({ _id: 'i1', price: 10 });
|
||||
await db.create({ _id: 'i2', price: 50 });
|
||||
await db.create({ _id: 'i3', price: 100 });
|
||||
|
||||
const results = await db.find<{ price: number }>({
|
||||
selector: { price: { $gt: 40 } },
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.every((r) => r.price > 40)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// βββ cleanAllData βββββββββββββββββββββββββββββββββββββββββββββ
|
||||
|
||||
describe('cleanAllData', () => {
|
||||
it('should remove all documents but keep the database intact', async () => {
|
||||
await db.create({ _id: 'c1', name: 'One' });
|
||||
await db.create({ _id: 'c2', name: 'Two' });
|
||||
await db.create({ _id: 'c3', name: 'Three' });
|
||||
|
||||
let all = await db.getAll();
|
||||
expect(all).toHaveLength(3);
|
||||
|
||||
await db.cleanAllData();
|
||||
|
||||
all = await db.getAll();
|
||||
expect(all).toHaveLength(0);
|
||||
|
||||
// Database should still be functional after cleaning
|
||||
await db.create({ _id: 'c4', name: 'Four' });
|
||||
all = await db.getAll();
|
||||
expect(all).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Generated
+561
-1
@@ -185,9 +185,15 @@ importers:
|
||||
dayjs:
|
||||
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'}
|
||||
|
||||
Reference in New Issue
Block a user