From b918a85782c25905e779de96ea7adc3f5ab02f78 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 24 Jun 2026 14:03:14 +0200 Subject: [PATCH 1/9] chore(platform): pin Bun and switch to a text-format lockfile Pin the Bun version in .tool-versions and replace the binary bun.lockb with the text-format bun.lock so dependency changes show up in diffs. Update CI and the Dockerfile to match. --- .github/workflows/unit-tests.yaml | 20 + .tool-versions | 2 +- Dockerfile | 6 +- bun.lock | 910 ++++++++++++++++++++++++++++++ bun.lockb | Bin 162262 -> 0 bytes 5 files changed, 935 insertions(+), 3 deletions(-) create mode 100644 bun.lock delete mode 100755 bun.lockb 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/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/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 f93c59658d8df7475d1168e6c0e50d6dc0c0cc4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162262 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

ow`?_YmYfO^%u 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 From 992b4353b71baf43d7a02b8bc52554961084bd2b Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 24 Jun 2026 14:03:33 +0200 Subject: [PATCH 2/9] feat(platform): add a Postgres connection pool and migration runner Add a shared pg pool (db/index.ts) and a migration runner (db/migrate.ts) that applies the SQL files in platform/migrations in order. Includes the first migration, which creates the lightning_clients table that client auth reads from. --- .../migrations/0001_lightning_clients.sql | 13 ++++ platform/src/db/index.ts | 45 ++++++++++++ platform/src/db/migrate.ts | 73 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 platform/migrations/0001_lightning_clients.sql create mode 100644 platform/src/db/index.ts create mode 100644 platform/src/db/migrate.ts 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/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(); + } +} From 4f26e8954439afda1e84cf7b548f5810ee9c5806 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 24 Jun 2026 14:04:11 +0200 Subject: [PATCH 3/9] feat(auth): authenticate /services/* against per-client credentials Add an always-on authenticate hook on /services/* that maps the api_key in the request body to a Lightning client via its SHA-256 in the lightning_clients table. On a known match the inbound key is never forwarded to the LLM: it is swapped for the client's stored anthropic_api_key, or stripped to fall back to the global key when that column is null. An unknown key is forwarded only if it is sk-ant-shaped, otherwise rejected with 401. Internal Apollo-to-Apollo calls are exempt via a per-process token injected into each Python child. Lookups are cached in memory with a single-flight, stale-while-revalidate refresh. Stored keys may be AES-256-GCM encrypted at rest. When the clients DB is unreachable the hook fails closed with 503, and decrypt, refresh and token-mismatch failures are reported to Sentry. --- package.json | 1 + platform/src/auth/enc-key.ts | 20 ++ platform/src/auth/hash.ts | 13 + platform/src/auth/instance-auth.ts | 385 +++++++++++++++++++++++ platform/src/auth/internal-token.ts | 62 ++++ platform/src/bridge.ts | 6 +- platform/src/index.ts | 3 + platform/src/middleware/services.ts | 78 ++++- platform/src/server.ts | 50 ++- platform/src/util/describe-modules.ts | 2 + platform/src/util/errors.ts | 26 ++ platform/src/util/instance-key-crypto.ts | 46 +++ platform/src/util/sentry.ts | 48 +++ services/util.py | 9 +- 14 files changed, 732 insertions(+), 17 deletions(-) create mode 100644 platform/src/auth/enc-key.ts create mode 100644 platform/src/auth/hash.ts create mode 100644 platform/src/auth/instance-auth.ts create mode 100644 platform/src/auth/internal-token.ts create mode 100644 platform/src/util/instance-key-crypto.ts create mode 100644 platform/src/util/sentry.ts diff --git a/package.json b/package.json index 2a431e5e..3f1fcf56 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,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/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/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..b3ef431e 100644 --- a/platform/src/util/describe-modules.ts +++ b/platform/src/util/describe-modules.ts @@ -16,6 +16,8 @@ export type ModuleDescription = { // TODO this is just a stub right now export default async (location: string): Promise => { const dirs = await readdir(location, { withFileTypes: true }); + // Skip leading-underscore directories: these are Python/build artefacts left + // under services/ (e.g. __pycache__), never mountable services. const services = dirs.filter( (dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") ); 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/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() From 06adebfd37f79017a3e8db3f4b906eaf48d332c4 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 24 Jun 2026 14:04:40 +0200 Subject: [PATCH 4/9] feat(auth): add a client provisioning CLI Add a client CLI (bun run client) for provisioning Lightning clients: create, list, rotate and remove rows in lightning_clients, reading secrets from stdin and encrypting them at rest. Add a migrate script to run pending migrations. --- package.json | 2 + platform/src/auth/client/cli.ts | 175 ++++++++++++++++++++++++ platform/src/auth/client/commands.ts | 98 +++++++++++++ platform/src/auth/client/read-secret.ts | 46 +++++++ platform/src/auth/client/store.ts | 66 +++++++++ 5 files changed, 387 insertions(+) create mode 100644 platform/src/auth/client/cli.ts create mode 100644 platform/src/auth/client/commands.ts create mode 100644 platform/src/auth/client/read-secret.ts create mode 100644 platform/src/auth/client/store.ts diff --git a/package.json b/package.json index 3f1fcf56..cfe79d09 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "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": { 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; +} From 369f8012aaba2ceb82d4c5c48c45d9ba43f5767d Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 24 Jun 2026 14:05:07 +0200 Subject: [PATCH 5/9] test(auth): cover the auth hook, key resolver, CLI and startup Add tests for the authenticate hook and key resolution, the client CLI (store, commands, secret reading), token hashing against fixed vectors, the encryption helper, the migration runner, and server startup. Extend the existing server tests to inject a configured auth instance. --- platform/test/auth.startup.test.ts | 76 ++ platform/test/auth/client/commands.test.ts | 191 ++++ platform/test/auth/client/read-secret.test.ts | 28 + platform/test/auth/client/store.test.ts | 79 ++ platform/test/db.test.ts | 46 + .../fixtures/auth/hash-token-vectors.json | 26 + platform/test/hash.test.ts | 23 + platform/test/server.test.ts | 835 +++++++++++++++++- .../test/util/instance-key-crypto.test.ts | 70 ++ 9 files changed, 1371 insertions(+), 3 deletions(-) create mode 100644 platform/test/auth.startup.test.ts create mode 100644 platform/test/auth/client/commands.test.ts create mode 100644 platform/test/auth/client/read-secret.test.ts create mode 100644 platform/test/auth/client/store.test.ts create mode 100644 platform/test/db.test.ts create mode 100644 platform/test/fixtures/auth/hash-token-vectors.json create mode 100644 platform/test/hash.test.ts create mode 100644 platform/test/util/instance-key-crypto.test.ts 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); + }); +}); From b3f8f388fbba46958b8e5b283bcd45801413e91d Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 24 Jun 2026 14:05:28 +0200 Subject: [PATCH 6/9] docs(auth): document client auth, the dual-DB setup and env vars Document the client auth model in the root README and the platform/src/auth README, note the APOLLO_CLIENTS_DB_URL / POSTGRES_URL fallback and the APOLLO_ENC_KEY / APOLLO_INTERNAL_TOKEN variables in .env.example and CLAUDE.md, and add the changeset. --- .changeset/client-auth.md | 20 +++ .env.example | 27 +++- CLAUDE.md | 41 ++++++ README.md | 275 ++++++++++++++++++++++++++++-------- platform/src/auth/README.md | 170 ++++++++++++++++++++++ 5 files changed, 471 insertions(+), 62 deletions(-) create mode 100644 .changeset/client-auth.md create mode 100644 platform/src/auth/README.md diff --git a/.changeset/client-auth.md b/.changeset/client-auth.md new file mode 100644 index 00000000..5267bf92 --- /dev/null +++ b/.changeset/client-auth.md @@ -0,0 +1,20 @@ +--- +"apollo": minor +--- + +Add an instance-auth gate to `/services/*` that maps a known caller's `api_key` +to a per-client Anthropic key. + +The inbound `api_key` already sent in the request body is hashed (SHA-256) and +looked up in a new `lightning_clients` table (via `APOLLO_CLIENTS_DB_URL`, or +`POSTGRES_URL` if that's unset, so the credentials can live in their own database +in production). On a known match +the credential is swapped for the client's stored `anthropic_api_key` (plaintext +or `enc:v1:` AES-256-GCM, decrypted with `APOLLO_ENC_KEY`) and never forwarded to +the LLM; an unknown `sk-ant-`-shaped key is forwarded unchanged (bring-your-own), +and an unknown non-`sk-ant-` key is rejected with `401`. Lookups are cached +in-process (~60s, single-flight with stale-while-revalidate). Internal +Apollo-to-Apollo calls are exempt via a per-process `APOLLO_INTERNAL_TOKEN`. + +Backward compatible: existing callers that pass an `sk-ant-` key are unaffected. +Operators provision clients with the new `client` CLI in `platform/src/auth/`. 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/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/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/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. From 94587870cddccb4bd06f6005590dbdbe5255c53a Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 24 Jun 2026 14:49:15 +0200 Subject: [PATCH 7/9] ci(docker): only move :latest for final releases Build the tag list in the manipulate-tag step and append openfn/apollo:latest only when the version has no hyphen. Pre-release tags (e.g. 1.4.0-pre.0) now push only their versioned image and leave :latest pointing at the last final release. --- .github/workflows/dockerize.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 }} From 498da4648c33a82bfd8bab1584c325dd8c8b437b Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Fri, 26 Jun 2026 09:08:12 +0100 Subject: [PATCH 8/9] version: 1.4.0 --- .changeset/client-auth.md | 20 -------------------- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 21 deletions(-) delete mode 100644 .changeset/client-auth.md diff --git a/.changeset/client-auth.md b/.changeset/client-auth.md deleted file mode 100644 index 5267bf92..00000000 --- a/.changeset/client-auth.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"apollo": minor ---- - -Add an instance-auth gate to `/services/*` that maps a known caller's `api_key` -to a per-client Anthropic key. - -The inbound `api_key` already sent in the request body is hashed (SHA-256) and -looked up in a new `lightning_clients` table (via `APOLLO_CLIENTS_DB_URL`, or -`POSTGRES_URL` if that's unset, so the credentials can live in their own database -in production). On a known match -the credential is swapped for the client's stored `anthropic_api_key` (plaintext -or `enc:v1:` AES-256-GCM, decrypted with `APOLLO_ENC_KEY`) and never forwarded to -the LLM; an unknown `sk-ant-`-shaped key is forwarded unchanged (bring-your-own), -and an unknown non-`sk-ant-` key is rejected with `401`. Lookups are cached -in-process (~60s, single-flight with stale-while-revalidate). Internal -Apollo-to-Apollo calls are exempt via a per-process `APOLLO_INTERNAL_TOKEN`. - -Backward compatible: existing callers that pass an `sk-ant-` key are unaffected. -Operators provision clients with the new `client` CLI in `platform/src/auth/`. 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/package.json b/package.json index cfe79d09..fed1c224 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "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", From a467fa9da9c7643be3c4043e46c8846c216eeed6 Mon Sep 17 00:00:00 2001 From: Joe Clark Date: Fri, 26 Jun 2026 09:09:04 +0100 Subject: [PATCH 9/9] remove comments --- platform/src/util/describe-modules.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/platform/src/util/describe-modules.ts b/platform/src/util/describe-modules.ts index b3ef431e..12215e71 100644 --- a/platform/src/util/describe-modules.ts +++ b/platform/src/util/describe-modules.ts @@ -13,11 +13,8 @@ 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 }); - // Skip leading-underscore directories: these are Python/build artefacts left - // under services/ (e.g. __pycache__), never mountable services. const services = dirs.filter( (dirent) => dirent.isDirectory() && !dirent.name.startsWith("_") );