diff --git a/.env.example b/.env.example index 727672a7..67f1bd48 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,30 @@ -OPENAI_API_KEY=sk-YOUR-API-KEY-HERE +# Instance auth: /services/* is always gated on the api_key callers send, looked +# up in lightning_clients via APOLLO_CLIENTS_DB_URL (falls back to POSTGRES_URL). +# A known client swaps in its stored Anthropic key; an unknown sk-ant- key is +# forwarded. See platform/src/auth/README.md. + +# Database holding the lightning_clients credentials table. Leave unset locally to +# share POSTGRES_URL (one DB for everything). In production point this at a separate +# credentials DB so client secrets don't co-locate with the docs data. The TS auth +# code, `bun run migrate`, and the `client` CLI all resolve this var; the Python docs +# services always use POSTGRES_URL. +# APOLLO_CLIENTS_DB_URL=postgresql://localhost:5432/apollo_clients + +# Optional at-rest encryption for stored client Anthropic keys. Base64 of 32 bytes +# (openssl rand -base64 32). See platform/src/auth/README.md. +# APOLLO_ENC_KEY= + +# Shared secret for internal Apollo-to-Apollo apollo() calls. In production set +# this to the SAME value across all processes — it is what lets self-calls through +# the gate, and the global ANTHROPIC_API_KEY is a dev-only fallback. If unset, +# each process mints its own token, which only works single-process-per-host. +# APOLLO_INTERNAL_TOKEN= + ANTHROPIC_API_KEY=sk-YOUR-API-KEY-HERE + +OPENAI_API_KEY=sk-YOUR-API-KEY-HERE PINECONE_KEY=YOUR-API-KEY-HERE -POSTGRES_URL=POSTGRES_URL=postgresql://localhost:5432/apollo_dev +POSTGRES_URL=postgresql://localhost:5432/apollo_dev SENTRY_DSN=YOUR-API-KEY-HERE GITHUB_TOKEN=KEY diff --git a/.github/workflows/dockerize.yaml b/.github/workflows/dockerize.yaml index efca5724..fa640d18 100644 --- a/.github/workflows/dockerize.yaml +++ b/.github/workflows/dockerize.yaml @@ -24,6 +24,15 @@ jobs: echo Docker Tag: $DOCKER_TAG echo "DOCKER_TAG=$DOCKER_TAG" >> $GITHUB_ENV + + # Only move :latest for final releases. A version with a hyphen + # (e.g. 1.4.0-pre.0) is a pre-release and must not repoint latest. + { + echo "DOCKER_TAGS<> $GITHUB_ENV - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx @@ -38,6 +47,4 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - tags: | - openfn/apollo:latest - openfn/apollo:v${{ env.DOCKER_TAG }} + tags: ${{ env.DOCKER_TAGS }} diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 379f4d90..fb90ea4a 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -33,11 +33,31 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: apollo + POSTGRES_PASSWORD: apollo + POSTGRES_DB: apollo_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + POSTGRES_URL: postgres://apollo:apollo@localhost:5432/apollo_test + steps: - uses: actions/checkout@v6 - name: Set up Bun uses: oven-sh/setup-bun@v2 + with: + bun-version-file: .tool-versions - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.tool-versions b/.tool-versions index b4bc15d3..628361b1 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ python 3.11.9 -bun 1.1.13 +bun 1.3.14 diff --git a/CHANGELOG.md b/CHANGELOG.md index 84650a11..5b857b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # apollo +## 1.4.0 + +### Minor Changes + +- b3f8f38: Add authorisation to all service routes. + ## 1.3.3 ### Patch Changes diff --git a/CLAUDE.md b/CLAUDE.md index 53210e4d..c0ed0931 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,47 @@ TypeScript) service modules. - **Service discovery**: `platform/src/util/describe-modules.ts` - Auto-mounts any `services//` directory not starting with `_`. Detects service type by checking for `.py` (Python) or `.ts` (TypeScript) index file. +- **Instance auth** (`platform/src/auth/`): `/services/*` uses a + map-if-known-else-forward auth hook that is always active (no flag). The auth surface + is split into three named concerns: the client-credential authenticate hook and Anthropic-key + resolver on the injectable `InstanceAuth` class (`instance-auth.ts`), the + internal-call exemption (`internal-token.ts`), and the shared `hashToken` + (`hash.ts`). The credential + is the `api_key` the caller (Lightning) already sends in the request body — + there is no bearer token and no Lightning-side change. Its SHA-256 is looked up + in the `lightning_clients` table via `APOLLO_CLIENTS_DB_URL` (falling back to + `POSTGRES_URL` when unset, so local dev needs only the one var; staging/prod point + `APOLLO_CLIENTS_DB_URL` at a separate credentials DB). The inbound `api_key` is + treated purely as a credential and is **never** forwarded to the LLM on a known + match: it is replaced with the matched client's stored `anthropic_api_key`, or + stripped (falling back to the global `ANTHROPIC_API_KEY`) when that column is + `NULL`. An *unknown* key is forwarded unchanged only if it is `sk-ant-`-shaped + (bring-your-own key); an unknown non-`sk-ant-` key is rejected with `401` + (likely a Lightning credential that must not leak to the LLM). No `api_key` + falls back to the global key. The resolver (`InstanceAuth.resolveKey`) returns a + tagged `KeyResolution` (`useKey`/`useGlobal`/`forward`/`passthrough`) dispatched + by a named switch in `services.ts`. The stored + `anthropic_api_key` may be plaintext or AES-256-GCM-encrypted (`enc:v1:` + values, decrypted with `APOLLO_ENC_KEY`; see + `platform/src/util/instance-key-crypto.ts` and the `client` CLI at + `platform/src/auth/client/`). Lookups are + cached in memory (~60s TTL), so the DB is hit at most once per minute per + process, not per request. The refresh is single-flight with + stale-while-revalidate, so a burst of requests at the TTL boundary shares one + DB read (cold start awaits it; a warm-but-stale cache is served while one + background refresh runs) rather than stampeding the DB. If the table can't be + reached, known-client swaps don't resolve and callers degrade to the + shape-checked forward path (the same `sk-ant-` rule applies; it does not + blanket-reject). The auth hook is scoped to + `/services/*`, so health/root endpoints outside that group are unaffected. + Internal Apollo-to-Apollo `apollo()` calls are exempt via a per-process + internal token (`APOLLO_INTERNAL_TOKEN`, minted at startup; `bridge.ts` injects + it into each spawned Python child's env via `getInternalToken()`, and + `services/util.py` echoes it back). This replaces the old loopback exemption so a + co-located Lightning is still required to authenticate. The authenticate hook and resolver + live on a single `InstanceAuth` instance constructed in `server.ts`; tests build + their own configured instance rather than poking module globals. Provisioning + lives in `platform/src/auth/`. ### Services Architecture diff --git a/Dockerfile b/Dockerfile index 1a6608ea..94015864 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ FROM python:3.11-bullseye WORKDIR /app COPY ./pyproject.toml ./poetry.lock ./ -COPY ./package.json bun.lockb ./ +COPY ./package.json bun.lock ./ +COPY ./.tool-versions ./ COPY ./tsconfig.json ./ COPY ./path.config ./ @@ -20,7 +21,8 @@ RUN python -m pipx install poetry ENV PATH="${PATH}:/root/.local/bin/" RUN poetry install --only main --no-root -RUN curl -fsSL https://bun.sh/install | bash +RUN BUN_VERSION="$(awk '/^bun / {print $2}' .tool-versions)" \ + && curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" ENV PATH="${PATH}:/root/.bun/bin/" RUN bun install diff --git a/README.md b/README.md index 61424545..bf9bc91a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,24 @@ This repo contains: - A number of python-based AI services - A number of Typescript-based data services +## Documentation + +This README covers running, debugging and deploying the server. Deeper docs live +alongside the code: + +- **Architecture** — see [Server Architecture](#server-architecture) below, and + each service's own README for service-specific detail. +- **Contributing & service conventions** — [`CONTRIBUTING.md`](CONTRIBUTING.md) + explains how to add and structure a Python service (entry.py, imports, + logging, code quality). +- **Services** — every service has its own README with payload specs and + examples. See the [Services](#services) index below. +- **Instance auth** — [`platform/src/auth/README.md`](platform/src/auth/README.md) + covers authenticating `/services/*` per client and managing per-client Anthropic keys. +- **Testing** — [`services/testing/README.md`](services/testing/README.md) + documents the shared acceptance-test harness (YAML specs + LLM-as-judge); + per-service guides live in each service's `tests/`. + ## Requirements To run this server locally, you'll need the following dependencies to be @@ -45,49 +63,21 @@ bun dev To see an index of the available language services, head to `localhost:3000`. -## Debugging - -The server defaults to port 3000. You can test any service directly with curl to -confirm Apollo is working independently of Lightning (or any other client). - -For example, to trigger a `workflow_chat` stream: - -```bash -curl -N -X POST http://localhost:3000/services/workflow_chat/stream \ - -H "Content-Type: application/json" \ - -d '{"content":"make a simple http workflow","history":[],"api_key":""}' -``` - -The `api_key` field is your Anthropic API key. If `ANTHROPIC_API_KEY` is already -set in your `.env`, you can omit it. In Lightning, this is configured via the -`ANTHROPIC_API_KEY` environment variable and passed through to Apollo on each -request. - -The `-N` flag disables buffering so SSE events appear as they arrive. You should -see a stream of `event: log` lines followed by `event: complete`. An -`event: error` response means the issue is inside Apollo. - -If the stream returns successfully here but Lightning isn't receiving it, the -issue is on the Lightning side -- check that -`APOLLO_ENDPOINT=http://localhost:3000` is set correctly in Lightning's -environment (no trailing slash). +## Python Setup -To check API key connectivity (Anthropic, OpenAI, Pinecone), hit the status -service: +This repo uses `poetry` to manage dependencies. -```bash -curl http://localhost:3000/services/status -``` +We use an "in-project" venv , which means a `.venv` folder will be created when +you run `poetry install`. -## Troubleshooting +All python is invoked through `entry.py`, which loads the environment properly +so that relative imports work. -If you get errors like `poetry: command not found` (error code 127), and poetry -is set up on your machine, you may need to add these env vars to your `.bashrc` -(or whatever you use): +You can invoke entry.py directly (ie, without HTTP or any intermediate js) +through bun from the root: ``` -export BUN_INSTALL="$HOME/.bun" -export PATH="$BUN_INSTALL/bin:$PATH" +bun py echo --input tmp/payload.json ``` ## Bun installation @@ -107,23 +97,6 @@ from a node_modules. None of this affects python. See [bun's install docs](https://bun.sh/docs/cli/install) for more details. -## Python Setup - -This repo uses `poetry` to manage dependencies. - -We use an "in-project" venv , which means a `.venv` folder will be created when -you run `poetry install`. - -All python is invoked through `entry.py`, which loads the environment properly -so that relative imports work. - -You can invoke entry.py directly (ie, without HTTP or any intermediate js) -through bun from the root: - -``` -bun py echo --input tmp/payload.json -``` - ## CLI To communicate with and test the server, you can use `@openfn/cli`. @@ -166,17 +139,63 @@ pass `-o` with a folder, those files will be written to disk. Some services require API keys. Rather than coding these into your JSON payloads directly, keys can be loaded -from the `.env` file at the root. +from the `.env` file at the root. See [`.env.example`](.env.example) for the +full list of keys and env vars Apollo reads. Also note that `tmp` dirs are untracked, so if you do want to store credentials in your json, keep it inside a tmp dir and it'll remain safe and secret. +## Debugging + +The server defaults to port 3000. You can test any service directly with curl to +confirm Apollo is working independently of Lightning (or any other client). + +For example, to trigger a `workflow_chat` stream: + +```bash +curl -N -X POST http://localhost:3000/services/workflow_chat/stream \ + -H "Content-Type: application/json" \ + -d '{"content":"make a simple http workflow","history":[],"api_key":""}' +``` + +The `api_key` field is your Anthropic API key. If `ANTHROPIC_API_KEY` is already +set in your `.env`, you can omit it. In Lightning, this is configured via the +`ANTHROPIC_API_KEY` environment variable and passed through to Apollo on each +request. + +The `-N` flag disables buffering so SSE events appear as they arrive. You should +see a stream of `event: log` lines followed by `event: complete`. An +`event: error` response means the issue is inside Apollo. + +If the stream returns successfully here but Lightning isn't receiving it, the +issue is on the Lightning side -- check that +`APOLLO_ENDPOINT=http://localhost:3000` is set correctly in Lightning's +environment (no trailing slash). + +To check API key connectivity (Anthropic, OpenAI, Pinecone), hit the status +service: + +```bash +curl http://localhost:3000/services/status +``` + +## Troubleshooting + +If you get errors like `poetry: command not found` (error code 127), and poetry +is set up on your machine, you may need to add these env vars to your `.bashrc` +(or whatever you use): + +``` +export BUN_INSTALL="$HOME/.bun" +export PATH="$BUN_INSTALL/bin:$PATH" +``` + ## Server Architecture The Apollo server uses bunjs with the Elysia framework. -It is a very lightweight server, with at the time of writing no authentication -or governance included. +It is a very lightweight server. By default it includes no authentication, but +instance auth can be enabled (see [Database](#database) below). Python services are hosted at `/services/`. Each service expects a POST request with a JSON body, and will return JSON. @@ -189,7 +208,143 @@ Python scripts are invoked through a child process. Each call to a service runs in its own context. Python modules are pretty free-form but must adhere to a minimal structure. See -the Contribution Guide for details. +the [Contribution Guide](CONTRIBUTING.md) for details. + +## Services + +Each service lives in `services//` and is auto-mounted by service +discovery. Every mounted service has its own README with payload specs and +examples — start there for anything service-specific. + +### Chat & orchestration + +| Service | What it does | +| --- | --- | +| [`global_chat`](services/global_chat/README.md) | Orchestrator and single entry point for OpenFn AI chat; routes to subagents or escalates to a planner. Also see its [`PAYLOAD_SPEC.md`](services/global_chat/PAYLOAD_SPEC.md). | +| [`job_chat`](services/job_chat/README.md) | AI chat for OpenFn job code, with a code-suggestions/auto-patch mode. | +| [`workflow_chat`](services/workflow_chat/README.md) | Generates and edits OpenFn workflow YAML, preserving job code and IDs. | +| [`doc_agent_chat`](services/doc_agent_chat/README.md) | Agentic chat over a project's uploaded documents (RAG). | + +### Docs, search & RAG + +| Service | What it does | +| --- | --- | +| [`search_docsite`](services/search_docsite/README.md) | Semantic search over the OpenFn docs (`docsite` Pinecone index). | +| [`embed_docsite`](services/embed_docsite/README.md) | Downloads and indexes the OpenFn docs into the `docsite` index. | +| [`doc_agent_upload`](services/doc_agent_upload/README.md) | Fetches and indexes project documents into the `doc-agent` index. | + +### Adaptors + +| Service | What it does | +| --- | --- | +| [`load_adaptor_docs`](services/load_adaptor_docs/README.md) | Parses adaptor function docs into Postgres. | +| [`search_adaptor_docs`](services/search_adaptor_docs/README.md) | Queries adaptor docs back out of Postgres by version. | +| [`latest_adaptors`](services/latest_adaptors/README.md) | Fetches the latest adaptor versions from the OpenFn repo. | +| [`adaptor_apis`](services/adaptor_apis/README.md) | **TypeScript** service: produces a JSON schema of an adaptor's API. | + +### Medical vocab & embeddings + +| Service | What it does | +| --- | --- | +| `vocab_mapper` | Maps medical vocabularies (LOINC/SNOMED) against the `apollo-mappings` index. (No README yet.) | +| [`embeddings`](services/embeddings/README.md) | Vector-store wrapper used by the vocab services. | +| [`embed_loinc_dataset`](services/embed_loinc_dataset/README.md) | Embeds the LOINC dataset into `apollo-mappings`. | +| [`embed_snomed_dataset`](services/embed_snomed_dataset/README.md) | Embeds the SNOMED dataset into `apollo-mappings`. | +| [`embeddings_demo`](services/embeddings_demo/README.md) | Standalone embeddings demo (Zilliz). | + +### Utilities & support + +| Service | What it does | +| --- | --- | +| [`status`](services/status/README.md) | Health check: validates Anthropic, OpenAI and Pinecone keys. | +| [`echo`](services/echo/README.md) | Test service that returns its input; useful for verifying the pipeline. | +| [`auth`](platform/src/auth/README.md) | Instance-auth hook + provisioning (server layer, under `platform/`, not a mounted service). See [Database](#database). | +| [`testing`](services/testing/README.md) | Shared acceptance-test harness (not a mounted service). | + +## Database + +Apollo uses Postgres for two tables: `adaptor_function_docs` (parsed adaptor +docs, used by `load_adaptor_docs` / `search_adaptor_docs`) and +`lightning_clients` (the instance-auth allow-list). + +There is no migration framework. The schema is just two `schema.sql` files you +apply with `psql`, both written with `CREATE TABLE IF NOT EXISTS` so re-running +them is safe: + +- [`services/load_adaptor_docs/schema.sql`](services/load_adaptor_docs/schema.sql) + — `adaptor_function_docs`. This table is also created lazily the first time + `load_adaptor_docs` runs, so applying it by hand is optional. +- `lightning_clients` — created and kept current by the migration runner + (`platform/src/db/migrate.ts`, migrations under + [`platform/migrations/`](platform/migrations/)). It is applied automatically at + Apollo startup when `POSTGRES_URL` is set; no manual `psql` step is needed. + +First, make sure you've configured your desired `POSTGRES_URL` in your `.env` +file. + +### Create the DB + +Create a Postgres DB matching your POSTGRES_URL from the `.env` file + +### To reset the DB + +`set -a; . ./.env; set +a; psql "$POSTGRES_URL" -c "DROP TABLE IF EXISTS lightning_clients, adaptor_function_docs CASCADE;"` + +### Run the migrations + +`lightning_clients` is migrated automatically at Apollo startup (see +`platform/src/db/migrate.ts`). Apply the adaptor-docs schema separately: + +`set -a; . ./.env; set +a; psql "$POSTGRES_URL" -f services/load_adaptor_docs/schema.sql` + +### Instance authentication (optional) + +`/services/*` can be authenticated so that only known clients (e.g. specific Lightning +instances) may call it, with Apollo using **each client's own Anthropic API +key** for that client's requests. + +- It is **transparent and backward compatible** (map-if-known-else-forward): the + auth hook is always active but only swaps in a key when it recognises the caller. + Clients are looked up in the `lightning_clients` table via `POSTGRES_URL`; if + that table can't be reached, known-client swaps simply don't resolve and every + caller degrades to the forward path (it does **not** blanket-reject). +- The credential is the **`api_key` the caller already sends in the request + body** — there is no bearer token, no `Authorization` header, and no + Lightning-side change. Apollo stores only a SHA-256 hash of it. +- On a match, the inbound `api_key` is treated purely as a credential and is + **never** forwarded to the LLM: it is replaced with the client's stored + Anthropic key (so LLM usage bills to that client), or stripped — falling back + to the global `ANTHROPIC_API_KEY` — if the client has no stored key. +- An **unrecognised** key is forwarded unchanged **only if it looks like an + Anthropic key** (prefix `sk-ant-`) — this is the bring-your-own-key path. An + unrecognised key that is _not_ `sk-ant-`-shaped is a likely Lightning + credential, so it is **rejected** (`401`) rather than forwarded, which would + leak it to the LLM. A request with no `api_key` falls back to the global key. +- Health/root endpoints (`/livez`, `/status`, `/`) are outside `/services/*` and + never subject to the auth hook. Internal Apollo-to-Apollo `apollo()` calls are exempt via a + per-process internal token (`APOLLO_INTERNAL_TOKEN`), not by network position. + +To enable it and provision clients, see +[`platform/src/auth/`](platform/src/auth/README.md). + +#### Deploying: pin `APOLLO_INTERNAL_TOKEN` in production + +`APOLLO_INTERNAL_TOKEN` is the mechanism that lets internal `apollo()` self-calls +through the auth hook: a self-call carries it in the `x-apollo-internal` header and the +hook matches it. Because the global `ANTHROPIC_API_KEY` is dev-only, a token that +fails to match is a dead end (a `401`), not a soft fallback to the global key — so +the match has to work in every topology. + +- **Always set `APOLLO_INTERNAL_TOKEN` to a shared value across the deployment in + production.** When it is set, the per-process minting path never runs. +- The per-process random mint is a **dev-only convenience**. Apollo assumes one + Bun process per host; the `apollo()` self-call relies on loopback calls landing + on the same process, so a minted token only works single-process-per-host. +- If `reusePort` clustering is ever enabled, a shared `APOLLO_INTERNAL_TOKEN` is + **required**, not optional: a self-call can otherwise be routed to a sibling + process that minted a different token and will `401`. Startup logs the token's + provenance (env vs minted) and warns when this dangerous combination is + detected; a mismatch at the hook is logged as a distinct, greppable warning. ## Websockets @@ -232,8 +387,8 @@ docker run -p 3000:3000 openfn-apollo ## Contributing -See the Contribution Guide for more details about how and where to contribute to -the Apollo platform. +See the [Contribution Guide](CONTRIBUTING.md) for more details about how and +where to contribute to the Apollo platform. ## Release diff --git a/bun.lock b/bun.lock new file mode 100644 index 00000000..bfec56c0 --- /dev/null +++ b/bun.lock @@ -0,0 +1,910 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "apollo", + "dependencies": { + "@changesets/cli": "^2.27.3", + "@elysiajs/html": "^1.4.0", + "@openfn/adaptor-apis": "^0.3.0", + "@sentry/bun": "^10.60.0", + "elysia": "1.4.27", + "jsdoc-babel": "^0.5.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.15.0", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-XmXYVs8CzJ1Aj79noVbn2weUO/XWtRyURpGqx7aU7DOXlUQhR0WKOQNF0okh7PCeY37vxf7kU3v57OAkEPm3ww=="], + + "@apm-js-collab/code-transformer-bundler-plugins": ["@apm-js-collab/code-transformer-bundler-plugins@0.5.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "es-module-lexer": "^2.1.0", "magic-string": "^0.30.21", "module-details-from-path": "^1.0.4" } }, "sha512-YxLBY5nGlurL7QeJLq6e5g0ouBpAp0pwgyA/5rHXEXwhiPLn9ZHbT+Y2LlP90GT872cSocfjWRYu/fnpuBudNQ=="], + + "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.10.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-2/Z3NTewJTruUkmsSnBC5bJlLNUd9keuD1OLlTEpim4FyLhm6m2Rnfv+wrFdUvFfhmH8CRdiDZBqBrn+wyaGuA=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-proposal-object-rest-spread": ["@babel/plugin-proposal-object-rest-spread@7.20.7", "", { "dependencies": { "@babel/compat-data": "^7.20.5", "@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.20.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], + + "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], + + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], + + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], + + "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], + + "@changesets/cli": ["@changesets/cli@2.31.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.1", "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg=="], + + "@changesets/config": ["@changesets/config@3.1.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q=="], + + "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], + + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.4", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg=="], + + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.16", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.10", "@changesets/config": "^3.1.4", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g=="], + + "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], + + "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], + + "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], + + "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="], + + "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], + + "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="], + + "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], + + "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], + + "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@elysiajs/html": ["@elysiajs/html@1.4.2", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Db7dmbkN7gptckMpU0/Fq9Qi3QuhQr/CH60A+8rs+RT+74NUC8sONs5nkfMm5oL+6kCUWCv19uUwOjBP7zsjYQ=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@jsdoc/salty": ["@jsdoc/salty@0.2.12", "", { "dependencies": { "lodash": "^4.18.1" } }, "sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ=="], + + "@kitajs/html": ["@kitajs/html@4.2.13", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-o+8e61EsoLDPTP7rsPkYolca1YFybHuxU2Lr5fWDZCUkYT/6uBlVkvnZUdCXMQKentJL9dxwpR8/xK2Q+U4LhA=="], + + "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="], + + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], + + "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@openfn/adaptor-apis": ["@openfn/adaptor-apis@0.3.1", "", { "dependencies": { "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-typescript": "^7.29.7", "@types/jsdoc-to-markdown": "^7.0.6", "acorn": "^8.16.0", "async-es": "^3.2.6", "file-set": "^5.3.0", "jsdoc-to-markdown": "^9.1.3", "rimraf": "^6.1.3", "ts-node": "10.9.1", "tsup": "8.5.0", "tsx": "^4.22.3", "typescript": "4.8.4", "yargs": "17.6.0" } }, "sha512-9/MiOAPGcbKTq4gUbsWk256LGMrGmM6+R1FLqX+KR0dq5mQcmAPTITl0ghBwguP8aLUuusUlGlLHar2zFcvx4w=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + + "@sentry/bun": ["@sentry/bun@10.60.0", "", { "dependencies": { "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", "@sentry/core": "10.60.0", "@sentry/node": "10.60.0", "@sentry/server-utils": "10.60.0" } }, "sha512-T10EMuppMUoHtNWivn5puR0bEYr/y5WuVYjoOG+wElgc4iqGSmJ3SXb7WebxW55VzLDoDpvM1tXjDHhNnlWCig=="], + + "@sentry/conventions": ["@sentry/conventions@0.12.0", "", {}, "sha512-z1JQrl/1SLY+8wpzvork6vl+fpsg/oCCxM7HWWhUnI/R+OGNyoIzieQuggX3uUMY7NBtp8UWCQx6FeFazzOF9g=="], + + "@sentry/core": ["@sentry/core@10.60.0", "", {}, "sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw=="], + + "@sentry/node": ["@sentry/node@10.60.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@sentry/core": "10.60.0", "@sentry/node-core": "10.60.0", "@sentry/opentelemetry": "10.60.0", "@sentry/server-utils": "10.60.0", "import-in-the-middle": "^3.0.0" } }, "sha512-u//paUrkKaCr0oNn7r7UulGydkYMSkU1wQOIpG/P/jf7psZWnyXhgeszHzUfZXo6pCdxXG9z9viPvzGjqPQN7A=="], + + "@sentry/node-core": ["@sentry/node-core@10.60.0", "", { "dependencies": { "@sentry/conventions": "^0.12.0", "@sentry/core": "10.60.0", "@sentry/opentelemetry": "10.60.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base"] }, "sha512-aXi9ixvP+hgUZPPZCRwMNHgY2I0gkSeoAKAUuysDJhWDmrygwfGdlkbGmmtW6PQjtMYFx69Igt5btvhjEBoJTw=="], + + "@sentry/opentelemetry": ["@sentry/opentelemetry@10.60.0", "", { "dependencies": { "@sentry/conventions": "^0.12.0", "@sentry/core": "10.60.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, "sha512-gl+2NVH+9RmTu7pd9kV1tKif+Th+p9tmnXR1l3Sb3Wqo1ir5FaNMKrloWEKMXjnepii9EJUrEHdSC+i8NoexxQ=="], + + "@sentry/server-utils": ["@sentry/server-utils@10.60.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", "@apm-js-collab/tracing-hooks": "^0.10.0", "@sentry/conventions": "^0.12.0", "@sentry/core": "10.60.0", "magic-string": "~0.30.0" } }, "sha512-SX+MzWM3nz5ttKT48rlfktm0ERyIpDLma+b6pYeWgW2oFHKcpIu0g0qMGJrZs4lKM3MlgV7IqLa4texMqTp9kQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], + + "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], + + "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], + + "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/jsdoc-to-markdown": ["@types/jsdoc-to-markdown@7.0.6", "", {}, "sha512-FB/oOam8P4WoGbkfLu6ciektQhqlVuL4VsbrGJp3/YDAlRGcoiOhXDnnPL73TtHYMsDZ7NHYhCGJn4hu0TZdHg=="], + + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + + "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + + "@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + + "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-back": ["array-back@6.2.3", "", {}, "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "async-es": ["async-es@3.2.6", "", {}, "sha512-9C2+oOPd7/EzIeneF4k24o75oY7OcHU/Isl7xIot12EBRwXonyuqKsmxwLuAbFWL6B/FucTQip09xTbiu1CA8A=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.38", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw=="], + + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + + "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], + + "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "cache-point": ["cache-point@3.0.1", "", { "dependencies": { "array-back": "^6.2.2" }, "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-itTIMLEKbh6Dw5DruXbxAgcyLnh/oPGVLBfTPqBOftASxHe8bAeXy7JkO4F0LvHqht7XqP5O/09h5UcHS2w0FA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], + + "catharsis": ["catharsis@0.9.0", "", { "dependencies": { "lodash": "^4.17.15" } }, "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chalk-template": ["chalk-template@0.4.0", "", { "dependencies": { "chalk": "^4.1.2" } }, "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg=="], + + "chardet": ["chardet@2.2.0", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "command-line-args": ["command-line-args@6.0.2", "", { "dependencies": { "array-back": "^6.2.3", "find-replace": "^5.0.2", "lodash.camelcase": "^4.3.0", "typical": "^7.3.0" }, "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ=="], + + "command-line-usage": ["command-line-usage@7.0.4", "", { "dependencies": { "array-back": "^6.2.2", "chalk-template": "^0.4.0", "table-layout": "^4.1.1", "typical": "^7.3.0" } }, "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "common-sequence": ["common-sequence@3.0.0", "", {}, "sha512-g/CgSYk93y+a1IKm50tKl7kaT/OjjTYVQlEbUlt/49ZLV1mcKpUU7iyDiqTAeLdb4QDtQfq3ako8y8v//fzrWQ=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "config-master": ["config-master@3.1.0", "", { "dependencies": { "walk-back": "^2.0.1" } }, "sha512-n7LBL1zBzYdTpF1mx5DNcZnZn05CWIdsdvtPL4MosvqbBUK3Rq6VWEtGUuF3Y0s9/CIhMejezqlSkP6TnCJ/9g=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "current-module-paths": ["current-module-paths@1.1.3", "", {}, "sha512-7AH+ZTRKikdK4s1RmY0l6067UD/NZc7p3zZVZxvmnH80G31kr0y0W0E6ibYM4IS01MEm8DiC5FnTcgcgkbFHoA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "diff": ["diff@4.0.4", "", {}, "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dmd": ["dmd@7.1.1", "", { "dependencies": { "array-back": "^6.2.2", "cache-point": "^3.0.0", "common-sequence": "^3.0.0", "file-set": "^5.2.2", "handlebars": "^4.7.8", "marked": "^4.3.0", "walk-back": "^5.1.1" }, "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-Ap2HP6iuOek7eShReDLr9jluNJm9RMZESlt29H/Xs1qrVMkcS9X6m5h1mBC56WMxNiSo0wvjGICmZlYUSFjwZQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.377", "", {}, "sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g=="], + + "elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], + + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-set": ["file-set@5.3.0", "", { "dependencies": { "array-back": "^6.2.2", "fast-glob": "^3.3.2" }, "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-FKCxdjLX0J6zqTWdT0RXIxNF/n7MyXXnsSUp0syLEOCKdexvPZ02lNNv2a+gpK9E3hzUYF3+eFZe32ci7goNUg=="], + + "file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-replace": ["find-replace@5.0.2", "", { "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "human-id": ["human-id@4.2.0", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-in-the-middle": ["import-in-the-middle@3.2.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + + "js2xmlparser": ["js2xmlparser@4.0.2", "", { "dependencies": { "xmlcreate": "^2.0.4" } }, "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA=="], + + "jsdoc": ["jsdoc@4.0.5", "", { "dependencies": { "@babel/parser": "^7.20.15", "@jsdoc/salty": "^0.2.1", "@types/markdown-it": "^14.1.1", "bluebird": "^3.7.2", "catharsis": "^0.9.0", "escape-string-regexp": "^2.0.0", "js2xmlparser": "^4.0.2", "klaw": "^3.0.0", "markdown-it": "^14.1.0", "markdown-it-anchor": "^8.6.7", "marked": "^4.0.10", "mkdirp": "^1.0.4", "requizzle": "^0.2.3", "strip-json-comments": "^3.1.0", "underscore": "~1.13.2" }, "bin": { "jsdoc": "jsdoc.js" } }, "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g=="], + + "jsdoc-api": ["jsdoc-api@9.3.6", "", { "dependencies": { "array-back": "^6.2.3", "cache-point": "^3.0.1", "current-module-paths": "^1.1.3", "file-set": "^5.3.0", "jsdoc": "^4.0.5", "object-to-spawn-args": "^2.0.1", "walk-back": "^5.1.2" }, "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-8JW0532+rXVw8LoZ1LIAeKofsV8QQZhnY3chxMHV9hQdsuDTshsajwk0b4EMxCqen2vZ2op/r/qeEsqZxsEQyg=="], + + "jsdoc-babel": ["jsdoc-babel@0.5.0", "", { "dependencies": { "jsdoc-regex": "^1.0.1", "lodash": "^4.17.10" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-PYfTbc3LNTeR8TpZs2M94NLDWqARq0r9gx3SvuziJfmJS7/AeMKvtj0xjzOX0R/4MOVA7/FqQQK7d6U0iEoztQ=="], + + "jsdoc-parse": ["jsdoc-parse@6.2.5", "", { "dependencies": { "array-back": "^6.2.2", "find-replace": "^5.0.1", "sort-array": "^5.0.0" } }, "sha512-8JaSNjPLr2IuEY4Das1KM6Z4oLHZYUnjRrr27hKSa78Cj0i5Lur3DzNnCkz+DfrKBDoljGMoWOiBVQbtUZJBPw=="], + + "jsdoc-regex": ["jsdoc-regex@1.0.1", "", {}, "sha512-CMFgT3K8GbmChWEfLWe6jlv9x33E8wLPzBjxIlh/eHLMcnDF+TF3CL265ZGBe029o1QdFepwVrQu0WuqqNPncg=="], + + "jsdoc-to-markdown": ["jsdoc-to-markdown@9.1.3", "", { "dependencies": { "array-back": "^6.2.2", "command-line-args": "^6.0.1", "command-line-usage": "^7.0.3", "config-master": "^3.1.0", "dmd": "^7.1.1", "jsdoc-api": "^9.3.5", "jsdoc-parse": "^6.2.5", "walk-back": "^5.1.1" }, "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"], "bin": { "jsdoc2md": "bin/cli.js" } }, "sha512-i9wi+6WHX0WKziv0ar88T8h7OmxA0LWdQaV23nY6uQyKvdUPzVt0o6YAaOceFuKRF5Rvlju5w/KnZBfdpDAlnw=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "klaw": ["klaw@3.0.0", "", { "dependencies": { "graceful-fs": "^4.1.9" } }, "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="], + + "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + + "markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="], + + "markdown-it-anchor": ["markdown-it-anchor@8.6.7", "", { "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" } }, "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA=="], + + "marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="], + + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "meriyah": ["meriyah@6.1.4", "", {}, "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], + + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + + "node-releases": ["node-releases@2.0.48", "", {}, "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-to-spawn-args": ["object-to-spawn-args@2.0.1", "", {}, "sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + + "requizzle": ["requizzle@0.2.4", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], + + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semifies": ["semifies@1.0.0", "", {}, "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "sort-array": ["sort-array@5.1.1", "", { "dependencies": { "array-back": "^6.2.2", "typical": "^7.1.1" }, "peerDependencies": { "@75lb/nature": "^0.1.1" }, "optionalPeers": ["@75lb/nature"] }, "sha512-EltS7AIsNlAFIM9cayrgKrM6XP94ATWwXP4LCL4IQbvbYhELSt2hZTrixg+AaQwnWFs/JGJgqU3rxMcNNWxGAA=="], + + "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], + + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "table-layout": ["table-layout@4.1.1", "", { "dependencies": { "array-back": "^6.2.2", "wordwrapjs": "^5.1.0" } }, "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "ts-node": ["ts-node@10.9.1", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsup": ["tsup@8.5.0", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.25.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "0.8.0-beta.0", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ=="], + + "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], + + "typescript": ["typescript@4.8.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="], + + "typical": ["typical@7.3.0", "", {}, "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw=="], + + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], + + "walk-back": ["walk-back@5.1.2", "", { "peerDependencies": { "@75lb/nature": "latest" }, "optionalPeers": ["@75lb/nature"] }, "sha512-uCgzIY1U7fyXvJm+mesY0xjf2HXu7mtTnptONwVQ11ur1JhMrUyQJn2fDje1CGFQDnTFTo1Slr1vRuvUS9PYoQ=="], + + "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], + + "whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + + "wordwrapjs": ["wordwrapjs@5.1.1", "", {}, "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "xmlcreate": ["xmlcreate@2.0.4", "", {}, "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yargs": ["yargs@17.6.0", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.0.0" } }, "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + + "@apm-js-collab/code-transformer/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@kitajs/ts-html-plugin/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + + "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "chalk-template/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "config-master/walk-back": ["walk-back@2.0.1", "", {}, "sha512-Nb6GvBR8UWX1D+Le+xUq0+Q1kFmRBIWVrfLnQAOmcpEzA9oAxwJ9gIr36t9TWYfzvWRvuMtjHiVsJYEkXWaTAQ=="], + + "handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "path-scurry/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "@kitajs/ts-html-plugin/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "@kitajs/ts-html-plugin/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "@kitajs/ts-html-plugin/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@kitajs/ts-html-plugin/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "@kitajs/ts-html-plugin/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "@kitajs/ts-html-plugin/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "@kitajs/ts-html-plugin/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "@kitajs/ts-html-plugin/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@kitajs/ts-html-plugin/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "@kitajs/ts-html-plugin/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index f93c5965..00000000 Binary files a/bun.lockb and /dev/null differ diff --git a/package.json b/package.json index 2a431e5e..fed1c224 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "apollo", "module": "platform/index.ts", - "version": "1.3.3", + "version": "1.4.0", "type": "module", "scripts": { "start": "NODE_ENV=production bun platform/src/index.ts", "dev": "bun --watch platform/src/index.ts", "build": "bun build platform/src/index.ts", "test": "bun test platform/test ", + "client": "bun platform/src/auth/client/cli.ts", + "migrate": "bun platform/src/db/migrate.ts", "py": "poetry run python services/entry.py " }, "devDependencies": { @@ -20,6 +22,7 @@ "@changesets/cli": "^2.27.3", "@elysiajs/html": "^1.4.0", "@openfn/adaptor-apis": "^0.3.0", + "@sentry/bun": "^10.60.0", "elysia": "1.4.27", "jsdoc-babel": "^0.5.0" }, diff --git a/platform/migrations/0001_lightning_clients.sql b/platform/migrations/0001_lightning_clients.sql new file mode 100644 index 00000000..18ffe3bf --- /dev/null +++ b/platform/migrations/0001_lightning_clients.sql @@ -0,0 +1,13 @@ +-- Instance auth allow-list of Lightning clients permitted to call Apollo. The +-- /services/* auth hook is always active: the api_key the caller sends is hashed and +-- looked up here. A known client swaps in its stored key; an unknown sk-ant- key +-- is forwarded; an unknown non-sk-ant- key is rejected. + +CREATE TABLE IF NOT EXISTS lightning_clients ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, -- human identifier for the client + auth_token_hash VARCHAR(64) UNIQUE NOT NULL, -- sha256 hex of the client's api_key credential (never the plaintext) + anthropic_api_key TEXT -- Anthropic key Apollo uses for this client; NULL => global env key. + -- Plaintext, OR an "enc:v1:..." value from encrypt_key.ts when + -- APOLLO_ENC_KEY is set. Both are accepted. +); diff --git a/platform/src/auth/README.md b/platform/src/auth/README.md new file mode 100644 index 00000000..878ad41b --- /dev/null +++ b/platform/src/auth/README.md @@ -0,0 +1,170 @@ +# Instance auth + +Restricts Apollo's `/services/*` endpoints so that only known Lightning instances +can call them, and makes Apollo use **its own per-client Anthropic API key** for each +request rather than trusting anything the caller sends. + +This is server-layer code: the runtime auth hook, the shared hash, and the internal-call +token live here under `platform/src/auth/`; the operator tooling sits alongside in +`platform/src/auth/client/` (the `client` CLI). The `lightning_clients` table is +created and kept current by the migration runner (`platform/src/db/migrate.ts`, +migrations under `platform/migrations/`). + +## How it works + +- The credential is the **`api_key` the caller already sends in the request + body** — the same field Lightning sends today. There is no bearer token, no + `Authorization` header, and **no change required on the Lightning side**. +- A single Postgres table, `lightning_clients`, is the allow-list. Each row has a + `name`, the **SHA-256 hash** of that client's `api_key` (never the plaintext), + and an optional `anthropic_api_key`. +- On every `/services/*` request the server reads `api_key` from the body, + hashes it, and looks for a matching row. The inbound `api_key` is treated + **purely as a credential and is never forwarded to the LLM** on a known match. +- On a match it is replaced with the client's stored `anthropic_api_key`, so all + LLM usage for that request bills to the key Apollo controls. If the column is + `NULL`, the inbound key is **stripped** and Apollo falls back to its global + `ANTHROPIC_API_KEY`. Either way the caller's key cannot pass through. +- **Performance:** lookups are cached per client on a ~60s TTL with single-flight, + stale-while-revalidate refresh, so the database is queried at most once per + minute per process per token, never on the per-request path to Anthropic. The + per-request cost is a hash plus a map lookup. +- **Transparent / backward compatible (map-if-known-else-forward):** the auth hook + is always active but only swaps in a key when it recognises the caller. An + unrecognised key is forwarded unchanged if it is `sk-ant-`-shaped + (bring-your-own key) and rejected (`401`) otherwise; a non-`sk-ant-` key is a + likely Lightning credential that must not reach the LLM. A request with no + `api_key` falls back to the global key. When this table can't be reached, + known-client swaps don't resolve and every caller degrades to that forward + path; it does **not** blanket-reject. +- The health endpoints (`/livez`, `/status`, `/`) sit outside `/services/*` and + are never subject to the auth hook. Internal Apollo-to-Apollo `apollo()` calls are exempt via a + per-process internal token (`APOLLO_INTERNAL_TOKEN`), not by network position. + +## Where the clients table lives + +The `lightning_clients` table is reached via **`APOLLO_CLIENTS_DB_URL`**, which falls +back to `POSTGRES_URL` when it isn't set. The TS auth code, the migration runner +(`bun run migrate`), and the `client` CLI all resolve the URL the same way, so they +always agree on which database they're touching. + +- **Local dev:** set only `POSTGRES_URL`. The clients table, the auth code, and the + Python docs services all share that one database, exactly as before this var + existed. You don't need to set a second URL to get started. +- **Production:** point `APOLLO_CLIENTS_DB_URL` at a **separate** database (its own + least-privilege user) so the per-client credentials (including the encrypted + Anthropic keys) don't co-locate with the docs data on `POSTGRES_URL`. This is the + advisable setup for any deployment holding real client secrets: a leak or a loose + grant on the docs DB then doesn't expose the credentials table, and the clients DB + can be locked down independently. + +The Python docs services (`adaptor_function_docs`) always use `POSTGRES_URL` and are +unaffected by the split. One caveat to keep in mind: with the two URLs pointing at +different databases, the TS side (clients) and Python side (docs) genuinely live +apart, so when you run a migration or register a client, make sure +`APOLLO_CLIENTS_DB_URL` resolves to the database you mean. On startup Apollo logs +which one it opened (`clients DB: using APOLLO_CLIENTS_DB_URL` / +`...falling back to POSTGRES_URL`). + +## The `client` CLI + +`bun run client` is the canonical way to manage Lightning clients. It carries four +subcommands — `add` / `rotate` / `encrypt` / `verify`. Run them from the repo root +so Bun loads `.env` (`APOLLO_ENC_KEY`, and `APOLLO_CLIENTS_DB_URL` or `POSTGRES_URL`). The Anthropic key is read +from **stdin** (a pipe or an interactive prompt), never from `argv`, so it never +lands in shell history or `ps`; the client **name** is a positional argument. + +1. Bring the schema up to date. The migration runner does this automatically at + Apollo startup when a clients DB URL is set, so usually no step is needed. To run + it on its own (e.g. before provisioning against a fresh DB): + + ```sh + bun run migrate + ``` + + This applies only the platform/auth schema (`lightning_clients`, `_migrations`). + The Python services own and self-initialise their own table + (`adaptor_function_docs`), so `bun run migrate` does not and should not touch it. + +2. Set a master encryption key in `.env` (once) — the CLI uses it to encrypt each + client's Anthropic key at rest: + + ```sh + echo "APOLLO_ENC_KEY=$(openssl rand -base64 32)" >> .env + ``` + +3. Add the client with a name and the Anthropic key Apollo should use for it (key + on stdin; needs a clients DB URL set too, since it writes the row itself): + + ```sh + echo "$KEY" | bun run client add acme + # or pull the key from a secret without it touching the shell: + cat /run/secrets/anthropic | bun run client add acme + ``` + + This writes the row to `lightning_clients` and prints **only** the `api_key` to + give the Lightning instance. No SQL to run by hand. Re-running `add` for an + existing name fails with a "use `rotate`" message rather than a raw constraint + error. + +4. The client is active as soon as its row is in the table — there is no flag to + set or restart needed. The startup log shows `Apollo instance auth: + lightning_clients lookup ready.` once the DB is reachable. (If the table is + missing or the DB is down, the log warns and callers fall to the forward path + rather than being rejected; known-client swaps just won't resolve.) + +5. Give the printed `api_key` to the Lightning instance. It keeps sending it as + `api_key` exactly as it does today — no other Lightning-side change. + +## Managing clients + +- **Rotate the Anthropic key** (keeping the same `api_key`/credential, so the + Lightning side needs no re-credentialling): + + ```sh + echo "$NEWKEY" | bun run client rotate acme + ``` + +- **Verify** that a client's stored key resolves under the current `APOLLO_ENC_KEY` + — reports `decrypts` / `plaintext` / `global` (NULL) / `DECRYPT_FAILED`, and exits + non-zero on failure: + + ```sh + bun run client verify acme + ``` + +- **Revoke:** `DELETE FROM lightning_clients WHERE name = '...';` directly in the + DB. Changes are picked up within ~60s (the server caches each client briefly); + restart Apollo to apply a revocation immediately. + +### `encrypt` — the lower-level subcommand + +`bun run client encrypt` prints the `enc:v1:…` value for the key on stdin and makes +**no DB write**. Useful for manual SQL / row-seeding — e.g. to add a client whose +`anthropic_api_key` is `NULL` (so it uses Apollo's global `ANTHROPIC_API_KEY`), +which `add` doesn't cover. Pair the printed value with an `auth_token_hash` you +compute yourself: + +```sh +echo "$KEY" | bun run client encrypt +``` + +## At-rest encryption + +`anthropic_api_key` is stored encrypted (AES-256-GCM) when written via the `client` +CLI (`add`/`rotate`/`encrypt`); plaintext rows are still accepted for backward +compatibility. + +- **Fail closed.** If an `enc:v1:` row can't be decrypted (wrong/missing + `APOLLO_ENC_KEY` or corrupt value), that client is dropped from the allow-list + and its requests get `401` — Apollo never falls back to the global key for an + encrypted-but-undecryptable row. A `NULL` key still means "use the global key". +- **Rotation** is manual: re-encrypt every `enc:v1:` row with the new key, then + swap `APOLLO_ENC_KEY` and restart. +- **What it protects.** The ciphertext is useless without `APOLLO_ENC_KEY`, so this + guards DB dumps, backups, read replicas, and accidental `SELECT`s in logs. It + does **not** protect a full Apollo host/process compromise: the running process + necessarily holds both the key and the decrypted values in memory. Protect the + table at rest (restricted access, DB encryption) regardless. + +The clients' `api_key` credentials are only ever stored and compared as hashes. diff --git a/platform/src/auth/client/cli.ts b/platform/src/auth/client/cli.ts new file mode 100644 index 00000000..a1aad825 --- /dev/null +++ b/platform/src/auth/client/cli.ts @@ -0,0 +1,175 @@ +// The `client` CLI: provision and manage Lightning clients in lightning_clients. +// This is the only file with import.meta.main, so importing store/commands from a +// test never parses args or exits. Run from the repo root so Bun loads .env +// (APOLLO_ENC_KEY, and APOLLO_CLIENTS_DB_URL or POSTGRES_URL): +// +// echo "$KEY" | bun run client add acme # mint + insert, prints the api_key +// echo "$NEWKEY" | bun run client rotate acme # replace the Anthropic key in place +// echo "$KEY" | bun run client encrypt # print an enc:v1: blob, no DB write +// bun run client verify acme # decrypt-check the stored key +// +// Keys are read from stdin (pipe or interactive prompt), never from argv. The +// client name is a positional argument (not secret). +import { clientsDbUrl, closeDb, getDb } from "../../db"; +import { parseEncKey } from "../../util/instance-key-crypto"; +import { requireEncKey } from "../enc-key"; +import { + ClientNotFoundError, + addClient, + encryptValue, + rotateClient, + verifyClient, + type VerifyStatus, +} from "./commands"; +import { readSecret } from "./read-secret"; +import { UNIQUE_VIOLATION } from "./store"; + +// Pre-flight the DB-backed subcommands before a secret is read from stdin, so a +// missing DB URL surfaces this guidance rather than a generic failure after the +// operator has already piped/typed the key. +function requireDbUrl(): boolean { + if (clientsDbUrl()) return true; + console.error( + "No clients DB URL is set; this reaches the database directly. Set APOLLO_CLIENTS_DB_URL\n" + + "(or POSTGRES_URL) to the instance you're working against, and run from the repo root so\n" + + "Bun reads .env." + ); + return false; +} + +function usage(): void { + console.error( + "Usage: bun run client \n\n" + + " add mint a credential, encrypt the Anthropic key (stdin), insert the row\n" + + " rotate replace an existing client's Anthropic key (stdin), keeping its credential\n" + + " encrypt print the enc:v1: blob for the key on stdin (no DB write)\n" + + " verify check the stored key decrypts under the current APOLLO_ENC_KEY\n\n" + + "Keys are read from stdin: `echo \"$KEY\" | bun run client add acme`." + ); +} + +async function runAdd(name?: string): Promise { + if (!name) { + console.error("Usage: bun run client add (Anthropic key on stdin)"); + return 1; + } + if (!requireDbUrl()) return 1; + const encKey = requireEncKey(process.env.APOLLO_ENC_KEY); + const anthropicKey = await readSecret(); + if (!anthropicKey) { + console.error("No key read from stdin."); + return 1; + } + try { + const { apiKey } = await addClient(getDb(), encKey, name, anthropicKey); + console.log(`Provisioned client "${name}". Give this api_key to the Lightning instance:`); + console.log(apiKey); + return 0; + } catch (err: any) { + if (err?.errno === UNIQUE_VIOLATION) { + console.error(`A client named "${name}" already exists. Use \`rotate\` to change its key.`); + } else { + console.error("add failed:", err?.message ?? err); + } + return 1; + } finally { + await closeDb(); + } +} + +async function runRotate(name?: string): Promise { + if (!name) { + console.error("Usage: bun run client rotate (new Anthropic key on stdin)"); + return 1; + } + if (!requireDbUrl()) return 1; + const encKey = requireEncKey(process.env.APOLLO_ENC_KEY); + const anthropicKey = await readSecret("New Anthropic key: "); + if (!anthropicKey) { + console.error("No key read from stdin."); + return 1; + } + try { + await rotateClient(getDb(), encKey, name, anthropicKey); + console.log(`Rotated the Anthropic key for client "${name}". Its api_key is unchanged.`); + return 0; + } catch (err: any) { + if (err instanceof ClientNotFoundError) { + console.error(`No client named "${name}". Use \`add\` to create one.`); + } else { + console.error("rotate failed:", err?.message ?? err); + } + return 1; + } finally { + await closeDb(); + } +} + +async function runEncrypt(): Promise { + const encKey = requireEncKey(process.env.APOLLO_ENC_KEY); + const value = await readSecret(); + if (!value) { + console.error("No value read from stdin."); + return 1; + } + console.log(encryptValue(encKey, value)); + return 0; +} + +function reportVerify(name: string, status: VerifyStatus): number { + switch (status) { + case "decrypts": + console.log(`Client "${name}": anthropic_api_key decrypts cleanly (enc:v1:).`); + return 0; + case "plaintext": + console.log(`Client "${name}": anthropic_api_key is stored as plaintext (used as-is).`); + return 0; + case "global": + console.log(`Client "${name}": anthropic_api_key is NULL, falls back to the global ANTHROPIC_API_KEY.`); + return 0; + case "decrypt_failed": + console.error(`Client "${name}": DECRYPT_FAILED. The stored enc:v1: key cannot be decrypted with the current APOLLO_ENC_KEY.`); + return 1; + case "unknown_client": + console.error(`No client named "${name}".`); + return 1; + } +} + +async function runVerify(name?: string): Promise { + if (!name) { + console.error("Usage: bun run client verify "); + return 1; + } + if (!requireDbUrl()) return 1; + // parseEncKey (not requireEncKey): a missing key is a valid DECRYPT_FAILED + // diagnosis for an encrypted row, and plaintext/NULL rows verify without one. + const encKey = parseEncKey(process.env.APOLLO_ENC_KEY); + try { + return reportVerify(name, await verifyClient(getDb(), encKey, name)); + } catch (err: any) { + console.error("verify failed:", err?.message ?? err); + return 1; + } finally { + await closeDb(); + } +} + +async function main(): Promise { + const [, , subcommand, name] = process.argv; + switch (subcommand) { + case "add": + return runAdd(name); + case "rotate": + return runRotate(name); + case "encrypt": + return runEncrypt(); + case "verify": + return runVerify(name); + default: + usage(); + return 1; + } +} + +if (import.meta.main) process.exit(await main()); diff --git a/platform/src/auth/client/commands.ts b/platform/src/auth/client/commands.ts new file mode 100644 index 00000000..c9d56931 --- /dev/null +++ b/platform/src/auth/client/commands.ts @@ -0,0 +1,98 @@ +import type { SQL } from "bun"; +import { ENC_PREFIX, decryptKey, encryptKey } from "../../util/instance-key-crypto"; +import { hashToken } from "../hash"; +import { + getClientByName, + insertClient, + mintApiKey, + updateClientKey, +} from "./store"; + +// The four client operations as plain async functions: db handle + master key in, +// result out. No console, no process.exit, no argv; cli.ts owns all of that. +// Crypto and hashing are reused from instance-key-crypto.ts and hash.ts; nothing +// here reimplements them. + +/** Thrown by rotateClient when no client carries the given name. cli.ts maps this + * to the "use add" message and a non-zero exit. */ +export class ClientNotFoundError extends Error { + constructor(public readonly clientName: string) { + super(`unknown client "${clientName}"`); + this.name = "ClientNotFoundError"; + } +} + +/** Add a client: mint an api_key, hash it (auth_token_hash), encrypt the Anthropic + * key, and insert the row. Returns the minted api_key for the operator to hand to + * Lightning. Propagates Postgres errno 23505 on a duplicate name (cli.ts maps it). */ +export async function addClient( + sql: SQL, + encKey: Buffer, + name: string, + anthropicKey: string +): Promise<{ apiKey: string }> { + const apiKey = mintApiKey(); + const authTokenHash = hashToken(apiKey); + const encAnthropic = encryptKey(anthropicKey, encKey); + await insertClient(sql, name, authTokenHash, encAnthropic); + return { apiKey }; +} + +/** Rotate a client's Anthropic key in place: encrypt the new key and UPDATE the + * row, leaving api_key/auth_token_hash untouched so Lightning keeps its + * credential. Throws ClientNotFoundError if the client doesn't exist. */ +export async function rotateClient( + sql: SQL, + encKey: Buffer, + name: string, + anthropicKey: string +): Promise { + const encAnthropic = encryptKey(anthropicKey, encKey); + const updated = await updateClientKey(sql, name, encAnthropic); + if (updated === 0) throw new ClientNotFoundError(name); +} + +/** Encrypt a value to its "enc:v1:…" form for manual SQL / row-seeding. No DB. */ +export function encryptValue(encKey: Buffer, plaintext: string): string { + return encryptKey(plaintext, encKey); +} + +/** How a stored anthropic_api_key resolves under the current APOLLO_ENC_KEY. */ +export type VerifyStatus = + | "decrypts" // "enc:v1:…" that decrypts cleanly + | "plaintext" // legacy plaintext, used as-is + | "global" // NULL -> falls back to the global ANTHROPIC_API_KEY + | "decrypt_failed" // "enc:v1:…" with no/wrong key or a corrupt blob + | "unknown_client"; // no row by that name + +/** Classify a stored value the same way instance-auth's decryptStoredKey does, but + * reporting the outcome instead of dropping the client. Pure; no DB. The branch + * order here (NULL -> global, no-prefix -> plaintext, no-key/decrypt-error -> + * fail) must track decryptStoredKey in instance-auth.ts: verify exists to predict + * the auth hook's behaviour, so the two cannot be allowed to diverge. */ +export function classifyStoredKey( + stored: string | null, + encKey: Buffer | null +): VerifyStatus { + if (stored === null) return "global"; + if (!stored.startsWith(ENC_PREFIX)) return "plaintext"; + if (!encKey) return "decrypt_failed"; + try { + decryptKey(stored, encKey); + return "decrypts"; + } catch { + return "decrypt_failed"; + } +} + +/** Operator-side decrypt check: look up the client by name and classify how its + * stored key resolves under the current APOLLO_ENC_KEY. */ +export async function verifyClient( + sql: SQL, + encKey: Buffer | null, + name: string +): Promise { + const row = await getClientByName(sql, name); + if (!row) return "unknown_client"; + return classifyStoredKey(row.anthropic_api_key, encKey); +} diff --git a/platform/src/auth/client/read-secret.ts b/platform/src/auth/client/read-secret.ts new file mode 100644 index 00000000..bfb80def --- /dev/null +++ b/platform/src/auth/client/read-secret.ts @@ -0,0 +1,46 @@ +import { createInterface } from "node:readline"; +import { Writable } from "node:stream"; + +// Read a secret (an Anthropic key) from stdin so it never lands in argv, shell +// history, or `ps`. Two paths: piped (non-TTY) reads to EOF; interactive (TTY) +// prompts on stderr and reads one line. + +/** Trim surrounding whitespace, matching hashToken's trim so a piped `echo "$KEY"` + * (trailing newline) and a typed prompt produce the same key. */ +export function trimSecret(raw: string): string { + return raw.trim(); +} + +/** Read a piped (non-TTY) stdin to EOF. The stream is injectable so the piped path + * is testable without a real terminal. */ +export async function readPipedSecret( + stream: AsyncIterable = process.stdin +): Promise { + const chunks: string[] = []; + for await (const chunk of stream) { + chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")); + } + return trimSecret(chunks.join("")); +} + +/** Prompt on stderr (so piped stdout stays clean) and read one line from a TTY. + * The prompt is written directly; readline's echo is routed to a discarding + * stream so the typed secret never reaches the terminal. */ +function readTtySecret(prompt: string): Promise { + process.stderr.write(prompt); + const muted = new Writable({ write: (_chunk, _enc, cb) => cb() }); + const rl = createInterface({ input: process.stdin, output: muted, terminal: true }); + return new Promise((resolve) => { + rl.question("", (answer) => { + process.stderr.write("\n"); + rl.close(); + resolve(trimSecret(answer)); + }); + }); +} + +/** Read a secret from stdin: piped when not a TTY, otherwise an interactive prompt. */ +export async function readSecret(prompt = "Anthropic key: "): Promise { + if (process.stdin.isTTY) return readTtySecret(prompt); + return readPipedSecret(process.stdin); +} diff --git a/platform/src/auth/client/store.ts b/platform/src/auth/client/store.ts new file mode 100644 index 00000000..c3f84468 --- /dev/null +++ b/platform/src/auth/client/store.ts @@ -0,0 +1,66 @@ +import type { SQL } from "bun"; +import { randomBytes } from "node:crypto"; + +// SQL and the small shared bits the client tooling needs. No console, no +// process.exit: callers (commands.ts/cli.ts) own I/O and exit codes. Each query +// takes the db handle as its first param, matching the rest of the app +// (getDb() is passed in rather than reached for here). + +// Postgres unique_violation. A duplicate name is the path an operator actually +// hits on `add`; a duplicate hash is effectively impossible (random 32 bytes). +export const UNIQUE_VIOLATION = "23505"; + +/** Mint a client api_key credential: 32 random bytes, base64url. Same randomBytes + * source as internal-token.ts; base64url (not hex) is the established credential + * encoding the hash contract in hash.ts is computed over. */ +export function mintApiKey(): string { + return randomBytes(32).toString("base64url"); +} + +export type ClientRow = { + name: string; + auth_token_hash: string; + anthropic_api_key: string | null; +}; + +/** Insert one client row. The name is bound as a parameter, so no value it can + * hold forms part of the SQL. Throws Postgres errno 23505 on a duplicate name. */ +export async function insertClient( + sql: SQL, + name: string, + authTokenHash: string, + encAnthropicKey: string +): Promise { + await sql` + INSERT INTO lightning_clients (name, auth_token_hash, anthropic_api_key) + VALUES (${name}, ${authTokenHash}, ${encAnthropicKey}) + `; +} + +/** Replace an existing client's encrypted Anthropic key in place, leaving + * api_key/auth_token_hash untouched so the Lightning side keeps its credential. + * Returns the number of rows updated (0 = no client by that name). */ +export async function updateClientKey( + sql: SQL, + name: string, + encAnthropicKey: string +): Promise { + const rows = (await sql` + UPDATE lightning_clients SET anthropic_api_key = ${encAnthropicKey} + WHERE name = ${name} + RETURNING name + `) as Array<{ name: string }>; + return rows.length; +} + +/** Look up one client row by name, or null if there is no such client. */ +export async function getClientByName( + sql: SQL, + name: string +): Promise { + const rows = (await sql` + SELECT name, auth_token_hash, anthropic_api_key + FROM lightning_clients WHERE name = ${name} LIMIT 1 + `) as ClientRow[]; + return rows[0] ?? null; +} diff --git a/platform/src/auth/enc-key.ts b/platform/src/auth/enc-key.ts new file mode 100644 index 00000000..38dd5ba6 --- /dev/null +++ b/platform/src/auth/enc-key.ts @@ -0,0 +1,20 @@ +import { parseEncKey } from "../util/instance-key-crypto"; + +// Shared APOLLO_ENC_KEY guard for the client CLI (auth/client/). The auth hook +// calls parseEncKey directly (it must degrade, not exit), so the exiting behaviour +// lives here, sourced from one place so every subcommand emits the same message. + +/** Parse APOLLO_ENC_KEY into a 32-byte key, or print an actionable error and exit. */ +export function requireEncKey(raw: string | undefined | null): Buffer { + const key = parseEncKey(raw); + if (key) return key; + console.error( + "APOLLO_ENC_KEY not found (needs to be base64 of exactly 32 bytes; it\n" + + "encrypts the Anthropic key).\n\n" + + "Run this from the repo root so Bun picks up .env. If it's still missing,\n" + + "the key isn't in .env yet. Add one with:\n\n" + + ' echo "APOLLO_ENC_KEY=$(openssl rand -base64 32)" >> .env\n\n' + + "then re-run (and restart Apollo so it can decrypt at runtime)." + ); + process.exit(1); +} diff --git a/platform/src/auth/hash.ts b/platform/src/auth/hash.ts new file mode 100644 index 00000000..b3abd7d7 --- /dev/null +++ b/platform/src/auth/hash.ts @@ -0,0 +1,13 @@ +import { createHash } from "node:crypto"; + +/** SHA-256 hex of a client credential, computed over its UTF-8 bytes. This is the + * one definition of the hash contract: provisioning writes it, the auth hook looks + * it up. Apollo is TS-only on this path, so there is no cross-language duplicate. + * + * Trims leading and trailing whitespace before hashing. The credential is its + * trimmed form: a token minted with `randomBytes(32).toString("base64url")` has + * no whitespace, so trim is identity for every stored hash, but a copy-paste + * newline or stray space at either call site can no longer split the contract. */ +export function hashToken(token: string): string { + return createHash("sha256").update(token.trim()).digest("hex"); +} diff --git a/platform/src/auth/instance-auth.ts b/platform/src/auth/instance-auth.ts new file mode 100644 index 00000000..a352d745 --- /dev/null +++ b/platform/src/auth/instance-auth.ts @@ -0,0 +1,385 @@ +import type { ApolloError } from "../util/errors"; +import { serviceUnavailable, unauthorized } from "../util/errors"; +import { ENC_PREFIX, decryptKey, parseEncKey } from "../util/instance-key-crypto"; +import { clientsDbUrl, getDb } from "../db"; +import { hashToken } from "./hash"; +import { checkInternalHeader } from "./internal-token"; +import { captureException } from "../util/sentry"; + +export type Client = { name: string; anthropicKey: string | null }; + +// Resolve a client by token hash. Used both for the injected pre-cache bypass +// (lookup) and the injected per-hash DB read driving the cache (dbLookup). +type Lookup = (hash: string) => Promise | Client | null; + +// Per-hash cache entry. An absent key means "never checked"; a "miss" means +// "checked, confirmed unknown". The two are distinguishable so a verified miss +// can be cached without being mistaken for a cold slot. +type CacheEntry = + | { kind: "hit"; client: Client; checkedAt: number } + | { kind: "miss"; checkedAt: number }; + +// Outcome of resolving one token hash. "unavailable" (DB never came up, or the read +// threw) is kept distinct from "absent" (lookup completed, no such client) so the auth +// hook can answer a non-sk-ant- caller with a retryable 503 instead of a misleading 401. +type LookupResult = + | { kind: "found"; client: Client } + | { kind: "absent" } + | { kind: "unavailable" }; + +/** Resolution of which key the outgoing payload carries. The names are + * load-bearing: services.ts dispatches them in a named switch so the + * inbound-credential-never-forwarded invariant is structural, not positional. */ +export type KeyResolution = + | { kind: "useKey"; key: string } // known client: swap in its stored Anthropic key + | { kind: "useGlobal" } // known client, NULL stored key: drop field -> global key + | { kind: "forward" } // unknown caller past the shape check: leave body as received + | { kind: "passthrough" }; // internal apollo() hop: leave body exactly as received + +// Anthropic keys are prefixed sk-ant-; our client credentials are base64url, so the +// two shapes don't collide. An unknown key without this prefix is treated as a +// (likely Lightning) credential and rejected rather than forwarded to the LLM. +const ANTHROPIC_KEY_PREFIX = "sk-ant-"; + +const CACHE_TTL_MS = 60_000; +// Tunable ceiling: the longest a stale entry survives a broken DB before eviction. +// Scales with the TTL by design (Stu signed off on the 3x multiple). +const MAX_STALENESS_MS = CACHE_TTL_MS * 3; + +// Returned when an encrypted key can't be decrypted: that client is omitted (fail +// closed) rather than falling back to the global key, which would mis-bill its usage. +const DECRYPT_FAILED = Symbol("decrypt-failed"); + +export interface InstanceAuthOptions { + // Master key for decrypting stored anthropic_api_key values. When omitted, read + // from APOLLO_ENC_KEY at init(); pass explicitly in tests to skip the env. + encKey?: Buffer | null; + // Test injection: a synchronous client resolver that bypasses the cache/DB path + // entirely. Production leaves this unset. + lookup?: Lookup | null; + // Test injection: a per-hash DB read that drives the real cache/single-flight/ + // staleness logic in place of Postgres. Setting it implies the DB is ready. + dbLookup?: Lookup | null; +} + +// Read a query param straight off the request URL. Fallback for the WS-upgrade hook, +// where Elysia may not have populated ctx.query before beforeHandle runs. +function queryParam(ctx: any, name: string): string { + const url = ctx?.request?.url; + if (typeof url !== "string") return ""; + try { + return new URL(url).searchParams.get(name)?.trim() ?? ""; + } catch { + return ""; + } +} + +// Map a completed lookup (Client | null) to a found/absent LookupResult. "unavailable" +// is never produced here; it arises only where the DB-backed path cannot complete a +// read (db not ready, or the read threw). +function toLookupResult(client: Client | null): LookupResult { + return client ? { kind: "found", client } : { kind: "absent" }; +} + +/** + * The instance-auth surface, owning all of what used to be module-level state: + * the per-client cache, single-flight handles, dbReady flag, and the encryption + * key. One instance per process is created in server.ts; tests construct their own + * with injected lookups, so there are no test-seam exports and no module globals. + */ +export class InstanceAuth { + // False until the lightning_clients lookup is usable. A down DB means no client + // can be resolved, so every caller degrades to the shape-checked forward path. + private dbReady = false; + private readonly clientCache = new Map(); + // Single-flight handles per hash: concurrent lookups for the same token share one + // promise, so a burst can never trigger more than one DB read for that hash. + private readonly lookupInFlight = new Map>(); + private encKey: Buffer | null; + private readonly lookupOverride: Lookup | null; + private readonly dbLookupOverride: Lookup | null; + + constructor(opts: InstanceAuthOptions = {}) { + this.encKey = opts.encKey ?? null; + this.lookupOverride = opts.lookup ?? null; + this.dbLookupOverride = opts.dbLookup ?? null; + // Injecting a per-hash DB read implies a reachable DB; mirror the old test seam. + if (this.dbLookupOverride) this.dbReady = true; + } + + // The auth hook is always active. This reads APOLLO_ENC_KEY and probes the + // lightning_clients lookup so known clients can be resolved; if the DB is + // unreachable, dbReady stays false and every caller degrades to the shape-checked + // forward path (it does not blanket-reject). + async init(): Promise { + this.encKey = parseEncKey(process.env.APOLLO_ENC_KEY); + if (process.env.APOLLO_ENC_KEY && !this.encKey) { + console.error( + "Apollo instance auth: APOLLO_ENC_KEY is set but is not valid base64 of 32 bytes; encrypted client keys cannot be decrypted and those clients will be REJECTED." + ); + } + + if (!clientsDbUrl()) { + this.dbReady = false; + console.warn( + "Apollo instance auth: neither APOLLO_CLIENTS_DB_URL nor POSTGRES_URL is set, so known clients cannot be looked up; callers fall to the shape-checked forward path." + ); + return; + } + + // Migrations have already run (server.ts), so the table exists if the DB is up. + // The probe is now just a reachability check that sets dbReady. + try { + await getDb()`SELECT 1`; + this.dbReady = true; + this.clientCache.clear(); // force a fresh load on the first request + console.log("Apollo instance auth: lightning_clients lookup ready."); + } catch (err) { + this.dbReady = false; + console.error( + "Apollo instance auth: the database could not be reached, so known-client swaps will not resolve; callers fall to the shape-checked forward path.", + err + ); + } + } + + // null => global env key; "enc:v1:…" => AES-256-GCM decrypt (DECRYPT_FAILED on + // error); anything else => legacy plaintext. + private decryptStoredKey( + stored: string | null, + clientName: string + ): string | null | typeof DECRYPT_FAILED { + if (stored === null) return null; + if (!stored.startsWith(ENC_PREFIX)) return stored; + if (!this.encKey) { + console.error( + `Apollo instance auth: client "${clientName}" has an encrypted anthropic_api_key but APOLLO_ENC_KEY is unset/invalid; omitting this client (fail closed).` + ); + captureException( + new Error( + "Apollo instance auth: encrypted client key but APOLLO_ENC_KEY unset/invalid" + ), + { reason: "missing-enc-key", client: clientName } + ); + return DECRYPT_FAILED; + } + try { + return decryptKey(stored, this.encKey); + } catch (err) { + console.error( + `Apollo instance auth: could not decrypt anthropic_api_key for client "${clientName}"; omitting this client (fail closed).`, + err + ); + captureException(err, { reason: "decrypt-error", client: clientName }); + return DECRYPT_FAILED; + } + } + + /** Turn one lightning_clients row into a Client, dropping it (fail closed) if its + * encrypted key can't be decrypted. */ + rowToClient(row: { + name: string; + anthropic_api_key: string | null; + }): Client | null { + const key = this.decryptStoredKey(row.anthropic_api_key, row.name); + if (key === DECRYPT_FAILED) return null; + return { name: row.name, anthropicKey: key }; + } + + // One targeted query for a single hash. Replaces the unbounded bulk SELECT: an + // unknown token now costs at most one round-trip on first sight, then a cached miss. + private async queryClient(hash: string): Promise { + const rows = (await getDb()` + SELECT name, anthropic_api_key + FROM lightning_clients WHERE auth_token_hash = ${hash} LIMIT 1 + `) as Array<{ + name: string; + anthropic_api_key: string | null; + }>; + const row = rows[0]; + return row ? this.rowToClient(row) : null; + } + + // Single-flight DB read for one hash: a concurrent caller for the same hash joins + // the in-flight promise (set synchronously before the await yields, so an eviction + // followed by a burst still produces one query). The handle is cleared in finally + // so a failed lookup never wedges the slot. + private loadClient(hash: string): Promise { + const inFlight = this.lookupInFlight.get(hash); + if (inFlight) return inFlight; + const read = this.dbLookupOverride ?? ((h: string) => this.queryClient(h)); + const promise = Promise.resolve(read(hash)).finally(() => { + this.lookupInFlight.delete(hash); + }); + this.lookupInFlight.set(hash, promise); + return promise; + } + + private cacheResult(hash: string, client: Client | null): Client | null { + this.clientCache.set( + hash, + client + ? { kind: "hit", client, checkedAt: Date.now() } + : { kind: "miss", checkedAt: Date.now() } + ); + return client; + } + + private async lookupClient(hash: string): Promise { + if (!this.dbReady) return { kind: "unavailable" }; // lookup never came up -> cannot verify + + const entry = this.clientCache.get(hash); + if (entry) { + const age = Date.now() - entry.checkedAt; + if (age <= CACHE_TTL_MS) { + return toLookupResult(entry.kind === "hit" ? entry.client : null); + } + if (age <= MAX_STALENESS_MS) { + // Stale but within the ceiling: serve the cached value now and refresh once + // in the background. A failed refresh leaves the entry to age further; only + // the single-flight read fires, so a burst at the boundary triggers one call. + void this.loadClient(hash) + .then((client) => this.cacheResult(hash, client)) + .catch((err) => { + // Guard the catch body: a throw inside the log/capture on this voided + // chain would otherwise surface as an unhandledRejection. + try { + console.error( + "Apollo instance auth: background refresh of a cached client failed; serving the stale entry until it ages out.", + err + ); + captureException(err, { + reason: "stale-refresh-error", + client: entry.kind === "hit" ? entry.client.name : null, + }); + } catch {} + }); + return toLookupResult(entry.kind === "hit" ? entry.client : null); + } + // Beyond the ceiling: evict and fall through to a cold, awaited lookup rather + // than serve a possibly-revoked client. + this.clientCache.delete(hash); + console.warn( + `Apollo instance auth: cache entry for a client token exceeded the max-staleness ceiling (${MAX_STALENESS_MS}ms); evicting and re-checking the database.` + ); + } + + // Cold (or just-evicted): every concurrent caller awaits the one shared read. On + // failure cache nothing and fail closed (a former hit now rejects; a former miss + // simply re-queries next time). + try { + const client = await this.loadClient(hash); + this.cacheResult(hash, client); + return toLookupResult(client); + } catch (err) { + console.error( + "Apollo instance auth: client lookup failed against the database; rejecting this request (fail closed).", + err + ); + return { kind: "unavailable" }; + } + } + + /** + * Client-credential authentication: the /services/* onBeforeHandle hook. Internal-call + * exemption is checked first and short-circuits; otherwise the inbound api_key is + * hashed and looked up. The auth hook is always active but only rejects two cases: a + * forged internal header, and an unknown non-sk-ant- key (a likely Lightning + * credential we must not forward to the LLM). Returning a value short-circuits the + * request with that body. + */ + authenticate = async (ctx: any): Promise => { + // Internal-call exemption wins precedence: Python children echo back the + // internal token (services/util.py apollo()), so such calls skip the api_key + // check. External callers can't forge it; it's a per-process secret never sent + // to clients. + const internal = checkInternalHeader(ctx); + if (internal.kind === "match") { + // Flag so the key resolves to passthrough: the forwarded api_key is left + // untouched rather than stripped, which would mis-bill a per-client key + // passed down an apollo() hop. + ctx.internalCall = true; + return; + } + if (internal.kind === "mismatch") { + // A non-empty internal header is a claim to be Apollo itself; a mismatch is + // either a sibling process without a shared APOLLO_INTERNAL_TOKEN or a forged + // header. Reject outright, never re-try as an external api_key caller, so a + // wrong internal header can't ride in on a valid body credential. + console.warn( + "Apollo internal token MISMATCH: x-apollo-internal present but does not match; likely a sibling process without a shared APOLLO_INTERNAL_TOKEN, or a forged header. Rejecting with 401." + ); + captureException( + new Error("Apollo instance auth: internal token mismatch"), + { reason: "internal-token-mismatch" } + ); + return unauthorized(ctx); + } + + // The credential is the api_key the caller sends. POST puts it in the body; a WS + // upgrade is a bodyless GET, so it rides as the ?api_key= query param instead + // (ctx.query, with a URL fallback for hooks where Elysia hasn't parsed it yet). + // A query-string token shows up in access/proxy logs, which is acceptable here: Apollo + // is internal and the token is hashed at rest, so a log leak doesn't expose the + // stored Anthropic key. Don't "fix" this by moving to a header: browsers can't + // set headers on a WS upgrade. No key at all takes the forward path (-> global + // key), so only proceed to lookup when one is present. + const apiKey = + (typeof ctx.body?.api_key === "string" ? ctx.body.api_key.trim() : "") || + (typeof ctx.query?.api_key === "string" ? ctx.query.api_key.trim() : "") || + queryParam(ctx, "api_key"); + if (!apiKey) return; + + const hash = hashToken(apiKey); + const result: LookupResult = this.lookupOverride + ? toLookupResult(await this.lookupOverride(hash)) + : await this.lookupClient(hash); + if (result.kind === "found") { + ctx.lightningClient = result.client; + return; + } + + // An sk-ant- key is a bring-your-own Anthropic key: it needs no client lookup and + // is forwarded unchanged even during a store outage, so it NEVER reaches the 503 + // below. On a WS upgrade it only rode the query string, so record it for the + // message handler to fold into the outgoing payload. + if (apiKey.startsWith(ANTHROPIC_KEY_PREFIX)) { + ctx.forwardApiKey = apiKey; + return; + } + + // Non-sk-ant- from here. Split on whose fault the failure is: + // - unavailable: we could not complete the lookup (DB never came up, or the read + // threw). That is our outage and is retryable, so 503, never a misleading 401, + // never a silent forward of a likely Lightning credential. + // - absent: the lookup completed and confirmed no such client, so 401 as before. + if (result.kind === "unavailable") { + captureException( + new Error( + "Apollo instance auth: client store unavailable; returning 503 rather than a misleading 401" + ), + { reason: "client-store-unavailable-503", tokenHash: hash } + ); + return serviceUnavailable(ctx); + } + return unauthorized(ctx); + }; + + /** + * Anthropic-key resolver. The result is dispatched by a named switch in + * services.ts; the inbound credential is never forwarded to the LLM in the + * useKey/useGlobal cases. Internal hops pass through untouched; a known client + * either swaps in its stored key or (NULL) falls back to the global key; every + * other caller forwards the body as received. + */ + resolveKey = (ctx: any): KeyResolution => { + if (ctx?.internalCall) return { kind: "passthrough" }; + const client = ctx?.lightningClient as Client | undefined; + if (client) { + return client.anthropicKey + ? { kind: "useKey", key: client.anthropicKey } + : { kind: "useGlobal" }; + } + return { kind: "forward" }; + }; +} diff --git a/platform/src/auth/internal-token.ts b/platform/src/auth/internal-token.ts new file mode 100644 index 00000000..225c65c6 --- /dev/null +++ b/platform/src/auth/internal-token.ts @@ -0,0 +1,62 @@ +import { randomBytes, timingSafeEqual } from "node:crypto"; + +// The internal-call exemption: a per-process secret that identifies genuine +// Apollo-to-Apollo calls. The bridge injects it into each Python child's env, so +// services/util.py apollo() echoes it back via the internal header; the auth hook +// exempts requests carrying it without trusting network position. +// +// MULTI-PROCESS: when APOLLO_INTERNAL_TOKEN is unset, each process mints its OWN +// token. apollo() self-calls hit 127.0.0.1:{port} and normally land on the same +// process, but if processes share a port (SO_REUSEPORT / clustering) a self-call +// can hit a sibling and 401. Set APOLLO_INTERNAL_TOKEN to the SAME value across +// processes in that case. + +export const INTERNAL_HEADER = "x-apollo-internal"; + +// Whether the token came from the environment (shared, topology-safe) or was +// minted per-process. Drives the startup provenance log/warn. +const fromEnv = !!process.env.APOLLO_INTERNAL_TOKEN; +const token = process.env.APOLLO_INTERNAL_TOKEN ?? randomBytes(32).toString("hex"); + +/** The per-process internal token. bridge.ts injects this into spawned Python + * children so their apollo() self-calls are recognised. */ +export function getInternalToken(): string { + return token; +} + +export function internalAuthHeader(): Record { + return { [INTERNAL_HEADER]: token }; +} + +/** Constant-time string compare, length-guarded. */ +function safeEqual(a: string, b: string): boolean { + const ab = Buffer.from(a); + const bb = Buffer.from(b); + return ab.length === bb.length && timingSafeEqual(ab, bb); +} + +export type InternalCheck = + | { kind: "absent" } // no internal header; fall through to the credential check + | { kind: "match" } // genuine Apollo-to-Apollo call, exempt + | { kind: "mismatch" }; // a claim to be Apollo that doesn't match, reject + +/** Inspect the request's internal header against this process's token. */ +export function checkInternalHeader(ctx: any): InternalCheck { + const header = ctx?.request?.headers?.get?.(INTERNAL_HEADER) ?? ""; + if (!header) return { kind: "absent" }; + return safeEqual(header, token) ? { kind: "match" } : { kind: "mismatch" }; +} + +/** Startup provenance log; warns when a minted token meets reusePort. */ +export function logInternalTokenProvenance(reusePort = false): void { + console.log( + fromEnv + ? "Apollo internal token: from APOLLO_INTERNAL_TOKEN (safe for any topology)." + : "Apollo internal token: minted per-process (safe only for single-process-per-host; set APOLLO_INTERNAL_TOKEN in production)." + ); + if (reusePort && !fromEnv) { + console.warn( + "Apollo internal token: reusePort is ON but the token was minted per-process, so apollo() self-calls may land on a sibling process and 401. Set APOLLO_INTERNAL_TOKEN to the SAME value across all processes." + ); + } +} diff --git a/platform/src/bridge.ts b/platform/src/bridge.ts index 5819f2fa..cbb46abf 100644 --- a/platform/src/bridge.ts +++ b/platform/src/bridge.ts @@ -2,6 +2,7 @@ import readline from "node:readline"; import path from "node:path"; import { spawn } from "node:child_process"; import { rm } from "node:fs/promises"; +import { getInternalToken } from "./auth/internal-token"; /** Run a python script @@ -42,7 +43,10 @@ export const run = async ( ...(outputPath ? ["--output", outputPath] : []), ...(port ? ["--port", `${port}`] : []), ], - {} + // Hand the internal token to the child explicitly so its apollo() self-calls + // are recognised by the auth hook. Spawned from here (the honest owner) rather than + // written back onto this process's env. + { env: { ...process.env, APOLLO_INTERNAL_TOKEN: getInternalToken() } } ); proc.on("error", async (err) => { diff --git a/platform/src/db/index.ts b/platform/src/db/index.ts new file mode 100644 index 00000000..58404f8a --- /dev/null +++ b/platform/src/db/index.ts @@ -0,0 +1,45 @@ +import { SQL } from "bun"; + +// Shared JS-side Postgres handle. Every TS DB consumer (the auth hook, provisioning) +// imports getDb() rather than constructing its own SQL — one pool per process, with +// consistent config and a single close() for graceful shutdown. + +// bun:sql's pool is unbounded by default; cap it so a burst of auth checks can't +// exhaust Postgres's connection limit. +const MAX_CONNECTIONS = 5; + +let sql: SQL | null = null; + +// The client-auth table (lightning_clients) can live in its own database, set via +// APOLLO_CLIENTS_DB_URL, so in staging/prod the credentials sit apart from the +// docs data on POSTGRES_URL. The fallback keeps local dev to one var: set only +// POSTGRES_URL and everything shares a single DB. Every TS DB consumer resolves the +// URL through here, so the two sides can't drift apart silently. +export function clientsDbUrl(): string | undefined { + return process.env.APOLLO_CLIENTS_DB_URL ?? process.env.POSTGRES_URL; +} + +/** Shared pooled connection, opened lazily on first call. See clientsDbUrl(). */ +export function getDb(): SQL { + if (sql) return sql; + const url = clientsDbUrl(); + if (!url) { + throw new Error( + "Neither APOLLO_CLIENTS_DB_URL nor POSTGRES_URL is set; cannot open a database connection." + ); + } + console.log( + process.env.APOLLO_CLIENTS_DB_URL + ? "clients DB: using APOLLO_CLIENTS_DB_URL." + : "clients DB: falling back to POSTGRES_URL." + ); + sql = new SQL({ url, max: MAX_CONNECTIONS }); + return sql; +} + +/** Close the shared pool and drop the handle so a later getDb() reopens cleanly. */ +export async function closeDb(): Promise { + if (!sql) return; + await sql.close(); + sql = null; +} diff --git a/platform/src/db/migrate.ts b/platform/src/db/migrate.ts new file mode 100644 index 00000000..4f3c3ae4 --- /dev/null +++ b/platform/src/db/migrate.ts @@ -0,0 +1,73 @@ +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; +import { clientsDbUrl, closeDb, getDb } from "./index"; + +// Canonical migrations location. .sql files here are applied in lexical order; +// applied filenames are recorded in _migrations so re-runs are a no-op (the +// version table is the source of truth, not IF NOT EXISTS guards in the DDL). +const MIGRATIONS_DIR = join(import.meta.dir, "../../migrations"); + +// Fixed key for the session/xact advisory lock that serialises the runner. +// Every instance uses the same key, so concurrent starters queue on it. +const MIGRATION_LOCK_KEY = 8314_2025; + +/** Apply any migrations not yet recorded. Returns the count applied this run. */ +export async function runMigrations(): Promise { + const sql = getDb(); + + const files = (await readdir(MIGRATIONS_DIR)) + .filter((f) => f.endsWith(".sql")) + .sort(); + + return await sql.begin(async (tx) => { + // Hold an advisory lock for the whole transaction: a racing instance waits + // here, then sees the migrations already recorded rather than colliding on + // CREATE TABLE. The lock releases automatically when the transaction ends. + await tx`SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_KEY})`; + + await tx` + CREATE TABLE IF NOT EXISTS _migrations ( + filename TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `; + + const applied = (await tx`SELECT filename FROM _migrations`) as Array<{ + filename: string; + }>; + const done = new Set(applied.map((r) => r.filename)); + + const pending = files.filter((f) => !done.has(f)); + for (const file of pending) { + const ddl = await Bun.file(join(MIGRATIONS_DIR, file)).text(); + await tx.unsafe(ddl); + await tx`INSERT INTO _migrations (filename) VALUES (${file})`; + } + + return pending.length; + }); +} + +// Standalone entrypoint: `bun run migrate` applies the platform/auth schema +// (lightning_clients, _migrations) and exits. The Python services own and +// self-initialise their own table, so this deliberately does not touch it. The +// server startup call (server.ts) is unaffected: import.meta.main is false there. +if (import.meta.main) { + if (!clientsDbUrl()) { + console.error( + "No clients DB URL is set; nothing to migrate against. Set APOLLO_CLIENTS_DB_URL\n" + + "(or POSTGRES_URL) to the instance you're migrating, and run from the repo root so\n" + + "Bun reads .env." + ); + process.exit(1); + } + try { + const applied = await runMigrations(); + console.log(`Applied ${applied} platform migration(s) (lightning_clients, _migrations).`); + } catch (err: any) { + console.error("Migration failed:", err?.message ?? err); + process.exitCode = 1; + } finally { + await closeDb(); + } +} diff --git a/platform/src/index.ts b/platform/src/index.ts index 380787e8..2f16458d 100644 --- a/platform/src/index.ts +++ b/platform/src/index.ts @@ -1,3 +1,6 @@ +import { initSentry } from "./util/sentry"; import start from "./server"; +initSentry(); + start(process.env.PORT); diff --git a/platform/src/middleware/services.ts b/platform/src/middleware/services.ts index 125e425e..c09e06ad 100644 --- a/platform/src/middleware/services.ts +++ b/platform/src/middleware/services.ts @@ -7,6 +7,9 @@ import describeModules, { type ModuleDescription, } from "../util/describe-modules"; import { isApolloError } from "../util/errors"; +import type { InstanceAuth } from "../auth/instance-auth"; + +const textEncoder = new TextEncoder(); const callService = ( m: ModuleDescription, @@ -23,10 +26,50 @@ const callService = ( } }; -export default async (app: Elysia, port: number) => { +export default async (app: Elysia, port: number, auth: InstanceAuth) => { console.log("Loading routes:"); const modules = await describeModules(path.resolve("./services")); + + // Apply the resolved key to an outgoing payload with an explicit switch so the + // inbound-credential-never-forwarded invariant is structural, not positional: a + // known client's stored key is swapped in (useKey), a NULL stored key drops the + // field so Python uses the global key (useGlobal), and every other caller forwards + // the body exactly as received (forward/passthrough). `ctx` is the upgrade-time + // context that carries lightningClient/internalCall: on POST the route ctx, on WS + // the captured ws.data, never a fresh per-message one. + const applyKey = (payload: Record, ctx: any) => { + const resolution = auth.resolveKey(ctx); + switch (resolution.kind) { + case "useKey": + payload.api_key = resolution.key; + break; + case "useGlobal": + delete payload.api_key; + break; + case "forward": + case "passthrough": + break; + default: { + // Exhaustiveness guard: a new KeyResolution tag must be a compile error + // here, not a silent forward of the inbound credential. + const _exhaustive: never = resolution; + throw new Error( + `unhandled KeyResolution: ${(resolution as { kind: string }).kind}` + ); + } + } + return payload; + }; + + const buildPayload = (ctx: any) => + applyKey({ ...(ctx.body ?? {}), session_id: ctx.uuid }, ctx); + app.group("/services", (app) => { + // Resolve every /services/* caller: swap a known client's key, forward an + // unknown sk-ant- (or absent) key, reject a forged internal header or an + // unknown non-sk-ant- key. + app.onBeforeHandle(auth.authenticate); + modules.forEach((m) => { const { name, readme } = m; console.log(" - mounted /services/" + name); @@ -34,10 +77,7 @@ export default async (app: Elysia, port: number) => { // simple post app.post(name, async (ctx) => { console.log(`POST /services/${name}: ${ctx.uuid}`); - const payload = { - ...(ctx.body ?? {}), - session_id: ctx.uuid, - }; + const payload = buildPayload(ctx); const result = await callService(m, port, payload as any); if (isApolloError(result)) { @@ -55,14 +95,10 @@ export default async (app: Elysia, port: number) => { // HTTP streaming app.post(`${name}/stream`, async (ctx) => { console.log(`STREAM START /services/${name}: ${ctx.uuid}`); - const payload = { - ...(ctx.body ?? {}), - session_id: ctx.uuid, - }; + const payload = buildPayload(ctx); const stream = new ReadableStream({ async start(controller) { - const encoder = new TextEncoder(); let isClosed = false; const sendSSE = (event: string, data: any) => { @@ -74,7 +110,7 @@ export default async (app: Elysia, port: number) => { data )}\n\n`; // console.log(message.trim()); - controller.enqueue(encoder.encode(message)); + controller.enqueue(textEncoder.encode(message)); } catch (error) { // Stream may have been closed isClosed = true; @@ -133,6 +169,12 @@ export default async (app: Elysia, port: number) => { // TODO in the web socket API, does it make more sense to open a socket at root // and then pick the service you want? So you'd connect to /ws an send { call: 'echo', payload: {} } app.ws(name, { + // Run the auth hook on the WS upgrade. The handshake is a bodyless GET, so a + // known client rides its credential as the ?api_key= query param (see + // auth.authenticate); the auth hook hashes and resolves it just like POST, stashing + // lightningClient on the upgrade context. ws.data is that same context, so + // the message handler resolves the outgoing key off it. + beforeHandle: auth.authenticate, open() { console.log(`Websocket connected at /services/${name}`); }, @@ -153,7 +195,17 @@ export default async (app: Elysia, port: number) => { }); }; - callService(m, port, message.data as any, onLog, onEvent).then( + // The credential rode the upgrade query string, not the message body. + // Seed a forwardable unknown key onto the payload so applyKey's + // forward case preserves it; a known client's useKey/useGlobal then + // overrides or drops it exactly as on POST. + const base: Record = { ...(message.data ?? {}) }; + if (base.api_key == null && (ws.data as any)?.forwardApiKey) { + base.api_key = (ws.data as any).forwardApiKey; + } + const payload = applyKey(base, ws.data); + + callService(m, port, payload as any, onLog, onEvent).then( (result) => { ws.send({ event: "complete", @@ -169,7 +221,7 @@ export default async (app: Elysia, port: number) => { }); // TODO: it would be lovely to render the markdown into nice rich html - app.get(`/${name}/README.md`, async (ctx) => readme); + app.get(`${name}/README.md`, async (ctx) => readme); }); return app; diff --git a/platform/src/server.ts b/platform/src/server.ts index 0d334ef0..7c99b2d8 100644 --- a/platform/src/server.ts +++ b/platform/src/server.ts @@ -5,9 +5,19 @@ import setupHealthcheck from "./middleware/healthcheck"; import setupServices from "./middleware/services"; import { html } from "@elysiajs/html"; import logRequest from "./util/log-request"; +import { InstanceAuth } from "./auth/instance-auth"; +import { logInternalTokenProvenance } from "./auth/internal-token"; +import { captureException } from "./util/sentry"; +import { clientsDbUrl, closeDb } from "./db"; +import { runMigrations } from "./db/migrate"; import { randomUUID } from "node:crypto"; -export default async (port: number | string = 3000) => { +export default async ( + port: number | string = 3000, + // One instance per process, shared by the auth hook and the key resolver. Tests + // pass a pre-configured instance (fake lookup) instead of the live DB-backed one. + auth: InstanceAuth = new InstanceAuth() +) => { const app = new Elysia(); app.use(html()); @@ -15,9 +25,45 @@ export default async (port: number | string = 3000) => { app.derive(() => ({ start: Date.now(), uuid: randomUUID() })); app.onAfterHandle(logRequest); + // Report unhandled throws to Sentry, then return nothing so Elysia produces + // its normal error response (returning a value would replace the body/status). + app.onError(({ error }) => { + captureException(error); + }); + await setupHealthcheck(app); await setupDir(app); - await setupServices(app, +port); + await setupServices(app, +port, auth); + + // Bring the schema up to date before auth probes it. Without a clients DB URL + // there is nothing to migrate; auth.init() then handles the fail-closed path on + // its own. + if (clientsDbUrl()) { + try { + const applied = await runMigrations(); + console.log( + applied > 0 ? `${applied} migration(s) applied.` : "Schema up to date." + ); + } catch (err) { + console.error("Apollo migrations failed to run.", err); + } + } + + // app.listen below sets no reusePort, so the multi-process internal-token warn + // is dormant; pass the flag here if clustering is ever enabled. + logInternalTokenProvenance(false); + await auth.init(); + + // No stop path exists otherwise; close the DB pool so a graceful pod termination + // (or Ctrl-C in dev) exits cleanly without orphaned Postgres connections. In-flight + // requests, open SSE streams, and spawned Python children are intentionally not + // drained — termination drops them rather than waiting them out. + const shutdown = async () => { + await closeDb(); + process.exit(0); + }; + process.on("SIGTERM", shutdown); + process.on("SIGINT", shutdown); console.log("Apollo Server listening on ", port); app.listen(port); diff --git a/platform/src/util/describe-modules.ts b/platform/src/util/describe-modules.ts index 0f818b71..12215e71 100644 --- a/platform/src/util/describe-modules.ts +++ b/platform/src/util/describe-modules.ts @@ -13,7 +13,6 @@ export type ModuleDescription = { readme?: string; }; -// TODO this is just a stub right now export default async (location: string): Promise => { const dirs = await readdir(location, { withFileTypes: true }); const services = dirs.filter( diff --git a/platform/src/util/errors.ts b/platform/src/util/errors.ts index eefbdc75..541aac87 100644 --- a/platform/src/util/errors.ts +++ b/platform/src/util/errors.ts @@ -8,3 +8,29 @@ export interface ApolloError { export function isApolloError(value: any): value is ApolloError { return value && typeof value.code === 'number'; } + +/** Build an ApolloError and set the matching HTTP status on the Elysia context, + * so every error path produces the same envelope shape from one definition. */ +export function apolloError( + ctx: any, + code: number, + type: string, + message: string, + details?: Record +): ApolloError { + if (ctx?.set) ctx.set.status = code; + return { code, type, message, ...(details ? { details } : {}) }; +} + +export function unauthorized(ctx: any): ApolloError { + return apolloError(ctx, 401, "UNAUTHORIZED", "Missing or invalid API key"); +} + +export function serviceUnavailable(ctx: any): ApolloError { + return apolloError( + ctx, + 503, + "SERVICE_UNAVAILABLE", + "Client verification is temporarily unavailable" + ); +} diff --git a/platform/src/util/instance-key-crypto.ts b/platform/src/util/instance-key-crypto.ts new file mode 100644 index 00000000..d180c639 --- /dev/null +++ b/platform/src/util/instance-key-crypto.ts @@ -0,0 +1,46 @@ +// AES-256-GCM helpers for the per-client anthropic_api_key in lightning_clients. +// Shared by the auth middleware (decrypt) and the client CLI (auth/client/, encrypt) +// so the byte format can't drift. Stored format: "enc:v1:"; master key is APOLLO_ENC_KEY (base64 of 32 bytes). Values without +// the prefix are treated as legacy plaintext elsewhere, so encryption is opt-in. +import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; + +export const ENC_PREFIX = "enc:v1:"; +const IV_BYTES = 12; // GCM nonce +const TAG_BYTES = 16; // GCM auth tag + +/** Decode APOLLO_ENC_KEY (base64 of exactly 32 bytes) into a key Buffer, or null if absent/malformed. */ +export function parseEncKey(raw: string | undefined | null): Buffer | null { + if (!raw) return null; + let buf: Buffer; + try { + buf = Buffer.from(raw.trim(), "base64"); + } catch { + return null; + } + return buf.length === 32 ? buf : null; +} + +export function encryptKey(plaintext: string, key: Buffer): string { + const iv = randomBytes(IV_BYTES); + const cipher = createCipheriv("aes-256-gcm", key, iv); + const ciphertext = Buffer.concat([ + cipher.update(plaintext, "utf8"), + cipher.final(), + ]); + const tag = cipher.getAuthTag(); + return ENC_PREFIX + Buffer.concat([iv, tag, ciphertext]).toString("base64"); +} + +/** Decrypt an "enc:v1:…" value; throws on wrong key, corrupt value, or failed auth tag. */ +export function decryptKey(stored: string, key: Buffer): string { + const blob = Buffer.from(stored.slice(ENC_PREFIX.length), "base64"); + const iv = blob.subarray(0, IV_BYTES); + const tag = blob.subarray(IV_BYTES, IV_BYTES + TAG_BYTES); + const ciphertext = blob.subarray(IV_BYTES + TAG_BYTES); + const decipher = createDecipheriv("aes-256-gcm", key, iv); + decipher.setAuthTag(tag); + return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString( + "utf8" + ); +} diff --git a/platform/src/util/sentry.ts b/platform/src/util/sentry.ts new file mode 100644 index 00000000..23634609 --- /dev/null +++ b/platform/src/util/sentry.ts @@ -0,0 +1,48 @@ +import * as Sentry from "@sentry/bun"; + +// Mirrors the Python side (services/entry.py): all errors captured, traces +// sampled per environment. +const TRACE_RATES: Record = { + development: 1.0, + staging: 0.05, + production: 0.03, + unknown: 0.0, +}; + +let enabled = false; + +/** + * Initialise Sentry once, before the server starts. A no-op when SENTRY_DSN is + * unset, matching the Python side. Also registers process-level handlers so an + * unhandled rejection (e.g. an auth.init() failure that index.ts does not await) + * or uncaught exception reaches Sentry. + */ +export const initSentry = (): void => { + const dsn = process.env.SENTRY_DSN; + if (!dsn) return; + + const environment = process.env.ENVIRONMENT ?? "unknown"; + + Sentry.init({ + dsn, + environment, + tracesSampleRate: TRACE_RATES[environment] ?? 0.0, + }); + + enabled = true; + + process.on("unhandledRejection", (reason) => captureException(reason)); + process.on("uncaughtException", (err) => captureException(err)); +}; + +/** + * Report an error to Sentry. A silent no-op when Sentry was not initialised + * (DSN absent), so call sites can fire it unconditionally. + */ +export const captureException = ( + err: unknown, + extras?: Record +): void => { + if (!enabled) return; + Sentry.captureException(err, extras ? { extra: extras } : undefined); +}; diff --git a/platform/test/auth.startup.test.ts b/platform/test/auth.startup.test.ts new file mode 100644 index 00000000..5fe8c628 --- /dev/null +++ b/platform/test/auth.startup.test.ts @@ -0,0 +1,76 @@ +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; + +// internal-token.ts captures the token provenance (env vs minted) once at module +// load, and logInternalTokenProvenance() logs it. To exercise both branches we +// re-import the module in a fresh registry per case with APOLLO_INTERNAL_TOKEN +// pre-set or absent. +const freshInternalToken = async () => { + const mod = `../src/auth/internal-token?cachebust=${Math.random()}`; + return import(mod); +}; + +describe("Internal-token startup provenance", () => { + const saved = process.env.APOLLO_INTERNAL_TOKEN; + let log: ReturnType; + let warn: ReturnType; + + beforeEach(() => { + log = spyOn(console, "log").mockImplementation(() => {}); + warn = spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + log.mockRestore(); + warn.mockRestore(); + if (saved === undefined) delete process.env.APOLLO_INTERNAL_TOKEN; + else process.env.APOLLO_INTERNAL_TOKEN = saved; + }); + + const logged = () => log.mock.calls.map(([m]) => String(m)).join("\n"); + const warned = () => warn.mock.calls.map(([m]) => String(m)).join("\n"); + + it("logs 'from APOLLO_INTERNAL_TOKEN' and returns the env value when set", async () => { + process.env.APOLLO_INTERNAL_TOKEN = "shared-token"; + const { logInternalTokenProvenance, getInternalToken } = + await freshInternalToken(); + logInternalTokenProvenance(); + expect(logged()).toContain("from APOLLO_INTERNAL_TOKEN"); + expect(logged()).not.toContain("minted per-process"); + // Pin the token's actual value, not just the log text: a regression that broke + // the derivation while leaving the provenance flag right would pass otherwise. + expect(getInternalToken()).toBe("shared-token"); + }); + + it("logs 'minted per-process' and mints a fresh random token when absent", async () => { + delete process.env.APOLLO_INTERNAL_TOKEN; + const a = await freshInternalToken(); + a.logInternalTokenProvenance(); + expect(logged()).toContain("minted per-process"); + expect(a.getInternalToken()).toMatch(/^[0-9a-f]{64}$/); + // A separate process mints its own distinct token. + const b = await freshInternalToken(); + expect(b.getInternalToken()).not.toBe(a.getInternalToken()); + }); + + it("warns about reusePort only when the token was minted AND reusePort is on", async () => { + delete process.env.APOLLO_INTERNAL_TOKEN; + const { logInternalTokenProvenance } = await freshInternalToken(); + logInternalTokenProvenance(true); + expect(warned()).toContain("reusePort"); + expect(warned()).toContain("APOLLO_INTERNAL_TOKEN"); + }); + + it("does not warn about reusePort when the token came from the env", async () => { + process.env.APOLLO_INTERNAL_TOKEN = "shared-token"; + const { logInternalTokenProvenance } = await freshInternalToken(); + logInternalTokenProvenance(true); + expect(warned()).not.toContain("reusePort"); + }); + + it("does not warn about reusePort when reusePort is off (minted token)", async () => { + delete process.env.APOLLO_INTERNAL_TOKEN; + const { logInternalTokenProvenance } = await freshInternalToken(); + logInternalTokenProvenance(false); + expect(warned()).not.toContain("reusePort"); + }); +}); diff --git a/platform/test/auth/client/commands.test.ts b/platform/test/auth/client/commands.test.ts new file mode 100644 index 00000000..b399e65a --- /dev/null +++ b/platform/test/auth/client/commands.test.ts @@ -0,0 +1,191 @@ +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { randomBytes } from "node:crypto"; +import { closeDb, getDb } from "../../../src/db"; +import { runMigrations } from "../../../src/db/migrate"; +import { hashToken } from "../../../src/auth/hash"; +import { decryptKey, encryptKey } from "../../../src/util/instance-key-crypto"; +import { InstanceAuth, type Client } from "../../../src/auth/instance-auth"; +import { getClientByName } from "../../../src/auth/client/store"; +import { + ClientNotFoundError, + addClient, + classifyStoredKey, + encryptValue, + rotateClient, + verifyClient, +} from "../../../src/auth/client/commands"; + +// A fake `sql` tagged-template that records each call's text and bound values, so +// add/rotate's mint->hash->encrypt key-prep is testable up to the SQL call with no +// DB. UPDATE ... RETURNING reads back `updateRows`; everything else resolves empty. +function captureSql(updateRows: Array<{ name: string }> = [{ name: "x" }]) { + const calls: Array<{ text: string; values: unknown[] }> = []; + const fn = (strings: TemplateStringsArray, ...values: unknown[]) => { + const text = strings.join(" ? "); + calls.push({ text, values }); + return Promise.resolve(/RETURNING/.test(text) ? updateRows : undefined); + }; + return Object.assign(fn, { calls }); +} + +describe("client/commands key-prep (no DB)", () => { + it("addClient mints, sha256-hashes the api_key, and encrypts the Anthropic key", async () => { + const encKey = randomBytes(32); + const sql = captureSql(); + const { apiKey } = await addClient(sql as any, encKey, "acme", "sk-ant-secret"); + + expect(sql.calls).toHaveLength(1); + const [{ text, values }] = sql.calls; + expect(text).toContain("INSERT INTO lightning_clients"); + expect(values[0]).toBe("acme"); + expect(values[1]).toBe(hashToken(apiKey)); // auth_token_hash is sha256 of the minted key + expect((values[2] as string).startsWith("enc:v1:")).toBe(true); + expect(decryptKey(values[2] as string, encKey)).toBe("sk-ant-secret"); + }); + + it("rotateClient updates only anthropic_api_key, never the api_key/auth_token_hash", async () => { + const encKey = randomBytes(32); + const sql = captureSql([{ name: "acme" }]); + await rotateClient(sql as any, encKey, "acme", "sk-ant-new"); + + expect(sql.calls).toHaveLength(1); + const [{ text, values }] = sql.calls; + expect(text).toContain("UPDATE lightning_clients"); + expect(text).toContain("anthropic_api_key"); + expect(text).not.toContain("auth_token_hash"); // the credential is left in place + expect(decryptKey(values[0] as string, encKey)).toBe("sk-ant-new"); + expect(values[1]).toBe("acme"); + }); + + it("rotateClient throws ClientNotFoundError when no row matches", async () => { + const sql = captureSql([]); // UPDATE matched nothing + await expect( + rotateClient(sql as any, randomBytes(32), "ghost", "sk-ant-x") + ).rejects.toBeInstanceOf(ClientNotFoundError); + }); + + it("encryptValue round-trips through decryptKey", () => { + const encKey = randomBytes(32); + const blob = encryptValue(encKey, "sk-ant-plain"); + expect(blob.startsWith("enc:v1:")).toBe(true); + expect(decryptKey(blob, encKey)).toBe("sk-ant-plain"); + }); +}); + +describe("client/commands classifyStoredKey (no DB)", () => { + const encKey = randomBytes(32); + + it("NULL -> global", () => { + expect(classifyStoredKey(null, encKey)).toBe("global"); + }); + it("a non-enc value -> plaintext", () => { + expect(classifyStoredKey("sk-ant-plain", encKey)).toBe("plaintext"); + }); + it("an enc:v1: value the key decrypts -> decrypts", () => { + expect(classifyStoredKey(encryptKey("x", encKey), encKey)).toBe("decrypts"); + }); + it("an enc:v1: value with the wrong key -> decrypt_failed", () => { + expect(classifyStoredKey(encryptKey("x", encKey), randomBytes(32))).toBe("decrypt_failed"); + }); + it("an enc:v1: value with no key -> decrypt_failed", () => { + expect(classifyStoredKey(encryptKey("x", encKey), null)).toBe("decrypt_failed"); + }); + it("a corrupt enc:v1: blob -> decrypt_failed", () => { + const good = encryptKey("x", encKey); + expect(classifyStoredKey(good.slice(0, -4) + "AAAA", encKey)).toBe("decrypt_failed"); + }); +}); + +// The security-critical invariant: what add writes is exactly what the auth hook +// looks up. Drive addClient's captured output through the auth hook's real resolution path and +// assert it recovers the plaintext key. +describe("addClient -> auth-hook resolution (no DB)", () => { + // An InstanceAuth whose lookup knows exactly the one row addClient wrote. + function gatedFor(encKey: Buffer, authTokenHash: string, storedKey: string) { + const auth = new InstanceAuth({ encKey }); + const clients: Record = { + [authTokenHash]: auth.rowToClient({ name: "acme", anthropic_api_key: storedKey }), + }; + return new InstanceAuth({ encKey, lookup: (hash) => clients[hash] ?? null }); + } + const ctxFor = (apiKey: string): any => ({ + request: { headers: { get: () => null } }, + body: { api_key: apiKey }, + set: { status: 200 }, + }); + + it("what addClient writes resolves back through the auth hook to the stored key", async () => { + const encKey = randomBytes(32); + const sql = captureSql(); + const { apiKey } = await addClient(sql as any, encKey, "acme", "sk-ant-provisioned-secret"); + const [{ values }] = sql.calls; + const gated = gatedFor(encKey, values[1] as string, values[2] as string); + + const ctx = ctxFor(apiKey); + await gated.authenticate(ctx); + expect(ctx.lightningClient?.name).toBe("acme"); + expect(gated.resolveKey(ctx)).toEqual({ kind: "useKey", key: "sk-ant-provisioned-secret" }); + }); + + it("a different api_key does not resolve the provisioned client", async () => { + const encKey = randomBytes(32); + const sql = captureSql(); + await addClient(sql as any, encKey, "acme", "sk-ant-secret"); + const [{ values }] = sql.calls; + const gated = gatedFor(encKey, values[1] as string, values[2] as string); + + const ctx = ctxFor("sk-ant-some-other-key"); + await gated.authenticate(ctx); + expect(ctx.lightningClient).toBeUndefined(); + expect(gated.resolveKey(ctx)).toEqual({ kind: "forward" }); + }); +}); + +// Live-DB tier: the end-to-end add/rotate/verify path against real Postgres. +const hasDb = !!process.env.POSTGRES_URL; +const describeDb = hasDb ? describe : describe.skip; + +if (!hasDb) { + console.log("commands.test.ts: POSTGRES_URL unset — skipping live-DB tests (run in CI)."); +} + +describeDb("client/commands end-to-end (live DB)", () => { + beforeAll(async () => { + await runMigrations(); + }); + + afterAll(async () => { + await getDb()`DELETE FROM lightning_clients WHERE name LIKE 'client-test-%'`; + await closeDb(); + }); + + const testName = () => `client-test-${randomBytes(6).toString("hex")}`; + + it("add inserts an encrypted row; rotate replaces the key but keeps the credential", async () => { + const encKey = randomBytes(32); + const name = testName(); + + const { apiKey } = await addClient(getDb(), encKey, name, "sk-ant-e2e-1"); + expect(apiKey).toBeTruthy(); + + const row1 = await getClientByName(getDb(), name); + expect(row1?.anthropic_api_key?.startsWith("enc:v1:")).toBe(true); + expect(decryptKey(row1!.anthropic_api_key!, encKey)).toBe("sk-ant-e2e-1"); + const hashBefore = row1?.auth_token_hash; + + await rotateClient(getDb(), encKey, name, "sk-ant-e2e-2"); + const row2 = await getClientByName(getDb(), name); + expect(decryptKey(row2!.anthropic_api_key!, encKey)).toBe("sk-ant-e2e-2"); + expect(row2?.auth_token_hash).toBe(hashBefore); // unchanged across rotate + }); + + it("verifyClient classifies a stored row and an unknown name", async () => { + const encKey = randomBytes(32); + expect(await verifyClient(getDb(), encKey, testName())).toBe("unknown_client"); + + const name = testName(); + await addClient(getDb(), encKey, name, "sk-ant-verify"); + expect(await verifyClient(getDb(), encKey, name)).toBe("decrypts"); + expect(await verifyClient(getDb(), randomBytes(32), name)).toBe("decrypt_failed"); + }); +}); diff --git a/platform/test/auth/client/read-secret.test.ts b/platform/test/auth/client/read-secret.test.ts new file mode 100644 index 00000000..0b253146 --- /dev/null +++ b/platform/test/auth/client/read-secret.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "bun:test"; +import { readPipedSecret, trimSecret } from "../../../src/auth/client/read-secret"; + +// Fake a piped (non-TTY) stdin: an async iterable of chunks, the shape +// readPipedSecret consumes. The TTY path needs a real terminal, so it isn't +// unit-tested here. +async function* streamOf(...chunks: Array) { + for (const chunk of chunks) yield chunk; +} + +describe("read-secret (piped path)", () => { + it("reads a piped value and trims the trailing newline", async () => { + expect(await readPipedSecret(streamOf("sk-ant-piped\n"))).toBe("sk-ant-piped"); + }); + + it("joins multiple chunks and trims surrounding whitespace", async () => { + expect(await readPipedSecret(streamOf(" sk-ant", "-multi \n"))).toBe("sk-ant-multi"); + }); + + it("decodes Uint8Array chunks", async () => { + const bytes = new TextEncoder().encode("sk-ant-bytes\n"); + expect(await readPipedSecret(streamOf(bytes))).toBe("sk-ant-bytes"); + }); + + it("trimSecret matches hashToken's trim semantics", () => { + expect(trimSecret(" x \n")).toBe("x"); + }); +}); diff --git a/platform/test/auth/client/store.test.ts b/platform/test/auth/client/store.test.ts new file mode 100644 index 00000000..045df2a7 --- /dev/null +++ b/platform/test/auth/client/store.test.ts @@ -0,0 +1,79 @@ +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { randomBytes } from "node:crypto"; +import { closeDb, getDb } from "../../../src/db"; +import { runMigrations } from "../../../src/db/migrate"; +import { hashToken } from "../../../src/auth/hash"; +import { encryptKey } from "../../../src/util/instance-key-crypto"; +import { + getClientByName, + insertClient, + mintApiKey, + updateClientKey, +} from "../../../src/auth/client/store"; + +// Live-DB tier. Skipped when POSTGRES_URL is unset (as in db.test.ts) so `bun test` +// stays usable offline; runs against the postgres:16 service in CI. +const hasDb = !!process.env.POSTGRES_URL; +const describeDb = hasDb ? describe : describe.skip; + +if (!hasDb) { + console.log("store.test.ts: POSTGRES_URL unset — skipping live-DB tests (run in CI)."); +} + +describeDb("client/store (live DB)", () => { + beforeAll(async () => { + await runMigrations(); + }); + + afterAll(async () => { + await getDb()`DELETE FROM lightning_clients WHERE name LIKE 'client-test-%'`; + await closeDb(); + }); + + const testName = () => `client-test-${randomBytes(6).toString("hex")}`; + + it("insertClient writes a row that getClientByName reads back", async () => { + const name = testName(); + const hash = hashToken(mintApiKey()); + const enc = encryptKey("sk-ant-stored", randomBytes(32)); + await insertClient(getDb(), name, hash, enc); + + const row = await getClientByName(getDb(), name); + expect(row).not.toBeNull(); + expect(row?.auth_token_hash).toBe(hash); + expect(row?.anthropic_api_key).toBe(enc); + }); + + it("a second insert of the same name throws the unique violation (23505)", async () => { + const name = testName(); + await insertClient(getDb(), name, hashToken(mintApiKey()), encryptKey("sk-ant-a", randomBytes(32))); + + let errno: string | undefined; + try { + await insertClient(getDb(), name, hashToken(mintApiKey()), encryptKey("sk-ant-b", randomBytes(32))); + } catch (err: any) { + errno = err?.errno; + } + expect(errno).toBe("23505"); + }); + + it("updateClientKey changes anthropic_api_key but leaves auth_token_hash untouched", async () => { + const name = testName(); + const hash = hashToken(mintApiKey()); + const oldEnc = encryptKey("sk-ant-old", randomBytes(32)); + await insertClient(getDb(), name, hash, oldEnc); + + const newEnc = encryptKey("sk-ant-new", randomBytes(32)); + const updated = await updateClientKey(getDb(), name, newEnc); + expect(updated).toBe(1); + + const row = await getClientByName(getDb(), name); + expect(row?.anthropic_api_key).toBe(newEnc); + expect(row?.auth_token_hash).toBe(hash); // the whole point of rotate + }); + + it("updateClientKey returns 0 for an unknown client", async () => { + const updated = await updateClientKey(getDb(), testName(), encryptKey("sk-ant-x", randomBytes(32))); + expect(updated).toBe(0); + }); +}); diff --git a/platform/test/db.test.ts b/platform/test/db.test.ts new file mode 100644 index 00000000..d77df663 --- /dev/null +++ b/platform/test/db.test.ts @@ -0,0 +1,46 @@ +import { afterAll, describe, expect, it } from "bun:test"; +import { closeDb, getDb } from "../src/db"; +import { runMigrations } from "../src/db/migrate"; + +// Real-connection coverage: no mock. Runs only when POSTGRES_URL points at a live +// database (set in CI against a Postgres service container). Skipped offline so +// `bun test` stays usable locally without Postgres. +const hasDb = !!process.env.POSTGRES_URL; +const describeDb = hasDb ? describe : describe.skip; + +if (!hasDb) { + console.log( + "db.test.ts: POSTGRES_URL unset — skipping real-connection DB tests (they run in CI)." + ); +} + +describeDb("DB helper (real connection)", () => { + afterAll(async () => { + await closeDb(); + }); + + it("getDb() opens a connection and runs SELECT 1", async () => { + const rows = (await getDb()`SELECT 1 AS one`) as Array<{ one: number }>; + expect(rows[0]?.one).toBe(1); + }); + + it("runMigrations() creates lightning_clients with the expected columns", async () => { + await runMigrations(); + + const cols = (await getDb()` + SELECT column_name FROM information_schema.columns + WHERE table_name = 'lightning_clients' + `) as Array<{ column_name: string }>; + const names = new Set(cols.map((c) => c.column_name)); + + expect(names.has("id")).toBe(true); + expect(names.has("name")).toBe(true); + expect(names.has("auth_token_hash")).toBe(true); + expect(names.has("anthropic_api_key")).toBe(true); + }); + + it("runMigrations() is idempotent against an already-provisioned database", async () => { + const applied = await runMigrations(); + expect(applied).toBe(0); + }); +}); diff --git a/platform/test/fixtures/auth/hash-token-vectors.json b/platform/test/fixtures/auth/hash-token-vectors.json new file mode 100644 index 00000000..ac5b9b76 --- /dev/null +++ b/platform/test/fixtures/auth/hash-token-vectors.json @@ -0,0 +1,26 @@ +{ + "_note": "expected_hex is sha256 of the CLEAN token over UTF-8, computed by hand (not from hashToken) so the test cannot be tautological. Every whitespace-padded variant must trim to the clean token and so share its digest. The credential is its trimmed form.", + "clean_token": "dGVzdC10b2tlbi1jYW5vbmljYWwtYmFzZTY0dXJs", + "vectors": [ + { + "label": "clean base64url token", + "input": "dGVzdC10b2tlbi1jYW5vbmljYWwtYmFzZTY0dXJs", + "expected_hex": "d6cbc963dbc36ad1c08fc8cc59e65ea0b099b93caea1f306814a8b3880532050" + }, + { + "label": "leading space", + "input": " dGVzdC10b2tlbi1jYW5vbmljYWwtYmFzZTY0dXJs", + "expected_hex": "d6cbc963dbc36ad1c08fc8cc59e65ea0b099b93caea1f306814a8b3880532050" + }, + { + "label": "trailing newline", + "input": "dGVzdC10b2tlbi1jYW5vbmljYWwtYmFzZTY0dXJs\n", + "expected_hex": "d6cbc963dbc36ad1c08fc8cc59e65ea0b099b93caea1f306814a8b3880532050" + }, + { + "label": "leading spaces and trailing space + newline", + "input": " dGVzdC10b2tlbi1jYW5vbmljYWwtYmFzZTY0dXJs \n", + "expected_hex": "d6cbc963dbc36ad1c08fc8cc59e65ea0b099b93caea1f306814a8b3880532050" + } + ] +} diff --git a/platform/test/hash.test.ts b/platform/test/hash.test.ts new file mode 100644 index 00000000..3b7d23b8 --- /dev/null +++ b/platform/test/hash.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "bun:test"; +import { hashToken } from "../src/auth/hash"; +import vectors from "./fixtures/auth/hash-token-vectors.json"; + +// Pins the credential-hash trim contract: every whitespace-padded variant of the +// clean token must hash to the same digest. expected_hex is a hand-computed +// constant in the fixture (sha256 of the clean token's UTF-8 bytes), never derived +// from hashToken — so a regression in the function breaks CI rather than the test +// rubber-stamping it. +describe("hashToken trim contract", () => { + for (const { label, input, expected_hex } of vectors.vectors) { + it(`hashes "${label}" to the canonical digest`, () => { + expect(hashToken(input)).toBe(expected_hex); + }); + } + + it("treats every padded variant as the clean token (no whitespace drift)", () => { + const clean = hashToken(vectors.clean_token); + for (const { input } of vectors.vectors) { + expect(hashToken(input)).toBe(clean); + } + }); +}); diff --git a/platform/test/server.test.ts b/platform/test/server.test.ts index d6ec22bc..36e3947e 100644 --- a/platform/test/server.test.ts +++ b/platform/test/server.test.ts @@ -1,11 +1,37 @@ -import { describe, expect, it } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it, setSystemTime, spyOn } from "bun:test"; +import { randomBytes } from "node:crypto"; +import { Elysia } from "elysia"; import setup from "../src/server"; +import { captureException } from "../src/util/sentry"; +import * as sentry from "../src/util/sentry"; +import { InstanceAuth, type Client } from "../src/auth/instance-auth"; +import { hashToken } from "../src/auth/hash"; +import { internalAuthHeader } from "../src/auth/internal-token"; +import { encryptKey } from "../src/util/instance-key-crypto"; const port = 9865; const baseUrl = `http://localhost:${port}`; -const app = await setup(port); +// extras of the first captureException call whose `reason` matches, or undefined. +// Lets the auth tests assert a capture fired with the expected reason without +// repeating the mock-calls scan and the extras cast at every site. +const capturedExtras = ( + spy: { mock: { calls: any[] } }, + reason: string +): Record | undefined => + spy.mock.calls.find(([, extras]) => extras?.reason === reason)?.[1]; + +// The shared listening app gets a synchronous lookup driven by a test-controlled +// map. Setting `knownClients` per test is how a fresh configuration is applied +// without any module-global poke seam; an empty/absent map routes everyone by the +// shape rule, exactly as a down DB would. +let knownClients: Record | null = null; +const sharedAuth = new InstanceAuth({ + lookup: (hash) => knownClients?.[hash] ?? null, +}); + +const app = await setup(port, sharedAuth); const get = (path: string) => { return new Request(`${baseUrl}/${path}`); @@ -124,9 +150,812 @@ describe("Python Services", () => { ); expect(response.status).toBe(200); - + const body = await response.json(); expect(body).toEqual({ success: true }); }); }); }); + +describe("Sentry", () => { + // No SENTRY_DSN is set in the test env, so the helper was never initialised. + it("captureException is a silent no-op when no DSN is configured", () => { + expect(() => captureException(new Error("test"))).not.toThrow(); + expect(() => captureException("not even an error", { foo: 1 })).not.toThrow(); + }); + + // Mirrors the onError hook server.ts registers: report, return nothing, and + // let Elysia produce its normal error response untouched. + it("an onError hook that only reports leaves the error response unchanged", async () => { + const boom = (app: Elysia) => + app.get("/boom", () => { + throw new Error("kaboom"); + }); + + const withHook = boom(new Elysia().onError(({ error }) => captureException(error))); + const without = boom(new Elysia()); + + const a = await withHook.handle(new Request("http://localhost/boom")); + const b = await without.handle(new Request("http://localhost/boom")); + + expect(a.status).toBe(b.status); + expect(await a.text()).toBe(await b.text()); + }); +}); + +describe("Instance authentication", () => { + // No real DB — the seam keys clients by SHA-256 of the api_key they send. ALPHA + // has a stored Anthropic key (swapped in); BETA has none (credential stripped). + // Any other key is unknown and routed by the shape check. + const ALPHA = "lightning-cred-alpha"; + const BETA = "lightning-cred-beta"; + const clients: Record = { + [hashToken(ALPHA)]: { name: "alpha", anthropicKey: "sk-ant-stored-alpha" }, + [hashToken(BETA)]: { name: "beta", anthropicKey: null }, + }; + + const postKey = (path: string, data: any, apiKey?: string) => + post(path, { ...data, ...(apiKey ? { api_key: apiKey } : {}) }); + + // One mode now: the auth hook is always active. Point the shared instance's injected + // lookup at the known-client map so rows 1/2 resolve; unknown keys fall to the + // shape check regardless. + beforeEach(() => { + knownClients = clients; + }); + + afterEach(() => { + knownClients = null; + }); + + // Row 1 + it("accepts a known credential and swaps in the client's stored key", async () => { + const res = await app.handle(postKey("services/echo", { x: 1 }, ALPHA)); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.x).toBe(1); + expect(body.api_key).toBe("sk-ant-stored-alpha"); + expect(body.api_key).not.toBe(ALPHA); + }); + + // Row 2 + it("strips the credential when the client has no stored key", async () => { + const res = await app.handle(postKey("services/echo", { x: 2 }, BETA)); + expect(res.status).toBe(200); + const body = await res.json(); + // No stored key → api_key dropped entirely (Apollo uses its global key). + expect(body.api_key).toBeUndefined(); + }); + + // Row 3 — unknown but sk-ant-shaped: bring-your-own key, forwarded unchanged. + it("forwards an unknown sk-ant-shaped key unchanged (bring-your-own)", async () => { + const res = await app.handle(postKey("services/echo", { x: 1 }, "sk-ant-byo")); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.api_key).toBe("sk-ant-byo"); + }); + + // Row 3b — unknown and NOT sk-ant-shaped: a likely Lightning credential; reject + // rather than forward it to the LLM. + it("rejects an unknown non-sk-ant- key with 401 (never forwarded)", async () => { + const res = await app.handle(postKey("services/echo", { x: 1 }, "lightning-cred-unknown")); + expect(res.status).toBe(401); + const body = await res.json(); + expect(body.code).toBe(401); + expect(body.type).toBe("UNAUTHORIZED"); + }); + + // Row 4 — no api_key at all: forwarded without the field (global key fallback). + it("forwards a request with no api_key (no 401), field absent", async () => { + const res = await app.handle(post("services/echo", { x: 1 })); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.api_key).toBeUndefined(); + }); + + // Row 1 and row 3 coexist: known-client swap and bring-your-own forward in one run. + it("serves the known-client swap and the bring-your-own forward side by side", async () => { + const swapped = await (await app.handle(postKey("services/echo", { x: 1 }, ALPHA))).json(); + const forwarded = await (await app.handle(postKey("services/echo", { x: 1 }, "sk-ant-byo"))).json(); + expect(swapped.api_key).toBe("sk-ant-stored-alpha"); + expect(forwarded.api_key).toBe("sk-ant-byo"); + }); + + it("leaves health and root endpoints open", async () => { + expect((await app.handle(get("livez"))).status).toBe(200); + expect((await app.handle(get(""))).status).toBe(200); + }); + + // Row 6 — bodyless README GET is served, no 401. + it("serves a bodyless README GET without a 401", async () => { + const res = await app.handle(get("services/echo/README.md")); + expect(res.status).toBe(200); + }); + + // Row 5 + it("exempts internal apollo() self-calls carrying the internal token", async () => { + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + const req = new Request(`${baseUrl}/services/echo`, { + method: "POST", + body: JSON.stringify({ x: 9 }), + headers: { "Content-Type": "application/json", ...internalAuthHeader() }, + }); + const res = await app.handle(req); + expect(res.status).toBe(200); + // Correct token: the mismatch warn must not fire. + expect( + warn.mock.calls.some(([m]) => String(m).includes("internal token MISMATCH")) + ).toBe(false); + } finally { + warn.mockRestore(); + } + }); + + it("rejects a bogus internal token with 401 and a distinct mismatch warn", async () => { + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + const req = new Request(`${baseUrl}/services/echo`, { + method: "POST", + body: JSON.stringify({ x: 9 }), + headers: { "Content-Type": "application/json", "x-apollo-internal": "nope" }, + }); + const res = await app.handle(req); + expect(res.status).toBe(401); + const warned = warn.mock.calls.map(([m]) => String(m)).join("\n"); + expect(warned).toContain("internal token MISMATCH"); + // Names both likely causes. + expect(warned).toContain("APOLLO_INTERNAL_TOKEN"); + expect(warned.toLowerCase()).toContain("forged"); + } finally { + warn.mockRestore(); + } + }); + + it("captures the internal-token mismatch and still rejects with 401", async () => { + const warn = spyOn(console, "warn").mockImplementation(() => {}); + const capture = spyOn(sentry, "captureException"); + try { + const req = new Request(`${baseUrl}/services/echo`, { + method: "POST", + body: JSON.stringify({ x: 9 }), + headers: { "Content-Type": "application/json", "x-apollo-internal": "nope" }, + }); + const res = await app.handle(req); + // Behaviour unchanged: a forged internal header still rejects. + expect(res.status).toBe(401); + // ...and the mismatch is no longer silent. + expect(capturedExtras(capture, "internal-token-mismatch")).toBeDefined(); + } finally { + capture.mockRestore(); + warn.mockRestore(); + } + }); + + it("rejects a wrong internal header even with a valid body api_key (no fall-through)", async () => { + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + const req = new Request(`${baseUrl}/services/echo`, { + method: "POST", + // ALPHA is a known, otherwise-valid credential; the wrong internal + // header must still reject it rather than authenticate via api_key. + body: JSON.stringify({ x: 9, api_key: ALPHA }), + headers: { "Content-Type": "application/json", "x-apollo-internal": "nope" }, + }); + const res = await app.handle(req); + expect(res.status).toBe(401); + expect( + warn.mock.calls.some(([m]) => String(m).includes("internal token MISMATCH")) + ).toBe(true); + } finally { + warn.mockRestore(); + } + }); + + it("does not emit the mismatch warn on the normal external path (no internal header)", async () => { + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + // An unknown non-sk-ant- key takes the explicit-fail path (no internal header). + const res = await app.handle(postKey("services/echo", { x: 1 }, "lightning-cred-unknown")); + expect(res.status).toBe(401); + expect( + warn.mock.calls.some(([m]) => String(m).includes("internal token MISMATCH")) + ).toBe(false); + } finally { + warn.mockRestore(); + } + }); + + // Row 5 — a per-client key resolved at the outer boundary must survive the hop. + it("passes a forwarded api_key through on internal self-calls untouched", async () => { + // Already authenticated upstream, so a forwarded api_key must survive into + // the payload rather than being stripped to the global key. + const req = new Request(`${baseUrl}/services/echo`, { + method: "POST", + body: JSON.stringify({ x: 9, api_key: "sk-ant-forwarded" }), + headers: { "Content-Type": "application/json", ...internalAuthHeader() }, + }); + const res = await app.handle(req); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.api_key).toBe("sk-ant-forwarded"); + }); + + // WS upgrade auth decision via app.handle(): under the forward model a bare + // upgrade is no longer rejected. app.handle() never performs a real socket + // upgrade (Bun upgrades only through the listening server), so this proves the + // auth hook forwarded rather than 401'd, not that the upgrade itself succeeds. The + // 101/end-to-end no-regression proof is the live-socket echo test above. + it("passes an unauthenticated WebSocket upgrade through the auth hook", async () => { + const req = new Request(`${baseUrl}/services/echo`, { + method: "GET", + headers: { + Connection: "Upgrade", + Upgrade: "websocket", + "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version": "13", + }, + }); + const res = await app.handle(req); + expect(res.status).not.toBe(401); + }); + + // Drive a real upgrade against the listening server, send one start message, and + // resolve with the first complete payload. Only a live socket exercises the + // upgrade + message round-trip (and so the ws.data context capture); app.handle() + // cannot. Extra headers (e.g. the internal token) ride the upgrade GET. + const wsRoundTrip = ( + query = "", + headers?: Record + ): Promise => + new Promise((resolve, reject) => { + const socket = new WebSocket( + `ws://localhost:${port}/services/echo${query}`, + headers ? { headers } : undefined + ); + const timer = setTimeout(() => { + socket.close(); + reject(new Error("ws round-trip timed out")); + }, 8000); + socket.addEventListener("error", (e) => { + clearTimeout(timer); + reject(e); + }); + socket.addEventListener("message", ({ data }) => { + const evt = JSON.parse(data as string); + if (evt.event === "complete") { + clearTimeout(timer); + socket.close(); + resolve(evt.data); + } + }); + socket.addEventListener("open", () => { + socket.send(JSON.stringify({ event: "start", data: { ws: 1 } })); + }); + }); + + // AC2 — a known client's token on the upgrade query string resolves to its stored + // Anthropic key in the start payload, just like POST. Pins both the query read and + // that ws.data carries the lightningClient set during beforeHandle. + it("swaps a known client's stored key on a WS upgrade via ?api_key=", async () => { + const body = await wsRoundTrip(`?api_key=${encodeURIComponent(ALPHA)}`); + expect(body.api_key).toBe("sk-ant-stored-alpha"); + expect(body.api_key).not.toBe(ALPHA); + expect(body.ws).toBe(1); + }); + + // AC3 — an unrecognised sk-ant- token on the upgrade connects and forwards as-is. + it("forwards an unknown sk-ant- token on a WS upgrade unchanged", async () => { + const body = await wsRoundTrip(`?api_key=sk-ant-ws-byo`); + expect(body.api_key).toBe("sk-ant-ws-byo"); + }); + + // Internal exemption holds on WS: the upgrade GET carries the internal header, so + // a forwarded per-client api_key passes through untouched (not stripped/swapped). + it("honours the internal token on a WS upgrade (passthrough)", async () => { + const socket = new WebSocket(`ws://localhost:${port}/services/echo`, { + headers: internalAuthHeader(), + }); + const body = await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + socket.close(); + reject(new Error("ws round-trip timed out")); + }, 8000); + socket.addEventListener("error", (e) => { + clearTimeout(timer); + reject(e); + }); + socket.addEventListener("message", ({ data }) => { + const evt = JSON.parse(data as string); + if (evt.event === "complete") { + clearTimeout(timer); + socket.close(); + resolve(evt.data); + } + }); + socket.addEventListener("open", () => { + socket.send( + JSON.stringify({ + event: "start", + data: { api_key: "sk-ant-internal-fwd" }, + }) + ); + }); + }); + expect(body.api_key).toBe("sk-ant-internal-fwd"); + }); +}); + +describe("Instance auth — DB-down forward path", () => { + // No known clients: every caller is "unknown". The shape rule still applies — an + // sk-ant- key forwards, a non-sk-ant- key fails explicitly. + beforeEach(() => { + knownClients = null; + }); + + // Row 7 + it("forwards an unknown sk-ant- key when the DB is down", async () => { + const res = await app.handle(post("services/echo", { x: 1, api_key: "sk-ant-byo" })); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.api_key).toBe("sk-ant-byo"); + }); + + // Row 8 + it("rejects an unknown non-sk-ant- key when the DB is down (never forwarded)", async () => { + const res = await app.handle(post("services/echo", { x: 1, api_key: "lightning-cred-unknown" })); + expect(res.status).toBe(401); + }); +}); + +describe("Instance auth — lookup never came up (dbReady false)", () => { + // The shared app injects a `lookup`, so it never reaches lookupClient's dbReady + // guard. Construct a bare InstanceAuth (no lookup/dbLookup => dbReady stays false) + // and drive authenticate() directly to exercise the real fail-closed path a production + // process takes when the DB never connected, distinct from the override-simulated + // "DB-down" rows above, which converge on the same observable behaviour. + const fakeCtx = (apiKey: string) => + ({ + request: { headers: { get: () => null } }, + body: { api_key: apiKey }, + set: { status: 200 }, + }) as any; + + it("forwards an unknown sk-ant- key via the shape rule (no DB)", async () => { + const auth = new InstanceAuth(); + const ctx = fakeCtx("sk-ant-byo"); + await auth.authenticate(ctx); + expect(ctx.forwardApiKey).toBe("sk-ant-byo"); + expect(ctx.set.status).toBe(200); + }); + + it("returns 503 for an unknown non-sk-ant- key when the lookup never came up (no DB)", async () => { + const auth = new InstanceAuth(); + const ctx = fakeCtx("lightning-cred-unknown"); + await auth.authenticate(ctx); + // dbReady is false, so we cannot verify the caller — that is our outage, not a bad credential: 503, never a misleading 401, never a forward. + expect(ctx.set.status).toBe(503); + expect(ctx.forwardApiKey).toBeUndefined(); + }); +}); + +describe("Instance auth cache refresh", () => { + // Drive the real lookupClient with a fake per-hash dbLookup so we can count DB + // reads per burst. A fresh InstanceAuth per test isolates the cache; authenticate() is + // called directly with a minimal ctx (no echo service). Ageing is simulated by + // advancing the system clock (setSystemTime) rather than poking cache internals — + // real setTimeout still fires in real time, so the single-flight sequencing holds. + const ALPHA = "lightning-cred-alpha"; + const UNKNOWN = "lightning-cred-unknown"; + const clientWith = (anthropicKey: string | null): Client => ({ + name: "alpha", + anthropicKey, + }); + const fakeCtx = (apiKey?: string) => + ({ + request: { headers: { get: () => null } }, + body: apiKey ? { api_key: apiKey } : {}, + set: { status: 200 }, + }) as any; + const tick = () => new Promise((r) => setTimeout(r, 10)); + const settle = () => new Promise((r) => setTimeout(r, 40)); + const TTL_MS = 60_000; + // Just over the TTL (within the ceiling), so the next read serves stale + refreshes. + const PAST_TTL = TTL_MS + 1; + // Just over the ceiling, so the next read evicts rather than serves. + const OVER_CEILING = TTL_MS * 3 + 1; + + // Advance the wall clock by `ms` from now so cached entries read as that much + // older without touching their internals. + const advanceClock = (ms: number) => setSystemTime(new Date(Date.now() + ms)); + + afterEach(() => { + setSystemTime(); // restore the real clock + }); + + it("collapses a cold-start burst into a single DB read", async () => { + let calls = 0; + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + await tick(); + return clientWith("sk-ant-stored-alpha"); + }, + }); + + const ctxs = Array.from({ length: 50 }, () => fakeCtx(ALPHA)); + await Promise.all(ctxs.map((c) => auth.authenticate(c))); + + expect(calls).toBe(1); + for (const c of ctxs) { + expect(c.lightningClient?.anthropicKey).toBe("sk-ant-stored-alpha"); + } + }); + + it("makes no DB call on a second request within the TTL", async () => { + let calls = 0; + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + return clientWith("sk-ant-stored-alpha"); + }, + }); + + await auth.authenticate(fakeCtx(ALPHA)); + await auth.authenticate(fakeCtx(ALPHA)); + expect(calls).toBe(1); + }); + + it("caches a negative result and serves it without a second DB call", async () => { + let calls = 0; + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + return null; // verified unknown + }, + }); + + const first = fakeCtx(UNKNOWN); + await auth.authenticate(first); + // sk-ant-shaped? no -> unknown non-anthropic key is rejected. + expect(first.set.status).toBe(401); + + const second = fakeCtx(UNKNOWN); + await auth.authenticate(second); + expect(second.set.status).toBe(401); + expect(calls).toBe(1); // miss cached: no second lookup within the TTL + }); + + it("serves the stale value while one background refresh runs", async () => { + let calls = 0; + let current = clientWith("sk-ant-v1"); + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + await tick(); + return current; + }, + }); + + // Cold start awaits the one load and warms the cache with v1. + const warm = fakeCtx(ALPHA); + await auth.authenticate(warm); + expect(calls).toBe(1); + expect(warm.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + + // New data lands in the DB; age the entry past the TTL but within the ceiling. + current = clientWith("sk-ant-v2"); + advanceClock(PAST_TTL); + + // The burst is served immediately from the stale v1 value and triggers exactly + // one background refresh (not one per request). + const ctxs = Array.from({ length: 25 }, () => fakeCtx(ALPHA)); + await Promise.all(ctxs.map((c) => auth.authenticate(c))); + expect(calls).toBe(2); + for (const c of ctxs) { + expect(c.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + } + + // Once the background refresh settles, the new value is visible — no extra reads. + await settle(); + const after = fakeCtx(ALPHA); + await auth.authenticate(after); + expect(after.lightningClient?.anthropicKey).toBe("sk-ant-v2"); + expect(calls).toBe(2); + }); + + it("keeps serving stale when the refresh fails, then recovers", async () => { + let calls = 0; + let fail = false; + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + if (fail) throw new Error("db down"); + return clientWith("sk-ant-v1"); + }, + }); + + const warm = fakeCtx(ALPHA); + await auth.authenticate(warm); + expect(warm.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + + // Refresh now fails; stale-within-ceiling callers stay authenticated. + fail = true; + advanceClock(PAST_TTL); + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const ctxs = Array.from({ length: 10 }, () => fakeCtx(ALPHA)); + await Promise.all(ctxs.map((c) => auth.authenticate(c))); + for (const c of ctxs) { + expect(c.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + } + + // Let the background refresh reject and run its catch under the mute. + await settle(); + + // Recover once the DB is back. + fail = false; + advanceClock(PAST_TTL); + const ok = fakeCtx(ALPHA); + await auth.authenticate(ok); + expect(ok.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + } finally { + error.mockRestore(); + } + }); + + it("captures the swallowed stale-refresh error instead of hiding it, still serving stale", async () => { + let calls = 0; + let fail = false; + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + if (fail) throw new Error("db down"); + return clientWith("sk-ant-v1"); + }, + }); + + const warm = fakeCtx(ALPHA); + await auth.authenticate(warm); + expect(warm.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + + // Age past the TTL but within the ceiling, then make the background refresh fail. + fail = true; + advanceClock(PAST_TTL); + const capture = spyOn(sentry, "captureException"); + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const ctx = fakeCtx(ALPHA); + await auth.authenticate(ctx); + // Behaviour unchanged: the stale value is still served within the window. + expect(ctx.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + + // Let the background refresh reject and run its catch. + await settle(); + expect(calls).toBe(2); + expect(capturedExtras(capture, "stale-refresh-error")).toBeDefined(); + } finally { + capture.mockRestore(); + error.mockRestore(); + } + }); + + it("evicts a positive entry past the ceiling and fails closed when the DB is down", async () => { + let fail = false; + const auth = new InstanceAuth({ + dbLookup: async () => { + if (fail) throw new Error("db down"); + return clientWith("sk-ant-v1"); + }, + }); + + const warm = fakeCtx(ALPHA); + await auth.authenticate(warm); + expect(warm.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + + // Push the entry past the ceiling with the DB now down: the read evicts and the + // awaited cold lookup fails, so the request is rejected rather than served stale. + fail = true; + advanceClock(OVER_CEILING); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const ctx = fakeCtx(ALPHA); + await auth.authenticate(ctx); + expect(ctx.lightningClient).toBeUndefined(); + // ALPHA is not sk-ant-shaped and the evicted-then-failed lookup could not verify it, so we 503 (our outage) rather than a misleading 401. + expect(ctx.set.status).toBe(503); + expect(warn.mock.calls.some(([m]) => String(m).includes("max-staleness ceiling"))).toBe(true); + expect(error.mock.calls.some(([m]) => String(m).includes("client lookup failed"))).toBe(true); + } finally { + warn.mockRestore(); + error.mockRestore(); + } + }); + + it("rechecks a negative entry past the ceiling rather than blocking permanently", async () => { + let result: Client | null = null; + let calls = 0; + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + return result; + }, + }); + + // First sight: not found, miss cached. + const first = fakeCtx(ALPHA); + await auth.authenticate(first); + expect(first.lightningClient).toBeUndefined(); + expect(calls).toBe(1); + + // The client gets provisioned; push the miss past the ceiling. + result = clientWith("sk-ant-v1"); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + advanceClock(OVER_CEILING); + const second = fakeCtx(ALPHA); + await auth.authenticate(second); + // The miss was evicted and re-queried, picking up the now-provisioned client. + expect(second.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + expect(calls).toBe(2); + } finally { + warn.mockRestore(); + } + }); + + it("collapses a burst straddling an eviction boundary into one DB read", async () => { + let calls = 0; + const auth = new InstanceAuth({ + dbLookup: async () => { + calls++; + await tick(); + return clientWith("sk-ant-v1"); + }, + }); + + // Warm, then age past the ceiling so the next reads must evict + cold-load. + await auth.authenticate(fakeCtx(ALPHA)); + expect(calls).toBe(1); + advanceClock(OVER_CEILING); + + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + const ctxs = Array.from({ length: 50 }, () => fakeCtx(ALPHA)); + await Promise.all(ctxs.map((c) => auth.authenticate(c))); + // One eviction, one shared cold lookup for the burst. + expect(calls).toBe(2); + for (const c of ctxs) { + expect(c.lightningClient?.anthropicKey).toBe("sk-ant-v1"); + } + } finally { + warn.mockRestore(); + } + }); + + it("returns 503 (not 401) when a cold DB read fails for a non-sk-ant- caller, capturing the outage", async () => { + const auth = new InstanceAuth({ + dbLookup: async () => { + throw new Error("db down"); + }, + }); + const error = spyOn(console, "error").mockImplementation(() => {}); + const capture = spyOn(sentry, "captureException"); + try { + const ctx = fakeCtx(ALPHA); // non-sk-ant-shaped credential + await auth.authenticate(ctx); + expect(ctx.set.status).toBe(503); + expect(ctx.lightningClient).toBeUndefined(); + expect(ctx.forwardApiKey).toBeUndefined(); + + const extras = capturedExtras(capture, "client-store-unavailable-503"); + expect(extras).toBeDefined(); + expect(extras?.tokenHash).toBeDefined(); + // The capture must never carry the raw credential. + expect(JSON.stringify(extras)).not.toContain(ALPHA); + } finally { + capture.mockRestore(); + error.mockRestore(); + } + }); + + it("still forwards an sk-ant- caller when a cold DB read fails (BYO key needs no lookup)", async () => { + const auth = new InstanceAuth({ + dbLookup: async () => { + throw new Error("db down"); + }, + }); + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const ctx = fakeCtx("sk-ant-byo"); + await auth.authenticate(ctx); + expect(ctx.set.status).toBe(200); + expect(ctx.forwardApiKey).toBe("sk-ant-byo"); + } finally { + error.mockRestore(); + } + }); +}); + +describe("Instance auth key encryption", () => { + it("round-trips encrypted, plaintext, and null keys through rowToClient", () => { + const key = randomBytes(32); + const auth = new InstanceAuth({ encKey: key }); + const enc = encryptKey("sk-ant-secret", key); + + expect( + auth.rowToClient({ name: "enc", anthropic_api_key: enc })?.anthropicKey + ).toBe("sk-ant-secret"); + expect( + auth.rowToClient({ name: "plain", anthropic_api_key: "sk-ant-plain" })?.anthropicKey + ).toBe("sk-ant-plain"); + expect( + auth.rowToClient({ name: "none", anthropic_api_key: null })?.anthropicKey + ).toBeNull(); + }); + + it("drops a client whose encrypted key can't be decrypted (wrong key)", () => { + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const enc = encryptKey("sk-ant-secret", randomBytes(32)); // encrypted with key A + const auth = new InstanceAuth({ encKey: randomBytes(32) }); // holds a different key + + expect(auth.rowToClient({ name: "bad", anthropic_api_key: enc })).toBeNull(); + } finally { + error.mockRestore(); + } + }); + + it("drops an encrypted key when APOLLO_ENC_KEY is not configured", () => { + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const auth = new InstanceAuth({ encKey: null }); + const enc = encryptKey("sk-ant-secret", randomBytes(32)); + + expect(auth.rowToClient({ name: "bad", anthropic_api_key: enc })).toBeNull(); + } finally { + error.mockRestore(); + } + }); + + // The two decrypt-failure branches stay fail-closed (row resolves to a + // miss) but are no longer silent, and carry distinct reasons so an operator can + // tell a global env misconfiguration from one corrupt/rotated row. + it("captures a distinct reason when APOLLO_ENC_KEY is missing, still a miss", () => { + const capture = spyOn(sentry, "captureException"); + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const auth = new InstanceAuth({ encKey: null }); + const enc = encryptKey("sk-ant-secret", randomBytes(32)); + + // Behaviour unchanged: the row still drops to a miss. + expect(auth.rowToClient({ name: "missing", anthropic_api_key: enc })).toBeNull(); + + const extras = capturedExtras(capture, "missing-enc-key"); + expect(extras).toBeDefined(); + // Non-secret identifier only — never the key, plaintext, or enc blob. + expect(extras?.client).toBe("missing"); + } finally { + capture.mockRestore(); + error.mockRestore(); + } + }); + + it("captures a distinct reason when an encrypted key won't decrypt, still a miss", () => { + const capture = spyOn(sentry, "captureException"); + const error = spyOn(console, "error").mockImplementation(() => {}); + try { + const enc = encryptKey("sk-ant-secret", randomBytes(32)); // encrypted with key A + const auth = new InstanceAuth({ encKey: randomBytes(32) }); // holds a different key + + expect(auth.rowToClient({ name: "corrupt", anthropic_api_key: enc })).toBeNull(); + + const extras = capturedExtras(capture, "decrypt-error"); + expect(extras).toBeDefined(); + expect(extras?.client).toBe("corrupt"); + } finally { + capture.mockRestore(); + error.mockRestore(); + } + }); +}); diff --git a/platform/test/util/instance-key-crypto.test.ts b/platform/test/util/instance-key-crypto.test.ts new file mode 100644 index 00000000..72917426 --- /dev/null +++ b/platform/test/util/instance-key-crypto.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "bun:test"; +import { randomBytes } from "node:crypto"; +import { + ENC_PREFIX, + decryptKey, + encryptKey, + parseEncKey, +} from "../../src/util/instance-key-crypto"; + +// Pure-function coverage for the at-rest key crypto provisioning depends on: the +// encrypt/decrypt round-trip a stored anthropic_api_key survives, and parseEncKey's +// accept/reject contract. No fakes needed. +describe("instance-key-crypto round-trip", () => { + it("decryptKey(encryptKey(x)) === x for arbitrary inputs", () => { + const key = randomBytes(32); + for (const plain of [ + "sk-ant-abc123", + "", + "a key with spaces and \n newlines", + "unicode: key — café 🔑", + randomBytes(64).toString("base64"), + ]) { + expect(decryptKey(encryptKey(plain, key), key)).toBe(plain); + } + }); + + it("tags ciphertext with the enc:v1: prefix", () => { + expect(encryptKey("sk-ant-secret", randomBytes(32))).toStartWith(ENC_PREFIX); + }); + + it("produces a different ciphertext each call (random IV) that still decrypts", () => { + const key = randomBytes(32); + const a = encryptKey("sk-ant-secret", key); + const b = encryptKey("sk-ant-secret", key); + expect(a).not.toBe(b); + expect(decryptKey(a, key)).toBe("sk-ant-secret"); + expect(decryptKey(b, key)).toBe("sk-ant-secret"); + }); + + it("fails to decrypt with the wrong key", () => { + const enc = encryptKey("sk-ant-secret", randomBytes(32)); + expect(() => decryptKey(enc, randomBytes(32))).toThrow(); + }); +}); + +describe("parseEncKey accept/reject contract", () => { + it("returns a 32-byte Buffer for base64 of exactly 32 bytes", () => { + const raw = randomBytes(32).toString("base64"); + const key = parseEncKey(raw); + expect(key).not.toBeNull(); + expect(key?.length).toBe(32); + }); + + it("returns null for undefined / null / empty", () => { + expect(parseEncKey(undefined)).toBeNull(); + expect(parseEncKey(null)).toBeNull(); + expect(parseEncKey("")).toBeNull(); + }); + + it("returns null for base64 that decodes to the wrong length", () => { + expect(parseEncKey(randomBytes(16).toString("base64"))).toBeNull(); + expect(parseEncKey(randomBytes(31).toString("base64"))).toBeNull(); + expect(parseEncKey(randomBytes(33).toString("base64"))).toBeNull(); + }); + + it("trims surrounding whitespace before decoding", () => { + const raw = randomBytes(32).toString("base64"); + expect(parseEncKey(` ${raw}\n`)?.length).toBe(32); + }); +}); diff --git a/services/util.py b/services/util.py index bdce2d24..ca08b6f3 100644 --- a/services/util.py +++ b/services/util.py @@ -96,7 +96,14 @@ def apollo(name: str, payload: dict) -> dict: :return: JSON response. """ url = f"http://127.0.0.1:{apollo_port}/services/{name}" - r = requests.post(url, json=payload) + # Mark internal Apollo-to-Apollo calls so they bypass instance auth (see + # platform/src/auth/). The bridge injects the token into this child's env when + # spawning it; absent (e.g. run standalone) the header is omitted. + headers = {} + internal_token = os.environ.get("APOLLO_INTERNAL_TOKEN") + if internal_token: + headers["X-Apollo-Internal"] = internal_token + r = requests.post(url, json=payload, headers=headers) return r.json()