diff --git a/.github/workflows/publish-flue-sdk.yml b/.github/workflows/publish-flue-sdk.yml new file mode 100644 index 000000000..4f5bc98fa --- /dev/null +++ b/.github/workflows/publish-flue-sdk.yml @@ -0,0 +1,65 @@ +name: Publish Flue SDK + +on: + push: + branches: [main] + paths: + - 'sdks/flue/**' + - '.github/workflows/publish-flue-sdk.yml' + workflow_dispatch: + +concurrency: + group: publish-flue-sdk + cancel-in-progress: false + +defaults: + run: + working-directory: sdks/flue + +jobs: + publish: + name: Test, build & publish to npm + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22.19' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Check if this exact version is already published + id: check + run: | + LOCAL_VERSION=$(node -p "require('./package.json').version") + if LOOKUP=$(npm view "@opencomputer/flue@$LOCAL_VERSION" version 2>&1); then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Version $LOCAL_VERSION is already published; skipping." + elif [[ "$LOOKUP" == *"E404"* ]]; then + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "Publishing version $LOCAL_VERSION." + else + echo "npm registry lookup failed; refusing to guess that the version is unpublished." >&2 + echo "$LOOKUP" >&2 + exit 1 + fi + + - name: Test + if: steps.check.outputs.skip == 'false' + run: npm test + + - name: Build + if: steps.check.outputs.skip == 'false' + run: npm run build + + - name: Publish + if: steps.check.outputs.skip == 'false' + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 000000000..77b8817bc --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,33 @@ +# Product + +## Register + +product + +## Users + +Developers and operators who deploy OpenComputer agents, start and steer durable sessions, and diagnose their lifecycle from the dashboard. They need to understand current state, queued work, output, and failures without learning framework-specific internals. + +## Product Purpose + +OpenComputer provides a direct operational surface for creating, controlling, and observing agents and their sessions. Success means an operator can act quickly, trust that the UI reflects durable platform state, and identify a failure without changing or redeploying production code. + +## Brand Personality + +Calm, direct, and technical. The product should feel dependable and precise while staying approachable to a developer using it for the first time. + +## Anti-references + +Avoid decorative AI imagery, conversational gimmicks, theatrical loading states, ornamental motion, and framework-specific terminology in shared product concepts. Do not invent novel controls where a familiar operator affordance is clearer. + +## Design Principles + +1. Show durable truth. Status, ordering, errors, and completion must follow the platform record rather than optimistic animation. +2. Keep the operator in flow. Common actions and state changes should be immediate, compact, and easy to scan. +3. Explain failures where they occur. Use actionable language and preserve enough context to diagnose the problem. +4. Keep framework details behind neutral product concepts unless the framework itself is the subject. +5. Preserve the existing interface vocabulary. Consistency and earned familiarity matter more than novelty. + +## Accessibility & Inclusion + +Meet WCAG AA for contrast and interaction. Preserve keyboard and screen-reader semantics, never rely on color alone for state, and provide reduced-motion behavior for every animation. diff --git a/cloudflare-workers/log-tail/src/index.test.ts b/cloudflare-workers/log-tail/src/index.test.ts new file mode 100644 index 000000000..e36803dbc --- /dev/null +++ b/cloudflare-workers/log-tail/src/index.test.ts @@ -0,0 +1,72 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import worker from "./index.ts"; + +const env = { + AXIOM_HOST: "https://axiom.test", + AXIOM_DATASET: "edge", + AXIOM_TOKEN: "test-token", +}; + +test("records a silent HTTP 5xx even when the invocation outcome is ok", async () => { + const originalFetch = globalThis.fetch; + let records: Array> = []; + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + records = JSON.parse(String(init?.body)); + return new Response(null, { status: 200 }); + }) as typeof fetch; + + try { + await worker.tail([{ + scriptName: "agt_test", + outcome: "ok", + eventTimestamp: Date.parse("2026-07-13T20:00:00Z"), + event: { + request: { + method: "POST", + url: "https://user:password@dispatch.test/agents/example/session?token=secret#fragment", + }, + response: { status: 500 }, + }, + logs: [], + exceptions: [], + }], env, {} as ExecutionContext); + } finally { + globalThis.fetch = originalFetch; + } + + assert.equal(records.length, 1); + assert.equal(records[0]?.level, "ERROR"); + assert.equal(records[0]?.msg, "worker request returned HTTP 500"); + assert.equal(records[0]?.service, "agt_test"); + assert.equal(records[0]?.response_status, 500); + assert.equal(records[0]?.request_url, "https://dispatch.test/agents/example/session"); + assert.equal(JSON.stringify(records[0]).includes("secret"), false); + assert.equal(JSON.stringify(records[0]).includes("password"), false); +}); + +test("fails the collector invocation when the durable sink rejects a batch", async () => { + const originalFetch = globalThis.fetch; + const originalConsoleError = console.error; + globalThis.fetch = (async () => new Response( + JSON.stringify({ code: 403, message: "not allowed to ingest into dataset" }), + { status: 403 }, + )) as typeof fetch; + console.error = () => {}; + + try { + await assert.rejects( + worker.tail([{ + scriptName: "agt_test", + outcome: "exception", + eventTimestamp: Date.parse("2026-07-13T20:00:00Z"), + logs: [], + exceptions: [], + }], env, {} as ExecutionContext), + /axiom ingest failed status=403/, + ); + } finally { + globalThis.fetch = originalFetch; + console.error = originalConsoleError; + } +}); diff --git a/cloudflare-workers/log-tail/src/index.ts b/cloudflare-workers/log-tail/src/index.ts index 6b1e1681f..bb5b93441 100644 --- a/cloudflare-workers/log-tail/src/index.ts +++ b/cloudflare-workers/log-tail/src/index.ts @@ -54,6 +54,22 @@ function safeStringify(v: unknown): string { catch { return String(v); } } +// Request query strings can carry browser client tokens (`?token=...`). Keep +// the route useful for diagnostics without copying credentials into Axiom. +function requestUrlForLogs(value?: string): string | undefined { + if (!value) return undefined; + try { + const url = new URL(value); + url.username = ""; + url.password = ""; + url.search = ""; + url.hash = ""; + return url.toString(); + } catch { + return undefined; + } +} + // Map CF console level → Axiom-friendly level string matching what our Go // slog handler emits ("INFO", "WARN", "ERROR", "DEBUG"). function level(l: TraceLog["level"]): string { @@ -76,7 +92,7 @@ export default { cell_id: "cf-edge", region: "global", outcome: item.outcome, - request_url: item.event?.request?.url, + request_url: requestUrlForLogs(item.event?.request?.url), request_method: item.event?.request?.method, response_status: item.event?.response?.status, cron: item.event?.cron, @@ -104,15 +120,20 @@ export default { }); } - // Synthetic record when a request ended in exception/exceededCpu/etc - // but had no console output — gives us a row to count "failed - // requests by script" in Axiom without joining streams. - if (item.outcome !== "ok" && item.logs.length === 0 && item.exceptions.length === 0) { + // Synthetic record when a request failed without logging. A Worker can + // catch an exception and return 5xx while Cloudflare still reports the + // invocation outcome as "ok", so response status is part of the failure + // contract too. This keeps silent server errors searchable in Axiom. + const responseStatus = item.event?.response?.status; + const serverError = typeof responseStatus === "number" && responseStatus >= 500; + if ((item.outcome !== "ok" || serverError) && item.logs.length === 0 && item.exceptions.length === 0) { records.push({ _time: new Date(item.eventTimestamp ?? Date.now()).toISOString(), time: new Date(item.eventTimestamp ?? Date.now()).toISOString(), level: "ERROR", - msg: `worker request ended with outcome=${item.outcome}`, + msg: serverError + ? `worker request returned HTTP ${responseStatus}` + : `worker request ended with outcome=${item.outcome}`, ...baseEnvelope, }); } @@ -131,7 +152,12 @@ export default { }); if (!resp.ok) { const body = await resp.text().catch(() => ""); - console.error(`axiom ingest failed status=${resp.status} body=${body.slice(0, 200)}`); + const message = `axiom ingest failed status=${resp.status} body=${body.slice(0, 200)}`; + console.error(message); + // The collector has Cloudflare Workers Logs enabled as an independent + // fallback. Throwing makes a broken durable sink an observable failed + // invocation instead of an apparently healthy, lossy success. + throw new Error(message); } }, }; diff --git a/cloudflare-workers/log-tail/wrangler.prod.toml b/cloudflare-workers/log-tail/wrangler.prod.toml index f89c09a0c..b133c5fb1 100644 --- a/cloudflare-workers/log-tail/wrangler.prod.toml +++ b/cloudflare-workers/log-tail/wrangler.prod.toml @@ -1,6 +1,6 @@ # log-tail Worker — PROD on Mo's CF account. # Tail consumer for opencomputer-edge-prod + opencomputer-events-ingest-prod. -# Forwards console output + exceptions to Axiom dataset cf-prod. +# Forwards console output + exceptions to Axiom dataset oc-platform-edge. # Deploy: `wrangler deploy -c wrangler.prod.toml` name = "opencomputer-log-tail-prod" @@ -9,8 +9,18 @@ compatibility_date = "2024-12-01" account_id = "b8f23cb87a7a6c64040d3134643da448" workers_dev = false +# Independent fallback for failures in the Axiom sink itself. Tenant/dispatch +# logs go to Axiom; collector invocation failures remain searchable in the +# Cloudflare Observability dashboard even when Axiom is unavailable. +[observability] +enabled = true + + [observability.logs] + invocation_logs = true + head_sampling_rate = 1 + # Secrets (set via `wrangler secret put --config wrangler.prod.toml`): -# AXIOM_TOKEN — xaat-... ingest token for the cf-prod dataset +# AXIOM_TOKEN — ingest-capable token scoped to the oc-platform-edge dataset [vars] AXIOM_HOST = "https://api.axiom.co" AXIOM_DATASET = "oc-platform-edge" diff --git a/cloudflare-workers/oc-gateway/.gitignore b/cloudflare-workers/oc-gateway/.gitignore new file mode 100644 index 000000000..42eb03400 --- /dev/null +++ b/cloudflare-workers/oc-gateway/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.dev.vars +.wrangler/ +dist/ diff --git a/cloudflare-workers/oc-gateway/README.md b/cloudflare-workers/oc-gateway/README.md new file mode 100644 index 000000000..1e89f6e84 --- /dev/null +++ b/cloudflare-workers/oc-gateway/README.md @@ -0,0 +1,148 @@ +# Agent model gateway + +The framework-neutral model gateway for hosted OpenComputer agent Workers. Flue is the first +adapter, but the permanent Worker and operator configuration are not framework-named. Contract and +rationale live in `oc-bg-agents` design 013 §4 and work item 022 W7-P. + +It **extends** the shipped managed-model path (does not replace it): org-level spend keeps flowing through the org's single OpenRouter inference key → the existing `model_meter` cron → Autumn (`opencomputer/cloudflare-workers/api-edge/src/{model_billing,model_meter,openrouter}.ts`, `token-billing.md`). The gateway only adds the injection point a CF Worker needs (it can't use the box secrets-proxy) plus **org+agt budget enforcement + best-effort per-session sub-metering**. It pushes **nothing** to Autumn. + +--- + +## Contract #1 — the Gateway HTTP contract + +### 1. Path shape + +An unmodified Flue app registers a managed provider at the gateway **inside `defineAgent`**: + +```ts +registerProvider('anthropic', { + baseUrl: `${env.OC_GATEWAY}/anthropic`, + apiKey: env.OC_SESSION_TOKEN, // the per-DEPLOY token (bound by W7 as an env var) + headers: { 'X-OC-Session': id }, // the DO's own init id = ses_… — best-effort attribution +}); +``` + +The provider client appends its native tail; the gateway strips the provider prefix and forwards to the OpenRouter base the box path already uses (`credential.ts` `MANAGED_ANTHROPIC_BASE = https://openrouter.ai/api`): + +| Gateway request | Forwarded to OpenRouter | +|---|---| +| `POST {gw}/anthropic/v1/messages` | `POST https://openrouter.ai/api/v1/messages` (Claude-Code path) | +| `POST {gw}/openai/chat/completions` | `POST https://openrouter.ai/api/v1/chat/completions` | +| `GET {gw}/healthz` | — (liveness) | + +Rule: `/{provider}/` → ` + `, query string preserved. `cloudflare/` is **out of scope** — `env.AI.run()` bypasses `fetch`, so the gateway can't meter it (design §4). + +### 2. The deploy token — per-DEPLOY, EdDSA, lease-fenced + +**Resolved token seam.** The token is **per-DEPLOY**, not per-session. Flue's `registerProvider` `apiKey` is a static string only, and its provider registry is isolate-global while CF co-locates many session-DOs of one agent's script in one isolate — so per-session data injected via `registerProvider` (the token OR the header) **races** across co-located sessions. Therefore the token carries only `(org, agt)` and the **hard cost-safety boundary is at the org+agt grain**. + +**Claims:** `{ org, agt, iat, exp, ep? }` — **no** `sub:session`, **no** `bud`. +- `org` — bare lowercase UUID from canonical owner `oc-org:`; selects the org's OpenRouter key. +- `agt` — canonical `^agt_[0-9a-f]{24}$` id for the deployed agent. +- `ep` — optional monotonic deploy epoch; a token below the current lease floor is fenced. + +**Prod hardening over the spike:** +- **EdDSA (Ed25519):** the minter (W7 deploy pipeline) holds the private key; the gateway holds only `GATEWAY_TOKEN_PUBLIC_KEY` — a compromised gateway can't forge tokens. Alg pinned (rejects `none`/HS256 swaps). +- **Lease-epoch fence** (`DeployLease` DO, per `${org}:${agt}`): the floor rises to a token's `ep` on first use, so a **rotated** deploy's higher-epoch token instantly supersedes older tokens (401 `token_superseded`). A **revoke without redeploy** is `POST /admin/lease/bump {org, agt, min_epoch}`. + +**Transport:** `Authorization: Bearer ` **or** `x-api-key: `. Verify = alg-pin + signature + `exp`/`iat` + exact bare-org/agent claim shapes. Failure → `401`. + +### 3. Enforcement grain (co-location refinement) + +- **HARD (the 402): org+agt.** `SpendCounter` DO keyed `agt:${org}:${agt}` — race-free (the value comes from the token, identical for every co-located session of the agent). `/check` gates **before** the call; over → `402 budget_exceeded`. Budget is looked up **server-side** (provisioned via `/admin/agent/budget`, else `AGENT_BUDGET_USD_DEFAULT`) — **never** carried in the token. +- **BEST-EFFORT per session: `X-OC-Session`.** `SpendCounter` DO keyed `sess:` — the gateway only **records** spend here for per-session visibility (dashboard W11). It is **never gated**, so a co-location race can't wrongly block a legitimate session. Exact per-session enforcement is deferred to an upstream Flue per-request resolver (tracked ask — see below). + +### 4. Request/response passthrough + +- **Request body:** buffered (model requests are small), `usage:{include:true}` injected so OpenRouter echoes cost, re-serialized. `cache_control` stripped for caching-unsafe models (§6). All other fields preserved. +- **Auth swap:** the tenant's `Authorization`/`x-api-key` **and** the `X-OC-Session` header are stripped; `Authorization: Bearer ` set. `http-referer`/`x-title` added for OR attribution. Everything else passes through. **No raw provider key ever reaches the tenant** — it holds only the deploy token; the OR key lives in the gateway. +- **Response:** OpenRouter's status, headers, body returned **untouched** — JSON or `text/event-stream`. + +### 5. Metering + reconciliation — one cost-source-of-truth + +- **On-path sub-meter:** `/check` gates (org+agt) before; `/add` commits cost after (off the response path via `waitUntil`), idempotent on the OpenRouter generation id. Recorded at the org+agt grain (authoritative for enforcement) **and** best-effort per session. +- **Cost source:** the `usage.cost` (USD) OpenRouter echoes per response (`cost.ts`; SSE terminal usage). Exact-cost fallback `GET /api/v1/generation?id=` (documented, unwired). +- **Reconciliation:** the gateway forwards through the **org's existing OR inference key**, so OR's per-key cumulative usage still captures Flue spend → `model_meter` cron → Autumn, **exactly as the brain-box path does today**. The gateway builds **no** billing path and pushes **nothing** to Autumn. Its counters are for **enforcement + display only**; they and OR's per-key usage are independent by design. +- **Budget refusal:** `402 {error:{type:"budget_exceeded", code:"insufficient_quota"}, oc:{org,agent,spent_usd,budget_usd}}` — a provider-style error so the Flue turn terminates and the tailer maps it to outcome `budget_exceeded` (§8). *The exact shape Flue surfaces cleanly is a live-verify item.* + +### 6. Org OpenRouter key resolution (from Infisical, via a sessions-api seam) + +The managed OR **inference** key's plaintext lives in **Infisical**, referenced by the org's managed credential and resolved only by `sessions-api` `resolveManagedSecret` (`credential.ts`). A CF Worker can't reach Infisical or the box secrets-proxy (design §4), so the gateway resolves it through a **dedicated internal sessions-api route** — mirroring the edge's dedicated-secret plaintext-key hand-off (`model_billing.ts §6.7.5`: a route carrying a live key gets its **own** secret, not the generic internal-auth one): + +``` +POST {GATEWAY_ORKEY_URL} Authorization: Bearer {GATEWAY_ORKEY_SECRET} body {"org": orgId} + → 200 {"key": "sk-or-..."} (resolveManagedSecret for the org's active managed credential) +``` + +The plaintext is cached per-org in-isolate with a 60 s TTL. There is no single-key test or production +override: missing seam configuration returns no key, and tests exercise the same org-scoped request. + +### 7. Prompt-caching safety + +Some models route (via OpenRouter) to a backend that rejects Anthropic `cache_control` breakpoints (`claude-3-haiku` → OR→Bedrock 400s; `claude-haiku-4.5` works). The gateway **strips `cache_control`** from the body for an env-extensible denylist (`CACHE_CONTROL_UNSAFE_MODELS`) so the call still completes; caching-safe models are untouched. + +### 8. Control-plane admin routes (guarded by `GATEWAY_ADMIN_SECRET`) + +- `POST /admin/agent/budget {org, agt, budget_usd|null}` — provision the org+agt hard cap (W1/W7 seam). +- `POST /admin/lease/bump {org, agt, min_epoch}` — revoke deploy tokens below `min_epoch` (no redeploy). + +--- + +## What's here + +| File | Role | +|---|---| +| `src/index.ts` | verify deploy token → lease fence → org+agt hard gate → org-key inject → forward → tee-meter → passthrough | +| `src/token.ts` | EdDSA per-deploy token mint/verify (Web Crypto, no deps) | +| `src/budget.ts` | `SpendCounter` DO — keyed spend counter + hard gate (µ$ integers); org+agt (hard) + per-session (tracked) | +| `src/deploylease.ts` | `DeployLease` DO — per-(org,agt) lease-epoch floor (rotation/revocation fence) | +| `src/orgkey.ts` | fail-closed org OR-key resolution via the dedicated sessions-api seam | +| `src/cost.ts` | per-response cost extraction (JSON + SSE) | +| `src/models.ts` | `cache_control` safety (strip for unsafe models) | +| `scripts/mint.ts` | mint a per-deploy token for live verification | +| `test/` | `logic` (25) + `integration` (11) — **36 green** | + +## Production deployment + +The permanent Worker identity is `oc-agent-gateway-prod`, exposed only at its Workers.dev URL. Its +fresh `SpendCounter` and `DeployLease` state is owned by that Worker. Production config fixes +`GATEWAY_ORKEY_URL` to `https://api.opencomputer.dev/internal/gateway/org-key`; it does not configure +`AGENT_BUDGET_USD_DEFAULT`. + +Default deploy fails intentionally. Production requires the explicit command: + +```bash +npm --prefix cloudflare-workers/oc-gateway run deploy:production +``` + +Set `GATEWAY_TOKEN_PUBLIC_KEY`, `GATEWAY_ORKEY_SECRET`, and `GATEWAY_ADMIN_SECRET` for the +`production` Wrangler environment one at a time. Never print their values. + +## Verification status + +- **In-process integration (green, CI-able):** `npx vitest run` drives the real worker handler + real `SpendCounter`/`DeployLease` DOs with `fetch` stubbed to a mock OpenRouter. Proves: 401 (no/expired/superseded token), forward with **org-key injection** (deploy token never reaches OR; session header never egresses) + `usage.include`, body passthrough, **org+agt hard enforcement** with bounded overshoot, **co-location** (two sessions share the org+agt cap), **per-session tracked-but-never-gated**, `cache_control` strip, admin provision. +- **Live turn:** one real `anthropic/*` turn through a local `wrangler dev` gateway → OpenRouter, against a **$1-capped throwaway** OR inference key minted from `OPENROUTER_PROVISIONING_KEY` and torn down after. + +### Live verification + +```bash +nvm use 22.19 +# 1. mint a $1-capped throwaway OR inference key + an Ed25519 keypair + a deploy token; write .dev.vars +# (helper reads OPENROUTER_PROVISIONING_KEY from a path arg — never sourced, never printed) +# 2. run the gateway locally (real DOs, real egress to openrouter.ai) +npx wrangler dev --port 8799 +# 3. one real anthropic turn THROUGH the gateway (unmodified-Flue shape) +curl -sN -X POST http://localhost:8799/anthropic/v1/messages \ + -H "authorization: Bearer $TOKEN" -H "x-oc-session: ses_live" -H 'content-type: application/json' \ + -d '{"model":"anthropic/claude-haiku-4.5","max_tokens":64,"messages":[{"role":"user","content":"say hi"}]}' +# → a real completion; OR bills the throwaway key; a low AGENT_BUDGET_USD_DEFAULT makes the 2nd call 402. +# 4. tear down: DELETE the OR key (by hash) and stop wrangler. +``` + +**Acceptance (buildout W3):** a real turn completes gateway → OpenRouter; the deploy token verifies (org+agt) and yields **no raw provider key** to the tenant; the org+agt budget refuses on-path (402); per-session spend is tracked by `X-OC-Session`. Org spend stays on the existing OpenRouter→Autumn cron. + +## Control-plane seams + +1. **Org OR-key route:** sessions-api exposes `POST {GATEWAY_ORKEY_URL}` with a dedicated bearer and returns `{key}` from the org's active managed credential. +2. **Per-agent budget provisioning (optional):** if a per-(org,agt) cap other than `AGENT_BUDGET_USD_DEFAULT` is wanted, W1/W7 calls `POST /admin/agent/budget`. +3. **Lease epoch (`ep`) minting:** W7 should mint a monotonic per-(org,agt) `ep` into the deploy token so rotation auto-fences; a leaked token is revoked via `POST /admin/lease/bump`. diff --git a/cloudflare-workers/oc-gateway/package-lock.json b/cloudflare-workers/oc-gateway/package-lock.json new file mode 100644 index 000000000..70d7aa151 --- /dev/null +++ b/cloudflare-workers/oc-gateway/package-lock.json @@ -0,0 +1,2913 @@ +{ + "name": "oc-agent-gateway", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "oc-agent-gateway", + "version": "0.1.0", + "devDependencies": { + "@cloudflare/workers-types": "^4.20240924.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0", + "wrangler": "^4.92.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260701.1.tgz", + "integrity": "sha512-Zd9Y1bah6DwwBN2RW8vJohffQrIUazb8UXnqSNecOxM+jJLhUuvv5IOG8dbHcV83TyZAubea6gsQXo2yH1lDdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260701.1.tgz", + "integrity": "sha512-yBLsjS1qCWqFyCY37qRUrYfzHHvMGvjh8zRKJ6MvUivYDhkZTzqduppK38FoqYvayLJ5KbcxH7zo5rkxGqbsaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260701.1.tgz", + "integrity": "sha512-vMfqSIMfoo4xmZXEuUVqLpSFS921YKjiR9q7kDXPi6Vld1PK74UHg9LZuBavT2KSyemHUCTpj9y/4JSYOEyQbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260701.1.tgz", + "integrity": "sha512-HRfwbKU2pK44V2NhoM0+iH0JJSj7nQ9Wv13ifIiGYCmTtDL8/zKtEhX7kQ3D4Vy/Cpjhttl0FkfqXj1aqLDPPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260701.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260701.1.tgz", + "integrity": "sha512-ngxCiIN9s/fM2o1IBMD0o1/mcXrv2NJVdyznh51UH8sQuvrTrXvV2nM0Uj/qU2wMwF6prgNBcdcd7AZeZGiBQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260702.1.tgz", + "integrity": "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==", + "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.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@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/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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/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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "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/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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/miniflare": { + "version": "4.20260701.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260701.0.tgz", + "integrity": "sha512-L6eAAi6IKtyb/7J6L+YsH2vb1yBrJWKRXI293JYDiMl70+6nncdAgigex58w6WBd+CwvdMsqOyNyGs95Op5gWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.28.0", + "workerd": "1.20260701.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/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.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/unenv/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/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.20260701.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260701.1.tgz", + "integrity": "sha512-uF813NG09JwNRRUfJ0zBomyTslSPM810dMj9LVvkQ7RAkLrQLzAlPU8Xh/3dIqZDo2bfd7tChbf2PtqLRARRJQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260701.1", + "@cloudflare/workerd-darwin-arm64": "1.20260701.1", + "@cloudflare/workerd-linux-64": "1.20260701.1", + "@cloudflare/workerd-linux-arm64": "1.20260701.1", + "@cloudflare/workerd-windows-64": "1.20260701.1" + } + }, + "node_modules/wrangler": { + "version": "4.107.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.107.0.tgz", + "integrity": "sha512-fw69ThymNitZ0oIEBU2yNeq3kK59UKz/jyA3udwRrQIAIsxX57q5qLOpPTN7qc5t8n9pnUeofe0uxtMuhQZW8w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "4.20260701.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260701.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260701.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/cloudflare-workers/oc-gateway/package.json b/cloudflare-workers/oc-gateway/package.json new file mode 100644 index 000000000..64cf60c33 --- /dev/null +++ b/cloudflare-workers/oc-gateway/package.json @@ -0,0 +1,20 @@ +{ + "name": "oc-agent-gateway", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "node -e \"throw new Error('choose an explicit environment; production is deploy:production')\"", + "deploy:production": "wrangler deploy --env production", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "mint": "node --experimental-strip-types scripts/mint.ts" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240924.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0", + "wrangler": "^4.92.0" + } +} diff --git a/cloudflare-workers/oc-gateway/scripts/mint.ts b/cloudflare-workers/oc-gateway/scripts/mint.ts new file mode 100644 index 000000000..899c51eb0 --- /dev/null +++ b/cloudflare-workers/oc-gateway/scripts/mint.ts @@ -0,0 +1,54 @@ +// Mint a per-DEPLOY gateway token (EdDSA). The control plane / deploy pipeline (W7) mints these in +// prod, binding the token as the tenant script's OC_SESSION_TOKEN env var; this is the dev/e2e helper. +// On first use it also generates an Ed25519 keypair. +// +// Generate + mint (the exact CP + gateway provisioning values on stderr; token on stdout): +// node --experimental-strip-types scripts/mint.ts --org 11111111-1111-4111-8111-111111111111 --agent agt_0123456789abcdef01234567 --ep 1 +// → set the gateway's GATEWAY_TOKEN_PUBLIC_KEY secret from the printed value. +// Reuse the control-plane private value so the gateway public key stays fixed: +// V3_GATEWAY_TOKEN_PRIVATE_KEY= node ... scripts/mint.ts ... + +import { mintDeployToken, type DeployClaims } from "../src/token.ts"; + +const b64url = (buf: ArrayBuffer) => Buffer.from(buf).toString("base64url"); +const pemFromDer = (der: ArrayBuffer) => { + const body = Buffer.from(der).toString("base64").match(/.{1,64}/g)?.join("\n") ?? ""; + return `-----BEGIN PRIVATE KEY-----\n${body}\n-----END PRIVATE KEY-----\n`; +}; +const derFromPemB64 = (value: string) => { + const pem = Buffer.from(value, "base64").toString("utf8"); + return Buffer.from(pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, ""), "base64"); +}; + +function arg(name: string, def?: string): string | undefined { + const i = process.argv.indexOf(`--${name}`); + return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : def; +} + +const ED = { name: "Ed25519" } as const; + +let privateKey: CryptoKey; +const existing = process.env.V3_GATEWAY_TOKEN_PRIVATE_KEY; +if (existing) { + privateKey = await crypto.subtle.importKey("pkcs8", derFromPemB64(existing), ED, true, ["sign"]); +} else { + const kp = (await crypto.subtle.generateKey(ED, true, ["sign", "verify"])) as CryptoKeyPair; + privateKey = kp.privateKey; + const publicValue = b64url(await crypto.subtle.exportKey("raw", kp.publicKey)); + const privateValue = Buffer.from(pemFromDer(await crypto.subtle.exportKey("pkcs8", kp.privateKey)), "utf8").toString("base64"); + console.error(`V3_GATEWAY_TOKEN_PRIVATE_KEY=${privateValue}`); + console.error(`V3_GATEWAY_TOKEN_PUBLIC_KEY=${publicValue}`); + console.error(`GATEWAY_TOKEN_PUBLIC_KEY=${publicValue}`); +} + +const now = Math.floor(Date.now() / 1000); +const ttl = Number(arg("ttl", "3600")); +const ep = arg("ep"); +const claims: DeployClaims = { + org: arg("org", "11111111-1111-4111-8111-111111111111")!, + agt: arg("agent", "agt_0123456789abcdef01234567")!, + ep: ep != null ? Number(ep) : undefined, + iat: now, + exp: now + ttl, +}; +console.log(await mintDeployToken(privateKey, claims)); diff --git a/cloudflare-workers/oc-gateway/src/budget.ts b/cloudflare-workers/oc-gateway/src/budget.ts new file mode 100644 index 000000000..53afc5e33 --- /dev/null +++ b/cloudflare-workers/oc-gateway/src/budget.ts @@ -0,0 +1,105 @@ +// SpendCounter — a strongly-consistent keyed spend counter + optional hard gate (design 013 §4/§8). +// +// GRAINS (resolved token seam + co-location refinement 2026-07-05). Flue's provider registry is +// isolate-global and CF co-locates many session-DOs of one agent's script in one isolate, so ANY +// per-session data injected via registerProvider (the static token OR an X-OC-Session header) races +// across co-located sessions. Therefore the gateway uses this counter at TWO grains: +// • HARD (cost-safety boundary): keyed `${org}:${agt}` — race-free because the per-DEPLOY token +// carries org+agt and nothing per-session. The gateway /check-gates here and returns 402 when over. +// This matches today's org-level OpenRouter→Autumn model; cost-safety is unchanged. +// • TRACKING (best-effort): keyed by the X-OC-Session header — the gateway only /add-records here for +// per-session visibility (dashboard W11) + soft budgets. It NEVER /check-gates a session instance, +// so a co-location race can never wrongly 402 a legitimate session. Exact per-session enforcement +// is deferred to an upstream Flue per-request resolver (tracked upstream ask, off the critical path). +// +// The budget is looked up SERVER-SIDE here — NEVER carried in the token. It is set by the control plane +// (POST /provision) or falls back to the gateway's configured default on first /check. +// +// WHY A DO (not KV): enforcement must be read-then-write consistent — concurrent calls (subagents, +// parallel tools) racing on KV would double-spend past the cap. A DO serializes /check and /add. +// +// This counter is for ENFORCEMENT + display ONLY — NOT a billing source. Org-level spend stays on the +// org's single OpenRouter inference key → the existing model_meter cron → Autumn (one cost-source-of- +// truth). Money is integer MICRODOLLARS (µ$, 1e-6 USD) to avoid float drift, matching model_meter. + +interface State { + spent_micro: number; + budget_micro: number | null; // null = uncapped + provisioned: boolean; // true once the control plane set an explicit cap (default no longer applies) + calls: number; + updated: number; +} + +export class SpendCounter { + private state: DurableObjectState; + constructor(state: DurableObjectState) { + this.state = state; + } + + private async load(): Promise { + const s = await this.state.storage.get("s"); + return s ?? { spent_micro: 0, budget_micro: null, provisioned: false, calls: 0, updated: 0 }; + } + + async fetch(req: Request): Promise { + const url = new URL(req.url); + const body = req.method === "POST" ? ((await req.json().catch(() => ({}))) as Record) : {}; + + // POST /provision {budget_micro} — the control plane sets this grain's explicit cap (W1/W7 seam). + // `budget_micro: null` = uncapped. Marks it provisioned so the gateway default is ignored. + if (url.pathname === "/provision") { + const s = await this.load(); + if (typeof body.budget_micro === "number") s.budget_micro = Math.max(0, Math.round(body.budget_micro)); + else if (body.budget_micro === null) s.budget_micro = null; + s.provisioned = true; + s.updated = Date.now(); + await this.state.storage.put("s", s); + return Response.json({ ok: true, budget_micro: s.budget_micro }); + } + + // POST /check {default_budget_micro?} → GATE a NEW call (used ONLY at the hard org+agt grain). On + // first sight of an unprovisioned key, adopt the gateway's default cap (server-side; never from the + // token). Allowed while spent < budget. Because /add lands after each response, the real overshoot + // bound is budget + the sum of every call concurrently in flight at the check boundary (not one + // call). The org's OpenRouter key cap remains the hard monetary ceiling. Runs before the call and + // is DO-serialized. + if (url.pathname === "/check") { + const s = await this.load(); + if (!s.provisioned && typeof body.default_budget_micro === "number") { + s.budget_micro = Math.max(0, Math.round(body.default_budget_micro)); + } + const allowed = s.budget_micro == null || s.spent_micro < s.budget_micro; + s.updated = Date.now(); + await this.state.storage.put("s", s); + return Response.json({ allowed, spent_micro: s.spent_micro, budget_micro: s.budget_micro }); + } + + // POST /add {cost_micro, idem} → commit a completed call's cost (from waitUntil, after the response). + // Called at BOTH grains. Idempotent on the caller's key (an OpenRouter generation id) so a retried + // meter never double-counts. Each DO instance has its own idem namespace, so the same generation id + // recorded at the org+agt grain and the session grain does not collide. + if (url.pathname === "/add") { + const s = await this.load(); + const costMicro = typeof body.cost_micro === "number" ? Math.max(0, Math.round(body.cost_micro)) : 0; + const idem = typeof body.idem === "string" ? body.idem : null; + if (idem) { + const seen = await this.state.storage.get(`idem:${idem}`); + if (seen) return Response.json({ spent_micro: s.spent_micro, deduped: true }); + await this.state.storage.put(`idem:${idem}`, true); + } + s.spent_micro += costMicro; + s.calls += 1; + s.updated = Date.now(); + await this.state.storage.put("s", s); + return Response.json({ spent_micro: s.spent_micro, calls: s.calls }); + } + + // GET /state → spend at this grain (dashboard, §9). + if (url.pathname === "/state") { + const s = await this.load(); + return Response.json(s); + } + + return new Response("not found", { status: 404 }); + } +} diff --git a/cloudflare-workers/oc-gateway/src/cost.ts b/cloudflare-workers/oc-gateway/src/cost.ts new file mode 100644 index 000000000..6e03cb664 --- /dev/null +++ b/cloudflare-workers/oc-gateway/src/cost.ts @@ -0,0 +1,66 @@ +// Per-response cost extraction — the on-path meter's input (design 013 §4, token-billing §9.7). +// +// OpenRouter echoes cost on both the Anthropic Messages and OpenAI paths (verified live 2026-06-29, +// token-billing.md §9.7): a `usage` object carries a `cost` field in USD when usage accounting is +// on. We inject `usage:{include:true}` into the request (openrouter.ts precedent) so the field is +// present, then read it here. Fallbacks, in order: `usage.cost` → `usage.total_cost` → null (we log +// and count 0, flagged — never guess a price). The AUTHORITATIVE org-level cost stays OpenRouter's +// per-key cumulative usage (the cron); this is the fast on-path estimate for per-session enforcement. +// +// The OpenRouter generation id (`id` on the response) is returned too — it is the meter's idempotency +// key (so a retried /add never double-counts) AND the handle for the exact-cost fallback +// (GET /api/v1/generation?id=… — documented, not wired in the spike). + +export interface ExtractedCost { + costUsd: number | null; // null = cost not found in the echo (flagged; counted as 0) + generationId: string | null; + source: "usage.cost" | "usage.total_cost" | "none"; +} + +function readCost(usage: unknown): { usd: number; source: ExtractedCost["source"] } | null { + if (!usage || typeof usage !== "object") return null; + const u = usage as Record; + if (typeof u.cost === "number") return { usd: u.cost, source: "usage.cost" }; + if (typeof u.total_cost === "number") return { usd: u.total_cost, source: "usage.total_cost" }; + return null; +} + +/** Extract cost + generation id from a fully-buffered JSON response body. */ +export function costFromJson(bodyText: string): ExtractedCost { + let obj: Record; + try { obj = JSON.parse(bodyText); } catch { return { costUsd: null, generationId: null, source: "none" }; } + const id = typeof obj.id === "string" ? obj.id : null; + const hit = readCost(obj.usage); + return { costUsd: hit?.usd ?? null, generationId: id, source: hit?.source ?? "none" }; +} + +/** Extract cost + generation id from an SSE stream body (text/event-stream). Scans `data:` lines for + * the terminal usage — Anthropic emits `message_delta`/`message_stop` with usage; OR's cost rides the + * final usage. Reads the whole (already-teed) stream; the client copy is untouched. */ +export async function costFromStream(stream: ReadableStream): Promise { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let buf = ""; + let best: ExtractedCost = { costUsd: null, generationId: null, source: "none" }; + for (;;) { + const { value, done } = await reader.read(); + if (value) buf += decoder.decode(value, { stream: true }); + let nl: number; + while ((nl = buf.indexOf("\n")) >= 0) { + const line = buf.slice(0, nl).trim(); + buf = buf.slice(nl + 1); + if (!line.startsWith("data:")) continue; + const data = line.slice(5).trim(); + if (!data || data === "[DONE]") continue; + try { + const obj = JSON.parse(data) as Record; + if (typeof obj.id === "string" && !best.generationId) best.generationId = obj.id; + // usage can sit at the top level or under a delta (`message_delta.usage`). + const hit = readCost(obj.usage) ?? readCost((obj.message as Record | undefined)?.usage); + if (hit) best = { ...best, costUsd: hit.usd, source: hit.source }; + } catch { /* non-JSON keep-alive/comment line */ } + } + if (done) break; + } + return best; +} diff --git a/cloudflare-workers/oc-gateway/src/deploylease.ts b/cloudflare-workers/oc-gateway/src/deploylease.ts new file mode 100644 index 000000000..190b0c60f --- /dev/null +++ b/cloudflare-workers/oc-gateway/src/deploylease.ts @@ -0,0 +1,71 @@ +// DeployLease — the per-(org, agt) lease-epoch floor that fences a rotated or revoked deploy token +// (design 013 §4 / buildout W3 "lease-epoch fence"). Keyed by `${org}:${agt}`. +// +// The deploy token (token.ts) carries a monotonic `ep` (deploy epoch, minted by W7). This DO holds +// the current floor for an (org, agt): +// - GATE: a token whose `ep` is BELOW the floor is fenced (superseded). A token at/above the floor +// is admitted and RAISES the floor to its `ep` — so the moment a redeploy's higher-epoch token is +// first used, every still-in-flight older-epoch token stops verifying. This is the rotation fence, +// automatic, no control-plane action required. +// - BUMP: the control plane raises the floor explicitly to REVOKE without a redeploy (e.g. a leaked +// token): POST /bump {min_epoch} sets floor = max(floor, min_epoch). To revoke epoch E, bump to E+1. +// +// A DO (not KV) so the floor is read-then-write consistent under concurrent calls. A token with no +// `ep` skips the fence entirely (lenient — the fence is opt-in on the mint side). + +interface Lease { + floor: number; // highest deploy epoch admitted / bumped; a token with ep < floor is fenced + updated: number; +} + +export class DeployLease { + private state: DurableObjectState; + constructor(state: DurableObjectState) { + this.state = state; + } + + private async load(): Promise { + const l = await this.state.storage.get("l"); + return l ?? { floor: 0, updated: 0 }; + } + + async fetch(req: Request): Promise { + const url = new URL(req.url); + const body = req.method === "POST" ? ((await req.json().catch(() => ({}))) as Record) : {}; + + // POST /gate {ep?} → { ok, fenced, floor }. Fence ep < floor; adopt (raise floor) on ep >= floor. + if (url.pathname === "/gate") { + const l = await this.load(); + const ep = typeof body.ep === "number" ? body.ep : null; + // Rollout compatibility only while unprovisioned. Once a floor exists, omission fails closed. + if (ep == null) return Response.json({ ok: l.floor === 0, fenced: l.floor > 0, floor: l.floor, missing_epoch: l.floor > 0 }); + if (ep < l.floor) return Response.json({ ok: false, fenced: true, floor: l.floor }); + if (ep > l.floor) { + l.floor = ep; + l.updated = Date.now(); + await this.state.storage.put("l", l); + } + return Response.json({ ok: true, fenced: false, floor: l.floor }); + } + + // POST /bump {min_epoch} → raise the floor for an explicit revocation. floor = max(floor, min_epoch). + if (url.pathname === "/bump") { + const l = await this.load(); + const min = typeof body.min_epoch === "number" ? Math.round(body.min_epoch) : null; + if (min != null && min > l.floor) { + l.floor = min; + l.updated = Date.now(); + await this.state.storage.put("l", l); + } + return Response.json({ floor: l.floor }); + } + + // GET /state → the current floor (ops / dashboard). + if (url.pathname === "/state") { + const l = await this.load(); + return Response.json(l); + } + + return new Response("not found", { status: 404 }); + } +} diff --git a/cloudflare-workers/oc-gateway/src/index.ts b/cloudflare-workers/oc-gateway/src/index.ts new file mode 100644 index 000000000..12259dcfa --- /dev/null +++ b/cloudflare-workers/oc-gateway/src/index.ts @@ -0,0 +1,312 @@ +// oc-agent-gateway — the thin OC Worker over OpenRouter (design 013 §4, contract #1 / W3). +// +// An unmodified Flue app registers the managed provider INSIDE defineAgent (resolved token seam): +// registerProvider('anthropic', { +// baseUrl: `${env.OC_GATEWAY}/anthropic`, +// apiKey: env.OC_SESSION_TOKEN, // the per-DEPLOY token — authorizes (org, agt) only +// headers: { 'X-OC-Session': id }, // the DO's own init id = ses_… — BEST-EFFORT attribution +// }); +// +// COST-SAFETY GRAIN (co-location refinement 2026-07-05). Flue's provider registry is isolate-global and +// CF co-locates many session-DOs of one agent's script in one isolate, so per-session data injected via +// registerProvider (the token OR the X-OC-Session header) RACES across co-located sessions. Therefore: +// - HARD enforcement (the 402) is at the **org+agt** grain — carried by the per-DEPLOY token, so it is +// race-free. This matches today's org-level OpenRouter→Autumn model; cost-safety is unchanged. +// - The X-OC-Session header is **best-effort per-session attribution** — recorded for visibility only, +// never gated (a co-location race must not wrongly block a legitimate session). Exact per-session +// enforcement is deferred to an upstream Flue per-request resolver (tracked ask, off the critical path). +// +// On every model call the Worker: +// (a) verifies the per-DEPLOY EdDSA token → (org, agt, ep). No session id, no budget in the token. +// (b) fences a superseded lease epoch (DeployLease DO, per org+agt) — a rotated/revoked token stops. +// (c) HARD-gates the org+agt budget ON-PATH (SpendCounter DO, keyed `${org}:${agt}`; budget looked up +// server-side, never in the token — §8). Over → 402 budget_exceeded. +// (d) injects the ORG's OpenRouter inference key (resolved from the credential store; never exposed). +// (e) makes the body prompt-caching-safe, injects usage accounting, forwards to OpenRouter. +// (f) sub-meters the response cost at the org+agt grain (authoritative) AND best-effort per session +// (X-OC-Session), leaving ORG-level spend on that same OR key → the existing model_meter cron → +// Autumn (one cost-source-of-truth). The gateway pushes NOTHING to Autumn. + +import { verifyDeployToken } from "./token.js"; +import { costFromJson, costFromStream } from "./cost.js"; +import { resolveOrgKey } from "./orgkey.js"; +import { unsafeModelMatchers, modelNeedsCacheStrip, stripCacheControl } from "./models.js"; +export { SpendCounter } from "./budget.js"; +export { DeployLease } from "./deploylease.js"; + +export interface Env { + // base64url raw 32-byte Ed25519 PUBLIC key. The minter (control plane / W7) holds the private key. + GATEWAY_TOKEN_PUBLIC_KEY: string; + // Spend counter + gate. Used at org+agt grain (hard) and per-session grain (tracked-only). + SPEND_COUNTER: DurableObjectNamespace; + // Per-(org, agt) lease-epoch floor that fences rotated/revoked deploy tokens. + DEPLOY_LEASE: DurableObjectNamespace; + // Default HARD budget (USD) per org+agt, applied to an unprovisioned grain on first sight. Unset = uncapped. + AGENT_BUDGET_USD_DEFAULT?: string; + // Org OR-key seam (orgkey.ts): dedicated internal sessions-api route + its bearer secret. + GATEWAY_ORKEY_URL?: string; + GATEWAY_ORKEY_SECRET?: string; + // Bearer that guards the control-plane admin routes (/admin/*). Unset → admin routes 404. + GATEWAY_ADMIN_SECRET?: string; + // Override OpenRouter base for tests; default = prod. + OPENROUTER_BASE?: string; + // Extra comma-separated model patterns whose OR route rejects cache_control (models.ts). + CACHE_CONTROL_UNSAFE_MODELS?: string; + // Optional per-session spend telemetry sink (OC_INGEST). Best-effort; absent = skip. + OC_INGEST_URL?: string; + OC_INGEST_AUTH?: string; +} + +const OR_BASE_DEFAULT = "https://openrouter.ai/api"; // == credential.ts MANAGED_ANTHROPIC_BASE +const ORG_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; +const AGENT_ID = /^agt_[0-9a-f]{24}$/; + +// Map a gateway path prefix → the OpenRouter path prefix (credential.ts managed bases). +// /anthropic/v1/messages → https://openrouter.ai/api/v1/messages (Claude-Code path) +// /openai/chat/completions → https://openrouter.ai/api/v1/chat/completions +function forwardUrl(base: string, pathname: string): string | null { + if (pathname === "/anthropic" || pathname.startsWith("/anthropic/")) { + return base + pathname.slice("/anthropic".length); // base already ends /api + } + if (pathname === "/openai" || pathname.startsWith("/openai/")) { + return base + "/v1" + pathname.slice("/openai".length); + } + return null; +} + +function bearer(h: Headers): string | null { + const a = h.get("authorization"); + if (a && /^Bearer\s+/i.test(a)) return a.replace(/^Bearer\s+/i, "").trim(); + const x = h.get("x-api-key"); // Anthropic-style clients put the apiKey here + return x ? x.trim() : null; +} + +function timingSafeEqual(a: string, b: string): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); + return diff === 0; +} + +function validSessionId(value: string | null): string | null { + const id = value?.trim() ?? ""; + return id.length <= 128 && /^ses_[A-Za-z0-9_-]+$/.test(id) ? id : null; +} + +const json = (obj: unknown, status = 200) => + new Response(JSON.stringify(obj), { status, headers: { "content-type": "application/json" } }); + +export default { + async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(req.url); + + if (req.method === "GET" && url.pathname === "/healthz") { + return json({ status: "ok", service: "oc-agent-gateway" }); + } + + // Control-plane admin routes (provision an org+agt budget, revoke a deploy lease). Guarded by a + // dedicated bearer; absent secret → not exposed. These are the W1/W7 control-plane seams. + if (url.pathname.startsWith("/admin/")) { + return admin(req, env, url); + } + + const target = forwardUrl(env.OPENROUTER_BASE || OR_BASE_DEFAULT, url.pathname); + if (!target) return json({ error: { type: "not_found", message: "unknown gateway path" } }, 404); + if (req.method !== "POST") return json({ error: { type: "method_not_allowed" } }, 405); + + // (a) verify the per-DEPLOY token (EdDSA — gateway holds only the public key). + const token = bearer(req.headers); + if (!token) return json({ error: { type: "unauthorized", message: "missing deploy token" } }, 401); + const nowSec = Math.floor(Date.now() / 1000); + const v = await verifyDeployToken(env.GATEWAY_TOKEN_PUBLIC_KEY, token, nowSec); + if (!v.ok) return json({ error: { type: "unauthorized", message: `invalid deploy token: ${v.reason}` } }, 401); + const { org: orgId, agt: agentId, ep } = v.claims; + + // Best-effort per-session attribution — the header may be stale under co-location; used for + // tracking only, never gated. Absent → we simply skip the per-session record (the call proceeds). + const sessionId = validSessionId(req.headers.get("x-oc-session")); + + // (b) lease-epoch fence — a rotated/revoked deploy token stops verifying (per org+agt, DO-serialized). + const leaseStub = env.DEPLOY_LEASE.get(env.DEPLOY_LEASE.idFromName(`${orgId}:${agentId}`)); + const gate = await leaseStub + .fetch("https://do/gate", { method: "POST", body: JSON.stringify({ ep }) }) + .then((r) => r.json() as Promise<{ ok: boolean; fenced?: boolean; floor: number }>); + if (gate.fenced) { + return json({ error: { type: "unauthorized", message: "deploy token superseded (stale lease epoch)", code: "token_superseded" } }, 401); + } + + // (c) HARD budget gate at the org+agt grain — ON-PATH, before the model call (§8), DO-serialized, + // race-free (org+agt from the token). Budget looked up SERVER-SIDE (provisioned cap, else the + // gateway default); NEVER carried in the token. + const agentKey = `agt:${orgId}:${agentId}`; + const agentBudget = env.SPEND_COUNTER.get(env.SPEND_COUNTER.idFromName(agentKey)); + const defaultBudgetMicro = parseUsdMicro(env.AGENT_BUDGET_USD_DEFAULT); + const check = await agentBudget + .fetch("https://do/check", { method: "POST", body: JSON.stringify({ default_budget_micro: defaultBudgetMicro }) }) + .then((r) => r.json() as Promise<{ allowed: boolean; spent_micro: number; budget_micro: number | null }>); + if (!check.allowed) { + // Refuse past the org+agt budget. Shaped as a provider-style error so the Flue/pi-ai turn + // terminates and the tailer maps it to outcome `budget_exceeded` (§8). Exact shape = live-verify. + return json({ + error: { type: "budget_exceeded", message: "org/agent model budget exhausted", code: "insufficient_quota" }, + oc: { org: orgId, agent: agentId, spent_usd: check.spent_micro / 1e6, budget_usd: (check.budget_micro ?? 0) / 1e6 }, + }, 402); + } + + // (d) resolve the ORG's OpenRouter inference key (from the credential-store seam; never exposed). + const orKey = await resolveOrgKey(env, orgId, Date.now()); + if (!orKey) return json({ error: { type: "server_error", message: "no OpenRouter key resolved for org" } }, 500); + + // (e) rewrite the body: strip cache_control for caching-unsafe models, inject usage:{include:true} + // so OpenRouter echoes cost (openrouter.ts precedent). Model bodies are small; buffer is fine. + const rawBody = await req.text(); + let outBody = rawBody; + try { + const parsed = JSON.parse(rawBody) as Record; + if (modelNeedsCacheStrip(parsed.model, unsafeModelMatchers(env.CACHE_CONTROL_UNSAFE_MODELS))) { + stripCacheControl(parsed); + } + const usage = (parsed.usage && typeof parsed.usage === "object" ? parsed.usage : {}) as Record; + usage.include = true; + parsed.usage = usage; + outBody = JSON.stringify(parsed); + } catch { + /* not JSON — forward verbatim */ + } + + // forward to OpenRouter with the org key swapped in. Strip the tenant's auth + the session header; + // pass the rest. + const fwdHeaders = new Headers(req.headers); + fwdHeaders.delete("x-api-key"); + fwdHeaders.delete("authorization"); + fwdHeaders.delete("x-oc-session"); + fwdHeaders.set("authorization", `Bearer ${orKey}`); + fwdHeaders.set("content-type", req.headers.get("content-type") || "application/json"); + fwdHeaders.set("content-length", String(new TextEncoder().encode(outBody).length)); + // OpenRouter attribution/routing headers (non-secret): help the OR dashboard + rankings. + fwdHeaders.set("http-referer", "https://opencomputer.dev"); + fwdHeaders.set("x-title", "OpenComputer"); + + const forwardTarget = target + (url.search || ""); + const upstream = await fetch(forwardTarget, { method: "POST", headers: fwdHeaders, body: outBody }); + + // (f) sub-meter the response cost, off the response path (waitUntil): authoritative at the org+agt + // grain + best-effort per session (for the dashboard). Passing the same generation id to both + // grains is safe — each DO instance has its own idempotency namespace. + const isStream = (upstream.headers.get("content-type") || "").includes("text/event-stream"); + const meterCopy = upstream.clone(); + ctx.waitUntil( + meter(meterCopy, isStream, env, { + agentBudget, + sessionCounter: sessionId + ? env.SPEND_COUNTER.get(env.SPEND_COUNTER.idFromName(`sess:${orgId}:${agentId}:${sessionId}`)) + : null, + sessionId, orgId, agentId, + }), + ); + + // Passthrough: return OpenRouter's response (status + headers + body) untouched to Flue. + return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: upstream.headers }); + }, +}; + +/** Parse a USD string into integer µ$, or null (uncapped) if unset/invalid/≤0. */ +function parseUsdMicro(usd?: string): number | null { + if (!usd) return null; + const n = Number(usd); + if (!Number.isFinite(n) || n <= 0) return null; + return Math.round(n * 1e6); +} + +function parseAdminBudgetMicro(value: unknown): number | null | undefined { + if (value === null) return null; + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined; + const micro = Math.round(value * 1e6); + return Number.isSafeInteger(micro) ? micro : undefined; +} + +// ── Control-plane admin routes (guarded by GATEWAY_ADMIN_SECRET) ── +async function admin(req: Request, env: Env, url: URL): Promise { + if (!env.GATEWAY_ADMIN_SECRET) return json({ error: { type: "not_found" } }, 404); + const auth = bearer(req.headers); + if (!auth || !timingSafeEqual(auth, env.GATEWAY_ADMIN_SECRET)) return json({ error: { type: "unauthorized" } }, 401); + if (req.method !== "POST") return json({ error: { type: "method_not_allowed" } }, 405); + const body = (await req.json().catch(() => ({}))) as Record; + + // POST /admin/agent/budget {org, agt, budget_usd|null} — provision the org+agt HARD cap (W1/W7 seam). + if (url.pathname === "/admin/agent/budget") { + const org = typeof body.org === "string" ? body.org : null; + const agt = typeof body.agt === "string" ? body.agt : null; + if (!org || !agt || !ORG_ID.test(org) || !AGENT_ID.test(agt)) { + return json({ error: { type: "bad_request", message: "canonical bare org UUID and agent id required" } }, 400); + } + const budgetMicro = parseAdminBudgetMicro(body.budget_usd); + if (budgetMicro === undefined) { + return json({ error: { type: "bad_request", message: "budget_usd must be a non-negative finite number or null" } }, 400); + } + const stub = env.SPEND_COUNTER.get(env.SPEND_COUNTER.idFromName(`agt:${org}:${agt}`)); + const r = await stub.fetch("https://do/provision", { method: "POST", body: JSON.stringify({ budget_micro: budgetMicro }) }); + return new Response(r.body, { status: r.status, headers: { "content-type": "application/json" } }); + } + + // POST /admin/lease/bump {org, agt, min_epoch} — revoke deploy tokens below min_epoch (no redeploy). + if (url.pathname === "/admin/lease/bump") { + const org = typeof body.org === "string" ? body.org : null; + const agt = typeof body.agt === "string" ? body.agt : null; + const minEpoch = typeof body.min_epoch === "number" ? body.min_epoch : null; + if (!org || !agt || !ORG_ID.test(org) || !AGENT_ID.test(agt) || minEpoch == null) { + return json({ error: { type: "bad_request", message: "canonical bare org UUID, agent id and min_epoch required" } }, 400); + } + const stub = env.DEPLOY_LEASE.get(env.DEPLOY_LEASE.idFromName(`${org}:${agt}`)); + const r = await stub.fetch("https://do/bump", { method: "POST", body: JSON.stringify({ min_epoch: minEpoch }) }); + return new Response(r.body, { status: r.status, headers: { "content-type": "application/json" } }); + } + + return json({ error: { type: "not_found" } }, 404); +} + +async function meter( + resp: Response, + isStream: boolean, + env: Env, + ctx: { + agentBudget: DurableObjectStub; + sessionCounter: DurableObjectStub | null; + sessionId: string | null; + orgId: string; + agentId: string; + }, +): Promise { + try { + if (resp.status >= 400) return; // a failed provider call bills nothing + const extracted = isStream + ? await costFromStream(resp.body ?? new ReadableStream()) + : costFromJson(await resp.text()); + const costMicro = extracted.costUsd != null ? Math.round(extracted.costUsd * 1e6) : 0; + // Authoritative: the org+agt grain that gates. + await ctx.agentBudget.fetch("https://do/add", { + method: "POST", + body: JSON.stringify({ cost_micro: costMicro, idem: extracted.generationId }), + }); + // Best-effort per-session tracking (visibility only; never gated). + if (ctx.sessionCounter) { + await ctx.sessionCounter + .fetch("https://do/add", { method: "POST", body: JSON.stringify({ cost_micro: costMicro, idem: extracted.generationId }) }) + .catch(() => {}); + } + // Per-session spend telemetry (dashboard, §9) — best-effort; NOT a billing source. Absent → skip. + if (env.OC_INGEST_URL) { + await fetch(env.OC_INGEST_URL, { + method: "POST", + headers: { "content-type": "application/json", ...(env.OC_INGEST_AUTH ? { "x-internal-auth": env.OC_INGEST_AUTH } : {}) }, + body: JSON.stringify({ + name: "gateway.model_call", session: ctx.sessionId, org: ctx.orgId, agent: ctx.agentId, + cost_usd: extracted.costUsd, cost_source: extracted.source, generation_id: extracted.generationId, + }), + }).catch(() => {}); + } + } catch { + // Metering must never affect the served response; a lost sample is tolerable (org billing is + // OR-authoritative). Enforcement degrades gracefully — worst case one uncounted call. + } +} diff --git a/cloudflare-workers/oc-gateway/src/models.ts b/cloudflare-workers/oc-gateway/src/models.ts new file mode 100644 index 000000000..c06769aff --- /dev/null +++ b/cloudflare-workers/oc-gateway/src/models.ts @@ -0,0 +1,51 @@ +// Prompt-caching safety (buildout W3: "Handle prompt-caching-safe models"). +// +// Some models route (via OpenRouter) to a backend that REJECTS Anthropic `cache_control` breakpoints +// — e.g. `claude-3-haiku` served through OR→Bedrock 400s the whole request. A Flue app written for +// Anthropic-native caching would then fail every turn on those models. Rather than restrict the model +// list (brittle as the catalog moves), the gateway STRIPS `cache_control` from the request body for a +// small, env-extensible denylist of known-unsafe models — the call still completes, just without +// caching. Models that support caching are untouched (no cost/perf regression). + +const DEFAULT_UNSAFE: RegExp[] = [ + /claude-3-haiku/i, // OR→Bedrock rejects cache_control (observed 1a) +]; + +/** Build the unsafe-model matchers, extended by a comma-separated env list (CACHE_CONTROL_UNSAFE_MODELS). */ +export function unsafeModelMatchers(extra?: string): RegExp[] { + const list = [...DEFAULT_UNSAFE]; + if (extra) { + for (const s of extra.split(",").map((x) => x.trim()).filter(Boolean)) { + try { + list.push(new RegExp(s, "i")); + } catch { + /* ignore an invalid pattern rather than break every request */ + } + } + } + return list; +} + +export function modelNeedsCacheStrip(model: unknown, matchers: RegExp[]): boolean { + return typeof model === "string" && matchers.some((re) => re.test(model)); +} + +/** Recursively delete every `cache_control` property in place. Returns how many were removed. */ +export function stripCacheControl(node: unknown): number { + if (Array.isArray(node)) { + let n = 0; + for (const v of node) n += stripCacheControl(v); + return n; + } + if (node && typeof node === "object") { + const o = node as Record; + let n = 0; + if ("cache_control" in o) { + delete o.cache_control; + n++; + } + for (const k of Object.keys(o)) n += stripCacheControl(o[k]); + return n; + } + return 0; +} diff --git a/cloudflare-workers/oc-gateway/src/orgkey.ts b/cloudflare-workers/oc-gateway/src/orgkey.ts new file mode 100644 index 000000000..58931482b --- /dev/null +++ b/cloudflare-workers/oc-gateway/src/orgkey.ts @@ -0,0 +1,56 @@ +// Org OpenRouter inference-key resolution (design 013 §4, buildout W3 "org OR key from Infisical"). +// +// The plaintext managed OR key lives in Infisical, sealed by sessions-api (referenced by the org's +// managed credential → `resolveManagedSecret`; edge `managed_model_keys` owns the key lifecycle). A +// CF Worker cannot reach Infisical or the box secrets-proxy (design §4), so the gateway resolves the +// key through a DEDICATED internal sessions-api seam — mirroring the edge's dedicated-secret +// plaintext-key hand-off (model_billing.ts §6.7.5): a route that carries a live key gets its OWN +// secret, never the generic internal-auth one. +// +// SEAM (sessions-api / L3 must provide — flagged in the W3 PR): +// POST {GATEWAY_ORKEY_URL} Authorization: Bearer {GATEWAY_ORKEY_SECRET} body {"org": orgId} +// → 200 {"key": "sk-or-..."} (resolveManagedSecret for the org's active managed credential) +// → 404/other on no active managed key. +// +// The plaintext is cached PER ORG in-isolate with a short TTL — it bounds exposure (evaporates with +// the isolate) and avoids hammering the seam on every model call in a turn. There is deliberately no +// single-key override: every environment exercises the org-scoped seam and missing config fails closed. + +export interface OrgKeyEnv { + GATEWAY_ORKEY_URL?: string; + GATEWAY_ORKEY_SECRET?: string; +} + +interface CacheEntry { + key: string; + exp: number; // epoch ms +} +const CACHE_TTL_MS = 60_000; +const cache = new Map(); + +/** Resolve the org's OpenRouter inference key, or null if unavailable. Never throws. */ +export async function resolveOrgKey(env: OrgKeyEnv, orgId: string, nowMs: number): Promise { + const hit = cache.get(orgId); + if (hit && hit.exp > nowMs) return hit.key; + + if (!env.GATEWAY_ORKEY_URL || !env.GATEWAY_ORKEY_SECRET) return null; + try { + const r = await fetch(env.GATEWAY_ORKEY_URL, { + method: "POST", + headers: { "content-type": "application/json", authorization: `Bearer ${env.GATEWAY_ORKEY_SECRET}` }, + body: JSON.stringify({ org: orgId }), + }); + if (!r.ok) return null; + const body = (await r.json()) as { key?: unknown }; + const key = typeof body.key === "string" && body.key ? body.key : null; + if (key) cache.set(orgId, { key, exp: nowMs + CACHE_TTL_MS }); + return key; + } catch { + return null; + } +} + +/** Test-only: clear the per-isolate cache between cases. */ +export function _clearOrgKeyCache(): void { + cache.clear(); +} diff --git a/cloudflare-workers/oc-gateway/src/token.ts b/cloudflare-workers/oc-gateway/src/token.ts new file mode 100644 index 000000000..ee43a4a56 --- /dev/null +++ b/cloudflare-workers/oc-gateway/src/token.ts @@ -0,0 +1,124 @@ +// The gateway deploy token (design 013 §4 / buildout contract #1, resolved token seam 2026-07-05). +// +// RESOLVED SEAM (option b, header-based). The gateway token is **per-DEPLOY**, not per-session: +// claims = { org, agt, iat, exp, ep? } — it authorizes an (org, agent) pair, nothing more. +// There is NO `sub:session` and NO `bud` claim. The session identity rides an `X-OC-Session` request +// header the tenant DO injects (Flue's `registerProvider` `apiKey` is a static string only, so it +// cannot carry per-session data — providers.ts:60); the per-session budget is looked up server-side +// in the SessionBudget DO keyed by that header. The token is bound as the tenant script's +// `OC_SESSION_TOKEN` env var by the deploy pipeline (W7) and rotates every redeploy. +// +// PROD hardening over the 1a spike (HS256 shared secret): +// 1. EdDSA (Ed25519). The MINTER (control plane / deploy pipeline) holds the private key; the +// gateway holds ONLY the public key — the same asymmetry as the turn token, so a compromised +// gateway cannot forge deploy tokens. Dependency-free — Ed25519 is in Workers' WebCrypto. +// 2. A **lease epoch** (`ep`, optional): a monotonic per-(org, agt) deploy counter. The gateway +// fences a token whose `ep` is below the current floor (DeployLease DO) — so a rotated or +// explicitly-revoked deploy token stops verifying even before `exp`. +// +// MINT↔VERIFY CONTRACT (W7 mints; the gateway verifies): +// alg "EdDSA"; claims { org, agt, iat, exp, ep? }; the gateway is configured with +// GATEWAY_TOKEN_PUBLIC_KEY = base64url(raw 32-byte Ed25519 public key). + +export interface DeployClaims { + /** Bare lowercase org UUID — selects the org's OpenRouter inference key. */ + org: string; + /** agent id — the deploy this token authorizes; attribution + the lease-fence key with `org`. */ + agt: string; + /** lease/deploy epoch (monotonic per (org, agt)). Below the DeployLease floor → fenced. Omit = no fence. */ + ep?: number; + /** issued-at / expiry (seconds). */ + iat: number; + exp: number; +} + +const enc = new TextEncoder(); +const dec = new TextDecoder(); +const ED = { name: "Ed25519" } as const; +const ORG_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; +const AGENT_ID = /^agt_[0-9a-f]{24}$/; + +function b64urlEncode(bytes: Uint8Array): string { + let s = ""; + for (const b of bytes) s += String.fromCharCode(b); + return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} +function b64urlDecode(s: string): Uint8Array { + const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4)); + const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad); + const out = new Uint8Array(b.length); + for (let i = 0; i < b.length; i++) out[i] = b.charCodeAt(i); + return out; +} + +/** Import a base64url raw 32-byte Ed25519 public key for verification. */ +async function importPublicKey(publicKeyB64url: string): Promise { + return crypto.subtle.importKey("raw", b64urlDecode(publicKeyB64url), ED, false, ["verify"]); +} + +export type VerifyResult = { ok: true; claims: DeployClaims } | { ok: false; reason: string }; + +/** Verify a per-deploy EdDSA token: alg pin + signature + exp/iat + required claims (org, agt). */ +export async function verifyDeployToken(publicKeyB64url: string, token: string, nowSec: number): Promise { + const parts = token.split("."); + if (parts.length !== 3) return { ok: false, reason: "malformed" }; + const [header, payload, sig] = parts; + + let alg: string; + try { + alg = (JSON.parse(dec.decode(b64urlDecode(header))) as { alg?: string }).alg ?? ""; + } catch { + return { ok: false, reason: "bad_header" }; + } + if (alg !== "EdDSA") return { ok: false, reason: "unexpected_alg" }; // pin — never accept "none"/HS256 + + let key: CryptoKey; + try { + key = await importPublicKey(publicKeyB64url); + } catch { + return { ok: false, reason: "bad_public_key" }; + } + + let valid: boolean; + try { + valid = await crypto.subtle.verify(ED, key, b64urlDecode(sig), enc.encode(`${header}.${payload}`)); + } catch { + return { ok: false, reason: "bad_signature_encoding" }; + } + if (!valid) return { ok: false, reason: "bad_signature" }; + + let claims: DeployClaims; + try { + claims = JSON.parse(dec.decode(b64urlDecode(payload))); + } catch { + return { ok: false, reason: "bad_payload" }; + } + if (!Number.isSafeInteger(claims.exp) || claims.exp <= nowSec) return { ok: false, reason: "expired" }; + if (!Number.isSafeInteger(claims.iat) || claims.iat <= 0) return { ok: false, reason: "bad_iat" }; + if (claims.iat > nowSec + 60) return { ok: false, reason: "future_iat" }; + if (!claims.org || !claims.agt) return { ok: false, reason: "missing_claims" }; + if (!ORG_ID.test(claims.org)) return { ok: false, reason: "bad_org" }; + if (!AGENT_ID.test(claims.agt)) return { ok: false, reason: "bad_agent" }; + if (claims.ep !== undefined && (!Number.isSafeInteger(claims.ep) || claims.ep < 0)) { + return { ok: false, reason: "bad_epoch" }; + } + return { ok: true, claims }; +} + +// ── Mint helpers — for the reference minter (W7) + tests. The gateway NEVER mints in prod. ── + +/** Generate an Ed25519 keypair; returns the private key + base64url raw public key (gateway config). */ +export async function generateKeyPair(): Promise<{ privateKey: CryptoKey; publicKeyB64url: string }> { + const kp = (await crypto.subtle.generateKey(ED, true, ["sign", "verify"])) as CryptoKeyPair; + const raw = new Uint8Array((await crypto.subtle.exportKey("raw", kp.publicKey)) as ArrayBuffer); + return { privateKey: kp.privateKey, publicKeyB64url: b64urlEncode(raw) }; +} + +/** Sign a deploy token with the Ed25519 private key (mint side). */ +export async function mintDeployToken(privateKey: CryptoKey, claims: DeployClaims): Promise { + const header = b64urlEncode(enc.encode(JSON.stringify({ alg: "EdDSA", typ: "JWT" }))); + const payload = b64urlEncode(enc.encode(JSON.stringify(claims))); + const signingInput = `${header}.${payload}`; + const sig = new Uint8Array(await crypto.subtle.sign(ED, privateKey, enc.encode(signingInput))); + return `${signingInput}.${b64urlEncode(sig)}`; +} diff --git a/cloudflare-workers/oc-gateway/test/integration.test.ts b/cloudflare-workers/oc-gateway/test/integration.test.ts new file mode 100644 index 000000000..5f644a3b1 --- /dev/null +++ b/cloudflare-workers/oc-gateway/test/integration.test.ts @@ -0,0 +1,291 @@ +// In-process integration: the REAL worker handler + REAL SpendCounter/DeployLease DOs, with `fetch` +// stubbed to a mock OpenRouter. Proves the whole on-path flow deterministically without wrangler: +// deploy-token verify → lease fence → HARD org+agt budget gate → org-key injection → forward → cost +// sub-meter (org+agt authoritative + best-effort per session) → passthrough → 402 over budget. +// Reflects the resolved token seam + co-location refinement: HARD enforcement is org+agt (race-free); +// the X-OC-Session header is best-effort per-session tracking, never gated. +// Run: npx vitest run + +import { describe, it, expect, beforeEach, beforeAll, vi, afterEach } from "vitest"; +import worker, { Env } from "../src/index.js"; +import { SpendCounter } from "../src/budget.js"; +import { DeployLease } from "../src/deploylease.js"; +import { generateKeyPair, mintDeployToken } from "../src/token.js"; +import { _clearOrgKeyCache } from "../src/orgkey.js"; + +const OR_KEY = "sk-or-v1-FAKE-org-key"; +const OR_BASE = "https://mock-openrouter.test/api"; +const OR_KEY_URL = "https://api.opencomputer.dev/internal/gateway/org-key"; +const ORG_ID = "11111111-1111-4111-8111-111111111111"; +const AGENT_ID = "agt_0123456789abcdef01234567"; + +// EdDSA keypair for the suite: the minter (control plane) holds PRIV, the gateway holds PUB. +let PRIV: CryptoKey; +let PUB: string; +beforeAll(async () => { const kp = await generateKeyPair(); PRIV = kp.privateKey; PUB = kp.publicKeyB64url; }); + +// ── a fake DurableObjectState backed by a Map (the real DO runs against it) ── +function fakeState() { + const store = new Map(); + return { storage: { + get: async (k: string) => store.get(k), + put: async (k: string, v: unknown) => void store.set(k, v), + } } as unknown as DurableObjectState; +} + +// ── a fake DO namespace: one real instance of `Klass` per name; `instances` exposed for assertions ── +function fakeNamespace }>(Klass: new (s: DurableObjectState) => T) { + const instances = new Map(); + const ns = { + idFromName: (n: string) => ({ toString: () => n, name: n }) as unknown as DurableObjectId, + get: (id: DurableObjectId) => { + const name = (id as unknown as { name: string }).name; + if (!instances.has(name)) instances.set(name, new Klass(fakeState())); + const inst = instances.get(name)!; + return { fetch: (input: RequestInfo, init?: RequestInit) => inst.fetch(new Request(typeof input === "string" ? input : (input as Request).url, init)) } as unknown as DurableObjectStub; + }, + } as unknown as DurableObjectNamespace; + return { ns, instances }; +} + +let lastAuthToOR: string | null; +let lastBodyToOR: Record | null; +let lastHeadersToOR: Headers | null; + +function mockFetch(perCallCost: number) { + return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input.toString(); + if (url === OR_KEY_URL) { + expect(new Headers(init?.headers).get("authorization")).toBe("Bearer dedicated-org-key-bearer"); + expect(JSON.parse(String(init?.body))).toEqual({ org: ORG_ID }); + return new Response(JSON.stringify({ key: OR_KEY }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + lastHeadersToOR = new Headers(init?.headers); + lastAuthToOR = lastHeadersToOR.get("authorization"); + lastBodyToOR = init?.body ? JSON.parse(init.body as string) : null; + expect(url.startsWith(OR_BASE)).toBe(true); // forwarded to the OR base, tail preserved + return new Response(JSON.stringify({ + id: "gen-" + Math.random().toString(36).slice(2), + type: "message", role: "assistant", + content: [{ type: "text", text: "pong" }], + usage: { input_tokens: 10, output_tokens: 3, cost: perCallCost }, + }), { status: 200, headers: { "content-type": "application/json" } }); + }); +} + +// a fresh env each test; `spend`/`lease` handles let us assert on per-grain DO state. +function mkEnv(extra: Partial = {}) { + const spend = fakeNamespace(SpendCounter); + const lease = fakeNamespace(DeployLease); + const env = { + GATEWAY_TOKEN_PUBLIC_KEY: PUB, + GATEWAY_ORKEY_URL: OR_KEY_URL, + GATEWAY_ORKEY_SECRET: "dedicated-org-key-bearer", + OPENROUTER_BASE: OR_BASE, + SPEND_COUNTER: spend.ns, + DEPLOY_LEASE: lease.ns, + ...extra, + } as Env; + return { env, spend, lease }; +} + +// waitUntil runs the metering; collect the promises so we can await them (the meter is off-path). +function ctx(): ExecutionContext { + const pending: Promise[] = []; + return { waitUntil: (p: Promise) => pending.push(p), passThroughOnException: () => {}, _pending: pending } as unknown as ExecutionContext; +} +async function drain(c: ExecutionContext) { await Promise.all((c as unknown as { _pending: Promise[] })._pending); } + +const MSG = JSON.stringify({ model: "anthropic/claude-sonnet-5", max_tokens: 16, messages: [{ role: "user", content: "ping" }] }); +const post = (token?: string, session?: string, body = MSG) => new Request("https://gw.test/anthropic/v1/messages", { + method: "POST", + headers: { + "content-type": "application/json", + ...(token ? { authorization: `Bearer ${token}` } : {}), + ...(session ? { "x-oc-session": session } : {}), + }, + body, +}); +const mint = async (o: Partial<{ org: string; agt: string; ep: number; iat: number; exp: number }> = {}) => { + const now = Math.floor(Date.now() / 1000); + return mintDeployToken(PRIV, { org: ORG_ID, agt: AGENT_ID, iat: now, exp: now + 3600, ...o }); +}; +const stateOf = async (inst: { fetch(r: Request): Promise }) => + (await inst.fetch(new Request("https://do/state"))).json() as Promise<{ spent_micro: number }>; + +describe("gateway on-path flow (resolved seam)", () => { + beforeEach(() => { + _clearOrgKeyCache(); + lastAuthToOR = null; + lastBodyToOR = null; + lastHeadersToOR = null; + }); + afterEach(() => vi.restoreAllMocks()); + + it("GET /healthz → ok", async () => { + const { env } = mkEnv(); + const res = await worker.fetch(new Request("https://gw.test/healthz"), env, ctx()); + expect(res.status).toBe(200); + expect((await res.json() as { status: string }).status).toBe("ok"); + }); + + it("rejects a POST with no deploy token (401)", async () => { + const { env } = mkEnv(); + const res = await worker.fetch(post(), env, ctx()); + expect(res.status).toBe(401); + }); + + it("rejects an expired token (401)", async () => { + const { env } = mkEnv(); + const now = Math.floor(Date.now() / 1000); + const expired = await mint({ iat: now - 10, exp: now - 5 }); + const res = await worker.fetch(post(expired), env, ctx()); + expect(res.status).toBe(401); + }); + + it("forwards a valid turn: injects the ORG key (not the deploy token), adds usage.include, passes the body through, does NOT leak the session header to OR", async () => { + vi.stubGlobal("fetch", mockFetch(0.02)); + const { env } = mkEnv(); + const token = await mint(); + const c = ctx(); + const res = await worker.fetch(post(token, "ses_ok"), env, c); + expect(res.status).toBe(200); + const body = await res.json() as { content: { text: string }[] }; + expect(body.content[0].text).toBe("pong"); + // the ORG key was injected; the deploy token never reached OpenRouter (no raw key to the tenant either) + expect(lastAuthToOR).toBe(`Bearer ${OR_KEY}`); + expect(lastAuthToOR).not.toContain(token); + // the session header is an OC-internal signal — it must not egress to OpenRouter + expect(lastHeadersToOR?.get("x-oc-session")).toBeNull(); + // usage.include injected so OR echoes cost + expect(lastBodyToOR?.usage).toMatchObject({ include: true }); + expect(lastBodyToOR?.model).toBe("anthropic/claude-sonnet-5"); + await drain(c); + }); + + it("proceeds WITHOUT an X-OC-Session header (best-effort attribution, not required)", async () => { + vi.stubGlobal("fetch", mockFetch(0.01)); + const { env } = mkEnv(); + const c = ctx(); + const res = await worker.fetch(post(await mint()), env, c); // no session header + expect(res.status).toBe(200); + await drain(c); + }); + + it("HARD-enforces the org+agt budget ON-PATH: refuses once org+agt spend reaches the cap (402)", async () => { + vi.stubGlobal("fetch", mockFetch(0.02)); // $0.02 per call + // default org+agt budget $0.03 → call1 (0<0.03) ok→0.02; call2 (0.02<0.03) ok→0.04; call3 refused. + const { env } = mkEnv({ AGENT_BUDGET_USD_DEFAULT: "0.03" }); + const token = await mint(); + const c1 = ctx(); const r1 = await worker.fetch(post(token, "ses_a"), env, c1); await drain(c1); + const c2 = ctx(); const r2 = await worker.fetch(post(token, "ses_a"), env, c2); await drain(c2); + const c3 = ctx(); const r3 = await worker.fetch(post(token, "ses_a"), env, c3); await drain(c3); + expect(r1.status).toBe(200); + expect(r2.status).toBe(200); + expect(r3.status).toBe(402); + const refusal = await r3.json() as { error: { type: string }; oc: { spent_usd: number; budget_usd: number } }; + expect(refusal.error.type).toBe("budget_exceeded"); + expect(refusal.oc.spent_usd).toBeCloseTo(0.04, 5); // bounded one-call overshoot past $0.03 + expect(refusal.oc.budget_usd).toBeCloseTo(0.03, 5); + }); + + it("co-location: two DIFFERENT sessions of the same org+agt SHARE the hard budget (grain is org+agt, not per-session)", async () => { + vi.stubGlobal("fetch", mockFetch(0.02)); + const { env, spend } = mkEnv({ AGENT_BUDGET_USD_DEFAULT: "0.03" }); + const token = await mint(); + // ses_x spends $0.02, ses_y spends $0.02 → org+agt total $0.04 ≥ $0.03 → the next call (either + // session) is refused. This is exactly the race-free property: budget is on the token's org+agt. + const cx = ctx(); const rx = await worker.fetch(post(token, "ses_x"), env, cx); await drain(cx); + const cy = ctx(); const ry = await worker.fetch(post(token, "ses_y"), env, cy); await drain(cy); + const cz = ctx(); const rz = await worker.fetch(post(token, "ses_z"), env, cz); await drain(cz); + expect(rx.status).toBe(200); + expect(ry.status).toBe(200); + expect(rz.status).toBe(402); // org+agt cap hit across sessions + // best-effort per-session tracking recorded each session's own spend separately + expect((await stateOf(spend.instances.get(`sess:${ORG_ID}:${AGENT_ID}:ses_x`)!)).spent_micro).toBe(20_000); + expect((await stateOf(spend.instances.get(`sess:${ORG_ID}:${AGENT_ID}:ses_y`)!)).spent_micro).toBe(20_000); + // and the authoritative org+agt grain summed them + expect((await stateOf(spend.instances.get(`agt:${ORG_ID}:${AGENT_ID}`)!)).spent_micro).toBe(40_000); + }); + + it("per-session counter is TRACKED but NEVER gated: a session over its own spend is not 402'd", async () => { + vi.stubGlobal("fetch", mockFetch(0.10)); // $0.10/call, well over any per-session intuition + const { env, spend } = mkEnv(); // no org+agt cap → uncapped hard grain + const token = await mint(); + for (let i = 0; i < 3; i++) { const c = ctx(); const r = await worker.fetch(post(token, "ses_hot"), env, c); await drain(c); expect(r.status).toBe(200); } + // the session accumulated $0.30 but was never blocked (no per-session hard gate) + expect((await stateOf(spend.instances.get(`sess:${ORG_ID}:${AGENT_ID}:ses_hot`)!)).spent_micro).toBe(300_000); + }); + + it("fences a superseded deploy lease epoch (401 token_superseded)", async () => { + vi.stubGlobal("fetch", mockFetch(0.001)); + const { env } = mkEnv(); // same DEPLOY_LEASE namespace → same lease for the canonical org+agent + const t2 = await mint({ ep: 2 }); + const c2 = ctx(); const r2 = await worker.fetch(post(t2, "ses_ep"), env, c2); await drain(c2); + expect(r2.status).toBe(200); // adopt epoch 2 + const t1 = await mint({ ep: 1 }); + const r1 = await worker.fetch(post(t1, "ses_ep"), env, ctx()); // the old deploy's token — superseded + expect(r1.status).toBe(401); + expect((await r1.json() as { error: { code?: string } }).error.code).toBe("token_superseded"); + }); + + it("strips cache_control for a caching-unsafe model, still injects usage.include", async () => { + vi.stubGlobal("fetch", mockFetch(0.001)); + const { env } = mkEnv(); + const token = await mint(); + const body = JSON.stringify({ + model: "anthropic/claude-3-haiku", max_tokens: 16, + system: [{ type: "text", text: "s", cache_control: { type: "ephemeral" } }], + messages: [{ role: "user", content: "ping" }], + }); + const c = ctx(); const r = await worker.fetch(post(token, "ses_cc", body), env, c); await drain(c); + expect(r.status).toBe(200); + expect(JSON.stringify(lastBodyToOR).includes("cache_control")).toBe(false); + expect(lastBodyToOR?.usage).toMatchObject({ include: true }); + }); + + it("admin: provision an org+agt cap, then it gates (402); guarded by GATEWAY_ADMIN_SECRET", async () => { + vi.stubGlobal("fetch", mockFetch(0.05)); + const { env } = mkEnv({ GATEWAY_ADMIN_SECRET: "adm" }); + // unauthorized admin call is rejected + const bad = await worker.fetch(new Request("https://gw.test/admin/agent/budget", { method: "POST", headers: { authorization: "Bearer nope", "content-type": "application/json" }, body: JSON.stringify({ org: ORG_ID, agt: AGENT_ID, budget_usd: 0.04 }) }), env, ctx()); + expect(bad.status).toBe(401); + // provision a $0.04 cap + const prov = await worker.fetch(new Request("https://gw.test/admin/agent/budget", { method: "POST", headers: { authorization: "Bearer adm", "content-type": "application/json" }, body: JSON.stringify({ org: ORG_ID, agt: AGENT_ID, budget_usd: 0.04 }) }), env, ctx()); + expect(prov.status).toBe(200); + const token = await mint(); + const c1 = ctx(); const r1 = await worker.fetch(post(token, "s1"), env, c1); await drain(c1); // 0→0.05 + const c2 = ctx(); const r2 = await worker.fetch(post(token, "s1"), env, c2); await drain(c2); // 0.05≥0.04 → 402 + expect(r1.status).toBe(200); + expect(r2.status).toBe(402); + }); + + it("admin: a zero budget is a real cap, while missing or invalid values are rejected", async () => { + vi.stubGlobal("fetch", mockFetch(0.01)); + const { env } = mkEnv({ GATEWAY_ADMIN_SECRET: "adm" }); + const headers = { authorization: "Bearer adm", "content-type": "application/json" }; + const endpoint = "https://gw.test/admin/agent/budget"; + + for (const budget_usd of [undefined, -1, "1", Number.MAX_VALUE]) { + const invalid = await worker.fetch(new Request(endpoint, { + method: "POST", + headers, + body: JSON.stringify({ org: ORG_ID, agt: AGENT_ID, ...(budget_usd === undefined ? {} : { budget_usd }) }), + }), env, ctx()); + expect(invalid.status).toBe(400); + } + + const provisioned = await worker.fetch(new Request(endpoint, { + method: "POST", + headers, + body: JSON.stringify({ org: ORG_ID, agt: AGENT_ID, budget_usd: 0 }), + }), env, ctx()); + expect(provisioned.status).toBe(200); + + const blocked = await worker.fetch(post(await mint(), "ses_zero"), env, ctx()); + expect(blocked.status).toBe(402); + }); +}); diff --git a/cloudflare-workers/oc-gateway/test/logic.test.ts b/cloudflare-workers/oc-gateway/test/logic.test.ts new file mode 100644 index 000000000..503e11134 --- /dev/null +++ b/cloudflare-workers/oc-gateway/test/logic.test.ts @@ -0,0 +1,231 @@ +// Pure-logic tests (no Workers runtime needed): EdDSA per-deploy token crypto, the SpendCounter DO's +// budget gate + idempotency + provision, the DeployLease DO's epoch fence + bump, cache_control safety, +// and cost extraction. The full on-path flow (forward + meter) is exercised by test/integration.test.ts. +// Run: npx vitest run + +import { afterEach, describe, it, expect, vi } from "vitest"; +import { generateKeyPair, mintDeployToken, verifyDeployToken, type DeployClaims } from "../src/token.js"; +import { costFromJson, costFromStream } from "../src/cost.js"; +import { unsafeModelMatchers, modelNeedsCacheStrip, stripCacheControl } from "../src/models.js"; +import { SpendCounter } from "../src/budget.js"; +import { DeployLease } from "../src/deploylease.js"; +import { _clearOrgKeyCache, resolveOrgKey } from "../src/orgkey.js"; + +const now = 1_800_000_000; +const ORG_ID = "11111111-1111-4111-8111-111111111111"; +const AGENT_ID = "agt_0123456789abcdef01234567"; +const b64url = (o: unknown) => btoa(JSON.stringify(o)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +const claims = (o: Partial = {}): DeployClaims => ({ + org: ORG_ID, agt: AGENT_ID, ep: 2, iat: now, exp: now + 3600, ...o, +}); + +function fakeState(): DurableObjectState { + const m = new Map(); + return { storage: { get: async (k: string) => m.get(k), put: async (k: string, v: unknown) => void m.set(k, v) } } as unknown as DurableObjectState; +} +const call = async (o: { fetch(r: Request): Promise }, path: string, body?: unknown) => + (await o.fetch(new Request(`https://do${path}`, { method: "POST", body: JSON.stringify(body ?? {}) }))).json() as Promise>; + +describe("deploy token (EdDSA, per-deploy {org, agt})", () => { + it("mint → verify round-trips the claims", async () => { + const { privateKey, publicKeyB64url } = await generateKeyPair(); + const v = await verifyDeployToken(publicKeyB64url, await mintDeployToken(privateKey, claims()), now); + expect(v.ok).toBe(true); + if (v.ok) { + expect(v.claims.org).toBe(ORG_ID); + expect(v.claims.agt).toBe(AGENT_ID); + expect(v.claims.ep).toBe(2); + // resolved seam: no per-session data in the token + const raw = v.claims as unknown as Record; + expect(raw.sub).toBeUndefined(); + expect(raw.bud).toBeUndefined(); + } + }); + it("rejects a token signed by a different key (gateway holds only the public key)", async () => { + const a = await generateKeyPair(); + const b = await generateKeyPair(); + const v = await verifyDeployToken(b.publicKeyB64url, await mintDeployToken(a.privateKey, claims()), now); + expect(v.ok).toBe(false); + }); + it("rejects a tampered payload", async () => { + const { privateKey, publicKeyB64url } = await generateKeyPair(); + const [h, , s] = (await mintDeployToken(privateKey, claims())).split("."); + const v = await verifyDeployToken(publicKeyB64url, `${h}.${b64url(claims({ org: "org_evil" }))}.${s}`, now); + expect(v.ok).toBe(false); + }); + it("rejects an expired token", async () => { + const { privateKey, publicKeyB64url } = await generateKeyPair(); + const v = await verifyDeployToken(publicKeyB64url, await mintDeployToken(privateKey, claims({ exp: now - 1 })), now); + expect(v.ok).toBe(false); + if (!v.ok) expect(v.reason).toBe("expired"); + }); + it("rejects a token missing org/agt", async () => { + const { privateKey, publicKeyB64url } = await generateKeyPair(); + const t = await mintDeployToken(privateKey, { org: "", agt: "", iat: now, exp: now + 3600 }); + const v = await verifyDeployToken(publicKeyB64url, t, now); + expect(v.ok).toBe(false); + if (!v.ok) expect(v.reason).toBe("missing_claims"); + }); + it.each([ + ["canonical owner instead of bare UUID", { org: `oc-org:${ORG_ID}` }, "bad_org"], + ["uppercase org UUID", { org: "ABCDEFAB-CDEF-4ABC-8DEF-ABCDEFABCDEF" }, "bad_org"], + ["non-canonical agent id", { agt: "agt_1" }, "bad_agent"], + ])("rejects %s", async (_name, overrides, reason) => { + const { privateKey, publicKeyB64url } = await generateKeyPair(); + const token = await mintDeployToken(privateKey, claims(overrides)); + const verified = await verifyDeployToken(publicKeyB64url, token, now); + expect(verified.ok).toBe(false); + if (!verified.ok) expect(verified.reason).toBe(reason); + }); + it("pins alg=EdDSA — rejects an alg-swap (none/HS256) header", async () => { + const { privateKey, publicKeyB64url } = await generateKeyPair(); + const [, p, s] = (await mintDeployToken(privateKey, claims())).split("."); + const v = await verifyDeployToken(publicKeyB64url, `${b64url({ alg: "none", typ: "JWT" })}.${p}.${s}`, now); + expect(v.ok).toBe(false); + if (!v.ok) expect(v.reason).toBe("unexpected_alg"); + }); +}); + +describe("org-key identity seam", () => { + afterEach(() => { _clearOrgKeyCache(); vi.restoreAllMocks(); }); + + it("fails closed without the dedicated route and bearer", async () => { + const fetchMock = vi.spyOn(globalThis, "fetch"); + expect(await resolveOrgKey({}, ORG_ID, Date.now())).toBeNull(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("sends the token's bare UUID exactly once to the sessions API", async () => { + let request: { url: string; auth: string | null; body: unknown } | undefined; + vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + request = { + url: String(input), + auth: new Headers(init?.headers).get("authorization"), + body: JSON.parse(String(init?.body)), + }; + return new Response(JSON.stringify({ key: "test-only-key" }), { status: 200 }); + })); + + const key = await resolveOrgKey({ + GATEWAY_ORKEY_URL: "https://api.opencomputer.dev/internal/gateway/org-key", + GATEWAY_ORKEY_SECRET: "dedicated-bearer", + }, ORG_ID, Date.now()); + + expect(key).toBe("test-only-key"); + expect(request).toEqual({ + url: "https://api.opencomputer.dev/internal/gateway/org-key", + auth: "Bearer dedicated-bearer", + body: { org: ORG_ID }, + }); + }); +}); + +describe("SpendCounter DO — budget gate + idempotency + provision", () => { + it("gates on the budget (spent < budget), applies the default on first check", async () => { + const c = new SpendCounter(fakeState()); + const capMicro = 1_000_000; // $1.00 in µ$ + expect((await call(c, "/check", { default_budget_micro: capMicro })).allowed).toBe(true); + await call(c, "/add", { cost_micro: 600_000, idem: "g1" }); + expect((await call(c, "/check", { default_budget_micro: capMicro })).allowed).toBe(true); // 0.6 < 1.0 + await call(c, "/add", { cost_micro: 600_000, idem: "g2" }); // now 1.2 > 1.0 + expect((await call(c, "/check", { default_budget_micro: capMicro })).allowed).toBe(false); + }); + + it("a provisioned cap wins over the gateway default", async () => { + const c = new SpendCounter(fakeState()); + await call(c, "/provision", { budget_micro: 100_000 }); // $0.10 explicit + await call(c, "/add", { cost_micro: 150_000, idem: "x" }); + // default is huge, but the provisioned $0.10 cap is what gates + expect((await call(c, "/check", { default_budget_micro: 999_000_000 })).allowed).toBe(false); + }); + + it("uncapped (no budget, no default) never gates", async () => { + const c = new SpendCounter(fakeState()); + await call(c, "/add", { cost_micro: 5_000_000, idem: "big" }); + expect((await call(c, "/check", {})).allowed).toBe(true); + }); + + it("dedupes /add by generation id (retried meter never double-counts)", async () => { + const c = new SpendCounter(fakeState()); + await call(c, "/add", { cost_micro: 100_000, idem: "gen-x" }); + const second = await call(c, "/add", { cost_micro: 100_000, idem: "gen-x" }); + expect(second.deduped).toBe(true); + const st = (await (await c.fetch(new Request("https://do/state"))).json()) as { spent_micro: number }; + expect(st.spent_micro).toBe(100_000); + }); +}); + +describe("DeployLease DO — lease-epoch fence + bump (revocation)", () => { + it("fences a stale deploy epoch (monotonic floor)", async () => { + const l = new DeployLease(fakeState()); + expect((await call(l, "/gate", { ep: 1 })).fenced).toBe(false); + expect((await call(l, "/gate", { ep: 2 })).fenced).toBe(false); // adopt the newer epoch → floor 2 + const stale = await call(l, "/gate", { ep: 1 }); // the old deploy's token is now superseded + expect(stale.fenced).toBe(true); + expect(stale.ok).toBe(false); + }); + it("an epoch-less token fails closed once a floor exists", async () => { + const l = new DeployLease(fakeState()); + await call(l, "/gate", { ep: 5 }); // raise the floor + const missing = await call(l, "/gate", {}); + expect(missing.fenced).toBe(true); + expect(missing.ok).toBe(false); + }); + it("bump revokes without a redeploy (raise the floor above the live token)", async () => { + const l = new DeployLease(fakeState()); + expect((await call(l, "/gate", { ep: 3 })).fenced).toBe(false); // floor 3, current token ep=3 valid + await call(l, "/bump", { min_epoch: 4 }); // revoke everything below 4 + expect((await call(l, "/gate", { ep: 3 })).fenced).toBe(true); // the still-live ep=3 token now fences + }); +}); + +describe("cache_control safety", () => { + const m = unsafeModelMatchers(); + it("flags claude-3-haiku, leaves sonnet alone", () => { + expect(modelNeedsCacheStrip("anthropic/claude-3-haiku", m)).toBe(true); + expect(modelNeedsCacheStrip("anthropic/claude-sonnet-4", m)).toBe(false); + }); + it("env extends the denylist", () => { + expect(modelNeedsCacheStrip("vendor/some-bedrock-model", unsafeModelMatchers("some-bedrock-model"))).toBe(true); + }); + it("strips every nested cache_control in place", () => { + const body = { + model: "anthropic/claude-3-haiku", + system: [{ type: "text", text: "sys", cache_control: { type: "ephemeral" } }], + messages: [{ role: "user", content: [{ type: "text", text: "hi", cache_control: { type: "ephemeral" } }] }], + }; + expect(stripCacheControl(body)).toBe(2); + expect(JSON.stringify(body).includes("cache_control")).toBe(false); + }); +}); + +describe("cost extraction", () => { + it("reads usage.cost from a JSON response", () => { + const c = costFromJson(JSON.stringify({ id: "gen-123", usage: { cost: 0.0042, prompt_tokens: 10 } })); + expect(c.costUsd).toBe(0.0042); + expect(c.generationId).toBe("gen-123"); + expect(c.source).toBe("usage.cost"); + }); + it("falls back to usage.total_cost", () => { + const c = costFromJson(JSON.stringify({ id: "g", usage: { total_cost: 0.01 } })); + expect(c.costUsd).toBe(0.01); + expect(c.source).toBe("usage.total_cost"); + }); + it("returns null cost when the echo lacks it (flagged, never guessed)", () => { + const c = costFromJson(JSON.stringify({ id: "g", usage: { prompt_tokens: 10 } })); + expect(c.costUsd).toBeNull(); + expect(c.source).toBe("none"); + }); + it("extracts cost + generation id from an SSE stream", async () => { + const sse = [ + 'data: {"id":"gen-9","type":"message_start"}', + 'data: {"type":"content_block_delta","delta":{"text":"hi"}}', + 'data: {"type":"message_delta","usage":{"cost":0.0009,"output_tokens":3}}', + "data: [DONE]", + "", + ].join("\n"); + const c = await costFromStream(new Response(sse).body!); + expect(c.costUsd).toBe(0.0009); + expect(c.generationId).toBe("gen-9"); + }); +}); diff --git a/cloudflare-workers/oc-gateway/test/mock-openrouter.mjs b/cloudflare-workers/oc-gateway/test/mock-openrouter.mjs new file mode 100644 index 000000000..54c3eb2c2 --- /dev/null +++ b/cloudflare-workers/oc-gateway/test/mock-openrouter.mjs @@ -0,0 +1,31 @@ +// Minimal mock OpenRouter for the local integration proof. Records the Authorization header it +// received (to prove the gateway injected the ORG key, not the session token) and echoes an +// Anthropic-Messages-shaped response carrying usage.cost (what the on-path meter reads). +import { createServer } from "node:http"; + +let lastAuth = null; +let lastBody = null; +const PORT = Number(process.env.MOCK_PORT || 8799); + +createServer((req, res) => { + if (req.url === "/__spy") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ lastAuth, lastBody })); + return; + } + let body = ""; + req.on("data", (c) => (body += c)); + req.on("end", () => { + lastAuth = req.headers["authorization"] || null; + try { lastBody = JSON.parse(body); } catch { lastBody = body; } + // Echo an anthropic-style completion with an OpenRouter cost echo. + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ + id: "gen-" + Math.floor(Date.now() / 1000), + type: "message", + role: "assistant", + content: [{ type: "text", text: "pong from mock" }], + usage: { input_tokens: 12, output_tokens: 4, cost: 0.02 }, // $0.02 per call + })); + }); +}).listen(PORT, () => console.log(`mock-openrouter on :${PORT}`)); diff --git a/cloudflare-workers/oc-gateway/tsconfig.json b/cloudflare-workers/oc-gateway/tsconfig.json new file mode 100644 index 000000000..2f022d943 --- /dev/null +++ b/cloudflare-workers/oc-gateway/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/cloudflare-workers/oc-gateway/wrangler.toml b/cloudflare-workers/oc-gateway/wrangler.toml new file mode 100644 index 000000000..0d82b6a4f --- /dev/null +++ b/cloudflare-workers/oc-gateway/wrangler.toml @@ -0,0 +1,46 @@ +name = "oc-agent-gateway-local" +main = "src/index.ts" +compatibility_date = "2025-06-01" +compatibility_flags = ["nodejs_compat"] +workers_dev = false + +# Local/dev Durable Objects. Production repeats these non-inheritable bindings under its explicit +# environment and gets fresh state under the permanent Worker identity. +[[durable_objects.bindings]] +name = "SPEND_COUNTER" +class_name = "SpendCounter" + +[[durable_objects.bindings]] +name = "DEPLOY_LEASE" +class_name = "DeployLease" + +[[migrations]] +tag = "v1" +new_sqlite_classes = ["SpendCounter", "DeployLease"] + +# Production is deliberately available only through `npm run deploy:production`. +[env.production] +name = "oc-agent-gateway-prod" +workers_dev = true + +[[env.production.durable_objects.bindings]] +name = "SPEND_COUNTER" +class_name = "SpendCounter" + +[[env.production.durable_objects.bindings]] +name = "DEPLOY_LEASE" +class_name = "DeployLease" + +[[env.production.migrations]] +tag = "v1" +new_sqlite_classes = ["SpendCounter", "DeployLease"] + +[env.production.vars] +GATEWAY_ORKEY_URL = "https://api.opencomputer.dev/internal/gateway/org-key" + +# Production secrets (set for --env production; never in this file): +# GATEWAY_TOKEN_PUBLIC_KEY — base64url raw 32-byte Ed25519 public key +# GATEWAY_ORKEY_SECRET — bearer for the dedicated org-key route +# GATEWAY_ADMIN_SECRET — bearer for /admin/* +# No single-key test override exists; production and tests both exercise the org-key seam. +# AGENT_BUDGET_USD_DEFAULT is intentionally absent; managed OpenRouter/Autumn limits are authoritative. diff --git a/cmd/oc/internal/commands/agent.go b/cmd/oc/internal/commands/agent.go index 3d95c1d83..cf230107f 100644 --- a/cmd/oc/internal/commands/agent.go +++ b/cmd/oc/internal/commands/agent.go @@ -173,6 +173,7 @@ var agentCmd = &cobra.Command{ func init() { registerAgentCrud() registerAgentDeploy() + registerAgentConfig() registerAgentSchedules() rootCmd.AddCommand(sessionCmd) } diff --git a/cmd/oc/internal/commands/agent_config.go b/cmd/oc/internal/commands/agent_config.go new file mode 100644 index 000000000..2260a2170 --- /dev/null +++ b/cmd/oc/internal/commands/agent_config.go @@ -0,0 +1,157 @@ +package commands + +// Preview Flue configuration has one source of truth: non-secret vars come from agent.toml and +// write-only secrets come from this command. Both are applied by the next explicit agent deploy. + +import ( + "fmt" + "io" + "net/url" + "os" + "strings" + + "github.com/opensandbox/opensandbox/cmd/oc/internal/client" + "github.com/spf13/cobra" +) + +type agentConfig struct { + Vars map[string]string `json:"vars"` + DeploymentRequired bool `json:"deployment_required,omitempty"` +} + +type agentSecret struct { + Name string `json:"name"` + Last4 string `json:"last4"` + UpdatedAt string `json:"updated_at"` + DeploymentRequired bool `json:"deployment_required,omitempty"` +} + +type agentSecretList struct { + Data []agentSecret `json:"data"` +} + +func agentConfigPath(id string) string { return "/v3/agents/" + id + "/config" } +func agentSecretsPath(id string) string { return "/v3/agents/" + id + "/secrets" } +func agentSecretPath(id, name string) string { + return agentSecretsPath(id) + "/" + url.PathEscape(name) +} + +// syncManifestVars makes agent.toml the only non-secret config source. An absent [vars] section is +// an empty desired map, so deleting the section and deploying removes prior bindings. +func syncManifestVars(cmd *cobra.Command, sc *client.Client, id string, m *manifest) error { + vars := m.Vars + if vars == nil { + vars = map[string]string{} + } + var saved agentConfig + if err := sc.PutJSON(cmd.Context(), agentConfigPath(id), map[string]interface{}{"vars": vars}, &saved); err != nil { + return fmt.Errorf("sync agent.toml [vars]: %w", err) + } + return nil +} + +var agentSecretCmd = &cobra.Command{ + Use: "secret", + Short: "Manage write-only Flue Worker secrets for the next deploy", +} + +var agentSecretListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List secret metadata (values are never returned)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + sc, err := sessionsClient(cmd) + if err != nil { + return err + } + id, err := targetAgentID(cmd, sc, nil) + if err != nil { + return err + } + var res agentSecretList + if err := sc.Get(cmd.Context(), agentSecretsPath(id), &res); err != nil { + return err + } + printer.Print(res.Data, func() { + if len(res.Data) == 0 { + fmt.Println("No secrets.") + return + } + rows := make([][]string, 0, len(res.Data)) + for _, secret := range res.Data { + rows = append(rows, []string{secret.Name, secret.Last4, formatAge(secret.UpdatedAt)}) + } + printer.Table([]string{"NAME", "LAST4", "UPDATED"}, rows) + }) + return nil + }, +} + +var agentSecretSetCmd = &cobra.Command{ + Use: "set --from-stdin", + Short: "Save or rotate a Worker secret for the next deploy", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fromStdin, _ := cmd.Flags().GetBool("from-stdin") + if !fromStdin { + return fmt.Errorf("secret values are accepted only via --from-stdin") + } + raw, err := io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("reading stdin: %w", err) + } + value := strings.TrimSuffix(strings.TrimSuffix(string(raw), "\n"), "\r") + if value == "" { + return fmt.Errorf("secret value cannot be empty") + } + + sc, err := sessionsClient(cmd) + if err != nil { + return err + } + id, err := targetAgentID(cmd, sc, nil) + if err != nil { + return err + } + var saved agentSecret + if err := sc.PutJSON(cmd.Context(), agentSecretPath(id, args[0]), map[string]string{"value": value}, &saved); err != nil { + return err + } + printer.Print(saved, func() { + fmt.Printf("Secret %s saved. Run `oc agent deploy` to apply it.\n", saved.Name) + }) + return nil + }, +} + +var agentSecretDeleteCmd = &cobra.Command{ + Use: "delete ", + Aliases: []string{"rm"}, + Short: "Remove a Worker secret on the next deploy", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + sc, err := sessionsClient(cmd) + if err != nil { + return err + } + id, err := targetAgentID(cmd, sc, nil) + if err != nil { + return err + } + if err := sc.Delete(cmd.Context(), agentSecretPath(id, args[0])); err != nil { + return err + } + fmt.Printf("Secret %s removed. Run `oc agent deploy` to apply it.\n", args[0]) + return nil + }, +} + +func registerAgentConfig() { + for _, command := range []*cobra.Command{agentSecretListCmd, agentSecretSetCmd, agentSecretDeleteCmd} { + command.Flags().String("agent", "", "Target agent id or name (else the cwd agent.toml)") + } + agentSecretSetCmd.Flags().Bool("from-stdin", false, "Read the secret value from stdin") + agentSecretCmd.AddCommand(agentSecretListCmd, agentSecretSetCmd, agentSecretDeleteCmd) + agentCmd.AddCommand(agentSecretCmd) +} diff --git a/cmd/oc/internal/commands/agent_config_test.go b/cmd/oc/internal/commands/agent_config_test.go new file mode 100644 index 000000000..8243ad34e --- /dev/null +++ b/cmd/oc/internal/commands/agent_config_test.go @@ -0,0 +1,22 @@ +package commands + +import ( + "strings" + "testing" +) + +func TestAgentSecretSetRejectsPositionalValue(t *testing.T) { + if err := agentSecretSetCmd.Args(agentSecretSetCmd, []string{"TOKEN", "secret-value"}); err == nil { + t.Fatal("expected a positional secret value to be rejected") + } +} + +func TestAgentSecretSetRequiresStdinFlag(t *testing.T) { + if err := agentSecretSetCmd.Flags().Set("from-stdin", "false"); err != nil { + t.Fatal(err) + } + err := agentSecretSetCmd.RunE(agentSecretSetCmd, []string{"TOKEN"}) + if err == nil || !strings.Contains(err.Error(), "--from-stdin") { + t.Fatalf("error = %v, want --from-stdin requirement", err) + } +} diff --git a/cmd/oc/internal/commands/agent_deploy.go b/cmd/oc/internal/commands/agent_deploy.go index d877ca4e3..d909e7dc9 100644 --- a/cmd/oc/internal/commands/agent_deploy.go +++ b/cmd/oc/internal/commands/agent_deploy.go @@ -16,8 +16,11 @@ import ( // ── agent.toml manifest + deploy bundle ── type manifest struct { - Name string `toml:"name"` - Model string `toml:"model"` + Name string `toml:"name"` + Model string `toml:"model"` + // Non-secret Flue Worker bindings. The manifest is authoritative: omitting + // [vars] clears prior values on deploy. Secrets never belong in agent.toml. + Vars map[string]string `toml:"vars"` Runtime struct { Family string `toml:"family"` Type string `toml:"type"` diff --git a/cmd/oc/internal/commands/agent_deploy_flue.go b/cmd/oc/internal/commands/agent_deploy_flue.go index 398e474a6..35f3b3d72 100644 --- a/cmd/oc/internal/commands/agent_deploy_flue.go +++ b/cmd/oc/internal/commands/agent_deploy_flue.go @@ -1,40 +1,81 @@ package commands -// Flue deploy flow (design 012 §11.7.8, flue-slice.md W4 + contracts 3/4/5/10). -// When agent.toml declares `[runtime] family = "flue"`, `oc agent deploy` does -// NOT read prompt.md/skills/; instead it builds the app into a content-addressed -// artifact, uploads it via a presigned PUT, and references the digest in the -// deployment. The host boot-verifies the artifact (a ~30–60s scratch-sandbox -// probe) before activating the revision — the existing poll absorbs that latency. +// Flue deploy flow (design 013 §6 — the Worker-for-Platforms Durable-Object model). +// When agent.toml declares `[runtime] family = "flue"`, `oc agent deploy` does NOT +// read prompt.md/skills/; it runs the app's own `flue build --target cloudflare`, then +// stages only regular .js/.mjs modules as one tar.gz in R2 via a presigned PUT, and +// POSTs the deployment referencing only the R2 bundle digest + a small canonical +// Flue descriptor + the entrypoint agent name — NO module bytes in the JSON, so the +// API host stays byte-free. The CP records +// a `verifying` deploy; an off-host runner fetches the bundle, composes, mints the +// per-deploy token, WfP-uploads, and finalizes. The existing deployment poll absorbs +// the runner latency (verifying → ready|failed). import ( "bytes" "context" + "encoding/json" "fmt" "io" "io/fs" "net/http" "os" "os/exec" + pathpkg "path" "path/filepath" + "regexp" + "strings" "time" + "github.com/opensandbox/opensandbox/cmd/oc/internal/bundle" "github.com/opensandbox/opensandbox/cmd/oc/internal/client" "github.com/opensandbox/opensandbox/cmd/oc/internal/credscan" - "github.com/opensandbox/opensandbox/cmd/oc/internal/bundle" "github.com/spf13/cobra" ) const ( - flueBuildOutputDir = "dist-oc" // oc-flue-build's output (gitignored in the app) - flueArtifactMaxBytes = 64 << 20 // contract 3: server caps at 64 MiB — fail early + // flueBuildOutputDir is `flue build --target cloudflare`'s output root; the tool + // writes the Cloudflare build under dist// (wrangler.json + the entry module + + // assets/), so we discover the wrangler beneath it rather than assume a flat layout. + flueBuildOutputDir = "dist" + flueBundleMaxBytes = 64 << 20 // server caps the staged bundle at 64 MiB — fail early ) -// artifactUploadResponse is the reply from POST /v3/agents/:id/artifacts (contract 3). -// AlreadyUploaded is set (and URL omitted) when the content-addressed object already exists: -// R2 is write-once, so the server refuses to re-issue a PUT for a pinned digest (a re-issuable -// PUT would let scan-clean bytes be swapped for key-bearing ones post-verify). The CLI then -// skips the PUT and references the digest directly. +var ( + flueBindingIdentifier = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`) + flueCompatibilityFlag = regexp.MustCompile(`^[A-Za-z0-9_-]{1,128}$`) +) + +type flueDOBinding struct { + Name string `json:"name"` + ClassName string `json:"class_name"` +} + +type flueWranglerDescriptor struct { + Main string `json:"main"` + CompatibilityDate string `json:"compatibility_date"` + CompatibilityFlags []string `json:"compatibility_flags"` + NoBundle bool `json:"no_bundle"` + DurableObjects struct { + Bindings []flueDOBinding `json:"bindings"` + } `json:"durable_objects"` +} + +type generatedFlueWrangler struct { + Main string `json:"main"` + CompatibilityDate string `json:"compatibility_date"` + CompatibilityFlags []string `json:"compatibility_flags"` + NoBundle bool `json:"no_bundle"` + DurableObjects struct { + Bindings []json.RawMessage `json:"bindings"` + } `json:"durable_objects"` +} + +// artifactUploadResponse is the reply from POST /v3/agents/:id/artifacts. AlreadyUploaded +// is set (and URL omitted) when the content-addressed object already exists: R2 is +// write-once, so the server refuses to re-issue a PUT for a pinned digest (a re-issuable +// PUT would let scan-clean bytes be swapped for key-bearing ones post-verify). The CLI +// then skips the PUT and references the digest directly. type artifactUploadResponse struct { URL string `json:"url"` ExpiresAt string `json:"expires_at"` @@ -42,8 +83,8 @@ type artifactUploadResponse struct { } func deployFlue(cmd *cobra.Command, sc *client.Client, dir string, m *manifest, noActivate bool) error { - // 1. Primary credential scan over the user's pre-bundle sources (§11.2.6): - // model keys come from the OC credential, never from committed code. + // 1. Primary credential scan over the user's source (§11.2.6): model keys come + // from the OC credential, never from committed code. Stays client-side. findings, err := credscan.ScanDir(dir) if err != nil { return fmt.Errorf("credential scan: %w", err) @@ -57,49 +98,61 @@ func deployFlue(cmd *cobra.Command, sc *client.Client, dir string, m *manifest, } // 2. Resolve the target agent (create with runtime=flue + no prompt if new). - // Done before the build so a runtime-family mismatch fails fast — ahead - // of the build and the artifact upload, not after a late server reject. + // Done before the build so a runtime-family mismatch fails fast — ahead of the + // build and the upload, not after. id, err := resolveDeployAgent(cmd, sc, m) if err != nil { return err } + // [vars] is part of the deployment input even though the values live in the + // agent config resource. Persist it before enqueueing so the off-host runner + // cannot race ahead and compose the Worker with stale bindings. Secrets are + // intentionally CLI/API only and are resolved by that same runner. + if err := syncManifestVars(cmd, sc, id, m); err != nil { + return err + } - // 3. Build the artifact with the app's own @opencomputer/flue devDependency. + // 3. Build the app with its own `flue` CLI (a devDependency). if err := runFlueBuild(cmd.Context(), dir); err != nil { return err } - // 4. Pack dist-oc/ into the tar.gz and content-address it: the digest is - // sha256 of the blob the server and box will hash byte-for-byte. - outDir := filepath.Join(dir, flueBuildOutputDir) - files, err := readArtifactFiles(outDir) + // 4. Extract the strict descriptor and stage only regular module files. Raw + // wrangler.json contains build-local paths and never leaves this machine. + // The digest is sha256 of the blob the server and box will hash byte-for-byte. + files, wrangler, err := readFlueBundle(filepath.Join(dir, flueBuildOutputDir)) if err != nil { return err } tarGz, err := bundle.Pack(files) if err != nil { - return fmt.Errorf("pack artifact: %w", err) + return fmt.Errorf("pack bundle: %w", err) } digest := bundle.Digest(tarGz) - if len(tarGz) > flueArtifactMaxBytes { - return fmt.Errorf("artifact is %d bytes, over the %d MiB limit", len(tarGz), flueArtifactMaxBytes>>20) + if len(tarGz) > flueBundleMaxBytes { + return fmt.Errorf("bundle is %d bytes, over the %d MiB limit", len(tarGz), flueBundleMaxBytes>>20) } - // 5. Upload: presigned PUT (contract 3). + // 5. Upload: presigned PUT to R2 (the API host never sees the bytes). if err := uploadArtifact(cmd.Context(), sc, id, digest, tarGz); err != nil { return err } - // 6. Deployment referencing the digest (contract 10 — no prompt/skills path). + // 6. Deployment referencing the R2 bundle digest + the canonical descriptor (no + // module bytes in the JSON). The CP keys the flue-DO path off the agent's + // runtime="flue" + the presence of flue_bundle_digest/flue_wrangler, then hands + // off to the off-host runner (fetch → compose → mint → WfP-upload) → verifying. rt := m.Runtime.Type if rt == "" { rt = "default" } input := map[string]interface{}{ - "type": "inline", - "model": m.Model, - "runtime": map[string]string{"type": rt}, - "framework_artifact_digest": digest, + "type": "inline", + "model": m.Model, + "runtime": map[string]string{"type": rt}, + "flue_bundle_digest": digest, // sha256: of the tar.gz staged in R2 + "flue_wrangler": wrangler, // strict adapter descriptor; never raw wrangler.json + "flue_agent_name": m.Name, // entrypoint agent (agent.toml name → DO admit address) } body := map[string]interface{}{"input": input, "activate": !noActivate} if idem, _ := cmd.Flags().GetString("idempotency-key"); idem != "" { @@ -111,7 +164,7 @@ func deployFlue(cmd *cobra.Command, sc *client.Client, dir string, m *manifest, } d := env.Deployment - // 7. Poll to terminal — deploy boots a verify sandbox before activating. + // 7. Poll to terminal while the off-host runner uploads and finalizes the deployment. if !terminalState(d.State) && d.State != "" { to, _ := cmd.Flags().GetInt("timeout") d, err = pollDeployment(cmd, sc, id, d.ID, time.Duration(to)*time.Second) @@ -147,61 +200,240 @@ func resolveDeployAgent(cmd *cobra.Command, sc *client.Client, m *manifest) (str return id, err } -// runFlueBuild runs the app's oc-flue-build (its own devDependency). Prefers the -// locally-installed bin; falls back to `npx --no-install`, which runs the -// package only if it is already in node_modules and NEVER fetches a same-named -// package from the registry (a supply-chain hole). Build output goes to stderr -// so stdout stays clean for --json. +// runFlueBuild runs the app's own `flue` CLI: `flue build --target cloudflare`. +// Prefers the locally-installed bin; falls back to `npx --no-install`, which runs the +// package only if it is already in node_modules and NEVER fetches a same-named package +// from the registry (a supply-chain hole). Build output goes to stderr so stdout stays +// clean for --json. func runFlueBuild(ctx context.Context, dir string) error { - bin := filepath.Join(dir, "node_modules", ".bin", "oc-flue-build") + args := []string{"build", "--target", "cloudflare"} + bin := filepath.Join(dir, "node_modules", ".bin", "flue") var c *exec.Cmd if _, err := os.Stat(bin); err == nil { - c = exec.CommandContext(ctx, bin) + c = exec.CommandContext(ctx, bin, args...) } else { - c = exec.CommandContext(ctx, "npx", "--no-install", "oc-flue-build") + c = exec.CommandContext(ctx, "npx", append([]string{"--no-install", "flue"}, args...)...) } c.Dir = dir + c.Env = flueBuildEnv() c.Stdout = os.Stderr c.Stderr = os.Stderr - // No stdin: oc-flue-build is a non-interactive bundler, and wiring the - // terminal through let an npx install prompt hijack it. + // No stdin: `flue build` is a non-interactive bundler, and wiring the terminal + // through would let an npx install prompt hijack it. if err := c.Run(); err != nil { - return fmt.Errorf("oc-flue-build failed: %w\n(run `npm install` so @opencomputer/flue is available, node >= 22)", err) + return fmt.Errorf("flue build failed: %w\n(run `npm install` so the flue CLI is available, node >= 22.19)", err) } return nil } -// readArtifactFiles walks the build output into a fileset with normalized modes, -// requiring artifact.json (the manifest the host validates + pins). -func readArtifactFiles(outDir string) ([]bundle.File, error) { - if info, err := os.Stat(outDir); err != nil || !info.IsDir() { - return nil, fmt.Errorf("build output %s not found — did oc-flue-build run?", outDir) +// flueBuildEnv keeps Wrangler's platform-deployment warnings out of `oc agent +// deploy`. OpenComputer consumes a strict subset of the generated descriptor and +// owns the eventual Worker-for-Platforms composition, so warnings about directly +// deploying that scratch Wrangler config (notably declarative Durable Object +// exports) are not actionable here. Errors still print. An explicit +// WRANGLER_LOG setting wins, which leaves a debugging escape hatch. +func flueBuildEnv() []string { + const key = "WRANGLER_LOG" + const fallback = key + "=error" + env := os.Environ() + prefix := key + "=" + for i, value := range env { + if !strings.HasPrefix(value, prefix) { + continue + } + if value == prefix { + env[i] = fallback + } + return env + } + return append(env, fallback) +} + +// readFlueBundle locates the generated wrangler, extracts the exact Flue descriptor, +// and reads only regular .js/.mjs modules rooted at the wrangler's directory. The raw +// wrangler resolution dump, .vite state and source maps are known control artifacts and +// are never archived; any other non-module fails loudly instead of producing a broken deploy. +func readFlueBundle(distDir string) ([]bundle.File, flueWranglerDescriptor, error) { + wranglerPath, err := findGeneratedWrangler(distDir) + if err != nil { + return nil, flueWranglerDescriptor{}, err + } + raw, err := os.ReadFile(wranglerPath) + if err != nil { + return nil, flueWranglerDescriptor{}, fmt.Errorf("read %s: %w", wranglerPath, err) + } + wrangler, err := extractFlueWranglerDescriptor(raw) + if err != nil { + return nil, flueWranglerDescriptor{}, fmt.Errorf("parse %s: %w", wranglerPath, err) + } + + bundleRoot := filepath.Dir(wranglerPath) + files, err := readBundleModules(bundleRoot) + if err != nil { + return nil, flueWranglerDescriptor{}, err + } + // The entry module wrangler.main names MUST be in the bundle (the runner uploads it + // to WfP as metadata.main_module). + mainRel := wrangler.Main + found := false + for _, f := range files { + if f.Path == mainRel { + found = true + break + } + } + if !found { + return nil, flueWranglerDescriptor{}, fmt.Errorf("entry module %q (wrangler.main) is not in the module output %s", mainRel, bundleRoot) + } + return files, wrangler, nil +} + +func safeFlueModulePath(value string) bool { + if value == "" || strings.HasPrefix(value, "/") || strings.Contains(value, `\`) { + return false + } + if ext := pathpkg.Ext(value); ext != ".js" && ext != ".mjs" { + return false + } + if pathpkg.Clean(value) != value { + return false + } + for _, segment := range strings.Split(value, "/") { + if segment == "" || segment == "." || segment == ".." { + return false + } + } + return true +} + +func extractFlueWranglerDescriptor(raw []byte) (flueWranglerDescriptor, error) { + var generated generatedFlueWrangler + if err := json.Unmarshal(raw, &generated); err != nil { + return flueWranglerDescriptor{}, err + } + if !safeFlueModulePath(generated.Main) { + return flueWranglerDescriptor{}, fmt.Errorf("main must be a safe relative .js/.mjs module path") + } + if parsed, err := time.Parse("2006-01-02", generated.CompatibilityDate); err != nil || parsed.Format("2006-01-02") != generated.CompatibilityDate { + return flueWranglerDescriptor{}, fmt.Errorf("compatibility_date must be a valid YYYY-MM-DD date") + } + if generated.CompatibilityFlags == nil || len(generated.CompatibilityFlags) > 64 { + return flueWranglerDescriptor{}, fmt.Errorf("compatibility_flags must be a unique array of valid flag names") + } + seenFlags := map[string]bool{} + for _, flag := range generated.CompatibilityFlags { + if !flueCompatibilityFlag.MatchString(flag) || seenFlags[flag] { + return flueWranglerDescriptor{}, fmt.Errorf("compatibility_flags must be a unique array of valid flag names") + } + seenFlags[flag] = true + } + if !generated.NoBundle { + return flueWranglerDescriptor{}, fmt.Errorf("no_bundle must be true") + } + + bindings := make([]flueDOBinding, 0, len(generated.DurableObjects.Bindings)) + names := map[string]bool{} + classes := map[string]bool{} + for _, rawBinding := range generated.DurableObjects.Bindings { + var fields map[string]json.RawMessage + if err := json.Unmarshal(rawBinding, &fields); err != nil { + return flueWranglerDescriptor{}, fmt.Errorf("invalid durable-object binding: %w", err) + } + if len(fields) != 2 || fields["name"] == nil || fields["class_name"] == nil { + return flueWranglerDescriptor{}, fmt.Errorf("durable-object bindings may contain only name and class_name") + } + var binding flueDOBinding + if err := json.Unmarshal(rawBinding, &binding); err != nil { + return flueWranglerDescriptor{}, fmt.Errorf("invalid durable-object binding: %w", err) + } + if !flueBindingIdentifier.MatchString(binding.Name) || !flueBindingIdentifier.MatchString(binding.ClassName) { + return flueWranglerDescriptor{}, fmt.Errorf("durable-object binding names and class names must be non-empty JavaScript identifiers") + } + if names[binding.Name] || classes[binding.ClassName] { + return flueWranglerDescriptor{}, fmt.Errorf("durable-object binding names and class names must be unique") + } + names[binding.Name] = true + classes[binding.ClassName] = true + bindings = append(bindings, binding) + } + if len(bindings) == 0 { + return flueWranglerDescriptor{}, fmt.Errorf("at least one same-script durable-object binding is required") + } + + var descriptor flueWranglerDescriptor + descriptor.Main = generated.Main + descriptor.CompatibilityDate = generated.CompatibilityDate + descriptor.CompatibilityFlags = append([]string(nil), generated.CompatibilityFlags...) + descriptor.NoBundle = true + descriptor.DurableObjects.Bindings = bindings + return descriptor, nil +} + +// findGeneratedWrangler returns the generated wrangler.json path — dist/wrangler.json +// (flat) or the single dist//wrangler.json `flue build` writes. Zero or more than +// one candidate is an error (nothing built / ambiguous output). +func findGeneratedWrangler(distDir string) (string, error) { + if info, err := os.Stat(distDir); err != nil || !info.IsDir() { + return "", fmt.Errorf("build output %s not found — did `flue build --target cloudflare` run?", distDir) + } + flat := filepath.Join(distDir, "wrangler.json") + if _, err := os.Stat(flat); err == nil { + return flat, nil + } + matches, _ := filepath.Glob(filepath.Join(distDir, "*", "wrangler.json")) + switch len(matches) { + case 0: + return "", fmt.Errorf("no wrangler.json under %s — `flue build --target cloudflare` produced no Cloudflare build", distDir) + case 1: + return matches[0], nil + default: + return "", fmt.Errorf("multiple wrangler.json under %s (%v) — ambiguous flue build output", distDir, matches) + } +} + +// readBundleModules walks the output into a module-only fileset with normalized modes +// and forward-slash, root-relative paths. Symlinks, special files and unexpected regular +// files fail closed. Only the generated wrangler, source maps and .vite state are ignored. +func readBundleModules(root string) ([]bundle.File, error) { + if info, err := os.Stat(root); err != nil || !info.IsDir() { + return nil, fmt.Errorf("build output %s not found — did `flue build --target cloudflare` run?", root) } var files []bundle.File - hasManifest := false - err := filepath.WalkDir(outDir, func(p string, d fs.DirEntry, walkErr error) error { + err := filepath.WalkDir(root, func(p string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() { + if p != root && d.Name() == ".vite" { + return filepath.SkipDir + } return nil } - rel, err := filepath.Rel(outDir, p) + if d.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("build output contains symlink %s; only regular modules are allowed", p) + } + rel, err := filepath.Rel(root, p) if err != nil { return err } rel = filepath.ToSlash(rel) - content, err := os.ReadFile(p) + st, err := d.Info() if err != nil { return err } - st, err := d.Info() + if !st.Mode().IsRegular() { + return fmt.Errorf("build output contains non-regular file %s", p) + } + if rel == "wrangler.json" || strings.HasSuffix(rel, ".map") { + return nil + } + if !safeFlueModulePath(rel) { + return fmt.Errorf("build output contains unsupported file %q; expected only .js/.mjs modules", rel) + } + content, err := os.ReadFile(p) if err != nil { return err } - if rel == "artifact.json" { - hasManifest = true - } files = append(files, bundle.File{ Path: rel, Mode: bundle.NormalizeMode(int(st.Mode().Perm())), @@ -210,32 +442,29 @@ func readArtifactFiles(outDir string) ([]bundle.File, error) { return nil }) if err != nil { - return nil, fmt.Errorf("read %s: %w", outDir, err) + return nil, fmt.Errorf("read %s: %w", root, err) } if len(files) == 0 { - return nil, fmt.Errorf("build output %s is empty", outDir) - } - if !hasManifest { - return nil, fmt.Errorf("%s/artifact.json missing — build did not produce a valid artifact", outDir) + return nil, fmt.Errorf("build output %s is empty", root) } return files, nil } -// uploadArtifact requests a presigned PUT URL, then PUTs the bundle to it. The -// PUT carries no OC auth (the signature is in the URL); Content-Type must match -// what the server signed for the object (application/gzip). +// uploadArtifact requests a presigned PUT URL, then PUTs the bundle to it. The PUT +// carries no OC auth (the signature is in the URL); Content-Type must match what the +// server signed for the object (application/gzip). func uploadArtifact(ctx context.Context, sc *client.Client, agentID, digest string, tarGz []byte) error { reqBody := map[string]interface{}{"digest": digest, "size_bytes": len(tarGz)} var resp artifactUploadResponse if err := sc.Post(ctx, "/v3/agents/"+agentID+"/artifacts", reqBody, &resp); err != nil { - return fmt.Errorf("request artifact upload url: %w", err) + return fmt.Errorf("request bundle upload url: %w", err) } if resp.AlreadyUploaded { // The digest's bytes are already in R2 (write-once); nothing to PUT. return nil } if resp.URL == "" { - return fmt.Errorf("artifact upload url response was empty") + return fmt.Errorf("bundle upload url response was empty") } req, err := http.NewRequestWithContext(ctx, http.MethodPut, resp.URL, bytes.NewReader(tarGz)) if err != nil { @@ -245,12 +474,12 @@ func uploadArtifact(ctx context.Context, sc *client.Client, agentID, digest stri req.ContentLength = int64(len(tarGz)) put, err := http.DefaultClient.Do(req) if err != nil { - return fmt.Errorf("upload artifact: %w", err) + return fmt.Errorf("upload bundle: %w", err) } defer put.Body.Close() if put.StatusCode >= 300 { snippet, _ := io.ReadAll(io.LimitReader(put.Body, 512)) - return fmt.Errorf("artifact upload failed (HTTP %d): %s", put.StatusCode, string(snippet)) + return fmt.Errorf("bundle upload failed (HTTP %d): %s", put.StatusCode, string(snippet)) } return nil } diff --git a/cmd/oc/internal/commands/agent_deploy_flue_test.go b/cmd/oc/internal/commands/agent_deploy_flue_test.go index 41ca04bfb..662eb050d 100644 --- a/cmd/oc/internal/commands/agent_deploy_flue_test.go +++ b/cmd/oc/internal/commands/agent_deploy_flue_test.go @@ -1,12 +1,15 @@ package commands -// Contract-mock end-to-end for the Flue deploy flow. A fake control plane -// implements contract 3 (POST /v3/agents/:id/artifacts + the presigned PUT) and -// contract 10 (deployment create + a verifying→ready poll). A throwaway -// `node_modules/.bin/oc-flue-build` script stands in for @opencomputer/flue, so -// the real runFlueBuild exec path runs and produces a dist-oc/. deployFlue is -// driven end to end; we assert the uploaded bytes are byte-exactly the canonical -// bundle for the digest the CLI advertised — the whole content-address chain. +// Contract-mock end-to-end for the Flue DO deploy flow (design 013 §6). A fake control +// plane implements the presigned-PUT bundle upload (POST /v3/agents/:id/artifacts + the +// signed PUT) and the deployment create + a verifying→ready poll. A throwaway +// `node_modules/.bin/flue` script stands in for the app's flue CLI, so the real +// runFlueBuild exec path runs and produces a deliberately noisy dist//. deployFlue +// is driven end to end; we assert the uploaded bytes are byte-exactly the canonical +// module-only tar.gz for the digest the CLI +// advertised (the content-address chain), and that the POSTed deployment body is the +// byte-free DO request: flue_bundle_digest + the canonical flue_wrangler descriptor + +// flue_agent_name, with no raw Wrangler dump, module bytes, or framework_artifact_digest. import ( "bytes" @@ -21,22 +24,28 @@ import ( "sync" "testing" - "github.com/opensandbox/opensandbox/cmd/oc/internal/client" "github.com/opensandbox/opensandbox/cmd/oc/internal/bundle" + "github.com/opensandbox/opensandbox/cmd/oc/internal/client" "github.com/opensandbox/opensandbox/cmd/oc/internal/output" "github.com/spf13/cobra" ) -// The exact bytes the fake build emits into dist-oc/ (heredoc appends a newline). +// The exact bytes the fake `flue build` emits into dist/e2e_flue/ (heredoc appends a +// trailing newline). The build is nested a directory deep, with an assets/ file, to +// exercise the dist// discovery and prove the WHOLE tree is staged. const ( - e2eArtifactBody = `{"entry":"oc.js","profile_version":1,"model":"anthropic/claude-sonnet-5"}` - e2eOcBody = `export const agent = "e2e";` + fakeAgentID = "agt_0123456789abcdef01234567" + e2eModuleBody = `export default { fetch() { return new Response("ok"); } };` + e2eAssetBody = `export const chunk = 1;` + e2eRuntimeBody = `export const runtime = "flue";` + e2eWranglerBody = `{"$schema":"../../node_modules/wrangler/config-schema.json","name":"e2e-flue","main":"index.js","compatibility_date":"2026-04-01","compatibility_flags":["nodejs_compat"],"no_bundle":true,"configPath":"/Users/developer/project/flue.config.ts","userConfigPath":"/Users/developer/project/wrangler.json","durable_objects":{"bindings":[{"name":"AGENT","class_name":"FlueE2EAgent"},{"name":"FLUE_REGISTRY","class_name":"FlueRegistry"}]},"vars":{"MUST_NOT_LEAVE":"raw-wrangler"},"migrations":[{"tag":"attacker-owned","new_sqlite_classes":["Wrong"]}],"routes":["example.com/*"],"services":[{"binding":"OTHER","service":"victim"}]}` ) type fakeCP struct { mu sync.Mutex self string createBody map[string]any + configPutBody map[string]any artifactDigest string artifactSize float64 uploaded []byte @@ -56,8 +65,13 @@ func (f *fakeCP) ServeHTTP(w http.ResponseWriter, r *http.Request) { writeJSON(map[string]any{"data": []any{}}) case r.Method == "POST" && r.URL.Path == "/v3/agents": _ = json.NewDecoder(r.Body).Decode(&f.createBody) - writeJSON(map[string]any{"id": "agt_e2e", "name": "e2e-flue", "model": "anthropic/claude-sonnet-5", "runtime": "flue"}) - case r.Method == "POST" && r.URL.Path == "/v3/agents/agt_e2e/artifacts": + writeJSON(map[string]any{"id": fakeAgentID, "name": "e2e-flue", "model": "anthropic/claude-sonnet-5", "runtime": "flue"}) + case r.Method == "PUT" && r.URL.Path == "/v3/agents/"+fakeAgentID+"/config": + _ = json.NewDecoder(r.Body).Decode(&f.configPutBody) + writeJSON(map[string]any{ + "vars": f.configPutBody["vars"], "deployment_required": true, + }) + case r.Method == "POST" && r.URL.Path == "/v3/agents/"+fakeAgentID+"/artifacts": var body map[string]any _ = json.NewDecoder(r.Body).Decode(&body) f.artifactDigest, _ = body["digest"].(string) @@ -72,44 +86,51 @@ func (f *fakeCP) ServeHTTP(w http.ResponseWriter, r *http.Request) { _, _ = buf.ReadFrom(r.Body) f.uploaded = buf.Bytes() w.WriteHeader(http.StatusOK) - case r.Method == "POST" && r.URL.Path == "/v3/agents/agt_e2e/deployments": + case r.Method == "POST" && r.URL.Path == "/v3/agents/"+fakeAgentID+"/deployments": _ = json.NewDecoder(r.Body).Decode(&f.deployBody) writeJSON(map[string]any{"deployment": map[string]any{"id": "dep_1", "state": "verifying"}}) - case r.Method == "GET" && r.URL.Path == "/v3/agents/agt_e2e/deployments/dep_1": + case r.Method == "GET" && r.URL.Path == "/v3/agents/"+fakeAgentID+"/deployments/dep_1": f.getCount++ if f.getCount < 2 { writeJSON(map[string]any{"id": "dep_1", "state": "verifying"}) // verifying → … } else { writeJSON(map[string]any{"id": "dep_1", "state": "ready", "active": true, "revision_id": "rev_1"}) // → terminal } - case r.Method == "GET" && r.URL.Path == "/v3/agents/agt_e2e/revisions": + case r.Method == "GET" && r.URL.Path == "/v3/agents/"+fakeAgentID+"/revisions": writeJSON(map[string]any{"data": []any{}}) default: http.Error(w, "unexpected "+r.Method+" "+r.URL.Path, http.StatusNotFound) } } -func TestDeployFlueEndToEnd(t *testing.T) { +func TestDeployFlueDoEndToEnd(t *testing.T) { if runtime.GOOS == "windows" { - t.Skip("fake oc-flue-build is a POSIX shell script") + t.Skip("fake flue build is a POSIX shell script") } + t.Setenv("WRANGLER_LOG", "") - // A Flue app dir: manifest + clean source + a stand-in build bin. + // A Flue app dir: manifest + clean source + a stand-in `flue` bin whose + // `build --target cloudflare` writes a deterministic but noisy dist/e2e_flue/. dir := t.TempDir() writeFile(t, filepath.Join(dir, "agent.toml"), "name = \"e2e-flue\"\nmodel = \"anthropic/claude-sonnet-5\"\n\n[runtime]\nfamily = \"flue\"\n", 0o644) writeFile(t, filepath.Join(dir, "src", "opencomputer.ts"), "import { serveOC } from '@opencomputer/flue';\nexport default serveOC(agent);\n", 0o644) - // The build emits a deterministic dist-oc/ (mirrors what oc-flue-build would). - buildScript := "#!/bin/sh\nset -e\nmkdir -p dist-oc\n" + - "cat > dist-oc/artifact.json <<'JSON'\n" + e2eArtifactBody + "\nJSON\n" + - "cat > dist-oc/oc.js <<'JS'\n" + e2eOcBody + "\nJS\n" - writeFile(t, filepath.Join(dir, "node_modules", ".bin", "oc-flue-build"), buildScript, 0o755) + buildScript := "#!/bin/sh\nset -e\ntest \"${WRANGLER_LOG:-}\" = error\nmkdir -p dist/e2e_flue/assets dist/e2e_flue/.vite dist/e2e_flue/.flue-vite\n" + + "cat > dist/e2e_flue/index.js <<'JS'\n" + e2eModuleBody + "\nJS\n" + + "cat > dist/e2e_flue/assets/chunk.js <<'JS'\n" + e2eAssetBody + "\nJS\n" + + "cat > dist/e2e_flue/.flue-vite/runtime.mjs <<'JS'\n" + e2eRuntimeBody + "\nJS\n" + + "printf '%s' '{\"version\":3}' > dist/e2e_flue/.vite/manifest.json\n" + + "printf '%s' '{\"version\":3}' > dist/e2e_flue/index.js.map\n" + + "cat > dist/e2e_flue/wrangler.json <<'JSON'\n" + e2eWranglerBody + "\nJSON\n" + writeFile(t, filepath.Join(dir, "node_modules", ".bin", "flue"), buildScript, 0o755) - // What the CLI must produce from that dist-oc/ (mode normalized to 0644). + // Only regular modules leave the machine. Raw Wrangler metadata, .vite state and + // source maps are absent; .flue-vite is a legitimate module path. expectedFiles := []bundle.File{ - {Path: "artifact.json", Mode: 0o644, Content: []byte(e2eArtifactBody + "\n")}, - {Path: "oc.js", Mode: 0o644, Content: []byte(e2eOcBody + "\n")}, + {Path: "index.js", Mode: 0o644, Content: []byte(e2eModuleBody + "\n")}, + {Path: "assets/chunk.js", Mode: 0o644, Content: []byte(e2eAssetBody + "\n")}, + {Path: ".flue-vite/runtime.mjs", Mode: 0o644, Content: []byte(e2eRuntimeBody + "\n")}, } expectedTarGz, err := bundle.Pack(expectedFiles) if err != nil { @@ -149,12 +170,11 @@ func TestDeployFlueEndToEnd(t *testing.T) { t.Errorf("create body carried a prompt for a flue agent: %v", f.createBody["prompt"]) } - // Content address the CLI advertised == the digest of the known fileset. + // The content address the CLI advertised == the digest of the module-only bundle, and + // the PUT body is byte-exactly that canonical tar.gz — the full upload integrity chain. if f.artifactDigest != expectedDigest { t.Errorf("advertised digest = %s, want %s", f.artifactDigest, expectedDigest) } - // size_bytes is honest, and the PUT body is byte-exactly the canonical bundle - // for that digest — the full upload integrity chain. if int(f.artifactSize) != len(f.uploaded) { t.Errorf("size_bytes %d != uploaded len %d", int(f.artifactSize), len(f.uploaded)) } @@ -162,21 +182,207 @@ func TestDeployFlueEndToEnd(t *testing.T) { t.Errorf("uploaded bytes are not the canonical bundle for the digest (len got %d want %d)", len(f.uploaded), len(expectedTarGz)) } - // Deployment referenced the same digest, via input.framework_artifact_digest. + // The deployment body is the byte-free DO request. input, _ := f.deployBody["input"].(map[string]any) if input == nil { t.Fatalf("deployment body had no input: %v", f.deployBody) } - if input["framework_artifact_digest"] != expectedDigest { - t.Errorf("deployment digest = %v, want %s", input["framework_artifact_digest"], expectedDigest) + if input["type"] != "inline" { + t.Errorf("input.type = %v, want inline", input["type"]) + } + if f.deployBody["activate"] != true { + t.Errorf("activate = %v, want true (no --no-activate)", f.deployBody["activate"]) + } + if input["flue_bundle_digest"] != expectedDigest { + t.Errorf("flue_bundle_digest = %v, want %s", input["flue_bundle_digest"], expectedDigest) + } + if input["flue_agent_name"] != "e2e-flue" { + t.Errorf("flue_agent_name = %v, want e2e-flue", input["flue_agent_name"]) + } + // No module bytes, no pre-013 digest field. + if _, ok := input["flue_module"]; ok { + t.Errorf("byte-free contract violated: input carried flue_module: %v", input["flue_module"]) } if _, ok := input["prompt"]; ok { - t.Errorf("deployment input carried a prompt: %v", input["prompt"]) + t.Errorf("flue DO deploy carried a prompt: %v", input["prompt"]) + } + if _, ok := input["framework_artifact_digest"]; ok { + t.Errorf("flue DO deploy carried a framework_artifact_digest: %v", input["framework_artifact_digest"]) + } + + // flue_wrangler is the exact canonical descriptor, not the generated resolution dump. + wr, _ := input["flue_wrangler"].(map[string]any) + if wr == nil { + t.Fatalf("input.flue_wrangler missing: %v", input) } + if wr["main"] != "index.js" { + t.Errorf("flue_wrangler.main = %v, want index.js", wr["main"]) + } + if wr["durable_objects"] == nil { + t.Errorf("flue_wrangler lost durable_objects: %v", wr) + } + wantKeys := map[string]bool{ + "main": true, "compatibility_date": true, "compatibility_flags": true, + "no_bundle": true, "durable_objects": true, + } + if len(wr) != len(wantKeys) { + t.Errorf("flue_wrangler keys = %v, want only canonical descriptor", wr) + } + for key := range wr { + if !wantKeys[key] { + t.Errorf("raw Wrangler capability %q escaped into deployment: %v", key, wr[key]) + } + } + if wr["compatibility_date"] != "2026-04-01" || wr["no_bundle"] != true { + t.Errorf("flue_wrangler profile changed: %v", wr) + } + encodedWrangler, _ := json.Marshal(wr) + for _, leaked := range []string{"/Users/developer", "MUST_NOT_LEAVE", "attacker-owned", "example.com", "victim"} { + if strings.Contains(string(encodedWrangler), leaked) { + t.Errorf("raw Wrangler value %q escaped into deployment: %s", leaked, encodedWrangler) + } + } + // The verifying→ready sequence was actually polled. if f.getCount < 2 { t.Errorf("expected the poll to observe verifying→ready (got %d GETs)", f.getCount) } + if vars, ok := f.configPutBody["vars"].(map[string]any); !ok || len(vars) != 0 { + t.Errorf("manifest without [vars] should clear desired vars, got %#v", f.configPutBody) + } +} + +func TestFlueBuildEnvPreservesExplicitWranglerLog(t *testing.T) { + t.Setenv("WRANGLER_LOG", "debug") + for _, value := range flueBuildEnv() { + if value == "WRANGLER_LOG=debug" { + return + } + } + t.Fatal("flueBuildEnv did not preserve explicit WRANGLER_LOG=debug") +} + +func TestExtractFlueWranglerDescriptorRejectsUnsafeInput(t *testing.T) { + valid := func() map[string]any { + var value map[string]any + if err := json.Unmarshal([]byte(e2eWranglerBody), &value); err != nil { + t.Fatal(err) + } + return value + } + tests := []struct { + name string + mutate func(map[string]any) + }{ + {name: "unsafe main", mutate: func(value map[string]any) { value["main"] = "../index.js" }}, + {name: "invalid compatibility date", mutate: func(value map[string]any) { value["compatibility_date"] = "2026-02-30" }}, + {name: "invalid compatibility flag", mutate: func(value map[string]any) { + value["compatibility_flags"] = []any{"nodejs_compat", "unsafe flag"} + }}, + {name: "duplicate compatibility flag", mutate: func(value map[string]any) { + value["compatibility_flags"] = []any{"nodejs_compat", "nodejs_compat"} + }}, + {name: "bundling enabled", mutate: func(value map[string]any) { value["no_bundle"] = false }}, + {name: "foreign script binding", mutate: func(value map[string]any) { + bindings := value["durable_objects"].(map[string]any)["bindings"].([]any) + bindings[0].(map[string]any)["script_name"] = "victim-worker" + }}, + {name: "missing durable objects", mutate: func(value map[string]any) { + value["durable_objects"].(map[string]any)["bindings"] = []any{} + }}, + {name: "duplicate binding name", mutate: func(value map[string]any) { + value["durable_objects"].(map[string]any)["bindings"] = []any{ + map[string]any{"name": "FLUE_REGISTRY", "class_name": "FlueE2EAgent"}, + map[string]any{"name": "FLUE_REGISTRY", "class_name": "FlueRegistry"}, + } + }}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + value := valid() + tc.mutate(value) + raw, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + if _, err := extractFlueWranglerDescriptor(raw); err == nil { + t.Fatalf("expected %s to be rejected", tc.name) + } + }) + } +} + +func TestExtractFlueWranglerDescriptorPreservesBuildProfile(t *testing.T) { + var value map[string]any + if err := json.Unmarshal([]byte(e2eWranglerBody), &value); err != nil { + t.Fatal(err) + } + value["compatibility_date"] = "2026-07-01" + value["compatibility_flags"] = []any{"nodejs_compat", "nodejs_als"} + bindings := value["durable_objects"].(map[string]any)["bindings"].([]any) + bindings[1] = map[string]any{"name": "FLUE_STATE", "class_name": "FlueState"} + + raw, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + descriptor, err := extractFlueWranglerDescriptor(raw) + if err != nil { + t.Fatalf("extract descriptor: %v", err) + } + if descriptor.CompatibilityDate != "2026-07-01" { + t.Fatalf("compatibility date = %q", descriptor.CompatibilityDate) + } + if strings.Join(descriptor.CompatibilityFlags, ",") != "nodejs_compat,nodejs_als" { + t.Fatalf("compatibility flags = %v", descriptor.CompatibilityFlags) + } + if descriptor.DurableObjects.Bindings[1].Name != "FLUE_STATE" || descriptor.DurableObjects.Bindings[1].ClassName != "FlueState" { + t.Fatalf("renamed Flue internal binding was not preserved: %v", descriptor.DurableObjects.Bindings) + } +} + +func TestReadBundleModulesRejectsSymlink(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink semantics differ on Windows") + } + root := t.TempDir() + outside := filepath.Join(t.TempDir(), "outside.js") + writeFile(t, outside, "export const secret = true;", 0o644) + writeFile(t, filepath.Join(root, "index.js"), "export {};", 0o644) + if err := os.Symlink(outside, filepath.Join(root, "leak.js")); err != nil { + t.Fatal(err) + } + if _, err := readBundleModules(root); err == nil || !strings.Contains(err.Error(), "symlink") { + t.Fatalf("expected symlink rejection, got %v", err) + } +} + +func TestReadBundleModulesRejectsUnexpectedRegularFile(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "index.js"), "export {};", 0o644) + writeFile(t, filepath.Join(root, "runtime.wasm"), "not really wasm", 0o644) + if _, err := readBundleModules(root); err == nil || !strings.Contains(err.Error(), "unsupported file") { + t.Fatalf("expected unsupported-file rejection, got %v", err) + } +} + +func TestSyncManifestVarsReplacesDesiredVars(t *testing.T) { + f := &fakeCP{} + srv := httptest.NewServer(f) + defer srv.Close() + f.self = srv.URL + + sc := client.NewSessionsAPI(srv.URL, "test-key") + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + m := &manifest{Vars: map[string]string{"PUBLIC_MODE": "careful", "MAX_ITEMS": "12"}} + if err := syncManifestVars(cmd, sc, fakeAgentID, m); err != nil { + t.Fatalf("syncManifestVars: %v", err) + } + vars, _ := f.configPutBody["vars"].(map[string]any) + if vars["PUBLIC_MODE"] != "careful" || vars["MAX_ITEMS"] != "12" { + t.Errorf("vars PUT = %#v", f.configPutBody["vars"]) + } } func TestDeployFlueBlocksOnLeakedKey(t *testing.T) { diff --git a/cmd/oc/internal/commands/update_check.go b/cmd/oc/internal/commands/update_check.go index 74ed655e0..f71159b49 100644 --- a/cmd/oc/internal/commands/update_check.go +++ b/cmd/oc/internal/commands/update_check.go @@ -29,7 +29,7 @@ type updateCheckCache struct { // per 24h. Any error bails silently — the CLI must never block or warn // on a failed version check. func maybePromptUpdate() { - if Version == "dev" { + if !isReleaseVersion(Version) { return } if os.Getenv("OC_NO_UPDATE_CHECK") != "" { @@ -75,6 +75,27 @@ func maybePromptUpdate() { } } +// isReleaseVersion distinguishes published dotted-numeric versions from local +// builds labelled with a branch, commit, or the default "dev". Comparing a label +// such as "flue-native-67bf0ee" as numeric zero produces a bogus update nag. +func isReleaseVersion(version string) bool { + parts := strings.Split(version, ".") + if len(parts) < 2 { + return false + } + for _, part := range parts { + if part == "" { + return false + } + for _, r := range part { + if r < '0' || r > '9' { + return false + } + } + } + return true +} + func updateCheckCachePath() (string, error) { dir, err := os.UserCacheDir() if err != nil { diff --git a/cmd/oc/internal/commands/update_check_test.go b/cmd/oc/internal/commands/update_check_test.go new file mode 100644 index 000000000..2e461b02d --- /dev/null +++ b/cmd/oc/internal/commands/update_check_test.go @@ -0,0 +1,26 @@ +package commands + +import "testing" + +func TestIsReleaseVersion(t *testing.T) { + t.Parallel() + cases := map[string]bool{ + "0.6.0.8": true, + "1.0": true, + "dev": false, + "flue-native-67bf0ee": false, + "0.6.0.8-dirty": false, + "v0.6.0.8": false, + "0": false, + "0..8": false, + } + for version, want := range cases { + version, want := version, want + t.Run(version, func(t *testing.T) { + t.Parallel() + if got := isReleaseVersion(version); got != want { + t.Fatalf("isReleaseVersion(%q) = %v, want %v", version, got, want) + } + }) + } +} diff --git a/docs/agent-sessions/agents.mdx b/docs/agent-sessions/agents.mdx index 952eb5be4..ff2eb4341 100644 --- a/docs/agent-sessions/agents.mdx +++ b/docs/agent-sessions/agents.mdx @@ -4,9 +4,9 @@ title: "Agents" description: "Reusable agent configuration" --- -An **agent** is reusable configuration: `name`, `prompt`, `model`, [`runtime`](/agent-sessions/runtimes), and optional [**skills**](/agent-sessions/revisions). Every [session](/agent-sessions/sessions) runs on an agent, so create one first, then start as many sessions on it as you like. The behavior (`prompt`/`model`/`skills`) is versioned as [**revisions**](/agent-sessions/revisions): every [deployment](/agent-sessions/revisions) — from the API, the CLI, or a [GitHub push](/agent-sessions/revisions#deploy-from-a-repo) — appends a new one, and you can roll back instantly. +An **agent** is a reusable identity, runtime, model, and deployed behavior. Every [session](/agent-sessions/sessions) runs on an agent, so create one first, then start as many sessions on it as you like. For the built-in runtimes, behavior is `prompt` plus optional skills and is frozen into independently routable [revisions](/agent-sessions/revisions). A [Flue](/agent-sessions/flue) agent instead carries its instructions, tools, and packaged skills in a compiled Flue app. -You can create and edit agents in the [dashboard](https://app.opencomputer.dev) too — the create dialog also lets you pick or add a [credential](/agent-sessions/credentials) inline. +You can create and edit built-in runtime agents in the [dashboard](https://app.opencomputer.dev). Flue agents are created and deployed from their app with `oc agent deploy`, then appear in the same agent and session views. @@ -46,7 +46,7 @@ Pass your model `key` inline and it's stored as a [credential](/agent-sessions/c ## Start simple, add capability as you go -The only thing an agent needs is a **model** and a **prompt** — create one and it runs on a **default, managed runtime that just works**. Everything beyond that is **additive**: reach for a rung only when you need it, and nothing earlier is undone. +For a built-in runtime, the only behavior an agent needs is a **model** and a **prompt**. Everything beyond that is additive: reach for a rung only when you need it, and nothing earlier is undone. Flue has a separate [app deployment path](/agent-sessions/flue#add-opencomputer-to-a-flue-app). | Rung | What you add | What you get | |---|---|---| @@ -55,12 +55,16 @@ The only thing an agent needs is a **model** and a **prompt** — create one and | **+ Deploy from a repo / CLI** | keep the agent's `prompt.md` + `skills/` in Git and [push to deploy](/agent-sessions/revisions#deploy-from-a-repo) | versioned, push-to-deploy, staged revisions + rollback — still the default runtime | | **+ Custom runtime** *(coming soon)* | bring your own runtime image | full control of the execution environment | -At every rung the agent is just a small directory — at most `agent.toml` + `prompt.md` + `skills/`. You can stop at any rung; most agents never go past skills. +At every built-in rung the agent is a small directory, at most `agent.toml` + `prompt.md` + `skills/`. You can stop at any rung; most agents never go past skills. ## Session-pinned config -When a session is created it **freezes** the agent's **active [revision](/agent-sessions/revisions)** — `prompt`, `model`, `runtime`, and `skills` — into the session, and pins the resolved credential too. (A session can start with a one-off [`model`](/agent-sessions/sessions#model) override in place of the agent's, pinned the same way for that session's life.) Editing the named agent afterwards never changes a running or resumed session — its behavior is pinned for its whole life. Update an agent freely: existing sessions keep what they started with, new sessions pick up the change. (Rotating a credential's **key value** is the exception — that flows to running sessions, so a compromised key can be replaced; see [credentials](/agent-sessions/credentials).) +For a built-in runtime, creating a session **freezes** the agent's active [revision](/agent-sessions/revisions), including its `prompt`, `model`, `runtime`, and `skills`. It also pins the resolved credential. A one-off [`model`](/agent-sessions/sessions#model) override is pinned in the same way. Editing the named agent does not change a running or resumed session: existing sessions keep what they started with, while new sessions pick up the change. Rotating a credential's **key value** is the exception, so a compromised key can be replaced; see [credentials](/agent-sessions/credentials). + +That isolation applies to the built-in runtimes. A Flue session records the selected deployment, but all sessions for that agent execute on its one live Worker. Deploying a new Flue Worker can therefore change existing sessions; see [Flue deployment behavior](/agent-sessions/flue#deployment-and-revision-behavior). ## Model `model` is a `provider/model` id, and its provider must match the [runtime](/agent-sessions/runtimes): `claude` runs `anthropic/…` (e.g. `anthropic/claude-opus-4-8`), `codex` runs `openai/…` (e.g. `openai/gpt-5-codex`). The model runs either **Managed** (via OpenComputer, billed to your credits — no key) or on [your own key](/agent-sessions/credentials) for that provider, so a key isn't required. The `runtime` / `model` / credential pairing is validated when you **create** the agent. `runtime` is fixed at creation — to switch engines, create a new agent; you can still update `model` later, but keep it within the runtime's provider. + +Flue currently uses Managed Anthropic access. Its model is declared in both the compiled app and `agent.toml`; per-session model overrides are not supported. diff --git a/docs/agent-sessions/api-reference.mdx b/docs/agent-sessions/api-reference.mdx index abaa5fd96..fc9f6d32f 100644 --- a/docs/agent-sessions/api-reference.mdx +++ b/docs/agent-sessions/api-reference.mdx @@ -78,15 +78,15 @@ oc agent deploy # deploy the agent directory in the cwd **Create params** — `agent` is **required**; the model resolves to **Managed** (default) or a [credential](#credentials), else `422 no_credential` / `422 managed_unavailable`. - `input` — the task (string or [envelope](/agent-sessions/messaging#message-input)). Truncate large inputs; let the agent fetch the rest via [sandbox tools](/agent-sessions/runtime-tools). -- `revision?` — pin a specific revision (number / `rev_…`); default = the agent's active one. Use to test a [staged](/agent-sessions/revisions#staging-vs-activating) revision. +- `revision?`: pin a specific revision (number / `rev_…`); default = the agent's active one. Use to test a [staged](/agent-sessions/revisions#staging-vs-activating) built-in runtime revision. A Flue agent has one live Worker, so an old revision does not isolate older Worker code. - `model?` — run this session on a different model than the agent's (same `provider/model` form; its provider must match the agent's [runtime](/agent-sessions/runtimes), e.g. `anthropic/…` for `claude`). Default = the agent's model. Pinned for the session's life like the rest of the snapshot. Rejected (`400 invalid`) for [flue](/agent-sessions/flue) agents — their model is fixed in the deployed artifact. - `key?` — get-or-create (one session per key). Keyless: an `Idempotency-Key` header makes create retry-safe. - `metadata?` — opaque routing state (≤ 16 KB); on get/list + **verbatim in webhooks**; never sent to the model, not indexed. -- `limits?` `{ tokens, turn_seconds, turns }` — non-monetary; tripping one ends the turn (matching `yield_reason`). -- `sources?` — repos checked out before turn 1 (the token never enters the sandbox; see [GitHub](#github-apps-and-repos)). Each registered `{ repo, ref, sha?, name? }` or inline `{ url, ref, sha, name?, auth }`. `ref` is required; `sha` is required for inline sources but **optional for a registered repo** — omit it and the control plane resolves `ref`→HEAD and pins that commit at create. +- `limits?` `{ tokens, turn_seconds, turns }`: non-monetary; tripping one ends a built-in runtime turn with the matching `yield_reason`. These limits are stored but not yet enforced for Flue sessions. +- `sources?`: repos checked out before turn 1 for built-in runtimes (the token never enters the sandbox; see [GitHub](#github-apps-and-repos)). Each registered `{ repo, ref, sha?, name? }` or inline `{ url, ref, sha, name?, auth }`. `ref` is required; `sha` is required for inline sources but **optional for a registered repo**. Flue sessions reject `sources` with `422 sources_not_supported`. - Client tokens: `scopes?` (`read`/`steer`), `ttl?` 60–86400 s (SDK `ttlSeconds`). -A session pins the agent's active revision for its whole life — editing the agent later never affects a running one. +A built-in runtime session pins the agent's active revision for its whole life. A Flue session records its selected deployment but executes on the agent's one live Worker; deploying that Worker can therefore affect an existing session. **`Session`** diff --git a/docs/agent-sessions/custom-runtimes.mdx b/docs/agent-sessions/custom-runtimes.mdx index 932243e74..2524b397a 100644 --- a/docs/agent-sessions/custom-runtimes.mdx +++ b/docs/agent-sessions/custom-runtimes.mdx @@ -1,14 +1,14 @@ --- mode: "wide" title: "Custom runtimes" -description: "Package your own agent harness as a runtime" +description: "Package your own agent implementation as a runtime" --- **Labs preview.** Custom runtimes are not part of the stable Durable Sessions API. Production sessions use the built-in [runtimes](/agent-sessions/runtimes): `claude`, `codex`, and `pi`. -Building on the [Flue framework](https://flueframework.com)? There's a supported path that skips all of this: [Run Flue agents](/agent-sessions/flue) deploys your Flue app onto the platform's brain contract directly. Custom runtimes remain the general escape hatch for any other harness. +Building on the [Flue framework](https://flueframework.com)? Use the supported [Flue runtime](/agent-sessions/flue) instead. It deploys Flue's Cloudflare target as an agent Worker with one Durable Object per session, so it does not use the custom brain-box contract described here. Custom runtimes remain the general escape hatch for other agent implementations. -A custom runtime lets you run your own agent harness on Durable Agent Sessions. Once registered, select it on an agent with `runtime: ""`; sessions, events, steering, webhooks, recovery, and sandbox tools use the same public API as built-in runtimes. +A custom runtime lets you run your own agent implementation on Durable Agent Sessions. Once registered, select it on an agent with `runtime: ""`; sessions, events, steering, webhooks, recovery, and sandbox tools use the same public API as built-in runtimes. The runtime image contains a **brain server**: a resident process around your agent SDK. OpenComputer supplies the platform adapter that reads the session log, owns fencing and idempotency, exposes tools, calls your brain over localhost, and commits the brain's stream as durable events. diff --git a/docs/agent-sessions/events.mdx b/docs/agent-sessions/events.mdx index a01ad3df2..498a7d8b3 100644 --- a/docs/agent-sessions/events.mdx +++ b/docs/agent-sessions/events.mdx @@ -17,7 +17,7 @@ turn.started turn.completed needs_input ``` -Everything between `turn.started` and `turn.completed` is that turn's work. **`turn.completed` is the "done" signal — await it** and read its `yield_reason` (here, `needs_input`). The `body` each type carries is in the table below. +Everything between `turn.started` and `turn.completed` is that turn's work. **`turn.completed` is the done signal, so await it.** New neutral completions report `outcome`; existing built-in runtime events may report the equivalent `yield_reason`. The `body` each type carries is in the table below. Each event is one envelope: @@ -39,7 +39,7 @@ Treat **`type`** as the stable discriminator. Unknown types should render from t | `agent.message` | `{ text }` | agent output (the result is whichever one `turn.completed` points to) | | `user.message` | `{ text }` | a steer you sent | | `turn.started` | `{ turn_id, input_from_seq, input_to_seq }` | a turn began, consuming input events in `[from,to]` | -| `turn.completed` | `{ turn_id, yield_reason, result_event_id? }` | **the "done" signal — await this** | +| `turn.completed` | `{ turn_id, outcome?, yield_reason?, result_event_id?, usage? }` | the done signal; normalize `outcome` and legacy `yield_reason` as the terminal reason | | `tool.call` | `{ tool, args_summary }` | the agent invoked a tool | | `exec.completed` | `{ command, exit_code, summary, content_ref?, bytes? }` | a finished command | | `error.runtime` · `error.model` · `error.task` | `{ code, message, retriable? }` | a failure | @@ -86,7 +86,7 @@ Read or stream with `after=` to get everything since that point — this is - Steers coalesce — one turn can pick up several — so [`POST /messages`](/agent-sessions/messaging) returns the *event* `seq`, not a turn. The turn that consumes your steer emits `turn.started` with `input_from_seq ≤ your_seq ≤ input_to_seq`; await its `turn.completed`, then `GET …/events?turn_id=` for everything it produced. + [`POST /messages`](/agent-sessions/messaging) returns the input event `seq`, not a turn id. Built-in runtimes may consume several queued inputs in one turn; Flue allocates one ordered turn for each accepted input. In both cases, `turn.started` reports the consumed `input_from_seq` and `input_to_seq`. Await its matching `turn.completed`, then use `GET …/events?turn_id=` for the turn's output. A large `body` is offloaded to a `content_ref`; fetch the bytes with `GET /sessions/:id/events/:eventId/content` (e.g. full `exec.completed` output). For named files, diffs, screenshots, reports, and previews, see [Artifacts & previews](/agent-sessions/artifacts) in Labs. diff --git a/docs/agent-sessions/flue.mdx b/docs/agent-sessions/flue.mdx index b5b3a8212..f62fd6e60 100644 --- a/docs/agent-sessions/flue.mdx +++ b/docs/agent-sessions/flue.mdx @@ -1,141 +1,170 @@ --- mode: "wide" title: "Flue" -description: "Deploy a Flue app to OpenComputer as a durable session" +description: "Deploy a Flue app as an OpenComputer agent" --- -**Experimental.** The supported profile below is intentionally -constrained and will widen. Flue is pre-1.0; each deploy bundles **your exact -Flue version** into the artifact, so upstream releases never break a deployed -agent — only new builds pick them up. + +**Experimental.** The supported profile is intentionally narrow. It currently covers direct text +sessions on managed Anthropic models, with an optional OpenComputer sandbox. Flue is pre-1.0, and +each deployment includes the exact Flue version installed in your app. + -[Flue](https://flueframework.com) is a TypeScript agent framework by the Astro -team. OpenComputer runs a Flue app as the resident process of a -[durable session](/agent-sessions/overview) — same sessions API, events, -steering, and watches as every other [runtime](/agent-sessions/runtimes). +[Flue](https://flueframework.com) is a TypeScript framework for durable agents. OpenComputer runs the +application Flue builds and connects it to OpenComputer agents, sessions, events, steering, model +billing, and optional sandboxes. -Your app stays plain Flue. You add one file — yours, committed, nothing is -generated or injected. It plays the role Flue's layout gives `cloudflare.ts`: -an optional platform-specific entry module. +Template: [`diggerhq/oc-flue-starter`](https://github.com/diggerhq/oc-flue-starter) -```ts src/opencomputer.ts -import { serveOC } from '@opencomputer/flue'; -import agent from './agents/support-triage.ts'; +## How Flue differs from the built-in runtimes -serveOC(agent); +The built-in runtimes run Claude Code, Codex, or pi as a managed coding agent. A Flue agent is +different: **your application is the runtime**. OpenComputer deploys and operates that app. + +| Concern | Built-in runtimes | Flue | +| --- | --- | --- | +| Agent loop | OpenComputer runs Claude Code, Codex, or pi | Your compiled Flue app contains the loop | +| Session compute | Managed per-session brain and workspace sandboxes | One deployed app, with one Durable Object instance per session | +| Conversation state | Checkpointed runtime files plus the OpenComputer event log | Flue stores its conversation in Durable Object SQLite; OpenComputer projects public milestones into its event log | +| Tools and skills | Platform tools plus uploaded `prompt.md` and `skills/` | `defineTool` code and packaged skill imports compiled into the Worker | +| Linux workspace | Part of the normal runtime | Absent by default; `ocSandbox` creates one only when an operation needs it | +| Deployment | Upload prompt, model, and skill configuration | Build and deploy the compiled app | + +Under the hood, OpenComputer runs Flue's Cloudflare target as an agent Worker and gives each session +its own Durable Object. This architecture lets a Flue session start without provisioning a virtual +machine. A model-only agent, or one whose custom tools use bundled data, never needs a sandbox. + +## How a turn runs + +```mermaid +sequenceDiagram + participant Client as Dashboard or API client + participant OC as OpenComputer sessions API + participant Worker as Deployed agent Worker + participant DO as Session Durable Object + participant Gateway as OpenComputer model gateway + participant Model as Model provider + participant Sandbox as Optional sandbox + + Client->>OC: Create session or send message + OC->>OC: Store input and allocate turn + OC-->>Client: Accepted + OC->>Worker: Dispatch accepted input + Worker->>DO: Admit the turn + DO->>Gateway: Model request + Gateway->>Model: Metered provider request + Model-->>DO: Response + opt First real shell or file operation + DO->>Sandbox: Resolve or create session sandbox + Sandbox-->>DO: Operation result + end + DO-->>OC: Durable conversation updates + OC->>OC: Append messages, tools, errors, and completion + OC-->>Client: Event stream update ``` -A full Flue app — with `app.ts`, channels, workflows — can add this file -unchanged and keep self-hosting: only what it imports ends up in the -OpenComputer artifact. +OpenComputer returns after it has durably accepted the input. It does not wait for dispatch, the +Durable Object, a model call, or sandbox creation. Flue executes one turn at a time for each session; +messages sent while a turn is working remain queued and run in order. -Template: [`diggerhq/oc-flue-starter`](https://github.com/diggerhq/oc-flue-starter). +Flue's Durable Object is the runtime's conversation store. The OpenComputer event log is the stable +consumer view used by the dashboard, API, and webhooks. OpenComputer resumes projection from a saved +cursor after an interruption, so clients do not need to understand Flue submission ids or stream +offsets. ## Quickstart +Requirements: Node 22.19 or newer, the `oc` CLI, and an OpenComputer organization with Managed model +access. + ```sh -git clone https://github.com/diggerhq/oc-flue-starter && cd oc-flue-starter +git clone https://github.com/diggerhq/oc-flue-starter +cd oc-flue-starter npm install -# create the agent (optional — `oc agent deploy` creates it from agent.toml if missing) -oc agent create support-triage --runtime flue --model anthropic/claude-sonnet-5 - -# build + upload + boot-verify + activate a revision +# Builds the Cloudflare target, deploys it, verifies the live Worker, and activates a revision. +# The agent is created from agent.toml when it does not exist yet. oc agent deploy -# talk to it -oc session create --input "Customer says order 1042 hasn't arrived — what do I tell them?" -oc session logs +oc session create \ + --agent support-triage \ + --input "Order 2203 arrived with a torn shoulder strap. What happens next?" ``` -Answer the agent's questions with `oc session steer "..."` or in -the [dashboard](https://app.opencomputer.dev). +Continue the session with `oc session steer "..."`, inspect it with +`oc session logs `, or open it in the +[dashboard](https://app.opencomputer.dev). -## Add to an existing Flue app +## Add OpenComputer to a Flue app -Three files, no changes to your agent code: +Install the integration alongside Flue: ```sh npm install @opencomputer/flue ``` -```ts src/opencomputer.ts -import { serveOC } from '@opencomputer/flue'; -import agent from './agents/your-agent.ts'; +Export the OpenComputer route and register the managed model gateway inside your agent initializer: + +```ts src/agents/support-triage.ts +import { defineAgent, defineAgentProfile } from '@flue/runtime'; +import { DEFAULT_MODEL, route, useOcGateway } from '@opencomputer/flue'; + +export { route }; + +export default defineAgent((ctx) => { + useOcGateway(ctx); + + return { + profile: defineAgentProfile({ + instructions: 'Triage customer requests and give the next concrete action.', + }), + model: DEFAULT_MODEL, + }; +}); +``` + +Use the default hosting app, which mounts Flue's routes, the deployment health endpoint, and +OpenComputer telemetry: -serveOC(agent); +```ts src/app.ts +export { default } from '@opencomputer/flue/app'; ``` +Declare the deployment target: + ```toml agent.toml -name = "your-agent" # the OpenComputer agent name -model = "anthropic/claude-sonnet-5" # must equal the model in defineAgent +name = "support-triage" +model = "anthropic/claude-haiku-4-5" [runtime] family = "flue" ``` -Then `oc agent deploy` from the app root — it creates the agent on first -deploy. If the app sets `sandbox:` or has a `db.ts`, the build tells you -(both are supplied by the platform — see the table below). +The manifest name must identify the exported Flue agent, and its model must match the model returned +by `defineAgent`. `DEFAULT_MODEL` currently resolves to `anthropic/claude-haiku-4-5`. -## What changes vs a stock Flue app +If your project owns a custom `app.ts`, keep Flue's routes, import `@opencomputer/flue/wire` for +telemetry, and expose a `GET /health` route that returns success. The default app is the simpler +choice when you do not need additional routes. -The sandbox, persistence, model, skills, tool-name, and credential rows are -enforced at build or deploy — violations fail before a session can exist. -Channels and workflows aren't checked: code not imported by `src/opencomputer.ts` -isn't in the artifact. +## What `@opencomputer/flue` supplies -| | Stock Flue | On OpenComputer | -| --- | --- | --- | -| Entry | Flue's generated server (`flue build --target node`) | Add `src/opencomputer.ts` calling `serveOC(agent)`. Your agent code is untouched; `flue dev` keeps working locally | -| Sandbox | You pick one (`local()`, containers, …) | **Leave `sandbox:` unset** — the session's workspace sandbox is supplied (build error) | -| Persistence | `db.ts` (or volatile in-memory default) | **No `db.ts`** — the conversation is stored durably in the session's state; survives restarts and hibernation (build error) | -| Model credentials | Env keys or `registerProvider` in code | **No keys anywhere in the app** — the deploy scans the artifact for key-shaped strings and fails on a hit. Managed billing or an [OC credential](/agent-sessions/credentials); routing is injected at run time | -| Model choice | In code | Still in code — but declared three times total (`defineAgent`, `agent.toml`, the OC agent) and all three **must be equal**; the deploy rejects divergence. Fixed for good at deploy: a [session](/agent-sessions/sessions#model) **can't override** a flue agent's model — passing `model` on a flue session is rejected | -| Skills | Packaged imports (`with {type:'skill'}`) or workspace files | **`src/skills//SKILL.md`** — ships with every deploy. Packaged imports are a build error (for now) | -| Asking the user | No human-in-the-loop primitive | An **`ask` tool** is added: the session yields `needs_input` and hibernates until the user replies | -| Channels / workflows | Slack/Discord ingress, `defineWorkflow` | Not supported — inbound is session messages + [GitHub watches](/agent-sessions/watches) | -| Tool names | Anything | `bash`, `read`, `write`, `edit`, `ls`, `grep`, `glob`, `say`, `ask` are **reserved** (build error) | - -## What `@opencomputer/flue` does - -`serveOC(agent)` runs your agent as the session's resident process and -connects it to the platform: - -- **Conversation persistence.** Opens Flue's conversation store on the - session's state volume. History survives process restarts, hibernation, - and machine moves. A `db.ts` is rejected at build because a second store - would fork the conversation history. -- **Sandbox.** Flue's built-in `read`/`write`/`edit`/`bash`/`grep`/`glob` - tools execute on the session's workspace sandbox — a separate machine from - your app process, where attached repos are checked out. Custom `defineTool` - code runs in-process with your app (see the network-access note below). -- **Two added tools.** `say` posts a message to the user mid-run. `ask` asks - a question: the session yields `needs_input` and hibernates until the user - replies, then the run continues with the answer. -- **Model credentials.** Registers the Anthropic provider with credentials - resolved from the platform — managed metering or your key. The bundle and - repo contain no credentials. -- **Turn handling.** Each session turn is admitted into Flue's engine with - an idempotent id, so platform-level retries can't double-run it. If the - platform side of a turn dies mid-run, the retry **re-attaches to the - still-running engine** and streams from where it left off — the model call - is not repeated. Every step — model text, custom tool calls, sandbox - operations — is written to the session's - [event log](/agent-sessions/events). -- **Retries and timeouts.** Flue's `durability` settings are overridden: one - engine attempt per turn, with the timeout set to the platform's turn - deadline. Cancel, retry, and deadline behavior is the platform's; there is - no second layer to reason about. - -`oc-flue-build` (the package's build command, run by `oc agent deploy`) -bundles `src/opencomputer.ts` with **your** installed Flue version via esbuild, collects -`src/skills/**` into the artifact, and stamps the metadata (`model`, versions) -the deploy validates against. - -## Custom tools - -Plain `defineTool` — typed with valibot, running in-process: +| Export | Purpose | +| --- | --- | +| `useOcGateway(ctx)` | Registers the managed Anthropic endpoint inside the Flue initializer | +| `route` | Opts an agent into the HTTP transport used by managed dispatch | +| `DEFAULT_MODEL` | Selects the supported prompt-caching-safe default model | +| `@opencomputer/flue/app` | Provides Flue routes, `GET /health`, and telemetry wiring | +| `@opencomputer/flue/wire` | Adds telemetry when your project owns its hosting app | +| `ocSandbox(ctx.env)` | Adds an optional, demand-driven Linux shell and filesystem | + +OpenComputer binds a platform-managed credential for its gateway during deployment. Your provider +key is not compiled into the app or exposed to the Worker. + +## Custom tools and packaged skills + +Custom `defineTool` handlers run inside the Worker. Import any data or templates they need so the +Cloudflare build includes those files: ```ts src/tools/lookup-order.ts import { defineTool } from '@flue/runtime'; @@ -152,101 +181,157 @@ export const lookupOrder = defineTool({ }); ``` -Two rules that follow from tools running inside the deployed artifact: - -- **Anything a tool needs at run time must be bundled** — `import` fixture - data and templates (as above); the repo checkout is not on the app's - filesystem. -- **Outbound network**: custom tools currently have unrestricted outbound - access from your app's process — where `defineTool` code runs, a separate - machine from the workspace sandbox. An egress policy is planned; until it - exists, don't embed secrets in the bundle to call your own APIs — prefer - self-contained tools. - -## Skills - -Put the agent's own skills at `src/skills//SKILL.md` -([SKILL.md format](/agent-sessions/skills)). The build packages them into the -artifact; at run time they are written into the agent's workspace, where -Flue's normal discovery finds them. Changing a skill is a redeploy; a -rollback also reverts skills. Separately, a repo attached as a session source -contributes its own `.agents/skills/**` (that convention means "skills for -agents working on that repo"); on a name clash, the app's skill wins. - -Skills take effect **on OpenComputer only**: `flue dev`'s default local -environment is an empty in-memory filesystem, so it exercises your loop and -custom tools, not skills. - -## Models - -`anthropic/`, same ids as the other runtimes — `anthropic/claude-sonnet-5` -(the template default), `anthropic/claude-opus-4-8`, -`anthropic/claude-haiku-4-5`. Managed billing and BYO keys behave exactly as -on `claude`/`pi`; see [credentials](/agent-sessions/credentials). - -## Limitations vs full Flue - -What the current profile does **not** cover, beyond the table above: - -- **Channels** (Slack, Discord, web ingress) don't run here — inbound is - session messages and [GitHub watches](/agent-sessions/watches). A full app - keeps its channels when self-hosting; code not imported by - `src/opencomputer.ts` never enters the artifact. -- **Workflows** (`defineWorkflow`) don't run here; a workflow admission - throws at run time. -- **One agent, one conversation per session.** `serveOC(agent)` hosts one - top-level agent, and a session is one conversation instance — Flue's - per-instance routing has no equivalent; fan out by creating sessions. -- **Subagents** (`session.task()`) bundle and execute in-process, but they - are outside the tested profile: their model declarations aren't validated - the way the top-level agent's is, and subagent steps may appear - incompletely in the session event log. -- **Anthropic models only.** Flue itself supports other providers (OpenAI, - Google, …); on OpenComputer there is no credential source for them yet. -- **`durability` settings are ignored.** The platform pins one engine - attempt per turn under the turn deadline; retry and timeout policy is the - platform's. Setting `durability:` in `defineAgent` has no effect. -- **Text in, text out.** Sessions exchange text; there is no way to send - the agent an image or a file attachment. -- **No HTTP endpoints.** A Flue app's own server doesn't run; requests - reach the agent only through the - [sessions API](/agent-sessions/overview). - -Expect this list to shrink. The profile is versioned -(`profile_version` in the artifact), so a deployed agent never changes -behavior until you rebuild. - -## Deploying - -`oc agent deploy` = build (`oc-flue-build`, which initializes your agent to -extract and check the profile) → upload the content-addressed artifact → -**boot-verify** → create and activate an immutable -[revision](/agent-sessions/revisions). Sessions pin their revision at create; -rollback is repointing. - -Boot-verify is the flue-specific step. Your artifact is your brain code, so -the platform boots it in a throwaway sandbox and waits for a healthy boot -before pinning a revision. A bundle that throws in `initialize()` — or can't -otherwise reach a healthy boot — **fails the deploy** with that error; it -never reaches a session, and the previously active revision keeps serving. So -a flue deploy isn't instant: it polls through a `verifying` state (tens of -seconds) while the boot runs, then activates. `--no-activate` still verifies — -a staged revision is a verified one that isn't live yet. The verify sandbox is -discarded afterward and never holds your model credential. +Package an agent-owned skill through Flue's supported import mechanism, then add it to the agent: + +```ts +import triage from '../skills/triage/SKILL.md' with { type: 'skill' }; + +// Inside the object returned by defineAgent: +{ + tools: [lookupOrder], + skills: [triage], +} +``` + +The skill is part of the Worker module graph. It is not copied into a workspace, and loading it does +not require a sandbox. Changing a tool, imported data file, or packaged skill requires another +deployment. + +Custom tools in the Worker can currently reach only platform-managed outbound hosts. Arbitrary +external `fetch` destinations are not part of the supported profile. Commands run through an +optional OpenComputer sandbox follow the sandbox's separate network policy. + +## Optional sandbox + +Add `ocSandbox` only when the agent needs a Linux shell or durable files: + +```ts +import { defineAgent, defineAgentProfile } from '@flue/runtime'; +import { + DEFAULT_MODEL, + ocSandbox, + route, + useOcGateway, +} from '@opencomputer/flue'; + +export { route }; + +export default defineAgent((ctx) => { + useOcGateway(ctx); + + return { + profile: defineAgentProfile({ + instructions: 'Work in the sandbox only when a shell or file operation is needed.', + }), + model: DEFAULT_MODEL, + sandbox: ocSandbox(ctx.env), + }; +}); +``` + +Declaring `ocSandbox` performs no provisioning during Worker startup or Flue runtime initialization. +The first real shell or file operation resolves one sandbox for the session; later operations and +turns reuse it. + +The workspace starts empty. Repository sources and workspace skill discovery are not implemented for +Flue sessions, so passing `sources` at session creation is rejected. + +## Variables and secrets + +Non-secret Worker variables belong in `agent.toml`: + +```toml agent.toml +[vars] +SUPPORT_REGION = "eu-west" +``` + +Pipe secret values over standard input. Values are write-only and never returned by the API: + +```sh +printf '%s' "$SUPPORT_API_KEY" | oc agent secret set SUPPORT_API_KEY --from-stdin +oc agent secret list + +# A configuration change takes effect on the next deployment. +oc agent deploy +``` + +Deleting a secret also requires a deployment before the Worker changes: + +```sh +oc agent secret delete SUPPORT_API_KEY +oc agent deploy +``` + +Binding names use uppercase letters, digits, and underscores. Names beginning with `OC_` or `FLUE_` +are reserved for the platform. Never put a secret in `agent.toml` or source code. + +## Deployment and revision behavior + +`oc agent deploy` performs these operations: + +1. Scans the source tree for credential-shaped values. +2. Runs the app's installed `flue build --target cloudflare`. +3. Uploads only the generated JavaScript modules and a restricted deployment descriptor. +4. Applies platform-owned bindings, variables, secrets, and Durable Object migrations, then uploads + the agent Worker. +5. Keeps the deployment in `verifying` until the exact live Worker returns two consecutive healthy + responses. Only then does the CLI report the revision as ready. + + +Flue currently has one live Worker per agent. Uploading a deployment changes the code used by new and +existing sessions before revision verification finishes. There is no isolated staged Worker, canary +route, or automatic Worker rollback. `--no-activate`, an old revision pin, and a pointer-only +`oc agent rollback` do not restore older Worker code. + +To restore a known-good build, check out that source and run `oc agent deploy` again. Flue and Durable +Object schema changes are forward-only, so do not downgrade across an incompatible Flue storage +version. + + +## Session behavior + +Flue uses the same session ids, text input, event stream, steering, cancellation, result endpoint, +and dashboard as the other runtimes. The important differences are: + +- Session creation and steering return after durable acceptance. Model execution continues + asynchronously. +- Each accepted input receives an OpenComputer `trn_...` id. Flue's internal submission identity is + not part of the public API. +- Follow-up messages queue behind the working turn and run in order. +- The model is compiled into the deployed app. Per-session model overrides are rejected. +- Generic `tokens`, `turn_seconds`, and `turns` limits are not yet enforced on the Flue execution + path. Do not use them as a safety boundary for this runtime. +- Archiving makes the OpenComputer session read-only but retains the Flue conversation in Durable + Object storage. Each session is subject to Cloudflare's + [10 GB Durable Object storage limit](https://developers.cloudflare.com/durable-objects/platform/limits/). + +## Current limitations + +- Direct text session messages are the supported ingress. Flue channels and workflows are not + connected to OpenComputer yet. +- Repository sources, checkout, pull-request publishing, watches, and repo-backed workspaces are not + supported for Flue sessions. +- File and image attachments are not supported. +- The managed gateway currently supports Anthropic models and requires Managed model access for the + organization. Per-session model selection is not supported. +- Worker code has a platform-managed outbound allowlist. Tenant-configurable egress is not available. +- Platform turn deadlines, maximum-turn enforcement, automatic conversation compaction, and + automatic tenant-Worker rollback are not available yet. +- Subagents may execute inside Flue, but their event projection and lifecycle are outside the tested + profile. ## Troubleshooting -- **Build fails (`oc-flue-build`)** — a profile violation, named in the - error: `sandbox:` set, a `db.ts`, a packaged-skill import, a reserved tool - name, or a model that isn't `anthropic/…`. Nothing is uploaded. -- **Deploy fails at verification** — the bundle built but didn't boot in the - scratch sandbox; the deploy output carries the probe error. Usual cause: a - top-level crash in your code (something that only happens at import time). -- **Model rejected at deploy** — the three model declarations diverge - (`defineAgent`, `agent.toml`, the OC agent). -- **`provider 401` on the first turn** — no usable model credential. Managed - billing is the default, so this usually means the org's billing isn't set - up yet — set up billing, or attach a valid Anthropic credential to the agent. -- **Skill not loading** — the agent's own skills go in - `src/skills//SKILL.md`; skills from a codebase require that repo - attached as a session source (`--source`). +- **`flue build` is unavailable**: run `npm install` with Node 22.19 or newer. `oc agent deploy` never + downloads a missing build tool for you. +- **Credential scan refuses the deploy**: remove the reported key from source and store application + secrets with `oc agent secret set ... --from-stdin`. +- **Deployment fails while verifying**: the uploaded Worker did not reach a stable healthy response. + Inspect the deployment error, correct the import-time or health-route failure, and deploy again. +- **Provider authentication fails on the first turn**: confirm that Managed model access is active for + the organization and that the agent uses an `anthropic/...` model. +- **A skill is missing**: import its `SKILL.md` with `with { type: 'skill' }` and include it in the + agent's `skills` array. +- **A session shows an error or stops advancing**: inspect the dashboard's **All events** view or run + `oc session logs `. Runtime failures are recorded without requiring a diagnostic + redeployment. diff --git a/docs/agent-sessions/overview.mdx b/docs/agent-sessions/overview.mdx index 961c6f187..e52472039 100644 --- a/docs/agent-sessions/overview.mdx +++ b/docs/agent-sessions/overview.mdx @@ -11,12 +11,12 @@ Create an agent, start a session, stream its event log, steer it with messages, ## Core behavior - + - Runtime crashes restart automatically - Idle sessions hibernate and wake on the next message - Hung runs stop cleanly instead of staying stuck - + - **Brain**: agent loop. **Hands**: files and commands. - Untrusted work stays contained in the hands sandbox - Your model key stays in the [secret store](/sandboxes/secrets), never in a sandbox @@ -33,11 +33,13 @@ Create an agent, start a session, stream its event log, steer it with messages, +[Flue](/agent-sessions/flue) uses the same session, event, steering, and webhook APIs on a different execution substrate. Your compiled Flue app is the runtime, each session gets its own Durable Object, and no Linux sandbox is created unless the app explicitly uses `ocSandbox`. + ## Get started - Create a reusable `{ name, model, prompt, runtime }` from your backend — `runtime: "claude"` (Anthropic models) or `"codex"` (OpenAI models). Run **Managed** (`credential: "managed"`, billed to your OpenComputer credits — no key) or pass your own model `key` inline / reference a saved [credential](/agent-sessions/credentials). Every [deployment](/agent-sessions/revisions) is versioned as a [revision](/agent-sessions/revisions) (roll back instantly), you can attach [skills](/agent-sessions/revisions#skills), and you can [deploy from a GitHub repo on push](/agent-sessions/revisions#deploy-from-a-repo). ([Agents](/agent-sessions/agents)) + Create a reusable `{ name, model, prompt, runtime }` from your backend, using `runtime: "claude"` for Anthropic models or `"codex"` for OpenAI models. Run **Managed** (`credential: "managed"`, billed to your OpenComputer credits) or pass your own model key. Built-in runtime deployments support isolated revisions, skills, repo push-to-deploy, and pointer rollback. To deploy your own Flue app instead, use the [Flue guide](/agent-sessions/flue). ([Agents](/agent-sessions/agents)) Start a session with `{ agent, input }`. The agent starts immediately and returns a browser-safe `client_token`. @@ -53,7 +55,7 @@ Create an agent, start a session, stream its event log, steer it with messages, Durable Agent Sessions architecture: your app starts, steers, and streams a durable event-log session; a managed runtime adapter commits events while the brain drives the agent loop and acts through the hands sandbox; the model runs on your key from the secret store, which never enters a sandbox; user-level events are delivered out via your webhook. -The event log is the durable record. Your app starts sessions, streams events, and steers with messages. The runtime drives the agent loop, acts through the hands sandbox for file and command work, and commits user-level events for webhook delivery. Your model key stays in the secret store. +The event log is the durable consumer record. Your app starts sessions, streams events, and steers with messages. Built-in runtimes drive the agent loop through brain and hands sandboxes. Flue executes in its Worker and Durable Object, then projects the same public milestone events. Managed model credentials stay outside both execution substrates. ## When to use sessions diff --git a/docs/agent-sessions/revisions.mdx b/docs/agent-sessions/revisions.mdx index 052f4e39e..73656daed 100644 --- a/docs/agent-sessions/revisions.mdx +++ b/docs/agent-sessions/revisions.mdx @@ -1,9 +1,9 @@ --- title: "Deployments & revisions" -description: "Deploy agent behavior, version it as immutable revisions, roll back instantly" +description: "Deploy and version agent behavior" --- -What you deploy is a small directory: +For the built-in runtimes, what you deploy is a small directory: ``` my-agent/ @@ -13,15 +13,17 @@ my-agent/ └─ runtime/ # optional — a custom runtime image (coming soon) ``` -`agent.toml` + `prompt.md` are all an agent needs; `skills/` and `runtime/` are additive — add them only when you do. A **deployment** packages this directory into an immutable [**revision**](#revisions); the **active revision** is what new [sessions](/agent-sessions/sessions) use, and you roll back by re-activating an earlier one. +`agent.toml` + `prompt.md` are all a built-in agent needs; `skills/` and `runtime/` are additive. A **deployment** packages this directory into an immutable [**revision**](#revisions), and the **active revision** is what new [sessions](/agent-sessions/sessions) use. -Deploy three ways — all the same underneath: the **API/SDK** (send it inline), the **`oc` CLI** (bundle a local directory), or a [**repo push**](#deploy-from-a-repo). +A [Flue](/agent-sessions/flue) deployment packages a compiled Cloudflare app instead. It records revisions in the same API, but currently has one live Worker per agent rather than independently routable revision artifacts. Its staging and rollback behavior is therefore different. + +Built-in runtime behavior can be deployed through the **API/SDK**, the **`oc` CLI**, or a [**repo push**](#deploy-from-a-repo). The current Flue profile uses `oc agent deploy` from a local app checkout because its build must run the app's installed Flue toolchain. ## Revisions -A **revision** is an immutable snapshot of an agent's behavior — the `prompt`, `model`, and skills from one deployment, frozen and numbered. Every deploy appends a new one (a repo push that changes nothing is skipped); the agent's **active revision** is the one new [sessions](/agent-sessions/sessions) run. +A **revision** is an immutable, numbered record of one deployment. For a built-in runtime, it freezes the `prompt`, `model`, and skills used by new sessions. For Flue, it records the app deployment and Worker version that passed verification. -Revisions are **linear** (the number only goes up, like a Vercel/Fly deployment) and never rewritten — [rolling back](#roll-back-and-promote) just moves the active pointer to an earlier one. In-flight sessions keep the revision they started on; only new sessions pick up a change. +Revisions are **linear** (the number only goes up) and never rewritten. Built-in runtime sessions keep the revision they started on; only new sessions pick up a change. Flue sessions share the agent's live Worker, so a Worker deployment also changes the code used by existing sessions. | Field | | |---|---| @@ -79,10 +81,12 @@ The response is a **deployment**: { "deployment": { "id": "dep_…", "state": "ready", "revision_id": "rev_…", "active": true } } ``` -Inline deployments finish synchronously (`state: "ready"` with a `revision_id`) — except [flue](/agent-sessions/flue) deploys, which return `state: "verifying"` and boot the uploaded artifact in a throwaway sandbox before pinning the revision; poll them like an async deployment (`verifying` → `ready`/`failed`). Asynchronous deployments (GitHub) return `state: "accepted"` — poll `GET /agents/:id/deployments/:deployment_id` until `ready` (or `failed`/`skipped`/`superseded`); a repo deploy also reports progress as a **commit status** on GitHub. +Inline deployments finish synchronously (`state: "ready"` with a `revision_id`) for the built-in runtimes. A [Flue](/agent-sessions/flue) deploy returns `state: "verifying"` while the managed deploy runner uploads the agent Worker and waits for two consecutive healthy responses from that exact live Worker. Poll it like an asynchronous deployment (`verifying` to `ready` or `failed`). GitHub deployments return `state: "accepted"`; poll `GET /agents/:id/deployments/:deployment_id` until terminal, and use the commit status on GitHub for the same result. ### Staging vs activating +Isolated staging is not available for Flue. A Flue upload changes the one live Worker even when deployment metadata requests `activate: false`. Do not use `--no-activate` or a pinned revision as a Flue canary. See [Flue deployment behavior](/agent-sessions/flue#deployment-and-revision-behavior). + By default a ready deployment is **activated** — its revision becomes what new sessions use. Pass **`activate: false`** to **stage** instead: the revision is created but not made active, so you can test it before it goes live. ```ts @@ -105,6 +109,8 @@ For [repo deployments](#deploy-from-a-repo) the **branch decides** and `activate ## Deploy from a repo +Repo-linked push-to-deploy is not available for Flue apps yet. Deploy their compiled Cloudflare target with `oc agent deploy` from a local or CI checkout. + Keep the agent directory (above) in Git and **push to deploy**. [Install the OpenComputer GitHub App](/agent-sessions/repos#connecting-github) on the repo, then connect it to an agent: @@ -144,21 +150,20 @@ No repo needed — the CLI bundles a local agent directory and deploys it: ```bash CLI oc agent deploy # deploy ./ (the agent named in agent.toml) oc agent deploy ./agents/issue-fixer # a specific directory -oc agent deploy --no-activate # stage instead of activate +oc agent deploy --no-activate # stage a built-in runtime revision ``` Same as a repo push — handy for CI that isn't GitHub, or trying a change before you commit. ### Flue framework agents -A [Flue](/agent-sessions/flue) agent (`[runtime] family = "flue"` in `agent.toml`) has no `prompt.md` or `skills/` — its behavior lives in code — so `oc agent deploy` builds and ships an artifact instead: +A [Flue](/agent-sessions/flue) agent (`[runtime] family = "flue"` in `agent.toml`) carries its instructions, tools, and packaged skills in code, so `oc agent deploy` builds and ships the Cloudflare app instead of reading `prompt.md` and the top-level `skills/` directory: ```bash CLI -oc agent create support-triage --runtime flue --model anthropic/claude-sonnet-5 # --prompt not needed -oc agent deploy # build → upload → verify → activate +oc agent deploy # flue build --target cloudflare, upload, verify live Worker ``` -Deploy runs the app's own build (`oc-flue-build`), uploads the content-addressed artifact, boot-verifies it in a scratch sandbox, then activates a revision — the same staging and rollback model as any other agent. See [Run Flue agents](/agent-sessions/flue). +The CLI creates the agent from `agent.toml` when needed, runs the locally installed Flue build, uploads the generated JavaScript modules, and waits for the exact live Worker to become stable. See [Run Flue agents](/agent-sessions/flue) for the required app wiring and current deployment constraints. ## Roll back and promote @@ -183,6 +188,8 @@ oc agent rollback 3 Instant — no rebuild. Running sessions are unaffected; new sessions use the newly-active revision. +The pointer-only rollback above applies to the built-in runtimes. It does not replace a Flue agent's live Worker bytes. To restore a Flue build, check out the known-good source and run `oc agent deploy` again. Do not downgrade across an incompatible Flue Durable Object schema version. + ## Inspect ```http REST API diff --git a/docs/agent-sessions/runtimes.mdx b/docs/agent-sessions/runtimes.mdx index 0aa36e97b..7c8fb6a3c 100644 --- a/docs/agent-sessions/runtimes.mdx +++ b/docs/agent-sessions/runtimes.mdx @@ -11,16 +11,16 @@ A **runtime** executes the agent loop: provider SDK, model calls, and tool use. | `claude` | `anthropic/…` (e.g. `anthropic/claude-opus-4-8`) | Managed, or an Anthropic key | | `codex` | `openai/…` (e.g. `openai/gpt-5-codex`) | Managed, or an OpenAI key | | `pi` | `anthropic/…` today — the runtime is provider-agnostic by construction; more providers land next | Managed, or an Anthropic key | -| `flue` experimental | `anthropic/…` today — bring your own [Flue](/agent-sessions/flue) app | Managed, or an Anthropic key | +| `flue` experimental | `anthropic/…` today, declared by your [Flue](/agent-sessions/flue) app | Managed | -`model` is passed straight through to the provider, so any model that provider serves works — only the **`provider/` prefix** is validated against the runtime (a bad prefix is rejected at agent create, a model the provider doesn't recognize fails on the first turn). A [session](/agent-sessions/sessions#model) can override this per run — pass `model` at create to run one session on a different model than its agent, validated the same way (brain-box runtimes `claude`/`codex`/`pi`; not `flue`, which pins its model in the artifact). Common choices: +For the built-in runtimes, `model` is passed straight through to the provider. The **`provider/` prefix** is validated against the runtime; an unknown provider model fails on the first turn. A [session](/agent-sessions/sessions#model) can override the model for one run. Flue instead declares its model in the app and `agent.toml`, and rejects per-session overrides. Common choices: - **`claude`:** `anthropic/claude-sonnet-5` (default, balanced), `anthropic/claude-opus-4-8` (most capable), `anthropic/claude-fable-5`, `anthropic/claude-haiku-4-5` (fastest/cheapest). - **`codex`:** `openai/gpt-5-codex`. - **`pi`:** the `anthropic/…` catalog above. The runtime itself is provider-agnostic; additional providers are next. -- **`flue`** (experimental): an `anthropic/…` model, declared in your app's code — see [Run Flue agents](/agent-sessions/flue) for the supported ids. +- **`flue`** (experimental): an `anthropic/…` model declared in your app and routed through Managed access. See [Run Flue agents](/agent-sessions/flue). -Sessions, [events](/agent-sessions/events), [steering](/agent-sessions/messaging), [tools](/agent-sessions/runtime-tools), and [webhooks](/agent-sessions/webhooks) share the same API across runtimes. `runtime` is fixed once the agent exists; to switch engines, create a new agent. +Sessions, [events](/agent-sessions/events), [steering](/agent-sessions/messaging), and [webhooks](/agent-sessions/webhooks) share the same consumer API across runtimes. Execution, tools, persistence, and recovery depend on the runtime. `runtime` is fixed once the agent exists; to switch engines, create a new agent. ## `claude` @@ -60,7 +60,7 @@ Content-Type: application/json ## `pi` -The [pi](https://github.com/earendil-works/pi) coding-agent harness (MIT). `pi` is built provider-agnostic — the harness drives whatever provider the model's `provider/` prefix names. Today it ships with `anthropic/…` models, on Managed billing or your Anthropic key; additional providers land next. Sandbox tools, the event stream, steering, and recovery are the same as the other runtimes. +The [pi](https://github.com/earendil-works/pi) coding agent (MIT). `pi` is provider-independent by design: it drives whatever provider the model's `provider/` prefix names. Today it ships with `anthropic/…` models, on Managed billing or your Anthropic key; additional providers land next. Sandbox tools, the event stream, steering, and recovery are the same as the other built-in runtimes. ```http POST https://api.opencomputer.dev/v3/agents @@ -80,19 +80,23 @@ The provider is taken from the model's `provider/` prefix — so as more provide ## `flue` experimental -Unlike the runtimes above, `flue` doesn't run a platform-provided harness — -it runs **your app**, built with the [Flue framework](https://flueframework.com) -and deployed with `oc agent deploy`. Your code defines the agent -(instructions, typed tools, subagents); OpenComputer provides the durable -session around it: the event log, sandbox tools, steering, hibernation, and -recovery are the same as every other runtime. `anthropic/…` models today, -Managed or your Anthropic key. Experimental — see -[Run Flue agents](/agent-sessions/flue) for the supported profile and -quickstart. +Unlike the runtimes above, `flue` does not run a platform-provided coding agent. It +runs **your compiled Flue app** as an agent Worker, with one Durable Object +instance holding each session's conversation. OpenComputer durably accepts +input, dispatches turns, routes managed model calls, and projects Flue's +durable updates into the shared event log. + +A Flue agent has no Linux sandbox by default. Add `ocSandbox` only when the +agent needs shell or file operations; the first real operation creates it. +This different hosting and recovery model is explained in +[How Flue differs from the built-in runtimes](/agent-sessions/flue#how-flue-differs-from-the-built-in-runtimes). The runtime, model, and credential have to agree. `claude` needs an `anthropic/…` model and an Anthropic key; `codex` needs an `openai/…` model and an OpenAI key; `pi` runs `anthropic/…` models today and needs an Anthropic key (or Managed). A mismatch is rejected when you create the agent, with a clear error — so a session never starts on a broken pairing. See [model credentials](/agent-sessions/credentials). -## Architecture +## Built-in runtime architecture + +The remainder of this page describes `claude`, `codex`, and `pi`. Flue uses the +Worker and Durable Object architecture described on the [Flue page](/agent-sessions/flue). The model is configuration, not part of the runtime image. The provider is always taken from the model's `provider/` prefix, never from the runtime — a runtime only declares which providers it can drive: `claude` and `pi` drive Anthropic today (`pi` is built to drive any provider — more land next), `codex` drives OpenAI. You choose the exact `provider/model` and key. @@ -103,7 +107,7 @@ A runtime has two components: The brain knows the SDK, the model, the prompt, its resumable state directory, and the sandbox tools exposed to it. It does **not** write OpenComputer events directly and does not call the sandbox API itself. The adapter reads new input from the [event log](/agent-sessions/events), sends a bounded turn to the brain, translates the brain's stream into session events, handles fencing/idempotency, and only declares the turn done after committed output is durable. -The built-in runtimes are built this way. See [example runtimes](https://github.com/diggerhq/oc-runtime-examples) for `claude` and `codex` brain servers side by side, and [custom runtimes](/agent-sessions/custom-runtimes) in Labs for the same brain/adapter contract applied to your own harness. +The built-in runtimes are built this way. See [example runtimes](https://github.com/diggerhq/oc-runtime-examples) for `claude` and `codex` brain servers side by side, and [custom runtimes](/agent-sessions/custom-runtimes) in Labs for the same brain/adapter contract applied to your own runtime implementation. A runtime also **starts fast**: the engine is prepared ahead of time and baked into the image, so a new or recreated session doesn't wait on a per-session download before its first step. @@ -127,7 +131,7 @@ Across turns the brain keeps provider-specific state under a checkpointed state When a session starts it pins the runtime and version it began on (alongside the rest of the [agent snapshot](/agent-sessions/agents#session-pinned-config)). `runtime` is fixed at agent creation — to run a different engine, create a new agent. Other agent edits (`model`, `prompt`, `limits`) never affect a running or resumed session: new sessions pick up the change, in-flight ones keep what they started with. -## Supervision and recovery +## Built-in supervision and recovery OpenComputer supervises runtime execution: diff --git a/docs/agent-sessions/sessions.mdx b/docs/agent-sessions/sessions.mdx index 58ae11374..4ba08753e 100644 --- a/docs/agent-sessions/sessions.mdx +++ b/docs/agent-sessions/sessions.mdx @@ -4,18 +4,20 @@ title: "Sessions" description: "Durable, resumable agent execution" --- -A **session** is an [agent](/agent-sessions/agents) run with an append-only [event log](/agent-sessions/events), a pinned agent snapshot, and a lifecycle status. Compute can attach, detach when idle, and reattach on the next message; the log persists. +A **session** is an [agent](/agent-sessions/agents) run with an append-only [event log](/agent-sessions/events), a pinned configuration snapshot, and a lifecycle status. The execution substrate depends on the [runtime](/agent-sessions/runtimes): built-in runtimes use managed sandboxes, while [Flue](/agent-sessions/flue) uses a deployed Worker and one Durable Object per session. The [dashboard](https://app.opencomputer.dev) lists your sessions and opens each with a live event stream you can watch and steer — handy for following or debugging a run without building UI. ## Session flow -1. You create a session from an [agent](/agent-sessions/agents). It **pins the agent's active [revision](/agent-sessions/revisions)** (prompt / model / skills) for its whole life — editing the agent later never affects a running session. Pass `revision` to pin a specific one instead (e.g. to test a [staged](/agent-sessions/revisions#staging-vs-activating) revision before promoting). Pass `model` to run this one session on a different model than the agent's — same `provider/model` form, and it must resolve to the agent's runtime's provider ([flue](/agent-sessions/flue) agents don't take an override; their model is fixed in the artifact). The model runs **Managed** (billed to your credits, no key — the default) or on a [model credential](/agent-sessions/credentials) you supply. Pass [`sources`](/agent-sessions/repos) to check repos out into `/workspace/sources/` before turn 1 — the GitHub token never enters the sandbox. The session starts immediately. -2. The [runtime](/agent-sessions/runtimes) executes the agent in a sandbox, appending [events](/agent-sessions/events) to the log as it works. -3. With nothing left to do, the session goes **idle** and the sandbox **hibernates**. -4. A [steer](/agent-sessions/messaging) message wakes it; it resumes with its prior context. Repeat. +1. You create a session from an [agent](/agent-sessions/agents). Built-in runtimes pin the active [revision](/agent-sessions/revisions), including its prompt, model, and skills. Flue records the selected deployment, but all of an agent's Flue sessions execute on its one live Worker; see [Flue deployment behavior](/agent-sessions/flue#deployment-and-revision-behavior). +2. OpenComputer stores the input, allocates a turn, and returns. Runtime execution continues asynchronously. Pass `model` to override a built-in runtime's model for this session; Flue rejects model overrides because its model is part of the deployed app. +3. The runtime appends [events](/agent-sessions/events) as it works. Built-in runtimes execute in managed sandboxes. Flue executes in its Worker and Durable Object, with no sandbox unless the app explicitly uses `ocSandbox`. +4. With nothing left to do, the session becomes **idle**. A [steer](/agent-sessions/messaging) message starts the next ordered turn with the same conversation context. -The runtime is supervised: crashes restart from durable state, hangs end at the turn deadline, and idle sessions survive sandbox reclaim. Work since the last logged event may repeat, so make side effects idempotent. Details: [runtime recovery](/agent-sessions/runtimes#failure-behavior). +Pass [`sources`](/agent-sessions/repos) to prepare `/workspace/sources/` for a built-in runtime. Flue does not yet support repository sources and rejects that parameter. + +The built-in runtimes restart from checkpointed state, enforce a turn deadline, and survive sandbox reclaim. Flue relies on Durable Object recovery and event projection instead; its current limit and recovery differences are listed on the [Flue page](/agent-sessions/flue#session-behavior). ## Lifecycle @@ -24,11 +26,11 @@ A **session** is an [agent](/agent-sessions/agents) run with an append-only [eve | `queued` | Scheduled; no turn is running. | | `running` | A turn is executing. | | `awaiting_input` | A turn ended asking you a question — steerable; reply to continue. | -| `idle` | No turn running; steerable; the sandbox is hibernated. | +| `idle` | No turn running; steerable; runtime state is retained. | | `failed` | The session errored out. | | `archived` | Closed; read-only. | -A session also carries `last_turn = { id, state, yield_reason?, result_event_id? }`; the fuller turn record (`started_at`, `completed_at`, `error`) comes from [`GET …/result`](/agent-sessions/api-reference) and `…/turns`. The **`yield_reason`** tells you *why* the last turn ended — `completed`, `needs_input` (the agent asked a question — the session is now `awaiting_input`; answer by [steering](/agent-sessions/messaging)), `budget_exceeded` / `deadline_exceeded` / `max_turns` (a [limit](#limits) tripped), or `canceled`. +A session also carries `last_turn = { id, state, yield_reason?, result_event_id? }`; the fuller turn record (`started_at`, `completed_at`, `error`) comes from [`GET …/result`](/agent-sessions/api-reference) and `…/turns`. The **`yield_reason`** tells you why the last turn ended: `completed`, `needs_input` (the agent asked a question and the session is now `awaiting_input`), `budget_exceeded`, `deadline_exceeded`, `max_turns`, or `canceled`. Limit outcomes require runtime enforcement; the current Flue path does not enforce the generic session limits below. ## Fetch the result @@ -83,11 +85,13 @@ Three independent knobs — don't overload one for another: Cap a session at create (or default it on the [agent](/agent-sessions/agents)) with `limits: { tokens, turn_seconds, turns }` — a token budget, per-turn wall-clock, and auto-run count. These are runtime limits, not spend controls; model usage is billed to your provider key (BYO) or to your OpenComputer credits (Managed). Hitting one ends the turn with the matching `yield_reason`. +These limits are not yet enforced for [Flue](/agent-sessions/flue) sessions. Do not use them as a deadline, turn-count, or token safety boundary for that runtime. + ## Model A session runs the model pinned in its agent snapshot. Pass `model` at create to run **this one session** on a different model — omit it and the session inherits the agent's model (the default). The override is the same `provider/model` form as the agent's model, and its provider must match the agent's [runtime](/agent-sessions/runtimes) (e.g. `anthropic/…` for a `claude` agent — a mismatched prefix is rejected at create, a model the provider doesn't recognize fails on the [first turn](/agent-sessions/runtimes)). Like the rest of the snapshot it's **pinned for the session's life** — there's no mid-session model switch; start another session to run another model. -Not available for [flue](/agent-sessions/flue) agents: their model is fixed in the deployed artifact (the [model triangle](/agent-sessions/flue)), so a `model` on a flue session is rejected. +Not available for [Flue](/agent-sessions/flue) agents: their model is fixed in the deployed app, so a `model` on a Flue session is rejected. @@ -119,6 +123,9 @@ Content-Type: application/json **`archive` is not delete** — the session's log is retained, just read-only. +For Flue, archive also retains the conversation in the session's Durable Object storage. See +[Flue session behavior](/agent-sessions/flue#session-behavior). + When the agent needs input, the turn ends with `yield_reason: "needs_input"` and the session status becomes `awaiting_input`; reply by [steering](/agent-sessions/messaging). **No cross-session memory.** Each session starts from the agent's prompt + that session's `input` — a durable log is *session history*, not memory the agent carries between sessions. diff --git a/scripts/flue-journey.sh b/scripts/flue-journey.sh index 9055513ea..dc658b288 100755 --- a/scripts/flue-journey.sh +++ b/scripts/flue-journey.sh @@ -4,48 +4,32 @@ set -euo pipefail # Flue agent — the stranger's journey (design 012 §11.8), run end to end against # a live sessions-api. clone → deploy → chat, using only the `oc` CLI. # -# This is the acceptance harness for the Flue slice: when the artifact-upload -# endpoint and the flue runtime image are live in prod, this script exercises the -# whole path a real user follows. -# -# ── Branch CLI build notes ────────────────────────────────────────────────── -# The Flue deploy flow (`oc agent deploy` on a family="flue" app) is not in a -# released `oc` yet — build it from this branch: -# -# cd cmd/oc && go build -o /tmp/oc-flue . && export OC=/tmp/oc-flue -# -# or just run this script from a checkout of the branch — with no $OC set it -# builds `oc` from ./cmd/oc automatically (needs a Go toolchain). +# This is the acceptance journey for the Flue slice. It exercises the same +# clone → install → deploy → session path a user follows. # # ── Prerequisites ─────────────────────────────────────────────────────────── -# - node >= 22.19 and npm (a Flue developer has these; oc-flue-build needs them) -# - OPENCOMPUTER_API_KEY exported (an OpenComputer API key) +# - node >= 22.19 and npm (required by the current Flue toolchain) +# - an authenticated `oc` CLI (`oc auth login` or OPENCOMPUTER_API_KEY) # - git # # ── Usage ─────────────────────────────────────────────────────────────────── -# OPENCOMPUTER_API_KEY=oc_... scripts/flue-journey.sh +# scripts/flue-journey.sh # # Env overrides: -# OC path to the oc binary (default: build from ./cmd/oc) +# OC path to the oc binary (default: oc from PATH) # STARTER_REPO starter git URL (default: diggerhq/oc-flue-starter) # SESSIONS_API_URL control-plane URL (default: the CLI default, prod) # AGENT_NAME agent name (default: from the starter's agent.toml) # INPUT first message to the agent -# SOURCE optional owner/repo[@ref] to attach as a working source # KEEP set to 1 to keep the temp workdir -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" STARTER_REPO="${STARTER_REPO:-https://github.com/diggerhq/oc-flue-starter}" -INPUT="${INPUT:-Customer says order 1042 has not arrived yet - what should I tell them?}" - -: "${OPENCOMPUTER_API_KEY:?export OPENCOMPUTER_API_KEY first}" +INPUT="${INPUT:-Customer says order 2203 arrived with a torn shoulder strap - what should I tell them?}" -# Resolve the oc binary — build from this branch if none was provided. -OC="${OC:-}" -if [[ -z "$OC" ]]; then - OC="$(mktemp -d)/oc" - echo "→ building oc from $REPO_ROOT/cmd/oc" - ( cd "$REPO_ROOT/cmd/oc" && go build -o "$OC" . ) +OC="${OC:-oc}" +if ! command -v "$OC" >/dev/null 2>&1; then + echo "oc CLI not found; install it or set OC=/path/to/oc" >&2 + exit 1 fi echo "→ using oc: $OC" "$OC" --version 2>/dev/null || true @@ -58,7 +42,7 @@ echo "→ clone $STARTER_REPO" git clone --depth 1 "$STARTER_REPO" "$WORK/app" cd "$WORK/app" -echo "→ npm install (brings in @opencomputer/flue → oc-flue-build)" +echo "→ npm install (Flue toolchain + OpenComputer integration)" npm install # Agent name: explicit override, else the starter's agent.toml [name]. @@ -80,7 +64,6 @@ echo "→ oc agent deploy (build → upload → boot-verify → activate)" echo "→ oc session create" create_args=(--input "$INPUT") [[ -n "${AGENT_NAME:-}" ]] && create_args+=(--agent "$AGENT_NAME") -[[ -n "${SOURCE:-}" ]] && create_args+=(--source "$SOURCE") SID="$("$OC" session create "${create_args[@]}" --json | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)" echo " session: $SID" diff --git a/sdks/flue/.gitignore b/sdks/flue/.gitignore new file mode 100644 index 000000000..f4e2c6d6b --- /dev/null +++ b/sdks/flue/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.tsbuildinfo diff --git a/sdks/flue/LICENSE b/sdks/flue/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/sdks/flue/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sdks/flue/README.md b/sdks/flue/README.md new file mode 100644 index 000000000..27494ebcb --- /dev/null +++ b/sdks/flue/README.md @@ -0,0 +1,52 @@ +# @opencomputer/flue + +Run a stock [Flue](https://flueframework.com) app as an OpenComputer agent. Your compiled app is the +runtime; OpenComputer connects it to managed sessions, model access, and optional sandboxes. This +package supplies the OpenComputer-specific wiring the app opts into. Deployments use Flue's stock +`flue build --target cloudflare` output, with one Durable Object per session. + +## What it gives you + +- **`useOcGateway(ctx)` + `DEFAULT_MODEL`**: point the managed `anthropic` provider at the + OpenComputer model gateway. Call it **inside** your `defineAgent` initializer. +- **`route`** — the HTTP-transport opt-in every OC-hosted agent must export (`export { route }`). +- **`ocSandbox(env)`** — optional, demand-driven Linux shell/files. Declaring it makes no network + request; the first actual sandbox operation resolves the persistent session sandbox. +- **`@opencomputer/flue/app`** — a default hosting app (`flue()` routes + `/health` + telemetry). Or + `import '@opencomputer/flue/wire'` from your own `app.ts` for telemetry only. + +## Minimal agent + +```ts +import { defineAgent, defineAgentProfile } from "@flue/runtime"; +import { useOcGateway, route, DEFAULT_MODEL } from "@opencomputer/flue"; + +export { route }; + +export default defineAgent((ctx) => { + useOcGateway(ctx); + return { + profile: defineAgentProfile({ instructions: "You help customers." }), + model: DEFAULT_MODEL, // prompt-caching-safe + }; +}); +``` + +`src/app.ts`: + +```ts +export { default } from "@opencomputer/flue/app"; +``` + +Then `flue build --target cloudflare` and `oc agent deploy`. See `oc-flue-starter` for a full example. + +The current profile supports direct text sessions and optional demand-driven sandboxes. Channels, +workflows, repository sources, pull-request publishing, attachments, and arbitrary Worker egress are +not supported yet. + +## Environment (set on the tenant script by the OC deploy) + +`OC_GATEWAY` and `OC_SESSION_TOKEN` are always managed by OpenComputer. Agents that explicitly use +`ocSandbox` use the managed `OC_SANDBOX_API`. User variables come from `agent.toml [vars]`; write-only +secrets are set with `oc agent secret` and both take effect on the next deployment. Reserved +`OC_`/`FLUE_` prefixes are platform-managed. diff --git a/sdks/flue/package-lock.json b/sdks/flue/package-lock.json new file mode 100644 index 000000000..62fd1eb2d --- /dev/null +++ b/sdks/flue/package-lock.json @@ -0,0 +1,5382 @@ +{ + "name": "@opencomputer/flue", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@opencomputer/flue", + "version": "0.2.0", + "license": "Apache-2.0", + "devDependencies": { + "@flue/runtime": "1.0.0-beta.9", + "@types/node": "^22.10.0", + "typescript": "^5.9.3", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=22.19" + }, + "peerDependencies": { + "@flue/runtime": "1.0.0-beta.9" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.975.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.1.tgz", + "integrity": "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.0", + "@aws-sdk/xml-builder": "^3.972.34", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.29.2", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.57.tgz", + "integrity": "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.59", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.59.tgz", + "integrity": "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.5.tgz", + "integrity": "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.1.tgz", + "integrity": "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-login": "^3.972.63", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.63.tgz", + "integrity": "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.67.tgz", + "integrity": "sha512-oYlzWst56rlhhjbYnexwv5hVLYe1cW4liLObhDfxDLI4RAQzleMVHQgQgx7XsC4HKj4e3kjT8v9DId+Pi/dndw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.57", + "@aws-sdk/credential-provider-http": "^3.972.59", + "@aws-sdk/credential-provider-ini": "^3.973.1", + "@aws-sdk/credential-provider-process": "^3.972.57", + "@aws-sdk/credential-provider-sso": "^3.973.1", + "@aws-sdk/credential-provider-web-identity": "^3.972.63", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/credential-provider-imds": "^4.4.7", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.57.tgz", + "integrity": "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.1.tgz", + "integrity": "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/token-providers": "3.1083.0", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1083.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1083.0.tgz", + "integrity": "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.63", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.63.tgz", + "integrity": "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/nested-clients": "^3.997.31", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.26.tgz", + "integrity": "sha512-RE1fu7Nn05vG0EUJM+8Sde2GFecC658WGaC/asPzLF6K4x3H5ZaDBcQtHRE67Gdgb1VZpyUUliYejHFK1qt0Uw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.22.tgz", + "integrity": "sha512-jtkgmhevnpzC1WeS+Y/sgymYbaQ6qg7pVOUl5cUT/8MiLptqrtnXQlNV80m+j2WIx5MIL7kVHIZNxxcK2tfUEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.39.tgz", + "integrity": "sha512-CS1spxRSezmTmI3PD+3Xrnp6KryTSEz0EefA8u6uGd0s2I0uXseWHALDI/03Wi0IUczXNWo2QrZEaHDuJNby/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.31.tgz", + "integrity": "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.975.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.39", + "@aws-sdk/types": "^3.974.0", + "@smithy/core": "^3.29.2", + "@smithy/fetch-http-handler": "^5.6.4", + "@smithy/node-http-handler": "^4.9.4", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.5.tgz", + "integrity": "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.39.tgz", + "integrity": "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.0", + "@smithy/signature-v4": "^5.6.3", + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.0.tgz", + "integrity": "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.34.tgz", + "integrity": "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@earendil-works/pi-agent-core": { + "version": "0.80.6", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.6.tgz", + "integrity": "sha512-Lvn89ko42h5ETUb6Z0Ku6ldskEqXaTdQBYvSa0+7bdG9V6rUEpXptv5e0OVZ1HDcvi8s6/2lGCQWsxKX+DFHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.80.6", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.80.6", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.6.tgz", + "integrity": "sha512-7xfLk8sANBp+bpPEbjoOZTbPxsa+++b1JXAoSJsNa3vbs9AHHEclmvg54XLQcxH+fuwaeti/g2jeIfJ+mVYLpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@flue/runtime": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@flue/runtime/-/runtime-1.0.0-beta.9.tgz", + "integrity": "sha512-ksh0ZkTVyqQnGvU3OnbVX6luAJwe6tt8q7O0vn99b7Cx6XcPTXzY/YEkXrOtCHzV6ZwfSdO9ZfaWbhTD1tdQuQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.80.2", + "@earendil-works/pi-ai": "^0.80.2", + "@hono/node-server": "^2.0.3", + "@hono/standard-validator": "^0.2.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "@standard-community/standard-json": "^0.3.5", + "@standard-community/standard-openapi": "^0.2.9", + "@valibot/to-json-schema": "^1.3.0", + "hono": "^4.8.3", + "hono-openapi": "^1.3.0", + "js-yaml": "^4.1.1", + "just-bash": "^3.0.1", + "openapi-types": "^12.1.3", + "quansync": "^0.2.11", + "ulidx": "^2.4.1", + "valibot": "^1.1.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@hono/node-server": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.8.tgz", + "integrity": "sha512-GuCWzLxwg218fy1JaHculFsdcuY12hxit83V+algozTPnwhNjLrRL/Alg9OYjLZLoUZ1rw/S4CdTMsnkSKCmFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@hono/standard-validator": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@hono/standard-validator/-/standard-validator-0.2.3.tgz", + "integrity": "sha512-bp9vHu6Va6SfMHC3D4ZLBbT/woi+AZ9CRdTXQu3kLJuLh2W/Gb9UO4hijS+BQAGFXi4EGpXdetxpzwTAawSVeg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@standard-schema/spec": "^1.0.0", + "hono": ">=3.9.0" + } + }, + "node_modules/@jitl/quickjs-ffi-types": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.32.0.tgz", + "integrity": "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jitl/quickjs-wasmfile-debug-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.32.0.tgz", + "integrity": "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-debug-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.32.0.tgz", + "integrity": "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.32.0.tgz", + "integrity": "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.32.0.tgz", + "integrity": "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.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/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@mongodb-js/zstd": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/zstd/-/zstd-7.0.0.tgz", + "integrity": "sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "node-addon-api": "^8.5.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">= 20.19.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@smithy/core": { + "version": "3.29.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.3.tgz", + "integrity": "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.8.tgz", + "integrity": "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.5.tgz", + "integrity": "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.4.tgz", + "integrity": "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.29.3", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@standard-community/standard-json": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@standard-community/standard-json/-/standard-json-0.3.5.tgz", + "integrity": "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/json-schema": "^7.0.15", + "@valibot/to-json-schema": "^1.3.0", + "arktype": "^2.1.20", + "effect": "^3.16.8", + "quansync": "^0.2.11", + "sury": "^10.0.0", + "typebox": "^1.0.17", + "valibot": "^1.1.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.24.5" + }, + "peerDependenciesMeta": { + "@valibot/to-json-schema": { + "optional": true + }, + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@standard-community/standard-openapi": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@standard-community/standard-openapi/-/standard-openapi-0.2.9.tgz", + "integrity": "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@standard-community/standard-json": "^0.3.5", + "@standard-schema/spec": "^1.0.0", + "arktype": "^2.1.20", + "effect": "^3.17.14", + "openapi-types": "^12.1.3", + "sury": "^10.0.0", + "typebox": "^1.0.0", + "valibot": "^1.1.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-openapi": "^4" + }, + "peerDependenciesMeta": { + "arktype": { + "optional": true + }, + "effect": { + "optional": true + }, + "sury": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "zod": { + "optional": true + }, + "zod-openapi": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@valibot/to-json-schema": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.7.1.tgz", + "integrity": "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "valibot": "^1.4.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.0.tgz", + "integrity": "sha512-SLhnTEqE5QpJHq/6zl9bsmImEP2adv+y6Wy+cJa7nVTRzQh1OZfCe9k29M5xN74LWnu0xa1zrUrq3KnOKl92Fg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.2.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", + "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/google-auth-library": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", + "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hono-openapi": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/hono-openapi/-/hono-openapi-1.3.1.tgz", + "integrity": "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@hono/standard-validator": "^0.2.0", + "@standard-community/standard-json": "^0.3.5", + "@standard-community/standard-openapi": "^0.2.9", + "@types/json-schema": "^7.0.15", + "hono": "^4.11.2", + "openapi-types": "^12.1.3" + }, + "peerDependenciesMeta": { + "@hono/standard-validator": { + "optional": true + }, + "hono": { + "optional": true + } + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/just-bash": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/just-bash/-/just-bash-3.1.0.tgz", + "integrity": "sha512-/t28X8EH3+j/zvELGkNhlFV2g6hc3oHBzy6zPBnNq2nqUN0DGS9PsD889JfQ7F6ll08gqFoutiS+VJHK30wZpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "diff": "^8.0.2", + "fast-xml-parser": "^5.7.3", + "file-type": "^21.2.0", + "ini": "^6.0.0", + "minimatch": "^10.1.1", + "modern-tar": "^0.7.3", + "papaparse": "^5.5.3", + "quickjs-emscripten": "^0.32.0", + "re2js": "^1.2.1", + "seek-bzip": "^2.0.0", + "smol-toml": "^1.6.0", + "sprintf-js": "^1.1.3", + "sql.js": "^1.13.0", + "turndown": "^7.2.2", + "yaml": "^2.8.2" + }, + "bin": { + "just-bash": "dist/bin/just-bash.js", + "just-bash-shell": "dist/bin/shell/shell.js" + }, + "optionalDependencies": { + "@mongodb-js/zstd": "^7.0.0", + "node-liblzma": "^2.0.3" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/layerr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-3.0.0.tgz", + "integrity": "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", + "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-liblzma": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-liblzma/-/node-liblzma-2.2.0.tgz", + "integrity": "sha512-s0KzNOWwOJJgPG6wxg6cKohnAl9Wk/oW1KrQaVzJBjQwVcUGPQCzpR46Ximygjqj/3KhOrtJXnYMp/xYAXp75g==", + "dev": true, + "hasInstallScript": true, + "license": "LGPL-3.0", + "optional": true, + "dependencies": { + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "bin": { + "nxz": "lib/cli/nxz.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/oorabona" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/papaparse": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.4.tgz", + "integrity": "sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/quickjs-emscripten": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.32.0.tgz", + "integrity": "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", + "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-release-sync": "0.32.0", + "quickjs-emscripten-core": "0.32.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quickjs-emscripten-core": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.32.0.tgz", + "integrity": "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/re2js": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-1.3.3.tgz", + "integrity": "sha512-s/I5zEAo79SUK0Qw4dpZKpiMwbQ6Gz0KU2NRr7eaO4x/p2g7Vvmn3hdeXDg8VsaUjfj/ora+e9oi27LX/C9+mw==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "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/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "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": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/turndown": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz", + "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + }, + "engines": { + "node": ">=18", + "npm": ">=9" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "dev": true, + "license": "MIT" + }, + "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/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ulidx": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/ulidx/-/ulidx-2.4.1.tgz", + "integrity": "sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "layerr": "^3.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/sdks/flue/package.json b/sdks/flue/package.json new file mode 100644 index 000000000..8f44e009f --- /dev/null +++ b/sdks/flue/package.json @@ -0,0 +1,68 @@ +{ + "name": "@opencomputer/flue", + "version": "0.2.0", + "description": "Run a Flue app as an OpenComputer agent with managed models and optional demand-driven sandboxes.", + "keywords": [ + "agents", + "flue", + "opencomputer", + "cloudflare-workers", + "durable-objects" + ], + "author": "OpenComputer", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/diggerhq/opencomputer.git", + "directory": "sdks/flue" + }, + "homepage": "https://github.com/diggerhq/opencomputer/tree/main/sdks/flue", + "bugs": { + "url": "https://github.com/diggerhq/opencomputer/issues" + }, + "type": "module", + "engines": { + "node": ">=22.19" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./app": { + "types": "./dist/app.d.ts", + "default": "./dist/app.js" + }, + "./wire": { + "types": "./dist/wire.d.ts", + "default": "./dist/wire.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "build": "npm run clean && tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "prepublishOnly": "npm test && npm run build" + }, + "sideEffects": [ + "./dist/wire.js" + ], + "peerDependencies": { + "@flue/runtime": "1.0.0-beta.9" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@flue/runtime": "1.0.0-beta.9", + "@types/node": "^22.10.0", + "typescript": "^5.9.3", + "vitest": "^3.0.0" + } +} diff --git a/sdks/flue/src/app.ts b/sdks/flue/src/app.ts new file mode 100644 index 000000000..5159b911e --- /dev/null +++ b/sdks/flue/src/app.ts @@ -0,0 +1,30 @@ +// The default OC hosting app. A scaffolded starter uses this as its `src/app.ts`. It composes the +// SAME app Flue's Cloudflare build generates for the no-`app.ts` case, then adds the `/health` probe +// the OC deploy/activate step needs (stock Flue exposes NO health route — Spike B finding) and installs +// the telemetry forwarder. Apps that own their `app.ts` instead `import '@opencomputer/flue/wire'` (§wire). +// +// WHY `createDefaultFlueApp()` from `@flue/runtime/internal` (and NOT `flue()` from +// `@flue/runtime/routing`): `flue()`'s route handlers read the module-scoped `runtimeConfig` at REQUEST +// time, which the generated Cloudflare entry sets via `configureFlueRuntime(...)` (imported from +// `@flue/runtime/internal`) at module load. The generated entry's no-`app.ts` path builds its app with +// `createDefaultFlueApp()` — the exact same `@flue/runtime/internal` entry — so the mounted `flue()` and +// the `configureFlueRuntime()` that seeds it share one module instance and requests never hit +// "flue() route invoked before runtime was configured". A `src/app.ts` that instead mounted `flue()` +// from `@flue/runtime/routing` (a different published entry) risked resolving a second `@flue/runtime` +// module instance whose `runtimeConfig` is never configured → every request 500s. Composing via the +// build's own entry keeps this app on the configured instance. +// +// Default export = a `Fetchable` (Hono qualifies), per Flue's routing contract. + +import { createDefaultFlueApp } from "@flue/runtime/internal"; +import { installOcObserver } from "./observe.js"; + +installOcObserver(); + +// createDefaultFlueApp() mounts flue() at '/' and installs Flue's canonical notFound/onError envelopes. +// Adding a path-specific '/health' route afterwards is safe — flue() only registers its own concrete +// paths (/agents, /workflows, /runs, /channels), so GET /health matches this handler directly. +const app = createDefaultFlueApp(); +app.get("/health", (c) => c.json({ status: "ok" })); + +export default app; diff --git a/sdks/flue/src/cf-env.ts b/sdks/flue/src/cf-env.ts new file mode 100644 index 000000000..c29a6ad2d --- /dev/null +++ b/sdks/flue/src/cf-env.ts @@ -0,0 +1,33 @@ +// Ambient Cloudflare-Workers env access (design 013 §4). On the `flue build --target cloudflare` +// build the real Worker bindings (`OC_GATEWAY`, `OC_SESSION_TOKEN`, `OC_SANDBOX_*`, `OC_INGEST`, …) +// live on the AMBIENT env exported by `cloudflare:workers` — the same one Flue's generated entry reads +// (`import { env } from 'cloudflare:workers'`). The per-agent `ctx.env` Flue threads into the +// initializer is EMPTY for these bindings, so OC helpers must read the ambient env instead. +// +// `cloudflare:workers` only resolves inside workerd, so importing it statically would break local +// `flue dev` on the node target and the package's own vitest. Load it lazily + guarded: on CF the +// dynamic import resolves and `ambientEnv` is populated during module graph evaluation (before any +// request); everywhere else the import rejects, is caught, and callers fall back to the passed env. + +let ambientEnv: Record | undefined; +try { + // `@vite-ignore` so consumer/test bundlers don't try to statically resolve the workerd built-in; + // on CF this is a runtime import of the ambient module, off CF it throws and we fall back. + const mod = (await import(/* @vite-ignore */ "cloudflare:workers")) as { + env?: Record; + }; + ambientEnv = mod.env; +} catch { + ambientEnv = undefined; +} + +/** + * Resolve the effective OC env: the Cloudflare ambient bindings layered over `fallback` (ambient wins). + * On CF this returns the real Worker bindings even though `ctx.env` is empty; off CF (local dev / node / + * tests) it returns `fallback` unchanged so an explicitly-passed env still works. + */ +export function ocResolveEnv>(fallback: T | undefined): T { + const base = (fallback ?? {}) as T; + if (!ambientEnv) return base; + return { ...base, ...ambientEnv } as T; +} diff --git a/sdks/flue/src/cloudflare-workers.d.ts b/sdks/flue/src/cloudflare-workers.d.ts new file mode 100644 index 000000000..3102313ca --- /dev/null +++ b/sdks/flue/src/cloudflare-workers.d.ts @@ -0,0 +1,6 @@ +// Minimal ambient type for the workerd-only `cloudflare:workers` virtual module, so the guarded +// dynamic import in `cf-env.ts` typechecks without pulling in `@cloudflare/workers-types`. The real +// module (present only on the `--target cloudflare` build) exports the ambient Worker `env` bindings. +declare module "cloudflare:workers" { + export const env: Record; +} diff --git a/sdks/flue/src/gateway.test.ts b/sdks/flue/src/gateway.test.ts new file mode 100644 index 000000000..6226dec0c --- /dev/null +++ b/sdks/flue/src/gateway.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ registerProvider: vi.fn() })); + +vi.mock("@flue/runtime", () => ({ registerProvider: mocks.registerProvider })); +vi.mock("./cf-env.js", () => ({ + ocResolveEnv: () => ({ OC_GATEWAY: "https://gateway.test" }), +})); + +import { route } from "./gateway.js"; + +describe("Flue gateway binding", () => { + it("binds the deploy token once and a tokenless request cannot overwrite it", async () => { + const next = vi.fn(async () => undefined); + await route({ env: { OC_SESSION_TOKEN: "deploy-token" } } as never, next); + await route({ env: {} } as never, next); + + expect(mocks.registerProvider).toHaveBeenCalledTimes(1); + expect(mocks.registerProvider).toHaveBeenCalledWith("anthropic", { + baseUrl: "https://gateway.test/anthropic", + apiKey: "deploy-token", + }); + expect(next).toHaveBeenCalledTimes(2); + }); +}); diff --git a/sdks/flue/src/gateway.ts b/sdks/flue/src/gateway.ts new file mode 100644 index 000000000..3c21dd540 --- /dev/null +++ b/sdks/flue/src/gateway.ts @@ -0,0 +1,87 @@ +// OC model-gateway wiring (design 013 §4). A stock Flue app points its managed `anthropic` provider at +// the OC gateway (a thin Worker over OpenRouter that injects the org key + meters per session). +// +// TOKEN SEAM — RESOLVED + VALIDATED end-to-end (2026-07-08). The per-deploy OC_SESSION_TOKEN is a Worker +// SECRET. Secrets are NOT on the CF ambient env (`cloudflare:workers`) at all — only plain vars like +// OC_GATEWAY are — and `ctx.env` at defineAgent-init is empty for the OC bindings too. So an init-time +// registerProvider reads the token falsy and every model call throws "No API key for provider: anthropic". +// The secret DOES live on the per-REQUEST env (`c.env`) — the same source ocSandbox reads at run time. +// Fix: bind the apiKey at RUN scope from the exported `route` middleware, reading OC_SESSION_TOKEN from +// `c.env` by DIRECT property access (NEVER spread c.env — the CF env is a proxy and spreading it throws) +// and OC_GATEWAY from the ambient snapshot, once per isolate. The token is per-DEPLOY (identical for +// every session), so the module-scoped provider registry needs one stable binding and no per-session +// mutation. Exact attribution remains an upstream per-request headers(ctx) concern. + +import { registerProvider } from "@flue/runtime"; +import type { AgentInitializerContext, AgentRouteHandler } from "@flue/runtime"; +import { ocResolveEnv } from "./cf-env.js"; + +/** Default managed model — MUST be prompt-caching-safe (Constraint): `claude-3-haiku` fails via + * OpenRouter→Bedrock; `claude-haiku-4-5` works. Use the pi-ai CATALOG id with DASHES + * (`claude-haiku-4-5`), never a dot (`claude-haiku-4.5`): the dotted id is absent from pi-ai's model + * catalog, so pi-ai can't derive the model's max output tokens and defaults `max_tokens` to 1 → empty + * completions. The dashed id resolves in the catalog and OpenRouter routes it too. Cheap + caching-safe. */ +export const DEFAULT_MODEL = "anthropic/claude-haiku-4-5"; + +export interface OcEnv { + /** Deployed gateway Worker base URL (set per tenant script by the OC deploy). */ + OC_GATEWAY?: string; + /** Signed deploy JWT the gateway verifies (`{org, agt, ep}`); never a raw provider key. */ + OC_SESSION_TOKEN?: string; + /** Telemetry sink for `observe()` (operator panel + spend attribution). */ + OC_INGEST?: string; + [key: string]: unknown; +} + +/** Register the managed `anthropic` provider from an OC env snapshot. Always registers the baseUrl (so the + * model specifier resolves); attaches the apiKey only when the per-deploy OC_SESSION_TOKEN is present in + * this snapshot. Returns true only when the apiKey actually landed — callers use that to stop rebinding. + * No-op when OC_GATEWAY is unset (local `flue dev` falls through to pi-ai's env-var key lookup). */ +function bindOcProvider(env: OcEnv): boolean { + const gw = env.OC_GATEWAY; + if (!gw) return false; + registerProvider("anthropic", { + baseUrl: `${gw.replace(/\/+$/, "")}/anthropic`, + ...(env.OC_SESSION_TOKEN ? { apiKey: env.OC_SESSION_TOKEN } : {}), + }); + return Boolean(env.OC_SESSION_TOKEN); +} + +/** + * Point the managed `anthropic` provider at the OC gateway. **Call this INSIDE the `defineAgent` + * initializer** — top-level module code is stripped by the CF build (proven in 1a). This runs at INIT + * scope, where the per-deploy OC_SESSION_TOKEN (a secret) is typically not yet readable, so it registers + * the baseUrl (model specifier resolves) and defers the apiKey to `route` (run scope) — see the token-seam + * note above. Binds the apiKey here too if the token happens to already be present. Reads the CF ambient + * env (`cloudflare:workers`), not `ctx.env`: on the `--target cloudflare` build the real Worker bindings + * live on the ambient env and `ctx.env` is empty for them. + */ +export function useOcGateway(ctx: AgentInitializerContext): void { + bindOcProvider(ocResolveEnv(ctx.env)); +} + +/** Set only after the run-scope secret lands. A tokenless read can never overwrite a working + * isolate-global provider binding. */ +let ocProviderBound = false; + +/** + * The HTTP-transport opt-in every OC-hosted agent MUST export as `route` (an agent is reachable at + * `/agents/:name/:id` only when its module exports `route` — flue-app.ts). ALSO the run-scope binder for + * the token seam: on the first request carrying OC_SESSION_TOKEN it binds the provider's apiKey before + * the turn's model call runs via `next()`. The provider registry is isolate-global and the token is + * deploy-static, so later requests must not mutate it. + * The OC dispatch Worker is the auth boundary (013 §3 B5), so the transport itself adds none. + */ +export const route: AgentRouteHandler = async (c, next) => { + // OC_SESSION_TOKEN is a Worker SECRET; secrets are absent from the ambient `cloudflare:workers` env + // (only vars like OC_GATEWAY live there), so the ambient-only read leaves the apiKey empty ("No API + // key"). The secret IS on the REQUEST env (c.env) — the same source ocSandbox reads in createSessionEnv. + // Read the token from c.env by DIRECT property access (never spread c.env — the CF env is a proxy and + // spreading it throws, which would break the request), and take OC_GATEWAY from the ambient snapshot. + if (!ocProviderBound) { + const amb = ocResolveEnv(undefined); + const token = (c.env as OcEnv | undefined)?.OC_SESSION_TOKEN ?? amb.OC_SESSION_TOKEN; + ocProviderBound = bindOcProvider({ ...amb, ...(token ? { OC_SESSION_TOKEN: token } : {}) }); + } + return next(); +}; diff --git a/sdks/flue/src/index.ts b/sdks/flue/src/index.ts new file mode 100644 index 000000000..2f972c1c1 --- /dev/null +++ b/sdks/flue/src/index.ts @@ -0,0 +1,12 @@ +// @opencomputer/flue — make a stock Flue agent OpenComputer-native (design 013 §4/§5). +// - useOcGateway + route + DEFAULT_MODEL: point managed anthropic at the OC gateway; HTTP-transport opt-in. +// - ocSandbox: optional, demand-driven durable shell/files as the agent's SandboxApi. +// - installOcObserver: forward lifecycle/usage to OC_INGEST. +// Default hosting app is at `@opencomputer/flue/app`; `@opencomputer/flue/wire` is the telemetry-only +// side-effect for apps with their own app.ts. + +export { useOcGateway, route, DEFAULT_MODEL } from "./gateway.js"; +export type { OcEnv } from "./gateway.js"; +export { ocSandbox, WORKSPACE_CWD } from "./sandbox.js"; +export type { OcSandboxEnv } from "./sandbox.js"; +export { installOcObserver } from "./observe.js"; diff --git a/sdks/flue/src/observe.ts b/sdks/flue/src/observe.ts new file mode 100644 index 000000000..ecc31fa36 --- /dev/null +++ b/sdks/flue/src/observe.ts @@ -0,0 +1,27 @@ +// Telemetry: forward Flue lifecycle/usage observations to OC_INGEST for the operator panel + spend +// attribution (design 013 §4; buildout Integration seams). The DO transcript stays authoritative — the +// TAILER is the event-truth path; `observe()` is a best-effort side channel, so this is fire-and-forget +// and never blocks or breaks a run. `observe` subscribers receive `ctx.env`, so this reads OC_INGEST per +// event and can be installed once at module load (isolate-scoped, matching observe's own scope). + +import { observe } from "@flue/runtime"; + +interface CtxEnv { OC_INGEST?: string; OC_SESSION_TOKEN?: string } + +/** Install the OC observation forwarder. Returns the unsubscribe fn. No-op per event when OC_INGEST unset. */ +export function installOcObserver(): () => void { + return observe((obs, ctx) => { + try { + const env = (ctx as { env?: CtxEnv }).env; + if (!env?.OC_INGEST) return; + const session = (obs as { session?: string }).session ?? (ctx as { id?: string }).id; + void fetch(env.OC_INGEST, { + method: "POST", + headers: { "content-type": "application/json", ...(env.OC_SESSION_TOKEN ? { authorization: `Bearer ${env.OC_SESSION_TOKEN}` } : {}) }, + body: JSON.stringify({ session, agent: (ctx as { agentName?: string }).agentName, event: obs }), + }).catch(() => {}); + } catch { + /* telemetry must never break the run */ + } + }); +} diff --git a/sdks/flue/src/sandbox.test.ts b/sdks/flue/src/sandbox.test.ts new file mode 100644 index 000000000..7c6e0ae8f --- /dev/null +++ b/sdks/flue/src/sandbox.test.ts @@ -0,0 +1,97 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { ocSandbox } from "./sandbox.js"; + +afterEach(() => vi.unstubAllGlobals()); + +async function initialize(id = "ses_1") { + const factory = ocSandbox({ + OC_SANDBOX_API: "https://api.opencomputer.test", + OC_SESSION_TOKEN: "deploy-token", + }); + const env = await factory.createSessionEnv({ id }); + // Flue beta.9's fixed harness bootstrap sequence. + expect(await env.exists("/workspace/AGENTS.md")).toBe(false); + expect(await env.exists("/workspace/CLAUDE.md")).toBe(false); + expect(await env.exists("/workspace/.agents/skills")).toBe(false); + expect(await env.readdir("/workspace")).toEqual([]); + return { factory, env }; +} + +describe("ocSandbox lazy allocation", () => { + it("makes zero requests for initialization and model/custom-tool-only work", async () => { + const fetchSpy = vi.fn(); + vi.stubGlobal("fetch", fetchSpy); + await initialize(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("shares one resolution across concurrent first operations and reuses it", async () => { + const calls: string[] = []; + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request) => { + const url = String(input); + calls.push(url); + if (url.includes("/flue/session-sandbox")) { + return new Response(JSON.stringify({ sandbox_id: "sbx_1" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (url.includes("/exec/run")) { + return new Response( + JSON.stringify({ exitCode: 0, stdout: "ok", stderr: "" }), + { + status: 200, + headers: { "content-type": "application/json" }, + }, + ); + } + return new Response("hello", { status: 200 }); + }), + ); + const { factory, env } = await initialize(); + await Promise.all([env.exec("echo one"), env.readFile("note.txt")]); + const laterTurn = await factory.createSessionEnv({ id: "ses_1" }); + expect(await laterTurn.exists("/workspace/AGENTS.md")).toBe(false); + expect(await laterTurn.exists("/workspace/CLAUDE.md")).toBe(false); + expect(await laterTurn.exists("/workspace/.agents/skills")).toBe(false); + expect(await laterTurn.readdir("/workspace")).toEqual([]); + await laterTurn.exec("echo later turn"); + + expect( + calls.filter((url) => url.includes("/flue/session-sandbox")), + ).toHaveLength(1); + expect( + calls.filter((url) => url.includes("/sandboxes/sbx_1/")), + ).toHaveLength(3); + }); + + it("surfaces resolution failure to the invoking operation and permits a later retry", async () => { + let resolves = 0; + vi.stubGlobal( + "fetch", + vi.fn(async (input: string | URL | Request) => { + const url = String(input); + if (url.includes("/flue/session-sandbox")) { + resolves++; + if (resolves === 1) + return new Response("unavailable", { status: 503 }); + return new Response(JSON.stringify({ sandbox_id: "sbx_retry" }), { + status: 200, + }); + } + return new Response( + JSON.stringify({ exitCode: 0, stdout: "ok", stderr: "" }), + { status: 200 }, + ); + }), + ); + const { env } = await initialize(); + await expect(env.exec("first")).rejects.toThrow( + "oc sandbox resolve failed", + ); + await expect(env.exec("second")).resolves.toMatchObject({ exitCode: 0 }); + expect(resolves).toBe(2); + }); +}); diff --git a/sdks/flue/src/sandbox.ts b/sdks/flue/src/sandbox.ts new file mode 100644 index 000000000..ea1476cb4 --- /dev/null +++ b/sdks/flue/src/sandbox.ts @@ -0,0 +1,308 @@ +// ocSandbox — an optional Flue `SandboxApi`/`SandboxFactory` driving an OpenComputer fleet sandbox +// over its public HTTP API. Merely declaring the adapter does not allocate a machine: the first real +// shell/file operation resolves the session sandbox and all concurrent callers share that promise. +// +// The first real shell/file operation resolves (and, when needed, provisions) the persistent session +// sandbox through the control plane; this client then proxies exec/fs. Endpoints (from +// @opencomputer/sdk, all fetch-based so they run in a CF DO): +// exec POST {base}/sandboxes/{id}/exec/run {args:["-c",cmd],cwd,envs,timeout} -> {exitCode,stdout,stderr} +// read GET {base}/sandboxes/{id}/files?path= +// write PUT {base}/sandboxes/{id}/files?path= (body = content) +// list GET {base}/sandboxes/{id}/files/list?path= -> [{name,...}] +// stat/exists/mkdir/rm compose over exec (shell), mirroring cloudflareSandbox. + +import { createSandboxSessionEnv } from "@flue/runtime"; +import type { + SandboxApi, + SandboxFactory, + FileStat, + ShellResult, + SessionEnv, +} from "@flue/runtime"; +import type { OcEnv } from "./gateway.js"; +import { ocResolveEnv } from "./cf-env.js"; + +/** Constant workspace cwd (matches the OC session contract — flue resolves skills at `${cwd}/.agents/skills`). */ +export const WORKSPACE_CWD = "/workspace"; + +export interface OcSandboxEnv extends OcEnv { + /** OC sandbox API base, e.g. `https://api.opencomputer.dev`. */ + OC_SANDBOX_API?: string; + /** Pre-resolved sandbox id, when the control plane injects it; else resolved lazily (see below). */ + OC_SANDBOX_ID?: string; +} + +class OcSandboxApi implements SandboxApi { + constructor( + private readonly base: string, + private readonly token: string, + private sandboxId: string, + ) {} + + private headers(extra?: Record): Record { + return { authorization: `Bearer ${this.token}`, ...extra }; + } + private url(suffix: string): string { + return `${this.base.replace(/\/+$/, "")}/sandboxes/${this.sandboxId}${suffix}`; + } + + async exec( + command: string, + options?: { + cwd?: string; + env?: Record; + timeoutMs?: number; + signal?: AbortSignal; + }, + ): Promise { + const body: Record = { + args: ["-c", command], + timeout: Math.ceil((options?.timeoutMs ?? 60_000) / 1000), + }; + if (options?.cwd) body.cwd = options.cwd; + if (options?.env) body.envs = options.env; + const resp = await fetch(this.url("/exec/run"), { + method: "POST", + headers: this.headers({ "content-type": "application/json" }), + body: JSON.stringify(body), + signal: options?.signal, + }); + if (!resp.ok) + throw new Error( + `oc sandbox exec failed: ${resp.status} ${(await resp.text()).slice(0, 200)}`, + ); + const r = (await resp.json()) as { + exitCode?: number; + stdout?: string; + stderr?: string; + }; + return { + stdout: r.stdout ?? "", + stderr: r.stderr ?? "", + exitCode: r.exitCode ?? 0, + }; + } + + async readFile(path: string): Promise { + const resp = await fetch( + this.url(`/files?path=${encodeURIComponent(path)}`), + { headers: this.headers() }, + ); + if (!resp.ok) throw new Error(`oc sandbox read ${path}: ${resp.status}`); + return resp.text(); + } + async readFileBuffer(path: string): Promise { + const resp = await fetch( + this.url(`/files?path=${encodeURIComponent(path)}`), + { headers: this.headers() }, + ); + if (!resp.ok) throw new Error(`oc sandbox read ${path}: ${resp.status}`); + return new Uint8Array(await resp.arrayBuffer()); + } + async writeFile(path: string, content: string | Uint8Array): Promise { + const resp = await fetch( + this.url(`/files?path=${encodeURIComponent(path)}`), + { + method: "PUT", + headers: this.headers({ "content-type": "application/octet-stream" }), + body: content, + }, + ); + if (!resp.ok) throw new Error(`oc sandbox write ${path}: ${resp.status}`); + } + async readdir(path: string): Promise { + const resp = await fetch( + this.url(`/files/list?path=${encodeURIComponent(path)}`), + { headers: this.headers() }, + ); + if (!resp.ok) throw new Error(`oc sandbox list ${path}: ${resp.status}`); + const entries = (await resp.json()) as Array<{ + name?: string; + path?: string; + }>; + return entries + .map((e) => e.name ?? (e.path ?? "").split("/").pop() ?? "") + .filter(Boolean); + } + + // stat/exists/mkdir/rm over the shell (mirrors cloudflareSandbox — the files API has no stat/mkdir/rm). + async stat(path: string): Promise { + const r = await this.exec(`stat -L -c '%s/%F' ${shq(path)}`); + if (r.exitCode !== 0) + throw new Error(`oc sandbox stat ${path}: ${r.stderr.slice(0, 120)}`); + const [sizeStr, kind = ""] = r.stdout.trim().split("/"); + return { + isFile: /regular file/.test(kind), + isDirectory: /directory/.test(kind), + size: Number(sizeStr) || undefined, + }; + } + async exists(path: string): Promise { + return (await this.exec(`test -e ${shq(path)}`)).exitCode === 0; + } + async mkdir(path: string, options?: { recursive?: boolean }): Promise { + const r = await this.exec( + `mkdir ${options?.recursive ? "-p " : ""}${shq(path)}`, + ); + if (r.exitCode !== 0) + throw new Error(`oc sandbox mkdir ${path}: ${r.stderr.slice(0, 120)}`); + } + async rm( + path: string, + options?: { recursive?: boolean; force?: boolean }, + ): Promise { + const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`; + const r = await this.exec(`rm ${flags ? `-${flags} ` : ""}${shq(path)}`); + if (r.exitCode !== 0 && !options?.force) + throw new Error(`oc sandbox rm ${path}: ${r.stderr.slice(0, 120)}`); + } +} + +/** + * Flue discovers workspace context while initializing every harness. OC does not materialize repo + * sources for Flue sessions yet, and a newly allocated sandbox is empty, so those fixed bootstrap + * probes must not turn a model-only turn into a machine allocation. Any operation outside the exact + * discovery sequence falls through to the real remote environment. + */ +class LazyOcSandboxApi implements SandboxApi { + private bootstrapping = true; + private readonly bootstrapAbsent: Set; + + constructor( + private readonly cwd: string, + private readonly load: () => Promise, + ) { + this.bootstrapAbsent = new Set([ + `${cwd}/AGENTS.md`, + `${cwd}/CLAUDE.md`, + `${cwd}/.agents/skills`, + ]); + } + + private remote(): Promise { + this.bootstrapping = false; + return this.load(); + } + + async exec( + command: string, + options?: { + cwd?: string; + env?: Record; + timeoutMs?: number; + signal?: AbortSignal; + }, + ): Promise { + return (await this.remote()).exec(command, options); + } + async readFile(path: string): Promise { + return (await this.remote()).readFile(path); + } + async readFileBuffer(path: string): Promise { + return (await this.remote()).readFileBuffer(path); + } + async writeFile(path: string, content: string | Uint8Array): Promise { + return (await this.remote()).writeFile(path, content); + } + async stat(path: string): Promise { + return (await this.remote()).stat(path); + } + async readdir(path: string): Promise { + if (this.bootstrapping && path === this.cwd) { + this.bootstrapping = false; + return []; + } + return (await this.remote()).readdir(path); + } + async exists(path: string): Promise { + if (this.bootstrapping && this.bootstrapAbsent.has(path)) return false; + return (await this.remote()).exists(path); + } + async mkdir(path: string, options?: { recursive?: boolean }): Promise { + return (await this.remote()).mkdir(path, options); + } + async rm( + path: string, + options?: { recursive?: boolean; force?: boolean }, + ): Promise { + return (await this.remote()).rm(path, options); + } +} + +function shq(s: string): string { + return `'${s.replace(/'/g, "'\\''")}'`; +} + +/** Resolve the session's OC sandbox id (control-plane seam). Uses the injected id when present, else a + * documented resolve endpoint keyed by the session id. Kept a single point so W1/W5 can pin the contract. */ +async function resolveSandboxId( + env: OcSandboxEnv, + sessionId: string, +): Promise { + if (env.OC_SANDBOX_ID) return env.OC_SANDBOX_ID; + const base = (env.OC_SANDBOX_API ?? "").replace(/\/+$/, ""); + const resp = await fetch( + `${base}/flue/session-sandbox?session=${encodeURIComponent(sessionId)}`, + { + method: "POST", + headers: { authorization: `Bearer ${env.OC_SESSION_TOKEN ?? ""}` }, + }, + ); + if (!resp.ok) + throw new Error( + `oc sandbox resolve failed for ${sessionId}: ${resp.status}`, + ); + const sandboxId = ((await resp.json()) as { sandbox_id?: unknown }) + .sandbox_id; + if (typeof sandboxId !== "string" || !sandboxId) { + throw new Error( + `oc sandbox resolve failed for ${sessionId}: response has no sandbox_id`, + ); + } + return sandboxId; +} + +/** + * The optional OC-fleet sandbox factory. Add `sandbox: ocSandbox(env)` only when an agent needs a + * Linux shell or durable files. The default starter intentionally omits it. + * + * Reads the CF ambient env (`cloudflare:workers`), not the passed `ctx.env`: on the `--target + * cloudflare` build the real `OC_SANDBOX_*`/`OC_SESSION_TOKEN` bindings live on the ambient env and + * `ctx.env` is empty for them (same reason as `useOcGateway`). `createSessionEnv` validates local + * configuration and returns a lazy environment without fetching or provisioning anything. + */ +export function ocSandbox( + env: OcSandboxEnv, + opts?: { cwd?: string }, +): SandboxFactory { + const cwd = opts?.cwd ?? WORKSPACE_CWD; + const remoteBySession = new Map>(); + return { + async createSessionEnv({ id }: { id: string }): Promise { + const resolved = ocResolveEnv(env); + if (!resolved.OC_SANDBOX_API) { + throw new Error( + "[oc-flue] ocSandbox: OC_SANDBOX_API is required for sandbox operations.", + ); + } + const load = (): Promise => { + const existing = remoteBySession.get(id); + if (existing) return existing; + const pending = (async () => { + const sandboxId = await resolveSandboxId(resolved, id); + return new OcSandboxApi( + (resolved.OC_SANDBOX_API ?? "").replace(/\/+$/, ""), + resolved.OC_SESSION_TOKEN ?? "", + sandboxId, + ); + })(); + remoteBySession.set(id, pending); + void pending.catch(() => { + if (remoteBySession.get(id) === pending) remoteBySession.delete(id); + }); + return pending; + }; + return createSandboxSessionEnv(new LazyOcSandboxApi(cwd, load), cwd); + }, + }; +} diff --git a/sdks/flue/src/wire.ts b/sdks/flue/src/wire.ts new file mode 100644 index 000000000..c4909c60c --- /dev/null +++ b/sdks/flue/src/wire.ts @@ -0,0 +1,8 @@ +// Side-effect module for apps that own their `app.ts`: `import '@opencomputer/flue/wire'` to forward +// Flue observations to OC_INGEST without adopting the default app. (Add `/health` to your own Hono app +// too — the OC deploy/activate probe expects it.) Marked in package.json `sideEffects` so it survives +// tree-shaking. + +import { installOcObserver } from "./observe.js"; + +installOcObserver(); diff --git a/sdks/flue/tsconfig.json b/sdks/flue/tsconfig.json new file mode 100644 index 000000000..496fdca88 --- /dev/null +++ b/sdks/flue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2023"], + "types": ["node"], + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true + }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/web/package-lock.json b/web/package-lock.json index 171c165a3..b5ed87599 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -46,7 +46,8 @@ "tailwindcss": "^4.3.1", "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", - "vite": "^8.1.0" + "vite": "^8.1.0", + "vitest": "^4.1.10" }, "engines": { "node": ">=20.19.0" @@ -746,6 +747,474 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -3090,6 +3559,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", @@ -3456,10 +3932,28 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -3781,6 +4275,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xterm/addon-fit": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", @@ -4039,6 +4646,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -4341,6 +4958,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -4919,9 +5546,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", - "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -5158,6 +5785,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -5221,6 +5855,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5683,6 +6361,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5753,6 +6441,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -7999,6 +8697,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -8290,6 +9002,13 @@ "url": "https://opencollective.com/express" } }, + "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/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9459,6 +10178,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -9509,6 +10235,13 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -9519,6 +10252,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -9826,6 +10566,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -9843,6 +10600,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10333,6 +11100,96 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/web-vitals": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz", @@ -10444,6 +11301,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/web/package.json b/web/package.json index be34d6897..1c1d55daa 100644 --- a/web/package.json +++ b/web/package.json @@ -10,6 +10,7 @@ "dev": "vite", "dev:preview": "VITE_PREVIEW=1 vite", "build": "tsc -b && vite build", + "test": "vitest run", "preview": "vite preview", "lint": "eslint .", "format": "prettier --write .", @@ -55,6 +56,7 @@ "tailwindcss": "^4.3.1", "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", - "vite": "^8.1.0" + "vite": "^8.1.0", + "vitest": "^4.1.10" } } diff --git a/web/src/api/client.ts b/web/src/api/client.ts index cb7254ea1..a4586af35 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -33,8 +33,10 @@ export type { SkillItem, AgentDeploy, Session, + AgentSnapshot, SessionEvent, Turn, + SessionResult, Destination, Delivery, SandboxWebhook, @@ -772,6 +774,19 @@ export const getSessionEvents = (id: string, level?: string) => S.SessionEventListSchema, ).then((r) => r.data) +// Turns — the per-submission execution records behind a session (state, timing, +// usage, error). Read-only; powers the submission-health panel. Newest first. +export const getSessionTurns = (id: string) => + apiFetch( + `/v3/sessions/${id}/turns`, + {}, + S.SessionTurnListSchema, + ).then((r) => r.data) + +// The latest turn + its result event (if the turn produced one). +export const getSessionResult = (id: string) => + apiFetch(`/v3/sessions/${id}/result`, {}, S.SessionResultSchema) + // Steer — post a user message into a session. export const sendMessage = ( id: string, diff --git a/web/src/api/mock.ts b/web/src/api/mock.ts index 991e68fb3..311736255 100644 --- a/web/src/api/mock.ts +++ b/web/src/api/mock.ts @@ -657,10 +657,14 @@ const credentials = [ const sessions = [ { + // A flue (CF-native) session — meters at the gateway, so its `usage` is empty here + // (spend renders "—"); the runtime badge is accented to set it apart from brain-box. id: 'ses_a1b2c3', status: 'running', agent_id: 'agt_3kf9xz', + agent_snapshot: { runtime: 'flue', model: 'anthropic/claude-haiku-4-5' }, head: 24, + usage: {}, created_at: at(0, 1), last_turn: { state: 'running' }, sandboxes: { brain: 'sbx_a1b2c3d4e5', hands: 'sbx_f6g7h8i9j0' }, @@ -669,7 +673,9 @@ const sessions = [ id: 'ses_d4e5f6', status: 'awaiting_input', agent_id: 'agt_3kf9xz', + agent_snapshot: { runtime: 'claude', model: 'anthropic/claude-sonnet-5' }, head: 12, + usage: { cost_usd: 0.0231, input_tokens: 8200, output_tokens: 640 }, created_at: at(0, 3), last_turn: { state: 'ok', yield_reason: 'needs_input' }, }, @@ -677,7 +683,10 @@ const sessions = [ id: 'ses_g7h8i9', status: 'idle', agent_id: 'agt_7mq2aa', + agent_snapshot: { runtime: 'pi', model: 'anthropic/claude-sonnet-5' }, head: 41, + // No cost reported → the spend column falls back to a token total. + usage: { input_tokens: 15000, output_tokens: 2200 }, created_at: at(1, 2), last_turn: { state: 'ok', yield_reason: 'completed' }, }, @@ -685,7 +694,9 @@ const sessions = [ id: 'ses_j1k2l3', status: 'failed', agent_id: 'agt_3kf9xz', + agent_snapshot: { runtime: 'codex', model: 'openai/gpt-5.3-codex' }, head: 8, + usage: { cost_usd: 0.11 }, created_at: at(2, 5), last_turn: { state: 'error', yield_reason: 'error' }, }, @@ -693,7 +704,9 @@ const sessions = [ id: 'ses_m4n5o6', status: 'archived', agent_id: 'agt_7mq2aa', + agent_snapshot: { runtime: 'claude', model: 'anthropic/claude-opus-4-8' }, head: 60, + usage: { cost_usd: 1.42, input_tokens: 320000, output_tokens: 18400 }, created_at: at(4, 1), last_turn: { state: 'ok', yield_reason: 'completed' }, }, @@ -722,6 +735,17 @@ const sessionEvents = [ { id: 'evt_3', seq: 3, + type: 'agent.thinking', + level: 'progress', + actor: { type: 'agent', display: 'PR Reviewer' }, + body: { + text: 'Fetch the PR head, then read the auth middleware diff before commenting.', + }, + ts: at(0, 1), + }, + { + id: 'evt_4', + seq: 4, type: 'tool.call', level: 'progress', actor: { type: 'runtime' }, @@ -729,8 +753,21 @@ const sessionEvents = [ ts: at(0, 1), }, { - id: 'evt_4', - seq: 4, + id: 'evt_5', + seq: 5, + type: 'exec.completed', + level: 'progress', + actor: { type: 'runtime' }, + body: { + tool: 'bash', + summary: 'fetched pull/412/head → FETCH_HEAD', + duration_ms: 412, + }, + ts: at(0, 1), + }, + { + id: 'evt_6', + seq: 6, type: 'agent.message', level: 'user', actor: { type: 'agent', display: 'PR Reviewer' }, @@ -740,8 +777,8 @@ const sessionEvents = [ ts: at(0, 1), }, { - id: 'evt_5', - seq: 5, + id: 'evt_7', + seq: 7, type: 'turn.completed', level: 'user', actor: { type: 'runtime' }, @@ -750,6 +787,29 @@ const sessionEvents = [ }, ] +// Turns power the submission-health panel (GET /v3/sessions/:id/turns), newest first. +const sessionTurns = [ + { + id: 'trn_2', + state: 'ok', + yield_reason: 'needs_input', + started_at: at(0, 1), + completed_at: at(0, 1), + active_seconds: 6.4, + usage: { cost_usd: 0.0121, input_tokens: 4200, output_tokens: 310 }, + }, + { + id: 'trn_1', + state: 'error', + yield_reason: 'error', + started_at: at(0, 2), + completed_at: at(0, 2), + active_seconds: 2.1, + usage: {}, + error: { code: 'provision_infra', message: 'brain sandbox failed to start' }, + }, +] + const destinations = [ { id: 'dst_1', @@ -767,8 +827,8 @@ const deliveries = [ { id: 'dlv_1', destination: 'dst_1', - event_id: 'evt_5', - event_seq: 5, + event_id: 'evt_7', + event_seq: 7, status: 'delivered', attempts: 1, last_attempt_at: at(0, 1), @@ -778,8 +838,8 @@ const deliveries = [ { id: 'dlv_2', destination: 'dst_1', - event_id: 'evt_4', - event_seq: 4, + event_id: 'evt_6', + event_seq: 6, status: 'failed', attempts: 3, last_attempt_at: at(0, 1), @@ -876,6 +936,11 @@ const ROUTES: Array<[RegExp, Handler]> = [ [/^\/v3\/agents$/, () => ({ data: agents })], [/^\/v3\/credentials$/, () => ({ data: credentials })], [/^\/v3\/sessions\/[^/]+\/events/, () => ({ data: sessionEvents })], + [/^\/v3\/sessions\/[^/]+\/turns$/, () => ({ data: sessionTurns })], + [ + /^\/v3\/sessions\/[^/]+\/result$/, + () => ({ last_turn: sessionTurns[0], result: sessionEvents[6] }), + ], [/^\/v3\/sessions\/[^/]+\/destinations$/, () => ({ data: destinations })], [/^\/v3\/sessions\/[^/]+\/deliveries$/, () => ({ data: deliveries })], [/^\/v3\/sessions\/[^/]+$/, () => sessions[0]], diff --git a/web/src/api/schemas.ts b/web/src/api/schemas.ts index 05d8e4799..16469e987 100644 --- a/web/src/api/schemas.ts +++ b/web/src/api/schemas.ts @@ -487,10 +487,23 @@ export const SlackManifestResponseSchema = z.object({ status: z.string(), }) +// The pinned effective agent tuple (design 009 §3.5) the session ran with. `runtime` +// is what distinguishes flue from the brain-box runtimes (claude/codex/pi) in read views. +export const AgentSnapshotSchema = z.object({ + runtime: z.string().nullish(), + model: z.string().nullish(), + prompt_hash: z.string().nullish(), + revision: z.union([z.string(), z.number()]).nullish(), + agent_revision_number: z.number().nullish(), + digest: z.string().nullish(), + skill_bundle_digest: z.string().nullish(), +}) + export const SessionSchema = z.object({ id: z.string(), status: z.string(), agent_id: z.string().nullable().optional(), + agent_snapshot: AgentSnapshotSchema.nullish(), credential_id: z.string().nullable().optional(), head: z.coerce.number().optional(), // current event seq; API returns it as a string ("0") last_turn: record.nullish(), @@ -525,7 +538,13 @@ export const SessionEventSchema = z.object({ level: z.string(), actor: ActorSchema.optional(), body: z.unknown().optional(), - content_ref: z.string().nullish(), // set when body spilled to blob storage + // Set together when the body spilled to blob storage (body > 32KB): the inline + // `body` is absent/partial, `content_ref` points at the blob, `body_bytes` is the size. + content_ref: z.string().nullish(), + body_truncated: z.boolean().nullish(), + // PostgreSQL bigint values are serialized as strings by the sessions API. + // Coerce here so spilled event bodies remain visible in the live timeline. + body_bytes: z.coerce.number().nullish(), refs: record.nullish(), source: z.string().optional(), turn_id: z.string().nullable().optional(), @@ -544,8 +563,19 @@ export const TurnSchema = z.object({ attempt: z.number().optional(), started_at: z.string().nullable().optional(), completed_at: z.string().nullable().optional(), - usage: record.optional(), - error: z.string().nullable().optional(), + active_seconds: z.number().nullish(), + result_event_id: z.string().nullish(), + usage: record.nullish(), + error: z.unknown().nullish(), // server serializes the error as an opaque object, not a string +}) +export const SessionTurnListSchema = z.object({ + data: z.array(TurnSchema), + next_cursor: z.string().nullish(), +}) +// GET /v3/sessions/:id/result → the latest turn + its result event (if any). +export const SessionResultSchema = z.object({ + last_turn: TurnSchema.nullable(), + result: SessionEventSchema.nullable(), }) export const DestinationSchema = z.object({ @@ -583,8 +613,10 @@ export type Credential = z.infer export type SlackConnection = z.infer export type SlackManifestResponse = z.infer export type Session = z.infer +export type AgentSnapshot = z.infer export type SessionEvent = z.infer export type Turn = z.infer +export type SessionResult = z.infer export type Destination = z.infer export type Delivery = z.infer diff --git a/web/src/components/runtime-badge.tsx b/web/src/components/runtime-badge.tsx new file mode 100644 index 000000000..82dc764bb --- /dev/null +++ b/web/src/components/runtime-badge.tsx @@ -0,0 +1,36 @@ +import { Bot, Cloud, type LucideIcon } from 'lucide-react' +import { runtimeLabel } from '@/lib/runtimes' +import { cn } from '@/lib/utils' + +// Runtime is a category, not a health state — so it gets a quiet, neutral pill (not a +// status tone). `flue` (the CF-native durable path) carries a subtle accent + a distinct +// icon so it reads apart from the brain-box runtimes (claude/codex/pi) at a glance. +const ICON: Record = { + flue: Cloud, +} + +export function RuntimeBadge({ + runtime, + className, +}: { + runtime: string | null | undefined + className?: string +}) { + if (!runtime) return + const Icon = ICON[runtime] ?? Bot + const isFlue = runtime === 'flue' + return ( + + + {runtimeLabel(runtime)} + + ) +} diff --git a/web/src/components/session-conversation.tsx b/web/src/components/session-conversation.tsx index a1c372f98..53d959725 100644 --- a/web/src/components/session-conversation.tsx +++ b/web/src/components/session-conversation.tsx @@ -5,7 +5,10 @@ import { Button } from '@/components/ui/button' import { bodyText, isOutOfCredits, + isTerminalSessionStatus, + isTurnInput, type GroupedTimeline, + type TurnState, } from '@/lib/session-turns' // A single chat bubble (You / Agent). Shared between the grouped Conversation @@ -44,69 +47,84 @@ export function MessageBubble({ ) } -// "A reply is coming" — a gentle agent-side typing indicator. Three dots pulse -// in sequence; no failure is implied. Shown while a turn is running with no -// answer yet, OR while input sits queued waiting for its follow-up turn to -// dispatch (the ~gap the user otherwise reads as "stuck"). +function TurnStateChip({ + state, +}: { + state: Extract +}) { + return ( + + {state === 'queued' ? ( +