From 23123774c7b0bb694c70ade249f23fc5b23c8b3c Mon Sep 17 00:00:00 2001 From: Soma <0421.soma@gmail.com> Date: Wed, 27 May 2026 18:34:53 +0900 Subject: [PATCH 01/45] feat(example-ui): minimal Drive-like UI on monas-sdk/gateway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Google-Drive-like web UI for the Monas protocol, built on monas-sdk via the monas-gateway HTTP API. Create / open / edit / rename / share / revoke / delete encrypted files and folders, with a live Protocol activity panel narrating CEK → AES-256-CTR → SHA-256 CID → storage → state-node → HPKE. - React + Vite + TypeScript, hand-built pink design system (no Tailwind). - Single backend: talks to the gateway via the Vite proxy (/api → :3000), plus /account-api → monas-account (:4002) for the P-256 signing key. - State-node operations (version history, latest version, integrity verification) surfaced inside the preview modal. - Local-only files (no Content Network) are handled gracefully; signing account is required up front before content creation. Co-Authored-By: Claude Opus 4.7 (1M context) --- example-ui/.env.example | 11 + example-ui/.gitignore | 12 + example-ui/.gitleaksignore | 5 + example-ui/README.md | 130 ++ example-ui/index.html | 13 + example-ui/package-lock.json | 1747 +++++++++++++++++++ example-ui/package.json | 24 + example-ui/public/monas.svg | 4 + example-ui/src/App.tsx | 517 ++++++ example-ui/src/api/account.ts | 34 + example-ui/src/api/content.ts | 88 + example-ui/src/api/crypto.ts | 70 + example-ui/src/api/http.ts | 132 ++ example-ui/src/api/share.ts | 95 + example-ui/src/api/stateNode.ts | 51 + example-ui/src/components/ActionModals.tsx | 158 ++ example-ui/src/components/FileBrowser.tsx | 197 +++ example-ui/src/components/IdentityModal.tsx | 172 ++ example-ui/src/components/Modal.tsx | 44 + example-ui/src/components/PipelinePanel.tsx | 117 ++ example-ui/src/components/PreviewModal.tsx | 252 +++ example-ui/src/components/SettingsModal.tsx | 96 + example-ui/src/components/ShareModal.tsx | 192 ++ example-ui/src/components/Sidebar.tsx | 64 + example-ui/src/components/Toast.tsx | 48 + example-ui/src/components/TopBar.tsx | 58 + example-ui/src/components/icons.tsx | 168 ++ example-ui/src/config.ts | 49 + example-ui/src/index.css | 977 +++++++++++ example-ui/src/main.tsx | 10 + example-ui/src/pipeline/flows.ts | 301 ++++ example-ui/src/pipeline/runner.ts | 87 + example-ui/src/pipeline/types.ts | 58 + example-ui/src/store/identity.ts | 54 + example-ui/src/store/registry.ts | 48 + example-ui/src/store/store.ts | 46 + example-ui/src/types.ts | 49 + example-ui/src/utils.ts | 40 + example-ui/tsconfig.json | 20 + example-ui/vite.config.ts | 31 + 40 files changed, 6269 insertions(+) create mode 100644 example-ui/.env.example create mode 100644 example-ui/.gitignore create mode 100644 example-ui/.gitleaksignore create mode 100644 example-ui/README.md create mode 100644 example-ui/index.html create mode 100644 example-ui/package-lock.json create mode 100644 example-ui/package.json create mode 100644 example-ui/public/monas.svg create mode 100644 example-ui/src/App.tsx create mode 100644 example-ui/src/api/account.ts create mode 100644 example-ui/src/api/content.ts create mode 100644 example-ui/src/api/crypto.ts create mode 100644 example-ui/src/api/http.ts create mode 100644 example-ui/src/api/share.ts create mode 100644 example-ui/src/api/stateNode.ts create mode 100644 example-ui/src/components/ActionModals.tsx create mode 100644 example-ui/src/components/FileBrowser.tsx create mode 100644 example-ui/src/components/IdentityModal.tsx create mode 100644 example-ui/src/components/Modal.tsx create mode 100644 example-ui/src/components/PipelinePanel.tsx create mode 100644 example-ui/src/components/PreviewModal.tsx create mode 100644 example-ui/src/components/SettingsModal.tsx create mode 100644 example-ui/src/components/ShareModal.tsx create mode 100644 example-ui/src/components/Sidebar.tsx create mode 100644 example-ui/src/components/Toast.tsx create mode 100644 example-ui/src/components/TopBar.tsx create mode 100644 example-ui/src/components/icons.tsx create mode 100644 example-ui/src/config.ts create mode 100644 example-ui/src/index.css create mode 100644 example-ui/src/main.tsx create mode 100644 example-ui/src/pipeline/flows.ts create mode 100644 example-ui/src/pipeline/runner.ts create mode 100644 example-ui/src/pipeline/types.ts create mode 100644 example-ui/src/store/identity.ts create mode 100644 example-ui/src/store/registry.ts create mode 100644 example-ui/src/store/store.ts create mode 100644 example-ui/src/types.ts create mode 100644 example-ui/src/utils.ts create mode 100644 example-ui/tsconfig.json create mode 100644 example-ui/vite.config.ts diff --git a/example-ui/.env.example b/example-ui/.env.example new file mode 100644 index 0000000..bdd4df6 --- /dev/null +++ b/example-ui/.env.example @@ -0,0 +1,11 @@ +# Dev-server proxy targets (used by vite.config.ts). +# These point the same-origin /api and /account-api paths at your local +# services so the browser never hits CORS during local development. +# +# Copy to `.env` and edit if your Docker maps different ports. + +# monas-gateway — the main backend the UI calls (embeds monas-sdk). +VITE_GATEWAY_TARGET=http://127.0.0.1:3000 + +# monas-account — only used by "create account" to seed the P-256 signing key. +VITE_ACCOUNT_TARGET=http://127.0.0.1:4002 diff --git a/example-ui/.gitignore b/example-ui/.gitignore new file mode 100644 index 0000000..5dceeb9 --- /dev/null +++ b/example-ui/.gitignore @@ -0,0 +1,12 @@ +node_modules +dist +dist2 +dist-ssr +*.local +.env +.DS_Store +*.tsbuildinfo +vite.config.ts.timestamp-*.mjs + +# Demo-recording harness — not part of the app (kept locally, not committed). +recording/ diff --git a/example-ui/.gitleaksignore b/example-ui/.gitleaksignore new file mode 100644 index 0000000..a55f902 --- /dev/null +++ b/example-ui/.gitleaksignore @@ -0,0 +1,5 @@ +# Reviewed false positives (gitleaks high-entropy vault-service-token rule +# misfiring on long camelCase identifiers, not real secrets). +# +# App.tsx:301 is the revoke handler comparing two public-key variable names. +src/App.tsx:vault-service-token:301 diff --git a/example-ui/README.md b/example-ui/README.md new file mode 100644 index 0000000..cccb383 --- /dev/null +++ b/example-ui/README.md @@ -0,0 +1,130 @@ +# Monas Drive — example UI + +A minimal, Google-Drive-like web UI for the Monas protocol, built on the +**monas-sdk** via the **monas-gateway** HTTP API. It lets you **create, open, +edit, rename, share, revoke and delete** files and folders, and surfaces the +encryption + state-node work behind every action in a live **Protocol activity** +panel (CEK → AES-256-CTR → SHA-256 CID → storage → state-node → HPKE). + +The UI talks to a **single backend — the gateway** — which embeds the SDK and +orchestrates everything server-side: + +``` +┌──────────────┐ /api/* (Vite proxy) ┌───────────────┐ embeds monas-sdk +│ this UI │ ────────────────────────▶ │ monas-gateway │ ─┬─▶ encrypt + store (monas-content) +│ (React+Vite) │ single endpoint │ :3000 │ ├─▶ state-node (:8080) +└──────────────┘ └───────────────┘ └─▶ sign (monas-account :4002) +``` + +## Run + +```bash +cd example-ui +npm install +npm run dev # http://localhost:5173 +``` + +You also need the gateway (and the services it calls) running, e.g. via your +local Docker. The gateway defaults to `:3000` and reads: + +``` +MONAS_API_PORT=3000 +MONAS_STATE_NODE_URL=http://127.0.0.1:8080 +MONAS_ACCOUNT_URL=http://127.0.0.1:4002 +MONAS_PERSISTENCE_DIR=... # recommended; otherwise CEK/shares are in-memory +``` + +### Endpoint & CORS + +The browser calls same-origin `/api/*`, and the **Vite dev server proxies it** +to the gateway — so you never hit CORS locally. The target is configurable in +`.env` (copy `.env.example`): + +``` +VITE_GATEWAY_TARGET=http://127.0.0.1:3000 +``` + +You can also repoint the gateway at runtime from the **Settings** dialog (gear +icon) — there are presets for *local (proxied)* and a *public API*. ⚠️ Pointing +at a cross-origin URL directly (not through the proxy) requires that server to +send permissive CORS headers. + +## Gateway / SDK endpoints used + +| Action | Gateway call | SDK model | +| ----------------- | ------------------------------------- | --------------------------------- | +| Create identity | `POST /keypair` | `GenerateKeypair{Input,Output}` | +| New file / Upload | `POST /content` | `CreateContent{Input,Output}` | +| Open / preview | `GET /content/{id}` | `GetContent{Input,Output}` | +| Edit contents | `PUT /content/{id}` | `UpdateContent{Input,Output}` | +| Delete | `DELETE /content/{id}` | `DeleteContent{Input,Output}` | +| Share | `POST /share` | `ShareContent{Input,Output}` | +| Prove access | `POST /share/decrypt` | `DecryptSharedContent{Input,Out}` | +| Revoke | `POST /share/revoke` | `RevokeShare{Input,Output}` | +| (history/version) | `POST /state/history`, `/state/...` | `state` models | + +Notes on the contract: + +- All content/keys are exchanged as **base64url (no padding)** — matching the + SDK models. +- Responses are wrapped in the SDK `ApiResponse` envelope + (`{ success, data, error: { type, message }, trace_id }`); the client unwraps + `data` or throws the typed error. +- `POST /content`, `PUT/DELETE /content/{id}`, `POST /share/revoke` and the + `/state/*` calls require an **`X-Request-Timestamp`** header. The UI sends the + current Unix time; the SDK then signs the state-node request via the account + service. + +## Accounts & the signing key + +Create your account from the UI: open the identity chip (top-right) → **Create +account**. With *Register as signing account* checked, the UI sends +`POST /accounts` to **monas-account** (via the `/account-api` proxy), which +registers a **P-256** key. The SDK uses that key to sign state-node requests for +**create / edit / delete**. + +This is needed because the gateway's `/keypair` is stateless — it returns a +fresh keypair (handy for share recipients) but does **not** register a signing +key. So: + +- **Create account** (signing) → `POST /account-api/accounts` → monas-account. +- **Add identity** (keypair-only, e.g. a share recipient) → `POST /api/keypair` + → gateway. + +Sharing (`/share`, `/share/decrypt`, `/share/revoke`) only uses the keypairs the +UI holds, so a recipient identity doesn't need to be a signing account. + +## What's real vs. illustrative + +A single gateway call does the whole orchestration server-side, so the Protocol +activity panel pairs **one real call** per action with **illustrative phases** +that narrate the protocol and read ids out of the response: + +- **Real:** the labelled "· gateway call" step in each run (and the share + unwrap+decrypt proof). Errors are shown verbatim with their SDK type. +- **Illustrative:** CEK generation, CID addressing, member selection, token + issuance, etc. — these happen *inside* the SDK call; the panel narrates them + with a short minimum duration for readability. + +## Notes + +- **Folders are logical** (path prefixes). The gateway has no folder/listing + concept, so the UI keeps its own file registry in `localStorage` + (`monas.registry.v2`). Identities and the endpoint live there too. Clearing + site data resets the demo. +- **Rename** of a file is local-only here (the SDK applies a new name on the + next content edit); folder rename re-paths its descendants locally. +- Private keys for demo identities are stored in `localStorage` so the HPKE + round-trip proof can run — fine for a local demo, not for production. + +## Project layout + +``` +src/ + api/ gateway client (account=keypair, content, share, stateNode=state) + base64url helpers + pipeline/ per-action flow definitions + sequential runner → drives the activity panel + store/ localStorage-backed registry + identities (React via useSyncExternalStore) + components/ TopBar, Sidebar, FileBrowser, PipelinePanel, modals, icons, toasts + config.ts single gateway endpoint (proxy default + presets) + App.tsx wiring: actions → pipeline → registry updates +``` diff --git a/example-ui/index.html b/example-ui/index.html new file mode 100644 index 0000000..9b9e297 --- /dev/null +++ b/example-ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Monas Drive — example UI + + +
+ + + diff --git a/example-ui/package-lock.json b/example-ui/package-lock.json new file mode 100644 index 0000000..4a7d2ea --- /dev/null +++ b/example-ui/package-lock.json @@ -0,0 +1,1747 @@ +{ + "name": "monas-example-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "monas-example-ui", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.16.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.360", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", + "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/example-ui/package.json b/example-ui/package.json new file mode 100644 index 0000000..a64d0a4 --- /dev/null +++ b/example-ui/package.json @@ -0,0 +1,24 @@ +{ + "name": "monas-example-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "Minimal Drive-like example UI for the Monas protocol (encrypted, content-addressed, state-node synced file sharing).", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.16.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/example-ui/public/monas.svg b/example-ui/public/monas.svg new file mode 100644 index 0000000..9ddc9aa --- /dev/null +++ b/example-ui/public/monas.svg @@ -0,0 +1,4 @@ + + + + diff --git a/example-ui/src/App.tsx b/example-ui/src/App.tsx new file mode 100644 index 0000000..f2029a3 --- /dev/null +++ b/example-ui/src/App.tsx @@ -0,0 +1,517 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { TopBar } from "./components/TopBar"; +import { Sidebar } from "./components/Sidebar"; +import { FileBrowser } from "./components/FileBrowser"; +import { PipelinePanel } from "./components/PipelinePanel"; +import { Toasts, pushToast } from "./components/Toast"; +import { Modal } from "./components/Modal"; +import { TextPromptModal, FileEditorModal, ConfirmModal } from "./components/ActionModals"; +import { IdentityModal } from "./components/IdentityModal"; +import { SettingsModal } from "./components/SettingsModal"; +import { ShareModal, type ShareInput } from "./components/ShareModal"; +import { PreviewModal } from "./components/PreviewModal"; + +import { + useEntries, + entriesIn, + addEntry, + updateEntry, + removeEntry, + allEntries, + descendantsOf, + folderPath, +} from "./store/registry"; +import { useIdentities, getActive } from "./store/identity"; +import { probeGateway } from "./api/http"; +import { + uuid, + utf8ToBase64Url, + fileToBase64Url, + base64UrlToUtf8, +} from "./api/crypto"; +import { runPipeline } from "./pipeline/runner"; +import type { RunView, StepSpec } from "./pipeline/types"; +import * as flows from "./pipeline/flows"; +import * as contentApi from "./api/content"; +import * as shareApi from "./api/share"; +import type { Entry } from "./types"; + +type Modal = + | { type: "none" } + | { type: "newFile" } + | { type: "newFolder" } + | { type: "rename"; entry: Entry } + | { type: "edit"; entry: Entry; text: string } + | { type: "delete"; entry: Entry } + | { type: "share"; entryId: string } + | { type: "preview"; entry: Entry; contentB64Url: string } + | { type: "identity" } + | { type: "settings" } + | { type: "loadingEdit" }; + +function mimeFromName(name: string): string { + const ext = name.split(".").pop()?.toLowerCase(); + const map: Record = { + txt: "text/plain", + md: "text/markdown", + json: "application/json", + csv: "text/csv", + html: "text/html", + js: "text/javascript", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + svg: "image/svg+xml", + webp: "image/webp", + }; + return (ext && map[ext]) || "text/plain"; +} + +export default function App() { + const entries = useEntries(); + const { identities, activeLabel } = useIdentities(); + const active = getActive(); + + const [path, setPath] = useState("/"); + const [modal, setModal] = useState({ type: "none" }); + const [runs, setRuns] = useState([]); + const [collapsed, setCollapsed] = useState(false); + const [busy, setBusy] = useState(false); + const [gatewayUp, setGatewayUp] = useState(null); + const fileInput = useRef(null); + + // ---- health polling ------------------------------------------------- + const poll = useCallback(async () => { + setGatewayUp(await probeGateway()); + }, []); + + useEffect(() => { + poll(); + const t = setInterval(poll, 6000); + return () => clearInterval(t); + }, [poll]); + + // ---- pipeline plumbing ---------------------------------------------- + const upsertRun = useCallback((run: RunView) => { + setRuns((prev) => { + const i = prev.findIndex((r) => r.id === run.id); + if (i >= 0) { + const copy = [...prev]; + copy[i] = run; + return copy; + } + return [run, ...prev].slice(0, 15); + }); + }, []); + + const run = useCallback( + (op: string, target: string, specs: StepSpec[]) => { + if (collapsed) setCollapsed(false); + return runPipeline(op, target, specs, upsertRun); + }, + [collapsed, upsertRun], + ); + + const current = entriesIn(path); + const liveEntry = (id: string) => allEntries().find((e) => e.id === id); + + // ---- actions -------------------------------------------------------- + const createFromBytes = async ( + name: string, + contentBase64Url: string, + sizeBytes: number, + mimeType: string, + ) => { + const specs = flows.createFileFlow({ name, contentBase64Url, sizeBytes, contentType: mimeType }); + const { ok, ctx } = await run("Create", name, specs); + if (ok && ctx.create) { + const created = ctx.create as contentApi.CreateContentOutput; + addEntry({ + id: uuid(), + kind: "file", + name, + parentPath: path, + sizeBytes, + mimeType, + createdAt: Date.now(), + updatedAt: Date.now(), + localContentId: created.content_id, + remoteContentId: created.remote_content_id || undefined, + syncedToStateNode: !!created.remote_content_id, + versionCount: 1, + shares: [], + }); + pushToast(`“${name}” encrypted & created`, "success"); + } else { + pushToast(`Failed to create “${name}”`, "error"); + } + }; + + // Creating content registers a signed request on the state-node, so a signing + // account must exist first. Guard up front (like share does) instead of letting + // the gateway call fail with a generic error. + const requireSigningAccount = (): boolean => { + if (identities.some((i) => i.isSigningAccount)) return true; + pushToast("Create a signing account first", "error"); + setModal({ type: "identity" }); + return false; + }; + + const handleNewFile = async (v: { name: string; text: string }) => { + if (!requireSigningAccount()) return; + setModal({ type: "none" }); + await createFromBytes(v.name, utf8ToBase64Url(v.text), new Blob([v.text]).size, mimeFromName(v.name)); + }; + + const handleUpload = async (file: File) => { + if (!requireSigningAccount()) return; + const b64 = await fileToBase64Url(file); + await createFromBytes(file.name, b64, file.size, file.type || mimeFromName(file.name)); + }; + + const handleNewFolder = (name: string) => { + setModal({ type: "none" }); + addEntry({ + id: uuid(), + kind: "folder", + name, + parentPath: path, + sizeBytes: 0, + createdAt: Date.now(), + updatedAt: Date.now(), + syncedToStateNode: false, + versionCount: 0, + shares: [], + }); + pushToast(`Folder “${name}” created`, "success"); + }; + + const handleEditOpen = async (entry: Entry) => { + setModal({ type: "loadingEdit" }); + try { + const res = await contentApi.getContent(entry.localContentId!); + setModal({ type: "edit", entry, text: base64UrlToUtf8(res.content) }); + } catch (e) { + pushToast(`Could not load contents: ${(e as Error).message}`, "error"); + setModal({ type: "edit", entry, text: "" }); + } + }; + + const handleEditSave = async (entry: Entry, v: { name: string; text: string }) => { + setModal({ type: "none" }); + const sizeBytes = new Blob([v.text]).size; + const renamed = v.name && v.name !== entry.name ? v.name : undefined; + const specs = flows.updateFileFlow({ + entry, + contentBase64Url: utf8ToBase64Url(v.text), + sizeBytes, + name: renamed, + }); + const { ok, ctx } = await run("Update", renamed || entry.name, specs); + if (ok && ctx.update) { + const upd = ctx.update as contentApi.UpdateContentOutput; + updateEntry(entry.id, { + localContentId: upd.version_id, + seriesId: upd.series_id, + sizeBytes, + versionCount: entry.versionCount + 1, + ...(renamed ? { name: renamed } : {}), + }); + pushToast(`“${renamed || entry.name}” updated`, "success"); + } else { + pushToast("Update failed", "error"); + } + }; + + const handleDelete = async (entry: Entry) => { + setModal({ type: "none" }); + if (entry.kind === "folder") { + await deleteFolder(entry); + return; + } + const specs = flows.deleteFileFlow({ entry }); + const { ok } = await run("Delete", entry.name, specs); + if (ok) { + removeEntry(entry.id); + pushToast(`“${entry.name}” deleted`, "success"); + } else { + pushToast("Delete failed", "error"); + } + }; + + const handleOpen = async (entry: Entry) => { + const specs = flows.openFileFlow({ entry }); + const { ok, ctx } = await run("Open", entry.name, specs); + if (ok && ctx.get) { + const g = ctx.get as contentApi.GetContentOutput; + setModal({ type: "preview", entry, contentB64Url: g.content }); + } else { + pushToast("Could not open file", "error"); + } + }; + + const handleShare = async (entry: Entry, input: ShareInput) => { + if (!active) { + pushToast("Create an identity first", "error"); + setModal({ type: "identity" }); + return; + } + setBusy(true); + const specs = flows.shareFlow({ + entry, + identity: active, + recipientPublicKeyB64Url: input.recipientPublicKeyB64Url, + recipientLabel: input.recipientLabel, + permissions: input.permissions, + recipientPrivateKeyB64Url: input.recipientPrivateKeyB64Url, + }); + const { ok, ctx } = await run("Share", entry.name, specs); + if (ok && ctx.share) { + const g = ctx.share as shareApi.ShareContentOutput; + const existing = entry.shares.filter((s) => s.recipientKeyId !== g.recipient_key_id); + updateEntry(entry.id, { + shares: [ + ...existing, + { + recipientPublicKeyB64Url: input.recipientPublicKeyB64Url, + recipientLabel: input.recipientLabel, + permissions: input.permissions, + senderKeyId: g.sender_key_id, + recipientKeyId: g.recipient_key_id, + envelope: g.key_envelope, + grantedAt: Date.now(), + }, + ], + }); + pushToast(`Shared with ${input.recipientLabel || "recipient"}`, "success"); + } else { + pushToast("Share failed", "error"); + } + setBusy(false); + }; + + const handleRevoke = async (entry: Entry, recipientPublicKeyB64Url: string) => { + if (!active) return; + setBusy(true); + const specs = flows.revokeFlow({ entry, identity: active, recipientPublicKeyB64Url }); + const { ok } = await run("Revoke", entry.name, specs); + if (ok) { + updateEntry(entry.id, { + shares: entry.shares.filter((s) => s.recipientPublicKeyB64Url !== recipientPublicKeyB64Url), + }); + pushToast("Access revoked & content re-encrypted", "success"); + } else { + pushToast("Revoke failed", "error"); + } + setBusy(false); + }; + + // ---- folder helpers ------------------------------------------------- + function renameFolder(entry: Entry, newName: string) { + const oldPath = folderPath(entry.parentPath, entry.name); + const newPath = folderPath(entry.parentPath, newName); + updateEntry(entry.id, { name: newName }); + for (const e of allEntries()) { + if (e.parentPath === oldPath || e.parentPath.startsWith(oldPath + "/")) { + updateEntry(e.id, { parentPath: newPath + e.parentPath.slice(oldPath.length) }); + } + } + } + + async function deleteFolder(entry: Entry) { + const here = folderPath(entry.parentPath, entry.name); + const desc = descendantsOf(here); + const files = desc.filter((e) => e.kind === "file"); + const specs: StepSpec[] = [ + { + title: `Delete ${files.length} encrypted file(s)`, + hint: "monas-sdk", + kind: "cleanup", + minMs: 200, + exec: async () => { + for (const f of files) { + try { + await contentApi.deleteContent({ + localContentId: f.localContentId!, + remoteContentId: f.remoteContentId || f.localContentId!, + }); + } catch { + /* best effort */ + } + } + return `Removed ${files.length} content network(s)`; + }, + }, + { + title: "Remove folder & contents", + hint: "registry", + kind: "cleanup", + minMs: 140, + exec: async () => "Folder tree cleared", + }, + ]; + const { ok } = await run("Delete folder", entry.name, specs); + if (ok) { + for (const e of desc) removeEntry(e.id); + removeEntry(entry.id); + pushToast(`Folder “${entry.name}” deleted`, "success"); + } + } + + const handleRename = async (entry: Entry, newName: string) => { + setModal({ type: "none" }); + if (entry.kind === "folder") { + renameFolder(entry, newName); + pushToast("Folder renamed", "success"); + return; + } + // A content rename is a metadata update; reuse the update flow with the + // current content unchanged is not possible without bytes, so we just + // rename locally and let the next edit carry the new name to the SDK. + updateEntry(entry.id, { name: newName }); + pushToast("Renamed", "success"); + }; + + // ---- dispatch from row menu ---------------------------------------- + const onAction = (action: string, entry: Entry) => { + switch (action) { + case "openFolder": + return setPath(folderPath(entry.parentPath, entry.name)); + case "open": + return handleOpen(entry); + case "update": + return handleEditOpen(entry); + case "rename": + return setModal({ type: "rename", entry }); + case "share": + if (!active) { + pushToast("Create an identity first", "error"); + return setModal({ type: "identity" }); + } + return setModal({ type: "share", entryId: entry.id }); + case "delete": + return setModal({ type: "delete", entry }); + } + }; + + const shareEntry = modal.type === "share" ? liveEntry(modal.entryId) : null; + + return ( +
+ setModal({ type: "identity" })} + onOpenSettings={() => setModal({ type: "settings" })} + /> +
+ setModal({ type: "newFile" })} + onNewFolder={() => setModal({ type: "newFolder" })} + onUpload={() => fileInput.current?.click()} + /> +
+ +
+ setCollapsed((c) => !c)} + onClear={() => setRuns([])} + /> +
+ + { + const f = e.target.files?.[0]; + e.target.value = ""; + if (f) handleUpload(f); + }} + /> + + {/* modals */} + {modal.type === "newFile" && ( + setModal({ type: "none" })} /> + )} + {modal.type === "newFolder" && ( + setModal({ type: "none" })} + /> + )} + {modal.type === "rename" && ( + handleRename(modal.entry, v)} + onClose={() => setModal({ type: "none" })} + /> + )} + {modal.type === "loadingEdit" && ( + setModal({ type: "none" })}> +
+ Fetching & decrypting current contents… +
+
+ )} + {modal.type === "edit" && ( + handleEditSave(modal.entry, v)} + onClose={() => setModal({ type: "none" })} + /> + )} + {modal.type === "delete" && ( + handleDelete(modal.entry)} + onClose={() => setModal({ type: "none" })} + /> + )} + {modal.type === "share" && shareEntry && ( + setModal({ type: "none" })} + /> + )} + {modal.type === "preview" && ( + setModal({ type: "none" })} + /> + )} + {modal.type === "identity" && setModal({ type: "none" })} />} + {modal.type === "settings" && ( + setModal({ type: "none" })} onSaved={poll} /> + )} + + +
+ ); +} diff --git a/example-ui/src/api/account.ts b/example-ui/src/api/account.ts new file mode 100644 index 0000000..bc0bebf --- /dev/null +++ b/example-ui/src/api/account.ts @@ -0,0 +1,34 @@ +// Keypair generation via the gateway (monas-sdk `generate_keypair`). +// POST /keypair { key_type } → { key_type, public_key, private_key } (base64url) +import { gateway, createAccountKey } from "./http"; +import { standardBase64ToBase64Url } from "./crypto"; + +export type KeyType = "secp256r1" | "secp256k1"; + +export interface GenerateKeypairOutput { + key_type: KeyType; + public_key: string; // base64url + private_key: string; // base64url +} + +// Ephemeral keypair (used for sharing recipients). Does NOT register a signing +// key with the account service. +export function generateKeypair(keyType: KeyType) { + return gateway("/keypair", { + method: "POST", + body: { key_type: keyType }, + }); +} + +// Create the signing account directly on monas-account. This both registers +// the key the SDK uses to sign state-node requests AND returns a keypair we can +// use as an identity. Keys are converted to base64url for consistency with the +// gateway/SDK models. +export async function createSigningAccount(keyType: KeyType): Promise { + const acct = await createAccountKey(keyType === "secp256r1" ? "P256" : "K256"); + return { + key_type: keyType, + public_key: standardBase64ToBase64Url(acct.public_key_base64), + private_key: standardBase64ToBase64Url(acct.secret_key_base64), + }; +} diff --git a/example-ui/src/api/content.ts b/example-ui/src/api/content.ts new file mode 100644 index 0000000..b4feac2 --- /dev/null +++ b/example-ui/src/api/content.ts @@ -0,0 +1,88 @@ +// Content operations via the gateway (monas-sdk content controller). +// The SDK does: CEK gen → AES-256-CTR → SHA-256 CID → storage → state-node +// register/update/delete (+ signing via account). All content is base64url. +import { gateway } from "./http"; + +export interface ContentMetadata { + name?: string; + content_type?: string; + created_at?: string; + updated_at?: string; +} + +export interface CreateContentOutput { + content_id: string; // local id (encCid) + remote_content_id?: string; // state-node Content Network id + created_at?: string; +} + +export function createContent(input: { contentBase64Url: string; name: string; contentType?: string }) { + return gateway("/content", { + method: "POST", + timestamp: true, + body: { + content: input.contentBase64Url, + metadata: { name: input.name, content_type: input.contentType }, + }, + }); +} + +export interface GetContentOutput { + content_id: string; + content: string; // decrypted, base64url + metadata?: ContentMetadata; +} + +export function getContent(localContentId: string) { + return gateway(`/content/${encodeURIComponent(localContentId)}`, { + method: "GET", + }); +} + +export interface UpdateContentOutput { + series_id: string; + previous_version_id: string; + version_id: string; // new local id + updated_at?: string; +} + +export function updateContent(input: { + localContentId: string; + remoteContentId: string; + contentBase64Url: string; + name?: string; +}) { + return gateway( + `/content/${encodeURIComponent(input.localContentId)}`, + { + method: "PUT", + timestamp: true, + body: { + local_content_id: input.localContentId, + remote_content_id: input.remoteContentId, + content: input.contentBase64Url, + metadata: input.name ? { name: input.name } : undefined, + }, + }, + ); +} + +export interface DeleteContentOutput { + content_id: string; + deleted: boolean; + deleted_at?: string; +} + +export function deleteContent(input: { localContentId: string; remoteContentId: string }) { + return gateway( + `/content/${encodeURIComponent(input.localContentId)}`, + { + method: "DELETE", + timestamp: true, + body: { + local_content_id: input.localContentId, + remote_content_id: input.remoteContentId, + }, + }, + ); +} diff --git a/example-ui/src/api/crypto.ts b/example-ui/src/api/crypto.ts new file mode 100644 index 0000000..a4ae9ca --- /dev/null +++ b/example-ui/src/api/crypto.ts @@ -0,0 +1,70 @@ +// Browser-side helpers. The protocol crypto (CEK, AES-256-CTR, HPKE, CID) all +// runs server-side inside monas-sdk via the gateway. Here we only need base64 / +// base64url plumbing, because the SDK models exchange bytes as base64url. + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ""; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); + } + return btoa(binary); +} + +function base64ToBytes(b64: string): Uint8Array { + const binary = atob(b64); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i); + return out; +} + +// --- base64url (no padding) — the encoding every SDK model uses --- +export function bytesToBase64Url(bytes: Uint8Array): string { + return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +export function base64UrlToBytes(b64url: string): Uint8Array { + const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/"); + const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - (b64.length % 4)); + return base64ToBytes(b64 + pad); +} + +export function utf8ToBase64Url(text: string): string { + return bytesToBase64Url(new TextEncoder().encode(text)); +} + +export function base64UrlToUtf8(b64url: string): string { + return new TextDecoder().decode(base64UrlToBytes(b64url)); +} + +// For , which needs standard base64. +export function base64UrlToStandard(b64url: string): string { + return bytesToBase64(base64UrlToBytes(b64url)); +} + +// monas-account returns keys as standard base64; the gateway/SDK want base64url. +export function standardBase64ToBase64Url(b64: string): string { + return bytesToBase64Url(base64ToBytes(b64)); +} + +export async function fileToBase64Url(file: File): Promise { + return bytesToBase64Url(new Uint8Array(await file.arrayBuffer())); +} + +export function byteLengthOfBase64Url(b64url: string): number { + return base64UrlToBytes(b64url).length; +} + +export function uuid(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return "id-" + Math.random().toString(36).slice(2) + Date.now().toString(36); +} + +// Abbreviate a long id/hash for display: "head…tail". +export function short(s: string, head = 12, tail = 8): string { + if (!s) return "—"; + if (s.length <= head + tail + 1) return s; + return `${s.slice(0, head)}…${s.slice(-tail)}`; +} diff --git a/example-ui/src/api/http.ts b/example-ui/src/api/http.ts new file mode 100644 index 0000000..afcdc1e --- /dev/null +++ b/example-ui/src/api/http.ts @@ -0,0 +1,132 @@ +import { loadEndpoints } from "../config"; + +// SDK error envelope: { type, message } tagged enum. +export class ApiError extends Error { + constructor( + public status: number, + message: string, + public kind: string = "Internal", + public traceId?: string, + ) { + super(message); + this.name = "ApiError"; + } +} + +interface SdkApiError { + type: string; + message: string; +} +interface ApiResponse { + success: boolean; + data?: T; + error?: SdkApiError; + trace_id: string; +} + +function gatewayBase(): string { + return loadEndpoints().gateway.replace(/\/+$/, ""); +} + +export function nowUnix(): number { + return Math.floor(Date.now() / 1000); +} + +interface RequestOptions { + method?: string; + body?: unknown; + /** Add an X-Request-Timestamp header (required by state-touching endpoints). */ + timestamp?: boolean; + headers?: Record; +} + +// Calls the gateway and unwraps the SDK ApiResponse. Throws ApiError on +// transport failure or a non-success envelope. +export async function gateway(path: string, opts: RequestOptions = {}): Promise { + const url = gatewayBase() + path; + const headers: Record = { ...(opts.headers || {}) }; + let body: BodyInit | undefined; + if (opts.body !== undefined) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(opts.body); + } + if (opts.timestamp) headers["X-Request-Timestamp"] = String(nowUnix()); + + let res: Response; + try { + res = await fetch(url, { + method: opts.method || (opts.body !== undefined ? "POST" : "GET"), + headers, + body, + }); + } catch (e) { + throw new ApiError( + 0, + `Network error reaching the gateway (${url}). Is monas-gateway running and the endpoint correct? ${ + (e as Error).message + }`, + ); + } + + const text = await res.text(); + let parsed: ApiResponse | null = null; + if (text) { + try { + parsed = JSON.parse(text) as ApiResponse; + } catch { + parsed = null; + } + } + + if (parsed && parsed.success === false && parsed.error) { + throw new ApiError(res.status, parsed.error.message, parsed.error.type, parsed.trace_id); + } + if (!res.ok) { + throw new ApiError(res.status, text || res.statusText); + } + if (parsed && "success" in parsed) { + return parsed.data as T; + } + return undefined as T; +} + +// Health probe for the connection indicator (gateway GET /health → 200). +export async function probeGateway(): Promise { + try { + const res = await fetch(gatewayBase() + "/health", { method: "GET" }); + return res.ok; + } catch { + return false; + } +} + +// --- monas-account service (only used to create the signing key) --- +// This service returns plain JSON (not the SDK ApiResponse envelope) and keys +// in standard base64. +export interface AccountCreateResponse { + algorithm: string; + public_key_base64: string; + secret_key_base64: string; +} + +export async function createAccountKey(keyType: "P256" | "K256"): Promise { + const base = loadEndpoints().accountService.replace(/\/+$/, ""); + let res: Response; + try { + res = await fetch(base + "/accounts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key_type: keyType }), + }); + } catch (e) { + throw new ApiError( + 0, + `Network error reaching the account service (${base}). Is monas-account running? ${ + (e as Error).message + }`, + ); + } + const text = await res.text(); + if (!res.ok) throw new ApiError(res.status, text || res.statusText); + return JSON.parse(text) as AccountCreateResponse; +} diff --git a/example-ui/src/api/share.ts b/example-ui/src/api/share.ts new file mode 100644 index 0000000..2fe69aa --- /dev/null +++ b/example-ui/src/api/share.ts @@ -0,0 +1,95 @@ +// Share operations via the gateway (monas-sdk share controller). +import { gateway } from "./http"; + +export type Permission = "read" | "write"; + +export interface KeyEnvelope { + enc: string; // base64url + wrapped_cek: string; // base64url + ciphertext: string; // base64url +} + +export interface DelegatedAccessToken { + delegated_token: string; + issued_at: number; + expires_at: number; + jti: string; +} + +export interface ShareContentOutput { + content_id: string; + recipient_public_key: string; + sender_key_id: string; + recipient_key_id: string; + key_envelope: KeyEnvelope; + delegated_access?: DelegatedAccessToken; + shared_at?: string; +} + +export function shareContent(input: { + contentId: string; // local content id + senderPublicKeyB64Url: string; + recipientPublicKeyB64Url: string; + permissions: Permission[]; +}) { + return gateway("/share", { + method: "POST", + body: { + content_id: input.contentId, + sender_public_key: input.senderPublicKeyB64Url, + recipient_public_key: input.recipientPublicKeyB64Url, + permissions: input.permissions, + }, + }); +} + +export interface RevokeShareOutput { + content_id: string; + recipient_public_key: string; + revoked: boolean; + revoked_at?: string; +} + +export function revokeShare(input: { + contentId: string; + senderPublicKeyB64Url: string; + recipientPublicKeyB64Url: string; +}) { + return gateway("/share/revoke", { + method: "POST", + timestamp: true, + body: { + content_id: input.contentId, + sender_public_key: input.senderPublicKeyB64Url, + recipient_public_key: input.recipientPublicKeyB64Url, + }, + }); +} + +export interface DecryptSharedContentOutput { + content_id: string; + content: string; // decrypted, base64url + version: string; + metadata?: { name?: string; content_type?: string }; +} + +export function decryptSharedContent(input: { + contentId: string; + privateKeyB64Url: string; + senderKeyId: string; + recipientKeyId: string; + keyEnvelope: KeyEnvelope; + version?: string; +}) { + return gateway("/share/decrypt", { + method: "POST", + body: { + content_id: input.contentId, + private_key: input.privateKeyB64Url, + sender_key_id: input.senderKeyId, + recipient_key_id: input.recipientKeyId, + key_envelope: input.keyEnvelope, + version: input.version, + }, + }); +} diff --git a/example-ui/src/api/stateNode.ts b/example-ui/src/api/stateNode.ts new file mode 100644 index 0000000..b7bcf5b --- /dev/null +++ b/example-ui/src/api/stateNode.ts @@ -0,0 +1,51 @@ +// State / version operations via the gateway (monas-sdk state controller). +import { gateway } from "./http"; + +export interface GetLatestVersionOutput { + content_id: string; + latest_version: string; + updated_at?: string; +} + +export function getLatestVersion(contentId: string) { + return gateway("/state/latest-version", { + method: "POST", + timestamp: true, + body: { content_id: contentId }, + }); +} + +export interface GetHistoryOutput { + content_id: string; + versions: string[]; +} + +export function getHistory(contentId: string, limit = 100) { + return gateway("/state/history", { + method: "POST", + timestamp: true, + body: { content_id: contentId, limit }, + }); +} + +export interface VerifyIntegrityOutput { + valid: boolean; + computed_hash: string; + reason?: string; +} + +export function verifyIntegrity(input: { + contentId: string; + contentBase64Url: string; + expectedVersion?: string; +}) { + return gateway("/state/verify-integrity", { + method: "POST", + timestamp: true, + body: { + content_id: input.contentId, + content: input.contentBase64Url, + expected_version: input.expectedVersion, + }, + }); +} diff --git a/example-ui/src/components/ActionModals.tsx b/example-ui/src/components/ActionModals.tsx new file mode 100644 index 0000000..282966c --- /dev/null +++ b/example-ui/src/components/ActionModals.tsx @@ -0,0 +1,158 @@ +import { useState } from "react"; +import { Modal } from "./Modal"; +import { Folder, Pencil, FileText, Trash, Lock } from "./icons"; + +export function TextPromptModal({ + title, + label, + initial, + confirmLabel, + kind, + busy, + onConfirm, + onClose, +}: { + title: string; + label: string; + initial?: string; + confirmLabel: string; + kind: "folder" | "rename"; + busy?: boolean; + onConfirm: (value: string) => void; + onClose: () => void; +}) { + const [value, setValue] = useState(initial || ""); + const submit = () => value.trim() && onConfirm(value.trim()); + return ( + : } + onClose={onClose} + footer={ + <> + + + + } + > +
+ + setValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && submit()} + placeholder={kind === "folder" ? "Untitled folder" : "name"} + /> +
+
+ ); +} + +export function FileEditorModal({ + mode, + initialName, + initialText, + busy, + onSubmit, + onClose, +}: { + mode: "create" | "edit"; + initialName?: string; + initialText?: string; + busy?: boolean; + onSubmit: (v: { name: string; text: string }) => void; + onClose: () => void; +}) { + const [name, setName] = useState(initialName || "untitled.txt"); + const [text, setText] = useState(initialText ?? ""); + + const valid = name.trim().length > 0; + return ( + } + onClose={onClose} + footer={ + <> + + + + } + > +
+ + setName(e.target.value)} + autoFocus={mode === "create"} + /> +
+
+ +