diff --git a/AGENTS.md b/AGENTS.md index 88ef924..07095a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,16 +52,6 @@ slack-bridge/ bridge.mjs Slack ↔ agent bridge (direct mode β€” Socket Mode) security.mjs πŸ”’ content wrapping, rate limiting, auth security.test.mjs πŸ”’ tests for security module -slack-broker/ - src/ Cloudflare Worker source (broker mode β€” Events API) - index.ts Worker entry point + request router - slack/ Slack signature verification, OAuth, API helpers - crypto/ Sealed boxes, authenticated encryption, signatures - routing/ KV-backed workspace registry + event forwarding - api/ Server registration + outbound message handling - test/ Vitest test suites (crypto, routing, integration) - wrangler.toml Cloudflare Workers config (KV namespaces, secrets) - package.json Dependencies: tweetnacl, vitest setup.sh one-time system setup (creates user, firewall, etc.) start.sh agent launcher (deployed to ~/runtime/start.sh) ``` diff --git a/docs/architecture.md b/docs/architecture.md index ebe2754..7232248 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,8 +10,7 @@ admin user β”‚ β”œβ”€β”€ bin/ β”‚ β”œβ”€β”€ pi/extensions/ β”‚ β”œβ”€β”€ pi/skills/ -β”‚ β”œβ”€β”€ slack-bridge/ # direct Slack integration (Socket Mode) -β”‚ └── slack-broker/ # shared Slack broker (Cloudflare Worker) +β”‚ └── slack-bridge/ # direct Slack integration (Socket Mode) root-managed releases β”œβ”€β”€ /opt/baudbot/ diff --git a/slack-broker/README.md b/slack-broker/README.md deleted file mode 100644 index 694240b..0000000 --- a/slack-broker/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# Slack Broker - -A Cloudflare Worker that routes messages between Slack workspaces and individual baudbot servers. All messages are end-to-end encrypted β€” the broker cannot read inbound messages and only decrypts outbound messages transiently to post them to Slack. - -## Architecture - -``` -Slack Workspace ──Events API──► Cloudflare Worker ──sealed box──► Baudbot Server - (Slack Broker) -Slack Workspace ◄──Slack API──── Cloudflare Worker ◄──crypto_box── Baudbot Server -``` - -**Inbound (Slack β†’ Server):** Slack sends events to the broker in plaintext (unavoidable Slack constraint). The broker encrypts the payload using `crypto_box_seal` (sealed box) with the server's public key, then forwards it. The broker **cannot decrypt** sealed boxes β€” only the server's private key can. - -**Outbound (Server β†’ Slack):** The server encrypts the message body with `crypto_box` (authenticated encryption) using the broker's public key. Routing metadata (channel, thread_ts) stays in cleartext so the broker can route without decrypting. The broker decrypts the body, posts to Slack, and immediately zeroes the plaintext from memory. - -## Setup - -### Prerequisites - -- [Node.js](https://nodejs.org/) 18+ -- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) (`npm i -g wrangler`) -- A Cloudflare account - -### 1. Create KV Namespaces - -```bash -wrangler kv namespace create WORKSPACE_ROUTING -wrangler kv namespace create OAUTH_STATE -``` - -Update the namespace IDs in `wrangler.toml`. - -### 2. Set Secrets - -```bash -# Generate a 32-byte seed for the broker's keypair -openssl rand -base64 32 | wrangler secret put BROKER_PRIVATE_KEY - -# From your Slack app configuration -wrangler secret put SLACK_CLIENT_ID -wrangler secret put SLACK_CLIENT_SECRET -wrangler secret put SLACK_SIGNING_SECRET -``` - -### 3. Deploy - -```bash -cd slack-broker -npm install -npm run deploy -``` - -### 4. Configure Slack App - -Point your Slack app's Event Subscriptions request URL to: -``` -https://.workers.dev/slack/events -``` - -Set the OAuth redirect URL to: -``` -https://.workers.dev/slack/oauth/callback -``` - -## API Reference - -### Public Endpoints - -| Method | Path | Description | -|--------|------|-------------| -| `GET` | `/health` | Health check | -| `GET` | `/api/broker-pubkey` | Get broker's public keys | -| `POST` | `/slack/events` | Slack Events API webhook | -| `GET` | `/slack/oauth/install` | Start OAuth install flow | -| `GET` | `/slack/oauth/callback` | Handle OAuth callback | - -### Server-to-Broker (authenticated) - -| Method | Path | Description | -|--------|------|-------------| -| `POST` | `/api/register` | Register a baudbot server for a workspace | -| `DELETE` | `/api/register` | Unlink a server (requires signature) | -| `POST` | `/api/send` | Send an encrypted message to Slack | - -### Registration Flow - -1. User visits `/slack/oauth/install` β†’ redirected to Slack β†’ authorizes app -2. Callback stores workspace with "pending" status, returns an auth code -3. Server calls `POST /api/register` with the auth code to link itself -4. Workspace status becomes "active" β€” events start flowing - -### Outbound Message Format - -```json -{ - "workspace_id": "T09192W1Z34", - "action": "chat.postMessage", - "routing": { - "channel": "C0A2G6TSDL6", - "thread_ts": "1771464783.614839" - }, - "encrypted_body": "", - "nonce": "", - "timestamp": 1771465000, - "signature": "" -} -``` - -Supported actions: `chat.postMessage`, `reactions.add`, `chat.update`. - -## Encryption Details - -| Primitive | Use | Library | -|-----------|-----|---------| -| `crypto_box_seal` (X25519 + XSalsa20-Poly1305) | Inbound: Slack β†’ server | libsodium-wrappers-sumo | -| `crypto_box` (X25519 + XSalsa20-Poly1305) | Outbound: server β†’ Slack | tweetnacl | -| Ed25519 | Envelope signatures | tweetnacl | -| HMAC-SHA256 | Slack request verification | Web Crypto API | - -### Key Types - -- **X25519 keypair** β€” encryption/decryption (sealed boxes + authenticated encryption) -- **Ed25519 keypair** β€” signing/verification (envelope authentication) -- Both derived from the same 32-byte seed (`BROKER_PRIVATE_KEY`) - -## Security Properties - -- βœ… Inbound messages encrypted in transit (sealed box, only server can decrypt) -- βœ… Outbound message body encrypted in transit (authenticated, broker decrypts transiently) -- βœ… Broker cannot persist message content (by design) -- βœ… Broker cannot read inbound messages (sealed box β€” no broker private key involved) -- βœ… Server authenticates broker (broker signs envelopes) -- βœ… Broker authenticates server (server signs outbound requests) -- βœ… Replay protection (timestamps + nonces on all messages) -- βœ… Auth code verification for server registration (one-time use, HMAC-SHA256) -- βœ… Bot tokens encrypted at rest in KV (nacl.secretbox) -- ❌ Perfect forward secrecy (would need session keys β€” future enhancement) -- ❌ Rate limiting (Phase 3 β€” pre-production requirement) - -### What the Broker Can See - -- Routing metadata: workspace_id, channel, thread_ts, timestamps -- Outbound message content: **transiently** (decrypted in memory to post to Slack, then best-effort zeroed β€” JS strings from JSON.parse cannot be deterministically zeroed, only the underlying Uint8Array buffer is cleared) - -### What the Broker Cannot See - -- Inbound message content (sealed box encryption) -- Historical messages (nothing is stored) - -## Development - -```bash -npm install -npm test # Run tests -npm run dev # Start local dev server -npm run typecheck # TypeScript type checking -``` - -## Testing - -54 tests across 3 suites: - -- **crypto.test.ts** β€” Sealed boxes, authenticated encryption, signatures, encoding -- **routing.test.ts** β€” KV registry CRUD, auth code hashing, event forwarding -- **integration.test.ts** β€” End-to-end flows, Slack signature verification, replay protection diff --git a/slack-broker/package-lock.json b/slack-broker/package-lock.json deleted file mode 100644 index d74baf3..0000000 --- a/slack-broker/package-lock.json +++ /dev/null @@ -1,3023 +0,0 @@ -{ - "name": "slack-broker", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "slack-broker", - "version": "0.1.0", - "dependencies": { - "libsodium-wrappers-sumo": "^0.8.2", - "tweetnacl": "^1.0.3" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20241230.0", - "typescript": "^5.7.0", - "vitest": "^2.1.0", - "wrangler": "^3.99.0" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", - "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "mime": "^3.0.0" - }, - "engines": { - "node": ">=16.13" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", - "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", - "dev": true, - "license": "MIT OR Apache-2.0", - "peerDependencies": { - "unenv": "2.0.0-rc.14", - "workerd": "^1.20250124.0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", - "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", - "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", - "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", - "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", - "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20260219.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260219.0.tgz", - "integrity": "sha512-jL2BNnDqbKXDrxhtKx+wVmQpv/P6w8J4WVFiuT9OMEPsw8V2TfTozoWTcCZ2AhE09yK406xQFE4mBq9IIgobuw==", - "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/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild-plugins/node-globals-polyfill": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", - "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", - "dev": true, - "license": "ISC", - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild-plugins/node-modules-polyfill": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", - "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", - "dev": true, - "license": "ISC", - "dependencies": { - "escape-string-regexp": "^4.0.0", - "rollup-plugin-node-polyfills": "^0.2.1" - }, - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "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.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "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.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "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.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "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.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "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.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "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-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "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.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "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.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "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.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "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.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "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.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "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.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "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.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "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.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "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.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "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.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "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.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "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.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "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/@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/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "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/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/as-table": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "printable-characters": "^1.0.42" - } - }, - "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/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/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/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "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", - "optional": true, - "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", - "optional": true - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "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", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "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/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/exit-hook": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", - "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, - "license": "MIT" - }, - "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-source": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "data-uri-to-buffer": "^2.0.0", - "source-map": "^0.6.1" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/libsodium-sumo": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.8.2.tgz", - "integrity": "sha512-uMgnjphJ717jLN+jFG1HUgNrK/gOVVfaO1DGZ1Ig/fKLKLVhvaH/sM1I1v784JFvmkJDaczDpi7xSYC4Jvdo1Q==", - "license": "ISC" - }, - "node_modules/libsodium-wrappers-sumo": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.8.2.tgz", - "integrity": "sha512-wd1xAY++Kr6VMikSaa4EPRAHJmFvNlGWiiwU3Jh3GR1zRYF3/I3vy/wYsr4k3LVsNzwb9sqfEQ4LdVQ6zEebyQ==", - "license": "ISC", - "dependencies": { - "libsodium-sumo": "^0.8.0" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "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/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/miniflare": { - "version": "3.20250718.3", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", - "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "acorn": "8.14.0", - "acorn-walk": "8.3.2", - "exit-hook": "2.2.1", - "glob-to-regexp": "0.4.1", - "stoppable": "1.1.0", - "undici": "^5.28.5", - "workerd": "1.20250718.0", - "ws": "8.18.0", - "youch": "3.3.4", - "zod": "3.22.3" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=16.13" - } - }, - "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/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "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/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, - "license": "MIT" - }, - "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": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "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/printable-characters": { - "version": "1.0.42", - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", - "dev": true, - "license": "Unlicense" - }, - "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "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.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-inject": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", - "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-walker": "^0.6.1", - "magic-string": "^0.25.3", - "rollup-pluginutils": "^2.8.1" - } - }, - "node_modules/rollup-plugin-inject/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/rollup-plugin-inject/node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/rollup-plugin-node-polyfills": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", - "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rollup-plugin-inject": "^3.0.0" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/rollup-pluginutils/node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true, - "license": "MIT" - }, - "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", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "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/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "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/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true, - "license": "MIT" - }, - "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/stacktracey": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", - "dev": true, - "license": "Unlicense", - "dependencies": { - "as-table": "^1.0.36", - "get-source": "^2.0.12" - } - }, - "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/stoppable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4", - "npm": ">=6" - } - }, - "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": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "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/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/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": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/unenv": { - "version": "2.0.0-rc.14", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", - "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "exsolve": "^1.0.1", - "ohash": "^2.0.10", - "pathe": "^2.0.3", - "ufo": "^1.5.4" - } - }, - "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/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": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "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": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "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": "2.1.9", - "@vitest/ui": "2.1.9", - "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/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/workerd": { - "version": "1.20250718.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", - "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20250718.0", - "@cloudflare/workerd-darwin-arm64": "1.20250718.0", - "@cloudflare/workerd-linux-64": "1.20250718.0", - "@cloudflare/workerd-linux-arm64": "1.20250718.0", - "@cloudflare/workerd-windows-64": "1.20250718.0" - } - }, - "node_modules/wrangler": { - "version": "3.114.17", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", - "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", - "dev": true, - "license": "MIT OR Apache-2.0", - "dependencies": { - "@cloudflare/kv-asset-handler": "0.3.4", - "@cloudflare/unenv-preset": "2.0.2", - "@esbuild-plugins/node-globals-polyfill": "0.2.3", - "@esbuild-plugins/node-modules-polyfill": "0.2.2", - "blake3-wasm": "2.1.5", - "esbuild": "0.17.19", - "miniflare": "3.20250718.3", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.14", - "workerd": "1.20250718.0" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=16.17.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2", - "sharp": "^0.33.5" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20250408.0" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", - "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", - "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", - "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", - "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", - "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", - "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", - "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", - "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", - "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", - "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", - "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", - "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", - "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", - "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", - "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", - "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", - "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", - "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", - "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", - "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", - "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/wrangler/node_modules/esbuild": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", - "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" - } - }, - "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/youch": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cookie": "^0.7.1", - "mustache": "^4.2.0", - "stacktracey": "^2.1.8" - } - }, - "node_modules/zod": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", - "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/slack-broker/package.json b/slack-broker/package.json deleted file mode 100644 index b41fe0a..0000000 --- a/slack-broker/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "slack-broker", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy", - "test": "vitest run", - "test:watch": "vitest", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "libsodium-wrappers-sumo": "^0.8.2", - "tweetnacl": "^1.0.3" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20241230.0", - "typescript": "^5.7.0", - "vitest": "^2.1.0", - "wrangler": "^3.99.0" - } -} diff --git a/slack-broker/src/api/register.ts b/slack-broker/src/api/register.ts deleted file mode 100644 index d0cbe2f..0000000 --- a/slack-broker/src/api/register.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Server registration endpoint. - * - * A baudbot server calls POST /api/register to link itself to a workspace. - * The request must include the auth_code from the OAuth flow to prove - * workspace ownership. - * - * Flow: - * 1. Server sends: workspace_id, server_pubkey, server_signing_pubkey, - * server_callback_url, auth_code - * 2. Broker verifies auth_code against stored hash - * 3. Broker activates the workspace with server details - * 4. Broker returns its own public keys - * - * DELETE /api/register unlinks a server (requires server signature). - */ - -import { - getWorkspace, - activateWorkspace, - deactivateWorkspace, - hashAuthCode, -} from "../routing/registry.js"; -import { verify, canonicalizeOutbound } from "../crypto/verify.js"; -import { decodeBase64, encodeBase64 } from "../util/encoding.js"; -import type { Env } from "../index.js"; - -interface RegisterRequest { - workspace_id: string; - server_pubkey: string; - server_signing_pubkey: string; - server_callback_url: string; - auth_code: string; -} - -interface RegisterResponse { - ok: boolean; - broker_pubkey?: string; - broker_signing_pubkey?: string; - error?: string; -} - -/** - * Handle server registration. - */ -export async function handleRegister( - request: Request, - env: Env, -): Promise { - if (request.method === "DELETE") { - return handleUnregister(request, env); - } - - if (request.method !== "POST") { - return jsonResponse({ ok: false, error: "method not allowed" }, 405); - } - - let body: RegisterRequest; - try { - body = (await request.json()) as RegisterRequest; - } catch { - return jsonResponse({ ok: false, error: "invalid JSON" }, 400); - } - - // Validate required fields - if ( - !body.workspace_id || - !body.server_pubkey || - !body.server_signing_pubkey || - !body.server_callback_url || - !body.auth_code - ) { - return jsonResponse({ ok: false, error: "missing required fields" }, 400); - } - - // Validate workspace_id matches Slack team ID format to prevent - // pipe-delimiter injection in canonicalized signatures. - if (!/^T[A-Z0-9]+$/.test(body.workspace_id)) { - return jsonResponse({ ok: false, error: "invalid workspace_id format" }, 400); - } - - // Validate callback URL - try { - const url = new URL(body.server_callback_url); - if (url.protocol !== "https:") { - return jsonResponse({ ok: false, error: "callback URL must use HTTPS" }, 400); - } - } catch { - return jsonResponse({ ok: false, error: "invalid callback URL" }, 400); - } - - // Look up workspace - const workspace = await getWorkspace(env.WORKSPACE_ROUTING, body.workspace_id); - if (!workspace) { - return jsonResponse({ ok: false, error: "workspace not found β€” complete OAuth install first" }, 404); - } - - // Reject re-registration of already-active workspaces. - // The current server must unregister first (DELETE /api/register). - if (workspace.status === "active") { - return jsonResponse({ ok: false, error: "workspace already active β€” unregister the current server first" }, 409); - } - - // Verify auth code (must not be empty β€” cleared after first successful registration) - if (!workspace.auth_code_hash) { - return jsonResponse({ ok: false, error: "auth code already consumed β€” re-install the Slack app to generate a new one" }, 403); - } - - const providedHash = await hashAuthCode(body.auth_code, env.BROKER_PRIVATE_KEY); - if (providedHash !== workspace.auth_code_hash) { - return jsonResponse({ ok: false, error: "invalid auth code" }, 403); - } - - // Activate workspace - const activated = await activateWorkspace( - env.WORKSPACE_ROUTING, - body.workspace_id, - body.server_callback_url, - body.server_pubkey, - body.server_signing_pubkey, - ); - - if (!activated) { - return jsonResponse({ ok: false, error: "failed to activate workspace" }, 500); - } - - // Return broker's public keys - const response: RegisterResponse = { - ok: true, - broker_pubkey: env.BROKER_PUBLIC_KEY, - broker_signing_pubkey: env.BROKER_SIGNING_PUBLIC_KEY, - }; - - return jsonResponse(response, 200); -} - -/** - * Handle server unregistration (DELETE /api/register). - * - * Requires the server to sign the request to prove identity. - */ -async function handleUnregister( - request: Request, - env: Env, -): Promise { - let body: { workspace_id: string; timestamp: number; signature: string }; - try { - body = (await request.json()) as typeof body; - } catch { - return jsonResponse({ ok: false, error: "invalid JSON" }, 400); - } - - if (!body.workspace_id || !body.timestamp || !body.signature) { - return jsonResponse({ ok: false, error: "missing required fields" }, 400); - } - - // Replay protection - const now = Math.floor(Date.now() / 1000); - if (Math.abs(now - body.timestamp) > 300) { - return jsonResponse({ ok: false, error: "timestamp too old" }, 400); - } - - // Look up workspace to get server's signing key - const workspace = await getWorkspace(env.WORKSPACE_ROUTING, body.workspace_id); - if (!workspace || workspace.status !== "active") { - return jsonResponse({ ok: false, error: "workspace not active" }, 404); - } - - // Verify server's signature - const canonical = canonicalizeOutbound( - body.workspace_id, - "unregister", - body.timestamp, - "", - ); - const serverSigningPubkey = decodeBase64(workspace.server_signing_pubkey); - const valid = verify(canonical, body.signature, serverSigningPubkey); - - if (!valid) { - return jsonResponse({ ok: false, error: "invalid signature" }, 403); - } - - await deactivateWorkspace(env.WORKSPACE_ROUTING, body.workspace_id); - return jsonResponse({ ok: true }, 200); -} - -function jsonResponse(data: unknown, status: number): Response { - return new Response(JSON.stringify(data), { - status, - headers: { "Content-Type": "application/json" }, - }); -} diff --git a/slack-broker/src/api/send.ts b/slack-broker/src/api/send.ts deleted file mode 100644 index ab9db6e..0000000 --- a/slack-broker/src/api/send.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Outbound message endpoint β€” POST /api/send - * - * Receives an encrypted message from a baudbot server, decrypts the body, - * posts it to Slack, then zeroes the plaintext from memory. - * - * Request format (structured encryption β€” Option A from spec): - * { - * "workspace_id": "T09192W1Z34", - * "action": "chat.postMessage" | "reactions.add" | "chat.update", - * "routing": { - * "channel": "C0A2G6TSDL6", - * "thread_ts": "1771464783.614839" // optional - * }, - * "encrypted_body": "", - * "nonce": "", - * "timestamp": 1771465000, - * "signature": "" - * } - * - * The broker: - * 1. Verifies the server's signature - * 2. Checks timestamp for replay protection - * 3. Decrypts the body using crypto_box_open - * 4. Posts to Slack using the appropriate API method - * 5. Zeroes plaintext from memory - */ - -import { boxDecrypt, zeroBytes } from "../crypto/box.js"; -import { verify, canonicalizeOutbound } from "../crypto/verify.js"; -import { decodeBase64, decodeUTF8 } from "../util/encoding.js"; -import { getWorkspace, decryptBotToken } from "../routing/registry.js"; -import { postMessage, addReaction, updateMessage } from "../slack/api.js"; -import type { Env } from "../index.js"; - -const FIVE_MINUTES = 5 * 60; -const VALID_ACTIONS = ["chat.postMessage", "reactions.add", "chat.update"] as const; -type SlackAction = (typeof VALID_ACTIONS)[number]; - -interface SendRequest { - workspace_id: string; - action: string; - routing: { - channel: string; - thread_ts?: string; - timestamp?: string; // for reactions.add and chat.update - emoji?: string; // for reactions.add - }; - encrypted_body: string; - nonce: string; - timestamp: number; - signature: string; -} - -/** - * Handle an outbound send request from a baudbot server. - */ -export async function handleSend( - request: Request, - env: Env, -): Promise { - if (request.method !== "POST") { - return jsonResponse({ ok: false, error: "method not allowed" }, 405); - } - - let body: SendRequest; - try { - body = (await request.json()) as SendRequest; - } catch { - return jsonResponse({ ok: false, error: "invalid JSON" }, 400); - } - - // Validate required fields - if ( - !body.workspace_id || - !body.action || - !body.routing?.channel || - !body.encrypted_body || - !body.nonce || - !body.timestamp || - !body.signature - ) { - return jsonResponse({ ok: false, error: "missing required fields" }, 400); - } - - // Validate action - if (!VALID_ACTIONS.includes(body.action as SlackAction)) { - return jsonResponse({ ok: false, error: `invalid action: ${body.action}` }, 400); - } - - // Replay protection - const now = Math.floor(Date.now() / 1000); - if (Math.abs(now - body.timestamp) > FIVE_MINUTES) { - return jsonResponse({ ok: false, error: "timestamp too old or too far in future" }, 400); - } - - // Look up workspace - const workspace = await getWorkspace(env.WORKSPACE_ROUTING, body.workspace_id); - if (!workspace || workspace.status !== "active") { - return jsonResponse({ ok: false, error: "workspace not active" }, 404); - } - - // Verify server's signature on the request - const canonical = canonicalizeOutbound( - body.workspace_id, - body.action, - body.timestamp, - body.encrypted_body, - ); - const serverSigningPubkey = decodeBase64(workspace.server_signing_pubkey); - const validSig = verify(canonical, body.signature, serverSigningPubkey); - - if (!validSig) { - return jsonResponse({ ok: false, error: "invalid signature" }, 403); - } - - // Decrypt the message body - let decryptedBytes: Uint8Array; - try { - const senderPubkey = decodeBase64(workspace.server_pubkey); - decryptedBytes = boxDecrypt( - body.encrypted_body, - body.nonce, - senderPubkey, - env.BROKER_PRIVATE_KEY_BYTES, - ); - } catch (err) { - const message = err instanceof Error ? err.message : "decryption failed"; - return jsonResponse({ ok: false, error: message }, 400); - } - - // Parse decrypted body - let decryptedBody: Record; - try { - decryptedBody = JSON.parse(decodeUTF8(decryptedBytes)) as Record; - } catch { - zeroBytes(decryptedBytes); - return jsonResponse({ ok: false, error: "decrypted body is not valid JSON" }, 400); - } - - // Execute the Slack API call - try { - // Decrypt the bot token (encrypted at rest in KV) - const botToken = decryptBotToken(workspace.bot_token, env.BROKER_PRIVATE_KEY); - - const result = await executeSlackAction( - body.action as SlackAction, - botToken, - body.routing, - decryptedBody, - ); - - // Zero the plaintext immediately after posting - zeroBytes(decryptedBytes); - - if (!result.ok) { - return jsonResponse({ ok: false, error: result.error ?? "slack api error" }, 502); - } - - return jsonResponse({ ok: true, ts: result.ts }, 200); - } catch (err) { - // Zero plaintext even on error - zeroBytes(decryptedBytes); - const message = err instanceof Error ? err.message : "unknown error"; - return jsonResponse({ ok: false, error: message }, 500); - } -} - -/** - * Dispatch to the appropriate Slack API method. - */ -async function executeSlackAction( - action: SlackAction, - botToken: string, - routing: SendRequest["routing"], - decryptedBody: Record, -): Promise<{ ok: boolean; ts?: string; error?: string }> { - switch (action) { - case "chat.postMessage": - return postMessage( - botToken, - routing.channel, - (decryptedBody.text as string) ?? "", - { - thread_ts: routing.thread_ts, - blocks: decryptedBody.blocks as unknown[] | undefined, - }, - ); - - case "reactions.add": - return addReaction( - botToken, - routing.channel, - routing.timestamp ?? "", - routing.emoji ?? (decryptedBody.emoji as string) ?? "", - ); - - case "chat.update": - return updateMessage( - botToken, - routing.channel, - routing.timestamp ?? "", - (decryptedBody.text as string) ?? "", - { - blocks: decryptedBody.blocks as unknown[] | undefined, - }, - ); - - default: - return { ok: false, error: `unsupported action: ${action}` }; - } -} - -function jsonResponse(data: unknown, status: number): Response { - return new Response(JSON.stringify(data), { - status, - headers: { "Content-Type": "application/json" }, - }); -} diff --git a/slack-broker/src/crypto/box.ts b/slack-broker/src/crypto/box.ts deleted file mode 100644 index 13b6db6..0000000 --- a/slack-broker/src/crypto/box.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Authenticated encryption β€” used for outbound (server β†’ Slack) path. - * - * The server encrypts the message body with crypto_box (server_sk, broker_pk). - * The broker decrypts transiently to post to Slack, then zeroes the plaintext. - * - * crypto_box provides: - * - Confidentiality (XSalsa20) - * - Integrity (Poly1305 MAC) - * - Authentication (sender identity verified via shared secret) - */ - -import nacl from "tweetnacl"; -import { decodeBase64, encodeBase64 } from "../util/encoding.js"; - -/** - * Encrypt with crypto_box (authenticated encryption). - * - * Used by the SERVER to encrypt outbound message bodies. - * Included here for testing and symmetry. - * - * @returns { ciphertext: base64, nonce: base64 } - */ -export function boxEncrypt( - plaintext: Uint8Array, - recipientPublicKey: Uint8Array, - senderSecretKey: Uint8Array, -): { ciphertext: string; nonce: string } { - const nonce = nacl.randomBytes(nacl.box.nonceLength); - const ciphertext = nacl.box(plaintext, nonce, recipientPublicKey, senderSecretKey); - - if (!ciphertext) { - throw new Error("boxEncrypt: encryption failed"); - } - - return { - ciphertext: encodeBase64(ciphertext), - nonce: encodeBase64(nonce), - }; -} - -/** - * Decrypt with crypto_box_open (authenticated decryption). - * - * Used by the BROKER to decrypt outbound message bodies from servers. - * The broker decrypts, posts to Slack, then callers must zero the result. - * - * @param ciphertextBase64 - base64-encoded ciphertext - * @param nonceBase64 - base64-encoded nonce - * @param senderPublicKey - the server's public key (authenticates sender) - * @param recipientSecretKey - the broker's secret key - * @returns decrypted plaintext bytes - */ -export function boxDecrypt( - ciphertextBase64: string, - nonceBase64: string, - senderPublicKey: Uint8Array, - recipientSecretKey: Uint8Array, -): Uint8Array { - const ciphertext = decodeBase64(ciphertextBase64); - const nonce = decodeBase64(nonceBase64); - - if (nonce.length !== nacl.box.nonceLength) { - throw new Error(`boxDecrypt: invalid nonce length (expected ${nacl.box.nonceLength}, got ${nonce.length})`); - } - - const plaintext = nacl.box.open(ciphertext, nonce, senderPublicKey, recipientSecretKey); - - if (!plaintext) { - throw new Error("boxDecrypt: decryption failed β€” invalid key, corrupted data, or wrong sender"); - } - - return plaintext; -} - -/** - * Best-effort memory cleanup β€” zeroes a Uint8Array to reduce plaintext - * residence in memory. Call this after posting the decrypted content to Slack. - * - * NOTE: This only zeroes the Uint8Array buffer. JS strings derived from the - * buffer (e.g. via TextDecoder or JSON.parse) are immutable and cannot be - * deterministically zeroed β€” they remain in memory until garbage collected. - * This is a limitation of the JS runtime, not a bug. - */ -export function zeroBytes(arr: Uint8Array): void { - arr.fill(0); -} diff --git a/slack-broker/src/crypto/seal.ts b/slack-broker/src/crypto/seal.ts deleted file mode 100644 index f9e9f95..0000000 --- a/slack-broker/src/crypto/seal.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Sealed box encryption β€” used for inbound (Slack β†’ server) path. - * - * The broker encrypts with the server's public key. Only the server's - * private key can decrypt. The broker CANNOT decrypt sealed boxes. - * - * Uses libsodium's native crypto_box_seal / crypto_box_seal_open for - * interoperability with standard libsodium implementations on the server. - * The nonce is derived using BLAKE2B(ephemeral_pk || recipient_pk) as - * per the libsodium spec. - */ - -import _sodium from "libsodium-wrappers-sumo"; -import { encodeBase64, decodeBase64 } from "../util/encoding.js"; - -/** Length of an X25519 public key in bytes. */ -const PUBLIC_KEY_BYTES = 32; - -/** Ensure libsodium is initialized before use. */ -async function sodium(): Promise { - await _sodium.ready; - return _sodium; -} - -/** - * Encrypt a message using a sealed box (crypto_box_seal). - * - * Returns base64-encoded ciphertext (ephemeral_pk || box output). - * Only the holder of `recipientPublicKey`'s corresponding private key can decrypt. - * - * Uses libsodium's native implementation with BLAKE2B nonce derivation - * for interoperability with standard libsodium on the server side. - */ -export async function sealedBoxEncrypt( - plaintext: Uint8Array, - recipientPublicKey: Uint8Array, -): Promise { - const s = await sodium(); - const sealed = s.crypto_box_seal(plaintext, recipientPublicKey); - return encodeBase64(sealed); -} - -/** - * Decrypt a sealed box (crypto_box_seal_open). - * - * Used on the SERVER side (not in the broker for inbound messages). - * Included here for testing and for potential future use. - */ -export async function sealedBoxDecrypt( - sealedBase64: string, - recipientPublicKey: Uint8Array, - recipientSecretKey: Uint8Array, -): Promise { - const s = await sodium(); - const sealed = decodeBase64(sealedBase64); - - if (sealed.length < PUBLIC_KEY_BYTES + s.crypto_box_MACBYTES) { - throw new Error("sealedBoxDecrypt: ciphertext too short"); - } - - try { - return s.crypto_box_seal_open(sealed, recipientPublicKey, recipientSecretKey); - } catch { - throw new Error("sealedBoxDecrypt: decryption failed β€” invalid key or corrupted data"); - } -} diff --git a/slack-broker/src/crypto/verify.ts b/slack-broker/src/crypto/verify.ts deleted file mode 100644 index 073d5f3..0000000 --- a/slack-broker/src/crypto/verify.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Signature creation and verification for envelope authentication. - * - * - The broker signs outbound envelopes (forwarded events) so servers can - * verify they came from the broker. - * - Servers sign outbound requests so the broker can verify the sender. - * - * Uses Ed25519 (tweetnacl's sign module) for deterministic signatures. - */ - -import nacl from "tweetnacl"; -import { decodeBase64, encodeBase64 } from "../util/encoding.js"; - -/** - * Sign a message with an Ed25519 secret key. - * - * @param message - the bytes to sign - * @param secretKey - 64-byte Ed25519 secret key - * @returns base64-encoded detached signature - */ -export function sign(message: Uint8Array, secretKey: Uint8Array): string { - const signature = nacl.sign.detached(message, secretKey); - return encodeBase64(signature); -} - -/** - * Verify a detached Ed25519 signature. - * - * @param message - the original message bytes - * @param signatureBase64 - base64-encoded detached signature - * @param publicKey - 32-byte Ed25519 public key - * @returns true if signature is valid - */ -export function verify( - message: Uint8Array, - signatureBase64: string, - publicKey: Uint8Array, -): boolean { - try { - const signature = decodeBase64(signatureBase64); - return nacl.sign.detached.verify(message, signature, publicKey); - } catch { - return false; - } -} - -/** - * Generate an Ed25519 signing keypair. - * - * @returns { publicKey, secretKey } β€” both as Uint8Arrays - */ -export function generateSigningKeypair(): nacl.SignKeyPair { - return nacl.sign.keyPair(); -} - -/** - * Construct the canonical bytes for envelope signing. - * - * Canonicalizes the envelope fields into a deterministic byte string - * to prevent field-ordering or encoding ambiguities. - * - * Format: "workspace_id|timestamp|encrypted_payload_base64" - */ -export function canonicalizeEnvelope( - workspaceId: string, - timestamp: number, - encryptedPayload: string, -): Uint8Array { - const canonical = `${workspaceId}|${timestamp}|${encryptedPayload}`; - return new TextEncoder().encode(canonical); -} - -/** - * Construct the canonical bytes for outbound request signing. - * - * Format: "workspace_id|action|timestamp|encrypted_body_base64" - */ -export function canonicalizeOutbound( - workspaceId: string, - action: string, - timestamp: number, - encryptedBody: string, -): Uint8Array { - const canonical = `${workspaceId}|${action}|${timestamp}|${encryptedBody}`; - return new TextEncoder().encode(canonical); -} diff --git a/slack-broker/src/index.ts b/slack-broker/src/index.ts deleted file mode 100644 index 6030ced..0000000 --- a/slack-broker/src/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Slack Broker β€” Cloudflare Worker entry point. - * - * Routes incoming requests to the appropriate handler: - * POST /slack/events β€” Slack Events API webhook - * GET /slack/oauth/install β€” Start OAuth install flow - * GET /slack/oauth/callback β€” Handle OAuth callback - * POST /api/register β€” Register a baudbot server - * DELETE /api/register β€” Unlink a baudbot server - * POST /api/send β€” Outbound: encrypted message β†’ Slack - * GET /api/broker-pubkey β€” Get broker's public keys - * GET /health β€” Health check - */ - -import { handleSlackEvent } from "./slack/events.js"; -import { handleOAuthInstall, handleOAuthCallback } from "./slack/oauth.js"; -import { handleRegister } from "./api/register.js"; -import { handleSend } from "./api/send.js"; -import { decodeBase64, encodeBase64 } from "./util/encoding.js"; -import nacl from "tweetnacl"; - -/** - * Environment bindings available to the worker. - */ -export interface Env { - // KV namespaces - WORKSPACE_ROUTING: KVNamespace; - OAUTH_STATE: KVNamespace; - - // Secrets (set via `wrangler secret put`) - BROKER_PRIVATE_KEY: string; // Base64-encoded X25519 private key (32-byte seed) - SLACK_CLIENT_ID: string; - SLACK_CLIENT_SECRET: string; - SLACK_SIGNING_SECRET: string; - - // Derived at request time (not stored as secrets) - BROKER_PUBLIC_KEY: string; // Base64-encoded X25519 public key - BROKER_SIGNING_PUBLIC_KEY: string; // Base64-encoded Ed25519 public key - BROKER_PRIVATE_KEY_BYTES: Uint8Array; - BROKER_SIGNING_KEY: Uint8Array; // Ed25519 secret key for signing envelopes - - // Execution context (for waitUntil) - _ctx?: ExecutionContext; -} - -/** Cloudflare KV namespace interface. */ -interface KVNamespace { - get(key: string): Promise; - put(key: string, value: string, options?: { expirationTtl?: number }): Promise; - delete(key: string): Promise; -} - -export default { - async fetch( - request: Request, - rawEnv: Record, - ctx: ExecutionContext, - ): Promise { - const url = new URL(request.url); - const path = url.pathname; - - // Derive keys from the secret seed - const privateKeyBase64 = rawEnv.BROKER_PRIVATE_KEY as string; - if (!privateKeyBase64) { - return new Response("server misconfigured: missing BROKER_PRIVATE_KEY", { status: 500 }); - } - - const seed = decodeBase64(privateKeyBase64); - const boxKeypair = nacl.box.keyPair.fromSecretKey(seed); - const signKeypair = nacl.sign.keyPair.fromSeed(seed); - - // Build the full Env object - const env: Env = { - WORKSPACE_ROUTING: rawEnv.WORKSPACE_ROUTING as unknown as KVNamespace, - OAUTH_STATE: rawEnv.OAUTH_STATE as unknown as KVNamespace, - BROKER_PRIVATE_KEY: privateKeyBase64, - SLACK_CLIENT_ID: rawEnv.SLACK_CLIENT_ID as string, - SLACK_CLIENT_SECRET: rawEnv.SLACK_CLIENT_SECRET as string, - SLACK_SIGNING_SECRET: rawEnv.SLACK_SIGNING_SECRET as string, - BROKER_PUBLIC_KEY: encodeBase64(boxKeypair.publicKey), - BROKER_SIGNING_PUBLIC_KEY: encodeBase64(signKeypair.publicKey), - BROKER_PRIVATE_KEY_BYTES: boxKeypair.secretKey, - BROKER_SIGNING_KEY: signKeypair.secretKey, - _ctx: ctx, - }; - - // TODO(Phase 3): Add rate limiting before production deployment. - // Per-workspace and per-IP limits on /api/send, /api/register, and /slack/events. - // Cloudflare Rate Limiting rules or a KV-based token bucket are both viable. - - // Route requests - try { - // Health check - if (path === "/health" && request.method === "GET") { - return new Response(JSON.stringify({ ok: true, service: "slack-broker" }), { - headers: { "Content-Type": "application/json" }, - }); - } - - // Broker public key endpoint - if (path === "/api/broker-pubkey" && request.method === "GET") { - return new Response( - JSON.stringify({ - ok: true, - broker_pubkey: env.BROKER_PUBLIC_KEY, - broker_signing_pubkey: env.BROKER_SIGNING_PUBLIC_KEY, - }), - { headers: { "Content-Type": "application/json" } }, - ); - } - - // Slack Events API - if (path === "/slack/events" && request.method === "POST") { - return handleSlackEvent(request, env); - } - - // OAuth install flow - if (path === "/slack/oauth/install" && request.method === "GET") { - return handleOAuthInstall(request, env); - } - - // OAuth callback - if (path === "/slack/oauth/callback" && request.method === "GET") { - return handleOAuthCallback(request, env); - } - - // Server registration - if (path === "/api/register") { - return handleRegister(request, env); - } - - // Outbound message sending - if (path === "/api/send" && request.method === "POST") { - return handleSend(request, env); - } - - // 404 for everything else - return new Response("not found", { status: 404 }); - } catch (err) { - // Never leak internal errors β€” log routing metadata only - console.error(`[${request.method}] ${path} β€” unhandled error:`, err instanceof Error ? err.message : "unknown"); - return new Response("internal error", { status: 500 }); - } - }, -}; diff --git a/slack-broker/src/routing/forward.ts b/slack-broker/src/routing/forward.ts deleted file mode 100644 index c1dcbeb..0000000 --- a/slack-broker/src/routing/forward.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Encrypt a Slack event and forward it to the registered server. - * - * Flow: - * 1. Look up workspace β†’ server_pubkey, server_url - * 2. Encrypt the event payload with sealed box (server_pubkey) - * 3. Sign the envelope with the broker's signing key - * 4. POST the envelope to the server's callback URL - * - * If the server is unreachable, the event is dropped (no queuing). - * The broker logs routing metadata (workspace_id, timestamp) but NEVER message content. - */ - -import { sealedBoxEncrypt } from "../crypto/seal.js"; -import { sign, canonicalizeEnvelope } from "../crypto/verify.js"; -import { decodeBase64, encodeUTF8 } from "../util/encoding.js"; -import type { WorkspaceRecord } from "./registry.js"; - -export interface ForwardEnvelope { - workspace_id: string; - encrypted: string; - timestamp: number; - signature: string; -} - -export interface ForwardResult { - ok: boolean; - status?: number; - error?: string; -} - -/** - * Encrypt and forward a Slack event to the registered server. - * - * @param event - the raw Slack event payload (JSON object) - * @param workspace - the workspace routing record - * @param brokerSigningKey - the broker's Ed25519 secret key for signing envelopes - * @returns result indicating success/failure - */ -export async function forwardEvent( - event: unknown, - workspace: WorkspaceRecord, - brokerSigningKey: Uint8Array, -): Promise { - if (workspace.status !== "active") { - return { ok: false, error: "workspace not active" }; - } - - if (!workspace.server_url || !workspace.server_pubkey) { - return { ok: false, error: "workspace missing server configuration" }; - } - - // Enforce HTTPS for all server callback URLs - try { - const url = new URL(workspace.server_url); - if (url.protocol !== "https:") { - return { ok: false, error: "server URL must use HTTPS" }; - } - } catch { - return { ok: false, error: "invalid server URL" }; - } - - // Serialize and encrypt - const plaintext = encodeUTF8(JSON.stringify(event)); - const serverPubkey = decodeBase64(workspace.server_pubkey); - const encrypted = await sealedBoxEncrypt(plaintext, serverPubkey); - - // Build and sign envelope - const timestamp = Math.floor(Date.now() / 1000); - const canonical = canonicalizeEnvelope(workspace.workspace_id, timestamp, encrypted); - const signature = sign(canonical, brokerSigningKey); - - const envelope: ForwardEnvelope = { - workspace_id: workspace.workspace_id, - encrypted, - timestamp, - signature, - }; - - // Forward to server β€” fire and forget with timeout - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10_000); - - const response = await fetch(workspace.server_url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Broker-Signature": signature, - "X-Broker-Timestamp": String(timestamp), - }, - body: JSON.stringify(envelope), - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - return { - ok: false, - status: response.status, - error: `server responded with ${response.status}`, - }; - } - - return { ok: true, status: response.status }; - } catch (err) { - const message = err instanceof Error ? err.message : "unknown error"; - return { ok: false, error: `forward failed: ${message}` }; - } -} diff --git a/slack-broker/src/routing/registry.ts b/slack-broker/src/routing/registry.ts deleted file mode 100644 index b32439c..0000000 --- a/slack-broker/src/routing/registry.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * KV-backed workspace routing registry. - * - * Maps workspace_id β†’ { server_url, server_pubkey, bot_token, status }. - * Stored in the WORKSPACE_ROUTING KV namespace. - * - * Status lifecycle: pending β†’ active β†’ (optionally) inactive - * - pending: OAuth complete, no server linked yet - * - active: server registered and verified - * - inactive: server unlinked or heartbeat timed out - */ - -import nacl from "tweetnacl"; -import { encodeBase64, decodeBase64 } from "../util/encoding.js"; - -export type WorkspaceStatus = "pending" | "active" | "inactive"; - -export interface WorkspaceRecord { - workspace_id: string; - team_name: string; - server_url: string; - /** Base64-encoded X25519 public key for sealing inbound messages. */ - server_pubkey: string; - /** Base64-encoded Ed25519 public key for verifying server signatures. */ - server_signing_pubkey: string; - /** Encrypted bot token (encrypted at rest in KV). */ - bot_token: string; - status: WorkspaceStatus; - /** ISO 8601 timestamp of last registration or heartbeat. */ - updated_at: string; - /** Auth code hash β€” used during registration to verify workspace ownership. */ - auth_code_hash: string; -} - -/** KV binding type for Cloudflare Workers. */ -export interface KVNamespace { - get(key: string, options?: { type?: string }): Promise; - put(key: string, value: string, options?: { expirationTtl?: number }): Promise; - delete(key: string): Promise; -} - -/** - * Get a workspace record from KV. - */ -export async function getWorkspace( - kv: KVNamespace, - workspaceId: string, -): Promise { - const raw = await kv.get(`workspace:${workspaceId}`); - if (!raw) return null; - return JSON.parse(raw) as WorkspaceRecord; -} - -/** - * Store a workspace record in KV. - */ -export async function putWorkspace( - kv: KVNamespace, - record: WorkspaceRecord, -): Promise { - await kv.put(`workspace:${record.workspace_id}`, JSON.stringify(record)); -} - -/** - * Encrypt a bot token with nacl.secretbox for storage in KV. - * Uses the first 32 bytes of the broker's private key as the secretbox key. - * - * @returns base64-encoded nonce + ciphertext - */ -export function encryptBotToken(botToken: string, brokerKeyBase64: string): string { - const key = decodeBase64(brokerKeyBase64).slice(0, nacl.secretbox.keyLength); - const nonce = nacl.randomBytes(nacl.secretbox.nonceLength); - const plaintext = new TextEncoder().encode(botToken); - const ciphertext = nacl.secretbox(plaintext, nonce, key); - // Output: nonce || ciphertext - const combined = new Uint8Array(nonce.length + ciphertext.length); - combined.set(nonce, 0); - combined.set(ciphertext, nonce.length); - return encodeBase64(combined); -} - -/** - * Decrypt a bot token from KV storage. - * - * @param encrypted - base64-encoded nonce + ciphertext (from encryptBotToken) - * @param brokerKeyBase64 - the broker's private key (base64) - * @returns the plaintext bot token - */ -export function decryptBotToken(encrypted: string, brokerKeyBase64: string): string { - const key = decodeBase64(brokerKeyBase64).slice(0, nacl.secretbox.keyLength); - const combined = decodeBase64(encrypted); - const nonce = combined.slice(0, nacl.secretbox.nonceLength); - const ciphertext = combined.slice(nacl.secretbox.nonceLength); - const plaintext = nacl.secretbox.open(ciphertext, nonce, key); - if (!plaintext) { - throw new Error("decryptBotToken: decryption failed β€” invalid key or corrupted data"); - } - return new TextDecoder().decode(plaintext); -} - -/** - * Create a pending workspace record after OAuth. - * The bot_token is encrypted with nacl.secretbox before storage. - * - * @param brokerKeyBase64 - the broker's private key for encrypting the bot token - */ -export async function createPendingWorkspace( - kv: KVNamespace, - workspaceId: string, - teamName: string, - botToken: string, - authCodeHash: string, - brokerKeyBase64: string, -): Promise { - const record: WorkspaceRecord = { - workspace_id: workspaceId, - team_name: teamName, - server_url: "", - server_pubkey: "", - server_signing_pubkey: "", - bot_token: encryptBotToken(botToken, brokerKeyBase64), - status: "pending", - updated_at: new Date().toISOString(), - auth_code_hash: authCodeHash, - }; - await putWorkspace(kv, record); - return record; -} - -/** - * Activate a workspace by registering a server. - * - * Clears auth_code_hash after activation to prevent auth code reuse. - * Returns null if the workspace doesn't exist or is already active - * (active workspaces must be deactivated before re-registering). - */ -export async function activateWorkspace( - kv: KVNamespace, - workspaceId: string, - serverUrl: string, - serverPubkey: string, - serverSigningPubkey: string, -): Promise { - const record = await getWorkspace(kv, workspaceId); - if (!record) return null; - - // Reject re-registration of already-active workspaces. - // The current server must unregister first (DELETE /api/register). - if (record.status === "active") return null; - - record.server_url = serverUrl; - record.server_pubkey = serverPubkey; - record.server_signing_pubkey = serverSigningPubkey; - record.status = "active"; - record.updated_at = new Date().toISOString(); - // Clear auth_code_hash β€” one-time use only. Prevents hijacking via code reuse. - record.auth_code_hash = ""; - await putWorkspace(kv, record); - return record; -} - -/** - * Deactivate a workspace (unlink server). - */ -export async function deactivateWorkspace( - kv: KVNamespace, - workspaceId: string, -): Promise { - const record = await getWorkspace(kv, workspaceId); - if (!record) return false; - - record.status = "inactive"; - record.server_url = ""; - record.server_pubkey = ""; - record.server_signing_pubkey = ""; - record.updated_at = new Date().toISOString(); - await putWorkspace(kv, record); - return true; -} - -/** - * Hash an auth code for storage using HMAC-SHA256, keyed with a broker secret. - * We never store raw auth codes. HMAC prevents offline brute-force without - * knowledge of the broker's secret key. - * - * @param authCode - the raw auth code to hash - * @param brokerSecret - the broker's private key (used as HMAC key) - */ -export async function hashAuthCode(authCode: string, brokerSecret: string): Promise { - const encoder = new TextEncoder(); - const key = await crypto.subtle.importKey( - "raw", - encoder.encode(brokerSecret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], - ); - const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(authCode)); - const bytes = new Uint8Array(mac); - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} diff --git a/slack-broker/src/slack/api.ts b/slack-broker/src/slack/api.ts deleted file mode 100644 index cf19a80..0000000 --- a/slack-broker/src/slack/api.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Slack Web API helpers. - * - * Used by the broker to post messages, add reactions, and update messages. - * These are the minimum API calls needed for the outbound path. - * - * SECURITY: Message content is only held transiently in memory. - * After calling Slack's API, callers should zero any plaintext buffers. - */ - -export interface SlackApiResult { - ok: boolean; - ts?: string; - error?: string; -} - -/** - * Post a message to a Slack channel. - */ -export async function postMessage( - botToken: string, - channel: string, - text: string, - options?: { - thread_ts?: string; - blocks?: unknown[]; - }, -): Promise { - const body: Record = { - channel, - text, - }; - - if (options?.thread_ts) body.thread_ts = options.thread_ts; - if (options?.blocks) body.blocks = options.blocks; - - return callSlackApi(botToken, "chat.postMessage", body); -} - -/** - * Add a reaction to a message. - */ -export async function addReaction( - botToken: string, - channel: string, - timestamp: string, - emoji: string, -): Promise { - return callSlackApi(botToken, "reactions.add", { - channel, - timestamp, - name: emoji, - }); -} - -/** - * Update an existing message. - */ -export async function updateMessage( - botToken: string, - channel: string, - timestamp: string, - text: string, - options?: { - blocks?: unknown[]; - }, -): Promise { - const body: Record = { - channel, - ts: timestamp, - text, - }; - - if (options?.blocks) body.blocks = options.blocks; - - return callSlackApi(botToken, "chat.update", body); -} - -/** - * Generic Slack Web API caller. - */ -async function callSlackApi( - botToken: string, - method: string, - body: Record, -): Promise { - try { - const response = await fetch(`https://slack.com/api/${method}`, { - method: "POST", - headers: { - "Authorization": `Bearer ${botToken}`, - "Content-Type": "application/json; charset=utf-8", - }, - body: JSON.stringify(body), - }); - - const data = (await response.json()) as SlackApiResult; - return data; - } catch (err) { - const message = err instanceof Error ? err.message : "unknown error"; - return { ok: false, error: `slack api call failed: ${message}` }; - } -} diff --git a/slack-broker/src/slack/events.ts b/slack-broker/src/slack/events.ts deleted file mode 100644 index f69163f..0000000 --- a/slack-broker/src/slack/events.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Slack Events API handler. - * - * Responsibilities: - * 1. Verify Slack request signatures (HMAC-SHA256) - * 2. Handle url_verification challenge (Slack app setup) - * 3. Parse event payloads and dispatch to the forwarding pipeline - * - * Replay protection: reject events with timestamps older than 5 minutes. - */ - -import { getWorkspace } from "../routing/registry.js"; -import { forwardEvent } from "../routing/forward.js"; -import type { Env } from "../index.js"; - -const FIVE_MINUTES = 5 * 60; - -/** - * Verify a Slack request signature. - * - * Slack signs requests with HMAC-SHA256 using the signing secret. - * See: https://api.slack.com/authentication/verifying-requests-from-slack - * - * @param signingSecret - the app's signing secret - * @param timestamp - X-Slack-Request-Timestamp header - * @param body - raw request body string - * @param signature - X-Slack-Signature header (v0=...) - * @returns true if the signature is valid and timestamp is fresh - */ -export async function verifySlackSignature( - signingSecret: string, - timestamp: string, - body: string, - signature: string, -): Promise { - // Replay protection: reject timestamps older than 5 minutes - const ts = parseInt(timestamp, 10); - if (isNaN(ts)) return false; - - const now = Math.floor(Date.now() / 1000); - if (Math.abs(now - ts) > FIVE_MINUTES) return false; - - // Compute expected signature - const sigBasestring = `v0:${timestamp}:${body}`; - const encoder = new TextEncoder(); - const key = await crypto.subtle.importKey( - "raw", - encoder.encode(signingSecret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], - ); - const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(sigBasestring)); - const expected = `v0=${Array.from(new Uint8Array(mac)) - .map((b) => b.toString(16).padStart(2, "0")) - .join("")}`; - - // Constant-time comparison - if (expected.length !== signature.length) return false; - let mismatch = 0; - for (let i = 0; i < expected.length; i++) { - mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i); - } - return mismatch === 0; -} - -interface SlackChallenge { - type: "url_verification"; - challenge: string; - token: string; -} - -interface SlackEventCallback { - type: "event_callback"; - team_id: string; - event: { - type: string; - [key: string]: unknown; - }; - [key: string]: unknown; -} - -type SlackPayload = SlackChallenge | SlackEventCallback; - -/** - * Handle an incoming Slack Events API request. - */ -export async function handleSlackEvent( - request: Request, - env: Env, -): Promise { - // Read and verify signature - const body = await request.text(); - const timestamp = request.headers.get("X-Slack-Request-Timestamp") ?? ""; - const signature = request.headers.get("X-Slack-Signature") ?? ""; - - const valid = await verifySlackSignature( - env.SLACK_SIGNING_SECRET, - timestamp, - body, - signature, - ); - - if (!valid) { - return new Response("invalid signature", { status: 401 }); - } - - // Parse payload - let payload: SlackPayload; - try { - payload = JSON.parse(body) as SlackPayload; - } catch { - return new Response("invalid JSON", { status: 400 }); - } - - // Handle url_verification challenge - if (payload.type === "url_verification") { - return new Response(JSON.stringify({ challenge: (payload as SlackChallenge).challenge }), { - headers: { "Content-Type": "application/json" }, - }); - } - - // Handle event callbacks - if (payload.type === "event_callback") { - const eventPayload = payload as SlackEventCallback; - const workspaceId = eventPayload.team_id; - - if (!workspaceId) { - return new Response("missing team_id", { status: 400 }); - } - - // Look up workspace routing - const workspace = await getWorkspace(env.WORKSPACE_ROUTING, workspaceId); - - if (!workspace || workspace.status !== "active") { - // ACK the event but don't forward β€” no active server - return new Response("ok", { status: 200 }); - } - - // Forward asynchronously β€” ACK Slack immediately (3-second deadline) - // Use waitUntil to continue processing after responding - const brokerSigningKey = env.BROKER_SIGNING_KEY; - - if (brokerSigningKey) { - const forwardPromise = forwardEvent( - eventPayload, - workspace, - brokerSigningKey, - ); - - // If we have a ctx for waitUntil, use it; otherwise fire-and-forget - if (env._ctx) { - env._ctx.waitUntil(forwardPromise); - } - } - - return new Response("ok", { status: 200 }); - } - - return new Response("unknown event type", { status: 400 }); -} diff --git a/slack-broker/src/slack/oauth.ts b/slack-broker/src/slack/oauth.ts deleted file mode 100644 index 3a45489..0000000 --- a/slack-broker/src/slack/oauth.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Slack OAuth install flow. - * - * Two endpoints: - * 1. GET /slack/oauth/install β€” redirect user to Slack's authorization page - * 2. GET /slack/oauth/callback β€” handle the OAuth callback, store bot token - * - * After OAuth completes, the workspace is in "pending" status until a - * baudbot server registers via the /api/register endpoint. - * - * The auth_code is generated during OAuth and must be presented during - * server registration to prove workspace ownership. - */ - -import { createPendingWorkspace, getWorkspace, hashAuthCode } from "../routing/registry.js"; -import type { Env } from "../index.js"; - -/** Required OAuth scopes for the Prime app. */ -const BOT_SCOPES = [ - "app_mentions:read", - "channels:history", - "chat:write", - "groups:history", - "im:history", - "reactions:write", - "users:read", -].join(","); - -/** - * Generate a cryptographically random string for OAuth state and auth codes. - */ -function generateRandomString(length: number): string { - const bytes = crypto.getRandomValues(new Uint8Array(length)); - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} - -/** - * Redirect to Slack's OAuth authorization page. - */ -export async function handleOAuthInstall( - request: Request, - env: Env, -): Promise { - const state = generateRandomString(32); - - // Store state with 10-minute expiry - await env.OAUTH_STATE.put(`oauth_state:${state}`, "valid", { - expirationTtl: 600, - }); - - const url = new URL("https://slack.com/oauth/v2/authorize"); - url.searchParams.set("client_id", env.SLACK_CLIENT_ID); - url.searchParams.set("scope", BOT_SCOPES); - url.searchParams.set("state", state); - - // Derive redirect URI from the current request's origin - const requestUrl = new URL(request.url); - const redirectUri = `${requestUrl.origin}/slack/oauth/callback`; - url.searchParams.set("redirect_uri", redirectUri); - - return Response.redirect(url.toString(), 302); -} - -interface SlackOAuthResponse { - ok: boolean; - access_token?: string; - token_type?: string; - team?: { id: string; name: string }; - bot_user_id?: string; - error?: string; -} - -/** - * Handle the OAuth callback from Slack. - * - * Exchanges the code for a bot token, creates a pending workspace record, - * and returns the auth_code that the user needs for server registration. - */ -export async function handleOAuthCallback( - request: Request, - env: Env, -): Promise { - const url = new URL(request.url); - const code = url.searchParams.get("code"); - const state = url.searchParams.get("state"); - const error = url.searchParams.get("error"); - - // User denied access - if (error) { - return new Response( - `

