diff --git a/.gitignore b/.gitignore index 44732d4..a260b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ CLAUDE.md # claude local settings .claude/settings.local.json + +# python venv for the separate-runner compat tests + caches +.venv/ +__pycache__/ diff --git a/README.md b/README.md index 7bd58a9..9fb10ea 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,53 @@ # api-proxy -A single Cloudflare Worker that reverse-proxies the OpenAI, Anthropic, and Google Gemini APIs behind **revocable "doppelganger" tokens**. You issue tokens from an admin dashboard and hand them out; each token is validated server-side and swapped for the real provider key before the request is forwarded. Consumers never see your real keys, and you can scope or revoke any token at any time. +A single Cloudflare Worker that reverse-proxies the OpenAI, Anthropic, and Google Gemini APIs — over **HTTP and WebSocket** — behind **revocable proxy tokens**. You issue tokens from an admin dashboard and hand them out; each token is validated server-side and swapped for the real provider key before the request is forwarded. Consumers never see your real keys, and you can scope or revoke any token at any time. -The consumer changes only **two things** in their normal SDK: the base URL (point at your worker) and the API key (use a doppelganger token). +## Use it -## How it works - -The doppelganger token rides in the SDK's normal auth slot. The worker reads it, validates it against KV, checks the token is scoped to the requested provider, strips every inbound auth header, sets the one real key, and forwards the request (path + query verbatim, streaming included). - -| Token arrives in | Provider | Upstream | Real key set as | -|---|---|---|---| -| `Authorization: Bearer` | OpenAI | `api.openai.com` | `Authorization: Bearer` | -| `x-api-key` | Anthropic | `api.anthropic.com` | `x-api-key` | -| `x-goog-api-key` / `?key=` | Gemini | `generativelanguage.googleapis.com` | `x-goog-api-key` | -| `Authorization: Bearer` + path `/v1beta/openai/*` | Gemini (OpenAI-compat) | `generativelanguage.googleapis.com` | `Authorization: Bearer` | - -## Client setup - -Point the SDK's base URL at the worker and use a doppelganger token as the key: +Works with the official **OpenAI**, **Anthropic**, and **Google GenAI** SDKs (Python and Node) — and, since the worker routes by auth header and forwards verbatim, with anything that speaks those APIs: the Vercel AI SDK, LangChain, LiteLLM, OpenAI-compatible tools, or raw `curl`. A client changes only **two things**: the base URL and the API key (a proxy token). -| SDK | base URL | key | +| Client | base URL | API key | |---|---|---| -| OpenAI (Python / Node) | `https:///v1` | token | -| Anthropic (Python / Node) | `https://` (no `/v1`) | token | -| Google `@google/genai` (Node) | `httpOptions.baseUrl = https://` | token | -| Gemini from Python | point the **OpenAI** SDK at `https:///v1beta/openai` | token | +| OpenAI SDK (Python / Node) | `https:///v1` | proxy token | +| Anthropic SDK (Python / Node) | `https://` (no `/v1`) | proxy token | +| Google `@google/genai` (Node) | `httpOptions.baseUrl = https://` | proxy token | +| Gemini via the OpenAI SDK | `https:///v1beta/openai` | proxy token | + +```python +# OpenAI SDK (Python); Node is identical +from openai import OpenAI +client = OpenAI(base_url="https:///v1", api_key="") +client.chat.completions.create( + model="gpt-5.4", messages=[{"role": "user", "content": "Hello"}]) +``` + +Or raw HTTP: ```bash -# OpenAI-style curl https:///v1/chat/completions \ - -H "authorization: Bearer " -H "content-type: application/json" \ + -H "authorization: Bearer " -H "content-type: application/json" \ -d '{"model":"gpt-5.4","messages":[{"role":"user","content":"Hello"}]}' ``` +Browser apps work too — the worker answers the CORS preflight and reflects the request Origin (provider browser opt-ins still apply, e.g. Anthropic's `dangerouslyAllowBrowser`). + +### WebSocket / realtime + +Realtime sockets proxy the same way — point the WebSocket at the worker and use a proxy token. The worker swaps the token for the real key on the upgrade handshake. + +| WebSocket API | URL | token slot | +|---|---|---| +| OpenAI Realtime (server) | `wss:///v1/realtime?model=…` | `Authorization: Bearer ` | +| OpenAI Realtime (browser) | `wss:///v1/realtime?model=…` | `Sec-WebSocket-Protocol: realtime, openai-insecure-api-key.` | +| OpenAI Responses (WebSocket mode) | `wss:///v1/responses` | `Authorization: Bearer ` | +| Gemini Live | `wss:///ws/…BidiGenerateContent?key=` | `?key=` query | + +A browser can't set the `Authorization` header on a WebSocket, so OpenAI smuggles the key in the `openai-insecure-api-key.` subprotocol — the worker reads it there and re-presents it as a Bearer header upstream. Anthropic has no WebSocket API. A long-lived socket is rate-limited and validated **once at connect**, so a revoke applies to the next connection, not an open stream. + +## How it works + +The proxy token rides in the SDK's normal auth slot. The worker validates it, checks it's scoped to the requested provider, strips every inbound auth header, sets the one real key, and forwards the request (path + query verbatim, streaming included). Routing is by which auth header the token arrives in — see [docs/architecture.md](docs/architecture.md) for the routing table and full design. + ## Setup ```bash @@ -55,41 +70,47 @@ Optional plain vars (NOT secrets) override the upstreams; they default to the re ## Admin dashboard -Visit `https:///admin`, sign in with `ADMIN_SECRET`, and create tokens: give each a label, the providers it may use (OpenAI / Anthropic / Gemini), and either type a token or generate one. The token is shown **once** at creation — copy it then; only its SHA-256 hash is stored. Disable or delete any token instantly. +Visit `https:///admin`, sign in with `ADMIN_SECRET`, and create tokens: give each a label, the providers it may use (OpenAI / Anthropic / Gemini), an optional expiry, and either type a token or generate one. The token is shown **once** at creation — copy it then; only its SHA-256 hash is stored. Disable or delete any token instantly. + +## Per-token controls + +- **Expiry** — optionally set an expiry at creation; past it the token is rejected and the dashboard shows it as `expired`. +- **Rate limit** — each token is capped at 100 requests / 60s (`429` + `Retry-After` over the limit). Tune `[[ratelimits]]` in `wrangler.toml`. It is a per-colo, loose ceiling for abuse protection, not a strict quota. +- **Scope & revoke** — a token only reaches the providers you check; disable or delete to revoke (KV propagation is up to ~60s). ## Security - Real provider keys are Cloudflare secrets, injected only into outbound requests — never in KV, never returned to callers. - Tokens are stored as SHA-256 hashes; a KV/dashboard dump yields unusable hashes, not live tokens. -- The worker strips all inbound auth headers before setting the real key, so a doppelganger token is never forwarded upstream. +- The worker strips all inbound auth headers before setting the real key, so a proxy token is never forwarded upstream. - Do not host the worker on a `*.openai.azure.com` / `*.cognitiveservices.azure.com` domain (the OpenAI SDK switches to Azure auth on those hostnames). ## Testing -Two tiers (Vitest): - ```bash nub run test:unit # tier 1: proxy logic in workerd (vitest-pool-workers), fast CI gate -nub run test:compat # tier 2: real openai / @anthropic-ai/sdk / @google/genai SDKs vs a local worker + mock upstream -nub run test # both +nub run test:compat # tier 2: real client libs (official SDKs, Vercel AI SDK, LangChain, Genkit) + raw fetch + a real wss round-trip vs a mock upstream +nub run test:py # tier 2 (Python): LiteLLM, LlamaIndex, instructor, Pydantic AI through the worker (needs the venv below) +nub run test # all of the above ``` -Tier 2 starts the real worker (`unstable_dev`) with `*_UPSTREAM` pointed at a `node:http` mock, seeds a token via the admin API, then drives each real SDK and asserts the forwarded request carries the real key (and never the token). +Tier 2 starts the real worker (`unstable_dev`) with `*_UPSTREAM` pointed at a `node:http` mock, seeds a token via the admin API, drives each real client, and asserts the forwarded request carries the real key (and never the token). The Python runner (`test/run-py.mjs`) owns the same worker + mock and runs each `*.py` as a thin client. **Each file in `test/sdk-compat/` is named after the package it drives and doubles as a usage example** — copy the `baseURL`/`apiKey` wiring from the file matching your client (e.g. `ai-sdk-openai.ts`, `langchain-anthropic.ts`, `genkit.ts`, `pydantic-ai.py`), from `fetch.ts` for raw HTTP, or from `websocket.ts` for a wss client. -## Disable / Enable +**What's tested, and what's by-construction.** The worker routes by *which auth slot a request uses*, not by SDK — so a provider's packages behave identically once the slot is fixed. We therefore test **each distinct library once, in one language** — the official `openai` / `@anthropic-ai/sdk` / `@google/genai` SDKs, the Vercel AI SDK, LangChain, Genkit, LiteLLM, LlamaIndex, instructor, and Pydantic AI (see `test/sdk-compat/`) — and treat the rest as compatible-by-construction: a tested SDK's other-language packages (`openai-python`/`-go`/`-java`/...), end-user apps (Aider, Cline, Continue, Open WebUI), and JVM/.NET frameworks (Spring AI, Semantic Kernel) each reuse a slot already proven. The per-provider proof matrix is in [docs/learnings/compat-is-the-auth-slot-not-the-sdk.md](docs/learnings/compat-is-the-auth-slot-not-the-sdk.md). Two gotchas: use Anthropic's normal API-key mode (its OAuth `authToken` mode sends `Bearer`, which would route to OpenAI), and the legacy `google-generativeai` Python SDK needs `transport="rest"` (it defaults to gRPC and won't traverse an HTTP proxy otherwise). -`schedule.sh` toggles the worker's `workers_dev` URL without deleting it: +The Python runner uses a local venv. One-time setup with [uv](https://docs.astral.sh/uv/): ```bash -./schedule.sh disable # now -./schedule.sh disable +30m # in 30 minutes -./schedule.sh enable 22:00 # at 10pm +uv venv +uv pip install -r test/requirements.txt ``` +> **Gemini is untested with the actual API.** No test hits a live provider — all three run against a mock upstream. OpenAI and Anthropic are additionally verified live in deployment; Gemini is **not**, because `GEMINI_API_KEY` isn't set yet, so the Gemini route has never run against the real Google Generative Language API. Treat it as built-but-unproven until a key is added. + ## Cost Cloudflare Workers free tier covers this (100k requests/day). You only pay upstream providers for API usage. ## Contributing -Issues are welcome. PRs are not accepted and will be auto-closed. +Issues are welcome. External PRs are not accepted and will be auto-closed. diff --git a/schedule.sh b/_legacy/schedule.sh similarity index 83% rename from schedule.sh rename to _legacy/schedule.sh index 2a3cdd4..4c1bd90 100755 --- a/schedule.sh +++ b/_legacy/schedule.sh @@ -10,9 +10,9 @@ for arg in "$@"; do *) time_args+=("$arg") ;; esac done -# Default target is the single token-gated worker (wrangler.toml). The provider flags -# still target the legacy per-provider workers during the transition. -config="${label:+wrangler.${label}.toml}"; config="${config:-wrangler.toml}" +# Archived helper (kept aside in _legacy/). Default target is the active token-gated worker at the +# repo root (wrangler.toml); the provider flags target the archived v1 workers in _legacy/v1/. +config="${label:+_legacy/v1/wrangler.${label}.toml}"; config="${config:-wrangler.toml}" name="${label:-api-proxy}" time_arg="${time_args[*]:-}" @@ -21,7 +21,7 @@ time_arg="${time_args[*]:-}" } [[ "$action" == "enable" ]] && from=false to=true || from=true to=false -dir=$(cd "$(dirname "$0")" && pwd) +dir=$(cd "$(dirname "$0")/.." && pwd) # repo root (this script lives in _legacy/) if [[ -z "$time_arg" ]]; then sed -i '' "s/workers_dev = $from/workers_dev = $to/" "$dir/$config" diff --git a/_legacy/v1/README.md b/_legacy/v1/README.md new file mode 100644 index 0000000..ecf38bb --- /dev/null +++ b/_legacy/v1/README.md @@ -0,0 +1,35 @@ +# v1 - one worker per provider (archived) + +The original proxy: **three separate Workers**, one per provider, each a thin pass-through +that swaps the hostname and injects the real key from its own secret. + +``` +client ──▶ openai-proxy ──(Bearer OPENAI_API_KEY)──▶ api.openai.com +client ──▶ claude-proxy ──(x-api-key ANTHROPIC_KEY)─▶ api.anthropic.com +client ──▶ gemini-proxy ──(x-goog-api-key GEMINI)──▶ generativelanguage.googleapis.com +``` + +| File | Worker | Upstream | Key slot it sets | +|---|---|---|---| +| `openai.ts` / `wrangler.openai.toml` | `openai-proxy` | api.openai.com | `Authorization: Bearer` | +| `claude.ts` / `wrangler.claude.toml` | `claude-proxy` | api.anthropic.com | `x-api-key` | +| `gemini.ts` / `wrangler.gemini.toml` | `gemini-proxy` | generativelanguage.googleapis.com | `x-goog-api-key` (and strips `?key=`) | + +## Why it was replaced + +- **No auth.** Each worker injected the real upstream key for *any* caller. Anyone who knew + the URL spent the key. There were no shareable, revocable tokens. +- **Three deploys, three URLs.** Clients had to know which worker maps to which provider, and + each needed its own secret and deploy. + +v2 (the active root worker) collapses all three into **one** worker that routes by auth header, +gates every request behind a hashed [proxy token](../../docs/learnings/proxy-token-security.md), +and adds the [OpenAI geo-403 egress fix](../../docs/learnings/openai-egress-geo-block.md). See +[provider routing by auth header](../../docs/learnings/provider-routing-by-auth-header.md) for how +one base URL serves all three. + +## Status + +Reference only - **not deployed, not built, not tested.** Kept to document where the project +started. The `main` paths in these tomls point at files in this folder, so each could still be +deployed standalone (`wrangler deploy -c _legacy/v1/wrangler.openai.toml`) if ever needed. diff --git a/src/claude.ts b/_legacy/v1/claude.ts similarity index 100% rename from src/claude.ts rename to _legacy/v1/claude.ts diff --git a/src/gemini.ts b/_legacy/v1/gemini.ts similarity index 100% rename from src/gemini.ts rename to _legacy/v1/gemini.ts diff --git a/src/openai.ts b/_legacy/v1/openai.ts similarity index 100% rename from src/openai.ts rename to _legacy/v1/openai.ts diff --git a/wrangler.claude.toml b/_legacy/v1/wrangler.claude.toml similarity index 80% rename from wrangler.claude.toml rename to _legacy/v1/wrangler.claude.toml index 591c989..1ce3c1f 100644 --- a/wrangler.claude.toml +++ b/_legacy/v1/wrangler.claude.toml @@ -1,5 +1,5 @@ name = "claude-proxy" -main = "src/claude.ts" +main = "claude.ts" compatibility_date = "2025-01-01" workers_dev = true preview_urls = false diff --git a/wrangler.gemini.toml b/_legacy/v1/wrangler.gemini.toml similarity index 80% rename from wrangler.gemini.toml rename to _legacy/v1/wrangler.gemini.toml index 6091d99..d698d4b 100644 --- a/wrangler.gemini.toml +++ b/_legacy/v1/wrangler.gemini.toml @@ -1,5 +1,5 @@ name = "gemini-proxy" -main = "src/gemini.ts" +main = "gemini.ts" compatibility_date = "2025-01-01" workers_dev = true preview_urls = false diff --git a/wrangler.openai.toml b/_legacy/v1/wrangler.openai.toml similarity index 80% rename from wrangler.openai.toml rename to _legacy/v1/wrangler.openai.toml index b5eb2ab..60bf453 100644 --- a/wrangler.openai.toml +++ b/_legacy/v1/wrangler.openai.toml @@ -1,5 +1,5 @@ name = "openai-proxy" -main = "src/openai.ts" +main = "openai.ts" compatibility_date = "2025-01-01" workers_dev = true preview_urls = false diff --git a/biome.json b/biome.json index fd151d6..190f5b6 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..909db2a --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,183 @@ +# api-proxy — Architecture + +A single Cloudflare Worker (Free plan) that reverse-proxies **OpenAI, Anthropic, and Google Gemini** behind shareable, revocable **proxy tokens**. A client changes only its base URL and API key; the worker validates the token, swaps in the real provider key server-side, and forwards the request verbatim. The real key never leaves Cloudflare. + +This document is the current design. Topic deep-dives with the "why" live in [`docs/learnings/`](learnings/); the retired one-worker-per-provider v1 lives in [`_legacy/v1/`](../_legacy/v1/). + +--- + +## 1. Problem + +v1 was three unauthenticated workers (one per provider), each injecting a shared real key for **anyone** who knew the URL — no per-user access, no revocation. v2 collapses them into one token-gated worker. + +## 2. Topology + +One worker, dispatched in `src/index.ts`: + +- **WebSocket upgrade** (`Upgrade: websocket`) → `handleWsProxy`, the wss hot path (§10), checked first so a realtime client never falls through to the HTTP branch. +- **`/admin/*`** → the Hono admin sub-app, wrapped in `try/catch` (→ 500) so an admin bug can never crash the proxy branch. +- **everything else** → `handleProxy`, the framework-free hot path (`src/proxy.ts` must never import Hono — it is pure functions plus a `fetch` handler). + +## 3. Request flow (proxy hot path) + +`handleProxy` is a thin wrapper: it answers an `OPTIONS` preflight directly, otherwise runs `proxyRequest` and reflects CORS headers onto the result (§8). `proxyRequest`: + +1. **Extract** the token from whichever auth slot it arrived in and **route** the provider from that slot (+ path); missing either → **401**. (§4) +2. **Validate** `SHA-256(token)` against KV — a miss, a disabled token, or an expired one → **401**. (§6) +3. Requested provider not in the token's scope → **403**. (§4) +4. **Rate-limit** on the hash — over the cap → **429** + `Retry-After` (fail-open). (§7) +5. **Rewrite** the URL to the upstream — protocol/host/port only; strip `?key=` for Gemini. (§13) +6. **Swap auth** — strip every inbound auth header, set the one real key. (§5) +7. **Fetch** the upstream (OpenAI adds a geo-403 fallback, §9), stream the response back unbuffered, and stamp `lastUsed` fire-and-forget. (§6, §9) + +## 4. Provider routing (by auth header) + +The client adds no path prefix and no custom header — routing reads **which auth slot the SDK populated** (`routeProvider`, `extractToken`): + +| Inbound signal | Provider | Upstream | +|---|---|---| +| `x-api-key` | `anthropic` | api.anthropic.com | +| `x-goog-api-key` or `?key=` | `gemini` | generativelanguage.googleapis.com | +| `Authorization: Bearer` + path `/v1beta/openai/*` | `gemini-openai` | generativelanguage.googleapis.com | +| `Authorization: Bearer` (else) | `openai` | api.openai.com | +| none | — | 401 | + +Auth slots are checked **before** `?key=`, so a request carrying `Authorization: Bearer` routes to openai / gemini-openai even when it also has `?key=`; the `x-goog-api-key or ?key=` equivalence holds only when no Bearer header is present. + +`gemini-openai` (the OpenAI-compatible Gemini endpoint) collapses to the `gemini` scope via `coarse()`; the distinction only selects the auth-swap branch. **Why no `/openai` `/anthropic` path prefix:** it would break Gemini's file-upload flow (absolute `x-goog-upload-url` round trip) and force every client to rewrite the SDK's own base path. See [`provider-routing-by-auth-header.md`](learnings/provider-routing-by-auth-header.md). + +## 5. Auth swap (security linchpin) + +Before forwarding, `swapAuth` deletes **every** inbound auth header and sets exactly one with the real key: + +```ts +headers.delete("x-api-key"); headers.delete("x-goog-api-key"); headers.delete("authorization"); +switch (provider) { + case "openai": case "gemini-openai": headers.set("authorization", `Bearer ${realKey}`); break; + case "anthropic": headers.set("x-api-key", realKey); break; + case "gemini": headers.set("x-goog-api-key", realKey); break; +} +``` + +Strip-all-then-set-one guarantees the proxy token is never forwarded upstream even if a client sends it in an unexpected slot, and closes dual-header leaks. A test asserts the token never appears in any outbound auth header. See [`proxy-token-security.md`](learnings/proxy-token-security.md). + +## 6. Token model & lifecycle + +KV namespace `TOKENS`, keyed by `SHA-256(token)` (hex). The plaintext is shown **once** at creation and never persisted (`src/tokens.ts`, `src/types.ts`): + +```ts +type TokenMetadata = { + label: string; + last4: string; // for display + providers: ("openai"|"anthropic"|"gemini")[]; // coarse scope + status: "active" | "disabled"; + createdAt: string; // ISO + expiresAt?: string; // ISO (UTC); absent = never expires +}; +``` + +- **Tokens** are opaque: `ptk_` + 32 url-safe chars (24 random bytes). Custom admin-typed tokens are allowed; validation is by hash of the full string. +- **Validation** (`getValidatedByHash`): returns the record only if `status === "active"` AND, when `expiresAt` is set, it parses to a future timestamp — malformed or past expiry is rejected **fail-closed**. Not KV `expirationTtl` (60s floor, silently deletes the record, orphans the `:lu` key) — see [`token-expiry-check-at-validate.md`](learnings/token-expiry-check-at-validate.md). +- **`lastUsed`** lives in a separate `:lu` key, written fire-and-forget on each proxied request. Keeping it out of the token record means stamping it can never resurrect or re-enable a token the admin just disabled or deleted. +- **Lifecycle:** `createToken`, `listTokens` (paginates KV, skips `:lu` keys), `updateToken` (label / providers / status), `deleteToken` (record + `:lu`). KV is eventually consistent (~60s), so revoke and new-token visibility can lag. + +## 7. Per-token rate limiting + +After validation, `RATE_LIMITER.limit({ key: hash })` (the Workers Rate Limiting binding) caps each token. Over the limit → `429` + `Retry-After: 60`. Wrapped in try/catch and **fail-open**: a missing or erroring binding must never brick the proxy. + +```toml +[[ratelimits]] +name = "RATE_LIMITER" +namespace_id = "1001" + + [ratelimits.simple] + limit = 100 # one shared ceiling for all tokens; tune freely + period = 60 # must be 10 or 60 +``` + +It is in-process (not a subrequest), keyed on the hash, and **per-colo + eventually consistent** — a loose ceiling for abuse protection, not a strict quota. Verified to run on the Free plan. See [`rate-limit-binding-free-and-loose.md`](learnings/rate-limit-binding-free-and-loose.md). + +## 8. CORS & browser support + +`handleProxy` short-circuits `OPTIONS` to a `204` preflight **before** the token checks (a preflight carries no auth header, so it would otherwise 401 and block every browser SDK). The preflight reflects the request `Origin`, reflects the requested `Access-Control-Request-Headers`, advertises a fixed method allow-list (`GET, POST, PUT, DELETE, OPTIONS`), and sets `Access-Control-Max-Age: 86400`. Every real response then passes through `withCors`, which reflects `Origin`, appends `Vary: Origin` (so per-Origin reflection is cache-safe), and exposes the Gemini resumable-upload headers (`x-goog-upload-url, x-goog-upload-status, x-goog-upload-chunk-granularity`). No `Origin` → no CORS headers (server-side callers are unaffected). Credentials mode is never enabled (SDKs send keys as headers, not cookies). Provider browser opt-ins still apply (e.g. Anthropic's `dangerouslyAllowBrowser`, which the SDK forwards as a header). + +**Gemini file uploads** pass through verbatim: the start call routes normally, Google returns an absolute, self-authenticating `x-goog-upload-url`, and the client uploads bytes **directly to Google** — that leg never transits the worker, so the 100 MB body cap is sidestepped and the real key is never on it. + +## 9. OpenAI geo-403 egress (North-America-pinned Durable Object) + +OpenAI 403s `unsupported_country_region_territory` when a request egresses from an unsupported colo (e.g. Hong Kong). A Worker's `fetch()` egresses from the colo the invocation runs in, fixed per invocation, so an in-invocation retry can't escape a bad colo. + +The fix is a fallback: try the fast edge `fetch()` first (requests that egress from a good colo return immediately); **only on the geo-403**, re-issue the same request through the `UsEgress` SQLite Durable Object pinned to North America (`locationHint: "wnam"`). Running in a US colo, its `fetch()` egresses from a supported region and succeeds. Only the OpenAI branch buffers the body (to replay it to the DO); a pool of DO ids spreads load. Anthropic and Gemini are untouched, and the real key never leaves Cloudflare. See [`openai-egress-geo-block.md`](learnings/openai-egress-geo-block.md). + +## 10. WebSocket (wss) proxying + +The token-swap model extends to WebSocket upgrades (`src/ws.ts`, `handleWsProxy`), dispatched before the HTTP branch on `Upgrade: websocket`. It serves the realtime/streaming sockets the HTTP APIs don't cover: **OpenAI** `/v1/realtime` and `/v1/responses` (WebSocket mode), and **Gemini Live** (`...BidiGenerateContent`). Anthropic has no wss API, so it is naturally excluded. + +The flow mirrors the HTTP path — extract token → validate hash → scope check → rate-limit → swap auth — then opens the upstream socket with `fetch(target, { Upgrade: websocket })` (the scheme stays `http(s):`; the header drives the upgrade), reads `resp.webSocket`, and pumps frames both ways through a `WebSocketPair`. It's a **manual pipe** (not a transparent pass-through) so the subprotocol the upstream negotiates is echoed back to the client deterministically — a browser handshake fails if the server picks none of the offered subprotocols. A non-101 upstream handshake (401/403/426) is surfaced to the client verbatim rather than as a generic error. + +**Auth on a WS handshake has a wider slot set than HTTP**, because a browser `WebSocket` cannot set request headers, so providers smuggle the key elsewhere: + +| Inbound slot | Provider | Swapped to (upstream) | +|---|---|---| +| `Authorization: Bearer ` | openai | `Authorization: Bearer ` | +| `Sec-WebSocket-Protocol: openai-insecure-api-key.` | openai | key entry dropped; real key set as `Authorization: Bearer` (the worker *can* set headers); `realtime` + org/project/beta subprotocols kept | +| `?key=` | gemini | `?key=` in the query (Gemini Live reads the key there, not a header) | + +`prepareWsUpstream` strips every inbound auth slot then sets exactly one — the WS analogue of `swapAuth` (§5) — so the proxy token never reaches the upstream in any slot (header, query, or subprotocol); a test asserts this. The OpenAI **geo-403 fallback (§9) applies here too**: a 403 from a bad colo re-issues the upgrade through the `UsEgress` DO. (The DO carries a WebSocket like a plain `fetch`; this reuses HTTP's egress path, though the geo-blocked WS hop itself isn't locally testable.) + +Caveats: the rate limit gates the **connection**, not each frame (one upgrade = one hit); a revoke takes effect on the next connection, not mid-stream (a long-lived socket is validated once, at upgrade time); and Cloudflare closes an idle socket after a quiet period, so a silent client should keep-alive. See [`websocket-proxy-auth-slots.md`](learnings/websocket-proxy-auth-slots.md). + +## 11. Admin dashboard + +Embedded **Hono** sub-app at `/admin` (`src/admin/`), server-rendered HTML via `hono/html` plus **HTMX 2.x** loaded from a CDN (zero client JS we author; nothing in the worker bundle but markup + attributes). + +- **Auth:** one `ADMIN_SECRET` password. `POST /admin/login` sets an HMAC-SHA256-signed cookie `cm_admin=.` (`Path=/admin; HttpOnly; Secure; SameSite=Strict; Max-Age=86400`). A middleware guards every `/admin/*` route except login; the signature check uses constant-time `crypto.subtle.verify`. +- **CRUD:** HTMX-driven over `/admin/api/tokens` — list (`GET`), create (`POST`; parses label, provider checkboxes, an optional `datetime-local` expiry normalized to UTC ISO, custom-or-generated token), edit / enable-disable (`PUT`), delete (`DELETE`). `:hash` params are validated as 64-hex. +- **UI:** an add-token card (label, token, **Expires (optional)**, provider checkboxes) and a token table (label, last-4, provider pills, status, **Expires**, last-used, disable/delete). The created plaintext is shown once; expired tokens render `expired` and dim the row. The list refreshes on load, on the `tokens-changed` event, and **every 10 s** (to surface new tokens / last-used despite KV's ~60 s list propagation). + +## 12. Real key handling + +`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `ADMIN_SECRET` are Cloudflare **secrets**, read at request time and injected only into the outbound request. Never logged, never stored in KV, never returned in a response body. The CORS and rate-limit paths never touch a real key. + +## 13. Storage, bindings & config (`wrangler.toml`) + +| Binding | Kind | Purpose | +|---|---|---| +| `TOKENS` | KV namespace | token store (by `SHA-256`) + `:lu` last-used keys | +| `US_EGRESS` | SQLite Durable Object (`UsEgress`) | NA-pinned egress fallback for OpenAI (HTTP + wss) | +| `RATE_LIMITER` | Rate Limit | per-token RPM ceiling | + +Plus `[[migrations]] tag="v1" new_sqlite_classes=["UsEgress"]`. Upstreams resolve through `upstreamBase()`: the `*_UPSTREAM` env vars (plain vars, not secrets) default to the real hosts and are overridden only by tests pointing at a mock; `rewriteToUpstream` rewrites just protocol/host/port. + +## 14. Testing (two tiers) + +| Tier | Runner | Scope | +|---|---|---| +| 1 — proxy logic | `@cloudflare/vitest-pool-workers` (workerd) | routing, auth swap, expiry, CORS, rate limit, geo-403 fallback, SSE passthrough, **WS upgrade + subprotocol auth swap** (`test/ws.test.ts`); mocks `fetch`, seeds KV directly | +| 2 — real client libs | `unstable_dev` worker + `node:http` mock upstream (Node via vitest; Python via `test/run-py.mjs`) | official `openai`/`@anthropic-ai/sdk`/`@google/genai` SDKs, Vercel AI SDK, LangChain, Genkit, LiteLLM, LlamaIndex, instructor, Pydantic AI, and a **real wss round-trip** (`websocket.ts`: `ws` client → worker → `ws` mock) — end-to-end | + +Tier 2 covers every auth slot — OpenAI (`Bearer`, `openai.ts` + `litellm.py`), Anthropic (`x-api-key`, `anthropic-ai-sdk.ts`), Gemini native (`x-goog-api-key`, `google-genai.ts`), Gemini OpenAI-compat (the OpenAI SDK at `/v1beta/openai`, `google-genai.ts`), and the Gemini `?key=` query slot plus verbatim path/query/body forwarding (`fetch.ts`) — plus streaming for the main three; OpenAI-compat streaming rides the same SSE passthrough, so it has no dedicated test. Each asserts the real key reaches the mock and the token never does. Compatibility is fixed by the **auth slot, not the SDK or language**, so each distinct library gets **one** end-to-end test in one language; a provider's other-language packages (`openai-python`/`-go`/...), end-user apps (Aider, Cline, Continue, Open WebUI), and JVM/.NET frameworks (Spring AI, Semantic Kernel) reuse a slot already proven and are documented rather than re-tested — see [`compat-is-the-auth-slot-not-the-sdk.md`](learnings/compat-is-the-auth-slot-not-the-sdk.md). **No test hits a live provider** (mock upstream only): OpenAI/Anthropic are verified live in deployment, but **Gemini has never run against the real Google API** (no key yet). + +## 15. Deployment + +```bash +nub install +nubx wrangler kv namespace create api-proxy-tokens # paste id into wrangler.toml +nubx wrangler secret put OPENAI_API_KEY # + ANTHROPIC / GEMINI / ADMIN_SECRET +nubx wrangler deploy +``` + +Free Workers plan covers it (100k req/day); you only pay upstream providers for usage. + +## 16. Security model + +Invariants are detailed in §5 (auth swap), §6 (token hashing, revoke-safe `lastUsed`), §11 (admin HMAC cookie), and §12 (real-key handling). + +Caveats: + +- KV is ~60s eventually consistent, so a revoke / expiry-flip is not instant — for an immediate cutoff, rotate the provider secret (instant, and the key stays in Cloudflare). +- Do not host on `*.openai.azure.com` / `*.cognitiveservices.azure.com` (the OpenAI SDK switches to Azure auth on those hostnames). + +## 17. Deferred / future + +The token data model leaves room (`limits`, `spend`) without carrying the weight now: spend / token-count caps + per-token usage analytics (needs a metering Durable Object and SSE usage parsing), multiple real keys per provider (key pools), concurrency limits and longer rate-limit windows, and instant (sub-minute) revocation via a DO allow/deny list. diff --git a/docs/learnings/README.md b/docs/learnings/README.md index 760468f..7769e55 100644 --- a/docs/learnings/README.md +++ b/docs/learnings/README.md @@ -2,12 +2,15 @@ A running log of project-specific knowledge worth not rediscovering. One topic per file, kept short. -Write a file for anything **non-general**: a gotcha, a constraint, a decision and its why, a platform or -API quirk we hit. The bar is "non-obvious and specific to this project," not "it changed the product -direction." Skip general/common knowledge anyone would already have. Don't rewrite history; append. +Write a file for anything **non-general**: a gotcha, a constraint, a decision and its why, a platform or API quirk we hit. The bar is "non-obvious and specific to this project," not "it changed the product direction." Skip general/common knowledge anyone would already have. Don't rewrite history; append. Each file: the problem, what we found, and the decision we keep. - [openai-egress-geo-block.md](openai-egress-geo-block.md) - why OpenAI 403'd ~40% of the time, and the North-America-pinned Durable Object that fixes it - [provider-routing-by-auth-header.md](provider-routing-by-auth-header.md) - one base URL, no path prefix; route by which auth slot the SDK used -- [doppelganger-token-security.md](doppelganger-token-security.md) - how a shareable token rides the SDK's auth slot without ever leaking the real key +- [proxy-token-security.md](proxy-token-security.md) - how a shareable token rides the SDK's auth slot without ever leaking the real key +- [token-expiry-check-at-validate.md](token-expiry-check-at-validate.md) - why expiry is checked at read time, not via KV TTL, and fail-closed on bad input +- [rate-limit-binding-free-and-loose.md](rate-limit-binding-free-and-loose.md) - the Workers Rate Limiting binding is free on the Free plan but a loose, per-colo ceiling +- [cors-preflight-and-upload-passthrough.md](cors-preflight-and-upload-passthrough.md) - why the browser preflight is answered before auth, and why the Gemini upload URL is passed through untouched +- [compat-is-the-auth-slot-not-the-sdk.md](compat-is-the-auth-slot-not-the-sdk.md) - why one test per auth slot proves every SDK/language/wrapper, so we don't add per-client tests +- [websocket-proxy-auth-slots.md](websocket-proxy-auth-slots.md) - proxying wss (OpenAI Realtime/Responses, Gemini Live), and why a browser smuggles the key in a subprotocol the worker must rewrite diff --git a/docs/learnings/compat-is-the-auth-slot-not-the-sdk.md b/docs/learnings/compat-is-the-auth-slot-not-the-sdk.md new file mode 100644 index 0000000..6d1ea76 --- /dev/null +++ b/docs/learnings/compat-is-the-auth-slot-not-the-sdk.md @@ -0,0 +1,47 @@ +# Compatibility is the auth slot, not the SDK + +## Problem + +Which clients do we need a compat test for? The candidates are endless: the official SDKs in six languages, the Vercel AI SDK, LangChain (JS + Python), LiteLLM, LlamaIndex, instructor, and every agent tool (Aider, Cline, Continue, Open WebUI, ...). Writing a test per client would never end, and most would be copies of each other. + +## What we found + +The proxy routes and authenticates **purely by which auth slot a request arrives in** plus one path check; it rewrites only host/port and forwards path + query + body verbatim (`src/proxy.ts` `routeProvider` / `extractToken`, `src/upstreams.ts` `rewriteToUpstream`). So a client's compatibility is decided by exactly two things: (1) which auth slot it puts the key in — one of the four the proxy reads (`x-api-key`, `x-goog-api-key`, `Authorization: Bearer`, or the `?key=` query param), with a single path check (`/v1beta/openai/`) splitting Bearer into the openai vs gemini-openai upstream — and (2) whether it lets you point its base URL at an arbitrary host. The SDK, the language, and the wrapper are irrelevant once those two are fixed. + +A source-level survey (official SDK source, provider docs, and wrapper source) confirms every client collapses onto one of these already-handled routes. **None hits a new slot or an unhandled path.** The table below lists the four provider routes SDKs actually use (`?key=` is covered after it): + +| Provider route | Slot the proxy keys on | Clients verified to use it | Wire proof | +|---|---|---|---| +| OpenAI | `Authorization: Bearer` | official SDKs: Python, Node, Go, Java, Ruby, .NET; `@ai-sdk/openai`; `@langchain/openai` (JS+Py); LiteLLM `openai/*`; LlamaIndex OpenAI; instructor; Aider; Cline; Continue; Open WebUI | Python `auth_headers → {"Authorization": f"Bearer {api_key}"}`; Go `Header.Set("authorization", "Bearer "+key)`; Ruby `bearer_auth`; Node sets the same `Authorization: Bearer` header | +| Anthropic | `x-api-key` (+ `anthropic-version`) | official SDKs: Python, Node, Go, Java, Ruby; `@ai-sdk/anthropic`; `@langchain/anthropic` (JS+Py); LlamaIndex Anthropic | every SDK's `auth_headers` sets `x-api-key` and auto-adds `anthropic-version: 2023-06-01` (forwarded verbatim) | +| Gemini (native) | `x-goog-api-key` | `@google/genai` (JS+Py), legacy `@google/generative-ai` / `google-generativeai`; `@ai-sdk/google`; `@langchain/google-genai` (JS+Py); LlamaIndex GoogleGenAI | `@ai-sdk/google` source: `'x-goog-api-key': loadApiKey(...)` — **not** `?key=`, **not** Bearer | +| Gemini (OpenAI-compat) | `Authorization: Bearer` + path `/v1beta/openai/` | any OpenAI SDK pointed at `…/v1beta/openai/` | Google's documented OpenAI-compat surface: `Authorization: Bearer `, `/v1beta/openai/chat/completions` | + +Every client also exposes a first-class base-URL override (`base_url` / `baseURL` / `WithBaseURL` / `OPENAI_BASE_URL` / `httpOptions.baseUrl` / `createX({ baseURL })` / `configuration.baseURL` / ...), so all can be aimed at the worker. + +All four slots the proxy reads are exercised end-to-end — the three header slots in the table plus the `?key=` query param (which no SDK uses, only raw HTTP via `fetch.ts`). The full list of tested libraries is in "What we test" below. + +## Caveats worth knowing (real divergences, not new slots) + +- **Anthropic OAuth/token mode.** Every Anthropic SDK can alternatively authenticate with `authToken` / `ANTHROPIC_AUTH_TOKEN`, which sends `Authorization: Bearer` instead of `x-api-key` — that would route to the **openai** slot here. Use the normal API-key (`x-api-key`) mode. +- **Legacy `google-generativeai` (Python) defaults to gRPC**, not HTTP, so it won't transit an HTTP proxy at all unless you set `transport="rest"`. The current `google-genai` SDK is HTTP by default. +- **OpenAI Responses API path.** Modern OpenAI clients (and the AI SDK 5 default) call `/v1/responses` rather than `/v1/chat/completions`. Both are `Authorization: Bearer` and forwarded verbatim, so both stay in the openai slot — different upstream endpoint, same proxy behavior. +- **Base-URL `/v1` convention differs per client.** The OpenAI SDK and `@ai-sdk/anthropic` want the `/v1` in the base URL; the official `@anthropic-ai/sdk` does **not** (it appends `/v1/messages` itself). Set each client's base URL the way that client documents it. + +## What we test, and what we document + +Compatibility is the slot, not the language — but a *library* is its own client with its own wiring (base-URL option, default endpoint, extra headers), so each distinct library gets one end-to-end test as a living usage example. We do **not** re-test the same library in every language: a provider's packages share one auth slot (the matrix above), so one language proves them all. + +**Tested end-to-end** (`test/sdk-compat/`, each file named after its package): + +- Node (`nub run test:compat`): the official `openai`, `@anthropic-ai/sdk`, `@google/genai`; the Vercel AI SDK (`@ai-sdk/openai`, `@ai-sdk/anthropic`, `@ai-sdk/google`); LangChain (`@langchain/openai`, `@langchain/anthropic`, `@langchain/google-genai`); Genkit (`@genkit-ai/google-genai`); raw `fetch`. +- Python (`nub run test:py`): LiteLLM, LlamaIndex (openai + anthropic + google-genai), instructor, Pydantic AI. + +**Documented as compatible-by-construction** (not separately tested) — each collapses onto a slot already proven above: + +- **Other-language packages of a tested SDK** — `openai-python` / `-go` / `-java` / `-ruby` / `-dotnet`, `anthropic` (py/go/java/ruby), `google-genai` (py). Same package family, same slot as the JS package already tested; re-testing each language is the redundancy we skip. +- **End-user apps, not importable libraries** — Aider, Cline, Continue, Open WebUI. Each speaks the OpenAI-compatible surface (Bearer slot) with a user-set base URL. +- **JVM / .NET frameworks** — Spring AI, Semantic Kernel. Same slots; no JVM/.NET toolchain in this repo to drive them. +- **Mastra** — `@mastra/core` 1.x is flagged by security advisory MAL-2026-6011 (embedded malicious code), so it is deliberately **not** pulled into the toolchain. It builds on the Vercel AI SDK, so by construction it uses the same Bearer slot already covered by `@ai-sdk/openai`. + +A new test is warranted only if a future client hits a genuinely new auth slot or routing path — which nothing in the current ecosystem does. diff --git a/docs/learnings/cors-preflight-and-upload-passthrough.md b/docs/learnings/cors-preflight-and-upload-passthrough.md new file mode 100644 index 0000000..07f7eaf --- /dev/null +++ b/docs/learnings/cors-preflight-and-upload-passthrough.md @@ -0,0 +1,45 @@ +# CORS preflight and upload passthrough + +Two browser-facing quirks from v2.1, both about what the proxy must *not* touch. + +## A. CORS preflight must run before token auth + +### Problem + +Browser SDK callers trigger a CORS preflight: the browser sends an `OPTIONS` request *first*, and that preflight carries **no auth header** (no `x-api-key`, no `Authorization`, no `?key=`). Run auth first and `extractToken`/`routeProvider` see nothing, so every preflight 401s - the browser then never fires the real request. All browser SDK callers break silently. + +### What we found + +`handleProxy` short-circuits `OPTIONS` to a `204` **before** any token work (`src/proxy.ts`, the `if (req.method === "OPTIONS")` at the top, ahead of `proxyRequest`): + +``` +inbound request + │ + ├─ method === OPTIONS? ──▶ corsPreflight ──▶ 204 (no token check) + └─ else ──▶ extractToken / routeProvider / validate / forward +``` + +The `204` reflects the caller's `Origin` and echoes the requested headers back (`access-control-allow-headers` = the inbound `access-control-request-headers`, else `*`). `withCors` also `append`s `Vary: Origin` to every response so caches don't mix per-origin replies. + +### Decision we keep + +Preflight is answered before auth, and the real key never rides any CORS path (`withCors`/`corsPreflight` only set `access-control-*` and `Vary`). Auth-first would be the natural instinct and it is wrong here. + +## B. Gemini resumable-upload URL is passed through, not rewritten + +### Problem + +Gemini's resumable upload returns an **absolute** `x-goog-upload-url` pointing straight at Google. If the proxy tried to own that flow, large uploads would hit the Worker's 100MB request-body cap. + +### What we found + +`rewriteToUpstream` (`src/upstreams.ts`) only swaps `protocol`/`hostname`/`port` on the *request* URL - it never rewrites the absolute `x-goog-upload-url` Google returns. So the client uploads bytes **directly to Google**, bypassing the Worker and its body cap. The proxy's only job is to let the browser *read* that header: `withCors` sets `access-control-expose-headers` to the `EXPOSE_HEADERS` constant (`x-goog-upload-url, x-goog-upload-status, x-goog-upload-chunk-granularity`). + +``` +client ──▶ proxy ──▶ Google : returns absolute x-goog-upload-url (not rewritten) +client ───────────▶ Google : uploads bytes straight to that URL (skips Worker + 100MB cap) +``` + +### Decision we keep + +Pass the upload URL through untouched; only expose the headers. This is also **why a path-prefix routing scheme would break Gemini** - the absolute upload URL can't carry a `/gemini/` prefix - which is the upload half of the case in [provider-routing-by-auth-header.md](provider-routing-by-auth-header.md). Token security on the normal path is unchanged (see [proxy-token-security.md](proxy-token-security.md)). diff --git a/docs/learnings/doppelganger-token-security.md b/docs/learnings/doppelganger-token-security.md deleted file mode 100644 index 9222f81..0000000 --- a/docs/learnings/doppelganger-token-security.md +++ /dev/null @@ -1,47 +0,0 @@ -# Doppelganger token security - -## Idea - -A "doppelganger" token is a shareable, revocable stand-in for a real provider key. The holder puts it -in the normal SDK auth slot; the proxy validates it, then swaps in the real key. You can hand someone -access without exposing your OpenAI/Anthropic/Gemini key, and revoke it any time. - -## Request flow - -``` -client SDK Worker upstream -────────── ────── ──────── -auth slot = ──▶ extract token from auth slot - look up SHA-256(token) in KV - ├─ not found / disabled ──▶ 401 - ├─ provider not in scope ──▶ 403 - └─ ok: - strip ALL auth headers - set ONE real key ──────────────────▶ api..com - (token never sent on) -``` - -## The decisions that keep it safe - -- **Token rides the SDK's auth slot.** No custom header, no path change. The client only swaps base URL - and key. The proxy reads the token from whichever slot routing matched - (see [provider-routing-by-auth-header.md](provider-routing-by-auth-header.md)). - -- **Strip-all-then-set-one.** Before forwarding, delete *every* inbound auth header - (`authorization`, `x-api-key`, `x-goog-api-key`) and set exactly one with the real key - (`src/proxy.ts` `swapAuth`). This guarantees the doppelganger token is never forwarded upstream, - even if a client sends it in an unexpected slot. A test asserts the token never appears in any - outbound auth header. - -- **Hashed at rest.** Tokens are stored as `SHA-256(token)` and shown to the admin exactly once at - creation. The KV value never contains the plaintext. - -- **Revoke-safe `lastUsed`.** Usage timestamps live in a separate `:lu` key, not in the token - record. Stamping "last used" on a hot path can therefore never recreate or re-enable a record that - was deleted or disabled - a revoked token stays revoked. - -## Scope (v1) - -Per-token: label, provider scope, enable/disable, revoke, last-used. Rate limits, spend caps, expiry, -and per-token analytics are deliberately deferred - the data model leaves room without carrying the -weight now. diff --git a/docs/learnings/openai-egress-geo-block.md b/docs/learnings/openai-egress-geo-block.md index 405757d..c9a5fc7 100644 --- a/docs/learnings/openai-egress-geo-block.md +++ b/docs/learnings/openai-egress-geo-block.md @@ -2,8 +2,7 @@ ## Problem -Through the Worker, OpenAI returned `403 unsupported_country_region_territory` intermittently -(~40% of requests). Anthropic was always fine. A valid key, correct path, correct auth. +Through the Worker, OpenAI returned `403 unsupported_country_region_territory` intermittently (~40% of requests). Anthropic was always fine. A valid key, correct path, correct auth. ## The mechanism in one picture @@ -25,33 +24,22 @@ Roughly 40% of invocations happened to egress via HKG, hence the ~40% failure. ## What we found -- Single probes of `GET /v1/models`, `POST /v1/chat/completions`, and streaming all succeeded, so - egress was not blanket-blocked. -- Hammering the same endpoint exposed the ~40% failure, and the failures correlated **100%** with - the Cloudflare egress colo: every request that egressed via **Hong Kong (HKG) returned 403**, every - one via **Singapore (SIN) returned 200**. It is OpenAI's country geo-restriction (HKG/CN unsupported), - not an IP-reputation or bot block. Anthropic works because it does not geo-block those regions. +- Single probes of `GET /v1/models`, `POST /v1/chat/completions`, and streaming all succeeded, so egress was not blanket-blocked. +- Hammering the same endpoint exposed the ~40% failure, and the failures correlated **100%** with the Cloudflare egress colo: every request that egressed via **Hong Kong (HKG) returned 403**, every one via **Singapore (SIN) returned 200**. It is OpenAI's country geo-restriction (HKG/CN unsupported), not an IP-reputation or bot block. Anthropic works because it does not geo-block those regions. - A Worker's `fetch()` egresses from the colo the invocation runs in, and that colo varies per request. -- The egress colo is **pinned per invocation**: six sequential subrequests inside one invocation always - hit the same colo. So an in-invocation **retry cannot escape a bad colo** - it just re-hits HKG. The - colo only re-rolls across separate invocations. +- The egress colo is **pinned per invocation**: six sequential subrequests inside one invocation always hit the same colo. So an in-invocation **retry cannot escape a bad colo** - it just re-hits HKG. The colo only re-rolls across separate invocations. ## What does NOT fix it -- **Smart Placement** and **`placement.region`** optimize *execution* location for *latency*, not - *egress country*. They have no notion of "OpenAI-supported region," can leave a Worker in HKG, and - for a single-subrequest proxy may not relocate at all. Community reports confirm they don't fix this. +- **Smart Placement** and **`placement.region`** optimize *execution* location for *latency*, not *egress country*. They have no notion of "OpenAI-supported region," can leave a Worker in HKG, and for a single-subrequest proxy may not relocate at all. Community reports confirm they don't fix this. - **Dedicated Egress IPs / Regional Services** would pin egress region, but they are paid/Enterprise. -- A **third-party relay** in a supported region (Vercel `iad1`, free VPS) works, but routes the real - OpenAI key through another host - rejected on the "key never leaves Cloudflare" rule. +- A **third-party relay** in a supported region (Vercel `iad1`, free VPS) works, but routes the real OpenAI key through another host - rejected on the "key never leaves Cloudflare" rule. ## The fix -Route **only the OpenAI hop** through a Durable Object pinned to North America with -`locationHint:"wnam"` (`src/egress.ts`). The DO runs in a US colo, so its `fetch()` egresses from an -OpenAI-supported region. It is wired as a **fallback**, not the default path: try the fast edge fetch -first and re-issue through the DO **only on the geo-403** (`src/proxy.ts`). The request body is buffered -for OpenAI so it can be replayed to the DO. +Route **only the OpenAI hop** through a Durable Object pinned to North America with `locationHint:"wnam"` (`src/egress.ts`). The DO runs in a US colo, so its `fetch()` egresses from an OpenAI-supported region. It is wired as a **fallback**, not the default path: try the fast edge fetch first and re-issue through the DO **only on the geo-403** (`src/proxy.ts`). The request body is buffered for OpenAI so it can be replayed to the DO. + +The egress DO is **pooled across 8 named instances** (`EGRESS_POOL=8`, `idFromName('oa-egress-N')` with a random `N`), so all OpenAI traffic isn't funneled through one DO. ``` OpenAI request @@ -71,11 +59,8 @@ OpenAI request Why this shape: - The real key never leaves Cloudflare. - Free on the Workers Free plan (SQLite-backed Durable Object). -- The ~60% of OpenAI calls that already egress from a good colo stay fast (no extra hop); Anthropic and - Gemini are untouched. +- The ~60% of OpenAI calls that already egress from a good colo stay fast (no extra hop); Anthropic and Gemini are untouched. ## Result -Post-fix stress test: 25/25 `200`, 0 `403`. The DO's egress colos verified as US (DFW/LAX/DEN/SJC/SEA). -Streaming survives the fallback. If another provider ever shows the same geo-403, apply the same DO -pattern by extending the `coarse(provider) === "openai"` branch. +Post-fix stress test: 25/25 `200`, 0 `403`. The DO's egress colos verified as US (DFW/LAX/DEN/SJC/SEA). Streaming survives the fallback. If another provider ever shows the same geo-403, apply the same DO pattern by extending the `coarse(provider) === "openai"` branch. diff --git a/docs/learnings/provider-routing-by-auth-header.md b/docs/learnings/provider-routing-by-auth-header.md index d56c11f..d52f33a 100644 --- a/docs/learnings/provider-routing-by-auth-header.md +++ b/docs/learnings/provider-routing-by-auth-header.md @@ -2,9 +2,7 @@ ## Problem -One consolidated base URL has to transparently serve OpenAI, Anthropic, and Gemini. The goal: a client -changes only its base URL and API key, nothing else. So the proxy must decide which upstream a request -is for without the client adding a path prefix or custom header. +One consolidated base URL has to transparently serve OpenAI, Anthropic, and Gemini. The goal: a client changes only its base URL and API key, nothing else. So the proxy must decide which upstream a request is for without the client adding a path prefix or custom header. ## What we found @@ -33,15 +31,11 @@ inbound request ## Why not a path prefix (e.g. `/openai/...`) -- It would break Gemini, whose file-upload flow returns absolute `x-goog-upload-url` paths the client - then calls directly; a prefix scheme can't survive that round trip. -- It would force every client to rewrite the SDK's own base path, defeating the "change only base URL + - key" promise. +- It would break Gemini, whose file-upload flow returns absolute `x-goog-upload-url` paths the client then calls directly; a prefix scheme can't survive that round trip. +- It would force every client to rewrite the SDK's own base path, defeating the "change only base URL + key" promise. Auth-slot routing keeps each SDK's native path intact, so it stays a true drop-in. ## Decision we keep -Route by auth header. The token is extracted from the same slot, validated, and then **all** inbound -auth headers are stripped and exactly one real key is set for the chosen provider (see -[doppelganger-token-security.md](doppelganger-token-security.md)). +Route by auth header. The token is extracted from the same slot, validated, and then **all** inbound auth headers are stripped and exactly one real key is set for the chosen provider (see [proxy-token-security.md](proxy-token-security.md)). diff --git a/docs/learnings/proxy-token-security.md b/docs/learnings/proxy-token-security.md new file mode 100644 index 0000000..3f9057f --- /dev/null +++ b/docs/learnings/proxy-token-security.md @@ -0,0 +1,34 @@ +# Proxy token security + +## Idea + +A proxy token is a shareable, revocable stand-in for a real provider key. The holder puts it in the normal SDK auth slot; the proxy validates it, then swaps in the real key. You can hand someone access without exposing your OpenAI/Anthropic/Gemini key, and revoke it any time. + +## Request flow + +``` +client SDK Worker upstream +────────── ────── ──────── +auth slot = ──▶ extract token from auth slot + look up SHA-256(token) in KV + ├─ not found / disabled ──▶ 401 + ├─ provider not in scope ──▶ 403 + └─ ok: + strip ALL auth headers + set ONE real key ──────────────────▶ api..com + (token never sent on) +``` + +## The decisions that keep it safe + +- **Token rides the SDK's auth slot.** No custom header, no path change. The client only swaps base URL and key. The proxy reads the token from whichever slot routing matched (see [provider-routing-by-auth-header.md](provider-routing-by-auth-header.md)). + +- **Strip-all-then-set-one.** Before forwarding, delete *every* inbound auth header (`authorization`, `x-api-key`, `x-goog-api-key`) and set exactly one with the real key (`src/proxy.ts` `swapAuth`). This guarantees the proxy token is never forwarded upstream, even if a client sends it in an unexpected slot. A test asserts the token never appears in any outbound auth header. + +- **Hashed at rest.** Tokens are stored as `SHA-256(token)` and shown to the admin exactly once at creation. The KV value never contains the plaintext. + +- **Revoke-safe `lastUsed`.** Usage timestamps live in a separate `:lu` key, not in the token record. Stamping "last used" on a hot path can therefore never recreate or re-enable a record that was deleted or disabled - a revoked token stays revoked. + +## Scope (v1) + +Per-token: label, provider scope, enable/disable, revoke, last-used. Rate limits, spend caps, expiry, and per-token analytics are deliberately deferred - the data model leaves room without carrying the weight now. diff --git a/docs/learnings/rate-limit-binding-free-and-loose.md b/docs/learnings/rate-limit-binding-free-and-loose.md new file mode 100644 index 0000000..b6f06fa --- /dev/null +++ b/docs/learnings/rate-limit-binding-free-and-loose.md @@ -0,0 +1,20 @@ +# Rate Limiting binding: free on Workers Free, but a loose per-colo ceiling + +## What we needed + +A per-token request-rate cap that costs nothing, needs no new storage, and never lets the real key leave Cloudflare. + +## What we found + +- The Workers **Rate Limiting binding** (`[[ratelimits]]` in `wrangler.toml` + `env.RATE_LIMITER.limit({ key })`) is an **in-process call, not a subrequest** - no subrequest budget hit, no storage, microseconds of CPU. +- **Free-plan eligibility is undocumented** (a research pass even fabricated a "no additional charge" quote). Verified empirically: `wrangler deploy` on the Free account accepts the binding (the summary lists `env.RATE_LIMITER (N requests/60s) — Rate Limit`) and `limit()` enforces — treat undocumented platform claims as "verify by deploying," not fact. +- It is **per-colo and eventually consistent**. With `limit = 2 / 60s`, ~13 rapid requests slipped through before denials began, and a client spread across two colos can get up to ~2× the limit. Cloudflare describes it as a "loose filter, not suited for strict abuse prevention." +- `period` must be exactly **10 or 60**. The limit is fixed per namespace at deploy time - no per-token-variable limit without tiered namespaces or a Durable Object counter. + +## Decisions we keep + +- One shared per-token ceiling (KISS), keyed on the token's **SHA-256 hash** (never the plaintext). +- **Fail-open:** wrap `limit()` in try/catch and allow on any error - a missing or flaky limiter must never brick the proxy. The real abuse defense is revoke + scope, not this loose ceiling. +- Return `429` + a static `Retry-After: 60` (the binding returns no reset time; the static value matches `period`). + +Related: [[proxy-token-security]]. diff --git a/docs/learnings/token-expiry-check-at-validate.md b/docs/learnings/token-expiry-check-at-validate.md new file mode 100644 index 0000000..7b80640 --- /dev/null +++ b/docs/learnings/token-expiry-check-at-validate.md @@ -0,0 +1,25 @@ +# Token expiry: check-at-validate, not KV TTL + +## Problem + +Optional per-token expiry, enforced cheaply, without a second storage backend. + +## What we found + +- **KV `expirationTtl` is the wrong tool:** 60s floor, it *deletes* the record on expiry (so the dashboard can't show an "expired" row), and it orphans the separate `:lu` last-used key. +- `expiresAt` is set once and never mutates, so a check at read time has no consistency window - it is exact and adds zero extra reads (the field rides in the JSON already fetched to validate). + +## Decision we keep + +Store optional `expiresAt` (UTC ISO); enforce in `getValidatedByHash`: + +```ts +if (meta.expiresAt) { + const t = Date.parse(meta.expiresAt); + if (Number.isNaN(t) || t <= Date.now()) return null; // fail-closed +} +``` + +**Fail-closed on malformed input:** `NaN <= Date.now()` is `false`, which fails *open* (a garbage `expiresAt` stays valid) - so `Number.isNaN(t)` is checked explicitly. The admin form converts its local `datetime-local` value to UTC ISO and rejects unparseable input at creation. Instant cutoff for a leak is still provider-key rotation, not expiry (KV revoke lags ~60s). + +Related: [[proxy-token-security]]. diff --git a/docs/learnings/websocket-proxy-auth-slots.md b/docs/learnings/websocket-proxy-auth-slots.md new file mode 100644 index 0000000..c4488a2 --- /dev/null +++ b/docs/learnings/websocket-proxy-auth-slots.md @@ -0,0 +1,41 @@ +# WebSocket proxying, and the wider auth-slot set + +## Problem + +The proxy only forwarded HTTP. A user's realtime client hit `wss://api.openai.com/v1/responses` directly with a raw key (a dead one → `invalid_api_key`), bypassing the proxy entirely because the proxy couldn't carry a WebSocket. We wanted the same token-swap for wss, for any provider that has a wss API, not just OpenAI. + +## What we found + +**The endpoint in the bug is real.** `wss://api.openai.com/v1/responses` is OpenAI's *Responses API WebSocket Mode* — distinct from the Realtime API (`/v1/realtime`). It authenticates **only** via the `Authorization: Bearer` header. So a proxy that handled only the Realtime subprotocol would pass the token straight through on `/v1/responses`. Both endpoints share `api.openai.com`, routed on path. + +**A plain Worker is enough — no new Durable Object.** Cloudflare Workers open an upstream socket with `fetch(url, { headers: { Upgrade: "websocket" } })` and read `resp.webSocket`; the scheme stays `http(s):`, the header drives the upgrade (a `ws://`/`wss://` URL is *not* how you do it in a Worker). Inbound, `new WebSocketPair()` + `return new Response(null, { status: 101, webSocket: client })`. Works on the Free plan; idle sockets don't burn CPU time. (A first research pass wrongly concluded a DO was required — it had reached for the Node `new WebSocket()` constructor, which doesn't exist in Workers.) + +**Manual pipe over transparent pass-through.** You *can* return the upstream's 101 response directly and let CF pipe both ends, but whether it faithfully echoes the negotiated `Sec-WebSocket-Protocol` back to the client is undocumented (and a browser handshake fails if the server doesn't pick one of the client's offered subprotocols). So we accept both ends in JS (`WebSocketPair` + frame pump) and set the echoed subprotocol explicitly. wrangler ≥ those old dev-WS echo bugs (workers-sdk #1767, fixed by PR #1930) — we're on 4.x, fine. + +**The WS auth-slot set is wider than HTTP**, because a browser `WebSocket` cannot set request headers: + +| Inbound slot | Provider | Why | +|---|---|---| +| `Authorization: Bearer ` | openai | server-side Realtime + all Responses-mode | +| `Sec-WebSocket-Protocol: realtime, openai-insecure-api-key.` | openai | browser Realtime — the only slot a browser can use | +| `?key=` query | gemini | Gemini Live (`…BidiGenerateContent`) reads the key in the query | + +Anthropic has no wss API (Messages is SSE-over-HTTP only) — naturally excluded. (Other providers surveyed — Deepgram, AssemblyAI, ElevenLabs, Azure — add yet more slots: `Authorization: Token`, `?token=`, `xi-api-key`, `Sec-WebSocket-Protocol: token, `. We don't proxy those, but the pattern holds: a wss proxy must read credentials from query params and subprotocols, not just headers.) + +## The decision we keep + +`src/ws.ts` mirrors the HTTP path (validate hash → scope → rate-limit) then `prepareWsUpstream` strips every inbound auth slot and sets exactly one upstream — the WS analogue of `swapAuth`: + +- OpenAI: real key as `Authorization: Bearer` (even when the client smuggled it in the subprotocol — the worker *can* set headers; we drop the `openai-insecure-api-key.*` entry and keep `realtime` + org/project/beta so negotiation still picks `realtime`). +- Gemini: real key in `?key=` (not a header — that's where Live reads it). + +The proxy token therefore never reaches the upstream in any slot (header, query, or subprotocol); a test asserts it. The OpenAI geo-403 fallback is reused as-is: a 403 from a bad colo re-issues the upgrade through the `UsEgress` DO, which carries a WebSocket exactly like a plain `fetch`. + +## Caveats + +- **Rate limit and token validation are per-connection, not per-frame.** One upgrade = one limiter hit; a revoke takes effect on the next connection, not on an open stream. For an immediate cutoff, rotate the provider secret. +- **Idle timeout.** Cloudflare closes a socket after a quiet period in both directions; a silent client should keep-alive (realtime audio/text traffic keeps it warm on its own). +- **The geo-blocked WS-over-DO hop is not locally testable.** The trigger logic is unit-tested with a faked DO; the live DO-carries-a-WebSocket hop reuses HTTP's (proven-in-prod) egress path but isn't separately exercised. Treat it as built-but-unproven for wss until a geo-blocked colo hits it live. +- **Live realtime is untested end-to-end.** Tests prove the upgrade, swap, and bidirectional frames against a mock `ws` upstream; no test connects to a real OpenAI/Gemini realtime endpoint. + +Sources: [CF Workers WebSockets](https://developers.cloudflare.com/workers/runtime-apis/websockets/), [Using the WebSockets API](https://developers.cloudflare.com/workers/examples/websockets/), [OpenAI Realtime (WebSocket)](https://developers.openai.com/api/docs/guides/realtime-websocket), [OpenAI Responses WebSocket Mode](https://developers.openai.com/api/docs/guides/websocket-mode), [Gemini Live API](https://ai.google.dev/gemini-api/docs/live-api/get-started-websocket). diff --git a/docs/superpowers/specs/2026-06-22-api-proxy-doppelganger-tokens-design.md b/docs/superpowers/specs/2026-06-22-api-proxy-doppelganger-tokens-design.md deleted file mode 100644 index 69a4b46..0000000 --- a/docs/superpowers/specs/2026-06-22-api-proxy-doppelganger-tokens-design.md +++ /dev/null @@ -1,221 +0,0 @@ -# api-proxy — Doppelganger Tokens Design - -- **Date:** 2026-06-22 -- **Status:** Approved; implementation in progress -- **Scope:** Replace the three transparent reverse-proxy workers with one token-gated worker plus an embedded admin dashboard. Issue shareable, revocable "doppelganger" API-key tokens that map to real provider keys server-side. - -> Naming: there is no "v1/v2" product split. This is the `api-proxy` project evolving. The new token-gated worker deploys under its own worker name so it can run alongside the existing transparent proxies during validation; the old `src/{claude,openai,gemini}.ts` files and their tomls are deleted once the new worker is reliable. - ---- - -## 1. Problem - -The current proxy is three minimal workers (`src/openai.ts`, `src/claude.ts`, `src/gemini.ts`). Each host-rewrites the request to one upstream and injects a single shared real key. The worker URLs are **unauthenticated** — anyone with a URL uses the owner's real key, with no per-user access control and no revocation. - -We want: hand someone a token they plug into their normal SDK (changing only the base URL + the key), have it work, and be able to revoke or scope that token at any time from a dashboard — all without exposing the real provider keys. - -## 2. Goals - -- A consumer uses their existing SDK by changing **two things**: the base URL (point at the worker) and the API key (use a doppelganger token). -- The owner mints, scopes (per provider), disables, and deletes tokens from an admin dashboard. -- Real provider keys never leave the worker and never live in KV. -- Support OpenAI, Anthropic, and Google Gemini, including streaming, for server-side SDK usage. - -## 3. Non-goals (deferred — see §11) - -Rate limits, spend/token caps, expiry dates, per-token usage analytics, browser/CORS support, Gemini file uploads, multiple real keys per provider, instant (sub-minute) revocation. - -## 4. Locked decisions - -| Decision | Choice | Why | -|---|---|---| -| Mechanism | Doppelganger token = the API key the SDK already sends. Worker reads it from the auth header, validates against KV, swaps in the real key. | Unifies "shareable key" and "common-ground SDK config"; client changes only base URL + key. | -| Topology | One worker, one base URL, **no provider path prefix**. Provider routing by which auth header the token arrives in (+ path for Gemini OpenAI-compat). | A `/openai` `/anthropic` `/gemini` prefix breaks Gemini native file uploads. The auth header already identifies the provider. | -| Architecture | **Single worker, embedded.** Top-level dispatch: `/admin/*` → Hono admin sub-app (wrapped in try/catch); everything else → framework-free proxy hot-path. | Proxy requests pay zero routing/SSR cost. Avoids two deploys / two secret sets / broken `schedule.sh`. Escape hatch: move `src/admin/*` to a second worker on the same KV namespace if it ever outgrows CRUD. | -| Token storage | Store **SHA-256(token)** in KV; show plaintext once at creation; dashboard shows label + last-4. | Foundational, hard to retrofit. A KV/dashboard dump yields unusable hashes. Standard practice. | -| Real keys | One real key per provider, env **secret**, shared by all tokens for that provider. | Token is an access/revocation handle, not a routing key to different accounts. | -| Admin stack | **Hono + JSX fragments + HTMX 2.x** (HTMX loaded from CDN, browser-side only — zero bytes in the Worker bundle). Pin HTMX 2.x (4.0 is alpha; do not adopt). | Concise, embeds as a one-line sub-app, ~14KB. Heavy frameworks (SvelteKit/Astro/React Router) own the entrypoint, force a 2nd worker, and add 50-500KB+. | -| Test runner | **Vitest `^4.1.0`** + `@cloudflare/vitest-pool-workers` (0.16.x). nub has no built-in runner; `nub run test` invokes vitest. | Cloudflare-supported path; runs inside workerd. | -| Package manager | **nub** (`nubjs.com`); lockfile `lock.yaml` (pnpm v9 format). | Project standard. | - -## 5. Architecture & module layout - -One worker, dispatched by path: - -``` -fetch(req, env, ctx): - if pathname startsWith "/admin": try { adminApp.fetch(req, env, ctx) } catch { 500 } - else: proxyHandler(req, env, ctx) -``` - -- **State:** one KV namespace, `TOKENS`. Key = `SHA-256(token)` (hex). Value = `TokenMetadata` (§7). -- **Secrets** (only these four): `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `ADMIN_SECRET`. Never in KV, never returned to callers. -- **Plain vars (NOT secrets):** `OPENAI_UPSTREAM`, `ANTHROPIC_UPSTREAM`, `GEMINI_UPSTREAM` — default to the real hosts; overridable for tests (§6, §Testing). - -``` -src/ - index.ts # fetch entry + dispatch - proxy.ts # ZERO framework deps: extractToken, routeProvider, swapAuth, stream passthrough. MUST NOT import Hono. - tokens.ts # KV helpers: sha256hex, generateToken (dgk_ + 32 base64url), create, list, getValidated, update, delete, touchLastUsed - upstreams.ts # UPSTREAM resolver: reads *_UPSTREAM env with real-host defaults; parses protocol+hostname+port (the test seam) - types.ts # shared TokenMetadata, Provider types (imported by proxy + admin) - admin/ - index.ts # new Hono(); mounts routes; HMAC cookie auth middleware - routes.tsx # GET/POST/PUT/DELETE /admin/api/tokens (HTML fragments); GET /admin; /admin/login, /admin/logout - layout.tsx # shell; loads HTMX 2.x from CDN; minimal inline CSS (no Tailwind JIT) - components.tsx # , , , -test/ - proxy.test.ts # TIER 1 (vitest-pool-workers) - sdk-compat/ - setup.ts # TIER 2 harness: mock upstream (node:http) + unstable_startWorker + capture helpers - openai.test.ts - anthropic.test.ts - gemini.test.ts -vitest.config.ts # cloudflare pool (tier 1) -vitest.compat.config.ts # node pool, --pool=forks (tier 2) -wrangler.toml # ONE config: name='api-proxy', [[kv_namespaces]] binding='TOKENS' -schedule.sh # kept -``` - -Deleted once the new worker is verified reliable: `src/openai.ts`, `src/claude.ts`, `src/gemini.ts`, `wrangler.openai.toml`, `wrangler.claude.toml`, `wrangler.gemini.toml`. - -## 6. Request flow (proxy path) - -``` -1. extractToken(req, url) - x-api-key || x-goog-api-key || Authorization: "Bearer X" -> X || ?key= - none -> 401 "missing token" -2. provider = routeProvider(req, url) // §6.1 -3. rec = getValidated(KV, sha256hex(token)) - miss || status != "active" -> 401 "invalid or revoked token" -4. coarse(provider) not in rec.providers -> 403 "token not allowed for provider" -5. swapAuth: delete x-api-key, x-goog-api-key, authorization; set the ONE real key -6. resolve upstream: u = parse(UPSTREAM[provider]); url.protocol=u.protocol; url.hostname=u.hostname; url.port=u.port - if provider startsWith "gemini": url.searchParams.delete("key") -7. fetch(new Request(url, { method, headers, body })) // path + query verbatim -8. return new Response(upstream.body, upstream) // stream straight through, no buffering -9. ctx.waitUntil(touchLastUsed(KV, hash)) // fire-and-forget -``` - -### 6.1 Provider routing table - -| Token arrives in | + path signal | Provider | Upstream (default, overridable) | Real key set as | -|---|---|---|---|---| -| `x-api-key` | — | `anthropic` | `ANTHROPIC_UPSTREAM` = `api.anthropic.com` | `x-api-key` | -| `x-goog-api-key` or `?key=` | — | `gemini` | `GEMINI_UPSTREAM` = `generativelanguage.googleapis.com` | `x-goog-api-key` | -| `Authorization: Bearer` | path starts `/v1beta/openai/` | `gemini-openai` | `GEMINI_UPSTREAM` | `Authorization: Bearer` | -| `Authorization: Bearer` | else | `openai` | `OPENAI_UPSTREAM` = `api.openai.com` | `Authorization: Bearer` | - -`routeProvider` returns one of `openai | anthropic | gemini | gemini-openai`. The provider-scope check (step 4) and the token's `providers` array use the coarse set `openai | anthropic | gemini`, so `gemini-openai` maps to the `gemini` scope. The `gemini` vs `gemini-openai` distinction only selects the swap branch in §6.2. - -### 6.2 Auth swap (security linchpin) - -Always **strip all three** inbound auth headers, then set exactly one: - -```ts -headers.delete("x-api-key"); -headers.delete("x-goog-api-key"); -headers.delete("authorization"); -switch (provider) { - case "openai": headers.set("authorization", `Bearer ${realKey}`); break; - case "anthropic": headers.set("x-api-key", realKey); break; - case "gemini": headers.set("x-goog-api-key", realKey); break; - case "gemini-openai": headers.set("authorization", `Bearer ${realKey}`); break; -} -``` - -Stripping-all-then-setting-one prevents the doppelganger token leaking upstream and closes the Anthropic dual-header leak and the duplicate-`x-goog-api-key` 401. - -## 7. Token model & lifecycle - -KV: `SHA-256(token)` (hex) → - -```ts -type TokenMetadata = { - label: string; - last4: string; - providers: ("openai" | "anthropic" | "gemini")[]; - status: "active" | "disabled"; - createdAt: string; // ISO - lastUsed?: string; // ISO - // reserved for Later (absent in v1): expiresAt, limits, spend -}; -``` - -- **Create:** admin supplies label + provider scopes, types a token or clicks generate (`dgk_` + 32 random base64url from `crypto.getRandomValues`). Worker stores `sha256hex(token) -> metadata`, returns the **plaintext once**. Never retrievable again. -- **List/Update/Delete:** by hash. Update edits label, providers, status (`active` ⇄ `disabled`). -- **last-used:** fire-and-forget KV write on each successful proxied request. - -> Revocation latency: KV is eventually consistent (~up to 60s). Acceptable for v1; instant revocation via Durable Object is a Later item. - -## 8. Admin dashboard - -Embedded **Hono** sub-app, server-rendered **JSX fragments + HTMX 2.x** (HTMX from CDN; no client JS we write; no UI/charting libs in the bundle). - -- **Auth:** single `ADMIN_SECRET` password → `POST /admin/login` sets an HMAC-SHA256-signed cookie `cm_admin=.`, `HttpOnly; SameSite=Strict; Max-Age=86400`. Middleware guards all `/admin/*` except login. -- **Routes:** HTMX-driven CRUD — `hx-get/post/put/delete` on `/admin/api/tokens`, `hx-swap="outerHTML"` on rows for create/edit/delete/enable-disable. `GET /admin` dashboard, `GET /admin/logout`. -- **UI:** add-token card (token field + generate, label, provider checkboxes), token table (label, last-4, provider pills, created, last-used, edit/delete). Plaintext token shown once after creation. -- **Blast-radius:** the dispatcher wraps `adminApp.fetch` in try/catch returning a plain 500, so an admin bug can never crash the proxy branch. - -## 9. Real key handling - -`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` are Cloudflare secrets resolved at request time and injected only into the outbound request. Never logged, never in KV, never in a response body. - -## 10. Gotchas handled in v1 - -1. **SSE streaming** — pass `upstream.body` straight through; never `await response.text()`. Preserve upstream `cache-control: no-transform`; no response-buffering features; ensure the zone isn't re-compressing `text/event-stream`. -2. **Verbatim path + query forwarding** — only protocol/host/port are rewritten; path and query (incl. Gemini `?alt=sse`) stay intact. -3. **Strip-all-then-set-one auth** (§6.2). -4. **Gemini `?key=` hygiene** — keep `url.searchParams.delete("key")` for raw-REST/curl callers. -5. **Hostname** — do not issue worker hostnames ending in `.openai.azure.com`, `.services.ai.azure.com`, `.cognitiveservices.azure.com`. - -## 11. Deferred (Later) - -KV value shape + module boundaries leave room; none built now: rate limits, spend/token caps (parse `usage`; OpenAI needs `stream_options.include_usage`), expiry, per-token analytics, browser/CORS preflight, Gemini file uploads (forward `x-goog-upload-url`), multiple real keys per provider, instant revocation + atomic counters via Durable Object. - -## 12. Testing (two-tier harness) - -Do not test proxy logic and real-SDK HTTP behavior with one tool — conflating them is the main flakiness source. - -**Tier 1 — proxy logic (always-on CI gate, ~1s):** `@cloudflare/vitest-pool-workers` inside workerd (`vitest.config.ts`). -- Seed KV directly: `env.TOKENS.put(sha256hex(token), JSON.stringify(meta))`. -- Capture the outbound call with `vi.spyOn(globalThis, "fetch")`; assert: (a) right upstream host, (b) real key swapped in AND doppelganger token absent (`.not.toContain(token)` on all three header slots), (c) path+query verbatim, (d) 401 on missing/invalid/revoked, 403 on provider-scope mismatch. -- SSE: mocked fetch returns a `ReadableStream` `text/event-stream`; drive via `createExecutionContext()`/`waitOnExecutionContext()`; read `response.body.getReader()` chunk-by-chunk; assert content-type preserved and chunks un-buffered. Tier 1 does not use the upstream env seam (it mocks fetch entirely). - -**Tier 2 — real-SDK compatibility (feature-branch + pre-deploy, ~10-20s):** `vitest.compat.config.ts`, Node pool, `--pool=forks`, serial. -- `wrangler unstable_startWorker` starts a real HTTP listener (not the deprecated `unstable_dev`). -- A `node:http` mock upstream captures the raw inbound request; point the worker's `*_UPSTREAM` env at it (the seam earns its keep). -- Seed the token via the worker's own `POST /admin/api/tokens` (also exercises create). -- Run the real `openai`, `@anthropic-ai/sdk`, `@google/genai` packages with `baseURL` = local worker and `apiKey` = doppelganger token. Assert on the captured request: real key present, doppelganger absent, exact path the SDK constructed (catches `:generateContent`, `/v1beta/openai`). -- SSE: mock writes `text/event-stream` chunks; consume via the SDK's own stream iterator; assert on connection-start headers, not the buffered body (avoids mid-stream-disconnect races). - -**Flakiness guards:** Tier 2 serial (shared mutable capture state + port); never `await body.text()/json()` in the proxy path; use the SDK's stream iterator for completion, not `setTimeout`. - -## 13. Dev commands (nub) & rollout - -``` -nub install # install all deps (CI: nub ci) -nub add hono # runtime dep -nub add -E -D vitest@^4.1.0 @cloudflare/vitest-pool-workers @cloudflare/workers-types -nub add -E -D openai @anthropic-ai/sdk @google/genai # tier-2 SDKs -nub run dev # "dev": wrangler dev (or: nubx wrangler dev) -nub run test:unit # vitest run --config vitest.config.ts -nub run test:compat # vitest run --config vitest.compat.config.ts --pool=forks -nub run test # test:unit && test:compat -``` - -Rollout: (1) build under `name = "api-proxy"` (distinct from the old `*-proxy` workers); (2) create KV namespace, set the four secrets, mint a test token, verify each SDK end-to-end incl. streaming; (3) once reliable, delete the three old `src/*.ts` + tomls. **README is stale** (shows `bun`/`bunx`) — update to nub + `lock.yaml`. - -## 14. Bloat-watch (single-worker discipline) - -The single-worker choice is only safe with discipline: -- `proxy.ts` MUST NOT import Hono or admin code — the hot-path stays framework-free pure functions. -- No npm UI/component libraries, charting libs, or Tailwind JIT output in the admin — server-rendered HTML + HTMX attributes + minimal inline CSS only (these bundle into the same worker and inflate cold-start). -- Keep the upstream seam to exactly three vars with real-host defaults; do not generalize into a routing/rewrite config. -- Pin HTMX 2.x; pin vitest `^4.1.0` with current `@cloudflare/vitest-pool-workers` 0.16.x. - -## 15. Open items to verify during implementation - -- Exact base-URL strings per SDK (esp. Vercel AI SDK Anthropic, whose provider default includes `/v1`) — confirmed at test time; the worker forwards verbatim so it's robust either way. -- Gemini OpenAI-compat coverage (`/v1beta/openai/chat/completions`) before documenting it as the recommended Python-Gemini path. -- KV propagation delay — measure to decide whether a short in-worker cache + bust-on-revoke is worth adding before the Durable-Object Later item. diff --git a/lefthook.yml b/lefthook.yml index 27a28f6..fa4918b 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,5 +4,5 @@ pre-commit: commands: check: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,css}" - run: nubx biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} + run: nubx biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} stage_fixed: true diff --git a/lock.yaml b/lock.yaml index 44d06a9..49705f6 100644 --- a/lock.yaml +++ b/lock.yaml @@ -9,44 +9,126 @@ importers: .: dependencies: hono: - specifier: ^4.12.25 - version: 4.12.25 + specifier: ^4.12.27 + version: 4.12.27 typescript: specifier: ^6.0.3 version: 6.0.3 wrangler: - specifier: ^4.100.0 - version: 4.100.0(@cloudflare/workers-types@4.20260615.1) + specifier: ^4.105.0 + version: 4.105.0(@cloudflare/workers-types@4.20260627.1) devDependencies: + '@ai-sdk/anthropic': + specifier: ^4.0.0 + version: 4.0.0(zod@4.4.3) + '@ai-sdk/google': + specifier: ^4.0.1 + version: 4.0.1(zod@4.4.3) + '@ai-sdk/openai': + specifier: ^4.0.1 + version: 4.0.1(zod@4.4.3) '@anthropic-ai/sdk': - specifier: ^0.104.1 - version: 0.104.1(zod@3.25.76) + specifier: ^0.106.0 + version: 0.106.0(zod@4.4.3) '@biomejs/biome': - specifier: ^2.5.0 - version: 2.5.0 + specifier: ^2.5.1 + version: 2.5.1 '@cloudflare/vitest-pool-workers': - specifier: ^0.16.18 - version: 0.16.18(@vitest/runner@4.1.9)(@vitest/snapshot@4.1.9)(vitest@4.1.9(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3))) + specifier: ^0.16.20 + version: 0.16.20(@cloudflare/workers-types@4.20260627.1)(@vitest/runner@4.1.9)(@vitest/snapshot@4.1.9)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3))) '@cloudflare/workers-types': - specifier: ^4.20260615.1 - version: 4.20260615.1 + specifier: ^4.20260627.1 + version: 4.20260627.1 + '@genkit-ai/google-genai': + specifier: ^1.39.0 + version: 1.39.0(genkit@1.39.0) '@google/genai': - specifier: ^2.8.0 - version: 2.8.0 + specifier: ^2.10.0 + version: 2.10.0 + '@langchain/anthropic': + specifier: ^1.5.1 + version: 1.5.1(@langchain/core@1.2.1)(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/core': + specifier: ^1.2.1 + version: 1.2.1(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/google-genai': + specifier: ^2.2.0 + version: 2.2.0(@langchain/core@1.2.1)(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + '@langchain/openai': + specifier: ^1.5.3 + version: 1.5.3(@langchain/core@1.2.1)(@opentelemetry/api@1.9.1)(ws@8.21.0) + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + ai: + specifier: ^7.0.3 + version: 7.0.3(zod@4.4.3) + genkit: + specifier: ^1.39.0 + version: 1.39.0 lefthook: specifier: ^2.1.9 version: 2.1.9 openai: - specifier: ^6.42.0 - version: 6.42.0(ws@8.21.0)(zod@3.25.76) + specifier: ^6.45.0 + version: 6.45.0(ws@8.21.0)(zod@4.4.3) vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@25.9.3)(esbuild@0.28.1)(vite@8.0.16(@types/node@25.9.3)) + version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(esbuild@0.28.1)(vite@8.0.16(@types/node@25.9.3)) + ws: + specifier: ^8.21.0 + version: 8.21.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 packages: - '@anthropic-ai/sdk@0.104.1': - resolution: {integrity: sha512-gGACa/+IaiXzRRmF96aOhamoBgapKRBiFWbmmTFP8aMkpaEcuStF+Q61bjo4vPxBM7gqWJNZqsngslRdnLHv0Q==} + '@ai-sdk/anthropic@4.0.0': + resolution: {integrity: sha512-N0lT1g6/5DEIZvalpkpwYRCdu7n5qb8qPN3PcTem6k4VkPBLC2+T2LAAyx1GS0eNOxavVa0CP7n2kCiye0yyfw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@4.0.3': + resolution: {integrity: sha512-V8p0skqA9I5ZvZ4JDrvdh+TrZ0OVvkoP4CPEMxy777yBcVO5febhtLUJ+t0V0ROvSIkWIm5R9nvyOsyZRtO9sQ==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@4.0.1': + resolution: {integrity: sha512-FZAliYDNgpe96H8fh8ccW2PrPYMf7fRoT/oySfRVdkqQUAO+DI5jIouYbF1D9xl1+qGb6s0zaRZKD8Ig+zDO4g==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@4.0.1': + resolution: {integrity: sha512-SZ036CbBdQcf8Q8EoGKU7kYBJpX+KPChGXUO8IPXDKMYvo/CDjBpPLWOm4DMa++s9skxuxTZtmNLqTeaY2GjTg==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@5.0.0': + resolution: {integrity: sha512-zj66M02jc6ASYwIgWZowsooDUwaVngeNZQ3H10GwcPMZ+KR6gHMhcUuKl6tkai+JPXTKDyHY1pnszuxRtw2D4A==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@4.0.0': + resolution: {integrity: sha512-fr9Gs89prDWiuox/T+kCA+i2cJkHpxU5S+tr4megjTzRC27ZsvFhwjU/+XrqqMbvBUlfmXxTOYWy8ng45dsjIg==} + engines: {node: '>=22'} + + '@anthropic-ai/sdk@0.103.0': + resolution: {integrity: sha512-1uG7RNgoHTUxzOXqSCODKt0UTVlxWiHk/2Tt2/uQJiPW7XzBeKVuJyd3Aw6T3LPyvZV/jDTnPLX7SaM70WLLjA==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@anthropic-ai/sdk@0.106.0': + resolution: {integrity: sha512-ufwVvYNDBj2dzOGupBCTaNzBLxqcTnGOzI4z8Wouxlt+mT3J3HuOmatgCy1VmwCHOUueqZ41ERhm0O99OUcbWA==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -58,63 +140,62 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.0': - resolution: {integrity: sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==} + '@biomejs/biome@2.5.1': + resolution: {integrity: sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.0': - resolution: {integrity: sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==} + '@biomejs/cli-darwin-arm64@2.5.1': + resolution: {integrity: sha512-npqDzvqv7vFaWRiNN1Te71siRgPaqS9MpqgYCdP/CrUbkJ7ApezaeaKjueKHRN/JH/6lRjJQAHi8acQDCAz22w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.0': - resolution: {integrity: sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==} + '@biomejs/cli-darwin-x64@2.5.1': + resolution: {integrity: sha512-RgwTqPAM8g2tn1j+b5oRjF/DbSBX8a4gwojtuG9XuhfK7GgomvZ9+T+tqjXiVbjLEeGJOoL6VEk8mvRTVeSybw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.0': - resolution: {integrity: sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==} + '@biomejs/cli-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-WMcvMLgByyTqVxGlq918NBBYliq9FRR9GAQVETHb+VjGVqXCZFfHlZHC1FX4ibuYY/Hg6TJE3rHU0xVrdJXNRw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] - '@biomejs/cli-linux-arm64@2.5.0': - resolution: {integrity: sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==} + '@biomejs/cli-linux-arm64@2.5.1': + resolution: {integrity: sha512-yhV35CzZh38VyMvTEXi3JTjxZBs++oCKK9KG8vB6VI5+uvQvZNR3BFWEKKzuOmx9DJJj7sQpZ4LQJcmbGTs3+Q==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.0': - resolution: {integrity: sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==} + '@biomejs/cli-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-ANTowtlLmPYm5yeMckWY8Xzb9Ix+JJP3tgHR/n6xRj1VWyIzzWtfRfih9hv9VmClwadpBvZduISZIbBsIlYG3A==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] - '@biomejs/cli-linux-x64@2.5.0': - resolution: {integrity: sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==} + '@biomejs/cli-linux-x64@2.5.1': + resolution: {integrity: sha512-J/7uHSX7NfoYDI7HijAkd8lnQIOrRb2W7j3X+tw4R+N5ExvXGsyXFiGdQcfcxfOmNQmZVSQOCDk757fwpzqQcg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.0': - resolution: {integrity: sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==} + '@biomejs/cli-win32-arm64@2.5.1': + resolution: {integrity: sha512-zgXnKNgWPC4iPF7Y1lR3STUeCUuZRpD6IiOrC7TZTlh0Lx6FiVUT05myuMQHQ9D+1cc7uyMldi4forE6lp0ivQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.0': - resolution: {integrity: sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==} + '@biomejs/cli-win32-x64@2.5.1': + resolution: {integrity: sha512-6uxpR9hvaglANkZemeSiN/FhYgkGasrEGn267eXIWvjrjJ2LhDlk251IhjVJq6MXzkV2/bcXwLwSroLyPtqRZg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -128,80 +209,57 @@ packages: workerd: optional: true - '@cloudflare/vitest-pool-workers@0.16.18': - resolution: {integrity: sha512-TEktXyevK9lkTWouElbIcDPK3YEfV+Szqgnlq5sNk+KYZR3LiDdYDaGNmUYgiT2LiiFeGU2yzCrcgmN8mJhqWQ==} + '@cloudflare/vitest-pool-workers@0.16.20': + resolution: {integrity: sha512-buw0YgsAMT7s60wcmyxbtciEJjMJzKcWzayDMPhWaqMqfQzW+0WPLV67Lobn4C80nkNQhYocEJPnrEhLWnOf+A==} peerDependencies: '@vitest/runner': ^4.1.0 '@vitest/snapshot': ^4.1.0 vitest: ^4.1.0 - '@cloudflare/workerd-darwin-64@1.20260611.1': - resolution: {integrity: sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-64@1.20260617.1': - resolution: {integrity: sha512-jWwmgEVVWbsHNrLSNXzwjJaH90VzRxq1cWkQFUidxyeUPnMxemeNE8I9qFAfrpzGgE11e9sKDcE3ettJW08swQ==} + '@cloudflare/workerd-darwin-64@1.20260625.1': + resolution: {integrity: sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260611.1': - resolution: {integrity: sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20260617.1': - resolution: {integrity: sha512-LHH7b565g9znfCUOkwbec6FG2rmRbsgCy6aJiU9KN662mNheWl5sw/iKleiFSiljPKQQP3HkjnC/NSkdgi/aSA==} + '@cloudflare/workerd-darwin-arm64@1.20260625.1': + resolution: {integrity: sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20260611.1': - resolution: {integrity: sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-64@1.20260617.1': - resolution: {integrity: sha512-FMnaAKXe4Cfd8TQurCVd9fs2XQVBFRCsP+Id/SRdUv89MlwYu9zXfoyx6BxM+brPTIUK38SHbo8iaxiwzLi9JQ==} + '@cloudflare/workerd-linux-64@1.20260625.1': + resolution: {integrity: sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260611.1': - resolution: {integrity: sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20260617.1': - resolution: {integrity: sha512-MRoifFYcqbxxIIQy7PqO5tFY/qPFSnjXzakWl0sO93l+HLyG35jRAgOi6jfqa4kBxc7gKKtH861DcewjxUfkjA==} + '@cloudflare/workerd-linux-arm64@1.20260625.1': + resolution: {integrity: sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20260611.1': - resolution: {integrity: sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw==} + '@cloudflare/workerd-windows-64@1.20260625.1': + resolution: {integrity: sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260617.1': - resolution: {integrity: sha512-rgBV9wQrv0OSKgCTTbhFUFY3sLGNANZ88aqaLvtmEn2gmbFVb1J4PDGochVUdB7NSEp4D/ghHva6/8SZmbONpw==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] + '@cloudflare/workers-types@4.20260627.1': + resolution: {integrity: sha512-QhVvier1eW9Ydw96N/Ez79i5CSGy7h39JucrUejQmAWQw9qBdlB1aOTjaD93EGYrV0t9U81YWetI3LcaxxILNw==} - '@cloudflare/workers-types@4.20260615.1': - resolution: {integrity: sha512-fGOiTwoLj/8bU8mj3VAfa1EULx4ceZhDwnjvY+afDBlSXI9pvY7PE9t62rGEhJjbAOGd7i5WUDun0eZCWBDrzg==} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -214,320 +272,296 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@google/genai@2.8.0': - resolution: {integrity: sha512-pc2ayxqO5+O7AvnHBqpNHIk7PAZkHZgL31tbyx0gJZBSS9qPYiQoqwK7oYOw/ePmG6QY4EMSu+304vD5QlhXAw==} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@firebase/app-check-interop-types@0.3.4': + resolution: {integrity: sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==} + + '@firebase/app-types@0.9.5': + resolution: {integrity: sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==} + + '@firebase/auth-interop-types@0.2.5': + resolution: {integrity: sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==} + + '@firebase/component@0.7.3': + resolution: {integrity: sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==} + engines: {node: '>=20.0.0'} + + '@firebase/database-compat@2.1.4': + resolution: {integrity: sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==} + engines: {node: '>=20.0.0'} + + '@firebase/database-types@1.0.20': + resolution: {integrity: sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==} + + '@firebase/database@1.1.3': + resolution: {integrity: sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==} + engines: {node: '>=20.0.0'} + + '@firebase/logger@0.5.1': + resolution: {integrity: sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==} + engines: {node: '>=20.0.0'} + + '@firebase/util@1.15.1': + resolution: {integrity: sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==} + engines: {node: '>=20.0.0'} + + '@genkit-ai/ai@1.39.0': + resolution: {integrity: sha512-e1ZI2ib4Y8ha2Zj/rN7Uioxd16rWMhalPe3krypLw8B4G36X8KZCqj2UhucKbCZJIffU0cxEvag7BP+JVQnsyQ==} + + '@genkit-ai/core@1.39.0': + resolution: {integrity: sha512-MI0cpmwc4K/KPVsXFAcHviOybt3T3IsVSsN37cEcOzo7mmv+s0JSdooxlQgZn68B6e3I8BNLL3Vk1vyc6qIlCw==} + + '@genkit-ai/firebase@1.37.0': + resolution: {integrity: sha512-yutXazOCGqGeauiShCaPj9fjcS3nn3FUTj7/0nbHXx8mLVhddBEwBIH96MqVGz9pqt3Fl57TCROPguLOmIZaQQ==} + peerDependencies: + '@google-cloud/firestore': ^7.11.0 + firebase: '>=11.5.0' + firebase-admin: '>=12.2' + genkit: ^1.37.0 + peerDependenciesMeta: + firebase: + optional: true + + '@genkit-ai/google-cloud@1.37.0': + resolution: {integrity: sha512-THLcekr0kxzTyZksFVQ/gCIcprlQlenG7aGdGWjAokqWdtX28CU+2pmQF64m++IxOsuuqISIABjpyp0/fSPgjQ==} + peerDependencies: + genkit: ^1.37.0 + + '@genkit-ai/google-genai@1.39.0': + resolution: {integrity: sha512-ZP9nA6TlBJ1ZHCmSkz56v0lrRS/Wfh6L09oHgeOIZyD/Pq76uX3W+Uly8dPAbDtNax63l64TsEYZH1HHt8rC8A==} + peerDependencies: + genkit: ^1.39.0 + + '@google-cloud/common@5.0.2': + resolution: {integrity: sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/firestore@7.11.6': + resolution: {integrity: sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==} + engines: {node: '>=14.0.0'} + + '@google-cloud/firestore@8.6.0': + resolution: {integrity: sha512-TdvZHfwQj5B5CSDEgDqyrhdVqtOSupmBXDQPasMAJiC64tjsGvyMooNiC43fdk1TsUHeklyoZ6/vQ1TjWKVMbg==} + engines: {node: '>=18'} + + '@google-cloud/logging-winston@6.0.2': + resolution: {integrity: sha512-rrNs4XXLtk0BAnL6kTjfAH26a/zVGsuZHd6Vve3Ip1fT4/Irz2QJtqI0XiPbT/yfmKqFmYa44GWPXtPPAi56ug==} + engines: {node: '>=18'} + peerDependencies: + winston: '>=3.2.1' + + '@google-cloud/logging@11.2.3': + resolution: {integrity: sha512-iUIJ+3Bi6aw9whahT4JmS5Ccd1Eej2Ss9RjOSW2+qcVaWcleepJtFUBuyU2CpewtniVibBUV/731MQw9SWxQ3Q==} + engines: {node: '>=14.0.0'} + + '@google-cloud/modelarmor@0.4.1': + resolution: {integrity: sha512-CT9TpQF443aatjhRRvazrYNOvUot26HnFP3hhgmV89QYygNPB6owWvGFFOTsKK4zSvTDfkeeb+E6diVXxn9t4g==} + engines: {node: '>=14.0.0'} + + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.19.0': + resolution: {integrity: sha512-5SOPXwC6RET4ZvXxw5D97dp8fWpqWEunHrzrUUGXhG4UAeedQe1KvYV8CK+fnaAbN2l2ha6QDYspT6z40TVY0g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@opentelemetry/core': ^1.0.0 + '@opentelemetry/resources': ^1.0.0 + '@opentelemetry/sdk-metrics': ^1.0.0 + + '@google-cloud/opentelemetry-cloud-trace-exporter@2.4.1': + resolution: {integrity: sha512-Dq2IyAyA9PCjbjLOn86i2byjkYPC59b5ic8k/L4q5bBWH0Jro8lzMs8C0G5pJfqh2druj8HF+oAIAlSdWQ+Z9Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@opentelemetry/core': ^1.0.0 + '@opentelemetry/resources': ^1.0.0 + '@opentelemetry/sdk-trace-base': ^1.0.0 + + '@google-cloud/opentelemetry-resource-util@2.4.0': + resolution: {integrity: sha512-/7ujlMoKtDtrbQlJihCjQnm31n2s2RTlvJqcSbt2jV3OkCzPAdo3u31Q13HNugqtIRUSk7bUoLx6AzhURkhW4w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/resources': ^1.0.0 + + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/precise-date@4.0.0': + resolution: {integrity: sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.21.0': + resolution: {integrity: sha512-l+IFTkd+6Y5LoAuXyYCKNAKtw/Ci+rAMqgdTB1jv4iZiLhw0rtq+0qjIRbBizXkNzEFmXiXUW0H7sZQQvk1ffA==} + engines: {node: '>=14'} + + '@google/genai@2.10.0': + resolution: {integrity: sha512-e4cFxj3tiuMtsgOT4G9c1hXyGJhg7/Buj7VVeBacRY3fRtkRZZ59Q3nuVp2xbq8BGQXLXCDB253qMhklMOeUDg==} engines: {node: '>=20.0.0'} peerDependencies: '@modelcontextprotocol/sdk': ^1.25.2 @@ -535,6 +569,24 @@ packages: '@modelcontextprotocol/sdk': optional: true + '@google/generative-ai@0.24.1': + resolution: {integrity: sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==} + engines: {node: '>=18.0.0'} + + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -685,6 +737,10 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -695,37 +751,507 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@langchain/anthropic@1.5.1': + resolution: {integrity: sha512-j92zCCd5BFH3rHMRzc2wBmSKDoVpinof1oh8aFiAz9TWbSOc4tGU4n6bqwy/wP0GH1uO96zZHLGCHBMPgrxTNw==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.2.1 + + '@langchain/core@1.2.1': + resolution: {integrity: sha512-NNG/cC5FGuHDOAP56h0ddp8Rfk8p+othWzEK5RV9JIG6RvnF5vGa5r0AEGtKfQieed7s1kC42GuIzVOBvMBL/g==} + engines: {node: '>=20'} + + '@langchain/google-genai@2.2.0': + resolution: {integrity: sha512-1mDqbmB6+iC6ZBQY15r5xJg9wPErnQ774inpKh6qi6BrrjadDwaPHoklJW5IXU94edKiDpm1akIzJCrQDWe6yA==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.2.0 + + '@langchain/openai@1.5.3': + resolution: {integrity: sha512-OStS2AUvy9oe/hEf/3ndBOFztUDOfuJYLNXh89m3iiJAI2Cp5Dp0n/pvpO27MO0b+VgENd+xSHVyQZ7fe+ulxg==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.2.1 + '@napi-rs/wasm-runtime@1.1.5': resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + '@opentelemetry/api-logs@0.52.1': + resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} + engines: {node: '>=14'} - '@poppinss/dumper@0.6.5': - resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@opentelemetry/auto-instrumentations-node@0.49.2': + resolution: {integrity: sha512-xtETEPmAby/3MMmedv8Z/873sdLTWg+Vq98rtm4wbwvAiXBB/ao8qRyzRlvR2MR6puEr+vIB/CXeyJnzNA3cyw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.4.1 - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + '@opentelemetry/context-async-hooks@1.25.1': + resolution: {integrity: sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + '@opentelemetry/core@1.25.1': + resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@protobufjs/eventemitter@1.1.1': - resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + '@opentelemetry/exporter-trace-otlp-grpc@0.52.1': + resolution: {integrity: sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 - '@protobufjs/fetch@1.1.1': + '@opentelemetry/exporter-trace-otlp-http@0.52.1': + resolution: {integrity: sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.52.1': + resolution: {integrity: sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/exporter-zipkin@1.25.1': + resolution: {integrity: sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.41.0': + resolution: {integrity: sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.43.0': + resolution: {integrity: sha512-pSxcWlsE/pCWQRIw92sV2C+LmKXelYkjkA7C5s39iPUi4pZ2lA1nIiw+1R/y2pDEhUHcaKkNyljQr3cx9ZpVlQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.43.1': + resolution: {integrity: sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.40.0': + resolution: {integrity: sha512-aZ4cXaGWwj79ZXSYrgFVsrDlE4mmf2wfvP9bViwRc0j75A6eN6GaHYHqufFGMTCqASQn5pIjjP+Bx+PWTGiofw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.40.0': + resolution: {integrity: sha512-JxbM39JU7HxE9MTKKwi6y5Z3mokjZB2BjwfqYi4B3Y29YO3I42Z7eopG6qq06yWZc+nQli386UDQe0d9xKmw0A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.38.0': + resolution: {integrity: sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.8.0': + resolution: {integrity: sha512-ieTm4RBIlZt2brPwtX5aEZYtYnkyqhAVXJI9RIohiBVMe5DxiwCwt+2Exep/nDVqGPX8zRBZUl4AEw423OxJig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.11.0': + resolution: {integrity: sha512-27urJmwkH4KDaMJtEv1uy2S7Apk4XbN4AgWMdfMJbi7DnOduJmeuA+DpJCwXB72tEWXo89z5T3hUVJIDiSNmNw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.38.0': + resolution: {integrity: sha512-Um07I0TQXDWa+ZbEAKDFUxFH40dLtejtExDOMLNJ1CL8VmOmA71qx93Qi/QG4tGkiI1XWqr7gF/oiMCJ4m8buQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.41.1': + resolution: {integrity: sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.38.0': + resolution: {integrity: sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.14.0': + resolution: {integrity: sha512-pVc8P5AgliC1DphyyBUgsxXlm2XaPH4BpYvt7rAZDMIqUpRk8gs19SioABtKqqxvFzg5jPtgJfJsdxq0Y+maLw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.38.1': + resolution: {integrity: sha512-WvssuKCuavu/hlq661u82UWkc248cyI/sT+c2dEIj6yCk0BUkErY1D+9XOO+PmHdJNE+76i2NdcvQX5rJrOe/w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.42.0': + resolution: {integrity: sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.52.1': + resolution: {integrity: sha512-EdSDiDSAO+XRXk/ZN128qQpBo1I51+Uay/LUPcPQhSRGf7fBPIEUBeOLQiItguGsug5MGOYjql2w/1wCQF3fdQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.40.0': + resolution: {integrity: sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.52.1': + resolution: {integrity: sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.42.0': + resolution: {integrity: sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.2.0': + resolution: {integrity: sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.39.0': + resolution: {integrity: sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.42.0': + resolution: {integrity: sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.39.0': + resolution: {integrity: sha512-eU1Wx1RRTR/2wYXFzH9gcpB8EPmhYlNDIUHzUXjyUE0CAXEJhBLkYNlzdaVCoQDw2neDqS+Woshiia6+emWK9A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.38.0': + resolution: {integrity: sha512-tPmyqQEZNyrvg6G+iItdlguQEcGzfE+bJkpQifmBXmWBnoS5oU3UxqtyYuXGL2zI9qQM5yMBHH4nRXWALzy7WA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.46.0': + resolution: {integrity: sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.41.0': + resolution: {integrity: sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.40.0': + resolution: {integrity: sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.40.0': + resolution: {integrity: sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.39.0': + resolution: {integrity: sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.38.0': + resolution: {integrity: sha512-stjow1PijcmUquSmRD/fSihm/H61DbjPlJuJhWUe7P22LFPjFhsrSeiB5vGj3vn+QGceNAs+kioUTzMGPbNxtg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.43.0': + resolution: {integrity: sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.41.0': + resolution: {integrity: sha512-Kpv0fJRk/8iMzMk5Ue5BsUJfHkBJ2wQoIi/qduU1a1Wjx9GLj6J2G17PHjPK5mnZjPNzkFOXFADZMfgDioliQw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.41.1': + resolution: {integrity: sha512-UqJAbxraBk7s7pQTlFi5ND4sAUs4r/Ai7gsAVZTQDbHl2kSsOp7gpHcpIuN5dpcI2xnuhM2tkH4SmEhbrv2S6Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.41.0': + resolution: {integrity: sha512-RJ1pwI3btykp67ts+5qZbaFSAAzacucwBet5/5EsKYtWBpHbWwV/qbGN/kIBzXg5WEZBhXLrR/RUq0EpEUpL3A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.40.0': + resolution: {integrity: sha512-sm/rH/GysY/KOEvZqYBZSLYFeXlBkHCgqPDgWc07tz+bHCN6mPs9P3otGOSTe7o3KAIM8Nc6ncCO59vL+jb2cA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.39.0': + resolution: {integrity: sha512-LaXnVmD69WPC4hNeLzKexCCS19hRLrUw3xicneAMkzJSzNJvPyk7G6I7lz7VjQh1cooObPBt9gNyd3hhTCUrag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.41.0': + resolution: {integrity: sha512-7fzDe9/FpO6NFizC/wnzXXX7bF9oRchsD//wFqy5g5hVEgXZCQ70IhxjrKdBvgjyIejR9T9zTvfQ6PfVKfkCAw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.13.0': + resolution: {integrity: sha512-Pob0+0R62AqXH50pjazTeGBy/1+SK4CYpFUBV5t7xpbpeuQezkkgVGvLca84QqjBqQizcXedjpUJLgHQDixPQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.5.0': + resolution: {integrity: sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.39.0': + resolution: {integrity: sha512-v/1xziLJ9CyB3YDjBSBzbB70Qd0JwWTo36EqWK5m3AR0CzsyMQQmf3ZIZM6sgx7hXMcRQ0pnEYhg6nhrUQPm9A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.52.1': + resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.52.1': + resolution: {integrity: sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.52.1': + resolution: {integrity: sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/otlp-transformer@0.52.1': + resolution: {integrity: sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/propagation-utils@0.30.16': + resolution: {integrity: sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/propagator-aws-xray@1.26.2': + resolution: {integrity: sha512-k43wxTjKYvwfce9L4eT8fFYy/ATmCfPHZPZsyT/6ABimf2KE1HafoOsIcxLOtmNSZt6dCvBIYCrXaOWta20xJg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-b3@1.25.1': + resolution: {integrity: sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.25.1': + resolution: {integrity: sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/resource-detector-alibaba-cloud@0.29.7': + resolution: {integrity: sha512-PExUl/R+reSQI6Y/eNtgAsk6RHk1ElYSzOa8/FHfdc/nLmx9sqMasBEpLMkETkzDP7t27ORuXe4F9vwkV2uwwg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@1.12.0': + resolution: {integrity: sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.2.12': + resolution: {integrity: sha512-iIarQu6MiCjEEp8dOzmBvCSlRITPFTinFB2oNKAjU6xhx8d7eUcjNOKhBGQTvuCriZrxrEvDaEEY9NfrPQ6uYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.4.4': + resolution: {integrity: sha512-ZEN2mq7lIjQWJ8NTt1umtr6oT/Kb89856BOmESLSvgSHbIwOFYs7cSfSRH5bfiVw6dXTQAVbZA/wLgCHKrebJA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.29.13': + resolution: {integrity: sha512-vdotx+l3Q+89PeyXMgKEGnZ/CwzwMtuMi/ddgD9/5tKZ08DfDGB2Npz9m2oXPHRCjc4Ro6ifMqFlRyzIvgOjhg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@1.25.1': + resolution: {integrity: sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.52.1': + resolution: {integrity: sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.25.1': + resolution: {integrity: sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-node@0.52.1': + resolution: {integrity: sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.25.1': + resolution: {integrity: sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.25.1': + resolution: {integrity: sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.25.1': + resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} '@protobufjs/float@1.0.2': @@ -842,6 +1368,9 @@ packages: resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@speed-highlight/core@1.2.17': resolution: {integrity: sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==} @@ -851,24 +1380,89 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} + engines: {node: '>= 10'} + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/aws-lambda@8.10.122': + resolution: {integrity: sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw==} + + '@types/bunyan@1.8.9': + resolution: {integrity: sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==} + + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.36': + resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/mysql@2.15.22': + resolution: {integrity: sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + '@types/node@25.9.3': resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} + '@types/pg-pool@2.0.4': + resolution: {integrity: sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==} + + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + '@vitest/expect@4.1.9': resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} @@ -898,15 +1492,98 @@ packages: '@vitest/utils@4.1.9': resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@workflow/serde@4.1.0': + resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} + ai@7.0.3: + resolution: {integrity: sha512-ze2nTtoNW1hYhkjAVUVoLQGC/vDmNv+nIo3n8aCv2QQS8WCuC8o3CF6ywnUlui/i7lfGU4V8aPClVRH62WUHeA==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 - base64-js@1.5.1: + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} bignumber.js@9.3.1: @@ -915,9 +1592,28 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -925,17 +1621,77 @@ packages: cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -945,42 +1701,152 @@ packages: supports-color: optional: true + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + dotprompt@1.1.2: + resolution: {integrity: sha512-24EU+eORQbPywBicIP44BiqykzEXFwZq1ZQKO5TEr9KrrENyDA7I1NzqhtmmEdQVfAXka0DEbSLPN5nerCqJ8A==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventid@2.0.1: + resolution: {integrity: sha512-sPNTqiMokAvV048P2c9+foqVJzk49o6d4e0D/sq5jog3pw+4kBgyR0gaM1FM7Mx6Kzd9dztesh9oYz1LWWOpzw==} + engines: {node: '>=10'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + farmhash-modern@1.1.0: + resolution: {integrity: sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==} + engines: {node: '>=18.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.9.3: + resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} + hasBin: true + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -990,43 +1856,239 @@ packages: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + firebase-admin@14.0.0: + resolution: {integrity: sha512-U88/r6VWiBQ05+UlLaF1A1AN4Y3SAGQKcQWawzafEAnXVaCZ21+2KclMPdlIQAAF5pUtN+FkXCSQnJEpc6QDZA==} + engines: {node: '>=22'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@2.5.6: + resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==} + engines: {node: '>= 0.12'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + gaxios@7.1.5: resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==} engines: {node: '>=18'} + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gcp-metadata@8.1.2: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} + genkit@1.39.0: + resolution: {integrity: sha512-Qz9ef/LfAEDMzO1+Nd6l6dMYK/V4ZfWDIp79rz/1V6xDM5VuJMkoqOTCDwXAviuAnjoO2noJug2wIK81ilMhWA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + google-auth-library@10.5.0: + resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} + engines: {node: '>=18'} + google-auth-library@10.7.0: resolution: {integrity: sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==} engines: {node: '>=18'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-gax@4.6.1: + resolution: {integrity: sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==} + engines: {node: '>=14'} + + google-gax@5.0.7: + resolution: {integrity: sha512-EhiqaWWJ+9h7sCcKJTsoo6tMcjokVHhWsbSuWCnZJT4vIBP3y4mAoFLnt9SzgkVZeq24ZsFaArr06nnYYku2yA==} + engines: {node: '>=18'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + google-logging-utils@1.1.3: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} - hono@4.12.25: - resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@137.1.0: + resolution: {integrity: sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==} + engines: {node: '>=14.0.0'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.12.27: + resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} engines: {node: '>=16.9.0'} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unsafe@1.0.1: + resolution: {integrity: sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -1034,9 +2096,33 @@ packages: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + jwks-rsa@4.1.0: + resolution: {integrity: sha512-sbkByqyATKYJP5F4RXj03N5TUNC0QLTjCAZvwTzC4BwJZ8e0/cWxN8YROnyUth2g1/ONWi4eSFHeu6oYalrc3Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >= 23.0.0} + jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} @@ -1044,6 +2130,29 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + langsmith@0.7.10: + resolution: {integrity: sha512-3EjJx9zGMzqF60eT9JADHF+Hn/T5ayTgEVp4d3M5yvJIJi3q6seX0p5jT8ecBCWBi1kIvvssWrcDxfwgSier7Q==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + ws: '>=7' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + ws: + optional: true + lefthook-darwin-arm64@2.1.9: resolution: {integrity: sha512-119HryNcvr4nqn0wUIrNPgpMEPn9yMQzEcW/lezRsnb56PCJriJB92+MCySPVcWDxJnZef7o0T3jdnPNiSH7Qg==} cpu: [arm64] @@ -1172,64 +2281,265 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.mapvalues@4.6.0: + resolution: {integrity: sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lru-memoizer@3.0.0: + resolution: {integrity: sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - miniflare@4.20260611.0: - resolution: {integrity: sha512-i+JwEo8vN96naz1WL3ntFgFyRluBDYL408zwhHKvR2jefJ464KsZ/gCmJAQ5k+oaWeb5Ug+s7yne5AyiAEswjg==} - engines: {node: '>=22.0.0'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} hasBin: true - miniflare@4.20260617.1: - resolution: {integrity: sha512-Go3/gzStm99QHptsSgU+q1S+xDfLoRgwjJNY80kaTVi0ENhTyqKq+sc4xZiWBSbM7uUcJwmzm8+QFKtcYLJ9nw==} + miniflare@4.20260625.0: + resolution: {integrity: sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA==} engines: {node: '>=22.0.0'} hasBin: true + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + nanoid@3.3.14: resolution: {integrity: sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obug@2.1.3: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} - openai@6.42.0: - resolution: {integrity: sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + openai@6.45.0: + resolution: {integrity: sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==} peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' ws: ^8.18.0 zod: ^3.25 || ^4.0 peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true ws: optional: true zod: optional: true + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + + path-expression-matcher@1.6.0: + resolution: {integrity: sha512-e5y7RCLHKjemsgQ4eqGJtPyr10ILz25HO7flzxhTV8bgvd5yHx98DGtCAtbVW9f2TqnYI/gEVZd+vz7snrdPTw==} + engines: {node: '>=14.0.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1241,14 +2551,93 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - protobufjs@7.6.4: - resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} - engines: {node: '>=12.0.0'} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + proto3-json-serializer@3.0.4: + resolution: {integrity: sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==} + engines: {node: '>=18'} + + protobufjs@7.6.4: + resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pumpify@2.0.1: + resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + retry-request@8.0.3: + resolution: {integrity: sha512-qqoc4kkGgP9cmQDWELlOpAmfgJOg0Yi7MT82ZjiPWu451ayju4itwomjM4/dBEliify8C1b3tSaeCOldugtwPQ==} + engines: {node: '>=18'} retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + rolldown@1.0.3: resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1257,35 +2646,151 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@7.8.4: resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} standardwebhooks@1.0.0: resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + string-width-cjs@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + aliasOf: string-width + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi-cjs@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + aliasOf: strip-ansi + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + teeny-request@10.1.3: + resolution: {integrity: sha512-5yDliI1uWkYPo7W+Zvrxg6YmoWuj5iC5EydewqrRTvc68nyMTZhlPPlLg6cptUGfbQAb+N9XDPDPzF6N081lug==} + engines: {node: '>=18'} + + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1301,24 +2806,43 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - undici@7.24.8: - resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} - engines: {node: '>=20.18.1'} - undici@7.28.0: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} @@ -1326,6 +2850,42 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uri-templates@0.2.0: + resolution: {integrity: sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==} + + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1415,52 +2975,71 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + websocket-driver@0.7.5: + resolution: {integrity: sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true - workerd@1.20260611.1: - resolution: {integrity: sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w==} - engines: {node: '>=16'} - hasBin: true + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - workerd@1.20260617.1: - resolution: {integrity: sha512-Re5pl6pdowt3ZmWUzGlOuB7jbRIIPetgKalmo4cYmucQnVhpo7/3e4MfpekbhLi2EhZZz5EY9NWRu8zFzuEZew==} + workerd@1.20260625.1: + resolution: {integrity: sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA==} engines: {node: '>=16'} hasBin: true - wrangler@4.100.0: - resolution: {integrity: sha512-dSQO7DO+mD6XDzkVWIWBoGLO3yw+lacWSc/KhFvd7pgfpth+kX98qb5SGRHZN8ACCDhhfwzDLXwB6qHsIHhfBg==} + wrangler@4.105.0: + resolution: {integrity: sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g==} engines: {node: '>=22.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20260611.1 + '@cloudflare/workers-types': ^4.20260625.1 peerDependenciesMeta: '@cloudflare/workers-types': optional: true - wrangler@4.103.0: - resolution: {integrity: sha512-3Lv1P5t2xcSEkSTKtG+Lz+3JFryuU7YPLkaCUj7gNe+CJsjZJLtUwqsh1x595QBxkIbCE0GAvDx2DCJUU4+oqw==} - engines: {node: '>=22.0.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260617.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true + wrap-ansi-cjs@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + aliasOf: wrap-ansi - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} - 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 + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} @@ -1474,123 +3053,194 @@ packages: utf-8-validate: optional: true + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: - '@anthropic-ai/sdk@0.104.1(zod@3.25.76)': + '@ai-sdk/anthropic@4.0.0(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.4.3) + zod: 4.4.3 + + '@ai-sdk/gateway@4.0.3(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/google@4.0.1(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.4.3) + zod: 4.4.3 + + '@ai-sdk/openai@4.0.1(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.4.3) + zod: 4.4.3 + + '@ai-sdk/provider-utils@5.0.0(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 4.0.0 + '@standard-schema/spec': 1.1.0 + '@workflow/serde': 4.1.0 + eventsource-parser: 3.1.0 + zod: 4.4.3 + + '@ai-sdk/provider@4.0.0': + dependencies: + json-schema: 0.4.0 + + '@anthropic-ai/sdk@0.103.0(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 - zod: 3.25.76 + zod: 4.4.3 + + '@anthropic-ai/sdk@0.106.0(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + standardwebhooks: 1.0.0 + zod: 4.4.3 '@babel/runtime@7.29.7': {} - '@biomejs/biome@2.5.0': + '@biomejs/biome@2.5.1': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.0 - '@biomejs/cli-darwin-x64': 2.5.0 - '@biomejs/cli-linux-arm64': 2.5.0 - '@biomejs/cli-linux-arm64-musl': 2.5.0 - '@biomejs/cli-linux-x64': 2.5.0 - '@biomejs/cli-linux-x64-musl': 2.5.0 - '@biomejs/cli-win32-arm64': 2.5.0 - '@biomejs/cli-win32-x64': 2.5.0 + '@biomejs/cli-darwin-arm64': 2.5.1 + '@biomejs/cli-darwin-x64': 2.5.1 + '@biomejs/cli-linux-arm64': 2.5.1 + '@biomejs/cli-linux-arm64-musl': 2.5.1 + '@biomejs/cli-linux-x64': 2.5.1 + '@biomejs/cli-linux-x64-musl': 2.5.1 + '@biomejs/cli-win32-arm64': 2.5.1 + '@biomejs/cli-win32-x64': 2.5.1 - '@biomejs/cli-darwin-arm64@2.5.0': + '@biomejs/cli-darwin-arm64@2.5.1': optional: true - '@biomejs/cli-darwin-x64@2.5.0': + '@biomejs/cli-darwin-x64@2.5.1': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.0': + '@biomejs/cli-linux-arm64-musl@2.5.1': optional: true - '@biomejs/cli-linux-arm64@2.5.0': + '@biomejs/cli-linux-arm64@2.5.1': optional: true - '@biomejs/cli-linux-x64-musl@2.5.0': + '@biomejs/cli-linux-x64-musl@2.5.1': optional: true - '@biomejs/cli-linux-x64@2.5.0': + '@biomejs/cli-linux-x64@2.5.1': optional: true - '@biomejs/cli-win32-arm64@2.5.0': + '@biomejs/cli-win32-arm64@2.5.1': optional: true - '@biomejs/cli-win32-x64@2.5.0': + '@biomejs/cli-win32-x64@2.5.1': optional: true - '@cloudflare/kv-asset-handler@0.5.0': {} + '@cfworker/json-schema@4.1.1': {} - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260611.1)': - dependencies: - unenv: 2.0.0-rc.24 - workerd: 1.20260611.1 + '@cloudflare/kv-asset-handler@0.5.0': {} - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260617.1)': + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260625.1)': dependencies: unenv: 2.0.0-rc.24 - workerd: 1.20260617.1 + workerd: 1.20260625.1 - '@cloudflare/vitest-pool-workers@0.16.18(@vitest/runner@4.1.9)(@vitest/snapshot@4.1.9)(vitest@4.1.9(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)))': + '@cloudflare/vitest-pool-workers@0.16.20(@cloudflare/workers-types@4.20260627.1)(@vitest/runner@4.1.9)(@vitest/snapshot@4.1.9)(vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(vite@8.0.16(@types/node@25.9.3)))': dependencies: '@vitest/runner': 4.1.9 '@vitest/snapshot': 4.1.9 cjs-module-lexer: 1.2.3 esbuild: 0.28.1 - miniflare: 4.20260617.1 - vitest: 4.1.9(@types/node@25.9.3)(esbuild@0.28.1)(vite@8.0.16(@types/node@25.9.3)) - wrangler: 4.103.0 + miniflare: 4.20260625.0 + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(esbuild@0.28.1)(vite@8.0.16(@types/node@25.9.3)) + wrangler: 4.105.0(@cloudflare/workers-types@4.20260627.1) zod: 3.25.76 transitivePeerDependencies: - - '@cloudflare/workers-types' - bufferutil - utf-8-validate - '@cloudflare/workerd-darwin-64@1.20260611.1': - optional: true - - '@cloudflare/workerd-darwin-64@1.20260617.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260611.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260617.1': + '@cloudflare/workerd-darwin-64@1.20260625.1': optional: true - '@cloudflare/workerd-linux-64@1.20260611.1': + '@cloudflare/workerd-darwin-arm64@1.20260625.1': optional: true - '@cloudflare/workerd-linux-64@1.20260617.1': + '@cloudflare/workerd-linux-64@1.20260625.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260611.1': + '@cloudflare/workerd-linux-arm64@1.20260625.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260617.1': + '@cloudflare/workerd-windows-64@1.20260625.1': optional: true - '@cloudflare/workerd-windows-64@1.20260611.1': - optional: true + '@cloudflare/workers-types@4.20260627.1': {} - '@cloudflare/workerd-windows-64@1.20260617.1': + '@colors/colors@1.6.0': optional: true - '@cloudflare/workers-types@4.20260615.1': {} - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + optional: true + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1612,179 +3262,436 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': - optional: true - '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.3': - optional: true - '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.3': - optional: true - '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.3': - optional: true - '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.3': - optional: true - '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.3': - optional: true - '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.3': - optional: true - '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.3': - optional: true - '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.3': - optional: true - '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.3': - optional: true - '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.3': - optional: true - '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.3': - optional: true - '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.3': - optional: true - '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.3': - optional: true - '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.3': - optional: true - '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.3': - optional: true - '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/linux-x64@0.28.1': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.28.1': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.28.1': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.28.1': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/openbsd-x64@0.28.1': + '@esbuild/win32-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@fastify/busboy@3.2.0': optional: true - '@esbuild/openharmony-arm64@0.28.1': + '@firebase/app-check-interop-types@0.3.4': optional: true - '@esbuild/sunos-x64@0.27.3': + '@firebase/app-types@0.9.5': + dependencies: + '@firebase/logger': 0.5.1 optional: true - '@esbuild/sunos-x64@0.28.1': + '@firebase/auth-interop-types@0.2.5': optional: true - '@esbuild/win32-arm64@0.27.3': + '@firebase/component@0.7.3': + dependencies: + '@firebase/util': 1.15.1 + tslib: 2.8.1 optional: true - '@esbuild/win32-arm64@0.28.1': + '@firebase/database-compat@2.1.4': + dependencies: + '@firebase/component': 0.7.3 + '@firebase/database': 1.1.3 + '@firebase/database-types': 1.0.20 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 optional: true - '@esbuild/win32-ia32@0.27.3': + '@firebase/database-types@1.0.20': + dependencies: + '@firebase/app-types': 0.9.5 + '@firebase/util': 1.15.1 optional: true - '@esbuild/win32-ia32@0.28.1': + '@firebase/database@1.1.3': + dependencies: + '@firebase/app-check-interop-types': 0.3.4 + '@firebase/auth-interop-types': 0.2.5 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + faye-websocket: 0.11.4 + tslib: 2.8.1 optional: true - '@esbuild/win32-x64@0.27.3': + '@firebase/logger@0.5.1': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/win32-x64@0.28.1': + '@firebase/util@1.15.1': + dependencies: + tslib: 2.8.1 optional: true - '@google/genai@2.8.0': + '@genkit-ai/ai@1.39.0(@google-cloud/firestore@7.11.6)(firebase-admin@14.0.0)(genkit@1.39.0)': dependencies: - google-auth-library: 10.7.0 - p-retry: 4.6.2 - protobufjs: 7.6.4 - ws: 8.20.1 + '@genkit-ai/core': 1.39.0(@google-cloud/firestore@7.11.6)(firebase-admin@14.0.0)(genkit@1.39.0) + '@opentelemetry/api': 1.9.1 + '@types/node': 20.19.43 + colorette: 2.0.20 + dotprompt: 1.1.2 + handlebars: 4.7.9 + json5: 2.2.3 + node-fetch: 3.3.2 + partial-json: 0.1.7 + uri-templates: 0.2.0 + uuid: 10.0.0 transitivePeerDependencies: - bufferutil + - encoding + - firebase - supports-color - utf-8-validate - '@img/colour@1.1.0': {} - - '@img/sharp-darwin-arm64@0.34.5': + '@genkit-ai/core@1.39.0(@google-cloud/firestore@7.11.6)(firebase-admin@14.0.0)(genkit@1.39.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + async-mutex: 0.5.0 + cors: 2.8.6 + dotprompt: 1.1.2 + express: 4.22.2 + get-port: 5.1.1 + json-schema: 0.4.0 + ws: 8.21.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true + '@cfworker/json-schema': 4.1.1 + '@genkit-ai/firebase': 1.37.0(@google-cloud/firestore@7.11.6)(firebase-admin@14.0.0)(genkit@1.39.0) + transitivePeerDependencies: + - bufferutil + - encoding + - firebase + - supports-color + - utf-8-validate + + '@genkit-ai/firebase@1.37.0(@google-cloud/firestore@7.11.6)(firebase-admin@14.0.0)(genkit@1.39.0)': + dependencies: + '@genkit-ai/google-cloud': 1.37.0(genkit@1.39.0) + '@google-cloud/firestore': 7.11.6 + firebase-admin: 14.0.0 + genkit: 1.39.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@genkit-ai/google-cloud@1.37.0(genkit@1.39.0)': + dependencies: + '@google-cloud/logging-winston': 6.0.2(winston@3.19.0) + '@google-cloud/modelarmor': 0.4.1 + '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.19.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.1)) + '@google-cloud/opentelemetry-cloud-trace-exporter': 2.4.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.1)) + '@google-cloud/opentelemetry-resource-util': 2.4.0(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1)) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/auto-instrumentations-node': 0.49.2(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pino': 0.41.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-winston': 0.39.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + genkit: 1.39.0 + google-auth-library: 9.15.1 + node-fetch: 3.3.2 + winston: 3.19.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@genkit-ai/google-genai@1.39.0(genkit@1.39.0)': + dependencies: + genkit: 1.39.0 + google-auth-library: 9.15.1 + jsonpath-plus: 10.4.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@google-cloud/common@5.0.2': + dependencies: + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + arrify: 2.0.1 + duplexify: 4.1.3 + extend: 3.0.2 + google-auth-library: 9.15.1 + html-entities: 2.6.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/firestore@7.11.6': + dependencies: + '@opentelemetry/api': 1.9.1 + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 4.6.1 + protobufjs: 7.6.4 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/firestore@8.6.0': + dependencies: + '@opentelemetry/api': 1.9.1 + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 5.0.7 + protobufjs: 7.6.4 + transitivePeerDependencies: + - supports-color + optional: true + + '@google-cloud/logging-winston@6.0.2(winston@3.19.0)': + dependencies: + '@google-cloud/logging': 11.2.3 + google-auth-library: 10.7.0 + lodash.mapvalues: 4.6.0 + winston: 3.19.0 + winston-transport: 4.9.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/logging@11.2.3': + dependencies: + '@google-cloud/common': 5.0.2 + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + arrify: 2.0.1 + dot-prop: 6.0.1 + eventid: 2.0.1 + extend: 3.0.2 + gcp-metadata: 6.1.1 + google-auth-library: 9.15.1 + google-gax: 4.6.1 + long: 5.3.2 + on-finished: 2.4.1 + pumpify: 2.0.1 + stream-events: 1.0.5 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/modelarmor@0.4.1': + dependencies: + google-gax: 5.0.7 + transitivePeerDependencies: + - supports-color + optional: true + + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.19.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.1))': + dependencies: + '@google-cloud/opentelemetry-resource-util': 2.4.0(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1)) + '@google-cloud/precise-date': 4.0.0 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.1) + google-auth-library: 9.15.1 + googleapis: 137.1.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/opentelemetry-cloud-trace-exporter@2.4.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.1))': + dependencies: + '@google-cloud/opentelemetry-resource-util': 2.4.0(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1)) + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + google-auth-library: 9.15.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/opentelemetry-resource-util@2.4.0(@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1))': + dependencies: + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + optional: true + + '@google-cloud/precise-date@4.0.0': + optional: true + + '@google-cloud/projectify@4.0.0': + optional: true + + '@google-cloud/promisify@4.0.0': + optional: true + + '@google-cloud/storage@7.21.0': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 5.9.3 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + html-entities: 2.6.0 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@google/genai@2.10.0': + dependencies: + google-auth-library: 10.5.0 + p-retry: 4.6.2 + protobufjs: 7.6.4 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@google/generative-ai@0.24.1': {} + + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.4 + yargs: 17.7.3 + optional: true + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.4 + yargs: 17.7.3 + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true '@img/sharp-darwin-x64@0.34.5': optionalDependencies: @@ -1826,73 +3733,796 @@ snapshots: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: 4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: 6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: 7.0.0 + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@langchain/anthropic@1.5.1(@langchain/core@1.2.1)(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': + dependencies: + '@anthropic-ai/sdk': 0.103.0(zod@4.4.3) + '@langchain/core': 1.2.1(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + zod: 4.4.3 + + '@langchain/core@1.2.1(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': + dependencies: + '@cfworker/json-schema': 4.1.1 + '@standard-schema/spec': 1.1.0 + js-tiktoken: 1.0.21 + langsmith: 0.7.10(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + mustache: 4.2.0 + p-queue: 6.6.2 + zod: 4.4.3 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + + '@langchain/google-genai@2.2.0(@langchain/core@1.2.1)(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0)': + dependencies: + '@google/generative-ai': 0.24.1 + '@langchain/core': 1.2.1(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + + '@langchain/openai@1.5.3(@langchain/core@1.2.1)(@opentelemetry/api@1.9.1)(ws@8.21.0)': + dependencies: + '@langchain/core': 1.2.1(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0) + js-tiktoken: 1.0.21 + openai: 6.45.0(ws@8.21.0)(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - '@smithy/hash-node' + - '@smithy/signature-v4' + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@nodable/entities@2.2.0': + optional: true + + '@opentelemetry/api-logs@0.52.1': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/auto-instrumentations-node@0.49.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.41.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-aws-lambda': 0.43.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-aws-sdk': 0.43.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-bunyan': 0.40.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-cassandra-driver': 0.40.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.38.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-cucumber': 0.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dns': 0.38.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-express': 0.41.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fastify': 0.38.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.14.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.38.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.42.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-grpc': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.40.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-ioredis': 0.42.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.2.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.39.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.42.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.39.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-memcached': 0.38.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.46.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.41.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.40.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.40.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-nestjs-core': 0.39.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-net': 0.38.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.43.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pino': 0.41.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis': 0.41.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis-4': 0.41.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-restify': 0.40.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-router': 0.39.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-socket.io': 0.41.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.13.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-undici': 0.5.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-winston': 0.39.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-alibaba-cloud': 0.29.7(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-azure': 0.2.12(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-container': 0.4.4(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp': 0.29.13(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + '@opentelemetry/context-async-hooks@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.25.1 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.28.0 + optional: true + + '@opentelemetry/exporter-trace-otlp-grpc@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-trace-otlp-http@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-trace-otlp-proto@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-zipkin@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + + '@opentelemetry/instrumentation-amqplib@0.41.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-aws-lambda@0.43.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-aws-xray': 1.26.2(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + '@types/aws-lambda': 8.10.122 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-aws-sdk@0.43.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagation-utils': 0.30.16(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-bunyan@0.40.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@types/bunyan': 1.8.9 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-cassandra-driver@0.40.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-connect@0.38.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + '@types/connect': 3.4.36 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-cucumber@0.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-dataloader@0.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-dns@0.38.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + semver: 7.8.4 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-express@0.41.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-fastify@0.38.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-fs@0.14.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-generic-pool@0.38.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-graphql@0.42.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-grpc@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-hapi@0.40.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-http@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + semver: 7.8.4 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-ioredis@0.42.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-kafkajs@0.2.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-knex@0.39.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-koa@0.42.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-lru-memoizer@0.39.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-memcached@0.38.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-mongodb@0.46.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-mongoose@0.41.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-mysql2@0.40.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-mysql@0.40.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + '@types/mysql': 2.15.22 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-nestjs-core@0.39.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-net@0.38.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-pg@0.43.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.1) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.4 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-pino@0.41.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-redis-4@0.41.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-redis@0.41.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-restify@0.40.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-router@0.39.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-socket.io@0.41.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-tedious@0.13.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-undici@0.5.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation-winston@0.39.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + optional: true + + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.8.4 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-grpc-exporter-base@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + protobufjs: 7.6.4 + + '@opentelemetry/propagation-utils@0.30.16(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 optional: true - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@opentelemetry/propagator-aws-xray@1.26.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 optional: true - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true + '@opentelemetry/propagator-b3@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true + '@opentelemetry/propagator-jaeger@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@opentelemetry/redis-common@0.36.2': optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@opentelemetry/resource-detector-alibaba-cloud@0.29.7(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@opentelemetry/resource-detector-aws@1.12.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 optional: true - '@img/sharp-wasm32@0.34.5': + '@opentelemetry/resource-detector-azure@0.2.12(@opentelemetry/api@1.9.1)': dependencies: - '@emnapi/runtime': 1.11.1 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@opentelemetry/resource-detector-container@0.4.4(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 optional: true - '@img/sharp-win32-ia32@0.34.5': + '@opentelemetry/resource-detector-gcp@0.29.13(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color optional: true - '@img/sharp-win32-x64@0.34.5': - optional: true + '@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 - '@jridgewell/resolve-uri@3.1.2': {} + '@opentelemetry/sdk-logs@0.52.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) - '@jridgewell/sourcemap-codec@1.5.5': {} + '@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + lodash.merge: 4.6.2 - '@jridgewell/trace-mapping@0.3.9': + '@opentelemetry/sdk-node@0.52.1(@opentelemetry/api@1.9.1)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-proto': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-zipkin': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.1)': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.25.1 + + '@opentelemetry/sdk-trace-node@1.25.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 1.25.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) + semver: 7.8.4 + + '@opentelemetry/semantic-conventions@1.25.1': {} + + '@opentelemetry/semantic-conventions@1.28.0': + optional: true + + '@opentelemetry/semantic-conventions@1.41.1': + optional: true + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) optional: true '@oxc-project/types@0.133.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -1978,32 +4608,124 @@ snapshots: '@sindresorhus/is@7.2.0': {} + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + optional: true + '@speed-highlight/core@1.2.17': {} '@stablelib/base64@1.0.1': {} '@standard-schema/spec@1.1.0': {} + '@tootallnate/once@2.0.1': + optional: true + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 optional: true + '@types/aws-lambda@8.10.122': + optional: true + + '@types/bunyan@1.8.9': + dependencies: + '@types/node': 25.9.3 + optional: true + + '@types/caseless@0.12.5': + optional: true + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.36': + dependencies: + '@types/node': 25.9.3 + optional: true + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} + '@types/json-schema@7.0.15': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 25.9.3 + optional: true + + '@types/long@4.0.2': + optional: true + + '@types/memcached@2.2.10': + dependencies: + '@types/node': 25.9.3 + optional: true + + '@types/ms@2.1.0': + optional: true + + '@types/mysql@2.15.22': + dependencies: + '@types/node': 25.9.3 + optional: true + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + '@types/node@25.9.3': dependencies: undici-types: 7.24.6 + '@types/pg-pool@2.0.4': + dependencies: + '@types/pg': 8.6.1 + optional: true + + '@types/pg@8.6.1': + dependencies: + '@types/node': 25.9.3 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + optional: true + + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 25.9.3 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.6 + optional: true + '@types/retry@0.12.0': {} + '@types/shimmer@1.2.0': {} + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 25.9.3 + optional: true + + '@types/tough-cookie@4.0.5': + optional: true + + '@types/triple-beam@1.3.5': + optional: true + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.43 + + '@vercel/oidc@3.2.0': {} + '@vitest/expect@4.1.9': dependencies: '@standard-schema/spec': 1.1.0 @@ -2044,70 +4766,285 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@workflow/serde@4.1.0': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + optional: true + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-import-attributes@1.9.5(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + agent-base@7.1.4: {} + ai@7.0.3(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 4.0.3(zod@4.4.3) + '@ai-sdk/provider': 4.0.0 + '@ai-sdk/provider-utils': 5.0.0(zod@4.4.3) + zod: 4.4.3 + + ajv-formats@3.0.1(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: + optional: true + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: + optional: true + + anynum@1.0.1: + optional: true + + array-flatten@1.1.1: {} + + arrify@2.0.1: + optional: true + assertion-error@2.0.1: {} + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + optional: true + + async@3.2.6: + optional: true + + asynckit@0.4.0: + optional: true + + balanced-match@1.0.2: + optional: true + base64-js@1.5.1: {} bignumber.js@9.3.1: {} blake3-wasm@2.1.5: {} + body-parser@1.20.5: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + optional: true + buffer-equal-constant-time@1.0.1: {} + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + chai@6.2.2: {} cjs-module-lexer@1.2.3: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + optional: true + + color-name@1.1.4: {} + + color-name@2.1.0: + optional: true + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + optional: true + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + optional: true + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + optional: true + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + optional: true + data-uri-to-buffer@4.0.1: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.3: dependencies: ms: 2.1.3 + delayed-stream@1.0.0: + optional: true + + depd@2.0.0: {} + + destroy@1.2.0: {} + detect-libc@2.1.2: {} + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + optional: true + + dotprompt@1.1.2: + dependencies: + handlebars: 4.7.9 + yaml: 2.9.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + optional: true + + eastasianwidth@0.2.0: + optional: true + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: + optional: true + + enabled@2.0.0: + optional: true + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@2.1.0: {} - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + optional: true esbuild@0.28.1: optionalDependencies: @@ -2138,32 +5075,182 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 - estree-walker@3.0.3: + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + etag@1.8.1: {} + + event-target-shim@5.0.1: + optional: true + + eventemitter3@4.0.7: {} + + eventid@2.0.1: dependencies: - '@types/estree': 1.0.9 + uuid: 8.3.2 + optional: true + + eventsource-parser@3.1.0: {} expect-type@1.3.0: {} + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.5 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + extend@3.0.2: {} + farmhash-modern@1.1.0: + optional: true + + fast-deep-equal@3.1.3: {} + fast-sha256@1.3.0: {} + fast-uri@3.1.2: {} + + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.6.0 + xml-naming: 0.1.0 + optional: true + + fast-xml-parser@5.9.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.0 + is-unsafe: 1.0.1 + path-expression-matcher: 1.6.0 + strnum: 2.4.1 + xml-naming: 0.1.0 + optional: true + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.5 + optional: true + fdir@6.5.0(picomatch@4.0.4): dependencies: picomatch: 4.0.4 + fecha@4.2.3: + optional: true + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + + firebase-admin@14.0.0: + dependencies: + '@fastify/busboy': 3.2.0 + '@firebase/database-compat': 2.1.4 + '@firebase/database-types': 1.0.20 + farmhash-modern: 1.1.0 + fast-deep-equal: 3.1.3 + google-auth-library: 10.7.0 + jsonwebtoken: 9.0.3 + jwks-rsa: 4.1.0 + optionalDependencies: + '@google-cloud/firestore': 8.6.0 + '@google-cloud/storage': 7.21.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + fn.name@1.1.0: + optional: true + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + optional: true + + form-data@2.5.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + optional: true + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 + forwarded@0.2.0: {} + + fresh@0.5.2: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + functional-red-black-tree@1.0.1: + optional: true + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + gaxios@7.1.5: dependencies: extend: 3.0.2 @@ -2172,6 +5259,15 @@ snapshots: transitivePeerDependencies: - supports-color + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gcp-metadata@8.1.2: dependencies: gaxios: 7.1.5 @@ -2180,6 +5276,62 @@ snapshots: transitivePeerDependencies: - supports-color + genkit@1.39.0: + dependencies: + '@genkit-ai/ai': 1.39.0(@google-cloud/firestore@7.11.6)(firebase-admin@14.0.0)(genkit@1.39.0) + '@genkit-ai/core': 1.39.0(@google-cloud/firestore@7.11.6)(firebase-admin@14.0.0)(genkit@1.39.0) + uuid: 10.0.0 + transitivePeerDependencies: + - bufferutil + - encoding + - firebase + - supports-color + - utf-8-validate + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-port@5.1.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + optional: true + + google-auth-library@10.5.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.5 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + gtoken: 8.0.0 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + google-auth-library@10.7.0: dependencies: base64-js: 1.5.1 @@ -2190,10 +5342,159 @@ snapshots: jws: 4.0.1 transitivePeerDependencies: - supports-color + optional: true + + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-gax@4.6.1: + dependencies: + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + '@types/long': 4.0.2 + abort-controller: 3.0.0 + duplexify: 4.1.3 + google-auth-library: 9.15.1 + node-fetch: 2.7.0 + object-hash: 3.0.0 + proto3-json-serializer: 2.0.2 + protobufjs: 7.6.4 + retry-request: 7.0.2 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + google-gax@5.0.7: + dependencies: + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.8.1 + duplexify: 4.1.3 + google-auth-library: 10.5.0 + google-logging-utils: 1.1.3 + node-fetch: 3.3.2 + object-hash: 3.0.0 + proto3-json-serializer: 3.0.4 + protobufjs: 7.6.4 + retry-request: 8.0.3 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + optional: true + + google-logging-utils@0.0.2: {} google-logging-utils@1.1.3: {} - hono@4.12.25: {} + googleapis-common@7.2.0: + dependencies: + extend: 3.0.2 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + qs: 6.15.2 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + googleapis@137.1.0: + dependencies: + google-auth-library: 9.15.1 + googleapis-common: 7.2.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + gopd@1.2.0: {} + + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gtoken@8.0.0: + dependencies: + gaxios: 7.1.5 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + optional: true + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.12.27: {} + + html-entities@2.6.0: + optional: true + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-parser-js@0.5.10: + optional: true + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.1 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true https-proxy-agent@7.0.6: dependencies: @@ -2202,6 +5503,54 @@ snapshots: transitivePeerDependencies: - supports-color + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.17.0 + acorn-import-attributes: 1.9.5(acorn@8.17.0) + cjs-module-lexer: 1.2.3 + module-details-from-path: 1.0.4 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-obj@2.0.0: + optional: true + + is-stream@2.0.1: {} + + is-unsafe@1.0.1: + optional: true + + isexe@2.0.0: + optional: true + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + optional: true + + jose@6.2.3: + optional: true + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + jsep@1.4.0: {} + json-bigint@1.0.0: dependencies: bignumber.js: 9.3.1 @@ -2211,12 +5560,50 @@ snapshots: '@babel/runtime': 7.29.7 ts-algebra: 2.0.0 + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + json5@2.2.3: {} + + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.4 + optional: true + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jwks-rsa@4.1.0: + dependencies: + '@types/jsonwebtoken': 9.0.10 + debug: 4.4.3 + jose: 6.2.3 + limiter: 1.1.5 + lru-cache: 11.5.1 + lru-memoizer: 3.0.0 + transitivePeerDependencies: + - supports-color + optional: true + jws@4.0.1: dependencies: jwa: 2.0.1 @@ -2224,6 +5611,16 @@ snapshots: kleur@4.1.5: {} + kuler@2.0.0: + optional: true + + langsmith@0.7.10(@opentelemetry/api@1.9.1)(openai@6.45.0(ws@8.21.0)(zod@4.4.3))(ws@8.21.0): + dependencies: + '@opentelemetry/api': 1.9.1 + openai: 6.45.0(ws@8.21.0)(zod@4.4.3) + p-queue: 6.6.2 + ws: 8.21.0 + lefthook-darwin-arm64@2.1.9: optional: true @@ -2316,64 +5713,226 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + limiter@1.1.5: + optional: true + + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: + optional: true + + lodash.includes@4.3.0: + optional: true + + lodash.isboolean@3.0.3: + optional: true + + lodash.isinteger@4.0.4: + optional: true + + lodash.isnumber@3.0.3: + optional: true + + lodash.isplainobject@4.0.6: + optional: true + + lodash.isstring@4.0.1: + optional: true + + lodash.mapvalues@4.6.0: + optional: true + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: + optional: true + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + optional: true + long@5.3.2: {} + lru-cache@10.4.3: + optional: true + + lru-cache@11.5.1: + optional: true + + lru-memoizer@3.0.0: + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 11.5.1 + optional: true + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - miniflare@4.20260611.0: + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.24.8 - workerd: 1.20260611.1 - ws: 8.20.1 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate + mime-db: 1.52.0 + + mime@1.6.0: {} - miniflare@4.20260617.1: + mime@3.0.0: + optional: true + + miniflare@4.20260625.0: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 undici: 7.28.0 - workerd: 1.20260617.1 + workerd: 1.20260625.1 ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: - bufferutil - utf-8-validate + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + optional: true + + minimist@1.2.8: {} + + minipass@7.1.3: + optional: true + + module-details-from-path@1.0.4: {} + + ms@2.0.0: {} + ms@2.1.3: {} + mustache@4.2.0: {} + nanoid@3.3.14: {} + negotiator@0.6.3: {} + + neo-async@2.6.2: {} + node-domexception@1.0.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + object-assign@4.1.1: {} + + object-hash@3.0.0: + optional: true + + object-inspect@1.13.4: {} + obug@2.1.3: {} - openai@6.42.0(ws@8.21.0)(zod@3.25.76): + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + optional: true + + openai@6.45.0(ws@8.21.0)(zod@4.4.3): dependencies: ws: 8.21.0 - zod: 3.25.76 + zod: 4.4.3 + + p-finally@1.0.0: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + optional: true + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 p-retry@4.6.2: dependencies: '@types/retry': 0.12.0 retry: 0.13.1 + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + package-json-from-dist@1.0.1: + optional: true + + parseurl@1.3.3: {} + + partial-json@0.1.7: {} + + path-expression-matcher@1.6.0: + optional: true + + path-key@3.1.1: + optional: true + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + optional: true + + path-to-regexp@0.1.13: {} + path-to-regexp@6.3.0: {} pathe@2.0.3: {} + pg-int8@1.0.1: + optional: true + + pg-protocol@1.15.0: + optional: true + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + optional: true + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -2384,6 +5943,30 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: + optional: true + + postgres-bytea@1.0.1: + optional: true + + postgres-date@1.0.7: + optional: true + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + optional: true + + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.6.4 + optional: true + + proto3-json-serializer@3.0.4: + dependencies: + protobufjs: 7.6.4 + optional: true + protobufjs@7.6.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -2398,8 +5981,88 @@ snapshots: '@types/node': 25.9.3 long: 5.3.2 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + + pumpify@2.0.1: + dependencies: + duplexify: 4.1.3 + inherits: 2.0.4 + pump: 3.0.4 + optional: true + + qs@6.15.2: + dependencies: + side-channel: 1.1.1 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.13 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + retry-request@8.0.3: + dependencies: + extend: 3.0.2 + teeny-request: 10.1.3 + transitivePeerDependencies: + - supports-color + optional: true + retry@0.13.1: {} + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + optional: true + rolldown@1.0.3: dependencies: '@oxc-project/types': 0.133.0 @@ -2423,8 +6086,38 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: + optional: true + + safer-buffer@2.1.2: {} + semver@7.8.4: {} + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -2456,10 +6149,56 @@ snapshots: '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + optional: true + + shebang-regex@3.0.0: + optional: true + + shimmer@1.2.1: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + 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 + siginfo@2.0.0: {} + signal-exit@4.1.0: + optional: true + source-map-js@1.2.1: {} + source-map@0.6.1: {} + + stack-trace@0.0.10: + optional: true + stackback@0.0.2: {} standardwebhooks@1.0.0: @@ -2467,10 +6206,94 @@ snapshots: '@stablelib/base64': 1.0.1 fast-sha256: 1.3.0 + statuses@2.0.2: {} + std-env@4.1.0: {} + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + optional: true + + stream-shift@1.0.3: + optional: true + + string-width-cjs@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + optional: true + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + optional: true + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + strip-ansi-cjs@6.0.1: + dependencies: + ansi-regex: 5.0.1 + optional: true + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + optional: true + + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + optional: true + + stubs@3.0.0: + optional: true + supports-color@10.2.2: {} + supports-preserve-symlinks-flag@1.0.0: {} + + teeny-request@10.1.3: + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + stream-events: 1.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + text-hex@1.0.0: + optional: true + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -2482,16 +6305,30 @@ snapshots: tinyrainbow@3.1.0: {} - ts-algebra@2.0.0: {} + toidentifier@1.0.1: {} - tslib@2.8.1: + tr46@0.0.3: {} + + triple-beam@1.4.1: optional: true + ts-algebra@2.0.0: {} + + tslib@2.8.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + typescript@6.0.3: {} - undici-types@7.24.6: {} + uglify-js@3.19.3: + optional: true + + undici-types@6.21.0: {} - undici@7.24.8: {} + undici-types@7.24.6: {} undici@7.28.0: {} @@ -2499,6 +6336,27 @@ snapshots: dependencies: pathe: 2.0.3 + unpipe@1.0.0: {} + + uri-templates@0.2.0: {} + + url-template@2.0.8: + optional: true + + util-deprecate@1.0.2: + optional: true + + utils-merge@1.0.1: {} + + uuid@10.0.0: {} + + uuid@8.3.2: + optional: true + + uuid@9.0.1: {} + + vary@1.1.2: {} + vite@8.0.16(@types/node@25.9.3): dependencies: '@types/node': 25.9.3 @@ -2523,8 +6381,9 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - vitest@4.1.9(@types/node@25.9.3)(esbuild@0.28.1)(vite@8.0.16(@types/node@25.9.3)): + vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(esbuild@0.28.1)(vite@8.0.16(@types/node@25.9.3)): dependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 25.9.3 '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(esbuild@0.28.1)(vite@8.0.16(@types/node@25.9.3)) @@ -2551,64 +6410,132 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} + + websocket-driver@0.7.5: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + optional: true + + websocket-extensions@0.1.4: + optional: true + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + optional: true + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - workerd@1.20260611.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260611.1 - '@cloudflare/workerd-darwin-arm64': 1.20260611.1 - '@cloudflare/workerd-linux-64': 1.20260611.1 - '@cloudflare/workerd-linux-arm64': 1.20260611.1 - '@cloudflare/workerd-windows-64': 1.20260611.1 - - workerd@1.20260617.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260617.1 - '@cloudflare/workerd-darwin-arm64': 1.20260617.1 - '@cloudflare/workerd-linux-64': 1.20260617.1 - '@cloudflare/workerd-linux-arm64': 1.20260617.1 - '@cloudflare/workerd-windows-64': 1.20260617.1 + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + optional: true - wrangler@4.100.0(@cloudflare/workers-types@4.20260615.1): + winston@3.19.0: dependencies: - '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260611.1) - '@cloudflare/workers-types': 4.20260615.1 - blake3-wasm: 2.1.5 - esbuild: 0.27.3 - miniflare: 4.20260611.0 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260611.1 + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + optional: true + + wordwrap@1.0.0: {} + + workerd@1.20260625.1: optionalDependencies: - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate + '@cloudflare/workerd-darwin-64': 1.20260625.1 + '@cloudflare/workerd-darwin-arm64': 1.20260625.1 + '@cloudflare/workerd-linux-64': 1.20260625.1 + '@cloudflare/workerd-linux-arm64': 1.20260625.1 + '@cloudflare/workerd-windows-64': 1.20260625.1 - wrangler@4.103.0: + wrangler@4.105.0(@cloudflare/workers-types@4.20260627.1): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260617.1) + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260625.1) + '@cloudflare/workers-types': 4.20260627.1 blake3-wasm: 2.1.5 esbuild: 0.28.1 - miniflare: 4.20260617.1 + miniflare: 4.20260625.0 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 - workerd: 1.20260617.1 + workerd: 1.20260625.1 optionalDependencies: fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - utf-8-validate - ws@8.20.1: {} + wrap-ansi-cjs@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + optional: true + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + optional: true + + wrappy@1.0.2: + optional: true ws@8.21.0: {} + xml-naming@0.1.0: + optional: true + + xtend@4.0.2: + optional: true + + y18n@5.0.8: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: + optional: true + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3 @@ -2622,4 +6549,10 @@ snapshots: cookie: 1.1.1 youch-core: 0.3.3 + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} + + zod@4.4.3: {} diff --git a/package.json b/package.json index ff831f6..c8fb101 100644 --- a/package.json +++ b/package.json @@ -7,25 +7,39 @@ "dev": "wrangler dev", "test:unit": "vitest run --config vitest.config.ts", "test:compat": "vitest run --config vitest.compat.config.ts", - "test": "nub run test:unit && nub run test:compat", + "test:py": "node test/run-py.mjs", + "test": "nub run test:unit && nub run test:compat && nub run test:py", "lint": "tsc --noEmit && biome check --write --unsafe .", "prepare": "lefthook install" }, "devDependencies": { - "@anthropic-ai/sdk": "^0.104.1", - "@biomejs/biome": "^2.5.0", - "@cloudflare/vitest-pool-workers": "^0.16.18", - "@cloudflare/workers-types": "^4.20260615.1", - "@google/genai": "^2.8.0", + "@ai-sdk/anthropic": "^4.0.0", + "@ai-sdk/google": "^4.0.1", + "@ai-sdk/openai": "^4.0.1", + "@anthropic-ai/sdk": "^0.106.0", + "@biomejs/biome": "^2.5.1", + "@cloudflare/vitest-pool-workers": "^0.16.20", + "@cloudflare/workers-types": "^4.20260627.1", + "@genkit-ai/google-genai": "^1.39.0", + "@google/genai": "^2.10.0", + "@langchain/anthropic": "^1.5.1", + "@langchain/core": "^1.2.1", + "@langchain/google-genai": "^2.2.0", + "@langchain/openai": "^1.5.3", + "@types/ws": "^8.18.1", + "ai": "^7.0.3", + "genkit": "^1.39.0", "lefthook": "^2.1.9", - "openai": "^6.42.0", - "vitest": "^4.1.9" + "openai": "^6.45.0", + "vitest": "^4.1.9", + "ws": "^8.21.0", + "zod": "^4.4.3" }, "peerDependencies": { "typescript": "^6.0.3" }, "dependencies": { - "hono": "^4.12.25", - "wrangler": "^4.100.0" + "hono": "^4.12.27", + "wrangler": "^4.105.0" } } diff --git a/src/admin/index.ts b/src/admin/index.ts index 83c2990..a6ab8c1 100644 --- a/src/admin/index.ts +++ b/src/admin/index.ts @@ -114,10 +114,18 @@ app.post("/api/tokens", async (c) => { const fd = await c.req.formData(); const providers = parseProviders(fd); const custom = fd.get("token"); + const rawExp = String(fd.get("expiresAt") || "").trim(); + let expiresAt: string | undefined; + if (rawExp) { + const d = new Date(rawExp); // datetime-local is local time; toISOString normalizes to UTC + if (Number.isNaN(d.getTime())) return c.text("invalid expiry", 400); + expiresAt = d.toISOString(); + } const { token } = await createToken(c.env.TOKENS, { label: String(fd.get("label") || ""), providers: providers.length ? providers : ["openai"], token: custom ? String(custom) : undefined, + expiresAt, }); return c.html(createdNotice(token), 200, { "HX-Trigger": "tokens-changed" }); }); diff --git a/src/admin/views.ts b/src/admin/views.ts index 0e16237..f5a033c 100644 --- a/src/admin/views.ts +++ b/src/admin/views.ts @@ -14,7 +14,7 @@ h1{font-size:20px;font-weight:600;margin:0 0 20px} .card{background:#15151c;border:1px solid #24242e;border-radius:12px;padding:18px;margin:0 0 18px} .card h2{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#9a9aa6;margin:0 0 14px} label{display:block;font-size:12px;color:#9a9aa6;margin:0 0 4px} -input[type=text],input[type=password]{width:100%;background:#0e0e14;border:1px solid #2a2a36;border-radius:8px;color:#e7e7ea;padding:9px 11px;font:inherit} +input[type=text],input[type=password],input[type=datetime-local]{width:100%;background:#0e0e14;border:1px solid #2a2a36;border-radius:8px;color:#e7e7ea;padding:9px 11px;font:inherit} .row{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end} .row>div{flex:1;min-width:160px} .checks{display:flex;gap:14px;margin:12px 0} @@ -42,12 +42,15 @@ const timeAgo = (iso?: string) => { return iso.slice(0, 10); }; -export const tokenRow = (r: Row) => html` - +export const tokenRow = (r: Row) => { + const expired = !!r.expiresAt && Date.parse(r.expiresAt) <= Date.now(); + return html` + ${r.label || "(no label)"} …${r.last4} ${providerPills(r.providers)} ${r.status} + ${expired ? html`expired` : html`${r.expiresAt ? r.expiresAt.slice(0, 10) : "never"}`} ${timeAgo(r.lastUsed)} - -`; + `; +}; export const tokenTable = (rows: Row[]) => html` @@ -80,12 +83,13 @@ export const tokenTable = (rows: Row[]) => html` + - ${rows.length ? rows.map(tokenRow) : html``} + ${rows.length ? rows.map(tokenRow) : html``}
Token Providers StatusExpires Last used
No tokens yet.
No tokens yet.
`; @@ -154,6 +158,10 @@ export const dashboardPage = () => html` +
+ + +
@@ -167,7 +175,7 @@ export const dashboardPage = () => html`

Tokens

-
+
Loading…
diff --git a/src/index.ts b/src/index.ts index 0e9092a..42db63a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,21 @@ import adminApp from "./admin"; import { handleProxy } from "./proxy"; import type { Env } from "./types"; +import { handleWsProxy } from "./ws"; export { UsEgress } from "./egress"; -// Top-level dispatch: /admin/* -> admin sub-app (isolated in try/catch so an admin -// bug can never crash the proxy branch), everything else -> the proxy hot-path. +// Top-level dispatch: a WebSocket upgrade -> the wss proxy; /admin/* -> admin sub-app (isolated +// in try/catch so an admin bug can never crash the proxy branch); everything else -> the proxy +// hot-path. export default { async fetch( req: Request, env: Env, ctx: ExecutionContext, ): Promise { + if (req.headers.get("upgrade")?.toLowerCase() === "websocket") + return handleWsProxy(req, env, ctx); const url = new URL(req.url); if (url.pathname === "/admin" || url.pathname.startsWith("/admin/")) { try { diff --git a/src/proxy.ts b/src/proxy.ts index b143a11..f8cb360 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -85,7 +85,7 @@ function errorResponse(status: number, error: string): Response { const EGRESS_POOL = 8; /** OpenAI 403s requests that egress from an unsupported region (e.g. the Hong Kong colo). */ -async function isGeoBlock(res: Response): Promise { +export async function isGeoBlock(res: Response): Promise { if (res.status !== 403) return false; try { return (await res.clone().text()).includes( @@ -97,18 +97,61 @@ async function isGeoBlock(res: Response): Promise { } /** A North-America-pinned egress stub, so its fetch() leaves from an OpenAI-supported region. */ -function egressStub(env: Env): DurableObjectStub { +export function egressStub(env: Env): DurableObjectStub { const id = env.US_EGRESS.idFromName( `oa-egress-${Math.floor(Math.random() * EGRESS_POOL)}`, ); return env.US_EGRESS.get(id, { locationHint: "wnam" }); } -/** Validate the doppelganger token, swap in the real key, forward to the upstream, stream back. */ +// Headers a browser must be told to expose so the Gemini resumable-upload flow works +// (the client reads x-goog-upload-url, then uploads bytes straight to Google). +const EXPOSE_HEADERS = + "x-goog-upload-url, x-goog-upload-status, x-goog-upload-chunk-granularity"; + +/** Reflect the caller's Origin so browser SDKs can read the response. No-op for + * non-browser callers (no Origin). The real key never rides on any CORS path. */ +function withCors(res: Response, req: Request): Response { + const origin = req.headers.get("origin"); + if (origin) { + res.headers.set("access-control-allow-origin", origin); + res.headers.append("vary", "Origin"); + res.headers.set("access-control-expose-headers", EXPOSE_HEADERS); + } + return res; +} + +/** Answer the browser preflight. OPTIONS carries no auth header, so it must be handled + * before the token checks - otherwise every browser SDK's preflight 401s. */ +function corsPreflight(req: Request): Response { + const res = new Response(null, { status: 204 }); + res.headers.set( + "access-control-allow-methods", + "GET, POST, PUT, DELETE, OPTIONS", + ); + res.headers.set( + "access-control-allow-headers", + req.headers.get("access-control-request-headers") || "*", + ); + res.headers.set("access-control-max-age", "86400"); + return withCors(res, req); +} + +/** Top-level proxy entry: CORS preflight, then reflect Origin onto every response. */ export async function handleProxy( req: Request, env: Env, ctx: ExecutionContext, +): Promise { + if (req.method === "OPTIONS") return corsPreflight(req); + return withCors(await proxyRequest(req, env, ctx), req); +} + +/** Validate the proxy token, swap in the real key, forward to the upstream, stream back. */ +async function proxyRequest( + req: Request, + env: Env, + ctx: ExecutionContext, ): Promise { const url = new URL(req.url); const token = extractToken(req, url); @@ -121,6 +164,21 @@ export async function handleProxy( if (!meta.providers.includes(coarse(provider))) return errorResponse(403, "token not allowed for provider"); + // Per-token rate limit, keyed on the hash (in-process binding, not a subrequest). + // Fail-open: a missing or erroring limiter must never brick the proxy. The binding + // counts per-colo, so it is a loose ceiling, not strict abuse prevention. + let allowed = true; + try { + allowed = (await env.RATE_LIMITER.limit({ key: hash })).success; + } catch { + allowed = true; + } + if (!allowed) { + const res = errorResponse(429, "rate limit exceeded"); + res.headers.set("retry-after", "60"); + return res; + } + const realKey = realKeyFor(provider, env); rewriteToUpstream(url, provider, env); if (provider === "gemini" || provider === "gemini-openai") diff --git a/src/tokens.ts b/src/tokens.ts index 29c830f..ac42187 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -18,15 +18,16 @@ function base64url(bytes: Uint8Array): string { return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } -/** A fresh opaque token: dgk_ + 32 url-safe chars (24 random bytes). */ +/** A fresh opaque token: ptk_ + 32 url-safe chars (24 random bytes). */ export function generateToken(): string { - return `dgk_${base64url(crypto.getRandomValues(new Uint8Array(24)))}`; + return `ptk_${base64url(crypto.getRandomValues(new Uint8Array(24)))}`; } export interface CreateInput { label: string; providers: CoarseProvider[]; token?: string; // admin-typed; otherwise generated + expiresAt?: string; // ISO (UTC); absent = never expires } export async function createToken( @@ -41,6 +42,7 @@ export async function createToken( providers: input.providers, status: "active", createdAt: new Date().toISOString(), + ...(input.expiresAt ? { expiresAt: input.expiresAt } : {}), }; await kv.put(hash, JSON.stringify(meta)); return { token, hash, meta }; @@ -67,7 +69,12 @@ export async function getValidatedByHash( hash: string, ): Promise { const meta = parseMeta(await kv.get(hash)); - return meta?.status === "active" ? meta : null; + if (meta?.status !== "active") return null; + if (meta.expiresAt) { + const t = Date.parse(meta.expiresAt); + if (Number.isNaN(t) || t <= Date.now()) return null; // fail-closed on bad/past + } + return meta; } /** Resolve a plaintext token to its metadata, only if it exists and is active. */ diff --git a/src/types.ts b/src/types.ts index e16c3b8..6671480 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,13 +7,15 @@ export interface TokenMetadata { providers: CoarseProvider[]; status: "active" | "disabled"; createdAt: string; // ISO + expiresAt?: string; // ISO (UTC); absent = never expires // lastUsed is stored in a separate `:lu` key (see tokens.ts), not here. - // reserved for Later (absent in v1): expiresAt, limits, spend + // reserved for Later: limits, spend } export interface Env { TOKENS: KVNamespace; US_EGRESS: DurableObjectNamespace; // North-America-pinned egress relay (see egress.ts) + RATE_LIMITER: RateLimit; // per-token RPM limiter (Workers Rate Limiting binding) OPENAI_API_KEY: string; ANTHROPIC_API_KEY: string; GEMINI_API_KEY: string; diff --git a/src/ws.ts b/src/ws.ts new file mode 100644 index 0000000..d5bfeea --- /dev/null +++ b/src/ws.ts @@ -0,0 +1,218 @@ +// The WebSocket proxy hot-path. Mirrors proxy.ts (validate the token -> swap in the real key -> +// forward), but for a WS upgrade handshake plus a bidirectional frame pump. Anthropic has no wss +// API today; OpenAI (`/v1/realtime`, `/v1/responses`) and Gemini Live (`...BidiGenerateContent`) +// do. Auth on a WS handshake can sit in a header, a `?key=` query param, or - because a browser +// WebSocket cannot set headers - the `Sec-WebSocket-Protocol` subprotocol. + +import { + coarse, + egressStub, + extractToken, + isGeoBlock, + realKeyFor, + routeProvider, +} from "./proxy"; +import { getValidatedByHash, sha256hex, touchLastUsed } from "./tokens"; +import type { Env, Provider } from "./types"; +import { rewriteToUpstream } from "./upstreams"; + +// OpenAI browser clients smuggle the key as a Sec-WebSocket-Protocol entry, since a browser +// WebSocket cannot set the Authorization header. Offered shape: ["realtime", +// "openai-insecure-api-key.", "openai-organization."?, "openai-project."?, +// "openai-beta.realtime-v1"?]. We read the key here and re-present it as a Bearer header upstream +// (the worker CAN set headers), keeping the remaining subprotocols so the handshake still +// negotiates "realtime". +const OPENAI_KEY_SUBPROTOCOL = "openai-insecure-api-key."; + +// Close codes a peer is not allowed to send back via close(); forward as a bare close() instead. +const CLOSE_FORBIDDEN = new Set([1004, 1005, 1006, 1015]); + +/** The proxy token from the subprotocol list, if a browser smuggled it there. */ +function subprotocolToken(header: string | null): string | null { + if (!header) return null; + for (const part of header.split(",")) { + const v = part.trim(); + if (v.startsWith(OPENAI_KEY_SUBPROTOCOL)) + return v.slice(OPENAI_KEY_SUBPROTOCOL.length) || null; + } + return null; +} + +/** Proxy token from any WS auth slot: the HTTP slots (header/query) plus the subprotocol. */ +export function extractWsToken(req: Request, url: URL): string | null { + return ( + extractToken(req, url) ?? + subprotocolToken(req.headers.get("sec-websocket-protocol")) + ); +} + +/** Provider from any WS auth slot. A subprotocol-smuggled key is always OpenAI. */ +export function routeWsProvider(req: Request, url: URL): Provider | null { + return ( + routeProvider(req, url) ?? + (subprotocolToken(req.headers.get("sec-websocket-protocol")) + ? "openai" + : null) + ); +} + +/** Rewrite the URL to the upstream and build the upgrade headers with the real key in the slot + * that provider's WS API expects. The proxy token leaves no slot. Pure + exported for tests. */ +export function prepareWsUpstream( + req: Request, + url: URL, + provider: Provider, + realKey: string, + env: Env, +): { target: string; headers: Headers } { + // Rewrite host/port (path + query kept). The scheme stays http(s): a Worker opens an upstream + // socket by fetching the http(s) URL with `Upgrade: websocket`, not by using a ws:// URL. + rewriteToUpstream(url, provider, env); + + const headers = new Headers(req.headers); + // Strip every inbound auth slot, then set exactly one upstream (the WS analogue of swapAuth). + headers.delete("x-api-key"); + headers.delete("x-goog-api-key"); + headers.delete("authorization"); + // The handshake headers are runtime-owned; drop the client's so CF regenerates them for the + // upstream leg, and signal upgrade intent explicitly. + headers.delete("sec-websocket-key"); + headers.delete("sec-websocket-version"); + headers.delete("sec-websocket-accept"); + headers.set("upgrade", "websocket"); + + if (provider === "gemini") { + url.searchParams.set("key", realKey); // Gemini Live takes the key in the query + } else { + url.searchParams.delete("key"); + if (provider === "anthropic") headers.set("x-api-key", realKey); + else headers.set("authorization", `Bearer ${realKey}`); // openai + gemini-openai + } + + // Drop the smuggled key entry from the subprotocol offer, keep the rest (realtime, org, + // project, beta). Remove the header entirely if nothing else remains. + const proto = headers.get("sec-websocket-protocol"); + if (proto) { + const kept = proto + .split(",") + .map((s) => s.trim()) + .filter((s) => s && !s.startsWith(OPENAI_KEY_SUBPROTOCOL)); + if (kept.length) headers.set("sec-websocket-protocol", kept.join(", ")); + else headers.delete("sec-websocket-protocol"); + } + + return { target: url.toString(), headers }; +} + +/** The close code to forward to the peer, or null to send a bare close(): a peer may not send the + * reserved/abnormal codes in CLOSE_FORBIDDEN or anything outside 1000-4999. */ +export function forwardCloseCode(code: number): number | null { + return code >= 1000 && code <= 4999 && !CLOSE_FORBIDDEN.has(code) + ? code + : null; +} + +/** Forward frames + a sanitized close/error from one socket to the other. */ +function pump(from: WebSocket, to: WebSocket): void { + from.addEventListener("message", (e: MessageEvent) => { + try { + to.send(e.data as string | ArrayBuffer); + } catch {} + }); + from.addEventListener("close", (e: CloseEvent) => { + try { + const code = forwardCloseCode(e.code); + if (code !== null) to.close(code, e.reason); + else to.close(); + } catch {} + }); + from.addEventListener("error", () => { + try { + to.close(1011, "upstream error"); + } catch {} + }); +} + +/** Validate the token, swap in the real key, open the upstream socket, pipe both ends. */ +export async function handleWsProxy( + req: Request, + env: Env, + ctx: ExecutionContext, +): Promise { + const url = new URL(req.url); + const token = extractWsToken(req, url); + const provider = routeWsProvider(req, url); + if (!token || !provider) + return new Response("missing token", { status: 401 }); + + const hash = await sha256hex(token); + const meta = await getValidatedByHash(env.TOKENS, hash); + if (!meta) return new Response("invalid or revoked token", { status: 401 }); + if (!meta.providers.includes(coarse(provider))) + return new Response("token not allowed for provider", { status: 403 }); + + // Per-token rate limit, same fail-open binding as the HTTP path. Gates the connection, not each + // frame: one upgrade = one limiter hit. + let allowed = true; + try { + allowed = (await env.RATE_LIMITER.limit({ key: hash })).success; + } catch { + allowed = true; + } + if (!allowed) + return new Response("rate limit exceeded", { + status: 429, + headers: { "retry-after": "60" }, + }); + + const realKey = realKeyFor(provider, env); + const { target, headers } = prepareWsUpstream( + req, + url, + provider, + realKey, + env, + ); + + let upstreamRes: Response; + try { + upstreamRes = await fetch(target, { headers }); + // Same OpenAI geo-403 escape hatch as HTTP: a 403 from a bad colo is retried from the + // NA-pinned egress DO, which carries the WS upgrade just like a plain fetch. + if (coarse(provider) === "openai" && (await isGeoBlock(upstreamRes))) + upstreamRes = await egressStub(env).fetch( + new Request(target, { headers }), + ); + } catch { + return new Response("upstream connect failed", { status: 502 }); + } + + const upstream = upstreamRes.webSocket; + if (!upstream) + // Upstream refused the upgrade (401/403/426/...). Surface its handshake response so the + // client sees the real error rather than a generic 502. + return new Response(upstreamRes.body, { + status: upstreamRes.status, + statusText: upstreamRes.statusText, + headers: upstreamRes.headers, + }); + + const [client, server] = Object.values(new WebSocketPair()); + // Keep binary frames as ArrayBuffer regardless of compatibility_date: newer dates deliver them as + // Blob, which workerd's send() rejects - a forwarded realtime audio frame would silently drop. + upstream.binaryType = "arraybuffer"; + server.binaryType = "arraybuffer"; + upstream.accept(); + server.accept(); + pump(server, upstream); + pump(upstream, server); + + ctx.waitUntil(touchLastUsed(env.TOKENS, hash)); + + const res = new Response(null, { status: 101, webSocket: client }); + // Echo the subprotocol the upstream actually chose (e.g. "realtime"); a browser handshake + // fails if the server doesn't pick one of the client's offered subprotocols. + const negotiated = upstreamRes.headers.get("sec-websocket-protocol"); + if (negotiated) res.headers.set("sec-websocket-protocol", negotiated); + return res; +} diff --git a/test/proxy-handler.test.ts b/test/proxy-handler.test.ts index afaf740..60ca4b7 100644 --- a/test/proxy-handler.test.ts +++ b/test/proxy-handler.test.ts @@ -174,16 +174,16 @@ describe("auth failures (upstream never called)", () => { }); describe("security invariant", () => { - it("never forwards the doppelganger token upstream", async () => { + it("never forwards the proxy token upstream", async () => { await createToken(env.TOKENS, { label: "sec", providers: ["openai"], - token: "SECRET-DOPPEL", + token: "SECRET-TOKEN", }); await call( new Request("https://proxy.example/v1/chat/completions", { method: "POST", - headers: { authorization: "Bearer SECRET-DOPPEL" }, + headers: { authorization: "Bearer SECRET-TOKEN" }, body: "{}", }), ); @@ -192,7 +192,7 @@ describe("security invariant", () => { captured!.headers.get("x-api-key"), captured!.headers.get("x-goog-api-key"), ].join("|"); - expect(slots).not.toContain("SECRET-DOPPEL"); + expect(slots).not.toContain("SECRET-TOKEN"); }); }); @@ -332,3 +332,113 @@ describe("SSE passthrough", () => { expect(text).toContain("[DONE]"); }); }); + +describe("CORS", () => { + it("answers the preflight OPTIONS without auth and never calls upstream", async () => { + const res = await call( + new Request("https://proxy.example/v1/messages", { + method: "OPTIONS", + headers: { + origin: "https://app.example", + "access-control-request-headers": "x-api-key, content-type", + }, + }), + ); + expect(res.status).toBe(204); + expect(res.headers.get("access-control-allow-origin")).toBe( + "https://app.example", + ); + expect(res.headers.get("access-control-allow-methods")).toContain("POST"); + expect(res.headers.get("access-control-allow-headers")).toBe( + "x-api-key, content-type", + ); + expect(res.headers.get("access-control-max-age")).toBe("86400"); + expect(captured).toBeNull(); + }); + + it("reflects Origin and exposes the Gemini upload headers on a proxied response", async () => { + await seed("tk-cors", ["anthropic"]); + const res = await call( + new Request("https://proxy.example/v1/messages", { + method: "POST", + headers: { "x-api-key": "tk-cors", origin: "https://app.example" }, + body: "{}", + }), + ); + expect(res.status).toBe(200); + expect(res.headers.get("access-control-allow-origin")).toBe( + "https://app.example", + ); + expect(res.headers.get("access-control-expose-headers")).toContain( + "x-goog-upload-url", + ); + }); + + it("omits CORS headers when no Origin is sent (server-side callers)", async () => { + await seed("tk-nocors", ["anthropic"]); + const res = await call( + new Request("https://proxy.example/v1/messages", { + method: "POST", + headers: { "x-api-key": "tk-nocors" }, + body: "{}", + }), + ); + expect(res.status).toBe(200); + expect(res.headers.get("access-control-allow-origin")).toBeNull(); + }); +}); + +describe("rate limiting", () => { + const real = (env as { RATE_LIMITER?: unknown }).RATE_LIMITER; + afterEach(() => { + (env as { RATE_LIMITER?: unknown }).RATE_LIMITER = real; + }); + const setLimiter = ( + limit: (o: { key: string }) => Promise<{ success: boolean }>, + ) => { + (env as { RATE_LIMITER: unknown }).RATE_LIMITER = { limit }; + }; + + it("429s with Retry-After when the limiter denies, without calling upstream", async () => { + await seed("tk-rl", ["anthropic"]); + setLimiter(async () => ({ success: false })); + const res = await call( + new Request("https://proxy.example/v1/messages", { + method: "POST", + headers: { "x-api-key": "tk-rl" }, + body: "{}", + }), + ); + expect(res.status).toBe(429); + expect(res.headers.get("retry-after")).toBe("60"); + expect(captured).toBeNull(); + }); + + it("forwards when the limiter allows", async () => { + await seed("tk-rl-ok", ["anthropic"]); + setLimiter(async () => ({ success: true })); + const res = await call( + new Request("https://proxy.example/v1/messages", { + method: "POST", + headers: { "x-api-key": "tk-rl-ok" }, + body: "{}", + }), + ); + expect(res.status).toBe(200); + }); + + it("fails open (forwards) when the limiter throws", async () => { + await seed("tk-rl-err", ["anthropic"]); + setLimiter(async () => { + throw new Error("limiter down"); + }); + const res = await call( + new Request("https://proxy.example/v1/messages", { + method: "POST", + headers: { "x-api-key": "tk-rl-err" }, + body: "{}", + }), + ); + expect(res.status).toBe(200); + }); +}); diff --git a/test/proxy.test.ts b/test/proxy.test.ts index 87d0f38..0dc9ff6 100644 --- a/test/proxy.test.ts +++ b/test/proxy.test.ts @@ -103,9 +103,9 @@ describe("coarse", () => { describe("swapAuth", () => { it("sets bearer for openai and strips the other slots", () => { const h = new Headers({ - authorization: "Bearer DOPPEL", - "x-api-key": "DOPPEL", - "x-goog-api-key": "DOPPEL", + authorization: "Bearer PROXY-TOKEN", + "x-api-key": "PROXY-TOKEN", + "x-goog-api-key": "PROXY-TOKEN", }); swapAuth(h, "openai", "REALKEY"); expect(h.get("authorization")).toBe("Bearer REALKEY"); @@ -114,8 +114,8 @@ describe("swapAuth", () => { }); it("sets x-api-key for anthropic and strips the other slots", () => { const h = new Headers({ - authorization: "Bearer DOPPEL", - "x-api-key": "DOPPEL", + authorization: "Bearer PROXY-TOKEN", + "x-api-key": "PROXY-TOKEN", }); swapAuth(h, "anthropic", "REALKEY"); expect(h.get("x-api-key")).toBe("REALKEY"); @@ -123,20 +123,20 @@ describe("swapAuth", () => { expect(h.get("x-goog-api-key")).toBeNull(); }); it("sets x-goog-api-key for gemini", () => { - const h = new Headers({ "x-goog-api-key": "DOPPEL" }); + const h = new Headers({ "x-goog-api-key": "PROXY-TOKEN" }); swapAuth(h, "gemini", "REALKEY"); expect(h.get("x-goog-api-key")).toBe("REALKEY"); }); it("sets bearer for gemini-openai", () => { - const h = new Headers({ authorization: "Bearer DOPPEL" }); + const h = new Headers({ authorization: "Bearer PROXY-TOKEN" }); swapAuth(h, "gemini-openai", "REALKEY"); expect(h.get("authorization")).toBe("Bearer REALKEY"); }); - it("never leaves the doppelganger token in any auth header", () => { + it("never leaves the proxy token in any auth header", () => { const h = new Headers({ - "x-api-key": "DOPPEL", - authorization: "Bearer DOPPEL", - "x-goog-api-key": "DOPPEL", + "x-api-key": "PROXY-TOKEN", + authorization: "Bearer PROXY-TOKEN", + "x-goog-api-key": "PROXY-TOKEN", }); swapAuth(h, "anthropic", "REALKEY"); const all = [ @@ -144,7 +144,7 @@ describe("swapAuth", () => { h.get("x-api-key"), h.get("x-goog-api-key"), ].join("|"); - expect(all).not.toContain("DOPPEL"); + expect(all).not.toContain("PROXY-TOKEN"); }); }); diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 0000000..f4f713f --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1,13 @@ +# Python deps for the separate-runner compat tests in test/sdk-compat/*.py. Not part of the Node toolchain. +# Pinned with ~= to lock the minor version: these libs shift default endpoints / base-URL handling +# across minors, which is exactly what the compat tests encode. Bump deliberately, then re-run. +# Setup (uv): uv venv && uv pip install -r test/requirements.txt +litellm~=1.89.3 +instructor~=1.15.3 +pydantic~=2.13.4 +openai~=2.43.0 +pydantic-ai-slim[openai]~=1.107.0 +llama-index-core~=0.14.22 +llama-index-llms-openai~=0.7.9 +llama-index-llms-anthropic~=0.11.6 +llama-index-llms-google-genai~=0.9.5 diff --git a/test/run-py.mjs b/test/run-py.mjs new file mode 100644 index 0000000..e2d449a --- /dev/null +++ b/test/run-py.mjs @@ -0,0 +1,270 @@ +import { spawn } from "node:child_process"; +import { existsSync, readdirSync } from "node:fs"; +import http from "node:http"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { unstable_dev } from "wrangler"; + +const repo = join(dirname(fileURLToPath(import.meta.url)), ".."); +const py = + process.platform === "win32" + ? join(repo, ".venv", "Scripts", "python.exe") + : join(repo, ".venv", "bin", "python"); +const dir = join(repo, "test", "sdk-compat"); + +if (!existsSync(py)) { + console.log( + "[py] .venv not found - skipping. Setup: uv venv && uv pip install -r test/requirements.txt", + ); + process.exit(0); +} + +const pyFiles = readdirSync(dir) + .filter((f) => f.endsWith(".py")) + .sort(); +if (pyFiles.length === 0) process.exit(0); + +const FAKE = { + openai: "FAKE-OPENAI-KEY", + anthropic: "FAKE-ANTHROPIC-KEY", + gemini: "FAKE-GEMINI-KEY", +}; +const ADMIN_SECRET = "compat-admin-secret"; +const TOKEN = "tk-py"; +const PER_FILE_TIMEOUT_MS = 90_000; + +// --- mock upstream (mirrors test/sdk-compat/setup.ts, plus control endpoints) --- +let captured = null; + +function providerFromPath(path) { + if (path.includes("/v1beta/openai/")) return "openai"; + if ( + path.includes(":generateContent") || + path.includes(":streamGenerateContent") || + path.startsWith("/v1beta/") + ) + return "gemini"; + if (path.includes("/v1/messages")) return "anthropic"; + return "openai"; +} + +function bodyFor(provider) { + if (provider === "anthropic") + return { + id: "msg_1", + type: "message", + role: "assistant", + model: "x", + content: [{ type: "text", text: "hi" }], + stop_reason: "end_turn", + usage: { input_tokens: 1, output_tokens: 1 }, + }; + if (provider === "gemini") + return { + candidates: [ + { + content: { parts: [{ text: "hi" }], role: "model" }, + finishReason: "STOP", + }, + ], + usageMetadata: { + promptTokenCount: 1, + candidatesTokenCount: 1, + totalTokenCount: 2, + }, + }; + return { + id: "chatcmpl_1", + object: "chat.completion", + created: 0, + model: "x", + choices: [ + { + index: 0, + message: { role: "assistant", content: "hi" }, + finish_reason: "stop", + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }; +} + +function writeSse(res, provider) { + res.writeHead(200, { + "content-type": "text/event-stream", + "cache-control": "no-cache", + }); + if (provider === "anthropic") { + res.write( + 'event: message_start\ndata: {"type":"message_start","message":{"id":"m","type":"message","role":"assistant","model":"x","content":[],"stop_reason":null,"usage":{"input_tokens":1,"output_tokens":0}}}\n\n', + ); + res.write( + 'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n', + ); + res.write( + 'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}\n\n', + ); + res.write( + 'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n', + ); + res.write( + 'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":1}}\n\n', + ); + res.write('event: message_stop\ndata: {"type":"message_stop"}\n\n'); + } else if (provider === "gemini") { + res.write( + 'data: {"candidates":[{"content":{"parts":[{"text":"hi"}],"role":"model"},"finishReason":"STOP"}]}\n\n', + ); + } else { + res.write( + 'data: {"id":"x","object":"chat.completion.chunk","created":0,"model":"x","choices":[{"index":0,"delta":{"role":"assistant","content":"hi"},"finish_reason":null}]}\n\n', + ); + res.write( + 'data: {"id":"x","object":"chat.completion.chunk","created":0,"model":"x","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', + ); + res.write("data: [DONE]\n\n"); + } + res.end(); +} + +function startMock() { + return new Promise((resolve) => { + const server = http.createServer((req, res) => { + const path = req.url ?? ""; + // control endpoints - the Python test reads/clears the capture over HTTP + if (path === "/__captured") { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(captured)); + return; + } + if (path === "/__reset") { + captured = null; + res.writeHead(204); + res.end(); + return; + } + const chunks = []; + req.on("data", (c) => chunks.push(c)); + req.on("end", () => { + const body = Buffer.concat(chunks).toString(); + captured = { + method: req.method ?? "", + path, + headers: req.headers, + body, + }; + const provider = providerFromPath(path); + const stream = + path.includes("streamGenerateContent") || + /[?&]alt=sse/.test(path) || + /"stream"\s*:\s*true/.test(body); + if (stream) return writeSse(res, provider); + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(bodyFor(provider))); + }); + }); + server.listen(0, "127.0.0.1", () => { + const { port } = server.address(); + resolve({ url: `http://127.0.0.1:${port}`, close: () => server.close() }); + }); + }); +} + +async function seedToken(workerUrl) { + const login = await fetch(`${workerUrl}/admin/login`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ password: ADMIN_SECRET }).toString(), + }); + if (login.status !== 200) + throw new Error(`admin login failed: ${login.status}`); + const cookie = (login.headers.get("set-cookie") ?? "").split(";")[0]; + const form = new URLSearchParams({ label: TOKEN, token: TOKEN }); + for (const p of ["openai", "anthropic", "gemini"]) + form.append("providers", p); + const res = await fetch(`${workerUrl}/admin/api/tokens`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded", cookie }, + body: form.toString(), + }); + if (res.status !== 200) throw new Error(`seed token failed: ${res.status}`); +} + +const mock = await startMock(); +const worker = await unstable_dev("src/index.ts", { + config: "wrangler.toml", + local: true, + vars: { + OPENAI_API_KEY: FAKE.openai, + ANTHROPIC_API_KEY: FAKE.anthropic, + GEMINI_API_KEY: FAKE.gemini, + ADMIN_SECRET, + OPENAI_UPSTREAM: mock.url, + ANTHROPIC_UPSTREAM: mock.url, + GEMINI_UPSTREAM: mock.url, + }, + experimental: { disableExperimentalWarning: true }, +}); +// Normalize the bind address: unstable_dev can report 0.0.0.0 / :: (which Node's fetch +// tolerates but Python's HTTP clients do not, hanging on their long default timeout). +const host = + !worker.address || worker.address === "0.0.0.0" || worker.address === "::" + ? "127.0.0.1" + : worker.address; +const workerUrl = `http://${host}:${worker.port}`; +console.log(`[py] worker ${workerUrl} mock ${mock.url}`); + +let failed = 0; +try { + await seedToken(workerUrl); + const env = { + ...process.env, + PROXY_WORKER_URL: workerUrl, + PROXY_MOCK_URL: mock.url, + PROXY_TOKEN: TOKEN, + PROXY_FAKE_OPENAI: FAKE.openai, + PROXY_FAKE_ANTHROPIC: FAKE.anthropic, + PROXY_FAKE_GEMINI: FAKE.gemini, + }; + // The seeded proxy token is the ONLY key a client should use; strip any real provider key from + // the child env so a client that reads a key from the environment can't bypass the token path. + for (const k of [ + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "GOOGLE_GENERATIVE_AI_API_KEY", + ]) + delete env[k]; + for (const file of pyFiles) { + captured = null; + console.log(`[py] ${file}`); + // Async spawn (NOT spawnSync): the mock lives in this event loop, and spawnSync would freeze + // it for the whole child run - so the worker could never reach the mock and every test would + // hang. Await exit via a Promise, with a hard timeout that kills a stuck child. + const code = await new Promise((resolve) => { + const child = spawn(py, [join(dir, file)], { stdio: "inherit", env }); + const timer = setTimeout(() => { + console.error( + `[py] ${file} TIMED OUT after ${PER_FILE_TIMEOUT_MS / 1000}s`, + ); + child.kill("SIGKILL"); + }, PER_FILE_TIMEOUT_MS); + child.on("exit", (c) => { + clearTimeout(timer); + resolve(c ?? 1); + }); + child.on("error", (e) => { + clearTimeout(timer); + console.error(`[py] ${file} spawn error: ${e.message}`); + resolve(1); + }); + }); + if (code !== 0) failed++; + } +} finally { + await worker.stop(); + mock.close(); +} + +process.exit(failed ? 1 : 0); diff --git a/test/sdk-compat/ai-sdk-anthropic.ts b/test/sdk-compat/ai-sdk-anthropic.ts new file mode 100644 index 0000000..63ddc84 --- /dev/null +++ b/test/sdk-compat/ai-sdk-anthropic.ts @@ -0,0 +1,54 @@ +import { createAnthropic } from "@ai-sdk/anthropic"; +import { generateText } from "ai"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let baseURL: string; +const TOKEN = "compat-ai-sdk-anthropic-token"; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + baseURL = w.url; + await seedToken(baseURL, { + token: TOKEN, + providers: ["anthropic"], + label: "ai-sdk-anthropic", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +beforeEach(() => mock.reset()); + +// @ai-sdk/anthropic appends only `/messages`, so baseURL must include /v1. +const model = () => + createAnthropic({ baseURL: `${baseURL}/v1`, apiKey: TOKEN })("claude-x"); + +describe("@ai-sdk/anthropic (Vercel AI SDK) compatibility", () => { + it("forwards generateText with x-api-key swapped to the real key and the token absent", async () => { + const r = await generateText({ + model: model(), + prompt: "hi", + maxOutputTokens: 16, + }); + expect(r.text).toContain("hi"); + const cap = mock.last(); + expect(cap?.path).toBe("/v1/messages"); + expect(cap?.headers["x-api-key"]).toBe(FAKE.anthropic); + expect(JSON.stringify(cap?.headers)).not.toContain(TOKEN); + }); +}); diff --git a/test/sdk-compat/ai-sdk-google.ts b/test/sdk-compat/ai-sdk-google.ts new file mode 100644 index 0000000..e281eba --- /dev/null +++ b/test/sdk-compat/ai-sdk-google.ts @@ -0,0 +1,55 @@ +import { createGoogleGenerativeAI } from "@ai-sdk/google"; +import { generateText } from "ai"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let baseURL: string; +const TOKEN = "compat-ai-sdk-google-token"; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + baseURL = w.url; + await seedToken(baseURL, { + token: TOKEN, + providers: ["gemini"], + label: "ai-sdk-google", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +beforeEach(() => mock.reset()); + +// @ai-sdk/google appends /models/:generateContent, so baseURL must include /v1beta. +// It sends the key in the x-goog-api-key header (not ?key=), so it routes to the gemini slot. +const model = () => + createGoogleGenerativeAI({ baseURL: `${baseURL}/v1beta`, apiKey: TOKEN })( + "gemini-2.5-flash", + ); + +describe("@ai-sdk/google (Vercel AI SDK) compatibility", () => { + it("forwards generateText with x-goog-api-key swapped and the token absent", async () => { + const r = await generateText({ model: model(), prompt: "hi" }); + expect(r.text).toContain("hi"); + const cap = mock.last(); + expect(cap?.path).toContain( + "/v1beta/models/gemini-2.5-flash:generateContent", + ); + expect(cap?.headers["x-goog-api-key"]).toBe(FAKE.gemini); + expect(JSON.stringify(cap?.headers)).not.toContain(TOKEN); + }); +}); diff --git a/test/sdk-compat/ai-sdk-openai.ts b/test/sdk-compat/ai-sdk-openai.ts new file mode 100644 index 0000000..846afb5 --- /dev/null +++ b/test/sdk-compat/ai-sdk-openai.ts @@ -0,0 +1,50 @@ +import { createOpenAI } from "@ai-sdk/openai"; +import { generateText } from "ai"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let baseURL: string; +const TOKEN = "compat-ai-sdk-openai-token"; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + baseURL = w.url; + await seedToken(baseURL, { + token: TOKEN, + providers: ["openai"], + label: "ai-sdk-openai", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +beforeEach(() => mock.reset()); + +// `.chat()` forces Chat Completions; the bare factory would hit /v1/responses (AI SDK 5+ default). +const model = () => + createOpenAI({ baseURL: `${baseURL}/v1`, apiKey: TOKEN }).chat("gpt-x"); + +describe("@ai-sdk/openai (Vercel AI SDK) compatibility", () => { + it("forwards generateText with the real key swapped in and the token absent", async () => { + const r = await generateText({ model: model(), prompt: "hi" }); + expect(r.text).toContain("hi"); + const cap = mock.last(); + expect(cap?.path).toBe("/v1/chat/completions"); + expect(cap?.headers.authorization).toBe(`Bearer ${FAKE.openai}`); + expect(JSON.stringify(cap?.headers)).not.toContain(TOKEN); + }); +}); diff --git a/test/sdk-compat/anthropic.test.ts b/test/sdk-compat/anthropic-ai-sdk.ts similarity index 100% rename from test/sdk-compat/anthropic.test.ts rename to test/sdk-compat/anthropic-ai-sdk.ts diff --git a/test/sdk-compat/fetch.ts b/test/sdk-compat/fetch.ts new file mode 100644 index 0000000..adaa29d --- /dev/null +++ b/test/sdk-compat/fetch.ts @@ -0,0 +1,70 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +// Raw fetch (no SDK) - covers the two things no official SDK exercises: the Gemini +// `?key=` query-param auth slot, verbatim request-body forwarding, and the CORS preflight. +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let url: string; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + url = w.url; + await seedToken(url, { token: "tk-fetch", providers: ["gemini"] }); +}); +afterAll(async () => { + await worker.stop(); + await mock.close(); +}); +beforeEach(() => mock.reset()); + +describe("raw fetch (no SDK)", () => { + it("routes the Gemini ?key= slot, strips the token, swaps the real key, forwards body verbatim", async () => { + const body = JSON.stringify({ + contents: [{ parts: [{ text: "ping-verbatim-42" }] }], + }); + const res = await fetch( + `${url}/v1beta/models/gemini-x:generateContent?key=tk-fetch&foo=bar`, + { method: "POST", headers: { "content-type": "application/json" }, body }, + ); + expect(res.status).toBe(200); + + const cap = mock.last(); + expect(cap).not.toBeNull(); + // real key swapped into the header slot + expect(cap?.headers["x-goog-api-key"]).toBe(FAKE.gemini); + // the ?key= token is stripped from the forwarded query; other params survive + expect(cap?.path).not.toContain("tk-fetch"); + expect(cap?.path).not.toContain("key="); + expect(cap?.path).toContain("foo=bar"); + // the token never appears in any outbound header + expect(JSON.stringify(cap?.headers)).not.toContain("tk-fetch"); + // request body forwarded byte-for-byte + expect(cap?.body).toBe(body); + }); + + it("answers a CORS preflight (OPTIONS) at the edge without a token or upstream call", async () => { + const res = await fetch(`${url}/v1/messages`, { + method: "OPTIONS", + headers: { + origin: "https://app.example", + "access-control-request-headers": "x-api-key, content-type", + }, + }); + expect(res.status).toBe(204); + expect(res.headers.get("access-control-allow-origin")).toBe( + "https://app.example", + ); + expect(res.headers.get("access-control-allow-methods")).toContain("POST"); + expect(mock.last()).toBeNull(); + }); +}); diff --git a/test/sdk-compat/genkit.ts b/test/sdk-compat/genkit.ts new file mode 100644 index 0000000..d639ef5 --- /dev/null +++ b/test/sdk-compat/genkit.ts @@ -0,0 +1,56 @@ +import { googleAI } from "@genkit-ai/google-genai"; +import { genkit } from "genkit"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let baseURL: string; +const TOKEN = "compat-genkit-token"; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + baseURL = w.url; + await seedToken(baseURL, { + token: TOKEN, + providers: ["gemini"], + label: "genkit", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +beforeEach(() => mock.reset()); + +// googleAI({ baseUrl }) is the bare host; the plugin builds `${baseUrl}/v1beta/models/:...` +// and sends the key in the x-goog-api-key header. +const ai = () => + genkit({ plugins: [googleAI({ apiKey: TOKEN, baseUrl: baseURL })] }); + +describe("genkit (@genkit-ai/google-genai) compatibility", () => { + it("forwards ai.generate with x-goog-api-key swapped and the token absent", async () => { + const r = await ai().generate({ + model: googleAI.model("gemini-2.5-flash"), + prompt: "hi", + }); + expect(r.text).toContain("hi"); + const cap = mock.last(); + expect(cap?.path).toContain( + "/v1beta/models/gemini-2.5-flash:generateContent", + ); + expect(cap?.headers["x-goog-api-key"]).toBe(FAKE.gemini); + expect(JSON.stringify(cap?.headers)).not.toContain(TOKEN); + }); +}); diff --git a/test/sdk-compat/gemini.test.ts b/test/sdk-compat/google-genai.ts similarity index 100% rename from test/sdk-compat/gemini.test.ts rename to test/sdk-compat/google-genai.ts diff --git a/test/sdk-compat/instructor.py b/test/sdk-compat/instructor.py new file mode 100644 index 0000000..529ed19 --- /dev/null +++ b/test/sdk-compat/instructor.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""instructor compatibility smoke test (thin client; the Node runner owns the worker + mock). + +instructor wraps the official `openai` client, so it hits /v1/chat/completions with Authorization: +Bearer. The mock returns plain text "hi", which can't satisfy the Pydantic response_model, so the +structured parse fails by design - but the request already forwarded, so we assert on the mock +capture (real key swapped in, token absent), not on the return value. + +Run it (also part of `nub run test`): nub run test:py +""" + +import json +import logging +import os +import sys +import urllib.request + +# This file is named after its package, so its own dir (sys.path[0]) would shadow +# `import instructor`; drop it before importing the package. +sys.path.pop(0) + +import instructor +import openai +from instructor import Mode +from pydantic import BaseModel + +# The mock's plain "hi" can't satisfy the schema, so instructor logs a retry-exhausted warning. +# That failure is expected and asserted-around below; mute the log so the test output stays clean. +logging.getLogger("instructor").setLevel(logging.CRITICAL) + +W = os.environ["PROXY_WORKER_URL"] +M = os.environ["PROXY_MOCK_URL"] +TOKEN = os.environ["PROXY_TOKEN"] +REAL = os.environ["PROXY_FAKE_OPENAI"] + + +class Hello(BaseModel): + greeting: str + + +def captured(): + with urllib.request.urlopen(f"{M}/__captured") as r: + return json.load(r) + + +def main(): + client = instructor.from_openai( + openai.OpenAI(base_url=f"{W}/v1", api_key=TOKEN), mode=Mode.TOOLS + ) + try: + client.create( + model="gpt-4o-mini", + response_model=Hello, + max_retries=0, + messages=[{"role": "user", "content": "say hi"}], + ) + except Exception: + # Expected: the mock's plain "hi" can't be coerced into Hello. The forward already happened. + pass + + cap = captured() + assert cap["path"] == "/v1/chat/completions", cap["path"] + assert cap["headers"].get("authorization") == f"Bearer {REAL}", cap["headers"].get("authorization") + assert TOKEN not in json.dumps(cap["headers"]), "proxy token leaked into upstream headers" + print("PASS: instructor -> proxy swapped the real key, token never egressed") + + +if __name__ == "__main__": + main() diff --git a/test/sdk-compat/langchain-anthropic.ts b/test/sdk-compat/langchain-anthropic.ts new file mode 100644 index 0000000..7bd14ac --- /dev/null +++ b/test/sdk-compat/langchain-anthropic.ts @@ -0,0 +1,55 @@ +import { ChatAnthropic } from "@langchain/anthropic"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let baseURL: string; +const TOKEN = "compat-langchain-anthropic-token"; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + baseURL = w.url; + await seedToken(baseURL, { + token: TOKEN, + providers: ["anthropic"], + label: "langchain-anthropic", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +beforeEach(() => mock.reset()); + +// anthropicApiUrl is the bare host; the @anthropic-ai/sdk underneath appends /v1/messages. +const client = () => + new ChatAnthropic({ + model: "claude-x", + apiKey: TOKEN, + anthropicApiUrl: baseURL, + maxTokens: 16, + maxRetries: 0, + }); + +describe("@langchain/anthropic (ChatAnthropic) compatibility", () => { + it("forwards invoke() with x-api-key swapped to the real key and the token absent", async () => { + const r = await client().invoke("hi"); + expect(String(r.content)).toContain("hi"); + const cap = mock.last(); + expect(cap?.path).toBe("/v1/messages"); + expect(cap?.headers["x-api-key"]).toBe(FAKE.anthropic); + expect(JSON.stringify(cap?.headers)).not.toContain(TOKEN); + }); +}); diff --git a/test/sdk-compat/langchain-google-genai.ts b/test/sdk-compat/langchain-google-genai.ts new file mode 100644 index 0000000..15a4cb1 --- /dev/null +++ b/test/sdk-compat/langchain-google-genai.ts @@ -0,0 +1,54 @@ +import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let baseURL: string; +const TOKEN = "compat-langchain-google-token"; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + baseURL = w.url; + await seedToken(baseURL, { + token: TOKEN, + providers: ["gemini"], + label: "langchain-google-genai", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +beforeEach(() => mock.reset()); + +// baseUrl is the bare host; the SDK builds `${baseUrl}/v1beta/models/:generateContent` +// and sends the key in the x-goog-api-key header. +const client = () => + new ChatGoogleGenerativeAI({ + model: "gemini-x", + apiKey: TOKEN, + baseUrl: baseURL, + }); + +describe("@langchain/google-genai (ChatGoogleGenerativeAI) compatibility", () => { + it("forwards invoke() with x-goog-api-key swapped and the token absent", async () => { + const r = await client().invoke("hi"); + expect(String(r.content)).toContain("hi"); + const cap = mock.last(); + expect(cap?.path).toContain("/v1beta/models/gemini-x:generateContent"); + expect(cap?.headers["x-goog-api-key"]).toBe(FAKE.gemini); + expect(JSON.stringify(cap?.headers)).not.toContain(TOKEN); + }); +}); diff --git a/test/sdk-compat/langchain-openai.ts b/test/sdk-compat/langchain-openai.ts new file mode 100644 index 0000000..7727047 --- /dev/null +++ b/test/sdk-compat/langchain-openai.ts @@ -0,0 +1,55 @@ +import { ChatOpenAI } from "@langchain/openai"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { + FAKE, + type MockUpstream, + seedToken, + startMockUpstream, + startWorker, +} from "./setup"; + +let mock: MockUpstream; +let worker: Unstable_DevWorker; +let baseURL: string; +const TOKEN = "compat-langchain-openai-token"; + +beforeAll(async () => { + mock = await startMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + baseURL = w.url; + await seedToken(baseURL, { + token: TOKEN, + providers: ["openai"], + label: "langchain-openai", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +beforeEach(() => mock.reset()); + +// `configuration` is passed straight to the underlying `openai` SDK; a plain model name +// (not gpt-5.x-pro) with no tools stays on /v1/chat/completions, not /v1/responses. +const client = () => + new ChatOpenAI({ + model: "gpt-x", + apiKey: TOKEN, + configuration: { baseURL: `${baseURL}/v1` }, + maxRetries: 0, + }); + +describe("@langchain/openai (ChatOpenAI) compatibility", () => { + it("forwards invoke() with the real key swapped in and the token absent", async () => { + const r = await client().invoke("hi"); + expect(String(r.content)).toContain("hi"); + const cap = mock.last(); + expect(cap?.path).toBe("/v1/chat/completions"); + expect(cap?.headers.authorization).toBe(`Bearer ${FAKE.openai}`); + expect(JSON.stringify(cap?.headers)).not.toContain(TOKEN); + }); +}); diff --git a/test/sdk-compat/litellm.py b/test/sdk-compat/litellm.py new file mode 100644 index 0000000..c8897b2 --- /dev/null +++ b/test/sdk-compat/litellm.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""LiteLLM compatibility smoke test (thin client; the Node runner owns the worker + mock). + +`test/run-py.mjs` starts the worker and a mock upstream, seeds a proxy token, and exports +PROXY_WORKER_URL / PROXY_MOCK_URL / PROXY_TOKEN / PROXY_FAKE_OPENAI. This file just drives LiteLLM +at the worker and asserts the mock saw the real key swapped in (and the token nowhere). + +Run it (also part of `nub run test`): nub run test:py +""" + +import json +import os +import sys +import urllib.request + +# This file is named after its package, so its own dir (sys.path[0]) would shadow +# `import litellm`; drop it before importing the package. +sys.path.pop(0) + +import litellm + +litellm.telemetry = False + +W = os.environ["PROXY_WORKER_URL"] +M = os.environ["PROXY_MOCK_URL"] +TOKEN = os.environ["PROXY_TOKEN"] +REAL = os.environ["PROXY_FAKE_OPENAI"] + + +def captured(): + with urllib.request.urlopen(f"{M}/__captured") as r: + return json.load(r) + + +def main(): + litellm.completion( + model="openai/gpt-x", + api_base=f"{W}/v1", + api_key=TOKEN, + messages=[{"role": "user", "content": "ping-from-litellm"}], + max_tokens=5, + timeout=20, + num_retries=0, + ) + cap = captured() + h = cap["headers"] + assert cap["path"] == "/v1/chat/completions", cap["path"] + assert h.get("authorization") == f"Bearer {REAL}", h.get("authorization") + assert TOKEN not in json.dumps(h), "proxy token leaked into upstream headers" + assert "ping-from-litellm" in cap["body"], "request body not forwarded" + print("PASS: litellm -> proxy swapped the real key, token never egressed, body forwarded verbatim") + + +if __name__ == "__main__": + main() diff --git a/test/sdk-compat/llama-index.py b/test/sdk-compat/llama-index.py new file mode 100644 index 0000000..dc8597a --- /dev/null +++ b/test/sdk-compat/llama-index.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""LlamaIndex compatibility smoke test (thin client; the Node runner owns the worker + mock). + +Drives all three LlamaIndex LLM integrations (OpenAI, Anthropic, GoogleGenAI) through the worker and +asserts the mock saw the real key swapped into the right slot, with the proxy token nowhere. Each +integration just wraps the official provider SDK, so this proves LlamaIndex forwards base_url + key +through cleanly for every provider. + +Run it (also part of `nub run test`): nub run test:py +""" + +import json +import os +import urllib.request + +from google.genai import types +from llama_index.core.llms import ChatMessage +from llama_index.llms.anthropic import Anthropic +from llama_index.llms.google_genai import GoogleGenAI +from llama_index.llms.openai import OpenAI + +W = os.environ["PROXY_WORKER_URL"] +M = os.environ["PROXY_MOCK_URL"] +TOKEN = os.environ["PROXY_TOKEN"] + + +def captured(): + with urllib.request.urlopen(f"{M}/__captured") as r: + return json.load(r) + + +def reset(): + urllib.request.urlopen(f"{M}/__reset").read() + + +def msg(): + return [ChatMessage(role="user", content="hi")] + + +def main(): + # Real model ids are required: the OpenAI/Anthropic classes look up the context window from + # the model name and raise on an unknown id (the request still goes to the worker regardless). + + # 1) OpenAI -> Authorization: Bearer -> /v1/chat/completions + reset() + OpenAI(api_base=f"{W}/v1", api_key=TOKEN, model="gpt-4o").chat(msg()) + cap = captured() + assert cap["path"] == "/v1/chat/completions", cap["path"] + assert cap["headers"].get("authorization") == f"Bearer {os.environ['PROXY_FAKE_OPENAI']}" + assert TOKEN not in json.dumps(cap["headers"]), "token leaked (openai)" + + # 2) Anthropic -> x-api-key -> /v1/messages (base_url is the bare host) + reset() + Anthropic( + base_url=W, api_key=TOKEN, model="claude-sonnet-4-6", max_tokens=16 + ).chat(msg()) + cap = captured() + assert cap["path"] == "/v1/messages", cap["path"] + assert cap["headers"].get("x-api-key") == os.environ["PROXY_FAKE_ANTHROPIC"] + assert TOKEN not in json.dumps(cap["headers"]), "token leaked (anthropic)" + + # 3) GoogleGenAI -> x-goog-api-key -> /v1beta/models/:generateContent + # Pass max_tokens AND context_window so __init__ skips a live models.get() validation call. + reset() + GoogleGenAI( + api_key=TOKEN, + model="gemini-2.5-flash", + max_tokens=16, + context_window=1000000, + http_options=types.HttpOptions(base_url=W), + ).chat(msg()) + cap = captured() + assert "/v1beta/models/gemini-2.5-flash:generateContent" in cap["path"], cap["path"] + assert cap["headers"].get("x-goog-api-key") == os.environ["PROXY_FAKE_GEMINI"] + assert TOKEN not in json.dumps(cap["headers"]), "token leaked (gemini)" + + print("PASS: llama-index (openai + anthropic + google-genai) -> proxy swapped each real key, token never egressed") + + +if __name__ == "__main__": + main() diff --git a/test/sdk-compat/openai.test.ts b/test/sdk-compat/openai.ts similarity index 100% rename from test/sdk-compat/openai.test.ts rename to test/sdk-compat/openai.ts diff --git a/test/sdk-compat/pydantic-ai.py b/test/sdk-compat/pydantic-ai.py new file mode 100644 index 0000000..67ca09c --- /dev/null +++ b/test/sdk-compat/pydantic-ai.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Pydantic AI compatibility smoke test (thin client; the Node runner owns the worker + mock). + +Pydantic AI's OpenAIChatModel wraps the official `openai` client, so it hits /v1/chat/completions +with Authorization: Bearer. We assert the mock saw the real key swapped in and the token nowhere. +(OpenAIChatModel = Chat Completions; OpenAIResponsesModel would hit /v1/responses, which we avoid.) + +Run it (also part of `nub run test`): nub run test:py +""" + +import json +import os +import urllib.request + +from pydantic_ai import Agent +from pydantic_ai.models.openai import OpenAIChatModel +from pydantic_ai.providers.openai import OpenAIProvider + +W = os.environ["PROXY_WORKER_URL"] +M = os.environ["PROXY_MOCK_URL"] +TOKEN = os.environ["PROXY_TOKEN"] +REAL = os.environ["PROXY_FAKE_OPENAI"] + + +def captured(): + with urllib.request.urlopen(f"{M}/__captured") as r: + return json.load(r) + + +def main(): + model = OpenAIChatModel( + "gpt-4o", provider=OpenAIProvider(base_url=f"{W}/v1", api_key=TOKEN) + ) + result = Agent(model).run_sync("hi") + assert "hi" in str(result.output), result.output + + cap = captured() + assert cap["path"] == "/v1/chat/completions", cap["path"] + assert cap["headers"].get("authorization") == f"Bearer {REAL}", cap["headers"].get("authorization") + assert TOKEN not in json.dumps(cap["headers"]), "proxy token leaked into upstream headers" + print("PASS: pydantic-ai -> proxy swapped the real key, token never egressed") + + +if __name__ == "__main__": + main() diff --git a/test/sdk-compat/setup.ts b/test/sdk-compat/setup.ts index f078e11..7704fb0 100644 --- a/test/sdk-compat/setup.ts +++ b/test/sdk-compat/setup.ts @@ -3,7 +3,7 @@ import type { AddressInfo } from "node:net"; import { type Unstable_DevWorker, unstable_dev } from "wrangler"; // Fake real-keys injected as the worker's bindings. Tests assert these reach the mock -// upstream (proving the swap) and that the doppelganger token never does. +// upstream (proving the swap) and that the proxy token never does. export const FAKE = { openai: "FAKE-OPENAI-KEY", anthropic: "FAKE-ANTHROPIC-KEY", diff --git a/test/sdk-compat/websocket.ts b/test/sdk-compat/websocket.ts new file mode 100644 index 0000000..d82bae6 --- /dev/null +++ b/test/sdk-compat/websocket.ts @@ -0,0 +1,135 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import type { Unstable_DevWorker } from "wrangler"; +import { WebSocket, WebSocketServer } from "ws"; +import { FAKE, seedToken, startWorker } from "./setup"; + +// Real end-to-end wss proxy: a `ws` client -> the worker (real upgrade) -> a `ws` mock upstream. +// Proves the outbound WS upgrade works in workerd, the real key reaches the upstream handshake, +// the proxy token never does, and frames flow both ways. The per-slot swap detail (subprotocol +// stripping etc.) is covered fast in the tier-1 test/ws.test.ts; here we prove the live socket. + +interface Handshake { + headers: http.IncomingHttpHeaders; + url: string; +} + +interface WsMock { + url: string; + last(): Handshake | null; + reset(): void; + close(): Promise; +} + +async function startWsMockUpstream(): Promise { + let last: Handshake | null = null; + const server = http.createServer(); + const wss = new WebSocketServer({ server }); + wss.on("connection", (socket, req) => { + last = { headers: req.headers, url: req.url ?? "" }; + // Echo every frame straight back (binary-preserving). + socket.on("message", (data, isBinary) => + socket.send(data, { binary: isBinary }), + ); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const port = (server.address() as AddressInfo).port; + return { + url: `http://127.0.0.1:${port}`, + last: () => last, + reset: () => { + last = null; + }, + close: () => + new Promise((res) => wss.close(() => server.close(() => res()))), + }; +} + +let mock: WsMock; +let worker: Unstable_DevWorker; +let wsBase: string; +const TOKEN = "compat-ws-token"; + +beforeAll(async () => { + mock = await startWsMockUpstream(); + const w = await startWorker(mock.url); + worker = w.worker; + // unstable_dev can report 0.0.0.0 / :: which a raw ws client cannot dial. + const host = + !worker.address || worker.address === "0.0.0.0" || worker.address === "::" + ? "127.0.0.1" + : worker.address; + wsBase = `ws://${host}:${worker.port}`; + await seedToken(w.url, { + token: TOKEN, + providers: ["openai", "gemini"], + label: "ws", + }); +}); + +afterAll(async () => { + await worker?.stop(); + await mock?.close(); +}); + +/** Open a proxied socket, send one frame, resolve with the echoed frame. */ +function roundtrip( + path: string, + opts?: ConstructorParameters[2], +): Promise { + return new Promise((resolve, reject) => { + const c = new WebSocket(`${wsBase}${path}`, opts); + const timer = setTimeout(() => { + c.terminate(); + reject(new Error("ws round-trip timed out")); + }, 15_000); + c.on("open", () => c.send(JSON.stringify({ type: "ping" }))); + c.on("message", (data) => { + clearTimeout(timer); + c.close(); + resolve(JSON.parse(data.toString())); + }); + c.on("error", (e) => { + clearTimeout(timer); + reject(e); + }); + }); +} + +describe("WebSocket proxy (end-to-end)", () => { + it("OpenAI Bearer: upgrades, swaps the real key into the handshake, echoes frames", async () => { + mock.reset(); + const echo = await roundtrip("/v1/responses", { + headers: { authorization: `Bearer ${TOKEN}` }, + }); + expect(echo).toEqual({ type: "ping" }); + const hs = mock.last(); + expect(hs).not.toBeNull(); + expect(hs!.headers.authorization).toBe(`Bearer ${FAKE.openai}`); + expect(JSON.stringify(hs)).not.toContain(TOKEN); // proxy token never reaches upstream + }); + + it("Gemini ?key=: upgrades with the query key swapped to the real key", async () => { + mock.reset(); + const echo = await roundtrip( + `/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${TOKEN}`, + ); + expect(echo).toEqual({ type: "ping" }); + const hs = mock.last(); + expect(hs).not.toBeNull(); + expect(hs!.url).toContain(`key=${FAKE.gemini}`); + expect(hs!.url).not.toContain(TOKEN); + expect(hs!.headers.authorization).toBeUndefined(); + }); + + it("rejects an unknown token at the handshake (upstream never opened)", async () => { + mock.reset(); + await expect( + roundtrip("/v1/responses", { + headers: { authorization: "Bearer ghost" }, + }), + ).rejects.toThrow(); + expect(mock.last()).toBeNull(); + }); +}); diff --git a/test/tokens.test.ts b/test/tokens.test.ts index ebd7356..47b81fa 100644 --- a/test/tokens.test.ts +++ b/test/tokens.test.ts @@ -21,8 +21,8 @@ describe("sha256hex", () => { }); describe("generateToken", () => { - it("has the dgk_ prefix and a url-safe body", () => { - expect(generateToken()).toMatch(/^dgk_[A-Za-z0-9_-]{32,}$/); + it("has the ptk_ prefix and a url-safe body", () => { + expect(generateToken()).toMatch(/^ptk_[A-Za-z0-9_-]{32,}$/); }); it("is unique across calls", () => { expect(generateToken()).not.toBe(generateToken()); @@ -35,7 +35,7 @@ describe("createToken + getValidated", () => { label: "alice", providers: ["openai"], }); - expect(token).toMatch(/^dgk_/); + expect(token).toMatch(/^ptk_/); expect(meta.last4).toBe(token.slice(-4)); const got = await getValidated(env.TOKENS, token); expect(got?.label).toBe("alice"); @@ -130,3 +130,36 @@ describe("touchLastUsed", () => { expect(await getValidated(env.TOKENS, token)).toBeNull(); }); }); + +describe("expiry (getValidatedByHash via getValidated)", () => { + const mk = (token: string, expiresAt?: string) => + createToken(env.TOKENS, { + label: token, + providers: ["openai"], + token, + expiresAt, + }); + + it("absent expiresAt stays valid", async () => { + const { token } = await mk("exp-none"); + expect(await getValidated(env.TOKENS, token)).not.toBeNull(); + }); + it("future expiresAt is valid", async () => { + const { token } = await mk( + "exp-future", + new Date(Date.now() + 3_600_000).toISOString(), + ); + expect(await getValidated(env.TOKENS, token)).not.toBeNull(); + }); + it("past expiresAt is rejected", async () => { + const { token } = await mk( + "exp-past", + new Date(Date.now() - 1000).toISOString(), + ); + expect(await getValidated(env.TOKENS, token)).toBeNull(); + }); + it("malformed expiresAt is rejected (fail-closed)", async () => { + const { token } = await mk("exp-bad", "not-a-date"); + expect(await getValidated(env.TOKENS, token)).toBeNull(); + }); +}); diff --git a/test/ws.test.ts b/test/ws.test.ts new file mode 100644 index 0000000..82afd50 --- /dev/null +++ b/test/ws.test.ts @@ -0,0 +1,363 @@ +import { + createExecutionContext, + env, + waitOnExecutionContext, +} from "cloudflare:test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createToken } from "../src/tokens"; +import { + extractWsToken, + forwardCloseCode, + handleWsProxy, + prepareWsUpstream, + routeWsProvider, +} from "../src/ws"; + +// A fake upstream 101 carrying a live WebSocket, so handleWsProxy can accept + pipe it. +function ws101(): Response { + const [upstream] = Object.values(new WebSocketPair()); + return new Response(null, { status: 101, webSocket: upstream }); +} +const geo403 = () => + new Response( + JSON.stringify({ error: { code: "unsupported_country_region_territory" } }), + { status: 403 }, + ); + +let captured: Request | null; +let upstreamReply: () => Response; + +beforeEach(() => { + captured = null; + upstreamReply = ws101; + vi.spyOn(globalThis, "fetch").mockImplementation( + async (input: RequestInfo | URL, init?: RequestInit) => { + captured = input instanceof Request ? input : new Request(input, init); + return upstreamReply(); + }, + ); +}); +afterEach(() => vi.restoreAllMocks()); + +async function callWs(req: Request): Promise { + const ctx = createExecutionContext(); + const res = await handleWsProxy(req, env, ctx); + await waitOnExecutionContext(ctx); + return res; +} + +const seed = ( + token: string, + providers: ("openai" | "anthropic" | "gemini")[], +) => createToken(env.TOKENS, { label: token, providers, token }); + +describe("WS auth-slot extraction", () => { + it("reads the token smuggled in the openai-insecure-api-key subprotocol", () => { + const req = new Request("https://proxy.example/v1/realtime?model=m", { + headers: { + "sec-websocket-protocol": + "realtime, openai-insecure-api-key.tk-123, openai-beta.realtime-v1", + }, + }); + const url = new URL(req.url); + expect(extractWsToken(req, url)).toBe("tk-123"); + expect(routeWsProvider(req, url)).toBe("openai"); + }); + + it("falls back to the header and query slots", () => { + const bearer = new Request("https://p/v1/responses", { + headers: { authorization: "Bearer tk-h" }, + }); + expect(extractWsToken(bearer, new URL(bearer.url))).toBe("tk-h"); + expect(routeWsProvider(bearer, new URL(bearer.url))).toBe("openai"); + + const query = new Request( + "https://p/ws/Service.BidiGenerateContent?key=tk-q", + ); + expect(extractWsToken(query, new URL(query.url))).toBe("tk-q"); + expect(routeWsProvider(query, new URL(query.url))).toBe("gemini"); + }); +}); + +describe("prepareWsUpstream (pure auth swap)", () => { + it("openai: real key as Bearer, host rewritten, path + query kept, scheme stays https", () => { + const req = new Request( + "https://proxy.example/v1/realtime?model=gpt-realtime-2", + { headers: { authorization: "Bearer tk" } }, + ); + const url = new URL(req.url); + const { target, headers } = prepareWsUpstream( + req, + url, + "openai", + "REAL", + env, + ); + const u = new URL(target); + expect(u.protocol).toBe("https:"); + expect(u.hostname).toBe("api.openai.com"); + expect(u.pathname).toBe("/v1/realtime"); + expect(u.searchParams.get("model")).toBe("gpt-realtime-2"); + expect(headers.get("authorization")).toBe("Bearer REAL"); + }); + + it("openai subprotocol: drops the key entry, keeps realtime, sets Bearer", () => { + const req = new Request("https://proxy.example/v1/realtime", { + headers: { + "sec-websocket-protocol": + "realtime, openai-insecure-api-key.tk, openai-beta.realtime-v1", + }, + }); + const { headers } = prepareWsUpstream( + req, + new URL(req.url), + "openai", + "REAL", + env, + ); + expect(headers.get("authorization")).toBe("Bearer REAL"); + const proto = headers.get("sec-websocket-protocol") ?? ""; + expect(proto).toContain("realtime"); + expect(proto).toContain("openai-beta.realtime-v1"); + expect(proto).not.toContain("openai-insecure-api-key"); + expect(proto).not.toContain("tk"); + }); + + it("gemini: real key in ?key=, no Authorization header", () => { + const req = new Request( + "https://proxy.example/ws/Service.BidiGenerateContent?key=tk", + ); + const url = new URL(req.url); + const { target, headers } = prepareWsUpstream( + req, + url, + "gemini", + "REAL", + env, + ); + const u = new URL(target); + expect(u.hostname).toBe("generativelanguage.googleapis.com"); + expect(u.searchParams.get("key")).toBe("REAL"); + expect(headers.get("authorization")).toBeNull(); + expect(headers.get("x-goog-api-key")).toBeNull(); + }); +}); + +describe("handleWsProxy: validation (upstream never opened)", () => { + it("401 when no token is present", async () => { + const res = await callWs(new Request("https://proxy.example/v1/realtime")); + expect(res.status).toBe(401); + expect(captured).toBeNull(); + }); + it("401 for an unknown token", async () => { + const res = await callWs( + new Request("https://proxy.example/v1/realtime", { + headers: { authorization: "Bearer ghost" }, + }), + ); + expect(res.status).toBe(401); + expect(captured).toBeNull(); + }); + it("403 when the token is not scoped to the provider", async () => { + await seed("tk-gem-only", ["gemini"]); + const res = await callWs( + new Request("https://proxy.example/v1/realtime", { + headers: { authorization: "Bearer tk-gem-only" }, + }), + ); + expect(res.status).toBe(403); + expect(captured).toBeNull(); + }); +}); + +describe("handleWsProxy: auth swap + upgrade", () => { + it("openai Bearer (/v1/responses): swaps in the real key and returns 101", async () => { + await seed("tk-oai", ["openai"]); + const res = await callWs( + new Request("https://proxy.example/v1/responses", { + headers: { authorization: "Bearer tk-oai" }, + }), + ); + expect(res.status).toBe(101); + const u = new URL(captured!.url); + expect(u.hostname).toBe("api.openai.com"); + expect(u.pathname).toBe("/v1/responses"); + expect(captured!.headers.get("authorization")).toBe( + "Bearer real-openai-key-FAKE", + ); + }); + + it("openai subprotocol smuggling: swaps to Bearer, strips the key subprotocol", async () => { + await seed("tk-rt", ["openai"]); + const res = await callWs( + new Request("https://proxy.example/v1/realtime?model=gpt-realtime-2", { + headers: { + "sec-websocket-protocol": "realtime, openai-insecure-api-key.tk-rt", + }, + }), + ); + expect(res.status).toBe(101); + expect(captured!.headers.get("authorization")).toBe( + "Bearer real-openai-key-FAKE", + ); + const proto = captured!.headers.get("sec-websocket-protocol") ?? ""; + expect(proto).toContain("realtime"); + expect(proto).not.toContain("openai-insecure-api-key"); + }); + + it("gemini ?key=: swaps the query key, sets no Authorization header", async () => { + await seed("tk-gem", ["gemini"]); + const res = await callWs( + new Request( + "https://proxy.example/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=tk-gem", + ), + ); + expect(res.status).toBe(101); + const u = new URL(captured!.url); + expect(u.hostname).toBe("generativelanguage.googleapis.com"); + expect(u.searchParams.get("key")).toBe("real-gemini-key-FAKE"); + expect(captured!.headers.get("authorization")).toBeNull(); + }); +}); + +describe("handleWsProxy: security invariant", () => { + it("never forwards the proxy token upstream in any slot", async () => { + await seed("SECRET-WS", ["openai"]); + await callWs( + new Request("https://proxy.example/v1/realtime", { + headers: { + "sec-websocket-protocol": + "realtime, openai-insecure-api-key.SECRET-WS", + }, + }), + ); + const blob = [ + captured!.url, + captured!.headers.get("authorization"), + captured!.headers.get("sec-websocket-protocol"), + ].join("|"); + expect(blob).not.toContain("SECRET-WS"); + }); +}); + +describe("handleWsProxy: OpenAI geo-403 fallback via the egress DO", () => { + const realEgress = env.US_EGRESS; + let egressCalls: Request[]; + afterEach(() => { + (env as { US_EGRESS: typeof realEgress }).US_EGRESS = realEgress; + }); + function fakeEgress(reply: () => Response) { + egressCalls = []; + const stub = { + fetch: async (r: Request) => { + egressCalls.push(r); + return reply(); + }, + }; + (env as { US_EGRESS: unknown }).US_EGRESS = { + idFromName: () => ({}), + get: () => stub, + }; + } + + it("retries the upgrade through the egress DO on a geo-403, with the real key", async () => { + await seed("tk-geo", ["openai"]); + fakeEgress(ws101); + upstreamReply = geo403; + const res = await callWs( + new Request("https://proxy.example/v1/responses", { + headers: { authorization: "Bearer tk-geo" }, + }), + ); + expect(res.status).toBe(101); + expect(egressCalls.length).toBe(1); + expect(new URL(egressCalls[0].url).hostname).toBe("api.openai.com"); + expect(egressCalls[0].headers.get("authorization")).toBe( + "Bearer real-openai-key-FAKE", + ); + }); + + it("never routes gemini through the egress DO (403 surfaces straight through)", async () => { + await seed("tk-gem2", ["gemini"]); + fakeEgress(ws101); + upstreamReply = geo403; + const res = await callWs( + new Request( + "https://proxy.example/ws/Service.BidiGenerateContent?key=tk-gem2", + ), + ); + expect(res.status).toBe(403); + expect(egressCalls.length).toBe(0); + }); +}); + +describe("handleWsProxy: rate limiting", () => { + const real = (env as { RATE_LIMITER?: unknown }).RATE_LIMITER; + afterEach(() => { + (env as { RATE_LIMITER?: unknown }).RATE_LIMITER = real; + }); + const setLimiter = ( + limit: (o: { key: string }) => Promise<{ success: boolean }>, + ) => { + (env as { RATE_LIMITER: unknown }).RATE_LIMITER = { limit }; + }; + + it("429s with Retry-After when denied, without opening the upstream", async () => { + await seed("tk-ws-rl", ["openai"]); + setLimiter(async () => ({ success: false })); + const res = await callWs( + new Request("https://proxy.example/v1/responses", { + headers: { authorization: "Bearer tk-ws-rl" }, + }), + ); + expect(res.status).toBe(429); + expect(res.headers.get("retry-after")).toBe("60"); + expect(captured).toBeNull(); + }); + + it("fails open (101) when the limiter throws", async () => { + await seed("tk-ws-err", ["openai"]); + setLimiter(async () => { + throw new Error("limiter down"); + }); + const res = await callWs( + new Request("https://proxy.example/v1/responses", { + headers: { authorization: "Bearer tk-ws-err" }, + }), + ); + expect(res.status).toBe(101); + }); +}); + +describe("handleWsProxy: upstream connect failure", () => { + it("returns 502 when the upstream connect throws (no propagation)", async () => { + await seed("tk-boom", ["openai"]); + upstreamReply = () => { + throw new Error("connect refused"); + }; + const res = await callWs( + new Request("https://proxy.example/v1/responses", { + headers: { authorization: "Bearer tk-boom" }, + }), + ); + expect(res.status).toBe(502); + }); +}); + +describe("forwardCloseCode (close-code sanitization)", () => { + it("passes through normal application codes (1000-4999, not reserved)", () => { + expect(forwardCloseCode(1000)).toBe(1000); + expect(forwardCloseCode(1011)).toBe(1011); + expect(forwardCloseCode(3000)).toBe(3000); + expect(forwardCloseCode(4999)).toBe(4999); + }); + it("downgrades reserved/abnormal codes to a bare close (null)", () => { + for (const c of [1004, 1005, 1006, 1015]) + expect(forwardCloseCode(c)).toBeNull(); + }); + it("downgrades out-of-range codes to a bare close (null)", () => { + expect(forwardCloseCode(999)).toBeNull(); + expect(forwardCloseCode(5000)).toBeNull(); + expect(forwardCloseCode(0)).toBeNull(); + }); +}); diff --git a/vitest.compat.config.ts b/vitest.compat.config.ts index 3b08105..7896bba 100644 --- a/vitest.compat.config.ts +++ b/vitest.compat.config.ts @@ -1,11 +1,13 @@ -import { defineConfig } from "vitest/config"; +import { configDefaults, defineConfig } from "vitest/config"; // Tier 2: real-SDK compatibility. Runs in Node (the SDKs run here), hits a locally // started worker whose *_UPSTREAM env points at a node:http mock. Serial to avoid -// shared-capture-state and port races. +// shared-capture-state and port races. Each test file is named after the client it drives; +// setup.ts is the shared harness (excluded). Python clients run separately (`nub run test:py`). export default defineConfig({ test: { - include: ["test/sdk-compat/**/*.test.ts"], + include: ["test/sdk-compat/*.ts"], + exclude: [...configDefaults.exclude, "test/sdk-compat/setup.ts"], pool: "forks", fileParallelism: false, // serial: each file owns a mock upstream + worker on its own port testTimeout: 30_000, diff --git a/wrangler.toml b/wrangler.toml index b953999..1e596a8 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -21,6 +21,17 @@ class_name = "UsEgress" tag = "v1" new_sqlite_classes = ["UsEgress"] +# Per-token request-rate limit (Workers Rate Limiting binding; in-process, not a subrequest). +# Keyed on the token hash; one shared ceiling for all tokens (KISS). Tune `limit` freely; +# `period` must be 10 or 60. namespace_id is an arbitrary string-integer, unique per ruleset. +[[ratelimits]] +name = "RATE_LIMITER" +namespace_id = "1001" + + [ratelimits.simple] + limit = 100 + period = 60 + # Secrets (set with `nubx wrangler secret put `), never committed: # OPENAI_API_KEY ANTHROPIC_API_KEY GEMINI_API_KEY ADMIN_SECRET # Optional upstream overrides (plain vars, NOT secrets) default to the real hosts: