From d84a860d15c3dd8f35c0a3368c2665cecdc1a641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Sun, 12 Apr 2026 23:43:56 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20ograph=20repo=20=E2=80=94=20e?= =?UTF-8?q?ngine=20(85=20tests)=20+=20cli=20(31=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted from uncaged monorepo (oc-xiaoju/uncaged). Resolves oc-xiaoju/uncaged#224. - @uncaged/ograph: CF Worker engine (events, projections, reactions) - @uncaged/ograph-cli: CLI for managing OGraph instances - Removed @uncaged/oid dependency (unused) - 116 tests, all passing - CI: GitHub Actions 小橘 🍊(NEKO Team) --- .github/workflows/ci.yml | 18 + .gitignore | 4 + README.md | 51 + package-lock.json | 5867 +++++++++++++++++ package.json | 17 + packages/cli/package.json | 46 + packages/cli/src/client.ts | 254 + packages/cli/src/commands/config.ts | 103 + packages/cli/src/commands/event-defs.ts | 80 + packages/cli/src/commands/events.ts | 109 + packages/cli/src/commands/health.ts | 40 + packages/cli/src/commands/object-defs.ts | 70 + packages/cli/src/commands/objects.ts | 97 + packages/cli/src/commands/projection-defs.ts | 144 + packages/cli/src/commands/projections.ts | 63 + packages/cli/src/commands/reactions.ts | 145 + packages/cli/src/config.ts | 65 + packages/cli/src/index.ts | 31 + packages/cli/test/client.test.ts | 374 ++ packages/cli/test/config.test.ts | 72 + packages/cli/tsconfig.json | 26 + packages/engine/migrations/0006_v2.sql | 44 + packages/engine/migrations/0007_reducers.sql | 30 + .../migrations/0008_simplify_reducers.sql | 15 + packages/engine/migrations/0009_reactions.sql | 10 + packages/engine/migrations/0010_v2.1.sql | 65 + packages/engine/migrations/0011_v2.2.sql | 82 + packages/engine/migrations/0012_v2.3.sql | 104 + packages/engine/migrations/0013_v2.4.sql | 113 + packages/engine/migrations/0014_bindings.sql | 46 + packages/engine/migrations/0015_sources.sql | 54 + .../migrations/0016_reaction_actions.sql | 17 + .../engine/migrations/0017_integer_ids.sql | 52 + .../engine/migrations/0018_reaction_logs.sql | 15 + packages/engine/migrations/0019_api_keys.sql | 10 + packages/engine/migrations/0020_handler.sql | 42 + .../engine/migrations/0021_request_logs.sql | 13 + packages/engine/package.json | 22 + packages/engine/src/auth.ts | 21 + packages/engine/src/engine.ts | 1321 ++++ packages/engine/src/html.d.ts | 4 + packages/engine/src/index.test.ts | 2040 ++++++ packages/engine/src/index.ts | 483 ++ packages/engine/src/reaction-executor.ts | 25 + packages/engine/src/types.ts | 240 + packages/engine/src/ui.html | 73 + packages/engine/tsconfig.json | 10 + packages/engine/vitest.config.ts | 20 + packages/engine/wrangler.toml | 15 + tsconfig.json | 20 + 50 files changed, 12682 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/client.ts create mode 100644 packages/cli/src/commands/config.ts create mode 100644 packages/cli/src/commands/event-defs.ts create mode 100644 packages/cli/src/commands/events.ts create mode 100644 packages/cli/src/commands/health.ts create mode 100644 packages/cli/src/commands/object-defs.ts create mode 100644 packages/cli/src/commands/objects.ts create mode 100644 packages/cli/src/commands/projection-defs.ts create mode 100644 packages/cli/src/commands/projections.ts create mode 100644 packages/cli/src/commands/reactions.ts create mode 100644 packages/cli/src/config.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/test/client.test.ts create mode 100644 packages/cli/test/config.test.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/engine/migrations/0006_v2.sql create mode 100644 packages/engine/migrations/0007_reducers.sql create mode 100644 packages/engine/migrations/0008_simplify_reducers.sql create mode 100644 packages/engine/migrations/0009_reactions.sql create mode 100644 packages/engine/migrations/0010_v2.1.sql create mode 100644 packages/engine/migrations/0011_v2.2.sql create mode 100644 packages/engine/migrations/0012_v2.3.sql create mode 100644 packages/engine/migrations/0013_v2.4.sql create mode 100644 packages/engine/migrations/0014_bindings.sql create mode 100644 packages/engine/migrations/0015_sources.sql create mode 100644 packages/engine/migrations/0016_reaction_actions.sql create mode 100644 packages/engine/migrations/0017_integer_ids.sql create mode 100644 packages/engine/migrations/0018_reaction_logs.sql create mode 100644 packages/engine/migrations/0019_api_keys.sql create mode 100644 packages/engine/migrations/0020_handler.sql create mode 100644 packages/engine/migrations/0021_request_logs.sql create mode 100644 packages/engine/package.json create mode 100644 packages/engine/src/auth.ts create mode 100644 packages/engine/src/engine.ts create mode 100644 packages/engine/src/html.d.ts create mode 100644 packages/engine/src/index.test.ts create mode 100644 packages/engine/src/index.ts create mode 100644 packages/engine/src/reaction-executor.ts create mode 100644 packages/engine/src/types.ts create mode 100644 packages/engine/src/ui.html create mode 100644 packages/engine/tsconfig.json create mode 100644 packages/engine/vitest.config.ts create mode 100644 packages/engine/wrangler.toml create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..312baba --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38ce724 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.wrangler/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..20ce93b --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# OGraph + +**Event Sourcing + Projection + Reaction engine on Cloudflare Workers.** + +Part of the [Uncaged](https://github.com/oc-xiaoju/uncaged) ecosystem. + +## Packages + +| Package | Description | npm | +|---|---|---| +| [`@uncaged/ograph`](packages/engine/) | CF Worker engine — events, projections, reactions | [![npm](https://img.shields.io/npm/v/@uncaged/ograph)](https://www.npmjs.com/package/@uncaged/ograph) | +| [`@uncaged/ograph-cli`](packages/cli/) | CLI for managing OGraph instances | [![npm](https://img.shields.io/npm/v/@uncaged/ograph-cli)](https://www.npmjs.com/package/@uncaged/ograph-cli) | + +## Core Concepts + +- **Event** — Immutable facts with typed properties and object references +- **Projection** — Derived state computed incrementally from events via reducers +- **Reaction** — Side effects triggered by projection state changes (webhooks, event emission, handlers) + +## Quick Start + +```bash +npm install -g @uncaged/ograph-cli +ograph deploy # Deploy to Cloudflare Workers +ograph event-def add # Define event types +ograph event add # Emit events +ograph projection list # Query projections +``` + +## Development + +```bash +npm install +npm test # Run all tests +cd packages/engine && npm run dev # Local dev server +``` + +## Architecture + +- **D1** for storage (events, projections, reactions) +- **Hono** for API routing +- **Incremental reduce** — projections track `last_event_id` for O(delta) updates +- **Dynamic handlers** — `new Function()` sandboxed execution with emit/log/kv API injection + +## License + +MIT + +--- + +*Built by 小橘 🍊 & 小墨 🖊️ — NEKO + KUMA Teams* diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..240ad75 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5867 @@ +{ + "name": "ograph", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ograph", + "workspaces": [ + "packages/engine", + "packages/cli" + ], + "devDependencies": { + "eslint": "^9.0.0", + "typescript": "^6.0.2" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.0.tgz", + "integrity": "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260409.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260409.1.tgz", + "integrity": "sha512-h/bkaC0HJL63aqAGnV0oagqpBiTSstabODThkeMSbG8kctl0Jb4jlq1pNHJPmYGazFNtfyagrUZFb6HN22GX7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260409.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260409.1.tgz", + "integrity": "sha512-HTAC+B9uSYcm+GjN3UYJjuun19GqYtK1bAFJ0KECXyfsgIDwH1MTzxbTxzJpZUbWLw8s0jcwCU06MWZj6cgnxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260409.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260409.1.tgz", + "integrity": "sha512-QIoNq5cgmn1ko8qlngmgZLXQr2KglrjvIwVFOyJI3rbIpt8631n/YMzHPiOWgt38Cb6tcni8fXOzkcvIX2lBDg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260409.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260409.1.tgz", + "integrity": "sha512-HJGBMTfPDb0GCjwdxWFx63wS20TYDVmtOuA5KVri/CiFnit71y++kmseVmemjsgLFFIzoEAuFG/xUh1FJLo6tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260409.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260409.1.tgz", + "integrity": "sha512-GttFO0+TvE0rJNQbDlxC6kq2Q7uFxoZRo74Z9d/trUrLgA14HEVTTXobYyiWrDZ9Qp2W5KN1CrXQXiko0zE38Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260412.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260412.1.tgz", + "integrity": "sha512-4jcYPBKH70/XOW40B6X/bEUlh+rjvem8wuvGJXuGebSScFcbJ5TuO5CjX/Nc8Y+RhH3RnTcynHX4tR6Rm0MNgA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.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-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "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-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "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/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "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/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", + "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/chai/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "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/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@uncaged/ograph": { + "resolved": "packages/engine", + "link": true + }, + "node_modules/@uncaged/ograph-cli": { + "resolved": "packages/cli", + "link": true + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "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/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hono": { + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonata": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.1.0.tgz", + "integrity": "sha512-OCzaRMK8HobtX8fp37uIVmL8CY1IGc/a6gLsDqz3quExFR09/U78HUzWYr7T31UEB6+Eu0/8dkVD5fFDOl9a8w==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/miniflare": { + "version": "4.20260409.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260409.0.tgz", + "integrity": "sha512-ayl6To4av0YuXsSivGgWLj+Ug8xZ0Qz3sGV8+Ok2LhNVl6m8m5ktEBM3LX9iT9MtLZRJwBlJrKcraNs/DlZQfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.24.4", + "workerd": "1.20260409.1", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "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.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/unenv/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.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/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerd": { + "version": "1.20260409.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260409.1.tgz", + "integrity": "sha512-kuWP20fAaqaLBqLbvUfY9nCF6c3C78L60G9lS6eVwBf+v8trVFIsAdLB/FtrnKm7vgVvpDzvFAfB80VIiVj95w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260409.1", + "@cloudflare/workerd-darwin-arm64": "1.20260409.1", + "@cloudflare/workerd-linux-64": "1.20260409.1", + "@cloudflare/workerd-linux-arm64": "1.20260409.1", + "@cloudflare/workerd-windows-64": "1.20260409.1" + } + }, + "node_modules/wrangler": { + "version": "4.81.1", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.81.1.tgz", + "integrity": "sha512-fppPXi+W2KJ5bx1zxdUYe1e7CHj5cWPFVBPXy8hSMZhrHeIojMe3ozAktAOw1voVuQjXzbZJf/GVKyVeSjbF8w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.16.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260409.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260409.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.3.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260409.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "packages/cli": { + "name": "@uncaged/ograph-cli", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "commander": "^12.0.0" + }, + "bin": { + "ograph": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.4.0", + "vitest": "^1.6.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/cli/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" + } + }, + "packages/engine": { + "name": "@uncaged/ograph", + "version": "0.1.0", + "dependencies": { + "hono": "^4.0.0", + "jsonata": "^2.1.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260403.0", + "typescript": "^6.0.2", + "vitest": "^4.1.2", + "wrangler": "^4.0.0" + } + }, + "packages/engine/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/engine/node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "packages/engine/node_modules/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/engine/node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/engine/node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/engine/node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/engine/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/engine/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "extraneous": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "packages/engine/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "packages/engine/node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "packages/engine/node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "packages/engine/node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2905920 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "ograph", + "private": true, + "workspaces": [ + "packages/engine", + "packages/cli" + ], + "scripts": { + "test": "npm run test --workspaces", + "lint": "eslint packages/*/src/", + "lint:fix": "eslint packages/*/src/ --fix" + }, + "devDependencies": { + "eslint": "^9.0.0", + "typescript": "^6.0.2" + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..6ce333b --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,46 @@ +{ + "name": "@uncaged/ograph-cli", + "version": "0.1.0", + "description": "OGraph CLI for object-graph database operations", + "type": "module", + "bin": { + "ograph": "./dist/index.js" + }, + "files": [ + "dist/" + ], + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "test": "vitest run", + "test:run": "vitest run" + }, + "keywords": [ + "ograph-cli", + "graph", + "database", + "cli", + "object" + ], + "author": "小墨 🖊️", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/oc-xiaoju/uncaged.git", + "directory": "packages/ograph-cli" + }, + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "commander": "^12.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.4.0", + "vitest": "^1.6.0" + } +} diff --git a/packages/cli/src/client.ts b/packages/cli/src/client.ts new file mode 100644 index 0000000..8d5c91b --- /dev/null +++ b/packages/cli/src/client.ts @@ -0,0 +1,254 @@ +// OGraph API client - v2.4 Event-Sourced API +import { loadConfig } from './config.js' + +// ─── Types ───────────────────────────────────────────────────────────────────── + +export interface ObjectDef { + name: string + created_at?: number +} + +export interface ObjectInstance { + id: number + type: string + created_at?: number +} + +export interface EventDefProperty { + type: 'ref' | 'string' | 'number' | 'boolean' + object_type?: string +} + +export interface EventDef { + name: string + hash?: string + schema: { properties: Record } + created_at?: number +} + +export interface OEvent { + id: number + type_hash: string + payload: Record + created_at?: number +} + +export interface ProjectionDefSource { + event_def: string + bindings: Record + expression: string +} + +export interface ProjectionDef { + name: string + sources?: ProjectionDefSource[] + params?: Record + value_schema?: { type: string } + initial_value?: unknown +} + +export interface Reaction { + id: number + projection_def_hash: string + params_hash: string + params: Record + action: 'webhook' | 'emit_event' + webhook_url?: string + emit_event_type?: string + emit_payload_template?: string + created_at: number +} + +export interface EmitEventResponse { + event: OEvent + reactions_fired: number +} + +export interface HealthResponse { + status: string + version: string +} + +// ─── Client Class ────────────────────────────────────────────────────────────── + +export class OGraphClient { + private endpoint?: string + private token?: string + + async init(): Promise { + const config = await loadConfig() + this.endpoint = config.endpoint + this.token = config.token + + if (!this.endpoint) { + throw new Error('API endpoint not configured. Run: ograph config set endpoint ') + } + if (!this.token) { + throw new Error('Auth token not configured. Run: ograph config set token ') + } + } + + private async request(path: string, options: RequestInit = {}): Promise { + const url = `${this.endpoint}${path}` + + const headers = new Headers(options.headers) + headers.set('Authorization', `Bearer ${this.token}`) + if (options.body && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json') + } + + try { + const response = await fetch(url, { ...options, headers }) + const result = await response.json() + + if (!response.ok) { + if (response.status === 401) { + throw new Error('Authentication failed. Check your token.') + } + const errorMessage = (result as { error?: string }).error ?? `HTTP ${response.status}: ${response.statusText}` + throw new Error(errorMessage) + } + + return result as T + } catch (error) { + if (error instanceof Error && error.message.includes('fetch')) { + throw new Error(`Cannot reach OGraph API at ${this.endpoint}`) + } + throw error + } + } + + // ─── Object-Defs ─────────────────────────────────────────────────────────────── + + async listObjectDefs(): Promise { + const res = await this.request<{ object_defs: ObjectDef[] }>('/object-defs') + return res.object_defs + } + + async createObjectDef(name: string): Promise { + return this.request('/object-defs', { + method: 'POST', + body: JSON.stringify({ name }), + }) + } + + // ─── Objects ─────────────────────────────────────────────────────────────────── + + async createObject(type: string): Promise { + return this.request('/objects', { + method: 'POST', + body: JSON.stringify({ type }), + }) + } + + async getObject(id: number): Promise { + return this.request(`/objects/${id}`) + } + + async listObjects(type?: string): Promise { + const path = type ? `/objects?type=${encodeURIComponent(type)}` : '/objects' + const res = await this.request<{ objects: ObjectInstance[] }>(path) + return res.objects + } + + // ─── Event-Defs ─────────────────────────────────────────────────────────────── + + async listEventDefs(): Promise { + const res = await this.request<{ event_defs: EventDef[] }>('/event-defs') + return res.event_defs + } + + async createEventDef(name: string, schema: { properties: Record }): Promise { + return this.request('/event-defs', { + method: 'POST', + body: JSON.stringify({ name, schema }), + }) + } + + // ─── Events ──────────────────────────────────────────────────────────────────── + + async emitEvent(type: string, payload: Record): Promise { + return this.request('/events', { + method: 'POST', + body: JSON.stringify({ type, payload }), + }) + } + + async getEvent(id: number): Promise { + return this.request(`/events/${id}`) + } + + async findEventsByRef(ref: number): Promise { + const res = await this.request<{ events: OEvent[] }>(`/events?ref=${ref}`) + return res.events + } + + // ─── Projection-Defs ────────────────────────────────────────────────────────── + + async listProjectionDefs(): Promise { + const res = await this.request<{ projection_defs: ProjectionDef[] }>('/projection-defs') + return res.projection_defs + } + + async createProjectionDef( + name: string, + sources: ProjectionDefSource[], + params: Record, + value_schema: { type: string }, + initial_value: unknown, + ): Promise { + return this.request('/projection-defs', { + method: 'POST', + body: JSON.stringify({ name, sources, params, value_schema, initial_value }), + }) + } + + // ─── Projections ────────────────────────────────────────────────────────────── + + async getProjection(name: string, params?: Record): Promise { + const qs = params && Object.keys(params).length > 0 ? '?' + new URLSearchParams(params).toString() : '' + const res = await this.request<{ value: unknown }>(`/projections/${encodeURIComponent(name)}${qs}`) + return res.value + } + + // ─── Reactions ──────────────────────────────────────────────────────────────── + + async createReaction( + projectionDef: string, + params: Record, + options: { + action?: 'webhook' | 'emit_event' + webhook_url?: string + emit_event_type?: string + emit_payload_template?: string + }, + ): Promise { + const action = options.action ?? 'webhook' + return this.request('/reactions', { + method: 'POST', + body: JSON.stringify({ + projection_def: projectionDef, + params, + action, + webhook_url: options.webhook_url, + emit_event_type: options.emit_event_type, + emit_payload_template: options.emit_payload_template, + }), + }) + } + + async listReactions(): Promise { + const res = await this.request<{ reactions: Reaction[] }>('/reactions') + return res.reactions + } + + async deleteReaction(id: number): Promise<{ ok: boolean }> { + return this.request<{ ok: boolean }>(`/reactions/${id}`, { method: 'DELETE' }) + } + + // ─── Health ──────────────────────────────────────────────────────────────────── + + async health(): Promise { + return this.request('/health') + } +} diff --git a/packages/cli/src/commands/config.ts b/packages/cli/src/commands/config.ts new file mode 100644 index 0000000..0576af6 --- /dev/null +++ b/packages/cli/src/commands/config.ts @@ -0,0 +1,103 @@ +// Configuration commands +import { Command } from 'commander' +import { loadConfig, setConfigValue } from '../config.js' + +// ─── Colors ──────────────────────────────────────────────────────────────────── + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + yellow: '\x1b[33m', + red: '\x1b[31m', +} + +function ok(msg: string) { + console.log(`${c.green}✓${c.reset} ${msg}`) +} +function info(msg: string) { + console.log(`${c.cyan}ℹ${c.reset} ${msg}`) +} +function warn(msg: string) { + console.log(`${c.yellow}⚠${c.reset} ${msg}`) +} +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +// ─── Commands ────────────────────────────────────────────────────────────────── + +export function createConfigCommand(): Command { + const config = new Command('config') + config.description('Configuration management') + + // config set + const setCmd = new Command('set') + setCmd.description('Set configuration value') + setCmd.argument('', 'Configuration key (endpoint|token)') + setCmd.argument('', 'Configuration value') + setCmd.action(async (key: string, value: string) => { + try { + if (!['endpoint', 'token'].includes(key)) { + fail(`Invalid config key: ${key}. Valid keys: endpoint, token`) + process.exit(1) + } + + await setConfigValue(key as 'endpoint' | 'token', value) + + if (key === 'token') { + ok(`Set ${key}: ${value.slice(0, 8)}${'*'.repeat(Math.max(0, value.length - 8))}`) + } else { + ok(`Set ${key}: ${value}`) + } + } catch (error) { + fail(`Failed to set config: ${error}`) + process.exit(1) + } + }) + + // config show + const showCmd = new Command('show') + showCmd.description('Show current configuration') + showCmd.action(async () => { + try { + const config = await loadConfig() + + if (Object.keys(config).length === 0) { + warn('No configuration found') + info('Use "ograph config set " to configure') + return + } + + console.log('Current configuration:') + console.log() + + if (config.endpoint) { + console.log(` endpoint: ${config.endpoint}`) + } + + if (config.token) { + const maskedToken = config.token.slice(0, 8) + '*'.repeat(Math.max(0, config.token.length - 8)) + console.log(` token: ${maskedToken}`) + } + + // Show missing required config + const missing = [] + if (!config.endpoint) missing.push('endpoint') + if (!config.token) missing.push('token') + + if (missing.length > 0) { + console.log() + warn(`Missing required config: ${missing.join(', ')}`) + } + } catch (error) { + fail(`Failed to load config: ${error}`) + process.exit(1) + } + }) + + config.addCommand(setCmd) + config.addCommand(showCmd) + + return config +} diff --git a/packages/cli/src/commands/event-defs.ts b/packages/cli/src/commands/event-defs.ts new file mode 100644 index 0000000..c85a640 --- /dev/null +++ b/packages/cli/src/commands/event-defs.ts @@ -0,0 +1,80 @@ +// event-defs commands +import { Command } from 'commander' +import { OGraphClient } from '../client.js' +import type { EventDefProperty } from '../client.js' + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createEventDefsCommand(): Command { + const cmd = new Command('event-defs') + cmd.description('Manage event type definitions') + cmd.option('--json', 'output raw JSON') + + // Default action: list + cmd.action(async (opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const defs = await client.listEventDefs() + if (opts.json) { + console.log(JSON.stringify(defs, null, 2)) + return + } + if (defs.length === 0) { + console.log('No event types defined.') + return + } + console.log(`${c.bold}Event Types${c.reset}`) + for (const d of defs) { + const props = Object.keys(d.schema?.properties ?? {}).join(', ') + console.log(` ${c.cyan}${d.name}${c.reset} {${props}}`) + } + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // create subcommand + const create = new Command('create') + create.description('Register or update an event type') + create.argument('', 'Event type name') + create.requiredOption('--schema ', 'Schema JSON, e.g. {"prop": {"type": "string"}}') + create.option('--json', 'output raw JSON') + create.action(async (name: string, opts: { schema: string; json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + let properties: Record + try { + properties = JSON.parse(opts.schema) as Record + } catch { + fail('Invalid JSON for --schema') + process.exit(1) + return + } + const def = await client.createEventDef(name, { properties }) + if (opts.json) { + console.log(JSON.stringify(def, null, 2)) + return + } + console.log(`${c.green}✓${c.reset} Registered event type: ${c.cyan}${def.name}${c.reset}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + cmd.addCommand(create) + return cmd +} diff --git a/packages/cli/src/commands/events.ts b/packages/cli/src/commands/events.ts new file mode 100644 index 0000000..a4b6a98 --- /dev/null +++ b/packages/cli/src/commands/events.ts @@ -0,0 +1,109 @@ +// events commands +import { Command } from 'commander' +import { OGraphClient } from '../client.js' + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createEventsCommand(): Command { + const cmd = new Command('events') + cmd.description('Emit and query events') + + // emit + const emit = new Command('emit') + emit.description('Emit an event') + emit.argument('', 'Event type name') + emit.requiredOption('--payload ', 'Event payload JSON') + emit.option('--json', 'output raw JSON') + emit.action(async (type: string, opts: { payload: string; json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + let payload: Record + try { + payload = JSON.parse(opts.payload) as Record + } catch { + fail('Invalid JSON for --payload') + process.exit(1) + return + } + const result = await client.emitEvent(type, payload) + if (opts.json) { + console.log(JSON.stringify(result, null, 2)) + return + } + console.log( + `${c.green}✓${c.reset} Emitted event: ${c.cyan}${result.event.id}${c.reset} (reactions_fired: ${result.reactions_fired})`, + ) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // get + const get = new Command('get') + get.description('Get an event by ID') + get.argument('', 'Event ID') + get.option('--json', 'output raw JSON') + get.action(async (id: string, opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const event = await client.getEvent(parseInt(id, 10)) + if (opts.json) { + console.log(JSON.stringify(event, null, 2)) + return + } + console.log(`${c.bold}Event${c.reset}`) + console.log(` id: ${c.cyan}${event.id}${c.reset}`) + console.log(` type_hash: ${event.type_hash}`) + console.log(` payload: ${JSON.stringify(event.payload)}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // find + const find = new Command('find') + find.description('Find events by object reference') + find.requiredOption('--ref ', 'Object ID to find related events') + find.option('--json', 'output raw JSON') + find.action(async (opts: { ref: string; json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const events = await client.findEventsByRef(parseInt(opts.ref, 10)) + if (opts.json) { + console.log(JSON.stringify(events, null, 2)) + return + } + if (events.length === 0) { + console.log('No events found for this object.') + return + } + console.log(`${c.bold}Events${c.reset}`) + for (const e of events) { + console.log(` ${c.cyan}${e.id}${c.reset} ${e.type_hash}`) + } + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + cmd.addCommand(emit) + cmd.addCommand(get) + cmd.addCommand(find) + return cmd +} diff --git a/packages/cli/src/commands/health.ts b/packages/cli/src/commands/health.ts new file mode 100644 index 0000000..de497de --- /dev/null +++ b/packages/cli/src/commands/health.ts @@ -0,0 +1,40 @@ +// health command +import { Command } from 'commander' +import { OGraphClient } from '../client.js' + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createHealthCommand(): Command { + const cmd = new Command('health') + cmd.description('Check API health') + cmd.option('--json', 'output raw JSON') + cmd.action(async (opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const result = await client.health() + if (opts.json) { + console.log(JSON.stringify(result, null, 2)) + return + } + const statusColor = result.status === 'ok' ? c.green : c.red + console.log(`${c.bold}Health${c.reset}`) + console.log(` status: ${statusColor}${result.status}${c.reset}`) + console.log(` version: ${c.cyan}${result.version}${c.reset}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + return cmd +} diff --git a/packages/cli/src/commands/object-defs.ts b/packages/cli/src/commands/object-defs.ts new file mode 100644 index 0000000..8bbd5e0 --- /dev/null +++ b/packages/cli/src/commands/object-defs.ts @@ -0,0 +1,70 @@ +// object-defs commands +import { Command } from 'commander' +import { OGraphClient } from '../client.js' + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + yellow: '\x1b[33m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createObjectDefsCommand(): Command { + const cmd = new Command('object-defs') + cmd.description('Manage object type definitions') + cmd.option('--json', 'output raw JSON') + + // Default action: list + cmd.action(async (opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const defs = await client.listObjectDefs() + if (opts.json) { + console.log(JSON.stringify(defs, null, 2)) + return + } + if (defs.length === 0) { + console.log('No object types defined.') + return + } + console.log(`${c.bold}Object Types${c.reset}`) + for (const d of defs) { + console.log(` ${c.cyan}${d.name}${c.reset}`) + } + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // create subcommand + const create = new Command('create') + create.description('Register a new object type') + create.argument('', 'Object type name') + create.option('--json', 'output raw JSON') + create.action(async (name: string, opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const def = await client.createObjectDef(name) + if (opts.json) { + console.log(JSON.stringify(def, null, 2)) + return + } + console.log(`${c.green}✓${c.reset} Created object type: ${c.cyan}${def.name}${c.reset}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + cmd.addCommand(create) + return cmd +} diff --git a/packages/cli/src/commands/objects.ts b/packages/cli/src/commands/objects.ts new file mode 100644 index 0000000..0b29dd8 --- /dev/null +++ b/packages/cli/src/commands/objects.ts @@ -0,0 +1,97 @@ +// objects commands +import { Command } from 'commander' +import { OGraphClient } from '../client.js' + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createObjectsCommand(): Command { + const cmd = new Command('objects') + cmd.description('Manage object instances') + + // create + const create = new Command('create') + create.description('Create a new object instance') + create.argument('', 'Object type') + create.option('--json', 'output raw JSON') + create.action(async (type: string, opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const obj = await client.createObject(type) + if (opts.json) { + console.log(JSON.stringify(obj, null, 2)) + return + } + console.log(`${c.green}✓${c.reset} Created object: ${c.cyan}${obj.id}${c.reset} (type: ${obj.type})`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // get + const get = new Command('get') + get.description('Get an object by ID') + get.argument('', 'Object ID') + get.option('--json', 'output raw JSON') + get.action(async (id: string, opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const obj = await client.getObject(parseInt(id, 10)) + if (opts.json) { + console.log(JSON.stringify(obj, null, 2)) + return + } + console.log(`${c.bold}Object${c.reset}`) + console.log(` id: ${c.cyan}${obj.id}${c.reset}`) + console.log(` type: ${obj.type}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // list + const list = new Command('list') + list.description('List object instances') + list.option('--type ', 'Filter by type') + list.option('--json', 'output raw JSON') + list.action(async (opts: { type?: string; json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const objects = await client.listObjects(opts.type) + if (opts.json) { + console.log(JSON.stringify(objects, null, 2)) + return + } + if (objects.length === 0) { + console.log('No objects found.') + return + } + console.log(`${c.bold}Objects${c.reset}`) + for (const o of objects) { + console.log(` ${c.cyan}${o.id}${c.reset} ${o.type}`) + } + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + cmd.addCommand(create) + cmd.addCommand(get) + cmd.addCommand(list) + return cmd +} diff --git a/packages/cli/src/commands/projection-defs.ts b/packages/cli/src/commands/projection-defs.ts new file mode 100644 index 0000000..2f4b4c3 --- /dev/null +++ b/packages/cli/src/commands/projection-defs.ts @@ -0,0 +1,144 @@ +// projection-defs commands +import { Command } from 'commander' +import { OGraphClient } from '../client.js' +import type { ProjectionDefSource } from '../client.js' + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createProjectionDefsCommand(): Command { + const cmd = new Command('projection-defs') + cmd.description('Manage projection definitions') + cmd.option('--json', 'output raw JSON') + + // Default action: list + cmd.action(async (opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const defs = await client.listProjectionDefs() + if (opts.json) { + console.log(JSON.stringify(defs, null, 2)) + return + } + if (defs.length === 0) { + console.log('No projection definitions found.') + return + } + console.log(`${c.bold}Projection Definitions${c.reset}`) + for (const d of defs) { + console.log(` ${c.cyan}${d.name}${c.reset} sources: ${d.sources?.length ?? 0}`) + } + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // create subcommand + const create = new Command('create') + create.description('Register or update a projection definition') + create.argument('', 'Projection name') + create.option( + '--source ', + 'Source entry JSON (repeatable), e.g. \'{"event_def":"UserCreated","bindings":{"user":"$subject"},"expression":"1"}\'', + (val: string, acc: string[]) => { + acc.push(val) + return acc + }, + [] as string[], + ) + create.option('--params ', 'Params JSON, e.g. {"obj": {"type": "ref"}}') + create.option('--value-schema ', 'Value schema JSON, e.g. {"type": "number"}') + create.option('--initial-value ', 'Initial value JSON') + create.option('--json', 'output raw JSON') + create.action( + async ( + name: string, + opts: { + source: string[] + params?: string + valueSchema?: string + initialValue?: string + json?: boolean + }, + ) => { + const client = new OGraphClient() + try { + await client.init() + + if (!opts.source || opts.source.length === 0) { + fail('At least one --source is required') + process.exit(1) + return + } + + const sources: ProjectionDefSource[] = [] + for (const s of opts.source) { + try { + sources.push(JSON.parse(s) as ProjectionDefSource) + } catch { + fail(`Invalid JSON for --source: ${s}`) + process.exit(1) + return + } + } + + let params: Record = {} + if (opts.params) { + try { + params = JSON.parse(opts.params) as Record + } catch { + fail('Invalid JSON for --params') + process.exit(1) + return + } + } + + let value_schema: { type: string } = { type: 'number' } + if (opts.valueSchema) { + try { + value_schema = JSON.parse(opts.valueSchema) as { type: string } + } catch { + fail('Invalid JSON for --value-schema') + process.exit(1) + return + } + } + + let initial_value: unknown = 0 + if (opts.initialValue !== undefined) { + try { + initial_value = JSON.parse(opts.initialValue) as unknown + } catch { + fail('Invalid JSON for --initial-value') + process.exit(1) + return + } + } + + const result = await client.createProjectionDef(name, sources, params, value_schema, initial_value) + if (opts.json) { + console.log(JSON.stringify(result, null, 2)) + return + } + console.log(`${c.green}✓${c.reset} Registered projection: ${c.cyan}${result.name}${c.reset}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }, + ) + + cmd.addCommand(create) + return cmd +} diff --git a/packages/cli/src/commands/projections.ts b/packages/cli/src/commands/projections.ts new file mode 100644 index 0000000..8c71903 --- /dev/null +++ b/packages/cli/src/commands/projections.ts @@ -0,0 +1,63 @@ +// projections commands +import { Command } from 'commander' +import { OGraphClient } from '../client.js' + +const c = { + reset: '\x1b[0m', + cyan: '\x1b[36m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createProjectionsCommand(): Command { + const cmd = new Command('projections') + cmd.description('Query projection values') + + // get + const get = new Command('get') + get.description('Get the current value of a projection') + get.argument('', 'Projection name') + get.option( + '--param ', + 'Query parameters (repeatable)', + (val, prev: string[]) => { + prev.push(val) + return prev + }, + [] as string[], + ) + get.option('--json', 'output raw JSON') + get.action(async (name: string, opts: { param: string[]; json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const params: Record = {} + for (const p of opts.param) { + const idx = p.indexOf('=') + if (idx === -1) { + fail(`Invalid --param format: "${p}" (expected key=value)`) + process.exit(1) + return + } + params[p.slice(0, idx)] = p.slice(idx + 1) + } + const value = await client.getProjection(name, params) + if (opts.json) { + console.log(JSON.stringify({ value }, null, 2)) + return + } + console.log(`${c.bold}Projection${c.reset} ${c.cyan}${name}${c.reset}`) + console.log(` value: ${JSON.stringify(value)}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + cmd.addCommand(get) + return cmd +} diff --git a/packages/cli/src/commands/reactions.ts b/packages/cli/src/commands/reactions.ts new file mode 100644 index 0000000..0f4e0aa --- /dev/null +++ b/packages/cli/src/commands/reactions.ts @@ -0,0 +1,145 @@ +// reactions commands +import { Command } from 'commander' +import { OGraphClient } from '../client.js' + +const c = { + reset: '\x1b[0m', + green: '\x1b[32m', + cyan: '\x1b[36m', + red: '\x1b[31m', + bold: '\x1b[1m', +} + +function fail(msg: string) { + console.error(`${c.red}✗${c.reset} ${msg}`) +} + +export function createReactionsCommand(): Command { + const cmd = new Command('reactions') + cmd.description('Manage reactions (projection triggers)') + + // create + const create = new Command('create') + create.description('Create a new reaction') + create.requiredOption('--projection ', 'Projection def name') + create.option('--params ', 'Params JSON') + create.option('--action ', 'Action type: webhook (default) or emit_event', 'webhook') + create.option('--webhook ', 'Webhook URL (required when --action webhook)') + create.option('--emit-type ', 'Event type to emit (required when --action emit_event)') + create.option('--emit-template ', 'JSONata template for emitted event payload') + create.option('--json', 'output raw JSON') + create.action( + async (opts: { + projection: string + params?: string + action: string + webhook?: string + emitType?: string + emitTemplate?: string + json?: boolean + }) => { + const client = new OGraphClient() + try { + await client.init() + let params: Record = {} + if (opts.params) { + try { + params = JSON.parse(opts.params) as Record + } catch { + fail('Invalid JSON for --params') + process.exit(1) + return + } + } + + const action = opts.action as 'webhook' | 'emit_event' + if (action !== 'webhook' && action !== 'emit_event') { + fail('--action must be "webhook" or "emit_event"') + process.exit(1) + return + } + if (action === 'webhook' && !opts.webhook) { + fail('--webhook is required when --action webhook') + process.exit(1) + return + } + if (action === 'emit_event' && !opts.emitType) { + fail('--emit-type is required when --action emit_event') + process.exit(1) + return + } + + const reaction = await client.createReaction(opts.projection, params, { + action, + webhook_url: opts.webhook, + emit_event_type: opts.emitType, + emit_payload_template: opts.emitTemplate, + }) + if (opts.json) { + console.log(JSON.stringify(reaction, null, 2)) + return + } + console.log( + `${c.green}✓${c.reset} Created reaction: ${c.cyan}${reaction.id}${c.reset} (action: ${reaction.action})`, + ) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }, + ) + + // list + const list = new Command('list') + list.description('List all reactions') + list.option('--json', 'output raw JSON') + list.action(async (opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const reactions = await client.listReactions() + if (opts.json) { + console.log(JSON.stringify(reactions, null, 2)) + return + } + if (reactions.length === 0) { + console.log('No reactions found.') + return + } + console.log(`${c.bold}Reactions${c.reset}`) + for (const r of reactions) { + const target = r.action === 'emit_event' ? `emit:${r.emit_event_type}` : (r.webhook_url ?? '') + console.log(` ${c.cyan}${r.id}${c.reset} [${r.action}] ${target}`) + } + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + // delete + const del = new Command('delete') + del.description('Delete a reaction') + del.argument('', 'Reaction ID') + del.option('--json', 'output raw JSON') + del.action(async (id: string, opts: { json?: boolean }) => { + const client = new OGraphClient() + try { + await client.init() + const result = await client.deleteReaction(parseInt(id, 10)) + if (opts.json) { + console.log(JSON.stringify(result, null, 2)) + return + } + console.log(`${c.green}✓${c.reset} Deleted reaction: ${id}`) + } catch (err) { + fail(String(err instanceof Error ? err.message : err)) + process.exit(1) + } + }) + + cmd.addCommand(create) + cmd.addCommand(list) + cmd.addCommand(del) + return cmd +} diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts new file mode 100644 index 0000000..8051474 --- /dev/null +++ b/packages/cli/src/config.ts @@ -0,0 +1,65 @@ +// Configuration management for OGraph CLI +import { readFile, writeFile, mkdir } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { homedir } from 'node:os' + +// ─── Types ───────────────────────────────────────────────────────────────────── + +export interface Config { + endpoint?: string + token?: string +} + +// ─── Paths ───────────────────────────────────────────────────────────────────── + +function getConfigDir(): string { + return process.env.OGRAPH_CONFIG_DIR || join(homedir(), '.config', 'ograph') +} + +function getConfigPath(): string { + return join(getConfigDir(), 'config.json') +} + +// ─── Config Functions ────────────────────────────────────────────────────────── + +export async function loadConfig(): Promise { + const CONFIG_PATH = getConfigPath() + if (!existsSync(CONFIG_PATH)) { + return {} + } + + try { + const data = await readFile(CONFIG_PATH, 'utf-8') + return JSON.parse(data) + } catch (error) { + console.warn('Warning: config.json is malformed, using empty config') + return {} + } +} + +export async function saveConfig(config: Config): Promise { + const CONFIG_DIR = getConfigDir() + const CONFIG_PATH = getConfigPath() + try { + // Ensure config directory exists + if (!existsSync(CONFIG_DIR)) { + await mkdir(CONFIG_DIR, { recursive: true }) + } + + await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8') + } catch (error) { + throw new Error(`Failed to save config: ${error}`) + } +} + +export async function setConfigValue(key: keyof Config, value: string): Promise { + const config = await loadConfig() + config[key] = value + await saveConfig(config) +} + +export async function getConfigValue(key: keyof Config): Promise { + const config = await loadConfig() + return config[key] +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..d4d26e1 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +// @uncaged/ograph-cli - OGraph CLI for Event-Sourced API v2.4 + +import { Command } from 'commander' +import { createConfigCommand } from './commands/config.js' +import { createObjectDefsCommand } from './commands/object-defs.js' +import { createObjectsCommand } from './commands/objects.js' +import { createEventDefsCommand } from './commands/event-defs.js' +import { createEventsCommand } from './commands/events.js' +import { createProjectionDefsCommand } from './commands/projection-defs.js' +import { createProjectionsCommand } from './commands/projections.js' +import { createReactionsCommand } from './commands/reactions.js' +import { createHealthCommand } from './commands/health.js' + +// ─── Main Program ────────────────────────────────────────────────────────────── + +const program = new Command() + +program.name('ograph').description('OGraph CLI for Event-Sourced object-graph operations').version('0.1.0') + +program.addCommand(createConfigCommand()) +program.addCommand(createObjectDefsCommand()) +program.addCommand(createObjectsCommand()) +program.addCommand(createEventDefsCommand()) +program.addCommand(createEventsCommand()) +program.addCommand(createProjectionDefsCommand()) +program.addCommand(createProjectionsCommand()) +program.addCommand(createReactionsCommand()) +program.addCommand(createHealthCommand()) + +program.parse() diff --git a/packages/cli/test/client.test.ts b/packages/cli/test/client.test.ts new file mode 100644 index 0000000..1175bc8 --- /dev/null +++ b/packages/cli/test/client.test.ts @@ -0,0 +1,374 @@ +// Test suite for OGraphClient v2.4 +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { OGraphClient } from '../src/client.js' + +// Mock the config module +vi.mock('../src/config.js', () => ({ + loadConfig: vi.fn(), +})) + +import { loadConfig } from '../src/config.js' +const mockLoadConfig = vi.mocked(loadConfig) + +// Mock fetch globally +const mockFetch = vi.fn() +global.fetch = mockFetch + +describe('OGraphClient v2.4', () => { + let client: OGraphClient + + beforeEach(() => { + client = new OGraphClient() + vi.clearAllMocks() + }) + + // ─── init ──────────────────────────────────────────────────────────────────── + + describe('init', () => { + it('should throw error if endpoint not configured', async () => { + mockLoadConfig.mockResolvedValue({}) + await expect(client.init()).rejects.toThrow('API endpoint not configured. Run: ograph config set endpoint ') + }) + + it('should throw error if token not configured', async () => { + mockLoadConfig.mockResolvedValue({ endpoint: 'https://api.example.com' }) + await expect(client.init()).rejects.toThrow('Auth token not configured. Run: ograph config set token ') + }) + + it('should initialize successfully with both endpoint and token', async () => { + mockLoadConfig.mockResolvedValue({ endpoint: 'https://api.example.com', token: 'test-token' }) + await expect(client.init()).resolves.not.toThrow() + }) + }) + + // ─── helpers ───────────────────────────────────────────────────────────────── + + async function initClient() { + mockLoadConfig.mockResolvedValue({ endpoint: 'https://api.example.com', token: 'test-token' }) + await client.init() + } + + function mockOk(data: unknown) { + mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve(data) }) + } + + function mockFail(status: number, error: string) { + mockFetch.mockResolvedValue({ + ok: false, + status, + statusText: error, + json: () => Promise.resolve({ error }), + }) + } + + // ─── object-defs ───────────────────────────────────────────────────────────── + + describe('listObjectDefs', () => { + it('returns object_defs array', async () => { + await initClient() + mockOk({ object_defs: [{ name: 'user' }, { name: 'task' }] }) + const result = await client.listObjectDefs() + expect(result).toEqual([{ name: 'user' }, { name: 'task' }]) + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/object-defs') + }) + }) + + describe('createObjectDef', () => { + it('POSTs to /object-defs with name', async () => { + await initClient() + mockOk({ name: 'user', created_at: 1234 }) + const result = await client.createObjectDef('user') + expect(result.name).toBe('user') + const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] + expect(url).toBe('https://api.example.com/object-defs') + expect(opts.method).toBe('POST') + expect(JSON.parse(opts.body as string)).toEqual({ name: 'user' }) + }) + }) + + // ─── objects ───────────────────────────────────────────────────────────────── + + describe('createObject', () => { + it('POSTs to /objects with type only (no custom id)', async () => { + await initClient() + mockOk({ id: 1, type: 'user', created_at: 1234 }) + const result = await client.createObject('user') + expect(result.id).toBe(1) + expect(typeof result.id).toBe('number') + const body = JSON.parse(mockFetch.mock.calls[0][1].body as string) + expect(body).toEqual({ type: 'user' }) + }) + }) + + describe('getObject', () => { + it('GETs /objects/:id with numeric id', async () => { + await initClient() + mockOk({ id: 42, type: 'user', created_at: 1234 }) + const result = await client.getObject(42) + expect(result.id).toBe(42) + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/objects/42') + }) + }) + + describe('listObjects', () => { + it('GETs /objects without filter', async () => { + await initClient() + mockOk({ objects: [{ id: 1, type: 'user', created_at: 1234 }] }) + const result = await client.listObjects() + expect(result).toHaveLength(1) + expect(result[0].id).toBe(1) + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/objects') + }) + + it('GETs /objects?type=user with filter', async () => { + await initClient() + mockOk({ objects: [] }) + await client.listObjects('user') + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/objects?type=user') + }) + }) + + // ─── event-defs ────────────────────────────────────────────────────────────── + + describe('listEventDefs', () => { + it('returns event_defs array', async () => { + await initClient() + mockOk({ event_defs: [{ name: 'UserCreated', schema: { properties: {} } }] }) + const result = await client.listEventDefs() + expect(result[0].name).toBe('UserCreated') + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/event-defs') + }) + }) + + describe('createEventDef', () => { + it('POSTs to /event-defs with name and schema', async () => { + await initClient() + const schema = { properties: { user: { type: 'ref' as const } } } + mockOk({ name: 'UserCreated', schema, hash: 'abc123' }) + const result = await client.createEventDef('UserCreated', schema) + expect(result.name).toBe('UserCreated') + const body = JSON.parse(mockFetch.mock.calls[0][1].body as string) + expect(body).toEqual({ name: 'UserCreated', schema }) + }) + }) + + // ─── events ────────────────────────────────────────────────────────────────── + + describe('emitEvent', () => { + it('POSTs to /events and returns {event, reactions_fired}', async () => { + await initClient() + mockOk({ event: { id: 1, type_hash: 'abc123', payload: { user: 1 }, created_at: 1234 }, reactions_fired: 0 }) + const result = await client.emitEvent('UserCreated', { user: 1 }) + expect(result.event.id).toBe(1) + expect(typeof result.event.id).toBe('number') + expect(result.event.type_hash).toBe('abc123') + expect(result.reactions_fired).toBe(0) + const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] + expect(url).toBe('https://api.example.com/events') + expect(opts.method).toBe('POST') + const body = JSON.parse(opts.body as string) + expect(body).toEqual({ type: 'UserCreated', payload: { user: 1 } }) + }) + }) + + describe('getEvent', () => { + it('GETs /events/:id with numeric id', async () => { + await initClient() + mockOk({ id: 5, type_hash: 'abc123', payload: {}, created_at: 1234 }) + const result = await client.getEvent(5) + expect(result.id).toBe(5) + expect(typeof result.id).toBe('number') + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/events/5') + }) + }) + + describe('findEventsByRef', () => { + it('GETs /events?ref=', async () => { + await initClient() + mockOk({ events: [{ id: 1, type_hash: 'abc123', payload: {}, created_at: 1234 }] }) + const result = await client.findEventsByRef(7) + expect(result).toHaveLength(1) + expect(result[0].id).toBe(1) + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/events?ref=7') + }) + }) + + // ─── projection-defs ───────────────────────────────────────────────────────── + + describe('listProjectionDefs', () => { + it('returns projection_defs array', async () => { + await initClient() + mockOk({ + projection_defs: [ + { + name: 'userCount', + sources: [{ event_def: 'UserCreated', bindings: {}, expression: '$count + 1' }], + value_schema: { type: 'number' }, + initial_value: 0, + }, + ], + }) + const result = await client.listProjectionDefs() + expect(result[0].name).toBe('userCount') + }) + }) + + describe('createProjectionDef', () => { + it('POSTs to /projection-defs with name, sources, params, value_schema, initial_value', async () => { + await initClient() + const sources = [{ event_def: 'UserCreated', bindings: {}, expression: '$count + 1' }] + const params = {} + const value_schema = { type: 'number' } + const initial_value = 0 + mockOk({ name: 'userCount', sources, params, value_schema, initial_value }) + const result = await client.createProjectionDef('userCount', sources, params, value_schema, initial_value) + expect(result.name).toBe('userCount') + const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] + expect(url).toBe('https://api.example.com/projection-defs') + expect(opts.method).toBe('POST') + const body = JSON.parse(opts.body as string) + expect(body.name).toBe('userCount') + expect(body.sources).toEqual(sources) + expect(body.value_schema).toEqual(value_schema) + expect(body.initial_value).toBe(0) + }) + }) + + // ─── projections ───────────────────────────────────────────────────────────── + + describe('getProjection', () => { + it('GETs /projections/:name', async () => { + await initClient() + mockOk({ value: 42 }) + const value = await client.getProjection('userCount') + expect(value).toBe(42) + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/projections/userCount') + }) + + it('appends params as query string', async () => { + await initClient() + mockOk({ value: 5 }) + await client.getProjection('tasksByUser', { userId: 'user_01ABC' }) + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/projections/tasksByUser?userId=user_01ABC') + }) + }) + + // ─── reactions ─────────────────────────────────────────────────────────────── + + describe('createReaction (webhook)', () => { + it('POSTs to /reactions with action=webhook', async () => { + await initClient() + mockOk({ + id: 1, + projection_def_hash: 'hashABC', + params_hash: 'paramHash', + params: {}, + action: 'webhook', + webhook_url: 'https://example.com/hook', + created_at: 1234, + }) + const result = await client.createReaction( + 'userCount', + {}, + { action: 'webhook', webhook_url: 'https://example.com/hook' }, + ) + expect(result.id).toBe(1) + expect(typeof result.id).toBe('number') + expect(result.action).toBe('webhook') + expect(result.webhook_url).toBe('https://example.com/hook') + const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] + expect(url).toBe('https://api.example.com/reactions') + expect(opts.method).toBe('POST') + const body = JSON.parse(opts.body as string) + expect(body.projection_def).toBe('userCount') + expect(body.action).toBe('webhook') + expect(body.webhook_url).toBe('https://example.com/hook') + }) + }) + + describe('createReaction (emit_event)', () => { + it('POSTs to /reactions with action=emit_event', async () => { + await initClient() + mockOk({ + id: 2, + projection_def_hash: 'hashABC', + params_hash: 'paramHash', + params: {}, + action: 'emit_event', + emit_event_type: 'TaskCompleted', + created_at: 1234, + }) + const result = await client.createReaction( + 'taskStatus', + {}, + { + action: 'emit_event', + emit_event_type: 'TaskCompleted', + }, + ) + expect(result.id).toBe(2) + expect(result.action).toBe('emit_event') + expect(result.emit_event_type).toBe('TaskCompleted') + const body = JSON.parse(mockFetch.mock.calls[0][1].body as string) + expect(body.action).toBe('emit_event') + expect(body.emit_event_type).toBe('TaskCompleted') + }) + }) + + describe('listReactions', () => { + it('GETs /reactions', async () => { + await initClient() + mockOk({ reactions: [] }) + const result = await client.listReactions() + expect(Array.isArray(result)).toBe(true) + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/reactions') + }) + }) + + describe('deleteReaction', () => { + it('DELETEs /reactions/:id with numeric id', async () => { + await initClient() + mockOk({ ok: true }) + const result = await client.deleteReaction(3) + expect(result.ok).toBe(true) + const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] + expect(url).toBe('https://api.example.com/reactions/3') + expect(opts.method).toBe('DELETE') + }) + }) + + // ─── health ────────────────────────────────────────────────────────────────── + + describe('health', () => { + it('GETs /health', async () => { + await initClient() + mockOk({ status: 'ok', version: '2.4.0' }) + const result = await client.health() + expect(result.status).toBe('ok') + expect(result.version).toBe('2.4.0') + expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/health') + }) + }) + + // ─── error handling ────────────────────────────────────────────────────────── + + describe('error handling', () => { + it('throws on 401', async () => { + await initClient() + mockFail(401, 'Unauthorized') + await expect(client.listObjectDefs()).rejects.toThrow('Authentication failed. Check your token.') + }) + + it('throws API error message on non-2xx', async () => { + await initClient() + mockFail(400, 'Invalid request') + await expect(client.createObjectDef('bad')).rejects.toThrow('Invalid request') + }) + + it('throws connection error on fetch failure', async () => { + await initClient() + mockFetch.mockRejectedValue(new Error('fetch failed: connection refused')) + await expect(client.health()).rejects.toThrow('Cannot reach OGraph API at https://api.example.com') + }) + }) +}) diff --git a/packages/cli/test/config.test.ts b/packages/cli/test/config.test.ts new file mode 100644 index 0000000..4672ccc --- /dev/null +++ b/packages/cli/test/config.test.ts @@ -0,0 +1,72 @@ +// Configuration tests +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { rm } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { randomUUID } from 'node:crypto' +import { loadConfig, saveConfig, setConfigValue, getConfigValue } from '../src/config.js' + +let testDir: string + +beforeEach(() => { + testDir = join(tmpdir(), `ograph-test-${randomUUID()}`) + process.env.OGRAPH_CONFIG_DIR = testDir +}) + +afterEach(async () => { + delete process.env.OGRAPH_CONFIG_DIR + await rm(testDir, { recursive: true, force: true }) +}) + +describe('config management', () => { + it('should return empty config when file does not exist', async () => { + const config = await loadConfig() + expect(config).toEqual({}) + }) + + it('should save and load config correctly', async () => { + const testConfig = { + endpoint: 'https://test.example.com', + token: 'test-token-123', + } + + await saveConfig(testConfig) + const loadedConfig = await loadConfig() + + expect(loadedConfig).toEqual(testConfig) + }) + + it('should set individual config values', async () => { + await setConfigValue('endpoint', 'https://api.example.com') + await setConfigValue('token', 'secret-token') + + const config = await loadConfig() + + expect(config.endpoint).toBe('https://api.example.com') + expect(config.token).toBe('secret-token') + }) + + it('should get individual config values', async () => { + await saveConfig({ + endpoint: 'https://get.example.com', + token: 'get-token-456', + }) + + const endpoint = await getConfigValue('endpoint') + const token = await getConfigValue('token') + const missing = await getConfigValue('nonexistent' as any) + + expect(endpoint).toBe('https://get.example.com') + expect(token).toBe('get-token-456') + expect(missing).toBeUndefined() + }) + + it('should handle malformed config file gracefully', async () => { + const { writeFile, mkdir } = await import('node:fs/promises') + await mkdir(testDir, { recursive: true }) + await writeFile(join(testDir, 'config.json'), '{bad json!!!}', 'utf-8') + + const config = await loadConfig() + expect(config).toEqual({}) + }) +}) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..34f04c7 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "allowJs": true, + "sourceMap": false, + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/packages/engine/migrations/0006_v2.sql b/packages/engine/migrations/0006_v2.sql new file mode 100644 index 0000000..8b54e1b --- /dev/null +++ b/packages/engine/migrations/0006_v2.sql @@ -0,0 +1,44 @@ +-- Drop all v1 tables +DROP TABLE IF EXISTS subscriptions; +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS edges; +DROP TABLE IF EXISTS objects; +DROP TABLE IF EXISTS relation_types; +DROP TABLE IF EXISTS object_types; + +-- v2 Schema + +CREATE TABLE types ( + name TEXT PRIMARY KEY, + kind TEXT NOT NULL CHECK(kind IN ('obj', 'evt')), + label TEXT NOT NULL +); + +CREATE TABLE relation_types ( + name TEXT PRIMARY KEY, + from_kind TEXT NOT NULL CHECK(from_kind IN ('obj', 'evt')), + to_kind TEXT NOT NULL CHECK(to_kind IN ('obj', 'evt')), + inverse TEXT NOT NULL, + UNIQUE(inverse) +); + +CREATE TABLE nodes ( + oid TEXT PRIMARY KEY, + type TEXT NOT NULL REFERENCES types(name), + data TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_nodes_type ON nodes(type); +CREATE INDEX idx_nodes_created ON nodes(created_at); + +CREATE TABLE edges ( + oid TEXT PRIMARY KEY, + from_oid TEXT NOT NULL REFERENCES nodes(oid), + rel TEXT NOT NULL REFERENCES relation_types(name), + to_oid TEXT NOT NULL REFERENCES nodes(oid), + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), + UNIQUE(from_oid, rel, to_oid) +); +CREATE INDEX idx_edges_from ON edges(from_oid); +CREATE INDEX idx_edges_to ON edges(to_oid); +CREATE INDEX idx_edges_rel ON edges(rel); diff --git a/packages/engine/migrations/0007_reducers.sql b/packages/engine/migrations/0007_reducers.sql new file mode 100644 index 0000000..daa9506 --- /dev/null +++ b/packages/engine/migrations/0007_reducers.sql @@ -0,0 +1,30 @@ +CREATE TABLE reducers ( + name TEXT PRIMARY KEY, + driven_by TEXT NOT NULL, + params TEXT NOT NULL, + filter TEXT NOT NULL, + expression TEXT NOT NULL, + mode TEXT NOT NULL DEFAULT 'fold' CHECK(mode IN ('fold', 'latest', 'window')), + window_size INTEGER, + initial_value TEXT +); + +CREATE TABLE projections ( + reducer TEXT NOT NULL REFERENCES reducers(name), + params TEXT NOT NULL, + params_hash TEXT NOT NULL, + value TEXT, + updated_by TEXT, + updated_at INTEGER, + live INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (reducer, params_hash) +); + +CREATE TABLE event_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + evt_oid TEXT NOT NULL, + processed_reducers TEXT, + processed_reactions TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_event_log_created ON event_log(created_at); diff --git a/packages/engine/migrations/0008_simplify_reducers.sql b/packages/engine/migrations/0008_simplify_reducers.sql new file mode 100644 index 0000000..b5104bd --- /dev/null +++ b/packages/engine/migrations/0008_simplify_reducers.sql @@ -0,0 +1,15 @@ +-- 重建 reducers 表(SQLite 不支持 DROP COLUMN) +CREATE TABLE reducers_new ( + name TEXT PRIMARY KEY, + driven_by TEXT NOT NULL, + params TEXT NOT NULL, + filter TEXT NOT NULL, + expression TEXT NOT NULL, + initial_value TEXT +); + +INSERT INTO reducers_new (name, driven_by, params, filter, expression, initial_value) + SELECT name, driven_by, params, filter, expression, initial_value FROM reducers; + +DROP TABLE reducers; +ALTER TABLE reducers_new RENAME TO reducers; diff --git a/packages/engine/migrations/0009_reactions.sql b/packages/engine/migrations/0009_reactions.sql new file mode 100644 index 0000000..c843e2b --- /dev/null +++ b/packages/engine/migrations/0009_reactions.sql @@ -0,0 +1,10 @@ +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + reducer TEXT NOT NULL REFERENCES reducers(name), + params_hash TEXT, + condition TEXT, + worker_url TEXT NOT NULL, + config TEXT NOT NULL DEFAULT '{}', + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_reducer ON reactions(reducer); diff --git a/packages/engine/migrations/0010_v2.1.sql b/packages/engine/migrations/0010_v2.1.sql new file mode 100644 index 0000000..4dfc204 --- /dev/null +++ b/packages/engine/migrations/0010_v2.1.sql @@ -0,0 +1,65 @@ +-- Drop v2.0 tables +DROP TABLE IF EXISTS reactions; +DROP TABLE IF EXISTS projections; +DROP TABLE IF EXISTS reducers; +DROP TABLE IF EXISTS event_log; +DROP TABLE IF EXISTS edges; +DROP TABLE IF EXISTS nodes; +DROP TABLE IF EXISTS relation_types; +DROP TABLE IF EXISTS types; + +-- v2.1 Schema + +CREATE TABLE event_types ( + name TEXT PRIMARY KEY, + label TEXT NOT NULL, + schema TEXT NOT NULL +); + +CREATE TABLE events ( + oid TEXT PRIMARY KEY, + type TEXT NOT NULL REFERENCES event_types(name), + payload TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_events_type ON events(type); +CREATE INDEX idx_events_created ON events(created_at); + +CREATE TABLE event_refs ( + event_oid TEXT NOT NULL REFERENCES events(oid), + property TEXT NOT NULL, + ref_oid TEXT NOT NULL, + PRIMARY KEY (event_oid, property) +); +CREATE INDEX idx_event_refs_obj ON event_refs(ref_oid); + +CREATE TABLE reducers ( + name TEXT PRIMARY KEY, + driven_by TEXT NOT NULL, + params TEXT NOT NULL, + filter TEXT NOT NULL, + expression TEXT NOT NULL, + initial_value TEXT +); + +CREATE TABLE projections ( + reducer TEXT NOT NULL REFERENCES reducers(name), + params TEXT NOT NULL, + params_hash TEXT NOT NULL, + value TEXT, + updated_by TEXT, + updated_at INTEGER, + live INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (reducer, params_hash) +); + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + reducer TEXT NOT NULL, + params_hash TEXT, + condition TEXT, + worker_url TEXT NOT NULL, + config TEXT NOT NULL DEFAULT '{}', + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_reducer ON reactions(reducer); diff --git a/packages/engine/migrations/0011_v2.2.sql b/packages/engine/migrations/0011_v2.2.sql new file mode 100644 index 0000000..29c3532 --- /dev/null +++ b/packages/engine/migrations/0011_v2.2.sql @@ -0,0 +1,82 @@ +-- OGraph v2.2 Schema +-- RFC-016 v2.2: Event, Projection, Reaction (8 tables) + +-- Drop all v2.1 tables +DROP TABLE IF EXISTS projection_deps; +DROP TABLE IF EXISTS projections; +DROP TABLE IF EXISTS projection_defs; +DROP TABLE IF EXISTS event_refs; +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS event_defs; +DROP TABLE IF EXISTS objects; +DROP TABLE IF EXISTS object_defs; +DROP TABLE IF EXISTS reactions; + +-- ============================================ +-- Definition Layer (3 tables) +-- ============================================ + +CREATE TABLE object_defs ( + name TEXT PRIMARY KEY +); + +CREATE TABLE event_defs ( + name TEXT PRIMARY KEY, + schema TEXT NOT NULL -- JSON: { properties: { ... } } +); + +CREATE TABLE projection_defs ( + name TEXT PRIMARY KEY, + driven_by TEXT NOT NULL, -- JSON: ["assigned", "reassigned"] + params TEXT NOT NULL, -- JSON: input schema, e.g. {"task": {"type": "ref"}} + filter TEXT NOT NULL, -- JSONata: $event 精筛 + expression TEXT NOT NULL, -- JSONata: ($state, $events) → new_state + initial_value TEXT -- JSON: 初始值 +); + +-- ============================================ +-- Instance Layer (5 tables) +-- ============================================ + +CREATE TABLE objects ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL REFERENCES object_defs(name), + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE events ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL REFERENCES event_defs(name), + payload TEXT NOT NULL, -- JSON: flat key-value + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE event_refs ( + event_id TEXT NOT NULL REFERENCES events(id), + property TEXT NOT NULL, + ref_id TEXT NOT NULL REFERENCES objects(id), + PRIMARY KEY (event_id, property) +); + +CREATE INDEX idx_event_refs_obj ON event_refs(ref_id); + +CREATE TABLE projections ( + def TEXT NOT NULL REFERENCES projection_defs(name), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, -- JSON: 实际参数值 + value TEXT, -- JSON: 当前值 + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), + updated_at INTEGER, + PRIMARY KEY (def, params_hash) +); + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def TEXT NOT NULL REFERENCES projection_defs(name), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, -- JSON: 监听哪个 projection 实例的参数 + webhook_url TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE INDEX idx_reactions_projection ON reactions(projection_def, params_hash); diff --git a/packages/engine/migrations/0012_v2.3.sql b/packages/engine/migrations/0012_v2.3.sql new file mode 100644 index 0000000..b4fb71f --- /dev/null +++ b/packages/engine/migrations/0012_v2.3.sql @@ -0,0 +1,104 @@ +-- OGraph v2.3 Schema +-- Immutable definitions (hash ID) + Mutable name pointers + +-- Drop all v2.2 tables +DROP TABLE IF EXISTS reactions; +DROP TABLE IF EXISTS projections; +DROP TABLE IF EXISTS event_refs; +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS objects; +DROP TABLE IF EXISTS projection_defs; +DROP TABLE IF EXISTS event_defs; +DROP TABLE IF EXISTS object_defs; + +-- ============================================ +-- Definition Versions (immutable, hash ID) +-- ============================================ + +CREATE TABLE object_def_versions ( + hash TEXT PRIMARY KEY, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE event_def_versions ( + hash TEXT PRIMARY KEY, + schema TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE projection_def_versions ( + hash TEXT PRIMARY KEY, + driven_by TEXT NOT NULL, -- JSON: [event_def_hash, ...] + params TEXT NOT NULL, + filter TEXT NOT NULL, + expression TEXT NOT NULL, + initial_value TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +-- ============================================ +-- Name Pointers (mutable) +-- ============================================ + +CREATE TABLE object_def_names ( + name TEXT PRIMARY KEY, + current_hash TEXT NOT NULL REFERENCES object_def_versions(hash), + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE event_def_names ( + name TEXT PRIMARY KEY, + current_hash TEXT NOT NULL REFERENCES event_def_versions(hash), + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE projection_def_names ( + name TEXT PRIMARY KEY, + current_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +-- ============================================ +-- Instances +-- ============================================ + +CREATE TABLE objects ( + id TEXT PRIMARY KEY, + type_hash TEXT NOT NULL REFERENCES object_def_versions(hash), + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE events ( + id TEXT PRIMARY KEY, + type_hash TEXT NOT NULL REFERENCES event_def_versions(hash), + payload TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE event_refs ( + event_id TEXT NOT NULL REFERENCES events(id), + property TEXT NOT NULL, + ref_id TEXT NOT NULL REFERENCES objects(id), + PRIMARY KEY (event_id, property) +); +CREATE INDEX idx_event_refs_obj ON event_refs(ref_id); + +CREATE TABLE projections ( + def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + value TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), + updated_at INTEGER, + PRIMARY KEY (def_hash, params_hash) +); + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + webhook_url TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_projection ON reactions(projection_def_hash, params_hash); diff --git a/packages/engine/migrations/0013_v2.4.sql b/packages/engine/migrations/0013_v2.4.sql new file mode 100644 index 0000000..d411ad5 --- /dev/null +++ b/packages/engine/migrations/0013_v2.4.sql @@ -0,0 +1,113 @@ +-- OGraph v2.4 Schema +-- object_defs 回归单表(去掉 object_def_versions + object_def_names) +-- version 表加 parent_hash(版本链)和 name(反查) +-- projection_def 加 value_schema NOT NULL + initial_value NOT NULL +-- projections.value NOT NULL +-- objects.type 直接存名字(不存 hash) + +-- Drop all v2.3 tables +DROP TABLE IF EXISTS reactions; +DROP TABLE IF EXISTS projections; +DROP TABLE IF EXISTS event_refs; +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS objects; +DROP TABLE IF EXISTS projection_def_names; +DROP TABLE IF EXISTS projection_def_versions; +DROP TABLE IF EXISTS event_def_names; +DROP TABLE IF EXISTS event_def_versions; +DROP TABLE IF EXISTS object_def_names; +DROP TABLE IF EXISTS object_def_versions; + +-- ============================================ +-- Object 定义(无版本,纯名字) +-- ============================================ + +CREATE TABLE object_defs ( + name TEXT PRIMARY KEY +); + +-- ============================================ +-- Event 定义(版本链 + 名字指针) +-- ============================================ + +CREATE TABLE event_def_versions ( + hash TEXT PRIMARY KEY, + name TEXT NOT NULL, -- 属于哪个定义(反查) + parent_hash TEXT REFERENCES event_def_versions(hash), -- 前一版本,首版 NULL + schema TEXT NOT NULL, -- JSON: { properties: { ... } } + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE event_def_names ( + name TEXT PRIMARY KEY, + current_hash TEXT NOT NULL REFERENCES event_def_versions(hash), + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +-- ============================================ +-- Projection 定义(版本链 + 名字指针 + value schema) +-- ============================================ + +CREATE TABLE projection_def_versions ( + hash TEXT PRIMARY KEY, + name TEXT NOT NULL, + parent_hash TEXT REFERENCES projection_def_versions(hash), + driven_by TEXT NOT NULL, -- JSON: [event_def_hash, ...] + params TEXT NOT NULL, -- JSON: input schema + filter TEXT NOT NULL, -- JSONata + expression TEXT NOT NULL, -- JSONata + value_schema TEXT NOT NULL, -- JSON: { type: "number" } 等 + initial_value TEXT NOT NULL, -- JSON: 必须符合 value_schema + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE projection_def_names ( + name TEXT PRIMARY KEY, + current_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +-- ============================================ +-- 实例表 +-- ============================================ + +CREATE TABLE objects ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL REFERENCES object_defs(name), + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE events ( + id TEXT PRIMARY KEY, + type_hash TEXT NOT NULL REFERENCES event_def_versions(hash), + payload TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE event_refs ( + event_id TEXT NOT NULL REFERENCES events(id), + property TEXT NOT NULL, + ref_id TEXT NOT NULL REFERENCES objects(id), + PRIMARY KEY (event_id, property) +); +CREATE INDEX idx_event_refs_obj ON event_refs(ref_id); + +CREATE TABLE projections ( + def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + value TEXT NOT NULL, -- NOT NULL,至少是 initial_value + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), + updated_at INTEGER, + PRIMARY KEY (def_hash, params_hash) +); + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + webhook_url TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_projection ON reactions(projection_def_hash, params_hash); diff --git a/packages/engine/migrations/0014_bindings.sql b/packages/engine/migrations/0014_bindings.sql new file mode 100644 index 0000000..6590dc9 --- /dev/null +++ b/packages/engine/migrations/0014_bindings.sql @@ -0,0 +1,46 @@ +-- Replace filter (JSONata) with bindings (structured ref matching) +-- Clean slate for projection_def_versions, projection_def_names, projections, reactions + +DROP TABLE IF EXISTS reactions; +DROP TABLE IF EXISTS projections; +DROP TABLE IF EXISTS projection_def_names; +DROP TABLE IF EXISTS projection_def_versions; + +CREATE TABLE projection_def_versions ( + hash TEXT PRIMARY KEY, + name TEXT NOT NULL, + parent_hash TEXT REFERENCES projection_def_versions(hash), + driven_by TEXT NOT NULL, + params TEXT NOT NULL, + bindings TEXT NOT NULL, -- JSON: { property: "$param" | "literal_id" } + expression TEXT NOT NULL, + value_schema TEXT NOT NULL, + initial_value TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE projection_def_names ( + name TEXT PRIMARY KEY, + current_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE projections ( + def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + value TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), + updated_at INTEGER, + PRIMARY KEY (def_hash, params_hash) +); + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + webhook_url TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_projection ON reactions(projection_def_hash, params_hash); diff --git a/packages/engine/migrations/0015_sources.sql b/packages/engine/migrations/0015_sources.sql new file mode 100644 index 0000000..866fbe3 --- /dev/null +++ b/packages/engine/migrations/0015_sources.sql @@ -0,0 +1,54 @@ +-- Drop old projection-related tables (clean slate, pre-launch) +DROP TABLE IF EXISTS reactions; +DROP TABLE IF EXISTS projections; +DROP TABLE IF EXISTS projection_def_names; +DROP TABLE IF EXISTS projection_def_versions; + +-- Recreate projection_def_versions WITHOUT driven_by, bindings, expression +CREATE TABLE projection_def_versions ( + hash TEXT PRIMARY KEY, + name TEXT NOT NULL, + parent_hash TEXT REFERENCES projection_def_versions(hash), + params TEXT NOT NULL, + value_schema TEXT NOT NULL, + initial_value TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +-- New: per-source bindings + expression +CREATE TABLE projection_def_sources ( + projection_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + event_def_hash TEXT NOT NULL, + bindings TEXT NOT NULL, + expression TEXT NOT NULL, + PRIMARY KEY (projection_hash, event_def_hash) +); +CREATE INDEX idx_pds_event ON projection_def_sources(event_def_hash); + +-- Recreate name pointers +CREATE TABLE projection_def_names ( + name TEXT PRIMARY KEY, + current_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +-- Recreate instance tables +CREATE TABLE projections ( + def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + value TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), + updated_at INTEGER, + PRIMARY KEY (def_hash, params_hash) +); + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + webhook_url TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_projection ON reactions(projection_def_hash, params_hash); diff --git a/packages/engine/migrations/0016_reaction_actions.sql b/packages/engine/migrations/0016_reaction_actions.sql new file mode 100644 index 0000000..c52f7ff --- /dev/null +++ b/packages/engine/migrations/0016_reaction_actions.sql @@ -0,0 +1,17 @@ +-- Add action type support to reactions (Phase 2: emit_event) +-- Pre-launch: drop and recreate reactions table + +DROP TABLE IF EXISTS reactions; + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + action TEXT NOT NULL DEFAULT 'webhook', -- 'webhook' | 'emit_event' + webhook_url TEXT, -- required when action = 'webhook' + emit_event_type TEXT, -- required when action = 'emit_event' + emit_payload_template TEXT, -- JSONata: optional template for emit_event + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_projection ON reactions(projection_def_hash, params_hash); diff --git a/packages/engine/migrations/0017_integer_ids.sql b/packages/engine/migrations/0017_integer_ids.sql new file mode 100644 index 0000000..9502e02 --- /dev/null +++ b/packages/engine/migrations/0017_integer_ids.sql @@ -0,0 +1,52 @@ +-- Migration 0017: Unify instance IDs to INTEGER AUTOINCREMENT +-- Pre-launch clean slate: drop and recreate all instance + reaction tables + +DROP TABLE IF EXISTS reactions; +DROP TABLE IF EXISTS projections; +DROP TABLE IF EXISTS event_refs; +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS objects; + +CREATE TABLE objects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL REFERENCES object_defs(name), + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type_hash TEXT NOT NULL REFERENCES event_def_versions(hash), + payload TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); + +CREATE TABLE event_refs ( + event_id INTEGER NOT NULL REFERENCES events(id), + property TEXT NOT NULL, + ref_id INTEGER NOT NULL REFERENCES objects(id), + PRIMARY KEY (event_id, property) +); +CREATE INDEX idx_event_refs_obj ON event_refs(ref_id); + +CREATE TABLE projections ( + def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + value TEXT NOT NULL, + last_event_id INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), + PRIMARY KEY (def_hash, params_hash) +); + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + action TEXT NOT NULL DEFAULT 'webhook', + webhook_url TEXT, + emit_event_type TEXT, + emit_payload_template TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_projection ON reactions(projection_def_hash, params_hash); diff --git a/packages/engine/migrations/0018_reaction_logs.sql b/packages/engine/migrations/0018_reaction_logs.sql new file mode 100644 index 0000000..56cdf36 --- /dev/null +++ b/packages/engine/migrations/0018_reaction_logs.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS reaction_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + reaction_id INTEGER NOT NULL, + trigger_event_id INTEGER NOT NULL, + projection_def TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + action TEXT NOT NULL, + status TEXT NOT NULL, + handler_output TEXT, + duration_ms INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX IF NOT EXISTS idx_rlog_reaction ON reaction_logs(reaction_id); +CREATE INDEX IF NOT EXISTS idx_rlog_event ON reaction_logs(trigger_event_id); diff --git a/packages/engine/migrations/0019_api_keys.sql b/packages/engine/migrations/0019_api_keys.sql new file mode 100644 index 0000000..3923823 --- /dev/null +++ b/packages/engine/migrations/0019_api_keys.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key_hash TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'ingest', + allowed_events TEXT NOT NULL DEFAULT '[]', + rate_limit INTEGER DEFAULT 100, + last_used_at INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); diff --git a/packages/engine/migrations/0020_handler.sql b/packages/engine/migrations/0020_handler.sql new file mode 100644 index 0000000..111dad6 --- /dev/null +++ b/packages/engine/migrations/0020_handler.sql @@ -0,0 +1,42 @@ +-- Add handler_code column to reactions table +-- Pre-launch: drop and recreate +DROP TABLE IF EXISTS reaction_logs; +DROP TABLE IF EXISTS reactions; + +CREATE TABLE reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + projection_def_hash TEXT NOT NULL REFERENCES projection_def_versions(hash), + params_hash TEXT NOT NULL, + params TEXT NOT NULL, + action TEXT NOT NULL DEFAULT 'webhook', + webhook_url TEXT, + emit_event_type TEXT, + emit_payload_template TEXT, + handler_code TEXT, + handler_timeout_ms INTEGER DEFAULT 5000, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_reactions_projection ON reactions(projection_def_hash, params_hash); + +CREATE TABLE reaction_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + reaction_id INTEGER NOT NULL, + trigger_event_id INTEGER NOT NULL, + projection_def TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + action TEXT NOT NULL, + status TEXT NOT NULL, + handler_output TEXT, + duration_ms INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX idx_rlog_reaction ON reaction_logs(reaction_id); +CREATE INDEX idx_rlog_event ON reaction_logs(trigger_event_id); + +CREATE TABLE IF NOT EXISTS reaction_kv ( + reaction_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (reaction_id, key) +); diff --git a/packages/engine/migrations/0021_request_logs.sql b/packages/engine/migrations/0021_request_logs.sql new file mode 100644 index 0000000..dd91b12 --- /dev/null +++ b/packages/engine/migrations/0021_request_logs.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS request_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + method TEXT NOT NULL, + path TEXT NOT NULL, + api_key_id INTEGER, + api_key_name TEXT, + status_code INTEGER NOT NULL, + error TEXT, + duration_ms INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) +); +CREATE INDEX IF NOT EXISTS idx_reqlog_key ON request_logs(api_key_id); +CREATE INDEX IF NOT EXISTS idx_reqlog_created ON request_logs(created_at); diff --git a/packages/engine/package.json b/packages/engine/package.json new file mode 100644 index 0000000..04c97d6 --- /dev/null +++ b/packages/engine/package.json @@ -0,0 +1,22 @@ +{ + "name": "@uncaged/ograph", + "version": "0.1.0", + "description": "OGraph \u2014 Event Sourcing + Projection + Reaction engine on Cloudflare Workers", + "type": "module", + "main": "src/index.ts", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "test": "vitest run" + }, + "dependencies": { + "hono": "^4.0.0", + "jsonata": "^2.1.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260403.0", + "typescript": "^6.0.2", + "vitest": "^4.1.2", + "wrangler": "^4.0.0" + } +} diff --git a/packages/engine/src/auth.ts b/packages/engine/src/auth.ts new file mode 100644 index 0000000..d9d9810 --- /dev/null +++ b/packages/engine/src/auth.ts @@ -0,0 +1,21 @@ +import { createMiddleware } from 'hono/factory' + +type Env = { + DB: D1Database + API_TOKEN: string +} + +export const bearerAuth = (expectedToken: string) => + createMiddleware<{ Bindings: Env }>(async (c, next) => { + const header = c.req.header('Authorization') + if (!header?.startsWith('Bearer ')) { + return c.json({ error: 'Missing or invalid Authorization header' }, 401) + } + + const token = header.slice(7) + if (token !== expectedToken) { + return c.json({ error: 'Invalid token' }, 401) + } + + await next() + }) diff --git a/packages/engine/src/engine.ts b/packages/engine/src/engine.ts new file mode 100644 index 0000000..e895c75 --- /dev/null +++ b/packages/engine/src/engine.ts @@ -0,0 +1,1321 @@ +// OGraph v2.5 Engine +// sources[] replaces driven_by + bindings + expression + +import jsonata from 'jsonata' +import type { + ObjectDef, + Object, + EventDef, + EventDefVersion, + Event, + ProjectionDef, + ProjectionDefVersion, + ProjectionDefSource, + Projection, + Reaction, + EventContext, + ReactionPayload, + ReactionLog, + PropertyDef, + ApiKey, + CreateApiKeyResponse, +} from './types' + +// Recursion guard for emit_event reactions +const MAX_REACTION_DEPTH = 3 +let _reactionDepth = 0 + +// ============================================ +// Hash Computation +// ============================================ + +async function contentHash(content: any): Promise { + const sortedKeys = Object.keys(content).sort() + const canonical: any = {} + for (const key of sortedKeys) { + canonical[key] = content[key] + } + const json = JSON.stringify(canonical) + const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(json)) + return Array.from(new Uint8Array(buf)) + .slice(0, 8) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +// ============================================ +// Name Resolution +// ============================================ + +export async function resolveEventDefName(db: D1Database, name: string): Promise { + const row = await db + .prepare('SELECT current_hash FROM event_def_names WHERE name = ?') + .bind(name) + .first<{ current_hash: string }>() + return row?.current_hash || null +} + +export async function resolveProjectionDefName(db: D1Database, name: string): Promise { + const row = await db + .prepare('SELECT current_hash FROM projection_def_names WHERE name = ?') + .bind(name) + .first<{ current_hash: string }>() + return row?.current_hash || null +} + +// ============================================ +// Object Defs +// ============================================ + +export async function createObjectDef(db: D1Database, name: string): Promise { + await db.prepare('INSERT OR IGNORE INTO object_defs (name) VALUES (?)').bind(name).run() + return { name } +} + +export async function listObjectDefs(db: D1Database): Promise { + const result = await db.prepare('SELECT name FROM object_defs ORDER BY name').all<{ name: string }>() + return (result.results || []).map((row) => ({ name: row.name })) +} + +// ============================================ +// Objects +// ============================================ + +export async function createObject(db: D1Database, typeName: string): Promise { + const exists = await db.prepare('SELECT 1 FROM object_defs WHERE name = ?').bind(typeName).first<{ 1: number }>() + if (!exists) throw new Error(`Object type ${typeName} not defined`) + + const createdAt = Date.now() + const result = await db + .prepare('INSERT INTO objects (type, created_at) VALUES (?, ?)') + .bind(typeName, createdAt) + .run() + const id = result.meta.last_row_id + return { id, type: typeName, created_at: createdAt } +} + +export async function getObject(db: D1Database, id: number): Promise { + const result = await db.prepare('SELECT id, type, created_at FROM objects WHERE id = ?').bind(id).first() + return result || null +} + +export async function listObjects( + db: D1Database, + typeName?: string, + limit = 50, + offset = 0, +): Promise<{ objects: Object[]; total: number }> { + // Get total count + let countQuery + if (typeName) { + countQuery = db.prepare('SELECT COUNT(*) as count FROM objects WHERE type = ?').bind(typeName) + } else { + countQuery = db.prepare('SELECT COUNT(*) as count FROM objects') + } + const countResult = await countQuery.first<{ count: number }>() + const total = countResult?.count || 0 + + // Get paginated results + let query + if (typeName) { + query = db + .prepare('SELECT id, type, created_at FROM objects WHERE type = ? ORDER BY created_at DESC LIMIT ? OFFSET ?') + .bind(typeName, limit, offset) + } else { + query = db + .prepare('SELECT id, type, created_at FROM objects ORDER BY created_at DESC LIMIT ? OFFSET ?') + .bind(limit, offset) + } + const result = await query.all() + return { objects: result.results || [], total } +} + +// ============================================ +// Event Defs +// ============================================ + +function validateEventSchema(schema: { properties: Record }): void { + if (!schema.properties || typeof schema.properties !== 'object') { + throw new Error('schema must have properties object') + } + for (const [key, prop] of Object.entries(schema.properties)) { + if (!['ref', 'string', 'number', 'boolean'].includes(prop.type)) { + throw new Error(`Invalid property type for ${key}: ${prop.type}`) + } + if (prop.type === 'ref' && prop.object_type) { + if (typeof prop.object_type !== 'string' && !Array.isArray(prop.object_type)) { + throw new Error(`object_type for ${key} must be string or array`) + } + } + } +} + +export async function createEventDef( + db: D1Database, + name: string, + schema: { properties: Record }, +): Promise<{ name: string; hash: string }> { + validateEventSchema(schema) + + // contentHash 基于 { schema }(不含 name/parent) + const hash = await contentHash({ schema }) + const now = Date.now() + + // 查 event_def_names 看是否已有(确定 parent_hash) + const existingName = await db + .prepare('SELECT current_hash FROM event_def_names WHERE name = ?') + .bind(name) + .first<{ current_hash: string }>() + const parentHash = existingName?.current_hash || null + + // 检查 version 是否已存在(相同 schema 不重复创建) + const existingVersion = await db + .prepare('SELECT hash FROM event_def_versions WHERE hash = ?') + .bind(hash) + .first<{ hash: string }>() + + if (!existingVersion) { + await db + .prepare('INSERT INTO event_def_versions (hash, name, parent_hash, schema, created_at) VALUES (?, ?, ?, ?, ?)') + .bind(hash, name, parentHash, JSON.stringify(schema), now) + .run() + } + + // UPSERT name pointer + await db + .prepare('INSERT OR REPLACE INTO event_def_names (name, current_hash, updated_at) VALUES (?, ?, ?)') + .bind(name, hash, now) + .run() + + return { name, hash } +} + +export async function listEventDefs(db: D1Database): Promise { + const result = await db + .prepare( + `SELECT n.name, n.current_hash, v.parent_hash, v.schema + FROM event_def_names n + JOIN event_def_versions v ON n.current_hash = v.hash + ORDER BY n.name`, + ) + .all<{ name: string; current_hash: string; parent_hash: string | null; schema: string }>() + return (result.results || []).map((row) => ({ + name: row.name, + hash: row.current_hash, + parent_hash: row.parent_hash, + schema: JSON.parse(row.schema), + })) +} + +// ============================================ +// Events +// ============================================ + +async function validateEventPayload( + db: D1Database, + schema: { properties: Record }, + payload: Record, +): Promise<{ refProperties: Map }> { + const refProperties = new Map() + + for (const [key, propDef] of Object.entries(schema.properties)) { + const value = payload[key] + if (value === undefined || value === null) continue + + switch (propDef.type) { + case 'string': + if (typeof value !== 'string') throw new Error(`${key} must be string`) + break + case 'number': + if (typeof value !== 'number') throw new Error(`${key} must be number`) + break + case 'boolean': + if (typeof value !== 'boolean') throw new Error(`${key} must be boolean`) + break + case 'ref': { + if (typeof value !== 'number') throw new Error(`${key} must be ref (number)`) + const obj = await getObject(db, value) + if (!obj) throw new Error(`Referenced object ${value} does not exist`) + if (propDef.object_type) { + const allowedTypes = Array.isArray(propDef.object_type) ? propDef.object_type : [propDef.object_type] + if (!allowedTypes.includes(obj.type)) { + throw new Error(`${key} ref ${value} type mismatch: expected ${allowedTypes.join('|')}, got ${obj.type}`) + } + } + refProperties.set(key, { refId: value, objectType: propDef.object_type }) + break + } + } + } + + return { refProperties } +} + +export async function createEvent( + db: D1Database, + typeName: string, + payload: Record, +): Promise<{ event: Event; reactions_fired: number; reaction_results: ReactionPayload[] }> { + const typeHash = await resolveEventDefName(db, typeName) + if (!typeHash) throw new Error(`Event type ${typeName} not defined`) + + const schemaRow = await db + .prepare('SELECT schema FROM event_def_versions WHERE hash = ?') + .bind(typeHash) + .first<{ schema: string }>() + if (!schemaRow) throw new Error(`Event type hash ${typeHash} not found in versions`) + + const schema = JSON.parse(schemaRow.schema) + const { refProperties } = await validateEventPayload(db, schema, payload) + + const createdAt = Date.now() + + const insertResult = await db + .prepare('INSERT INTO events (type_hash, payload, created_at) VALUES (?, ?, ?)') + .bind(typeHash, JSON.stringify(payload), createdAt) + .run() + const id = insertResult.meta.last_row_id + + for (const [property, { refId }] of refProperties.entries()) { + await db + .prepare('INSERT INTO event_refs (event_id, property, ref_id) VALUES (?, ?, ?)') + .bind(id, property, refId) + .run() + } + + const event: Event = { id, type_hash: typeHash, payload, created_at: createdAt } + const { fired: reactionsFired, payloads: reactionResults } = await triggerReactionChain(db, event) + + return { event, reactions_fired: reactionsFired, reaction_results: reactionResults } +} + +export async function getEvent(db: D1Database, id: number): Promise { + const row = await db + .prepare('SELECT id, type_hash, payload, created_at FROM events WHERE id = ?') + .bind(id) + .first<{ id: number; type_hash: string; payload: string; created_at: number }>() + if (!row) return null + return { ...row, payload: JSON.parse(row.payload) } +} + +export async function findEventsByRef( + db: D1Database, + refId?: number, + limit = 50, + offset = 0, +): Promise<{ events: Event[]; total: number }> { + let countQuery, dataQuery + if (refId !== undefined) { + countQuery = db + .prepare( + `SELECT COUNT(DISTINCT e.id) as count + FROM events e + JOIN event_refs er ON e.id = er.event_id + WHERE er.ref_id = ?`, + ) + .bind(refId) + dataQuery = db + .prepare( + `SELECT DISTINCT e.id, e.type_hash, e.payload, e.created_at + FROM events e + JOIN event_refs er ON e.id = er.event_id + WHERE er.ref_id = ? + ORDER BY e.created_at DESC + LIMIT ? OFFSET ?`, + ) + .bind(refId, limit, offset) + } else { + countQuery = db.prepare('SELECT COUNT(*) as count FROM events') + dataQuery = db + .prepare('SELECT id, type_hash, payload, created_at FROM events ORDER BY created_at DESC LIMIT ? OFFSET ?') + .bind(limit, offset) + } + + const countResult = await countQuery.first<{ count: number }>() + const total = countResult?.count || 0 + + const rows = await dataQuery.all<{ id: number; type_hash: string; payload: string; created_at: number }>() + const events = (rows.results || []).map((row) => ({ ...row, payload: JSON.parse(row.payload) })) + + return { events, total } +} + +// ============================================ +// Projection Defs +// ============================================ + +function validateValueSchema(valueSchema: { type: string }): void { + if (!valueSchema || !valueSchema.type) { + throw new Error('value_schema must have type field') + } + const validTypes = ['ref', 'string', 'number', 'boolean', 'array', 'object'] + if (!validTypes.includes(valueSchema.type)) { + throw new Error(`Invalid value_schema type: ${valueSchema.type}`) + } +} + +function validateInitialValue(initialValue: any, valueSchema: { type: string }): void { + if (initialValue === undefined || initialValue === null) { + throw new Error('initial_value is required (cannot be null or undefined)') + } + + // 简单类型校验 + switch (valueSchema.type) { + case 'string': + if (typeof initialValue !== 'string') throw new Error('initial_value must be string') + break + case 'number': + if (typeof initialValue !== 'number') throw new Error('initial_value must be number') + break + case 'boolean': + if (typeof initialValue !== 'boolean') throw new Error('initial_value must be boolean') + break + case 'ref': + if (typeof initialValue !== 'string') throw new Error('initial_value for ref must be string') + break + case 'array': + if (!Array.isArray(initialValue)) throw new Error('initial_value must be array') + break + case 'object': + if (typeof initialValue !== 'object' || Array.isArray(initialValue)) + throw new Error('initial_value must be object') + break + } +} + +export async function createProjectionDef( + db: D1Database, + name: string, + sources: Array<{ event_def: string; bindings: Record; expression: string }>, + params: Record, + valueSchema: { type: string }, + initialValue: any, +): Promise<{ name: string; hash: string }> { + validateValueSchema(valueSchema) + validateInitialValue(initialValue, valueSchema) + + if (!sources || sources.length === 0) { + throw new Error('At least one source is required') + } + + const resolvedSources: Array<{ event_def_hash: string; bindings: Record; expression: string }> = [] + + for (const source of sources) { + const eventHash = await resolveEventDefName(db, source.event_def) + if (!eventHash) throw new Error(`Event type ${source.event_def} not defined`) + + for (const [, value] of Object.entries(source.bindings)) { + if (typeof value !== 'string') throw new Error('All binding values must be strings') + if (value.startsWith('$')) { + const paramKey = value.slice(1) + if (!(paramKey in params)) + throw new Error(`Binding references param ${paramKey} which is not defined in params`) + } + } + + resolvedSources.push({ event_def_hash: eventHash, bindings: source.bindings, expression: source.expression }) + } + + const sortedSources = [...resolvedSources].sort((a, b) => a.event_def_hash.localeCompare(b.event_def_hash)) + const hash = await contentHash({ + sources: sortedSources, + params, + value_schema: valueSchema, + initial_value: initialValue, + }) + const now = Date.now() + + const existingName = await db + .prepare('SELECT current_hash FROM projection_def_names WHERE name = ?') + .bind(name) + .first<{ current_hash: string }>() + const parentHash = existingName?.current_hash || null + + const existingVersion = await db + .prepare('SELECT hash FROM projection_def_versions WHERE hash = ?') + .bind(hash) + .first<{ hash: string }>() + + if (!existingVersion) { + await db + .prepare( + 'INSERT INTO projection_def_versions (hash, name, parent_hash, params, value_schema, initial_value, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)', + ) + .bind( + hash, + name, + parentHash, + JSON.stringify(params), + JSON.stringify(valueSchema), + JSON.stringify(initialValue), + now, + ) + .run() + + for (const src of resolvedSources) { + await db + .prepare( + 'INSERT INTO projection_def_sources (projection_hash, event_def_hash, bindings, expression) VALUES (?, ?, ?, ?)', + ) + .bind(hash, src.event_def_hash, JSON.stringify(src.bindings), src.expression) + .run() + } + } + + await db + .prepare('INSERT OR REPLACE INTO projection_def_names (name, current_hash, updated_at) VALUES (?, ?, ?)') + .bind(name, hash, now) + .run() + + return { name, hash } +} + +export async function listProjectionDefs(db: D1Database): Promise { + const rows = await db + .prepare( + `SELECT n.name, n.current_hash, v.parent_hash, v.params, v.value_schema, v.initial_value + FROM projection_def_names n + JOIN projection_def_versions v ON n.current_hash = v.hash + ORDER BY n.name`, + ) + .all<{ + name: string + current_hash: string + parent_hash: string | null + params: string + value_schema: string + initial_value: string + }>() + + const defs: ProjectionDef[] = [] + for (const row of rows.results || []) { + const sourceRows = await db + .prepare('SELECT event_def_hash, bindings, expression FROM projection_def_sources WHERE projection_hash = ?') + .bind(row.current_hash) + .all<{ event_def_hash: string; bindings: string; expression: string }>() + + const sources: ProjectionDefSource[] = (sourceRows.results || []).map((s) => ({ + event_def_hash: s.event_def_hash, + bindings: JSON.parse(s.bindings), + expression: s.expression, + })) + + defs.push({ + name: row.name, + hash: row.current_hash, + parent_hash: row.parent_hash, + params: JSON.parse(row.params), + sources, + value_schema: JSON.parse(row.value_schema), + initial_value: JSON.parse(row.initial_value), + }) + } + + return defs +} + +// ============================================ +// Projections +// ============================================ + +function computeParamsHash(params: Record): string { + const sortedKeys = Object.keys(params).sort() + const normalized = sortedKeys.map((k) => `${k}=${params[k]}`).join('&') + return btoa(normalized).replace(/=/g, '') +} + +function buildEventContext(event: Event): EventContext { + return { + id: event.id, + type: event.type_hash, + timestamp: event.created_at, + ...event.payload, + } +} + +function resolveBindings( + bindings: Record, + params: Record, +): { property: string; refId: number }[] { + return Object.entries(bindings).map(([property, value]) => { + if (value.startsWith('$')) { + const paramKey = value.slice(1) + const refId = params[paramKey] + if (refId === undefined || refId === null) + throw new Error(`Binding ${property} references param ${paramKey} which is not provided`) + return { property, refId: Number(refId) } + } + return { property, refId: Number(value) } + }) +} + +function buildEventsQuery( + eventDefHash: string, + resolvedBindings: { property: string; refId: number }[], + sinceEventId: number, +): { sql: string; binds: any[] } { + const binds: any[] = [] + let sql = 'SELECT e.id, e.type_hash, e.payload, e.created_at FROM events e' + + resolvedBindings.forEach((b, i) => { + sql += ` JOIN event_refs r${i} ON r${i}.event_id = e.id AND r${i}.property = ? AND r${i}.ref_id = ?` + binds.push(b.property, b.refId) + }) + + sql += ' WHERE e.type_hash = ?' + binds.push(eventDefHash) + + if (sinceEventId > 0) { + sql += ' AND e.id > ?' + binds.push(sinceEventId) + } + + sql += ' ORDER BY e.id ASC' + return { sql, binds } +} + +export async function getProjection(db: D1Database, defName: string, params: Record): Promise { + const defHash = await resolveProjectionDefName(db, defName) + if (!defHash) throw new Error(`Projection def ${defName} not found`) + + const paramsHash = computeParamsHash(params) + + const defRow = await db + .prepare('SELECT params, value_schema, initial_value FROM projection_def_versions WHERE hash = ?') + .bind(defHash) + .first<{ params: string; value_schema: string; initial_value: string }>() + + if (!defRow) throw new Error(`Projection def hash ${defHash} not found in versions`) + + const initialValue = JSON.parse(defRow.initial_value) + + const sourceRows = await db + .prepare('SELECT event_def_hash, bindings, expression FROM projection_def_sources WHERE projection_hash = ?') + .bind(defHash) + .all<{ event_def_hash: string; bindings: string; expression: string }>() + + const sources = (sourceRows.results || []).map((s) => ({ + event_def_hash: s.event_def_hash, + bindings: JSON.parse(s.bindings) as Record, + expression: s.expression, + })) + + const cached = await db + .prepare('SELECT value, last_event_id FROM projections WHERE def_hash = ? AND params_hash = ?') + .bind(defHash, paramsHash) + .first<{ value: string; last_event_id: number }>() + + let baseState: any + let sinceEventId: number + + if (cached) { + baseState = JSON.parse(cached.value) + sinceEventId = cached.last_event_id + } else { + baseState = initialValue + sinceEventId = 0 + } + + const allEvents: Array = [] + + for (let i = 0; i < sources.length; i++) { + const source = sources[i] + const resolved = resolveBindings(source.bindings, params) + const { sql, binds } = buildEventsQuery(source.event_def_hash, resolved, sinceEventId) + const eventRows = await db + .prepare(sql) + .bind(...binds) + .all<{ id: number; type_hash: string; payload: string; created_at: number }>() + + for (const row of eventRows.results || []) { + allEvents.push({ ...row, payload: JSON.parse(row.payload), _sourceIdx: i }) + } + } + + allEvents.sort((a, b) => a.id - b.id) + + if (allEvents.length === 0) { + if (!cached) { + const now = Date.now() + await db + .prepare( + `INSERT INTO projections (def_hash, params_hash, params, value, last_event_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(def_hash, params_hash) DO UPDATE SET value = excluded.value, last_event_id = excluded.last_event_id`, + ) + .bind(defHash, paramsHash, JSON.stringify(params), JSON.stringify(baseState), 0, now) + .run() + } + return baseState + } + + let state = baseState + for (const event of allEvents) { + const source = sources.find((s) => s.event_def_hash === event.type_hash) + if (!source) continue + + const eventContext = buildEventContext(event) + const expr = jsonata(source.expression) + try { + state = await expr.evaluate({ state, event: eventContext, params }) + } catch (err) { + throw new Error(`Projection expression eval failed: ${err}`) + } + } + + const lastEventId = allEvents[allEvents.length - 1].id + const now = Date.now() + await db + .prepare( + `INSERT INTO projections (def_hash, params_hash, params, value, last_event_id, created_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(def_hash, params_hash) DO UPDATE SET value = excluded.value, last_event_id = excluded.last_event_id`, + ) + .bind(defHash, paramsHash, JSON.stringify(params), JSON.stringify(state), lastEventId, now) + .run() + + return state +} + +// ============================================ +// Reactions +// ============================================ + +export async function createReaction( + db: D1Database, + projectionDefName: string, + params: Record, + options: { + action?: 'webhook' | 'emit_event' | 'handler' + webhook_url?: string + emit_event_type?: string + emit_payload_template?: string + handler_code?: string + handler_timeout_ms?: number + }, +): Promise { + const action = options.action || 'webhook' + if (action === 'webhook' && !options.webhook_url) { + throw new Error('webhook_url is required when action is webhook') + } + if (action === 'emit_event' && !options.emit_event_type) { + throw new Error('emit_event_type is required when action is emit_event') + } + if (action === 'handler' && !options.handler_code) { + throw new Error('handler_code is required when action is handler') + } + + const defHash = await resolveProjectionDefName(db, projectionDefName) + if (!defHash) throw new Error(`Projection def ${projectionDefName} not found`) + + const paramsHash = computeParamsHash(params) + const createdAt = Date.now() + + await db + .prepare( + 'INSERT INTO reactions (projection_def_hash, params_hash, params, action, webhook_url, emit_event_type, emit_payload_template, handler_code, handler_timeout_ms, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + ) + .bind( + defHash, + paramsHash, + JSON.stringify(params), + action, + options.webhook_url || null, + options.emit_event_type || null, + options.emit_payload_template || null, + options.handler_code || null, + options.handler_timeout_ms ?? null, + createdAt, + ) + .run() + + const result = await db + .prepare('SELECT id FROM reactions WHERE projection_def_hash = ? AND params_hash = ? ORDER BY id DESC LIMIT 1') + .bind(defHash, paramsHash) + .first<{ id: number }>() + + if (!result) throw new Error('Failed to create reaction') + + return { + id: result.id, + projection_def_hash: defHash, + params_hash: paramsHash, + params, + action, + webhook_url: options.webhook_url, + emit_event_type: options.emit_event_type, + emit_payload_template: options.emit_payload_template, + handler_code: options.handler_code, + handler_timeout_ms: options.handler_timeout_ms, + created_at: createdAt, + } +} + +export async function listReactions( + db: D1Database, + limit = 50, + offset = 0, +): Promise<{ reactions: Reaction[]; total: number }> { + const countQuery = db.prepare('SELECT COUNT(*) as count FROM reactions') + const countResult = await countQuery.first<{ count: number }>() + const total = countResult?.count || 0 + + const rows = await db + .prepare( + 'SELECT id, projection_def_hash, params_hash, params, action, webhook_url, emit_event_type, emit_payload_template, handler_code, handler_timeout_ms, created_at FROM reactions ORDER BY id LIMIT ? OFFSET ?', + ) + .bind(limit, offset) + .all<{ + id: number + projection_def_hash: string + params_hash: string + params: string + action: string + webhook_url: string | null + emit_event_type: string | null + emit_payload_template: string | null + handler_code: string | null + handler_timeout_ms: number | null + created_at: number + }>() + + const reactions = (rows.results || []).map((row) => ({ + ...row, + params: JSON.parse(row.params), + action: (row.action || 'webhook') as 'webhook' | 'emit_event' | 'handler', + webhook_url: row.webhook_url || undefined, + emit_event_type: row.emit_event_type || undefined, + emit_payload_template: row.emit_payload_template || undefined, + handler_code: row.handler_code || undefined, + handler_timeout_ms: row.handler_timeout_ms || undefined, + })) + return { reactions, total } +} + +export async function deleteReaction(db: D1Database, id: number): Promise { + await db.prepare('DELETE FROM reactions WHERE id = ?').bind(id).run() +} + +// ============================================ +// Handler Execution +// ============================================ + +async function executeHandler( + db: D1Database, + reactionId: number, + handlerCode: string, + timeoutMs: number, + context: { old_value: any; new_value: any; params: Record; event: any }, +): Promise<{ + output: string + status: 'success' | 'failed' + emittedEvents: Array<{ type: string; payload: Record }> +}> { + const logs: string[] = [] + const emittedEvents: Array<{ type: string; payload: Record }> = [] + + const emitFn = async (type: string, payload: Record) => { + emittedEvents.push({ type, payload }) + } + + const logFn = (...args: any[]) => { + logs.push(args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')) + } + + const kvObj = { + get: async (key: string) => { + const row = await db + .prepare('SELECT value FROM reaction_kv WHERE reaction_id = ? AND key = ?') + .bind(reactionId, key) + .first<{ value: string }>() + return row ? JSON.parse(row.value) : null + }, + set: async (key: string, value: any) => { + await db + .prepare('INSERT OR REPLACE INTO reaction_kv (reaction_id, key, value) VALUES (?, ?, ?)') + .bind(reactionId, key, JSON.stringify(value)) + .run() + }, + del: async (key: string) => { + await db.prepare('DELETE FROM reaction_kv WHERE reaction_id = ? AND key = ?').bind(reactionId, key).run() + }, + } + + try { + const fn = new Function( + 'emit', + 'log', + 'kv', + 'old_value', + 'new_value', + 'params', + 'event', + `return (async () => { ${handlerCode} })()`, + ) + + const result = await Promise.race([ + fn(emitFn, logFn, kvObj, context.old_value, context.new_value, context.params, context.event), + new Promise((_, reject) => setTimeout(() => reject(new Error('Handler timeout')), timeoutMs)), + ]) + + if (result !== undefined) { + logs.push(`return: ${typeof result === 'object' ? JSON.stringify(result) : String(result)}`) + } + + return { output: logs.join('\n'), status: 'success', emittedEvents } + } catch (err: any) { + logs.push(`error: ${err.message || String(err)}`) + return { output: logs.join('\n'), status: 'failed', emittedEvents: [] } + } +} + +// ============================================ +// Reaction Trigger Chain +// ============================================ + +async function triggerReactionChain( + db: D1Database, + event: Event, +): Promise<{ fired: number; payloads: ReactionPayload[] }> { + _reactionDepth++ + try { + return await _triggerReactionChainInner(db, event) + } finally { + _reactionDepth-- + } +} + +async function _triggerReactionChainInner( + db: D1Database, + event: Event, +): Promise<{ fired: number; payloads: ReactionPayload[] }> { + const affectedRows = await db + .prepare('SELECT DISTINCT projection_hash FROM projection_def_sources WHERE event_def_hash = ?') + .bind(event.type_hash) + .all<{ projection_hash: string }>() + + if (!affectedRows.results || affectedRows.results.length === 0) return { fired: 0, payloads: [] } + + const eventRefRows = await db + .prepare('SELECT property, ref_id FROM event_refs WHERE event_id = ?') + .bind(event.id) + .all<{ property: string; ref_id: number }>() + const eventRefs = new Map() + for (const ref of eventRefRows.results || []) { + eventRefs.set(ref.property, ref.ref_id) + } + + let totalFired = 0 + const payloads: ReactionPayload[] = [] + const emittedEvents: Array<{ type: string; payload: Record }> = [] + + for (const row of affectedRows.results) { + const reactionRows = await db + .prepare( + 'SELECT id, params_hash, params, action, webhook_url, emit_event_type, emit_payload_template, handler_code, handler_timeout_ms FROM reactions WHERE projection_def_hash = ?', + ) + .bind(row.projection_hash) + .all<{ + id: number + params_hash: string + params: string + action: string + webhook_url: string | null + emit_event_type: string | null + emit_payload_template: string | null + handler_code: string | null + handler_timeout_ms: number | null + }>() + + if (!reactionRows.results || reactionRows.results.length === 0) continue + + const sourceRow = await db + .prepare('SELECT bindings FROM projection_def_sources WHERE projection_hash = ? AND event_def_hash = ?') + .bind(row.projection_hash, event.type_hash) + .first<{ bindings: string }>() + + if (!sourceRow) continue + const bindings: Record = JSON.parse(sourceRow.bindings) + + for (const reactionRow of reactionRows.results) { + const params = JSON.parse(reactionRow.params) + + let matches = true + for (const [property, value] of Object.entries(bindings)) { + const eventRefId = eventRefs.get(property) + if (eventRefId === undefined) { + matches = false + break + } + let expectedRefId: number + if (value.startsWith('$')) { + const paramKey = value.slice(1) + const paramVal = params[paramKey] + if (paramVal === undefined || paramVal === null) { + matches = false + break + } + expectedRefId = Number(paramVal) + } else { + expectedRefId = Number(value) + } + if (eventRefId !== expectedRefId) { + matches = false + break + } + } + + if (!matches) continue + + const nameRow = await db + .prepare('SELECT name FROM projection_def_names WHERE current_hash = ?') + .bind(row.projection_hash) + .first<{ name: string }>() + if (!nameRow) continue + + const paramsHash = computeParamsHash(params) + const defRow = await db + .prepare('SELECT initial_value FROM projection_def_versions WHERE hash = ?') + .bind(row.projection_hash) + .first<{ initial_value: string }>() + const cachedRow = await db + .prepare('SELECT value FROM projections WHERE def_hash = ? AND params_hash = ?') + .bind(row.projection_hash, paramsHash) + .first<{ value: string }>() + const oldValue = cachedRow ? JSON.parse(cachedRow.value) : defRow ? JSON.parse(defRow.initial_value) : null + + const startTime = Date.now() + const newValue = await getProjection(db, nameRow.name, params) + const durationMs = Date.now() - startTime + + if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { + const reactionAction = reactionRow.action || 'webhook' + + let handlerOutput: string | null = null + let status: 'success' | 'failed' = 'success' + + if (reactionAction === 'emit_event' && reactionRow.emit_event_type) { + try { + let emitPayload: Record + if (reactionRow.emit_payload_template) { + const tmpl = jsonata(reactionRow.emit_payload_template) + emitPayload = await tmpl.evaluate({ + old_value: oldValue, + new_value: newValue, + params, + event: buildEventContext(event), + }) + } else { + emitPayload = { old_value: oldValue, new_value: newValue, ...params } + } + emittedEvents.push({ type: reactionRow.emit_event_type, payload: emitPayload }) + } catch (err: any) { + status = 'failed' + handlerOutput = err.message || String(err) + } + } + + if (reactionAction === 'handler' && reactionRow.handler_code) { + const result = await executeHandler( + db, + reactionRow.id, + reactionRow.handler_code, + reactionRow.handler_timeout_ms || 5000, + { old_value: oldValue, new_value: newValue, params, event: buildEventContext(event) }, + ) + emittedEvents.push(...result.emittedEvents) + handlerOutput = result.output + status = result.status + } + + const logResult = await db + .prepare( + 'INSERT INTO reaction_logs (reaction_id, trigger_event_id, projection_def, old_value, new_value, action, status, handler_output, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + ) + .bind( + reactionRow.id, + event.id, + nameRow.name, + JSON.stringify(oldValue), + JSON.stringify(newValue), + reactionAction, + status, + handlerOutput, + durationMs, + Date.now(), + ) + .run() + + const logId = logResult.meta.last_row_id + + const payload = buildReactionPayload(reactionRow.id, nameRow.name, params, oldValue, newValue, event) + payload.log_id = logId + payloads.push(payload) + totalFired++ + + await pruneReactionLogs(db, reactionRow.id) + } else { + await db + .prepare( + 'INSERT INTO reaction_logs (reaction_id, trigger_event_id, projection_def, action, status, created_at) VALUES (?, ?, ?, ?, ?, ?)', + ) + .bind(reactionRow.id, event.id, nameRow.name, reactionRow.action || 'webhook', 'skipped', Date.now()) + .run() + + await pruneReactionLogs(db, reactionRow.id) + } + } + } + + for (const emitted of emittedEvents) { + try { + if (_reactionDepth < MAX_REACTION_DEPTH) { + await createEvent(db, emitted.type, emitted.payload) + } + } catch { + // Ignore emit errors + } + } + + return { fired: totalFired, payloads } +} + +// ============================================ +// Reaction Log Pruning +// ============================================ + +async function pruneReactionLogs(db: D1Database, reactionId: number): Promise { + const count = await db + .prepare('SELECT COUNT(*) as cnt FROM reaction_logs WHERE reaction_id = ?') + .bind(reactionId) + .first<{ cnt: number }>() + if (count && count.cnt > 1000) { + await db + .prepare( + 'DELETE FROM reaction_logs WHERE reaction_id = ? AND id NOT IN (SELECT id FROM reaction_logs WHERE reaction_id = ? ORDER BY id DESC LIMIT 1000)', + ) + .bind(reactionId, reactionId) + .run() + } +} + +// ============================================ +// Reaction Logs Query +// ============================================ + +export async function listReactionLogs( + db: D1Database, + limit: number, + offset: number, + reactionId?: number, +): Promise<{ logs: ReactionLog[]; total: number }> { + let countQuery + if (reactionId !== undefined) { + countQuery = db.prepare('SELECT COUNT(*) as count FROM reaction_logs WHERE reaction_id = ?').bind(reactionId) + } else { + countQuery = db.prepare('SELECT COUNT(*) as count FROM reaction_logs') + } + const countResult = await countQuery.first<{ count: number }>() + const total = countResult?.count || 0 + + let dataQuery + if (reactionId !== undefined) { + dataQuery = db + .prepare( + 'SELECT id, reaction_id, trigger_event_id, projection_def, old_value, new_value, action, status, handler_output, duration_ms, created_at FROM reaction_logs WHERE reaction_id = ? ORDER BY id DESC LIMIT ? OFFSET ?', + ) + .bind(reactionId, limit, offset) + } else { + dataQuery = db + .prepare( + 'SELECT id, reaction_id, trigger_event_id, projection_def, old_value, new_value, action, status, handler_output, duration_ms, created_at FROM reaction_logs ORDER BY id DESC LIMIT ? OFFSET ?', + ) + .bind(limit, offset) + } + + const rows = await dataQuery.all<{ + id: number + reaction_id: number + trigger_event_id: number + projection_def: string + old_value: string | null + new_value: string | null + action: string + status: string + handler_output: string | null + duration_ms: number | null + created_at: number + }>() + + const logs: ReactionLog[] = (rows.results || []).map((row) => ({ + id: row.id, + reaction_id: row.reaction_id, + trigger_event_id: row.trigger_event_id, + projection_def: row.projection_def, + old_value: row.old_value ? JSON.parse(row.old_value) : null, + new_value: row.new_value ? JSON.parse(row.new_value) : null, + action: row.action, + status: row.status as 'success' | 'failed' | 'skipped', + handler_output: row.handler_output || undefined, + duration_ms: row.duration_ms || undefined, + created_at: row.created_at, + })) + + return { logs, total } +} + +// ============================================ +// Reaction Payload Builder (for webhook POST) +// ============================================ + +export function buildReactionPayload( + reactionId: number, + projectionDef: string, + params: Record, + oldValue: any, + newValue: any, + event: Event, +): ReactionPayload { + return { + reaction_id: reactionId, + projection_def: projectionDef, + params, + old_value: oldValue, + new_value: newValue, + event: buildEventContext(event), + timestamp: Date.now(), + } +} + +// ============================================ +// API Keys +// ============================================ + +async function sha256(input: string): Promise { + const encoder = new TextEncoder() + const data = encoder.encode(input) + const hash = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +function generateApiKey(): string { + return 'ogk_' + crypto.randomUUID().replace(/-/g, '') +} + +export async function createApiKey( + db: D1Database, + name: string, + role: 'admin' | 'ingest' | 'readonly' = 'ingest', + allowedEvents: string[] = [], + rateLimit: number = 100, +): Promise { + const key = generateApiKey() + const keyHash = await sha256(key) + const createdAt = Date.now() + + const result = await db + .prepare( + 'INSERT INTO api_keys (key_hash, name, role, allowed_events, rate_limit, created_at) VALUES (?, ?, ?, ?, ?, ?)', + ) + .bind(keyHash, name, role, JSON.stringify(allowedEvents), rateLimit, createdAt) + .run() + + const id = result.meta.last_row_id + + return { + id, + name, + role, + allowed_events: allowedEvents, + rate_limit: rateLimit, + created_at: createdAt, + key, + } +} + +export async function listApiKeys( + db: D1Database, + limit = 50, + offset = 0, +): Promise<{ api_keys: ApiKey[]; total: number }> { + const countResult = await db.prepare('SELECT COUNT(*) as count FROM api_keys').first<{ count: number }>() + const total = countResult?.count || 0 + + const rows = await db + .prepare( + 'SELECT id, name, role, allowed_events, rate_limit, last_used_at, created_at FROM api_keys ORDER BY id DESC LIMIT ? OFFSET ?', + ) + .bind(limit, offset) + .all<{ + id: number + name: string + role: string + allowed_events: string + rate_limit: number + last_used_at: number | null + created_at: number + }>() + + const apiKeys: ApiKey[] = (rows.results || []).map((row) => ({ + id: row.id, + name: row.name, + role: row.role as 'admin' | 'ingest' | 'readonly', + allowed_events: JSON.parse(row.allowed_events), + rate_limit: row.rate_limit, + last_used_at: row.last_used_at || undefined, + created_at: row.created_at, + })) + + return { api_keys: apiKeys, total } +} + +export async function deleteApiKey(db: D1Database, id: number): Promise { + await db.prepare('DELETE FROM api_keys WHERE id = ?').bind(id).run() +} + +export async function validateApiKey( + db: D1Database, + key: string, + eventType?: string, +): Promise<{ valid: boolean; error?: string; apiKey?: ApiKey }> { + const keyHash = await sha256(key) + + const row = await db + .prepare( + 'SELECT id, name, role, allowed_events, rate_limit, last_used_at, created_at FROM api_keys WHERE key_hash = ?', + ) + .bind(keyHash) + .first<{ + id: number + name: string + role: string + allowed_events: string + rate_limit: number + last_used_at: number | null + created_at: number + }>() + + if (!row) return { valid: false, error: 'invalid_key' } + + const apiKey: ApiKey = { + id: row.id, + name: row.name, + role: row.role as 'admin' | 'ingest' | 'readonly', + allowed_events: JSON.parse(row.allowed_events), + rate_limit: row.rate_limit, + last_used_at: row.last_used_at || undefined, + created_at: row.created_at, + } + + if (eventType && apiKey.role === 'ingest') { + if (!apiKey.allowed_events.includes(eventType)) { + return { valid: false, error: 'event_not_allowed' } + } + } + + await db.prepare('UPDATE api_keys SET last_used_at = ? WHERE id = ?').bind(Date.now(), apiKey.id).run() + + return { valid: true, apiKey } +} diff --git a/packages/engine/src/html.d.ts b/packages/engine/src/html.d.ts new file mode 100644 index 0000000..44aa36b --- /dev/null +++ b/packages/engine/src/html.d.ts @@ -0,0 +1,4 @@ +declare module '*.html' { + const content: string + export default content +} diff --git a/packages/engine/src/index.test.ts b/packages/engine/src/index.test.ts new file mode 100644 index 0000000..9be8d76 --- /dev/null +++ b/packages/engine/src/index.test.ts @@ -0,0 +1,2040 @@ +// OGraph v2.4 Tests +// object_defs 回归单表 + version 表加 parent_hash + value_schema NOT NULL + +import { describe, it, expect, beforeEach } from 'vitest' +import appExport from './index' + +const API_TOKEN = 'test-token-secret' +const VERSION = '2.4.0' + +// ============================================ +// D1 Mock +// ============================================ + +function createD1Mock(tables: Record[]>) { + function findRows(sql: string, binds: unknown[]): Record[] { + const sqlLower = sql.toLowerCase().trim() + + if (sqlLower.startsWith('select')) { + const tableMatch = sql.match(/FROM\s+(\w+)/i) + if (!tableMatch) return [] + const table = tableMatch[1] + let rows = tables[table] ?? [] + + let bindIdx = 0 + + // Handle JOIN queries (supports multiple JOINs) + if (sqlLower.includes('join')) { + const fromAliasMatch = sql.match(/FROM\s+(\w+)\s+(\w+)/i) + const fromAlias = fromAliasMatch && fromAliasMatch[2].toUpperCase() !== 'JOIN' ? fromAliasMatch[2] : null + + const joinRegex = /JOIN\s+(\w+)\s+(\w+)\s+ON\s+((?:(?!JOIN).)+?)(?=\s+JOIN|\s+WHERE|\s+ORDER|\s+LIMIT|\s*$)/gi + let joinMatches = [...sql.matchAll(joinRegex)] + if (joinMatches.length === 0) { + const simpleJoinMatch = sql.match(/JOIN\s+(\w+)\s+(\w+)\s+ON\s+(.+?)(?:\s+WHERE|\s+ORDER|\s+LIMIT|\s*$)/i) + if (simpleJoinMatch) joinMatches = [simpleJoinMatch as unknown as RegExpExecArray] + } + + for (const jm of joinMatches) { + const joinTable = jm[1] + const joinAlias = jm[2] + const onClause = jm[3].trim() + + const onConditions = onClause.split(/\s+AND\s+/i) + const joinTableRows = tables[joinTable] || [] + + // Pre-scan for ? placeholders to know which binds to consume + const condBindIndices: number[] = [] + for (const cond of onConditions) { + if (/=\s*\?/.test(cond)) { + condBindIndices.push(bindIdx++) + } + } + + // Reset bindIdx to re-consume during actual matching + bindIdx -= condBindIndices.length + + const newRows: Record[] = [] + for (const row of rows) { + for (const joinRow of joinTableRows) { + let allMatch = true + let localBindIdx = bindIdx + for (const cond of onConditions) { + const eqParts = cond.match(/(\w+)\.(\w+)\s*=\s*(?:(\w+)\.(\w+)|(\?))/i) + if (!eqParts) { + allMatch = false + break + } + const [, lAlias, lCol, rAlias, rCol, isParam] = eqParts + const resolveVal = (alias: string, col: string) => { + if (alias === joinAlias) return joinRow[col] + if (alias === fromAlias || alias === table) return row[col] + return row[col] + } + if (isParam === '?') { + const lVal = resolveVal(lAlias, lCol) + const paramVal = binds[localBindIdx++] + if (lVal !== paramVal) { + allMatch = false + break + } + } else if (rAlias && rCol) { + const lVal = resolveVal(lAlias, lCol) + const rVal = resolveVal(rAlias!, rCol!) + if (lVal !== rVal) { + allMatch = false + break + } + } + } + if (allMatch) newRows.push({ ...row, ...joinRow }) + } + } + bindIdx += condBindIndices.length + rows = newRows + } + } + + const whereMatch = sql.match(/WHERE\s+(.+?)(?:\s+ORDER|\s+LIMIT|\s+$|$)/is) + if (whereMatch) { + const whereClause = whereMatch[1].trim() + const conditions = whereClause.split(/\s+AND\s+/i) + + for (const cond of conditions) { + if (cond === '1=1') continue + + const inMatch = cond.match(/(\w+(?:\.\w+)?)\s+IN\s*\(([^)]+)\)/i) + if (inMatch) { + const colExpr = inMatch[1] + const col = colExpr.includes('.') ? colExpr.split('.')[1] : colExpr + const placeholders = inMatch[2].split(',').map((p) => p.trim()) + const inValues: unknown[] = [] + for (const p of placeholders) { + if (p === '?') inValues.push(binds[bindIdx++]) + } + rows = rows.filter((r) => inValues.includes(r[col])) + continue + } + + const gtMatch = cond.match(/(\w+(?:\.\w+)?)\s*>\s*\?/) + if (gtMatch) { + const colExpr = gtMatch[1] + const col = colExpr.includes('.') ? colExpr.split('.')[1] : colExpr + const val = binds[bindIdx++] as number + rows = rows.filter((r) => (r[col] as number) > val) + continue + } + + const eqMatch = cond.match(/(\w+(?:\.\w+)?)\s*=\s*\?/) + if (eqMatch) { + const colExpr = eqMatch[1] + const col = colExpr.includes('.') ? colExpr.split('.')[1] : colExpr + const val = binds[bindIdx++] + rows = rows.filter((r) => r[col] === val) + } + } + } + + const orderMatch = sql.match(/ORDER\s+BY\s+(.+?)(?:\s+LIMIT|\s*$)/i) + if (orderMatch) { + const orderClauses = orderMatch[1].split(',').map((c) => c.trim()) + const orderSpecs = orderClauses.map((clause) => { + const parts = clause.split(/\s+/) + const colExpr = parts[0] + const col = colExpr.includes('.') ? colExpr.split('.')[1] : colExpr + const dir = (parts[1] || 'ASC').toUpperCase() + return { col, dir } + }) + rows = [...rows].sort((a, b) => { + for (const { col, dir } of orderSpecs) { + const aVal = a[col] + const bVal = b[col] + if (aVal < bVal) return dir === 'ASC' ? -1 : 1 + if (aVal > bVal) return dir === 'ASC' ? 1 : -1 + } + return 0 + }) + } + + // Handle SELECT DISTINCT (non-COUNT) + if (sqlLower.includes('select distinct') && !sqlLower.includes('count(distinct')) { + const distinctColMatch = sql.match(/SELECT\s+DISTINCT\s+(\w+(?:\.\w+)?)/i) + if (distinctColMatch) { + const colExpr = distinctColMatch[1] + const col = colExpr.includes('.') ? colExpr.split('.')[1] : colExpr + const seen = new Set() + rows = rows.filter((r) => { + if (seen.has(r[col])) return false + seen.add(r[col]) + return true + }) + } + } + + // Handle COUNT(*) + if (sqlLower.includes('count(*)') || sqlLower.includes('count(distinct')) { + const aliasMatch = sql.match(/COUNT\([^)]*\)\s+(?:as\s+)?(\w+)/i) + const alias = aliasMatch ? aliasMatch[1] : 'count' + const distinctMatch = sql.match(/COUNT\(DISTINCT\s+(\w+(?:\.\w+)?)\)/i) + if (distinctMatch) { + const colExpr = distinctMatch[1] + const col = colExpr.includes('.') ? colExpr.split('.')[1] : colExpr + const uniqueVals = new Set(rows.map((r) => r[col])) + return [{ [alias]: uniqueVals.size }] + } + return [{ [alias]: rows.length }] + } + + // Handle LIMIT and OFFSET + const limitMatch = sql.match(/LIMIT\s+(\?|\d+)(?:\s+OFFSET\s+(\?|\d+))?/i) + if (limitMatch) { + const limitVal = limitMatch[1] === '?' ? (binds[bindIdx++] as number) : parseInt(limitMatch[1], 10) + const offsetVal = limitMatch[2] + ? limitMatch[2] === '?' + ? (binds[bindIdx++] as number) + : parseInt(limitMatch[2], 10) + : 0 + rows = rows.slice(offsetVal, offsetVal + limitVal) + } + + return rows + } + + return [] + } + + const autoIncrementCounters: Record = {} + const autoIncrementTables = new Set(['objects', 'events', 'reactions', 'reaction_logs', 'api_keys', 'request_logs']) + + return { + prepare: (sql: string) => { + const binds: unknown[] = [] + return { + bind(...args: unknown[]) { + binds.push(...args) + return this + }, + run() { + const sqlLower = sql.toLowerCase().trim() + let lastRowId = 0 + if (sqlLower.startsWith('insert')) { + const tableMatch = sql.match(/INTO\s+(\w+)/i) + if (tableMatch) { + const table = tableMatch[1] + if (!tables[table]) tables[table] = [] + const valuesMatch = sql.match(/VALUES\s*\(([^)]+)\)/i) + if (valuesMatch) { + const placeholders = valuesMatch[1].split(',').map((p) => p.trim()) + const columnsMatch = sql.match(/\(([^)]+)\)\s*VALUES/i) + const columns = columnsMatch ? columnsMatch[1].split(',').map((c) => c.trim()) : [] + const row: Record = {} + columns.forEach((col, i) => { + if (placeholders[i] === '?') { + row[col] = binds[i] + } + }) + if (sqlLower.includes('or replace')) { + const compositePKs: Record = { reaction_kv: ['reaction_id', 'key'] } + const pkCols = compositePKs[table] || [columns[0]] + const existingIdx = tables[table].findIndex((r) => pkCols.every((col) => r[col] === row[col])) + if (existingIdx >= 0) { + tables[table][existingIdx] = row + } else { + tables[table].push(row) + } + } else if (sqlLower.includes('or ignore')) { + const pkCol = columns[0] + const exists = tables[table].find((r) => r[pkCol] === row[pkCol]) + if (!exists) tables[table].push(row) + } else if (sqlLower.includes('on conflict')) { + const conflictMatch = sql.match(/ON\s+CONFLICT\s*\(([^)]+)\)/i) + if (conflictMatch) { + const conflictCols = conflictMatch[1].split(',').map((c) => c.trim()) + const existingIdx = tables[table].findIndex((r) => conflictCols.every((col) => r[col] === row[col])) + if (existingIdx >= 0) { + const updateMatch = sql.match(/DO\s+UPDATE\s+SET\s+(.+?)$/i) + if (updateMatch) { + const updates = updateMatch[1].split(',').map((u) => u.trim()) + for (const u of updates) { + const setMatch = u.match(/(\w+)\s*=\s*excluded\.(\w+)/i) + if (setMatch) { + tables[table][existingIdx][setMatch[1]] = row[setMatch[2]] + } + } + } + } else { + tables[table].push(row) + } + } + } else { + if (autoIncrementTables.has(table)) { + if (!autoIncrementCounters[table]) autoIncrementCounters[table] = 0 + autoIncrementCounters[table]++ + row.id = autoIncrementCounters[table] + lastRowId = autoIncrementCounters[table] + } + tables[table].push(row) + } + } + } + } else if (sqlLower.startsWith('update')) { + const tableMatch = sql.match(/UPDATE\s+(\w+)/i) + if (tableMatch) { + const table = tableMatch[1] + const setMatch = sql.match(/SET\s+(.+?)\s+WHERE/i) + const whereMatch = sql.match(/WHERE\s+(.+)/i) + if (setMatch && whereMatch) { + const setClauses = setMatch[1].split(',').map((s) => s.trim()) + const whereCond = whereMatch[1].trim() + const whereEq = whereCond.match(/(\w+)\s*=\s*\?/) + let setBindIdx = 0 + const setUpdates: Array<{ col: string; val: unknown }> = [] + for (const clause of setClauses) { + const m = clause.match(/(\w+)\s*=\s*\?/) + if (m) setUpdates.push({ col: m[1], val: binds[setBindIdx++] }) + } + if (whereEq) { + const whereCol = whereEq[1] + const whereVal = binds[setBindIdx] + for (const row of tables[table] || []) { + if (row[whereCol] === whereVal) { + for (const u of setUpdates) row[u.col] = u.val + } + } + } + } + } + } else if (sqlLower.startsWith('delete')) { + const tableMatch = sql.match(/FROM\s+(\w+)/i) + if (tableMatch) { + const table = tableMatch[1] + const whereMatch = sql.match(/WHERE\s+(.+)/i) + if (whereMatch) { + const cond = whereMatch[1].trim() + const notInMatch = cond.match( + /(\w+)\s*=\s*\?\s+AND\s+(\w+)\s+NOT\s+IN\s*\(\s*SELECT\s+(\w+)\s+FROM\s+(\w+)\s+WHERE\s+(\w+)\s*=\s*\?\s+ORDER\s+BY\s+(\w+)\s+DESC\s+LIMIT\s+(\d+)\s*\)/i, + ) + if (notInMatch) { + const filterCol = notInMatch[1] + const filterVal = binds[0] + const idCol = notInMatch[2] + const subIdCol = notInMatch[3] + const subFilterCol = notInMatch[5] + const subFilterVal = binds[1] + const orderCol = notInMatch[6] + const limitNum = parseInt(notInMatch[7], 10) + const subRows = (tables[table] || []) + .filter((r) => r[subFilterCol] === subFilterVal) + .sort((a, b) => (b[orderCol] as number) - (a[orderCol] as number)) + .slice(0, limitNum) + const keepIds = new Set(subRows.map((r) => r[subIdCol])) + tables[table] = (tables[table] || []).filter( + (r) => !(r[filterCol] === filterVal && !keepIds.has(r[idCol])), + ) + } else { + const conditions = cond.split(/\s+AND\s+/i) + if (conditions.length > 1) { + let delBindIdx = 0 + tables[table] = (tables[table] || []).filter((r) => { + let matchAll = true + let localIdx = delBindIdx + for (const c of conditions) { + const em = c.match(/(\w+)\s*=\s*\?/) + if (em) { + if (r[em[1]] !== binds[localIdx++]) matchAll = false + } + } + return !matchAll + }) + delBindIdx += conditions.length + } else { + const eqMatch = cond.match(/(\w+)\s*=\s*\?/) + if (eqMatch) { + const col = eqMatch[1] + const val = binds[0] + tables[table] = (tables[table] || []).filter((r) => r[col] !== val) + } + } + } + } else { + tables[table] = [] + } + } + } + return { success: true, meta: { last_row_id: lastRowId } } + }, + all() { + const rows = findRows(sql, binds) + return { results: rows as T[] } + }, + first() { + const rows = findRows(sql, binds) + return rows[0] as T | null + }, + } + }, + } as unknown as D1Database +} + +// ============================================ +// Test Setup +// ============================================ + +let app: ReturnType +let db: D1Database +let tables: Record[]> + +beforeEach(() => { + tables = { + object_defs: [], + event_def_versions: [], + event_def_names: [], + projection_def_versions: [], + projection_def_names: [], + projection_def_sources: [], + objects: [], + events: [], + event_refs: [], + projections: [], + reactions: [], + reaction_logs: [], + api_keys: [], + reaction_kv: [], + request_logs: [], + } + db = createD1Mock(tables) + app = appExport +}) + +function req(method: string, path: string, body?: unknown, token = API_TOKEN) { + const headers: HeadersInit = { 'Content-Type': 'application/json' } + if (token) headers['Authorization'] = `Bearer ${token}` + return new Request(`http://test${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) +} + +// ============================================ +// Health & Auth +// ============================================ + +describe('Health', () => { + it('GET /health returns ok + version', async () => { + const res = await app.fetch(req('GET', '/health'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json).toEqual({ status: 'ok', version: VERSION }) + }) +}) + +describe('Auth', () => { + it('returns 401 without token', async () => { + const res = await app.fetch(req('POST', '/object-defs', { name: 'test' }, ''), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(401) + }) + + it('returns 401 with wrong token', async () => { + const res = await app.fetch(req('POST', '/object-defs', { name: 'test' }, 'wrong'), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(401) + }) +}) + +// ============================================ +// Object Defs +// ============================================ + +describe('Object Defs', () => { + it('POST /object-defs creates object_def (no version/hash)', async () => { + const res = await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(201) + const json = await res.json() + expect(json.name).toBe('agent') + expect(json).not.toHaveProperty('hash') // v2.4: no hash for object_defs + expect(tables.object_defs).toHaveLength(1) + expect(tables.object_defs[0]).toEqual({ name: 'agent' }) + }) + + it('POST /object-defs with same name is idempotent (INSERT OR IGNORE)', async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + const res2 = await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + expect(res2.status).toBe(201) + expect(tables.object_defs).toHaveLength(1) + }) + + it('GET /object-defs lists defs without hash', async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const res = await app.fetch(req('GET', '/object-defs'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.object_defs).toHaveLength(2) + expect(json.object_defs[0]).toEqual({ name: 'agent' }) + }) +}) + +// ============================================ +// Objects +// ============================================ + +describe('Objects', () => { + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /objects creates object with type=name and integer id', async () => { + const res = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(201) + const json = await res.json() + expect(typeof json.id).toBe('number') + expect(json.type).toBe('agent') + expect(tables.objects).toHaveLength(1) + expect(tables.objects[0].type).toBe('agent') + }) + + it('GET /objects/:id returns object', async () => { + const created = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + const { id } = await created.json() + const res = await app.fetch(req('GET', `/objects/${id}`), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.id).toBe(id) + expect(json.type).toBe('agent') + }) + + it('GET /objects?type=X filters by name directly', async () => { + await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const res = await app.fetch(req('GET', '/objects?type=agent'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.objects).toHaveLength(1) + expect(json.total).toBe(1) + expect(json.objects[0].type).toBe('agent') + }) + + it('GET /objects with pagination (limit & offset)', async () => { + await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + const res = await app.fetch(req('GET', '/objects?limit=1&offset=0'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.objects).toHaveLength(1) + expect(json.total).toBe(3) + }) +}) + +// ============================================ +// Event Defs +// ============================================ + +describe('Event Defs', () => { + it('POST /event-defs creates version + name pointer (with parent_hash=null)', async () => { + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + const res = await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(201) + const json = await res.json() + expect(json.name).toBe('task_assigned') + expect(json.hash).toBeDefined() + expect(tables.event_def_versions).toHaveLength(1) + expect(tables.event_def_versions[0].parent_hash).toBeNull() // 首版 + expect(tables.event_def_versions[0].name).toBe('task_assigned') // v2.4: version 存 name + expect(tables.event_def_names).toHaveLength(1) + }) + + it('POST /event-defs with different schema creates new version (with parent_hash)', async () => { + const schema1 = { properties: { participant: { type: 'ref' as const } } } + const schema2 = { properties: { participant: { type: 'string' as const } } } + const res1 = await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema: schema1 }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const { hash: hash1 } = await res1.json() + const res2 = await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema: schema2 }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res2.status).toBe(201) + expect(tables.event_def_versions).toHaveLength(2) // two different hashes + const v2 = tables.event_def_versions.find((v) => v.hash !== hash1) + expect(v2?.parent_hash).toBe(hash1) // v2.4: parent_hash 指向前一版本 + expect(tables.event_def_names).toHaveLength(1) // name points to latest + }) + + it('GET /event-defs lists defs with parent_hash', async () => { + const schema = { properties: { participant: { type: 'ref' as const } } } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + const res = await app.fetch(req('GET', '/event-defs'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.event_defs).toHaveLength(1) + expect(json.event_defs[0].name).toBe('task_assigned') + expect(json.event_defs[0].hash).toBeDefined() + expect(json.event_defs[0].parent_hash).toBeNull() + expect(json.event_defs[0].schema).toEqual(schema) + }) +}) + +// ============================================ +// Events +// ============================================ + +describe('Events', () => { + let agentId: number + let taskId: number + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + agentId = (await agentRes.json()).id + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + taskId = (await taskRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /events creates event with resolved type_hash', async () => { + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(201) + const json = await res.json() + expect(typeof json.event.id).toBe('number') + expect(json.event.type_hash).toBeDefined() + expect(json.event.payload).toEqual(payload) + expect(tables.events).toHaveLength(1) + expect(tables.event_refs).toHaveLength(2) + }) + + it('POST /events validates ref type against object.type name (not hash)', async () => { + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(201) + }) + + it('GET /events/:id returns event', async () => { + const created = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const { event } = await created.json() + const res = await app.fetch(req('GET', `/events/${event.id}`), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.id).toBe(event.id) + expect(json.payload).toEqual({ participant: agentId, subject: taskId }) + }) + + it('GET /events?ref=X returns events by ref', async () => { + await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { + DB: db, + API_TOKEN: API_TOKEN, + }, + ) + const res = await app.fetch(req('GET', `/events?ref=${taskId}`), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.events).toHaveLength(1) + expect(json.total).toBe(1) + }) +}) + +// ============================================ +// Projection Defs (v2.4: value_schema + initial_value NOT NULL) +// ============================================ + +describe('Projection Defs', () => { + beforeEach(async () => { + const schema = { properties: { participant: { type: 'ref' as const } } } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /projection-defs requires value_schema and initial_value', async () => { + const body = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + const res = await app.fetch(req('POST', '/projection-defs', body), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(201) + const json = await res.json() + expect(json.name).toBe('current_assignee') + expect(json.hash).toBeDefined() + expect(tables.projection_def_versions).toHaveLength(1) + expect(tables.projection_def_versions[0].parent_hash).toBeNull() + expect(tables.projection_def_versions[0].name).toBe('current_assignee') + }) + + it('POST /projection-defs with different definition creates new version (with parent_hash)', async () => { + const body1 = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + const body2 = { + ...body1, + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.id', + }, + ], + } + const res1 = await app.fetch(req('POST', '/projection-defs', body1), { DB: db, API_TOKEN: API_TOKEN }) + const { hash: hash1 } = await res1.json() + const res2 = await app.fetch(req('POST', '/projection-defs', body2), { DB: db, API_TOKEN: API_TOKEN }) + expect(res2.status).toBe(201) + expect(tables.projection_def_versions).toHaveLength(2) + const v2 = tables.projection_def_versions.find((v) => v.hash !== hash1) + expect(v2?.parent_hash).toBe(hash1) + }) + + it('GET /projection-defs lists defs with value_schema, sources and parent_hash', async () => { + const body = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + await app.fetch(req('POST', '/projection-defs', body), { DB: db, API_TOKEN: API_TOKEN }) + const res = await app.fetch(req('GET', '/projection-defs'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.projection_defs).toHaveLength(1) + expect(json.projection_defs[0].name).toBe('current_assignee') + expect(json.projection_defs[0].value_schema).toEqual({ type: 'ref' }) + expect(json.projection_defs[0].initial_value).toBe('') + expect(json.projection_defs[0].parent_hash).toBeNull() + expect(json.projection_defs[0].sources).toHaveLength(1) + expect(json.projection_defs[0].sources[0].bindings).toEqual({ subject: '$task_id' }) + expect(json.projection_defs[0].sources[0].expression).toBe('event.participant') + }) +}) + +// ============================================ +// Projections (value NOT NULL) +// ============================================ + +describe('Projections', () => { + let agentId: number + let taskId: number + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + agentId = (await agentRes.json()).id + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + taskId = (await taskRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + const projDef = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + await app.fetch(req('POST', '/projection-defs', projDef), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('GET /projections/:name returns initial_value when no events', async () => { + const res = await app.fetch(req('GET', `/projections/current_assignee?task_id=${taskId}`), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.value).toBe('') + expect(tables.projections).toHaveLength(1) + expect(tables.projections[0].value).toBe('""') + }) + + it('GET /projections/:name computes value lazily after event', async () => { + await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { + DB: db, + API_TOKEN: API_TOKEN, + }, + ) + const res = await app.fetch(req('GET', `/projections/current_assignee?task_id=${taskId}`), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.value).toBe(agentId) + }) +}) + +// ============================================ +// Reactions +// ============================================ + +describe('Reactions', () => { + beforeEach(async () => { + const schema = { properties: { participant: { type: 'ref' as const } } } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + const projDef = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + await app.fetch(req('POST', '/projection-defs', projDef), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /reactions creates reaction with resolved projection_def_hash', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: 1 }, + webhook_url: 'https://hook.example.com/notify', + } + const res = await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(201) + const json = await res.json() + expect(json.id).toBeDefined() + expect(json.projection_def_hash).toBeDefined() + expect(tables.reactions).toHaveLength(1) + }) + + it('GET /reactions lists reactions', async () => { + await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: 1 }, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const res = await app.fetch(req('GET', '/reactions'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.reactions).toHaveLength(1) + expect(json.total).toBe(1) + }) + + it('DELETE /reactions/:id deletes reaction', async () => { + const created = await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: 1 }, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const { id } = await created.json() + const res = await app.fetch(req('DELETE', `/reactions/${id}`), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + expect(tables.reactions).toHaveLength(0) + }) +}) + +// ============================================ +// E2E: Event → Projection Update → Reactions Fired +// ============================================ + +describe('E2E: Reaction Chain', () => { + let agentId: number + let taskId: number + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + agentId = (await agentRes.json()).id + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + taskId = (await taskRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + const projDef = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + await app.fetch(req('POST', '/projection-defs', projDef), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /events fires reactions', async () => { + await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: taskId }, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(201) + const json = await res.json() + expect(json.reactions_fired).toBeGreaterThan(0) + }) + + it('reaction_results include old_value and new_value (#213)', async () => { + await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: taskId }, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const json = await res.json() + expect(json.reaction_results).toHaveLength(1) + expect(json.reaction_results[0].old_value).toBe('') + expect(json.reaction_results[0].new_value).toBe(agentId) + expect(json.reaction_results[0].projection_def).toBe('current_assignee') + }) + + it('reaction does not fire when projection value unchanged (#213)', async () => { + await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: taskId }, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const res1 = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const json1 = await res1.json() + expect(json1.reactions_fired).toBe(1) + + const res2 = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const json2 = await res2.json() + expect(json2.reactions_fired).toBe(0) + expect(json2.reaction_results).toHaveLength(0) + }) + + it('emit_event reaction creates new event when projection changes (#213 phase 2)', async () => { + await app.fetch( + req('POST', '/event-defs', { + name: 'task_assignee_changed', + schema: { properties: { task: { type: 'ref' as const, object_type: 'task' } } }, + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const task2Res = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const task2Id = (await task2Res.json()).id + + const reactionRes = await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'emit_event', + emit_event_type: 'task_assignee_changed', + emit_payload_template: '{ "task": params.task_id }', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(reactionRes.status).toBe(201) + const reaction = await reactionRes.json() + expect(reaction.action).toBe('emit_event') + + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const json = await res.json() + expect(json.reactions_fired).toBe(1) + + const eventsRes = await app.fetch(req('GET', `/events?ref=${taskId}`), { DB: db, API_TOKEN: API_TOKEN }) + const eventsJson = await eventsRes.json() + expect(eventsJson.events.length).toBeGreaterThanOrEqual(1) + }) +}) + +describe('Error Cases: Object Defs', () => { + it('POST /object-defs missing name → 400', async () => { + const res = await app.fetch(req('POST', '/object-defs', {}), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(400) + const json = await res.json() + expect(json.error).toContain('Missing name') + }) + + it('POST /object-defs empty name → 400', async () => { + const res = await app.fetch(req('POST', '/object-defs', { name: '' }), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(400) + const json = await res.json() + expect(json.error).toContain('Missing name') + }) +}) + +describe('Error Cases: Objects', () => { + it('POST /objects type not defined → 500', async () => { + const res = await app.fetch(req('POST', '/objects', { type: 'nonexistent' }), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('Object type nonexistent not defined') + }) + + it('GET /objects/:id not found → 404', async () => { + const res = await app.fetch(req('GET', '/objects/99999'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(404) + }) + + it('GET /objects?type=nonexistent → returns empty array', async () => { + const res = await app.fetch(req('GET', '/objects?type=nonexistent'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.objects).toEqual([]) + expect(json.total).toBe(0) + }) +}) + +describe('Error Cases: Event Defs', () => { + it('POST /event-defs missing schema → 400', async () => { + const res = await app.fetch(req('POST', '/event-defs', { name: 'test' }), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(400) + }) + + it('POST /event-defs schema without properties → 500', async () => { + const res = await app.fetch(req('POST', '/event-defs', { name: 'test', schema: {} }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('properties') + }) + + it('POST /event-defs invalid property type → 500', async () => { + const schema = { properties: { field: { type: 'invalid' } } } + const res = await app.fetch(req('POST', '/event-defs', { name: 'test', schema }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('Invalid property type') + }) + + it('POST /event-defs duplicate schema → idempotent (same hash)', async () => { + const schema = { properties: { field: { type: 'string' as const } } } + const res1 = await app.fetch(req('POST', '/event-defs', { name: 'test', schema }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const json1 = await res1.json() + const res2 = await app.fetch(req('POST', '/event-defs', { name: 'test', schema }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const json2 = await res2.json() + expect(json1.hash).toBe(json2.hash) + }) +}) + +describe('Error Cases: Events', () => { + let agentId: number + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + agentId = (await agentRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + count: { type: 'number' as const }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'test_event', schema }), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /events type not defined → 500', async () => { + const res = await app.fetch(req('POST', '/events', { type: 'nonexistent', payload: {} }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('Event type nonexistent not defined') + }) + + it('POST /events ref nonexistent object → 500', async () => { + const res = await app.fetch(req('POST', '/events', { type: 'test_event', payload: { participant: 99999 } }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('does not exist') + }) + + it('POST /events ref type mismatch → 500', async () => { + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const taskId = (await taskRes.json()).id + const res = await app.fetch(req('POST', '/events', { type: 'test_event', payload: { participant: taskId } }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('type mismatch') + }) + + it('POST /events payload property wrong type → 500', async () => { + const res = await app.fetch( + req('POST', '/events', { type: 'test_event', payload: { participant: agentId, count: 'string' } }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('must be number') + }) + + it('GET /events/:id not found → 404', async () => { + const res = await app.fetch(req('GET', '/events/99999'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(404) + }) + + it('GET /events without ref → returns all events', async () => { + const res = await app.fetch(req('GET', '/events'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.events).toBeDefined() + expect(json.total).toBeDefined() + }) + + it('GET /events?ref=nonexistent → returns empty array', async () => { + const res = await app.fetch(req('GET', '/events?ref=99999'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.events).toEqual([]) + expect(json.total).toBe(0) + }) +}) + +describe('Error Cases: Projection Defs', () => { + beforeEach(async () => { + const schema = { properties: { field: { type: 'string' as const } } } + await app.fetch(req('POST', '/event-defs', { name: 'test_event', schema }), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /projection-defs missing value_schema → 400', async () => { + const res = await app.fetch( + req('POST', '/projection-defs', { + name: 'test_proj', + sources: [{ event_def: 'test_event', bindings: {}, expression: '"value"' }], + params: {}, + initial_value: '', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(400) + }) + + it('POST /projection-defs missing initial_value → 400', async () => { + const res = await app.fetch( + req('POST', '/projection-defs', { + name: 'test_proj', + sources: [{ event_def: 'test_event', bindings: {}, expression: '"value"' }], + params: {}, + value_schema: { type: 'string' }, + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(400) + }) + + it('POST /projection-defs missing sources → 400', async () => { + const res = await app.fetch( + req('POST', '/projection-defs', { + name: 'test_proj', + params: {}, + value_schema: { type: 'string' }, + initial_value: '', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(400) + const json = await res.json() + expect(json.error).toContain('sources') + }) + + it('POST /projection-defs invalid value_schema type → 500', async () => { + const res = await app.fetch( + req('POST', '/projection-defs', { + name: 'test_proj', + sources: [{ event_def: 'test_event', bindings: {}, expression: '"value"' }], + params: {}, + value_schema: { type: 'invalid' }, + initial_value: '', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('Invalid value_schema type') + }) + + it('POST /projection-defs initial_value type mismatch → 500', async () => { + const res = await app.fetch( + req('POST', '/projection-defs', { + name: 'test_proj', + sources: [{ event_def: 'test_event', bindings: {}, expression: '"value"' }], + params: {}, + value_schema: { type: 'number' }, + initial_value: 'not_a_number', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('initial_value must be number') + }) + + it('POST /projection-defs source with nonexistent event → 500', async () => { + const res = await app.fetch( + req('POST', '/projection-defs', { + name: 'test_proj', + sources: [{ event_def: 'nonexistent', bindings: {}, expression: '"value"' }], + params: {}, + value_schema: { type: 'string' }, + initial_value: '', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('Event type nonexistent not defined') + }) + + it('POST /projection-defs duplicate definition → idempotent (same hash)', async () => { + const body = { + name: 'test_proj', + sources: [{ event_def: 'test_event', bindings: {}, expression: '"value"' }], + params: {}, + value_schema: { type: 'string' }, + initial_value: '', + } + const res1 = await app.fetch(req('POST', '/projection-defs', body), { DB: db, API_TOKEN: API_TOKEN }) + const json1 = await res1.json() + const res2 = await app.fetch(req('POST', '/projection-defs', body), { DB: db, API_TOKEN: API_TOKEN }) + const json2 = await res2.json() + expect(json1.hash).toBe(json2.hash) + }) +}) + +describe('Error Cases: Projections', () => { + it('GET /projections/:name not found → 500', async () => { + const res = await app.fetch(req('GET', '/projections/nonexistent?param=value'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('Projection def nonexistent not found') + }) +}) + +describe('Error Cases: Reactions', () => { + it('POST /reactions projection_def not found → 500', async () => { + const res = await app.fetch( + req('POST', '/reactions', { + projection_def: 'nonexistent', + params: {}, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + expect(res.status).toBe(500) + const json = await res.json() + expect(json.error).toContain('Projection def nonexistent not found') + }) + + it('DELETE /reactions/:id nonexistent → 200 (idempotent)', async () => { + const res = await app.fetch(req('DELETE', '/reactions/99999'), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(200) + }) +}) + +// ============================================ +// Reaction Logs (#217) +// ============================================ + +describe('Reaction Logs', () => { + let agentId: number + let taskId: number + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + agentId = (await agentRes.json()).id + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + taskId = (await taskRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + const projDef = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + await app.fetch(req('POST', '/projection-defs', projDef), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: taskId }, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + }) + + it('reaction log created on fire with status=success', async () => { + const payload = { participant: agentId, subject: taskId } + const eventRes = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const eventJson = await eventRes.json() + expect(eventJson.reactions_fired).toBe(1) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + expect(logsRes.status).toBe(200) + const logsJson = await logsRes.json() + expect(logsJson.logs.length).toBeGreaterThanOrEqual(1) + const successLog = logsJson.logs.find((l: any) => l.status === 'success') + expect(successLog).toBeDefined() + expect(successLog.projection_def).toBe('current_assignee') + expect(successLog.action).toBe('webhook') + }) + + it('reaction log skipped on no change', async () => { + const payload = { participant: agentId, subject: taskId } + await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + + await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const skippedLog = logsJson.logs.find((l: any) => l.status === 'skipped') + expect(skippedLog).toBeDefined() + }) + + it('log includes correct projection_def, old_value, new_value', async () => { + const payload = { participant: agentId, subject: taskId } + await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const log = logsJson.logs.find((l: any) => l.status === 'success') + expect(log).toBeDefined() + expect(log.projection_def).toBe('current_assignee') + expect(log.old_value).toBe('') + expect(log.new_value).toBe(agentId) + }) + + it('filter by reaction_id works', async () => { + const payload = { participant: agentId, subject: taskId } + await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + + const reactionId = tables.reactions[0].id + const filteredRes = await app.fetch(req('GET', `/reaction-logs?reaction_id=${reactionId}`), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const filteredJson = await filteredRes.json() + expect(filteredJson.logs.length).toBeGreaterThanOrEqual(1) + expect(filteredJson.logs.every((l: any) => l.reaction_id === reactionId)).toBe(true) + + const emptyRes = await app.fetch(req('GET', '/reaction-logs?reaction_id=99999'), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const emptyJson = await emptyRes.json() + expect(emptyJson.logs).toHaveLength(0) + expect(emptyJson.total).toBe(0) + }) +}) + +// ============================================ +// Reaction Handler (#220) +// ============================================ + +describe('Reaction Handler', () => { + let agentId: number + let taskId: number + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + agentId = (await agentRes.json()).id + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN: API_TOKEN }) + taskId = (await taskRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN }) + const projDef = { + name: 'current_assignee', + sources: [ + { + event_def: 'task_assigned', + bindings: { subject: '$task_id' }, + expression: 'event.participant', + }, + ], + params: { task_id: { type: 'ref' } }, + value_schema: { type: 'ref' }, + initial_value: '', + } + await app.fetch(req('POST', '/projection-defs', projDef), { DB: db, API_TOKEN: API_TOKEN }) + }) + + it('POST /reactions with action=handler + handler_code succeeds', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + handler_code: 'log("hello")', + } + const res = await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(201) + const json = await res.json() + expect(json.action).toBe('handler') + expect(json.handler_code).toBe('log("hello")') + }) + + it('POST /reactions with action=handler without handler_code → 400', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + } + const res = await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + expect(res.status).toBe(400) + const json = await res.json() + expect(json.error).toContain('handler_code is required') + }) + + it('handler receives context (old_value, new_value, params, event)', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + handler_code: + 'log("old=" + JSON.stringify(old_value)); log("new=" + JSON.stringify(new_value)); log("params=" + JSON.stringify(params)); log("event_id=" + event.id)', + } + await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const json = await res.json() + expect(json.reactions_fired).toBe(1) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const successLog = logsJson.logs.find((l: any) => l.status === 'success') + expect(successLog).toBeDefined() + expect(successLog.handler_output).toContain('old=') + expect(successLog.handler_output).toContain('new=') + expect(successLog.handler_output).toContain('params=') + expect(successLog.handler_output).toContain('event_id=') + }) + + it('handler emit() creates new event via reaction chain', async () => { + await app.fetch( + req('POST', '/event-defs', { + name: 'task_assignee_changed', + schema: { properties: { task: { type: 'ref' as const, object_type: 'task' } } }, + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + handler_code: 'await emit("task_assignee_changed", { task: params.task_id }); log("emitted")', + } + await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const json = await res.json() + expect(json.reactions_fired).toBe(1) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const successLog = logsJson.logs.find((l: any) => l.status === 'success' && l.action === 'handler') + expect(successLog).toBeDefined() + expect(successLog.handler_output).toContain('emitted') + }) + + it('handler log() output appears in reaction_logs.handler_output', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + handler_code: 'log("processed"); log("done")', + } + await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + + const payload = { participant: agentId, subject: taskId } + await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const successLog = logsJson.logs.find((l: any) => l.status === 'success') + expect(successLog).toBeDefined() + expect(successLog.handler_output).toContain('processed') + expect(successLog.handler_output).toContain('done') + }) + + it('handler kv.set/get persists across multiple triggers', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + handler_code: + 'const count = (await kv.get("count")) || 0; await kv.set("count", count + 1); log("count=" + (count + 1))', + } + await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + + const agent2Res = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN: API_TOKEN }) + const agent2Id = (await agent2Res.json()).id + + await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agent2Id, subject: taskId } }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const handlerLogs = logsJson.logs.filter((l: any) => l.status === 'success' && l.action === 'handler') + expect(handlerLogs.length).toBe(2) + const outputs = handlerLogs.map((l: any) => l.handler_output) + expect(outputs).toContain('count=1') + expect(outputs).toContain('count=2') + }) + + it('handler runtime error → status=failed, error in handler_output', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + handler_code: 'throw new Error("boom")', + } + await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + + const payload = { participant: agentId, subject: taskId } + await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const failedLog = logsJson.logs.find((l: any) => l.status === 'failed') + expect(failedLog).toBeDefined() + expect(failedLog.handler_output).toContain('boom') + }) + + it('handler timeout → status=failed, error mentions timeout', async () => { + const body = { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'handler', + handler_code: 'await new Promise(resolve => setTimeout(resolve, 10000))', + handler_timeout_ms: 100, + } + await app.fetch(req('POST', '/reactions', body), { DB: db, API_TOKEN: API_TOKEN }) + + const payload = { participant: agentId, subject: taskId } + await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + + const logsRes = await app.fetch(req('GET', '/reaction-logs'), { DB: db, API_TOKEN: API_TOKEN }) + const logsJson = await logsRes.json() + const failedLog = logsJson.logs.find((l: any) => l.status === 'failed') + expect(failedLog).toBeDefined() + expect(failedLog.handler_output).toContain('timeout') + }, 10000) + + it('backward compat: webhook still works', async () => { + await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: taskId }, + webhook_url: 'https://hook.example.com', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const json = await res.json() + expect(json.reactions_fired).toBe(1) + expect(json.reaction_results[0].old_value).toBe('') + expect(json.reaction_results[0].new_value).toBe(agentId) + }) + + it('backward compat: emit_event still works', async () => { + await app.fetch( + req('POST', '/event-defs', { + name: 'task_assignee_changed', + schema: { properties: { task: { type: 'ref' as const, object_type: 'task' } } }, + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + + await app.fetch( + req('POST', '/reactions', { + projection_def: 'current_assignee', + params: { task_id: taskId }, + action: 'emit_event', + emit_event_type: 'task_assignee_changed', + emit_payload_template: '{ "task": params.task_id }', + }), + { DB: db, API_TOKEN: API_TOKEN }, + ) + const payload = { participant: agentId, subject: taskId } + const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), { + DB: db, + API_TOKEN: API_TOKEN, + }) + const json = await res.json() + expect(json.reactions_fired).toBe(1) + }) +}) + +// ============================================ +// API Key Management (#219) +// ============================================ + +describe('API Key Management', () => { + describe('CRUD', () => { + it('POST /api-keys creates key — returns id + name + key (plaintext)', async () => { + const res = await app.fetch(req('POST', '/api-keys', { name: 'test-key' }), { DB: db, API_TOKEN }) + expect(res.status).toBe(201) + const json = await res.json() + expect(json.id).toBeDefined() + expect(json.name).toBe('test-key') + expect(json.key).toBeDefined() + expect(json.role).toBe('ingest') + expect(json.allowed_events).toEqual([]) + expect(json.rate_limit).toBe(100) + }) + + it('key starts with ogk_', async () => { + const res = await app.fetch(req('POST', '/api-keys', { name: 'test-key' }), { DB: db, API_TOKEN }) + const json = await res.json() + expect(json.key).toMatch(/^ogk_/) + }) + + it('GET /api-keys lists keys without key_hash or plaintext key', async () => { + await app.fetch(req('POST', '/api-keys', { name: 'key-1' }), { DB: db, API_TOKEN }) + await app.fetch(req('POST', '/api-keys', { name: 'key-2' }), { DB: db, API_TOKEN }) + const res = await app.fetch(req('GET', '/api-keys'), { DB: db, API_TOKEN }) + expect(res.status).toBe(200) + const json = await res.json() + expect(json.api_keys).toHaveLength(2) + expect(json.total).toBe(2) + for (const k of json.api_keys) { + expect(k).not.toHaveProperty('key') + expect(k).not.toHaveProperty('key_hash') + expect(k.name).toBeDefined() + expect(k.id).toBeDefined() + } + }) + + it('DELETE /api-keys/:id — key no longer in list', async () => { + const created = await app.fetch(req('POST', '/api-keys', { name: 'to-delete' }), { DB: db, API_TOKEN }) + const { id } = await created.json() + const delRes = await app.fetch(req('DELETE', `/api-keys/${id}`), { DB: db, API_TOKEN }) + expect(delRes.status).toBe(200) + const listRes = await app.fetch(req('GET', '/api-keys'), { DB: db, API_TOKEN }) + const listJson = await listRes.json() + expect(listJson.api_keys).toHaveLength(0) + }) + + it('duplicate key names are allowed', async () => { + await app.fetch(req('POST', '/api-keys', { name: 'dup' }), { DB: db, API_TOKEN }) + const res = await app.fetch(req('POST', '/api-keys', { name: 'dup' }), { DB: db, API_TOKEN }) + expect(res.status).toBe(201) + const listRes = await app.fetch(req('GET', '/api-keys'), { DB: db, API_TOKEN }) + const listJson = await listRes.json() + expect(listJson.api_keys).toHaveLength(2) + }) + }) + + describe('Auth', () => { + let agentId: number + let taskId: number + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN }) + agentId = (await agentRes.json()).id + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN }) + taskId = (await taskRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN }) + }) + + it('POST /events with valid API key and correct event type → 201', async () => { + const keyRes = await app.fetch( + req('POST', '/api-keys', { name: 'ingest-key', role: 'ingest', allowed_events: ['task_assigned'] }), + { DB: db, API_TOKEN }, + ) + const { key } = await keyRes.json() + + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, key), + { DB: db, API_TOKEN }, + ) + expect(res.status).toBe(201) + }) + + it('POST /events with invalid Bearer token → 401', async () => { + const res = await app.fetch( + req( + 'POST', + '/events', + { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, + 'ogk_invalidtoken', + ), + { DB: db, API_TOKEN }, + ) + expect(res.status).toBe(401) + }) + + it('POST /events without Authorization header → 401', async () => { + const request = new Request('http://test/events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + }) + const res = await app.fetch(request, { DB: db, API_TOKEN }) + expect(res.status).toBe(401) + }) + + it('POST /events with deleted key → 401', async () => { + const keyRes = await app.fetch( + req('POST', '/api-keys', { name: 'temp-key', role: 'ingest', allowed_events: ['task_assigned'] }), + { DB: db, API_TOKEN }, + ) + const { id, key } = await keyRes.json() + + await app.fetch(req('DELETE', `/api-keys/${id}`), { DB: db, API_TOKEN }) + + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, key), + { DB: db, API_TOKEN }, + ) + expect(res.status).toBe(401) + }) + + it('POST /events with valid key but wrong event type → 403', async () => { + const keyRes = await app.fetch( + req('POST', '/api-keys', { name: 'limited-key', role: 'ingest', allowed_events: ['other_event'] }), + { DB: db, API_TOKEN }, + ) + const { key } = await keyRes.json() + + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, key), + { DB: db, API_TOKEN }, + ) + expect(res.status).toBe(403) + }) + + it('empty allowed_events means no events allowed for ingest role', async () => { + const keyRes = await app.fetch( + req('POST', '/api-keys', { name: 'empty-key', role: 'ingest', allowed_events: [] }), + { DB: db, API_TOKEN }, + ) + const { key } = await keyRes.json() + + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, key), + { DB: db, API_TOKEN }, + ) + expect(res.status).toBe(403) + }) + + it('existing API_TOKEN still works for POST /events', async () => { + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN }, + ) + expect(res.status).toBe(201) + }) + + it('admin role API key bypasses event type check', async () => { + const keyRes = await app.fetch(req('POST', '/api-keys', { name: 'admin-key', role: 'admin' }), { + DB: db, + API_TOKEN, + }) + const { key } = await keyRes.json() + + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, key), + { DB: db, API_TOKEN }, + ) + expect(res.status).toBe(201) + }) + }) +}) + +// ============================================ +// Request Logs (#221) +// ============================================ + +describe('Request Logs', () => { + let agentId: number + let taskId: number + + function mockExecutionCtx() { + const pending: Promise[] = [] + return { + ctx: { + waitUntil(p: Promise) { + pending.push(p) + }, + passThroughOnException() {}, + }, + flush: () => Promise.all(pending), + } + } + + beforeEach(async () => { + await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN }) + await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN }) + const agentRes = await app.fetch(req('POST', '/objects', { type: 'agent' }), { DB: db, API_TOKEN }) + agentId = (await agentRes.json()).id + const taskRes = await app.fetch(req('POST', '/objects', { type: 'task' }), { DB: db, API_TOKEN }) + taskId = (await taskRes.json()).id + const schema = { + properties: { + participant: { type: 'ref' as const, object_type: 'agent' }, + subject: { type: 'ref' as const, object_type: 'task' }, + }, + } + await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN }) + }) + + it('request log created after POST /events', async () => { + const execCtx = mockExecutionCtx() + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN }, + execCtx.ctx as any, + ) + expect(res.status).toBe(201) + await execCtx.flush() + + const logsRes = await app.fetch(req('GET', '/request-logs'), { DB: db, API_TOKEN }) + const logsJson = await logsRes.json() + expect(logsJson.logs.length).toBeGreaterThanOrEqual(1) + const postLog = logsJson.logs.find((l: any) => l.path === '/events' && l.method === 'POST') + expect(postLog).toBeDefined() + expect(postLog.status_code).toBe(201) + }) + + it('request log includes api_key_name for API key auth', async () => { + const keyRes = await app.fetch( + req('POST', '/api-keys', { name: 'log-test-key', role: 'ingest', allowed_events: ['task_assigned'] }), + { DB: db, API_TOKEN }, + ) + const { key } = await keyRes.json() + + const execCtx = mockExecutionCtx() + const res = await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, key), + { DB: db, API_TOKEN }, + execCtx.ctx as any, + ) + expect(res.status).toBe(201) + await execCtx.flush() + + const logsRes = await app.fetch(req('GET', '/request-logs'), { DB: db, API_TOKEN }) + const logsJson = await logsRes.json() + const postLog = logsJson.logs.find((l: any) => l.path === '/events' && l.method === 'POST') + expect(postLog).toBeDefined() + expect(postLog.api_key_name).toBe('log-test-key') + expect(postLog.api_key_id).toBeDefined() + }) + + it('GET /request-logs supports api_key_id filter', async () => { + const keyRes = await app.fetch( + req('POST', '/api-keys', { name: 'filter-key', role: 'ingest', allowed_events: ['task_assigned'] }), + { DB: db, API_TOKEN }, + ) + const { key, id: keyId } = await keyRes.json() + + const execCtx1 = mockExecutionCtx() + await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }, key), + { DB: db, API_TOKEN }, + execCtx1.ctx as any, + ) + await execCtx1.flush() + + const execCtx2 = mockExecutionCtx() + await app.fetch( + req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }), + { DB: db, API_TOKEN }, + execCtx2.ctx as any, + ) + await execCtx2.flush() + + const filteredRes = await app.fetch(req('GET', `/request-logs?api_key_id=${keyId}`), { DB: db, API_TOKEN }) + const filteredJson = await filteredRes.json() + expect(filteredJson.logs.length).toBe(1) + expect(filteredJson.logs[0].api_key_id).toBe(keyId) + + const allRes = await app.fetch(req('GET', '/request-logs'), { DB: db, API_TOKEN }) + const allJson = await allRes.json() + expect(allJson.logs.length).toBe(2) + }) +}) diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts new file mode 100644 index 0000000..5023fdd --- /dev/null +++ b/packages/engine/src/index.ts @@ -0,0 +1,483 @@ +/** + * OGraph Gateway + Engine (unified Worker) + * + * Route classification: + * - PUBLIC: GET /health — no auth required + * - EXTERNAL: POST /events — API key (Bearer) or API_TOKEN + * - ADMIN: Everything else — API_TOKEN only + * - INTERNAL: Reaction handler execution — engine internal only + */ + +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { bearerAuth } from './auth' +import UI_HTML from './ui.html' +import { + createObjectDef, + listObjectDefs, + createObject, + getObject, + listObjects, + createEventDef, + listEventDefs, + createEvent, + getEvent, + findEventsByRef, + createProjectionDef, + listProjectionDefs, + getProjection, + createReaction, + listReactions, + deleteReaction, + listReactionLogs, + createApiKey, + listApiKeys, + deleteApiKey, + validateApiKey, +} from './engine' +import type { + CreateObjectDefRequest, + CreateObjectRequest, + CreateEventDefRequest, + CreateEventRequest, + CreateProjectionDefRequest, + CreateReactionRequest, + CreateApiKeyRequest, + ReactionPayload, +} from './types' + +type Bindings = { + DB: D1Database + API_TOKEN: string +} + +type Variables = { + apiKeyId: number | null + apiKeyName: string | null +} + +const app = new Hono<{ Bindings: Bindings; Variables: Variables }>() + +app.use('*', cors()) + +app.use('*', async (c, next) => { + const start = Date.now() + await next() + const duration = Date.now() - start + + const path = new URL(c.req.url).pathname + if (path === '/health' || path.startsWith('/ui')) return + + try { + c.executionCtx.waitUntil( + c.env.DB.prepare( + 'INSERT INTO request_logs (method, path, api_key_id, api_key_name, status_code, error, duration_ms, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + ) + .bind( + c.req.method, + path, + c.get('apiKeyId') || null, + c.get('apiKeyName') || null, + c.res.status, + c.res.status >= 400 + ? await c.res + .clone() + .text() + .catch(() => null) + : null, + duration, + Date.now(), + ) + .run(), + ) + } catch { + // executionCtx not available in test, skip + } +}) + +// ============================================ +// UI (no auth, served before auth middleware) +// ============================================ + +app.get('/ui', (c) => { + return c.html(UI_HTML) +}) +app.get('/ui/*', (c) => { + return c.html(UI_HTML) +}) + +// ============================================ +// Health +// ============================================ + +app.get('/health', (c) => { + return c.json({ status: 'ok', version: '2.4.0' }) +}) + +// Auth middleware for all routes except health, ui, and POST /events (which has its own dual auth) +app.use('*', async (c, next) => { + if (c.req.path === '/health' || c.req.path.startsWith('/ui')) return next() + if (c.req.method === 'POST' && c.req.path === '/events') return next() + return bearerAuth(c.env.API_TOKEN)(c, next) +}) + +// ============================================ +// Object Defs +// ============================================ + +app.post('/object-defs', async (c) => { + try { + const body = await c.req.json() + if (!body.name) return c.json({ error: 'Missing name' }, 400) + const result = await createObjectDef(c.env.DB, body.name) + return c.json(result, 201) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +app.get('/object-defs', async (c) => { + const defs = await listObjectDefs(c.env.DB) + return c.json({ object_defs: defs }) +}) + +// ============================================ +// Objects +// ============================================ + +app.post('/objects', async (c) => { + try { + const body = await c.req.json() + if (!body.type) return c.json({ error: 'Missing type' }, 400) + const obj = await createObject(c.env.DB, body.type) + return c.json(obj, 201) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +app.get('/objects/:id', async (c) => { + const id = parseInt(c.req.param('id'), 10) + if (isNaN(id)) return c.json({ error: 'Invalid id' }, 400) + const obj = await getObject(c.env.DB, id) + if (!obj) return c.json({ error: 'Not found' }, 404) + return c.json(obj) +}) + +app.get('/objects', async (c) => { + const type = c.req.query('type') + const limitParam = c.req.query('limit') + const offsetParam = c.req.query('offset') + + const limit = Math.min(parseInt(limitParam || '50', 10), 200) + const offset = parseInt(offsetParam || '0', 10) + + const result = await listObjects(c.env.DB, type, limit, offset) + return c.json(result) +}) + +// ============================================ +// Event Defs +// ============================================ + +app.post('/event-defs', async (c) => { + try { + const body = await c.req.json() + if (!body.name || !body.schema) return c.json({ error: 'Missing name or schema' }, 400) + const result = await createEventDef(c.env.DB, body.name, body.schema) + return c.json(result, 201) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +app.get('/event-defs', async (c) => { + const defs = await listEventDefs(c.env.DB) + return c.json({ event_defs: defs }) +}) + +// ============================================ +// Events +// ============================================ + +app.post('/events', async (c) => { + // Dual auth: check if this request already passed API_TOKEN auth. + // If not (i.e., Bearer token is not the API_TOKEN), validate as API key. + const authHeader = c.req.header('Authorization') + const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null + + if (!bearerToken) { + return c.json({ error: 'Missing or invalid Authorization header' }, 401) + } + + let body: CreateEventRequest + try { + body = await c.req.json() + } catch { + return c.json({ error: 'Invalid JSON body' }, 400) + } + + if (!body.type || !body.payload) return c.json({ error: 'Missing type or payload' }, 400) + + // If the token is not API_TOKEN, treat it as an API key + if (bearerToken !== c.env.API_TOKEN) { + const result = await validateApiKey(c.env.DB, bearerToken, body.type) + if (!result.valid) { + if (result.error === 'event_not_allowed') { + return c.json({ error: 'Event type not allowed for this API key' }, 403) + } + return c.json({ error: 'Invalid API key' }, 401) + } + if (result.apiKey) { + c.set('apiKeyId', result.apiKey.id) + c.set('apiKeyName', result.apiKey.name) + } + } + + try { + const { event, reactions_fired, reaction_results } = await createEvent(c.env.DB, body.type, body.payload) + + // Fire-and-forget webhook POSTs for reactions (if any) + if (reaction_results.length > 0) { + try { + c.executionCtx.waitUntil(fireReactionWebhooks(c.env.DB, reaction_results)) + } catch { + // executionCtx not available in test environment, skip webhook firing + } + } + + return c.json({ event, reactions_fired, reaction_results }, 201) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +app.get('/events/:id', async (c) => { + const id = parseInt(c.req.param('id'), 10) + if (isNaN(id)) return c.json({ error: 'Invalid id' }, 400) + const event = await getEvent(c.env.DB, id) + if (!event) return c.json({ error: 'Not found' }, 404) + return c.json(event) +}) + +app.get('/events', async (c) => { + const refParam = c.req.query('ref') + const refId = refParam ? parseInt(refParam, 10) : undefined + const limitParam = c.req.query('limit') + const offsetParam = c.req.query('offset') + + const limit = Math.min(parseInt(limitParam || '50', 10), 200) + const offset = parseInt(offsetParam || '0', 10) + + const result = await findEventsByRef(c.env.DB, refId, limit, offset) + return c.json(result) +}) + +// ============================================ +// Projection Defs +// ============================================ + +app.post('/projection-defs', async (c) => { + try { + const body = await c.req.json() + if (!body.name || !body.value_schema || body.initial_value === undefined) { + return c.json({ error: 'Missing name, value_schema, or initial_value' }, 400) + } + if (!body.sources || !Array.isArray(body.sources) || body.sources.length === 0) { + return c.json({ error: 'Missing or empty sources array' }, 400) + } + const result = await createProjectionDef( + c.env.DB, + body.name, + body.sources, + body.params, + body.value_schema, + body.initial_value, + ) + return c.json(result, 201) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +app.get('/projection-defs', async (c) => { + const defs = await listProjectionDefs(c.env.DB) + return c.json({ projection_defs: defs }) +}) + +// ============================================ +// Projections +// ============================================ + +app.get('/projections/:name', async (c) => { + try { + const name = c.req.param('name') + const rawParams = c.req.queries() + const params: Record = {} + for (const [key, values] of Object.entries(rawParams)) { + params[key] = values[0] // take first value + } + const value = await getProjection(c.env.DB, name, params) + return c.json({ value }) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +// ============================================ +// Reactions +// ============================================ + +app.post('/reactions', async (c) => { + try { + const body = await c.req.json() + const action = body.action || 'webhook' + if (!body.projection_def || !body.params) { + return c.json({ error: 'Missing projection_def or params' }, 400) + } + if (action === 'webhook' && !body.webhook_url) { + return c.json({ error: 'webhook_url is required when action is webhook' }, 400) + } + if (action === 'emit_event' && !body.emit_event_type) { + return c.json({ error: 'emit_event_type is required when action is emit_event' }, 400) + } + if (action === 'handler' && !body.handler_code) { + return c.json({ error: 'handler_code is required when action is handler' }, 400) + } + const reaction = await createReaction(c.env.DB, body.projection_def, body.params, { + action, + webhook_url: body.webhook_url, + emit_event_type: body.emit_event_type, + emit_payload_template: body.emit_payload_template, + handler_code: body.handler_code, + handler_timeout_ms: body.handler_timeout_ms, + }) + return c.json(reaction, 201) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +app.get('/reactions', async (c) => { + const limitParam = c.req.query('limit') + const offsetParam = c.req.query('offset') + + const limit = Math.min(parseInt(limitParam || '50', 10), 200) + const offset = parseInt(offsetParam || '0', 10) + + const result = await listReactions(c.env.DB, limit, offset) + return c.json(result) +}) + +app.delete('/reactions/:id', async (c) => { + const id = parseInt(c.req.param('id'), 10) + await deleteReaction(c.env.DB, id) + return c.json({ deleted: id }) +}) + +// ============================================ +// Reaction Logs +// ============================================ + +app.get('/reaction-logs', async (c) => { + const limit = parseInt(c.req.query('limit') || '50', 10) + const offset = parseInt(c.req.query('offset') || '0', 10) + const reactionId = c.req.query('reaction_id') ? parseInt(c.req.query('reaction_id')!, 10) : undefined + const result = await listReactionLogs(c.env.DB, limit, offset, reactionId) + return c.json(result) +}) + +// ============================================ +// Request Logs +// ============================================ + +app.get('/request-logs', async (c) => { + const db = c.env.DB + const limit = parseInt(c.req.query('limit') || '50', 10) + const offset = parseInt(c.req.query('offset') || '0', 10) + const apiKeyId = c.req.query('api_key_id') ? parseInt(c.req.query('api_key_id')!, 10) : undefined + + let query = 'SELECT * FROM request_logs' + const binds: any[] = [] + if (apiKeyId !== undefined) { + query += ' WHERE api_key_id = ?' + binds.push(apiKeyId) + } + query += ' ORDER BY id DESC LIMIT ? OFFSET ?' + binds.push(limit, offset) + + let countQuery = 'SELECT COUNT(*) as total FROM request_logs' + if (apiKeyId !== undefined) { + countQuery += ' WHERE api_key_id = ?' + } + + const [rows, countRow] = await Promise.all([ + db + .prepare(query) + .bind(...binds) + .all(), + db + .prepare(countQuery) + .bind(...(apiKeyId !== undefined ? [apiKeyId] : [])) + .first<{ total: number }>(), + ]) + + return c.json({ logs: rows.results, total: countRow?.total || 0 }) +}) + +// ============================================ +// API Keys +// ============================================ + +app.post('/api-keys', async (c) => { + try { + const body = await c.req.json() + if (!body.name) return c.json({ error: 'Missing name' }, 400) + const result = await createApiKey(c.env.DB, body.name, body.role, body.allowed_events, body.rate_limit) + return c.json(result, 201) + } catch (err: any) { + return c.json({ error: err.message || 'Internal error' }, 500) + } +}) + +app.get('/api-keys', async (c) => { + const limitParam = c.req.query('limit') + const offsetParam = c.req.query('offset') + const limit = Math.min(parseInt(limitParam || '50', 10), 200) + const offset = parseInt(offsetParam || '0', 10) + const result = await listApiKeys(c.env.DB, limit, offset) + return c.json(result) +}) + +app.delete('/api-keys/:id', async (c) => { + const id = parseInt(c.req.param('id'), 10) + await deleteApiKey(c.env.DB, id) + return c.json({ deleted: id }) +}) + +// ============================================ +// Helper: Fire Reaction Webhooks +// ============================================ + +async function fireReactionWebhooks(db: D1Database, payloads: ReactionPayload[]): Promise { + for (const payload of payloads) { + try { + // Look up webhook URL for this reaction + const reaction = await db + .prepare('SELECT webhook_url FROM reactions WHERE id = ?') + .bind(payload.reaction_id) + .first<{ webhook_url: string }>() + if (!reaction) continue + + await fetch(reaction.webhook_url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + } catch { + // Ignore webhook errors + } + } +} + +export default app diff --git a/packages/engine/src/reaction-executor.ts b/packages/engine/src/reaction-executor.ts new file mode 100644 index 0000000..2f1f398 --- /dev/null +++ b/packages/engine/src/reaction-executor.ts @@ -0,0 +1,25 @@ +import type { ReactionPayload } from './types' + +/** + * Async reaction executor — fire-and-forget HTTP POST to worker_url + */ +export async function executeReaction(payload: ReactionPayload): Promise { + try { + await fetch(payload.worker_url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + reaction_id: payload.reaction_id, + reducer: payload.reducer, + params: payload.params, + old_value: payload.old_value, + new_value: payload.new_value, + evt_oid: payload.evt_oid, + config: payload.config, + }), + }) + } catch { + // Fire and forget — log but don't throw + console.error(`Reaction ${payload.reaction_id} failed: ${payload.worker_url}`) + } +} diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts new file mode 100644 index 0000000..d80e07c --- /dev/null +++ b/packages/engine/src/types.ts @@ -0,0 +1,240 @@ +// OGraph v2.4 Types +// object_defs 回归单表(去掉 versions + names) +// version 表加 parent_hash(版本链)和 name(反查) +// projection_def 加 value_schema NOT NULL + initial_value NOT NULL +// objects.type 直接存名字(不存 hash) + +// ============================================ +// Definition Layer +// ============================================ + +export interface PropertyDef { + type: 'ref' | 'string' | 'number' | 'boolean' + object_type?: string | string[] // for ref type: polymorphic support +} + +// Event & Projection Definition Versions (immutable, with version chain) +export interface EventDefVersion { + hash: string + name: string // 属于哪个定义(反查) + parent_hash: string | null // 前一版本,首版 NULL + schema: { + properties: Record + } + created_at: number +} + +export interface ProjectionDefSource { + event_def_hash: string + bindings: Record + expression: string +} + +export interface ProjectionDefVersion { + hash: string + name: string + parent_hash: string | null + params: Record + sources: ProjectionDefSource[] + value_schema: { type: string } // e.g., { type: "number" } or { type: "ref" } + initial_value: any // NOT NULL + created_at: number +} + +// Name pointers (mutable) +export interface EventDefName { + name: string + current_hash: string + updated_at: number +} + +export interface ProjectionDefName { + name: string + current_hash: string + updated_at: number +} + +// Combined view (for API responses) +export interface ObjectDef { + name: string +} + +export interface EventDef { + name: string + hash: string + parent_hash: string | null + schema: { + properties: Record + } +} + +export interface ProjectionDef { + name: string + hash: string + parent_hash: string | null + params: Record + sources: ProjectionDefSource[] + value_schema: { type: string } + initial_value: any +} + +// ============================================ +// Instance Layer +// ============================================ + +export interface Object { + id: number + type: string // 直接存名字(不存 hash) + created_at: number +} + +export interface Event { + id: number + type_hash: string + payload: Record + created_at: number +} + +export interface EventRef { + event_id: number + property: string + ref_id: number +} + +export interface Projection { + def_hash: string + params_hash: string + params: Record + value: any // NOT NULL + last_event_id: number + created_at: number +} + +export interface Reaction { + id: number + projection_def_hash: string + params_hash: string + params: Record + action: 'webhook' | 'emit_event' | 'handler' + webhook_url?: string + emit_event_type?: string + emit_payload_template?: string // JSONata: (old_value, new_value, params, event) → payload + handler_code?: string + handler_timeout_ms?: number + created_at: number +} + +// ============================================ +// Runtime Context +// ============================================ + +export interface EventContext { + id: number + type: string + timestamp: number + [key: string]: any +} + +export interface ReactionPayload { + reaction_id: number + projection_def: string + params: Record + old_value: any + new_value: any + event: EventContext + timestamp: number + log_id?: number +} + +export interface ReactionLog { + id: number + reaction_id: number + trigger_event_id: number + projection_def: string + old_value: any + new_value: any + action: string + status: 'success' | 'failed' | 'skipped' + handler_output?: string + duration_ms?: number + created_at: number +} + +// ============================================ +// API Key Types +// ============================================ + +export interface ApiKey { + id: number + name: string + role: 'admin' | 'ingest' | 'readonly' + allowed_events: string[] + rate_limit: number + last_used_at?: number + created_at: number +} + +export interface CreateApiKeyRequest { + name: string + role?: 'admin' | 'ingest' | 'readonly' + allowed_events?: string[] + rate_limit?: number +} + +export interface CreateApiKeyResponse extends ApiKey { + key: string +} + +// ============================================ +// API Types +// ============================================ + +export interface CreateObjectDefRequest { + name: string +} + +export interface CreateObjectRequest { + type: string // name(不需要解析 hash) +} + +export interface CreateEventDefRequest { + name: string + schema: { + properties: Record + } +} + +export interface CreateEventRequest { + type: string // name (will be resolved to hash) + payload: Record +} + +export interface CreateEventResponse { + event: Event + reactions_fired?: number + reaction_results?: ReactionPayload[] +} + +export interface CreateProjectionDefRequest { + name: string + sources: Array<{ event_def: string; bindings: Record; expression: string }> + params: Record + value_schema: { type: string } + initial_value: any // NOT NULL +} + +export interface GetProjectionRequest { + name: string // will be resolved to hash + params: Record +} + +export interface CreateReactionRequest { + projection_def: string // name (will be resolved to hash) + params: Record + action?: 'webhook' | 'emit_event' | 'handler' // default: 'webhook' + webhook_url?: string + emit_event_type?: string + emit_payload_template?: string // JSONata expression + handler_code?: string + handler_timeout_ms?: number // default 5000 +} diff --git a/packages/engine/src/ui.html b/packages/engine/src/ui.html new file mode 100644 index 0000000..c9655d9 --- /dev/null +++ b/packages/engine/src/ui.html @@ -0,0 +1,73 @@ + + + + + + OGraph UI + + + + +
+ + diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json new file mode 100644 index 0000000..54aae4e --- /dev/null +++ b/packages/engine/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["@cloudflare/workers-types/2023-07-01"] + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts new file mode 100644 index 0000000..726bcbc --- /dev/null +++ b/packages/engine/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, + plugins: [ + { + name: 'html-raw', + enforce: 'pre', + load(id: string) { + if (id.endsWith('.html')) { + return `export default ""` + } + }, + }, + ], +}) diff --git a/packages/engine/wrangler.toml b/packages/engine/wrangler.toml new file mode 100644 index 0000000..232db43 --- /dev/null +++ b/packages/engine/wrangler.toml @@ -0,0 +1,15 @@ +name = "ograph" +main = "src/index.ts" +compatibility_date = "2026-04-03" + +rules = [ + { type = "Text", globs = ["**/*.html"] } +] + +[vars] +VERSION = "2.4.0" + +[[d1_databases]] +binding = "DB" +database_name = "ograph" +database_id = "69bd3763-e049-4460-bbde-1eb5954b9dbf" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c476ec8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "lib": ["ES2022"] + }, + "exclude": ["node_modules", "dist", ".wrangler"] +}