Installation cancelled

${escapeHtml(error)}

`, - { status: 200, headers: { "Content-Type": "text/html" } }, - ); - } - - if (!code || !state) { - return new Response("missing code or state", { status: 400 }); - } - - // Verify state parameter - const storedState = await env.OAUTH_STATE.get(`oauth_state:${state}`); - if (!storedState) { - return new Response("invalid or expired state", { status: 400 }); - } - // Delete state to prevent reuse - await env.OAUTH_STATE.delete(`oauth_state:${state}`); - - // Exchange code for token - const redirectUri = `${url.origin}/slack/oauth/callback`; - const tokenResponse = await fetch("https://slack.com/api/oauth.v2.access", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - client_id: env.SLACK_CLIENT_ID, - client_secret: env.SLACK_CLIENT_SECRET, - code, - redirect_uri: redirectUri, - }), - }); - - const tokenData = (await tokenResponse.json()) as SlackOAuthResponse; - - if (!tokenData.ok || !tokenData.access_token || !tokenData.team) { - return new Response( - `

Installation failed

${escapeHtml(tokenData.error ?? "unknown error")}

`, - { status: 200, headers: { "Content-Type": "text/html" } }, - ); - } - - // Reject re-install if workspace is already active. - // The current server must unregister first to prevent silent disconnection. - const existing = await getWorkspace(env.WORKSPACE_ROUTING, tokenData.team.id); - if (existing && existing.status === "active") { - return new Response( - ` - -

⚠️ Workspace already active

-

Baudbot is already connected to ${escapeHtml(tokenData.team.name)} with an active server.

-

To re-install, first unregister the current server (run baudbot unregister), then try again.

- -`, - { status: 200, headers: { "Content-Type": "text/html" } }, - ); - } - - // Generate auth code for server registration - const authCode = generateRandomString(32); - const authCodeHashed = await hashAuthCode(authCode, env.BROKER_PRIVATE_KEY); - - // Store workspace with pending status (bot token encrypted at rest) - await createPendingWorkspace( - env.WORKSPACE_ROUTING, - tokenData.team.id, - tokenData.team.name, - tokenData.access_token, - authCodeHashed, - env.BROKER_PRIVATE_KEY, - ); - - // Return success page with auth code - return new Response( - ` - -

βœ… Baudbot installed in ${escapeHtml(tokenData.team.name)}

-

Your workspace ID: ${escapeHtml(tokenData.team.id)}

-

Your auth code (save this β€” you'll need it during server setup):

-
${escapeHtml(authCode)}
-

Run baudbot setup --slack-broker on your server and enter this code when prompted.

- -`, - { status: 200, headers: { "Content-Type": "text/html" } }, - ); -} - -/** Minimal HTML escaping. */ -function escapeHtml(str: string): string { - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); -} diff --git a/slack-broker/src/util/encoding.ts b/slack-broker/src/util/encoding.ts deleted file mode 100644 index ca21849..0000000 --- a/slack-broker/src/util/encoding.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Base64 encoding/decoding utilities. - * - * Uses standard base64 (not URL-safe) for consistency with tweetnacl-util - * and common Slack/crypto conventions. - */ - -/** - * Encode a Uint8Array to a base64 string. - */ -export function encodeBase64(bytes: Uint8Array): string { - let binary = ""; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]!); - } - return btoa(binary); -} - -/** - * Decode a base64 string to a Uint8Array. - */ -export function decodeBase64(base64: string): Uint8Array { - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes; -} - -/** - * Encode a UTF-8 string to Uint8Array. - */ -export function encodeUTF8(str: string): Uint8Array { - return new TextEncoder().encode(str); -} - -/** - * Decode a Uint8Array to a UTF-8 string. - */ -export function decodeUTF8(bytes: Uint8Array): string { - return new TextDecoder().decode(bytes); -} diff --git a/slack-broker/test/crypto.test.ts b/slack-broker/test/crypto.test.ts deleted file mode 100644 index 92bdd8b..0000000 --- a/slack-broker/test/crypto.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Unit tests for crypto modules: sealed boxes, authenticated encryption, signatures. - */ - -import { describe, it, expect } from "vitest"; -import nacl from "tweetnacl"; -import { sealedBoxEncrypt, sealedBoxDecrypt } from "../src/crypto/seal.js"; -import { boxEncrypt, boxDecrypt, zeroBytes } from "../src/crypto/box.js"; -import { - sign, - verify, - generateSigningKeypair, - canonicalizeEnvelope, - canonicalizeOutbound, -} from "../src/crypto/verify.js"; -import { encodeBase64, decodeBase64, encodeUTF8, decodeUTF8 } from "../src/util/encoding.js"; - -describe("sealed box (crypto_box_seal)", () => { - it("encrypts and decrypts a message", async () => { - const recipientKeypair = nacl.box.keyPair(); - const plaintext = encodeUTF8("hello from Slack"); - - const sealed = await sealedBoxEncrypt(plaintext, recipientKeypair.publicKey); - expect(sealed).toBeTruthy(); - expect(typeof sealed).toBe("string"); - - const decrypted = await sealedBoxDecrypt( - sealed, - recipientKeypair.publicKey, - recipientKeypair.secretKey, - ); - expect(decodeUTF8(decrypted)).toBe("hello from Slack"); - }); - - it("different encryptions produce different ciphertexts (ephemeral keys)", async () => { - const recipientKeypair = nacl.box.keyPair(); - const plaintext = encodeUTF8("same message"); - - const sealed1 = await sealedBoxEncrypt(plaintext, recipientKeypair.publicKey); - const sealed2 = await sealedBoxEncrypt(plaintext, recipientKeypair.publicKey); - - // Ephemeral keypairs mean different ciphertexts - expect(sealed1).not.toBe(sealed2); - - // Both decrypt to the same plaintext - const d1 = await sealedBoxDecrypt(sealed1, recipientKeypair.publicKey, recipientKeypair.secretKey); - const d2 = await sealedBoxDecrypt(sealed2, recipientKeypair.publicKey, recipientKeypair.secretKey); - expect(decodeUTF8(d1)).toBe("same message"); - expect(decodeUTF8(d2)).toBe("same message"); - }); - - it("fails to decrypt with wrong private key", async () => { - const recipientKeypair = nacl.box.keyPair(); - const wrongKeypair = nacl.box.keyPair(); - const plaintext = encodeUTF8("secret message"); - - const sealed = await sealedBoxEncrypt(plaintext, recipientKeypair.publicKey); - - await expect( - sealedBoxDecrypt(sealed, wrongKeypair.publicKey, wrongKeypair.secretKey), - ).rejects.toThrow("decryption failed"); - }); - - it("fails on truncated ciphertext", async () => { - const recipientKeypair = nacl.box.keyPair(); - - await expect( - sealedBoxDecrypt("AAAA", recipientKeypair.publicKey, recipientKeypair.secretKey), - ).rejects.toThrow("ciphertext too short"); - }); - - it("handles empty plaintext", async () => { - const recipientKeypair = nacl.box.keyPair(); - const plaintext = new Uint8Array(0); - - const sealed = await sealedBoxEncrypt(plaintext, recipientKeypair.publicKey); - const decrypted = await sealedBoxDecrypt( - sealed, - recipientKeypair.publicKey, - recipientKeypair.secretKey, - ); - expect(decrypted.length).toBe(0); - }); - - it("handles large payloads", async () => { - const recipientKeypair = nacl.box.keyPair(); - const plaintext = new Uint8Array(100_000); - // Fill in chunks β€” crypto.getRandomValues has a 65536-byte limit - for (let i = 0; i < plaintext.length; i += 65536) { - const chunk = plaintext.subarray(i, Math.min(i + 65536, plaintext.length)); - crypto.getRandomValues(chunk); - } - - const sealed = await sealedBoxEncrypt(plaintext, recipientKeypair.publicKey); - const decrypted = await sealedBoxDecrypt( - sealed, - recipientKeypair.publicKey, - recipientKeypair.secretKey, - ); - expect(decrypted).toEqual(plaintext); - }); -}); - -describe("authenticated box (crypto_box)", () => { - it("encrypts and decrypts a message", () => { - const sender = nacl.box.keyPair(); - const recipient = nacl.box.keyPair(); - const plaintext = encodeUTF8('{"text": "hello"}'); - - const { ciphertext, nonce } = boxEncrypt(plaintext, recipient.publicKey, sender.secretKey); - expect(ciphertext).toBeTruthy(); - expect(nonce).toBeTruthy(); - - const decrypted = boxDecrypt(ciphertext, nonce, sender.publicKey, recipient.secretKey); - expect(decodeUTF8(decrypted)).toBe('{"text": "hello"}'); - }); - - it("verifies sender identity (fails with wrong sender key)", () => { - const sender = nacl.box.keyPair(); - const recipient = nacl.box.keyPair(); - const imposter = nacl.box.keyPair(); - const plaintext = encodeUTF8("authentic message"); - - const { ciphertext, nonce } = boxEncrypt(plaintext, recipient.publicKey, sender.secretKey); - - // Try to decrypt claiming a different sender - expect(() => - boxDecrypt(ciphertext, nonce, imposter.publicKey, recipient.secretKey), - ).toThrow("decryption failed"); - }); - - it("fails with wrong recipient key", () => { - const sender = nacl.box.keyPair(); - const recipient = nacl.box.keyPair(); - const wrongRecipient = nacl.box.keyPair(); - const plaintext = encodeUTF8("secret"); - - const { ciphertext, nonce } = boxEncrypt(plaintext, recipient.publicKey, sender.secretKey); - - expect(() => - boxDecrypt(ciphertext, nonce, sender.publicKey, wrongRecipient.secretKey), - ).toThrow("decryption failed"); - }); - - it("fails with invalid nonce length", () => { - const sender = nacl.box.keyPair(); - const recipient = nacl.box.keyPair(); - const plaintext = encodeUTF8("test"); - - const { ciphertext } = boxEncrypt(plaintext, recipient.publicKey, sender.secretKey); - const badNonce = encodeBase64(new Uint8Array(8)); // Wrong length - - expect(() => - boxDecrypt(ciphertext, badNonce, sender.publicKey, recipient.secretKey), - ).toThrow("invalid nonce length"); - }); - - it("each encryption uses a unique nonce", () => { - const sender = nacl.box.keyPair(); - const recipient = nacl.box.keyPair(); - const plaintext = encodeUTF8("same"); - - const r1 = boxEncrypt(plaintext, recipient.publicKey, sender.secretKey); - const r2 = boxEncrypt(plaintext, recipient.publicKey, sender.secretKey); - - expect(r1.nonce).not.toBe(r2.nonce); - expect(r1.ciphertext).not.toBe(r2.ciphertext); - }); -}); - -describe("zeroBytes", () => { - it("zeroes a buffer", () => { - const buf = new Uint8Array([1, 2, 3, 4, 5]); - zeroBytes(buf); - expect(buf).toEqual(new Uint8Array([0, 0, 0, 0, 0])); - }); -}); - -describe("signatures (Ed25519)", () => { - it("sign and verify round-trip", () => { - const keypair = generateSigningKeypair(); - const message = encodeUTF8("important envelope"); - - const sig = sign(message, keypair.secretKey); - expect(verify(message, sig, keypair.publicKey)).toBe(true); - }); - - it("rejects tampered message", () => { - const keypair = generateSigningKeypair(); - const message = encodeUTF8("original"); - - const sig = sign(message, keypair.secretKey); - const tampered = encodeUTF8("tampered"); - expect(verify(tampered, sig, keypair.publicKey)).toBe(false); - }); - - it("rejects wrong public key", () => { - const keypair1 = generateSigningKeypair(); - const keypair2 = generateSigningKeypair(); - const message = encodeUTF8("test"); - - const sig = sign(message, keypair1.secretKey); - expect(verify(message, sig, keypair2.publicKey)).toBe(false); - }); - - it("rejects invalid signature format", () => { - const keypair = generateSigningKeypair(); - const message = encodeUTF8("test"); - - expect(verify(message, "not-valid-base64!!!", keypair.publicKey)).toBe(false); - }); - - it("rejects truncated signature", () => { - const keypair = generateSigningKeypair(); - const message = encodeUTF8("test"); - - const sig = sign(message, keypair.secretKey); - const truncated = sig.slice(0, 10); - expect(verify(message, truncated, keypair.publicKey)).toBe(false); - }); -}); - -describe("canonicalize", () => { - it("envelope canonicalization is deterministic", () => { - const a = canonicalizeEnvelope("T123", 1000, "encrypted_data"); - const b = canonicalizeEnvelope("T123", 1000, "encrypted_data"); - expect(a).toEqual(b); - }); - - it("envelope canonicalization differs with different inputs", () => { - const a = canonicalizeEnvelope("T123", 1000, "data1"); - const b = canonicalizeEnvelope("T123", 1000, "data2"); - expect(a).not.toEqual(b); - }); - - it("outbound canonicalization is deterministic", () => { - const a = canonicalizeOutbound("T123", "chat.postMessage", 1000, "body"); - const b = canonicalizeOutbound("T123", "chat.postMessage", 1000, "body"); - expect(a).toEqual(b); - }); - - it("outbound canonicalization includes action", () => { - const a = canonicalizeOutbound("T123", "chat.postMessage", 1000, "body"); - const b = canonicalizeOutbound("T123", "reactions.add", 1000, "body"); - expect(a).not.toEqual(b); - }); -}); - -describe("encoding utilities", () => { - it("base64 round-trip", () => { - const original = new Uint8Array([0, 1, 127, 128, 255]); - const encoded = encodeBase64(original); - const decoded = decodeBase64(encoded); - expect(decoded).toEqual(original); - }); - - it("utf8 round-trip", () => { - const original = "Hello 🌍 world β€” test"; - const encoded = encodeUTF8(original); - const decoded = decodeUTF8(encoded); - expect(decoded).toBe(original); - }); - - it("base64 of empty array", () => { - const encoded = encodeBase64(new Uint8Array(0)); - const decoded = decodeBase64(encoded); - expect(decoded.length).toBe(0); - }); -}); diff --git a/slack-broker/test/integration.test.ts b/slack-broker/test/integration.test.ts deleted file mode 100644 index 2f07c8b..0000000 --- a/slack-broker/test/integration.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * End-to-end message flow integration tests. - * - * Tests the complete paths: - * 1. Inbound: Slack event β†’ verify signature β†’ encrypt β†’ forward to server - * 2. Outbound: Server sends encrypted reply β†’ broker decrypts β†’ posts to Slack - * 3. Registration: OAuth β†’ register server β†’ activate workspace - * 4. Slack signature verification edge cases - */ - -import { describe, it, expect, vi, beforeEach } from "vitest"; -import nacl from "tweetnacl"; -import { verifySlackSignature } from "../src/slack/events.js"; -import { boxEncrypt, boxDecrypt } from "../src/crypto/box.js"; -import { sealedBoxDecrypt } from "../src/crypto/seal.js"; -import { sign, canonicalizeOutbound, canonicalizeEnvelope, verify } from "../src/crypto/verify.js"; -import { encodeBase64, decodeBase64, encodeUTF8, decodeUTF8 } from "../src/util/encoding.js"; -import { - createPendingWorkspace, - activateWorkspace, - getWorkspace, - hashAuthCode, - decryptBotToken, - type KVNamespace, -} from "../src/routing/registry.js"; - -/** Test broker key (base64-encoded 32-byte key). */ -const TEST_BROKER_KEY = encodeBase64(nacl.randomBytes(32)); - -/** In-memory KV mock. */ -function createMockKV(): KVNamespace { - const store = new Map(); - return { - async get(key: string) { - return store.get(key) ?? null; - }, - async put(key: string, value: string) { - store.set(key, value); - }, - async delete(key: string) { - store.delete(key); - }, - }; -} - -/** - * Generate a valid Slack signature for testing. - */ -async function makeSlackSignature( - signingSecret: string, - timestamp: string, - body: string, -): Promise { - const sigBasestring = `v0:${timestamp}:${body}`; - const encoder = new TextEncoder(); - const key = await crypto.subtle.importKey( - "raw", - encoder.encode(signingSecret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], - ); - const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(sigBasestring)); - return `v0=${Array.from(new Uint8Array(mac)) - .map((b) => b.toString(16).padStart(2, "0")) - .join("")}`; -} - -describe("Slack signature verification", () => { - const signingSecret = "test_signing_secret_1234567890"; - - it("accepts a valid signature", async () => { - const timestamp = String(Math.floor(Date.now() / 1000)); - const body = '{"type":"url_verification","challenge":"abc"}'; - const sig = await makeSlackSignature(signingSecret, timestamp, body); - - const valid = await verifySlackSignature(signingSecret, timestamp, body, sig); - expect(valid).toBe(true); - }); - - it("rejects an invalid signature", async () => { - const timestamp = String(Math.floor(Date.now() / 1000)); - const body = '{"type":"url_verification"}'; - - const valid = await verifySlackSignature(signingSecret, timestamp, body, "v0=deadbeef"); - expect(valid).toBe(false); - }); - - it("rejects a stale timestamp (>5 minutes old)", async () => { - const staleTimestamp = String(Math.floor(Date.now() / 1000) - 400); - const body = '{"type":"event_callback"}'; - const sig = await makeSlackSignature(signingSecret, staleTimestamp, body); - - const valid = await verifySlackSignature(signingSecret, staleTimestamp, body, sig); - expect(valid).toBe(false); - }); - - it("rejects a future timestamp (>5 minutes ahead)", async () => { - const futureTimestamp = String(Math.floor(Date.now() / 1000) + 400); - const body = '{"type":"event_callback"}'; - const sig = await makeSlackSignature(signingSecret, futureTimestamp, body); - - const valid = await verifySlackSignature(signingSecret, futureTimestamp, body, sig); - expect(valid).toBe(false); - }); - - it("rejects non-numeric timestamp", async () => { - const valid = await verifySlackSignature(signingSecret, "not-a-number", "{}", "v0=abc"); - expect(valid).toBe(false); - }); - - it("rejects wrong signing secret", async () => { - const timestamp = String(Math.floor(Date.now() / 1000)); - const body = '{"type":"url_verification"}'; - const sig = await makeSlackSignature("wrong_secret", timestamp, body); - - const valid = await verifySlackSignature(signingSecret, timestamp, body, sig); - expect(valid).toBe(false); - }); - - it("rejects signature with wrong length", async () => { - const timestamp = String(Math.floor(Date.now() / 1000)); - const body = "test"; - const valid = await verifySlackSignature(signingSecret, timestamp, body, "v0=ab"); - expect(valid).toBe(false); - }); -}); - -describe("end-to-end inbound flow", () => { - it("encrypts an event that only the server can decrypt", async () => { - // Setup: broker and server keypairs - const brokerSignKeypair = nacl.sign.keyPair(); - const serverBoxKeypair = nacl.box.keyPair(); - - // Simulate: broker encrypts a Slack event for the server - const slackEvent = { - type: "event_callback", - team_id: "T123", - event: { type: "app_mention", text: "<@U123> hello", channel: "C456", ts: "1234.5678" }, - }; - - const { sealedBoxEncrypt } = await import("../src/crypto/seal.js"); - const plaintext = encodeUTF8(JSON.stringify(slackEvent)); - const encrypted = await sealedBoxEncrypt(plaintext, serverBoxKeypair.publicKey); - - // Sign the envelope - const timestamp = Math.floor(Date.now() / 1000); - const canonical = canonicalizeEnvelope("T123", timestamp, encrypted); - const signature = sign(canonical, brokerSignKeypair.secretKey); - - // Server side: verify signature and decrypt - const isValid = verify(canonical, signature, brokerSignKeypair.publicKey); - expect(isValid).toBe(true); - - const decrypted = await sealedBoxDecrypt( - encrypted, - serverBoxKeypair.publicKey, - serverBoxKeypair.secretKey, - ); - - const recoveredEvent = JSON.parse(decodeUTF8(decrypted)); - expect(recoveredEvent.team_id).toBe("T123"); - expect(recoveredEvent.event.text).toBe("<@U123> hello"); - }); - - it("broker cannot decrypt its own sealed box output", async () => { - const brokerBoxKeypair = nacl.box.keyPair(); - const serverBoxKeypair = nacl.box.keyPair(); - - const { sealedBoxEncrypt } = await import("../src/crypto/seal.js"); - const plaintext = encodeUTF8("sensitive message"); - const encrypted = await sealedBoxEncrypt(plaintext, serverBoxKeypair.publicKey); - - // Broker tries to decrypt with its OWN keys β€” should fail - await expect( - sealedBoxDecrypt(encrypted, brokerBoxKeypair.publicKey, brokerBoxKeypair.secretKey), - ).rejects.toThrow("decryption failed"); - }); -}); - -describe("end-to-end outbound flow", () => { - it("server encrypts a reply that the broker can decrypt", () => { - // Setup - const brokerBoxKeypair = nacl.box.keyPair(); - const serverBoxKeypair = nacl.box.keyPair(); - const serverSignKeypair = nacl.sign.keyPair(); - - // Server encrypts the message body - const messageBody = JSON.stringify({ text: "Here's your answer!", blocks: [] }); - const { ciphertext, nonce } = boxEncrypt( - encodeUTF8(messageBody), - brokerBoxKeypair.publicKey, - serverBoxKeypair.secretKey, - ); - - // Server signs the request - const timestamp = Math.floor(Date.now() / 1000); - const canonical = canonicalizeOutbound("T123", "chat.postMessage", timestamp, ciphertext); - const signature = sign(canonical, serverSignKeypair.secretKey); - - // Broker side: verify signature - const isValid = verify(canonical, signature, serverSignKeypair.publicKey); - expect(isValid).toBe(true); - - // Broker decrypts the body - const decryptedBytes = boxDecrypt( - ciphertext, - nonce, - serverBoxKeypair.publicKey, - brokerBoxKeypair.secretKey, - ); - - const recovered = JSON.parse(decodeUTF8(decryptedBytes)); - expect(recovered.text).toBe("Here's your answer!"); - }); - - it("third party cannot forge a server message", () => { - const brokerBoxKeypair = nacl.box.keyPair(); - const serverBoxKeypair = nacl.box.keyPair(); - const serverSignKeypair = nacl.sign.keyPair(); - const attackerSignKeypair = nacl.sign.keyPair(); - - // Attacker tries to send a message signed with their own key - const body = JSON.stringify({ text: "malicious" }); - const { ciphertext, nonce } = boxEncrypt( - encodeUTF8(body), - brokerBoxKeypair.publicKey, - serverBoxKeypair.secretKey, - ); - - const timestamp = Math.floor(Date.now() / 1000); - const canonical = canonicalizeOutbound("T123", "chat.postMessage", timestamp, ciphertext); - - // Attacker signs with their key - const attackerSig = sign(canonical, attackerSignKeypair.secretKey); - - // Broker verifies against the REAL server's signing key β€” should fail - const isValid = verify(canonical, attackerSig, serverSignKeypair.publicKey); - expect(isValid).toBe(false); - }); -}); - -describe("end-to-end registration flow", () => { - let kv: KVNamespace; - - beforeEach(() => { - kv = createMockKV(); - }); - - it("full OAuth β†’ register β†’ activate cycle", async () => { - // 1. OAuth completes β€” create pending workspace - const authCode = "secret-auth-code-12345"; - const authCodeHashed = await hashAuthCode(authCode, TEST_BROKER_KEY); - - await createPendingWorkspace(kv, "T123", "Test Team", "xoxb-fake-token", authCodeHashed, TEST_BROKER_KEY); - - // Verify pending state - let ws = await getWorkspace(kv, "T123"); - expect(ws!.status).toBe("pending"); - expect(ws!.server_url).toBe(""); - - // Verify bot token is encrypted (not plaintext) - expect(ws!.bot_token).not.toBe("xoxb-fake-token"); - expect(decryptBotToken(ws!.bot_token, TEST_BROKER_KEY)).toBe("xoxb-fake-token"); - - // 2. Server generates keys and registers - const serverBoxKeypair = nacl.box.keyPair(); - const serverSignKeypair = nacl.sign.keyPair(); - - // Verify auth code matches - const providedHash = await hashAuthCode(authCode, TEST_BROKER_KEY); - expect(providedHash).toBe(ws!.auth_code_hash); - - // 3. Activate - const activated = await activateWorkspace( - kv, - "T123", - "https://my-server.example.com/broker/inbound", - encodeBase64(serverBoxKeypair.publicKey), - encodeBase64(serverSignKeypair.publicKey), - ); - - expect(activated!.status).toBe("active"); - expect(activated!.server_url).toBe("https://my-server.example.com/broker/inbound"); - // Auth code hash should be cleared after activation - expect(activated!.auth_code_hash).toBe(""); - - // 4. Verify the stored keys can be used for crypto - ws = await getWorkspace(kv, "T123"); - const storedPubkey = decodeBase64(ws!.server_pubkey); - expect(storedPubkey).toEqual(serverBoxKeypair.publicKey); - - // 5. Re-registration should be rejected (workspace is active) - const reactivated = await activateWorkspace( - kv, "T123", "https://evil.example.com", "pk", "spk", - ); - expect(reactivated).toBeNull(); - }); - - it("rejects registration with wrong auth code", async () => { - const authCode = "correct-code"; - const authCodeHashed = await hashAuthCode(authCode, TEST_BROKER_KEY); - - await createPendingWorkspace(kv, "T123", "Test Team", "xoxb-fake", authCodeHashed, TEST_BROKER_KEY); - - const wrongHash = await hashAuthCode("wrong-code", TEST_BROKER_KEY); - const ws = await getWorkspace(kv, "T123"); - expect(wrongHash).not.toBe(ws!.auth_code_hash); - }); -}); - -describe("replay protection", () => { - it("signature with old timestamp fails verification at application level", () => { - // Signatures themselves don't expire, but the broker checks timestamps - // before calling verify(). This test documents the canonicalization - // includes the timestamp, so replaying with a different timestamp - // will produce a different canonical form and fail verification. - const keypair = nacl.sign.keyPair(); - const timestamp1 = 1000; - const timestamp2 = 2000; - - const canonical1 = canonicalizeEnvelope("T123", timestamp1, "encrypted_data"); - const canonical2 = canonicalizeEnvelope("T123", timestamp2, "encrypted_data"); - - const sig1 = sign(canonical1, keypair.secretKey); - - // Signature from timestamp1 doesn't verify against canonical2 - expect(verify(canonical2, sig1, keypair.publicKey)).toBe(false); - }); -}); diff --git a/slack-broker/test/routing.test.ts b/slack-broker/test/routing.test.ts deleted file mode 100644 index 4a403ce..0000000 --- a/slack-broker/test/routing.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -/** - * Unit tests for registry (KV-backed workspace routing) and forwarding logic. - */ - -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { - getWorkspace, - putWorkspace, - createPendingWorkspace, - activateWorkspace, - deactivateWorkspace, - hashAuthCode, - encryptBotToken, - decryptBotToken, - type WorkspaceRecord, - type KVNamespace, -} from "../src/routing/registry.js"; -import { forwardEvent, type ForwardResult } from "../src/routing/forward.js"; -import nacl from "tweetnacl"; -import { encodeBase64 } from "../src/util/encoding.js"; - -/** Test broker key (base64-encoded 32-byte key). */ -const TEST_BROKER_KEY = encodeBase64(nacl.randomBytes(32)); - -/** In-memory KV mock. */ -function createMockKV(): KVNamespace { - const store = new Map(); - return { - async get(key: string) { - return store.get(key) ?? null; - }, - async put(key: string, value: string) { - store.set(key, value); - }, - async delete(key: string) { - store.delete(key); - }, - }; -} - -describe("registry", () => { - let kv: KVNamespace; - - beforeEach(() => { - kv = createMockKV(); - }); - - it("returns null for unknown workspace", async () => { - const result = await getWorkspace(kv, "T_UNKNOWN"); - expect(result).toBeNull(); - }); - - it("creates a pending workspace with encrypted bot token", async () => { - const record = await createPendingWorkspace(kv, "T123", "Test Team", "xoxb-token", "hash123", TEST_BROKER_KEY); - - expect(record.workspace_id).toBe("T123"); - expect(record.team_name).toBe("Test Team"); - // bot_token is now encrypted β€” should NOT be plaintext - expect(record.bot_token).not.toBe("xoxb-token"); - // Decrypting should yield the original - expect(decryptBotToken(record.bot_token, TEST_BROKER_KEY)).toBe("xoxb-token"); - expect(record.status).toBe("pending"); - expect(record.auth_code_hash).toBe("hash123"); - expect(record.server_url).toBe(""); - expect(record.server_pubkey).toBe(""); - - // Verify it's stored in KV - const fetched = await getWorkspace(kv, "T123"); - expect(fetched).toEqual(record); - }); - - it("activates a workspace with server details and clears auth_code_hash", async () => { - await createPendingWorkspace(kv, "T123", "Test Team", "xoxb-token", "hash123", TEST_BROKER_KEY); - - const activated = await activateWorkspace( - kv, - "T123", - "https://server.example.com/broker/inbound", - "server_pubkey_base64", - "server_signing_pubkey_base64", - ); - - expect(activated).not.toBeNull(); - expect(activated!.status).toBe("active"); - expect(activated!.server_url).toBe("https://server.example.com/broker/inbound"); - expect(activated!.server_pubkey).toBe("server_pubkey_base64"); - expect(activated!.server_signing_pubkey).toBe("server_signing_pubkey_base64"); - // Auth code hash should be cleared after activation - expect(activated!.auth_code_hash).toBe(""); - }); - - it("rejects activation of already-active workspace", async () => { - await createPendingWorkspace(kv, "T123", "Test Team", "xoxb-token", "hash123", TEST_BROKER_KEY); - await activateWorkspace(kv, "T123", "https://server1.example.com", "pk1", "spk1"); - - // Second activation should fail - const result = await activateWorkspace(kv, "T123", "https://server2.example.com", "pk2", "spk2"); - expect(result).toBeNull(); - - // Original server should still be registered - const ws = await getWorkspace(kv, "T123"); - expect(ws!.server_url).toBe("https://server1.example.com"); - }); - - it("returns null when activating non-existent workspace", async () => { - const result = await activateWorkspace(kv, "T_NONE", "url", "pk", "spk"); - expect(result).toBeNull(); - }); - - it("deactivates a workspace", async () => { - await createPendingWorkspace(kv, "T123", "Test Team", "xoxb-token", "hash123", TEST_BROKER_KEY); - await activateWorkspace(kv, "T123", "https://server.example.com", "pk", "spk"); - - const result = await deactivateWorkspace(kv, "T123"); - expect(result).toBe(true); - - const workspace = await getWorkspace(kv, "T123"); - expect(workspace!.status).toBe("inactive"); - expect(workspace!.server_url).toBe(""); - expect(workspace!.server_pubkey).toBe(""); - }); - - it("returns false when deactivating non-existent workspace", async () => { - const result = await deactivateWorkspace(kv, "T_NONE"); - expect(result).toBe(false); - }); - - it("put and get workspace round-trip", async () => { - const record: WorkspaceRecord = { - workspace_id: "T999", - team_name: "Round Trip", - server_url: "https://example.com", - server_pubkey: "pk", - server_signing_pubkey: "spk", - bot_token: "xoxb-test", - status: "active", - updated_at: new Date().toISOString(), - auth_code_hash: "abc", - }; - - await putWorkspace(kv, record); - const fetched = await getWorkspace(kv, "T999"); - expect(fetched).toEqual(record); - }); -}); - -describe("hashAuthCode (HMAC-SHA256)", () => { - it("produces a hex string", async () => { - const hash = await hashAuthCode("test-code", "broker-secret"); - expect(hash).toMatch(/^[0-9a-f]{64}$/); - }); - - it("is deterministic", async () => { - const h1 = await hashAuthCode("same-code", "broker-secret"); - const h2 = await hashAuthCode("same-code", "broker-secret"); - expect(h1).toBe(h2); - }); - - it("differs for different inputs", async () => { - const h1 = await hashAuthCode("code-a", "broker-secret"); - const h2 = await hashAuthCode("code-b", "broker-secret"); - expect(h1).not.toBe(h2); - }); - - it("differs for different secrets (keyed)", async () => { - const h1 = await hashAuthCode("same-code", "secret-1"); - const h2 = await hashAuthCode("same-code", "secret-2"); - expect(h1).not.toBe(h2); - }); -}); - -describe("workspace_id validation", () => { - it("accepts valid Slack team IDs", () => { - expect(/^T[A-Z0-9]+$/.test("T09192W1Z34")).toBe(true); - expect(/^T[A-Z0-9]+$/.test("T123")).toBe(true); - expect(/^T[A-Z0-9]+$/.test("TABCDEF012")).toBe(true); - }); - - it("rejects IDs with pipe delimiter (injection)", () => { - expect(/^T[A-Z0-9]+$/.test("T123|evil")).toBe(false); - }); - - it("rejects IDs that don't start with T", () => { - expect(/^T[A-Z0-9]+$/.test("U123")).toBe(false); - expect(/^T[A-Z0-9]+$/.test("123")).toBe(false); - }); - - it("rejects empty or T-only IDs", () => { - expect(/^T[A-Z0-9]+$/.test("")).toBe(false); - expect(/^T[A-Z0-9]+$/.test("T")).toBe(false); - }); -}); - -describe("bot token encryption", () => { - it("encrypts and decrypts a bot token", () => { - const token = "xoxb-1234567890-abcdefghij"; - const encrypted = encryptBotToken(token, TEST_BROKER_KEY); - expect(encrypted).not.toBe(token); - expect(decryptBotToken(encrypted, TEST_BROKER_KEY)).toBe(token); - }); - - it("different encryptions produce different ciphertexts (random nonce)", () => { - const token = "xoxb-same-token"; - const e1 = encryptBotToken(token, TEST_BROKER_KEY); - const e2 = encryptBotToken(token, TEST_BROKER_KEY); - expect(e1).not.toBe(e2); - // Both decrypt to the same value - expect(decryptBotToken(e1, TEST_BROKER_KEY)).toBe(token); - expect(decryptBotToken(e2, TEST_BROKER_KEY)).toBe(token); - }); - - it("fails to decrypt with wrong key", () => { - const token = "xoxb-secret"; - const wrongKey = encodeBase64(nacl.randomBytes(32)); - const encrypted = encryptBotToken(token, TEST_BROKER_KEY); - expect(() => decryptBotToken(encrypted, wrongKey)).toThrow("decryption failed"); - }); -}); - -describe("forwardEvent", () => { - const brokerSignKeypair = nacl.sign.keyPair(); - - function makeActiveWorkspace(overrides?: Partial): WorkspaceRecord { - const serverKeypair = nacl.box.keyPair(); - return { - workspace_id: "T123", - team_name: "Test", - server_url: "https://server.example.com/broker/inbound", - server_pubkey: encodeBase64(serverKeypair.publicKey), - server_signing_pubkey: "", - bot_token: "xoxb-test", - status: "active", - updated_at: new Date().toISOString(), - auth_code_hash: "hash", - ...overrides, - }; - } - - it("rejects forwarding to non-active workspace", async () => { - const workspace = makeActiveWorkspace({ status: "pending" }); - const result = await forwardEvent({}, workspace, brokerSignKeypair.secretKey); - expect(result.ok).toBe(false); - expect(result.error).toContain("not active"); - }); - - it("rejects workspace with missing server_url", async () => { - const workspace = makeActiveWorkspace({ server_url: "" }); - const result = await forwardEvent({}, workspace, brokerSignKeypair.secretKey); - expect(result.ok).toBe(false); - expect(result.error).toContain("missing server configuration"); - }); - - it("rejects workspace with missing server_pubkey", async () => { - const workspace = makeActiveWorkspace({ server_pubkey: "" }); - const result = await forwardEvent({}, workspace, brokerSignKeypair.secretKey); - expect(result.ok).toBe(false); - expect(result.error).toContain("missing server configuration"); - }); - - it("rejects non-HTTPS server URL", async () => { - const workspace = makeActiveWorkspace({ server_url: "http://server.example.com/inbound" }); - const result = await forwardEvent({}, workspace, brokerSignKeypair.secretKey); - expect(result.ok).toBe(false); - expect(result.error).toContain("HTTPS"); - }); - - it("forwards an event to a server (mocked fetch)", async () => { - const workspace = makeActiveWorkspace(); - const event = { type: "event_callback", event: { type: "message", text: "hi" } }; - - // Mock global fetch - const fetchSpy = vi.fn().mockResolvedValue(new Response("ok", { status: 200 })); - const originalFetch = globalThis.fetch; - globalThis.fetch = fetchSpy; - - try { - const result = await forwardEvent(event, workspace, brokerSignKeypair.secretKey); - expect(result.ok).toBe(true); - expect(result.status).toBe(200); - - // Verify fetch was called with the right URL - expect(fetchSpy).toHaveBeenCalledTimes(1); - const [url, options] = fetchSpy.mock.calls[0]!; - expect(url).toBe(workspace.server_url); - expect(options.method).toBe("POST"); - - // Verify envelope structure - const body = JSON.parse(options.body); - expect(body.workspace_id).toBe("T123"); - expect(body.encrypted).toBeTruthy(); - expect(body.timestamp).toBeTypeOf("number"); - expect(body.signature).toBeTruthy(); - - // Verify headers - expect(options.headers["Content-Type"]).toBe("application/json"); - expect(options.headers["X-Broker-Signature"]).toBeTruthy(); - expect(options.headers["X-Broker-Timestamp"]).toBeTruthy(); - } finally { - globalThis.fetch = originalFetch; - } - }); - - it("reports server error status", async () => { - const workspace = makeActiveWorkspace(); - - const fetchSpy = vi.fn().mockResolvedValue(new Response("bad", { status: 503 })); - const originalFetch = globalThis.fetch; - globalThis.fetch = fetchSpy; - - try { - const result = await forwardEvent({}, workspace, brokerSignKeypair.secretKey); - expect(result.ok).toBe(false); - expect(result.status).toBe(503); - expect(result.error).toContain("503"); - } finally { - globalThis.fetch = originalFetch; - } - }); - - it("handles network errors gracefully", async () => { - const workspace = makeActiveWorkspace(); - - const fetchSpy = vi.fn().mockRejectedValue(new Error("connection refused")); - const originalFetch = globalThis.fetch; - globalThis.fetch = fetchSpy; - - try { - const result = await forwardEvent({}, workspace, brokerSignKeypair.secretKey); - expect(result.ok).toBe(false); - expect(result.error).toContain("connection refused"); - } finally { - globalThis.fetch = originalFetch; - } - }); -}); diff --git a/slack-broker/tsconfig.json b/slack-broker/tsconfig.json deleted file mode 100644 index a7f4941..0000000 --- a/slack-broker/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "lib": ["ES2022"], - "types": ["@cloudflare/workers-types"], - "strict": true, - "noUncheckedIndexedAccess": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "test"] -} diff --git a/slack-broker/vitest.config.ts b/slack-broker/vitest.config.ts deleted file mode 100644 index 570df33..0000000 --- a/slack-broker/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - environment: "node", - include: ["test/**/*.test.ts"], - }, -}); diff --git a/slack-broker/wrangler.toml b/slack-broker/wrangler.toml deleted file mode 100644 index a647329..0000000 --- a/slack-broker/wrangler.toml +++ /dev/null @@ -1,32 +0,0 @@ -name = "slack-broker" -main = "src/index.ts" -compatibility_date = "2024-12-30" - -# KV Namespaces β€” create with: -# wrangler kv namespace create WORKSPACE_ROUTING -# wrangler kv namespace create OAUTH_STATE -[[kv_namespaces]] -binding = "WORKSPACE_ROUTING" -id = "PLACEHOLDER_WORKSPACE_ROUTING_ID" - -[[kv_namespaces]] -binding = "OAUTH_STATE" -id = "PLACEHOLDER_OAUTH_STATE_ID" - -# Secrets β€” set with: -# wrangler secret put BROKER_PRIVATE_KEY -# wrangler secret put SLACK_CLIENT_ID -# wrangler secret put SLACK_CLIENT_SECRET -# wrangler secret put SLACK_SIGNING_SECRET - -# Dev overrides -[env.dev] -name = "slack-broker-dev" - -[[env.dev.kv_namespaces]] -binding = "WORKSPACE_ROUTING" -id = "PLACEHOLDER_DEV_WORKSPACE_ROUTING_ID" - -[[env.dev.kv_namespaces]] -binding = "OAUTH_STATE" -id = "PLACEHOLDER_DEV_OAUTH_STATE_ID"