diff --git a/.github/workflows/access-site.yml b/.github/workflows/access-site.yml new file mode 100644 index 00000000..314f3a61 --- /dev/null +++ b/.github/workflows/access-site.yml @@ -0,0 +1,92 @@ +name: access-site + +# Tests, provisions and deploys access.modelcontextprotocol.io (access-site/). +# The Worker bundles src/config, so config changes redeploy it too. +on: + pull_request: + paths: + - 'access-site/**' + - 'src/config/**' + - '.github/workflows/access-site.yml' + push: + branches: + - main + paths: + - 'access-site/**' + - 'src/config/**' + - '.github/workflows/access-site.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Typecheck and test + runs-on: ubuntu-latest + defaults: + run: + working-directory: access-site + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: access-site/package-lock.json + - run: npm ci + - run: npm run typecheck + - run: npm test + + deploy: + name: Provision and deploy + needs: test + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: production + concurrency: + group: deploy-access-site + cancel-in-progress: false + defaults: + run: + working-directory: access-site + # Secrets are exposed only to the steps that need them, never to `npm ci` + # (package install scripts run there). + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + persist-credentials: false + - name: Check setup + id: setup + env: + HAS_TOKEN: ${{ secrets.CLOUDFLARE_ACCESS_SITE_TOKEN != '' }} + HAS_SIGNING_KEY: ${{ secrets.ACCESS_SITE_OIDC_SIGNING_KEY != '' }} + HAS_CLIENT_SECRET: ${{ secrets.ACCESS_SITE_OIDC_CLIENT_SECRET != '' }} + run: | + if [ "$HAS_TOKEN" = true ] && [ "$HAS_SIGNING_KEY" = true ] && [ "$HAS_CLIENT_SECRET" = true ]; then + echo "ready=true" >> "$GITHUB_OUTPUT" + else + echo "::notice::access-site is not set up yet (see access-site/README.md); skipping deploy." + echo "ready=false" >> "$GITHUB_OUTPUT" + fi + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + if: steps.setup.outputs.ready == 'true' + with: + node-version: '24' + cache: 'npm' + cache-dependency-path: access-site/package-lock.json + - if: steps.setup.outputs.ready == 'true' + run: npm ci + - name: Provision and deploy + if: steps.setup.outputs.ready == 'true' + env: + # Token scoped to: Workers Scripts edit, D1 edit, and Workers Routes / + # DNS edit on the modelcontextprotocol.io zone (for the custom domain). + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_ACCESS_SITE_TOKEN }} + OIDC_SIGNING_KEY: ${{ secrets.ACCESS_SITE_OIDC_SIGNING_KEY }} + OIDC_CLIENT_SECRET: ${{ secrets.ACCESS_SITE_OIDC_CLIENT_SECRET }} + GITHUB_CLIENT_SECRET: ${{ secrets.ACCESS_SITE_GITHUB_CLIENT_SECRET }} + OIDC_PREVIOUS_JWK: ${{ secrets.ACCESS_SITE_OIDC_PREVIOUS_JWK }} + run: ./scripts/deploy.sh diff --git a/README.md b/README.md index b46bd811..97179b0a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Infrastructure as Code for managing access to MCP community resources using Pulu - **Google Workspace Groups**: Automatically syncs group memberships for @modelcontextprotocol.io email accounts - **Email Groups**: Groups with `isEmailGroup: true` accept emails from anyone (including external users) and notify all members. External posts are moderated for security. - **Google Workspace User Accounts**: Provisions @modelcontextprotocol.io accounts for members of roles with `provisionUser: true` (directly, or via a role nested under one through `github.parent` — e.g. SDK teams under `sdk-maintainers`, working groups under `working-groups`) +- **GitHub sign-in for Google Workspace** ([`access-site/`](access-site/README.md)): [access.modelcontextprotocol.io](https://access.modelcontextprotocol.io) is the OIDC identity provider for Google Workspace, so maintainers sign in to their account with GitHub. Who can sign in, and as which address, comes from `users.ts`. - **Discord**: Syncs the Discord roles declared in [`src/config/roles.ts`](src/config/roles.ts) to the members' Discord IDs in `users.ts`, and manages the settings of the channels declared in [`src/config/channels.ts`](src/config/channels.ts), including thread-only forum/media channels. See [Discord channels](#discord-channels) below. - **Cloudflare Access (security-room)**: Syncs the Cloudflare Zero Trust Access policy that decides who can sign in to `securityroom.modelcontextprotocol.io` from the roles declared in [`src/config/accessPolicies.ts`](src/config/accessPolicies.ts). See [Cloudflare Access (security-room)](#cloudflare-access-security-room) below. - **npm & PyPI Package Publishing Access** (declared, not applied): Expected registry access is declared in [`src/config/packageAccess.ts`](src/config/packageAccess.ts) and drift against the live npm registry is detected by CI — but changes are applied manually by a maintainer. See [npm & PyPI Package Publishing Access](#npm--pypi-package-publishing-access) below for why and how. diff --git a/access-site/README.md b/access-site/README.md new file mode 100644 index 00000000..9ff09d48 --- /dev/null +++ b/access-site/README.md @@ -0,0 +1,95 @@ +# access.modelcontextprotocol.io + +Sign in to MCP Google Workspace with GitHub. + +A small Cloudflare Worker (Hono, jose, D1) that is the OpenID Connect provider for Google Workspace's custom OIDC SSO profile. Maintainers sign in to Google with their GitHub account; there are no Google passwords. + +Accounts stay infrastructure as code. Who has an account, and which address, is `users.ts`: a GitHub user can sign in as `@modelcontextprotocol.io` only if their entry has a `googleEmailPrefix`, isn't opted out, and is in a role with `provisionUser`, the same rule Pulumi uses to create the account. The Worker bundles `src/config` and is redeployed on every config change, so removing someone from `users.ts` stops their sign-in on the next deploy. + +The Worker holds no Google credentials and creates nothing in Google. + +## How sign-in works + +```mermaid +sequenceDiagram + actor M as Maintainer + participant G as Google + participant W as access.modelcontextprotocol.io + participant GH as GitHub + M->>G: Open Gmail + G->>W: /authorize (OIDC) + W->>GH: GitHub App sign-in + GH-->>W: user ID, login + W->>W: login -> address in users.ts + W-->>G: code + G->>W: /token + W-->>G: ID token: sub=, email=@modelcontextprotocol.io + G-->>M: Signed in +``` + +GitHub users with no account in `users.ts` get a page explaining how to request one. + +## Security + +- **Accounts come from `users.ts`**, which only core maintainers can approve (CODEOWNERS). Anyone who can merge a `googleEmailPrefix` change can map a GitHub account to that Workspace address, so treat those changes like credential changes. +- **GitHub logins are pinned to GitHub user IDs** on first sign-in (`github_identities` in D1). GitHub frees a login when its owner renames, so if someone else registers that name, sign-in is refused until `users.ts` is updated and the pin is cleared (see Operations). +- **Codes and requests are single-use and short-lived**: pending requests last 10 minutes and authorization codes 2 minutes, stored hashed and consumed with `DELETE ... RETURNING`. PKCE is enforced when Google sends it, and a verifier without a challenge is rejected (RFC 9700). Redirect URIs must match exactly; client credentials are form-decoded (RFC 6749 2.3.1) and compared in constant time. +- **Rate limited**: `/authorize` and `/login` allow 30 requests per minute per IP, so anonymous traffic can't exhaust D1 writes. +- **Minimal GitHub access**: the GitHub App has no repository or organization permissions, and its user token is revoked right after the user is read. +- **No Google credentials** in the Worker; it cannot create, change or read Google accounts. +- **CI**: actions are pinned to commit SHAs, Git credentials aren't persisted, and deploy secrets are only exposed to the deploy step, never to `npm ci`. +- **Break-glass**: super admins are exempt from the SSO profile. The GitHub org requires 2FA, which covers Google skipping its own 2-step verification for SSO users. + +Sign-ins and refusals are logged as JSON lines in Workers Logs. + +## Setup + +Cloudflare provisioning is done by CI: on every merge to `main`, [`scripts/deploy.sh`](scripts/deploy.sh) finds or creates the `access-site` D1 database, applies migrations, and deploys the Worker with its secrets. Until the secrets below exist, the deploy job is skipped with a notice, so merging early is safe. Non-secret settings live in `wrangler.toml` and change by PR. + +One-time steps, in order. Secrets go straight from the command line into GitHub secrets (needs `openssl` and `gh`), so they never pass between people. + +1. **Cloudflare API token** (Cloudflare admin): create a token on the MCP Domain Account scoped to Workers Scripts edit, D1 edit, and Workers Routes and DNS edit on the `modelcontextprotocol.io` zone. Store it as `CLOUDFLARE_ACCESS_SITE_TOKEN` in the `production` environment. +2. **OIDC signing key** (anyone with access to the repo's secrets): + ```sh + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + | gh secret set ACCESS_SITE_OIDC_SIGNING_KEY --env production -R modelcontextprotocol/access + ``` +3. **GitHub App** (org owner): create it at . + - Homepage URL `https://access.modelcontextprotocol.io`, callback URL `https://access.modelcontextprotocol.io/login`; leave token expiry on; no device flow; webhook off + - Permissions: Account → Email addresses: read-only (the sign-in library reads it; it is not used). Nothing else. + - Installable only on this account; no need to install it + - Generate a client secret and store it: `gh secret set ACCESS_SITE_GITHUB_CLIENT_SECRET --env production -R modelcontextprotocol/access` (paste when prompted) + - Put the client ID (not secret) in `GITHUB_CLIENT_ID` in `wrangler.toml` by PR +4. **OIDC client secret** (whoever does step 6, so it stays with one person): + ```sh + openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n' > oidc-client-secret.txt + gh secret set ACCESS_SITE_OIDC_CLIENT_SECRET --env production -R modelcontextprotocol/access < oidc-client-secret.txt + ``` + Keep `oidc-client-secret.txt` for step 6, then delete it. +5. **Deploy**: merge (or re-run the `access-site` workflow) and check `https://access.modelcontextprotocol.io/.well-known/openid-configuration`. +6. **Google SSO profile** (super admin): in the [Admin console](https://admin.google.com), Security → Authentication → SSO with third-party IdP → add a custom **OIDC** profile: + - Issuer URL `https://access.modelcontextprotocol.io`, client ID `google-workspace`, client secret from step 4 + - Change password URL `https://github.com/settings/security` (passwords now live on GitHub) + - Copy the **Redirect URI** Google shows into `OIDC_REDIRECT_URIS` in `wrangler.toml` by PR + - Assign the profile to a pilot sub-OU holding only the pilot users first, then to **Model Context Protocol**. Super admins always keep Google sign-in, which is the break-glass path. + +## Operations + +- **Pilot**: `PILOT_GITHUB_LOGINS` in `wrangler.toml` limits sign-in to the listed GitHub logins (initially `sambhav`); a test checks each one maps to an account. Add logins to widen the pilot, and remove the line by PR to open sign-in to everyone in `users.ts`. +- **Rotate the signing key**: copy the current key object (`keys[0]`) from `https://access.modelcontextprotocol.io/jwks` into the `ACCESS_SITE_OIDC_PREVIOUS_JWK` secret, replace `ACCESS_SITE_OIDC_SIGNING_KEY` as in setup step 2, and re-run the workflow. A day later set `ACCESS_SITE_OIDC_PREVIOUS_JWK` to `{}` and re-run. +- **Logs**: sign-ins and refusals are JSON lines in Workers Logs (dashboard, or `npx wrangler tail`). `signin.denied` with `reason: identity_mismatch` means a pinned login came from a different GitHub account. +- **Clear an identity pin** (after confirming the person, e.g. they recreated their GitHub account): `npx wrangler d1 execute access-site --remote --command "DELETE FROM github_identities WHERE login = ''"`. + +## Development + +```sh +npm ci +npm test # vitest, Node 22+ (uses node:sqlite in place of D1) +npm run typecheck +npm run dev # wrangler dev +``` + +## Google behaviour + +- The redirect URI is specific to each SSO profile and only shown once the profile exists, so it is set by PR after setup step 6. +- Google doesn't document whether it sends PKCE; both cases are supported. diff --git a/access-site/migrations/0001_init.sql b/access-site/migrations/0001_init.sql new file mode 100644 index 00000000..6b417e7a --- /dev/null +++ b/access-site/migrations/0001_init.sql @@ -0,0 +1,30 @@ +-- Sign-in requests from Google, held while the user is at GitHub. +CREATE TABLE pending_authorizations ( + id_hash TEXT PRIMARY KEY, + client_id TEXT NOT NULL, + redirect_uri TEXT NOT NULL, + state TEXT, + nonce TEXT, + code_challenge TEXT, + expires_at INTEGER NOT NULL +); + +-- Single-use authorization codes (stored hashed; consumed with DELETE ... RETURNING). +CREATE TABLE authorization_codes ( + code_hash TEXT PRIMARY KEY, + client_id TEXT NOT NULL, + redirect_uri TEXT NOT NULL, + nonce TEXT, + code_challenge TEXT, + claims TEXT NOT NULL, + expires_at INTEGER NOT NULL +); + +-- First GitHub user ID seen signing in with each login (trust on first use). +-- A different ID for the same login means the login was freed by a rename +-- and registered by someone else, so sign-in is refused. +CREATE TABLE github_identities ( + login TEXT PRIMARY KEY, + github_id TEXT NOT NULL, + first_seen INTEGER NOT NULL +); diff --git a/access-site/package-lock.json b/access-site/package-lock.json new file mode 100644 index 00000000..4c3e3067 --- /dev/null +++ b/access-site/package-lock.json @@ -0,0 +1,2686 @@ +{ + "name": "@mcp/access-site", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@mcp/access-site", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@hono/oauth-providers": "^0.9.0", + "hono": "^4.13.13", + "jose": "^6.2.13" + }, + "devDependencies": { + "@cloudflare/workers-types": "^5.20261024.1", + "@types/node": "^24.19.2", + "typescript": "^5.9.2", + "vitest": "^5.0.5", + "wrangler": "^4.152.0" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.2.tgz", + "integrity": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261019.1.tgz", + "integrity": "sha512-5ZILbAta/fBQvhmAYeCE7dKMMYIr7N9wJEX50fEGLKSIwGtvT0HzSfHNIK/O7cBQ4gHQ2WfIiPClvwcye6E0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261019.1.tgz", + "integrity": "sha512-cww2Ab6lVMxpTiEEjKmPI58p5EMo0vYTZSH+3WkqXJdQgc3ES66GuL247+U2B2YWajhhu2tqj7jySrXneU749g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261019.1.tgz", + "integrity": "sha512-4++VRi9vkQROYqrpZYOixIJuVDmujkwrT4kszsxNFlvcmsX6Mr2GumvxIN3gIixS3vJUh3Umdtkthqj5X0S3Xw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261019.1.tgz", + "integrity": "sha512-75PVtJOwfjScQ53sbLtQXI7D8yP1c21t+WEUPaYbqkzaz4ZqfR5wAd+zuWs8G5Q7kOj2SQIvEfY5Sn8Y+UVrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261019.1.tgz", + "integrity": "sha512-z49fvKTETMCnG6wnMcQwxx9CpryFceiKz04FRjq26EkhCyzRNJ6c3UXhyZz3r5f1CXnnBZc5DcU8ZouPkUJCqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "5.20261024.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20261024.1.tgz", + "integrity": "sha512-sNhbgBsuVvmumSurgCcIrXShqACPfRt/XlYFstBeANu5BHEJhXW+e7PmHxDNYRXxKYZV57toUfkLwFp8/0SZKA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/oauth-providers": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@hono/oauth-providers/-/oauth-providers-0.9.0.tgz", + "integrity": "sha512-OgDPB+AM6OgA5ys1y+7G24bkb8V4OINr+6JFDSuSYni80Mmd7BGpJ0e02StQDMPL9ebuWI8ABhxx1bk+9VBcBA==", + "license": "MIT", + "engines": { + "node": ">=18.4.0" + }, + "peerDependencies": { + "hono": ">=3.0.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hono": { + "version": "4.13.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", + "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/jose": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz", + "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/miniflare": { + "version": "5.20261019.2-beta", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261019.2-beta.tgz", + "integrity": "sha512-QNgCA/axizZ7kv7RV3nwbRoz+w4watBlwIsyXjwgYHw4eSw5BrR1F2152s3Kz+Bzy/YxjKFHrZl3LzBRbPgYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.5", + "undici": "7.29.1", + "workerd": "1.20261019.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/workerd": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261019.1.tgz", + "integrity": "sha512-UUYRUq/7DmeCl11YriNbcVbjUZVLtt8uuCXFu+nz77hGFf8d4DEVXoCYtnA+MlBatE4+wxoj47DVqzK3xGBAiw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20261019.1", + "@cloudflare/workerd-darwin-arm64": "1.20261019.1", + "@cloudflare/workerd-linux-64": "1.20261019.1", + "@cloudflare/workerd-linux-arm64": "1.20261019.1", + "@cloudflare/workerd-windows-64": "1.20261019.1" + } + }, + "node_modules/wrangler": { + "version": "4.152.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.152.0.tgz", + "integrity": "sha512-lmXTF1sCDEqBaEMpreGczvg/x3AIh1d/qxc4BN7utQ4jEDwdA4YG0L6kr3ev0TtVLHRb6QLySNAOAunwn+dHeA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.2", + "miniflare": "5.20261019.2-beta", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20261019.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20261019.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/access-site/package.json b/access-site/package.json new file mode 100644 index 00000000..a969ac78 --- /dev/null +++ b/access-site/package.json @@ -0,0 +1,31 @@ +{ + "name": "@mcp/access-site", + "version": "0.1.0", + "private": true, + "description": "access.modelcontextprotocol.io: sign in to MCP Google Workspace with GitHub (OIDC provider for Google)", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "license": "MIT", + "dependencies": { + "@hono/oauth-providers": "^0.9.0", + "hono": "^4.13.13", + "jose": "^6.2.13" + }, + "devDependencies": { + "@cloudflare/workers-types": "^5.20261024.1", + "@types/node": "^24.19.2", + "typescript": "^5.9.2", + "vitest": "^5.0.5", + "wrangler": "^4.152.0" + }, + "engines": { + "node": ">=24" + }, + "overrides": { + "undici": "^7.30.1" + } +} diff --git a/access-site/scripts/deploy.sh b/access-site/scripts/deploy.sh new file mode 100755 index 00000000..db6533a5 --- /dev/null +++ b/access-site/scripts/deploy.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Provisions and deploys access.modelcontextprotocol.io. Run by CI +# (.github/workflows/access-site.yml); safe to re-run. +# +# 1. Finds the "access-site" D1 database, creating it on first run +# 2. Applies D1 migrations +# 3. Deploys the Worker, uploading secrets from the environment +# +# Required env: CLOUDFLARE_API_TOKEN, OIDC_SIGNING_KEY, OIDC_CLIENT_SECRET +# Optional env: GITHUB_CLIENT_SECRET (uploaded when set; the previous value is kept otherwise), +# OIDC_PREVIOUS_JWK (previous public JWK during key rotation; uploaded when set, "{}" retires it) +# Non-secret settings (account, GitHub App client ID, Google redirect URI) are in wrangler.toml. +set -euo pipefail +cd "$(dirname "$0")/.." + +: "${CLOUDFLARE_API_TOKEN:?missing}" "${OIDC_SIGNING_KEY:?missing}" "${OIDC_CLIENT_SECRET:?missing}" + +DB_NAME=access-site +db_id() { npx wrangler d1 list --json | jq -r --arg n "$DB_NAME" '.[] | select(.name == $n) | .uuid'; } + +DB_ID="$(db_id)" +if [ -z "$DB_ID" ]; then + echo "Creating D1 database $DB_NAME" + npx wrangler d1 create "$DB_NAME" >/dev/null + DB_ID="$(db_id)" +fi +[ -n "$DB_ID" ] || { echo "Could not find or create D1 database $DB_NAME" >&2; exit 1; } +sed -i "s/REPLACE_WITH_D1_DATABASE_ID/${DB_ID}/" wrangler.toml + +for placeholder in REPLACE_WITH_GITHUB_APP_CLIENT_ID REPLACE_WITH_GOOGLE_SSO_REDIRECT_URI; do + if grep -q "$placeholder" wrangler.toml; then + echo "::warning::$placeholder is still a placeholder in wrangler.toml; sign-in will not work until it is set." + fi +done + +npx wrangler d1 migrations apply "$DB_NAME" --remote + +secrets="$(mktemp)" +trap 'rm -f "$secrets"' EXIT +chmod 600 "$secrets" +jq -n \ + --arg signing "$OIDC_SIGNING_KEY" \ + --arg client "$OIDC_CLIENT_SECRET" \ + --arg github "${GITHUB_CLIENT_SECRET:-}" \ + --arg previous "${OIDC_PREVIOUS_JWK:-}" \ + '{OIDC_SIGNING_KEY: $signing, OIDC_CLIENT_SECRET: $client} + + (if $github == "" then {} else {GITHUB_CLIENT_SECRET: $github} end) + + (if $previous == "" then {} else {OIDC_PREVIOUS_JWK: $previous} end)' >"$secrets" + +npx wrangler deploy --secrets-file "$secrets" diff --git a/access-site/src/crypto.ts b/access-site/src/crypto.ts new file mode 100644 index 00000000..a749cb8c --- /dev/null +++ b/access-site/src/crypto.ts @@ -0,0 +1,65 @@ +/** + * Small helpers around jose and WebCrypto. All JWT/JWK work goes through jose; + * this file only adds key caching and the few primitives jose doesn't cover. + */ +import { calculateJwkThumbprint, exportJWK, importPKCS8, type JWK } from 'jose'; + +const encoder = new TextEncoder(); + +export function nowSeconds(): number { + return Math.floor(Date.now() / 1000); +} + +function base64Url(bytes: ArrayBuffer | Uint8Array): string { + const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); + let binary = ''; + for (const b of arr) binary += String.fromCharCode(b); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +export function randomToken(bytes = 32): string { + return base64Url(crypto.getRandomValues(new Uint8Array(bytes))); +} + +/** SHA-256, base64url. Used for PKCE and to store codes and session IDs hashed. */ +export async function sha256(input: string): Promise { + return base64Url(await crypto.subtle.digest('SHA-256', encoder.encode(input))); +} + +/** Constant-time comparison for secrets. */ +export function timingSafeEqual(a: string, b: string): boolean { + const ab = encoder.encode(a); + const bb = encoder.encode(b); + let diff = ab.length ^ bb.length; + for (let i = 0; i < Math.max(ab.length, bb.length); i++) diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0); + return diff === 0; +} + +/** RFC 7636 S256 check. */ +export async function verifyPkce(verifier: string, challenge: string): Promise { + if (!/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) return false; + return timingSafeEqual(await sha256(verifier), challenge); +} + +export interface SigningKey { + privateKey: CryptoKey; + publicJwk: JWK & { kid: string }; +} + +const signingKeys = new Map>(); + +/** Loads the OIDC signing key once per isolate; kid is the RFC 7638 thumbprint. */ +export function loadSigningKey(pkcs8Pem: string): Promise { + let cached = signingKeys.get(pkcs8Pem); + if (!cached) { + cached = (async () => { + const privateKey = await importPKCS8(pkcs8Pem, 'RS256', { extractable: true }); + const { kty, n, e } = await exportJWK(privateKey); + const publicJwk = { kty, n, e, alg: 'RS256', use: 'sig' }; + const kid = await calculateJwkThumbprint(publicJwk); + return { privateKey, publicJwk: { ...publicJwk, kid } }; + })(); + signingKeys.set(pkcs8Pem, cached); + } + return cached; +} diff --git a/access-site/src/db.ts b/access-site/src/db.ts new file mode 100644 index 00000000..b8d0fe54 --- /dev/null +++ b/access-site/src/db.ts @@ -0,0 +1,127 @@ +/** + * Short-lived sign-in state in D1. Rows are single-use (read with + * DELETE ... RETURNING), stored under a SHA-256 hash of their ID, and expire + * within minutes. Nothing about accounts is stored here. + */ +import { nowSeconds, sha256 } from './crypto'; + +export interface PendingAuthorization { + client_id: string; + redirect_uri: string; + state: string | null; + nonce: string | null; + /** PKCE S256 challenge; null if the client did not send one (it still authenticates with its secret). */ + code_challenge: string | null; +} + +export interface AuthorizationCode extends Omit { + claims: Record; +} + +/** Removes expired rows. */ +async function deleteExpired(db: D1Database): Promise { + const now = nowSeconds(); + await db.batch([ + db.prepare(`DELETE FROM pending_authorizations WHERE expires_at < ?`).bind(now), + db.prepare(`DELETE FROM authorization_codes WHERE expires_at < ?`).bind(now), + ]); +} + +/** Share of new sign-ins that also clean up expired rows (keeps D1 writes low). */ +const CLEANUP_RATE = 0.05; + +export async function savePending( + db: D1Database, + id: string, + p: PendingAuthorization, + ttlSeconds: number +): Promise { + if (Math.random() < CLEANUP_RATE) await deleteExpired(db); + await db + .prepare( + `INSERT INTO pending_authorizations + (id_hash, client_id, redirect_uri, state, nonce, code_challenge, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .bind( + await sha256(id), + p.client_id, + p.redirect_uri, + p.state, + p.nonce, + p.code_challenge, + nowSeconds() + ttlSeconds + ) + .run(); +} + +export async function takePending( + db: D1Database, + id: string +): Promise { + const row = await db + .prepare(`DELETE FROM pending_authorizations WHERE id_hash = ? RETURNING *`) + .bind(await sha256(id)) + .first(); + if (!row || row.expires_at < nowSeconds()) return null; + return row; +} + +export async function saveCode( + db: D1Database, + code: string, + c: AuthorizationCode, + ttlSeconds: number +): Promise { + await db + .prepare( + `INSERT INTO authorization_codes + (code_hash, client_id, redirect_uri, nonce, code_challenge, claims, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .bind( + await sha256(code), + c.client_id, + c.redirect_uri, + c.nonce, + c.code_challenge, + JSON.stringify(c.claims), + nowSeconds() + ttlSeconds + ) + .run(); +} + +export async function takeCode(db: D1Database, code: string): Promise { + const row = await db + .prepare(`DELETE FROM authorization_codes WHERE code_hash = ? RETURNING *`) + .bind(await sha256(code)) + .first & { claims: string; expires_at: number }>(); + if (!row || row.expires_at < nowSeconds()) return null; + return { ...row, claims: JSON.parse(row.claims) as Record }; +} + +/** + * Pins each GitHub login to the first GitHub user ID seen signing in with it + * (trust on first use). GitHub frees a login when its owner renames, so a + * different ID for a pinned login means someone else registered that name. + * Returns false on a mismatch. Clear the row only after confirming the + * person (e.g. they deleted and recreated their GitHub account). + */ +export async function pinGithubIdentity( + db: D1Database, + login: string, + githubId: string +): Promise { + const key = login.toLowerCase(); + await db + .prepare( + `INSERT OR IGNORE INTO github_identities (login, github_id, first_seen) VALUES (?, ?, ?)` + ) + .bind(key, githubId, nowSeconds()) + .run(); + const row = await db + .prepare(`SELECT github_id FROM github_identities WHERE login = ?`) + .bind(key) + .first<{ github_id: string }>(); + return row?.github_id === githubId; +} diff --git a/access-site/src/eligibility.ts b/access-site/src/eligibility.ts new file mode 100644 index 00000000..ea5fd0fd --- /dev/null +++ b/access-site/src/eligibility.ts @@ -0,0 +1,32 @@ +/** + * Who may sign in, and as which Workspace address, read from the same config + * Pulumi uses to provision accounts (src/config in this repo). users.ts stays + * the source of truth; the Worker bundles it and is redeployed on every change. + */ +import { MEMBERS } from '../../src/config/users'; +import { buildRoleLookup, type Role } from '../../src/config/roles'; +import { hasProvisionUserRole, type Member } from '../../src/config/utils'; +import type { RoleId } from '../../src/config/roleIds'; + +const roleLookup: Map = buildRoleLookup(); + +const membersByLogin = new Map(); +for (const member of MEMBERS) { + if (member.github) membersByLogin.set(member.github.toLowerCase(), member); +} + +export function findMember(githubLogin: string): Member | undefined { + return membersByLogin.get(githubLogin.toLowerCase()); +} + +/** + * The Workspace address this GitHub user signs in as, or null if they have + * none: not in users.ts, no googleEmailPrefix, opted out, or no longer in a + * role that grants an account (same rule Pulumi uses to provision). + */ +export function workspaceEmailFor(githubLogin: string, domain: string): string | null { + const member = findMember(githubLogin); + if (!member?.googleEmailPrefix || member.skipGoogleUserProvisioning) return null; + if (!hasProvisionUserRole(member.memberOf, roleLookup)) return null; + return `${member.googleEmailPrefix.toLowerCase()}@${domain}`; +} diff --git a/access-site/src/env.ts b/access-site/src/env.ts new file mode 100644 index 00000000..bf53763f --- /dev/null +++ b/access-site/src/env.ts @@ -0,0 +1,26 @@ +export interface Env { + DB: D1Database; + /** Per-IP limit for /authorize and /login (optional so tests run without it). */ + RATE_LIMITER?: RateLimit; + + ISSUER: string; + WORKSPACE_DOMAIN: string; + GITHUB_CLIENT_ID: string; + OIDC_CLIENT_ID: string; + /** Comma-separated list of exact redirect URIs accepted from the OIDC client. */ + OIDC_REDIRECT_URIS: string; + /** + * Optional comma-separated GitHub logins. When set, only these users can + * sign in (rollout pilot); remove it to open sign-in to everyone in users.ts. + */ + PILOT_GITHUB_LOGINS?: string; + + OIDC_CLIENT_SECRET: string; + /** RSA private key, PKCS#8 PEM. */ + OIDC_SIGNING_KEY: string; + /** Optional public JWK (JSON) of the previous signing key, published during rotation. */ + OIDC_PREVIOUS_JWK?: string; + GITHUB_CLIENT_SECRET: string; +} + +export type AppEnv = { Bindings: Env }; diff --git a/access-site/src/index.ts b/access-site/src/index.ts new file mode 100644 index 00000000..3d18446c --- /dev/null +++ b/access-site/src/index.ts @@ -0,0 +1,156 @@ +import { Hono } from 'hono'; +import { deleteCookie, getCookie } from 'hono/cookie'; +import { createMiddleware } from 'hono/factory'; +import { HTTPException } from 'hono/http-exception'; +import { secureHeaders } from 'hono/secure-headers'; +import { githubAuth } from '@hono/oauth-providers/github'; +import type { AppEnv, Env } from './env'; +import { takePending } from './db'; +import { + authorize, + completeAuthorization, + discovery, + jwks, + PENDING_COOKIE, + token, + userinfo, + type GitHubIdentity, +} from './oidc'; +import * as pages from './pages'; + +const app = new Hono(); + +app.use( + '*', + secureHeaders({ + contentSecurityPolicy: { + defaultSrc: ["'none'"], + styleSrc: ["'unsafe-inline'"], + frameAncestors: ["'none'"], + baseUri: ["'none'"], + }, + referrerPolicy: 'no-referrer', + }) +); + +/** + * Per-IP rate limit for the unauthenticated, browser-facing endpoints, which + * write to D1 or call GitHub. The binding is optional so tests and local dev + * work without it; production config always sets it. + */ +const rateLimited = createMiddleware(async (c, next) => { + if (c.env.RATE_LIMITER) { + const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'; + const { success } = await c.env.RATE_LIMITER.limit({ key: ip }); + if (!success) { + return c.html(pages.errorPage('Too many sign-in attempts. Please wait a minute.'), 429); + } + } + await next(); +}); + +// --- OIDC provider (used by Google) ----------------------------------------- + +app.get('/.well-known/openid-configuration', discovery); +app.get('/jwks', jwks); +app.get('/authorize', rateLimited, authorize); +app.post('/token', token); +app.on(['GET', 'POST'], '/userinfo', userinfo); + +// --- GitHub sign-in, then back to Google ------------------------------------- + +/** The GitHub token is only needed to read the user once; revoke it right away. */ +async function revokeGitHubToken(env: Env, accessToken: string): Promise { + try { + await fetch(`https://api.github.com/applications/${env.GITHUB_CLIENT_ID}/token`, { + method: 'DELETE', + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Basic ${btoa(`${env.GITHUB_CLIENT_ID}:${env.GITHUB_CLIENT_SECRET}`)}`, + 'Content-Type': 'application/json', + 'User-Agent': 'access.modelcontextprotocol.io', + }, + body: JSON.stringify({ access_token: accessToken }), + }); + } catch { + // best effort; the token expires on its own + } +} + +app.get( + '/login', + rateLimited, + async (c, next) => { + // Only start a GitHub sign-in when Google is waiting for one. + if (!getCookie(c, PENDING_COOKIE)) return c.redirect('/'); + return githubAuth({ + client_id: c.env.GITHUB_CLIENT_ID, + client_secret: c.env.GITHUB_CLIENT_SECRET, + redirect_uri: `${c.env.ISSUER}/login`, + })(c, next); + }, + async (c) => { + // The sign-in library's OAuth state cookie is single-use; drop it. + deleteCookie(c, 'state', { path: '/', secure: true }); + const githubToken = c.get('token')?.token; + if (githubToken) { + const revoke = revokeGitHubToken(c.env, githubToken); + try { + c.executionCtx.waitUntil(revoke); + } catch { + await revoke; // no execution context (tests) + } + } + + const user = c.get('user-github'); + if (typeof user?.id !== 'number' || typeof user.login !== 'string') { + throw new HTTPException(502, { message: 'GitHub did not return a user.' }); + } + const identity: GitHubIdentity = { + id: String(user.id), + login: user.login, + name: user.name ?? null, + }; + + const pendingId = getCookie(c, PENDING_COOKIE); + deleteCookie(c, PENDING_COOKIE, { path: '/', secure: true }); + const pending = pendingId ? await takePending(c.env.DB, pendingId) : null; + if (!pending) { + return c.html(pages.errorPage('Sign-in expired. Please start again from Google.'), 400); + } + + const result = await completeAuthorization(c.env, identity, pending); + const log = { github_id: identity.id, github_login: identity.login }; + if (!result.ok) { + console.log(JSON.stringify({ event: 'signin.denied', reason: result.reason, ...log })); + return c.html( + result.reason === 'identity_mismatch' + ? pages.identityMismatchPage(identity.login) + : result.reason === 'not_in_pilot' + ? pages.pilotPage(identity.login) + : pages.noAccountPage(identity.login, c.env.WORKSPACE_DOMAIN), + 403 + ); + } + console.log(JSON.stringify({ event: 'signin', ...log })); + return c.redirect(result.location); + } +); + +app.get('/', (c) => c.html(pages.homePage(c.env.WORKSPACE_DOMAIN))); +app.get('/healthz', (c) => c.text('ok')); + +app.onError((err, c) => { + if (err instanceof HTTPException && err.status < 500) { + return c.html( + pages.errorPage('Sign-in was interrupted. Please start again from Google.'), + err.status + ); + } + console.error(err); + return c.html(pages.errorPage('Something went wrong. Please try again in a minute.'), 500); +}); + +export { app }; + +export default { fetch: app.fetch } satisfies ExportedHandler; diff --git a/access-site/src/oidc.ts b/access-site/src/oidc.ts new file mode 100644 index 00000000..6e40cb80 --- /dev/null +++ b/access-site/src/oidc.ts @@ -0,0 +1,319 @@ +/** + * OpenID Connect provider for Google Workspace's custom OIDC SSO profile. + * + * Authorization code flow only, one confidential client (Google), RS256 ID + * tokens. The subject is the numeric GitHub user ID; the email claim is the + * Workspace address configured for that GitHub user in users.ts, which is + * what Google matches on. + */ +import type { Context } from 'hono'; +import { setCookie } from 'hono/cookie'; +import { SignJWT, createLocalJWKSet, jwtVerify, type JWK } from 'jose'; +import type { AppEnv, Env } from './env'; +import { loadSigningKey, randomToken, timingSafeEqual, verifyPkce } from './crypto'; +import { + pinGithubIdentity, + saveCode, + savePending, + takeCode, + type PendingAuthorization, +} from './db'; +import { workspaceEmailFor } from './eligibility'; + +export interface GitHubIdentity { + id: string; + login: string; + name: string | null; +} + +export const PENDING_COOKIE = '__Host-mcp_pending'; +const PENDING_TTL = 600; +const CODE_TTL = 120; +const TOKEN_TTL = 300; + +type Ctx = Context; + +function redirectUris(env: Env): string[] { + return env.OIDC_REDIRECT_URIS.split(',') + .map((u) => u.trim()) + .filter(Boolean); +} + +function oauthError(c: Ctx, error: string, description: string, status: 400 | 401 = 400) { + c.header('Cache-Control', 'no-store'); + return c.json({ error, error_description: description }, status); +} + +export function discovery(c: Ctx) { + const iss = c.env.ISSUER; + return c.json({ + issuer: iss, + authorization_endpoint: `${iss}/authorize`, + token_endpoint: `${iss}/token`, + userinfo_endpoint: `${iss}/userinfo`, + jwks_uri: `${iss}/jwks`, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + scopes_supported: ['openid', 'email', 'profile'], + token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], + code_challenge_methods_supported: ['S256'], + claims_supported: ['sub', 'email', 'email_verified', 'name', 'preferred_username'], + }); +} + +async function publicKeys(env: Env): Promise { + const keys: JWK[] = [(await loadSigningKey(env.OIDC_SIGNING_KEY)).publicJwk]; + // During rotation, also publish the previous public key. "{}" (or anything + // that isn't an RSA JWK) means there is none. + if (env.OIDC_PREVIOUS_JWK) { + try { + const previous = JSON.parse(env.OIDC_PREVIOUS_JWK) as JWK; + if (previous.kty === 'RSA' && previous.n && previous.e && previous.kid) { + keys.push({ + kty: 'RSA', + n: previous.n, + e: previous.e, + kid: previous.kid, + alg: 'RS256', + use: 'sig', + }); + } + } catch { + // ignore a malformed value rather than breaking sign-in + } + } + return keys; +} + +export async function jwks(c: Ctx) { + c.header('Cache-Control', 'public, max-age=300'); + return c.json({ keys: await publicKeys(c.env) }); +} + +/** GET /authorize: validate Google's request, park it, and send the user to GitHub. */ +export async function authorize(c: Ctx) { + const q = c.req.query(); + const env = c.env; + + // Never redirect to an unknown client or URI; show an error instead. + if (q.client_id !== env.OIDC_CLIENT_ID) return c.text('Unknown client_id', 400); + const redirectUri = q.redirect_uri ?? ''; + if (!redirectUris(env).includes(redirectUri)) return c.text('Unregistered redirect_uri', 400); + + const fail = (error: string) => { + const url = new URL(redirectUri); + url.searchParams.set('error', error); + if (q.state) url.searchParams.set('state', q.state); + return c.redirect(url.toString()); + }; + if (q.response_type !== 'code') return fail('unsupported_response_type'); + if (!(q.scope ?? '').split(' ').includes('openid')) return fail('invalid_scope'); + if ((q.state?.length ?? 0) > 1024 || (q.nonce?.length ?? 0) > 1024) + return fail('invalid_request'); + + let codeChallenge: string | null = null; + if (q.code_challenge !== undefined) { + if (q.code_challenge_method !== 'S256' || !/^[A-Za-z0-9_-]{43}$/.test(q.code_challenge)) { + return fail('invalid_request'); + } + codeChallenge = q.code_challenge; + } + + const pending: PendingAuthorization = { + client_id: q.client_id, + redirect_uri: redirectUri, + state: q.state ?? null, + nonce: q.nonce ?? null, + code_challenge: codeChallenge, + }; + const pendingId = randomToken(); + await savePending(env.DB, pendingId, pending, PENDING_TTL); + setCookie(c, PENDING_COOKIE, pendingId, { + path: '/', + httpOnly: true, + secure: true, + sameSite: 'Lax', + maxAge: PENDING_TTL, + }); + return c.redirect('/login'); +} + +/** During the rollout pilot, only the listed GitHub logins may sign in. */ +function inPilot(env: Env, login: string): boolean { + const pilot = (env.PILOT_GITHUB_LOGINS ?? '') + .split(',') + .map((l) => l.trim().toLowerCase()) + .filter(Boolean); + return pilot.length === 0 || pilot.includes(login.toLowerCase()); +} + +export type AuthorizationResult = + | { ok: true; location: string } + | { ok: false; reason: 'no_account' | 'not_in_pilot' | 'identity_mismatch' }; + +/** + * Called after GitHub sign-in when a Google request is pending. Returns the + * redirect back to Google with a code, or why this GitHub user can't sign in: + * no Workspace account in users.ts, or a login that is pinned to a different + * GitHub user ID (a freed and re-registered login). + */ +export async function completeAuthorization( + env: Env, + identity: GitHubIdentity, + pending: PendingAuthorization +): Promise { + const email = workspaceEmailFor(identity.login, env.WORKSPACE_DOMAIN); + if (!email) return { ok: false, reason: 'no_account' }; + if (!inPilot(env, identity.login)) return { ok: false, reason: 'not_in_pilot' }; + if (!(await pinGithubIdentity(env.DB, identity.login, identity.id))) { + return { ok: false, reason: 'identity_mismatch' }; + } + + const claims = { + sub: identity.id, + email, + email_verified: true, + name: identity.name || identity.login, + preferred_username: identity.login, + auth_time: Math.floor(Date.now() / 1000), + }; + const code = randomToken(); + await saveCode( + env.DB, + code, + { + client_id: pending.client_id, + redirect_uri: pending.redirect_uri, + nonce: pending.nonce, + code_challenge: pending.code_challenge, + claims, + }, + CODE_TTL + ); + const url = new URL(pending.redirect_uri); + url.searchParams.set('code', code); + if (pending.state) url.searchParams.set('state', pending.state); + return { ok: true, location: url.toString() }; +} + +/** RFC 6749 section 2.3.1: Basic credentials are form-urlencoded, then base64. */ +function formDecode(value: string): string { + return decodeURIComponent(value.replace(/\+/g, ' ')); +} + +function clientCredentials(c: Ctx, body: Record) { + const header = c.req.header('Authorization'); + if (header?.startsWith('Basic ')) { + try { + const decoded = atob(header.slice(6)); + const colon = decoded.indexOf(':'); + if (colon < 0) return { id: '', secret: '' }; + return { + id: formDecode(decoded.slice(0, colon)), + secret: formDecode(decoded.slice(colon + 1)), + }; + } catch { + return { id: '', secret: '' }; + } + } + return { + id: typeof body.client_id === 'string' ? body.client_id : '', + secret: typeof body.client_secret === 'string' ? body.client_secret : '', + }; +} + +/** POST /token: exchange a single-use code for an ID token. */ +export async function token(c: Ctx) { + const env = c.env; + const body = (await c.req.parseBody()) as Record; + const client = clientCredentials(c, body); + if ( + !timingSafeEqual(client.id, env.OIDC_CLIENT_ID) || + !timingSafeEqual(client.secret, env.OIDC_CLIENT_SECRET) + ) { + c.header('WWW-Authenticate', 'Basic realm="token"'); + return oauthError(c, 'invalid_client', 'Client authentication failed', 401); + } + if (body.grant_type !== 'authorization_code') { + return oauthError(c, 'unsupported_grant_type', 'Only authorization_code is supported'); + } + if (typeof body.code !== 'string') return oauthError(c, 'invalid_request', 'Missing code'); + + const code = await takeCode(env.DB, body.code); + if (!code || code.client_id !== client.id || code.redirect_uri !== body.redirect_uri) { + return oauthError(c, 'invalid_grant', 'Invalid or expired code'); + } + const verifier = typeof body.code_verifier === 'string' ? body.code_verifier : ''; + if (code.code_challenge) { + if (!(await verifyPkce(verifier, code.code_challenge))) { + return oauthError(c, 'invalid_grant', 'PKCE verification failed'); + } + } else if (verifier) { + // RFC 9700: a verifier without a challenge at /authorize is a downgrade attempt. + return oauthError(c, 'invalid_grant', 'Unexpected code_verifier'); + } + + const { privateKey, publicJwk } = await loadSigningKey(env.OIDC_SIGNING_KEY); + const sub = String(code.claims.sub); + const idToken = await new SignJWT({ + ...code.claims, + ...(code.nonce ? { nonce: code.nonce } : {}), + }) + .setProtectedHeader({ alg: 'RS256', kid: publicJwk.kid, typ: 'JWT' }) + .setIssuer(env.ISSUER) + .setAudience(client.id) + .setSubject(sub) + .setIssuedAt() + .setExpirationTime(`${TOKEN_TTL}s`) + .sign(privateKey); + const accessToken = await new SignJWT({ + ...code.claims, + client_id: client.id, + scope: 'openid email profile', + }) + .setProtectedHeader({ alg: 'RS256', kid: publicJwk.kid, typ: 'at+jwt' }) + .setIssuer(env.ISSUER) + .setAudience(`${env.ISSUER}/userinfo`) + .setSubject(sub) + .setIssuedAt() + .setExpirationTime(`${TOKEN_TTL}s`) + .sign(privateKey); + + c.header('Cache-Control', 'no-store'); + return c.json({ + access_token: accessToken, + token_type: 'Bearer', + expires_in: TOKEN_TTL, + id_token: idToken, + scope: 'openid email profile', + }); +} + +/** GET|POST /userinfo */ +export async function userinfo(c: Ctx) { + const header = c.req.header('Authorization') ?? ''; + if (!header.startsWith('Bearer ')) { + c.header('WWW-Authenticate', 'Bearer'); + return oauthError(c, 'invalid_token', 'Missing bearer token', 401); + } + try { + const { payload } = await jwtVerify( + header.slice(7), + createLocalJWKSet({ keys: await publicKeys(c.env) }), + { issuer: c.env.ISSUER, audience: `${c.env.ISSUER}/userinfo`, typ: 'at+jwt' } + ); + c.header('Cache-Control', 'no-store'); + return c.json({ + sub: payload.sub, + email: payload.email, + email_verified: payload.email_verified, + name: payload.name, + preferred_username: payload.preferred_username, + }); + } catch { + c.header('WWW-Authenticate', 'Bearer error="invalid_token"'); + return oauthError(c, 'invalid_token', 'Invalid or expired token', 401); + } +} diff --git a/access-site/src/pages.ts b/access-site/src/pages.ts new file mode 100644 index 00000000..098b5e35 --- /dev/null +++ b/access-site/src/pages.ts @@ -0,0 +1,93 @@ +import { html } from 'hono/html'; +import type { HtmlEscapedString } from 'hono/utils/html'; + +type Html = HtmlEscapedString | Promise; + +function layout(body: Html): Html { + return html` + + + + + MCP sign-in + + + +
+

Model Context Protocol sign-in

+ ${body} +
+ + `; +} + +export function homePage(domain: string): Html { + return layout( + html`

+ This service signs maintainers in to their @${domain} Google account with + GitHub. Open Gmail or any Google app to + sign in. +

+

+ Accounts are managed in + modelcontextprotocol/access. +

` + ); +} + +export function noAccountPage(login: string, domain: string): Html { + return layout( + html`

@${login} has no @${domain} account.

+

+ Accounts are set up in + modelcontextprotocol/access: if + you are a maintainer, open a PR adding googleEmailPrefix to your entry in + src/config/users.ts. +

` + ); +} + +export function identityMismatchPage(login: string): Html { + return layout( + html`

Sign-in refused for @${login}.

+

+ This GitHub login was previously used by a different GitHub account. If you renamed your + account, ask in the Infrastructure WG channel to update your entry in + modelcontextprotocol/access. +

` + ); +} + +export function pilotPage(login: string): Html { + return layout( + html`

GitHub sign-in isn't enabled for @${login} yet.

+

+ It is being piloted with a few maintainers first. Please use your usual sign-in for now. +

` + ); +} + +export function errorPage(message: string): Html { + return layout(html`

${message}

`); +} diff --git a/access-site/test/helpers.ts b/access-site/test/helpers.ts new file mode 100644 index 00000000..c831e5f5 --- /dev/null +++ b/access-site/test/helpers.ts @@ -0,0 +1,157 @@ +/** + * Test harness: a D1 stand-in over node:sqlite (real SQL, including + * DELETE ... RETURNING), generated keys, and a fetch mock for GitHub. Picks + * real members from src/config so tests follow the live rules. + */ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; +import { exportPKCS8, generateKeyPair } from 'jose'; +import { vi } from 'vitest'; +import type { Env } from '../src/env'; +import { MEMBERS } from '../../src/config/users'; +import { buildRoleLookup } from '../../src/config/roles'; +import { hasProvisionUserRole } from '../../src/config/utils'; + +// --- D1 stand-in --------------------------------------------------------------- + +class Statement { + constructor( + private db: DatabaseSync, + private sql: string, + private params: unknown[] = [] + ) {} + bind(...params: unknown[]) { + return new Statement(this.db, this.sql, params); + } + private args() { + return this.params as never[]; + } + async first(): Promise { + return (this.db.prepare(this.sql).get(...this.args()) as T | undefined) ?? null; + } + async run() { + const r = this.db.prepare(this.sql).run(...this.args()); + return { success: true, results: [], meta: { changes: Number(r.changes) } }; + } +} + +export function createD1(): D1Database { + const db = new DatabaseSync(':memory:'); + db.exec(readFileSync(join(import.meta.dirname, '../migrations/0001_init.sql'), 'utf8')); + return { + prepare: (sql: string) => new Statement(db, sql), + batch: async (stmts: Statement[]) => { + db.exec('BEGIN'); + try { + const out = []; + for (const s of stmts) out.push(await s.run()); + db.exec('COMMIT'); + return out; + } catch (e) { + db.exec('ROLLBACK'); + throw e; + } + }, + } as unknown as D1Database; +} + +// --- Env ------------------------------------------------------------------------- + +export const ISSUER = 'https://access.modelcontextprotocol.io'; +export const GOOGLE_REDIRECT = 'https://www.google.com/a/oidc/callback-test'; +export const CLIENT_SECRET = 'test-client-secret'; + +export async function createEnv(overrides: Partial = {}): Promise { + const signing = await generateKeyPair('RS256', { extractable: true }); + return { + DB: createD1(), + ISSUER, + WORKSPACE_DOMAIN: 'modelcontextprotocol.io', + GITHUB_CLIENT_ID: 'gh-client', + GITHUB_CLIENT_SECRET: 'gh-secret', + OIDC_CLIENT_ID: 'google-workspace', + OIDC_REDIRECT_URIS: GOOGLE_REDIRECT, + OIDC_CLIENT_SECRET: CLIENT_SECRET, + OIDC_SIGNING_KEY: await exportPKCS8(signing.privateKey), + ...overrides, + }; +} + +// --- Config-driven test identities --------------------------------------------- + +const roleLookup = buildRoleLookup(); + +/** A member who has a Workspace account in users.ts. */ +export function memberWithAccount() { + const m = MEMBERS.find( + (m) => + m.github && + m.googleEmailPrefix && + !m.skipGoogleUserProvisioning && + hasProvisionUserRole(m.memberOf, roleLookup) + ); + if (!m) throw new Error('no member with a Workspace account in config'); + return m; +} + +/** An eligible member with no googleEmailPrefix (no account yet). */ +export function memberWithoutAccount() { + const m = MEMBERS.find( + (m) => + m.github && + !m.googleEmailPrefix && + !m.skipGoogleUserProvisioning && + hasProvisionUserRole(m.memberOf, roleLookup) + ); + if (!m) throw new Error('no eligible member without an account in config'); + return m; +} + +/** A configured member with a prefix but no role that grants an account, if any; else any ineligible member. */ +export function ineligibleMember() { + const m = MEMBERS.find((m) => m.github && !hasProvisionUserRole(m.memberOf, roleLookup)); + if (!m) throw new Error('no ineligible member in config'); + return m; +} + +// --- GitHub fetch mock -------------------------------------------------------------- + +export function mockGitHub(user: { id: number; login: string; name: string | null }) { + const json = (data: unknown) => + new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }); + const mock = vi.fn(async (input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url; + if (url === 'https://github.com/login/oauth/access_token') { + return json({ access_token: 'gh-user-token', token_type: 'bearer', scope: '' }); + } + if (url === 'https://api.github.com/user') return json({ ...user, email: null }); + if (url === 'https://api.github.com/user/emails') { + return json([{ email: 'person@example.com', primary: true, verified: true }]); + } + if (url === 'https://api.github.com/applications/gh-client/token') { + return new Response(null, { status: 204 }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal('fetch', mock); + return mock; +} + +// --- Cookie jar for multi-step flows ------------------------------------------------- + +export class CookieJar { + private cookies = new Map(); + store(res: Response) { + for (const header of res.headers.getSetCookie()) { + const [pair] = header.split(';'); + const [name, ...value] = pair!.split('='); + const v = value.join('='); + if (/Max-Age=0/i.test(header) || v === '') this.cookies.delete(name!); + else this.cookies.set(name!, v); + } + } + header(): string { + return [...this.cookies].map(([k, v]) => `${k}=${v}`).join('; '); + } +} diff --git a/access-site/test/oidc.test.ts b/access-site/test/oidc.test.ts new file mode 100644 index 00000000..7417935f --- /dev/null +++ b/access-site/test/oidc.test.ts @@ -0,0 +1,366 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createLocalJWKSet, jwtVerify } from 'jose'; +import { app } from '../src/index'; +import type { Env } from '../src/env'; +import { sha256 } from '../src/crypto'; +import { workspaceEmailFor } from '../src/eligibility'; +import { MEMBERS } from '../../src/config/users'; +import { + CLIENT_SECRET, + CookieJar, + GOOGLE_REDIRECT, + ISSUER, + createEnv, + ineligibleMember, + memberWithAccount, + memberWithoutAccount, + mockGitHub, +} from './helpers'; + +const VERIFIER = 'v'.repeat(43) + '-verifier-for-tests'; +const DOMAIN = 'modelcontextprotocol.io'; + +let env: Env; +let lastJar: CookieJar; +let lastGitHub: ReturnType; + +async function call(path: string, jar: CookieJar) { + const res = await app.request(`${ISSUER}${path}`, { headers: { Cookie: jar.header() } }, env); + jar.store(res); + return res; +} + +/** Runs Google -> /authorize -> GitHub -> back; returns the final response. */ +async function signIn( + github: { id: number; login: string }, + params: Record = {} +) { + lastGitHub = mockGitHub({ ...github, name: 'Test Person' }); + const jar = new CookieJar(); + lastJar = jar; + const query = new URLSearchParams( + Object.entries({ + client_id: 'google-workspace', + redirect_uri: GOOGLE_REDIRECT, + response_type: 'code', + scope: 'openid email profile', + state: 'google-state', + nonce: 'google-nonce', + code_challenge: await sha256(VERIFIER), + code_challenge_method: 'S256', + ...params, + }).filter((e): e is [string, string] => e[1] !== undefined) + ); + const authorize = await call(`/authorize?${query}`, jar); + expect(authorize.status).toBe(302); + expect(authorize.headers.get('Location')).toBe('/login'); + + const toGitHub = await call('/login', jar); + expect(toGitHub.status).toBe(302); + const githubUrl = new URL(toGitHub.headers.get('Location')!); + expect(githubUrl.origin).toBe('https://github.com'); + + return call(`/login?code=gh-code&state=${githubUrl.searchParams.get('state')}`, jar); +} + +function codeFrom(res: Response): string { + return new URL(res.headers.get('Location')!).searchParams.get('code')!; +} + +async function exchange(code: string, extra: Record = {}) { + return app.request( + `${ISSUER}/token`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${btoa(`google-workspace:${CLIENT_SECRET}`)}`, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: GOOGLE_REDIRECT, + code_verifier: VERIFIER, + ...extra, + }), + }, + env + ); +} + +beforeEach(async () => { + env = await createEnv(); + vi.spyOn(console, 'log').mockImplementation(() => {}); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +describe('users.ts mapping', () => { + it('maps a configured member to their address, case-insensitively', () => { + const m = memberWithAccount(); + expect(workspaceEmailFor(m.github!.toUpperCase(), DOMAIN)).toBe( + `${m.googleEmailPrefix!.toLowerCase()}@${DOMAIN}` + ); + }); + it('returns null without a prefix, without an eligible role, or when unknown', () => { + expect(workspaceEmailFor(memberWithoutAccount().github!, DOMAIN)).toBeNull(); + expect(workspaceEmailFor(ineligibleMember().github!, DOMAIN)).toBeNull(); + expect(workspaceEmailFor('definitely-not-a-configured-user', DOMAIN)).toBeNull(); + }); +}); + +describe('discovery', () => { + it('publishes metadata and a public-only JWKS', async () => { + const meta = (await ( + await app.request(`${ISSUER}/.well-known/openid-configuration`, {}, env) + ).json()) as Record; + expect(meta.issuer).toBe(ISSUER); + expect(meta.token_endpoint).toBe(`${ISSUER}/token`); + const jwks = (await (await app.request(`${ISSUER}/jwks`, {}, env)).json()) as { + keys: { kid: string; d?: string }[]; + }; + expect(jwks.keys).toHaveLength(1); + expect(jwks.keys[0]!.kid).toBeTruthy(); + expect(jwks.keys[0]!.d).toBeUndefined(); + }); + + it('publishes the previous key during rotation, and ignores "{}"', async () => { + const current = (await (await app.request(`${ISSUER}/jwks`, {}, env)).json()) as { + keys: Record[]; + }; + const previous = { ...current.keys[0]!, kid: 'previous-kid' }; + const rotating = await createEnv({ OIDC_PREVIOUS_JWK: JSON.stringify(previous) }); + const both = (await (await app.request(`${ISSUER}/jwks`, {}, rotating)).json()) as { + keys: { kid: string }[]; + }; + expect(both.keys.map((k) => k.kid)).toContain('previous-kid'); + expect(both.keys).toHaveLength(2); + + const retired = await createEnv({ OIDC_PREVIOUS_JWK: '{}' }); + const one = (await (await app.request(`${ISSUER}/jwks`, {}, retired)).json()) as { + keys: unknown[]; + }; + expect(one.keys).toHaveLength(1); + }); +}); + +describe('sign-in', () => { + it('signs in a configured member with the GitHub ID as subject', async () => { + const member = memberWithAccount(); + const back = await signIn({ id: 4242, login: member.github! }); + expect(back.status).toBe(302); + const location = new URL(back.headers.get('Location')!); + expect(`${location.origin}${location.pathname}`).toBe(GOOGLE_REDIRECT); + expect(location.searchParams.get('state')).toBe('google-state'); + + const res = await exchange(codeFrom(back)); + expect(res.status).toBe(200); + const body = (await res.json()) as { id_token: string; access_token: string }; + + const jwks = (await (await app.request(`${ISSUER}/jwks`, {}, env)).json()) as { keys: never[] }; + const { payload } = await jwtVerify(body.id_token, createLocalJWKSet(jwks), { + issuer: ISSUER, + audience: 'google-workspace', + }); + expect(payload.sub).toBe('4242'); + expect(payload.email).toBe(`${member.googleEmailPrefix!.toLowerCase()}@${DOMAIN}`); + expect(payload.nonce).toBe('google-nonce'); + expect(payload.preferred_username).toBe(member.github); + + const info = await app.request( + `${ISSUER}/userinfo`, + { headers: { Authorization: `Bearer ${body.access_token}` } }, + env + ); + expect(((await info.json()) as { sub: string }).sub).toBe('4242'); + }); + + it('works when the client sends no PKCE challenge', async () => { + const back = await signIn( + { id: 1, login: memberWithAccount().github! }, + { code_challenge: undefined, code_challenge_method: undefined } + ); + expect((await exchange(codeFrom(back), { code_verifier: '' })).status).toBe(200); + }); + + it('refuses GitHub users without an account in users.ts', async () => { + for (const login of [ + memberWithoutAccount().github!, + ineligibleMember().github!, + 'definitely-not-a-configured-user', + ]) { + const res = await signIn({ id: 7, login }); + expect(res.status, login).toBe(403); + expect(await res.text()).toContain('googleEmailPrefix'); + } + }); + + it('rejects code replay', async () => { + const code = codeFrom(await signIn({ id: 2, login: memberWithAccount().github! })); + expect((await exchange(code)).status).toBe(200); + const replay = await exchange(code); + expect(replay.status).toBe(400); + expect(((await replay.json()) as { error: string }).error).toBe('invalid_grant'); + }); + + it('rejects a wrong PKCE verifier and a wrong client secret', async () => { + const login = memberWithAccount().github!; + const code = codeFrom(await signIn({ id: 3, login })); + expect((await exchange(code, { code_verifier: 'x'.repeat(43) })).status).toBe(400); + + const code2 = codeFrom(await signIn({ id: 3, login })); + const res = await app.request( + `${ISSUER}/token`, + { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code: code2, + redirect_uri: GOOGLE_REDIRECT, + code_verifier: VERIFIER, + client_id: 'google-workspace', + client_secret: 'wrong', + }), + }, + env + ); + expect(res.status).toBe(401); + }); + + it('never redirects to an unregistered redirect_uri', async () => { + const res = await app.request( + `${ISSUER}/authorize?client_id=google-workspace&redirect_uri=${encodeURIComponent('https://evil.example/cb')}&response_type=code&scope=openid`, + {}, + env + ); + expect(res.status).toBe(400); + expect(res.headers.get('Location')).toBeNull(); + }); + + it('does not start a GitHub sign-in unless Google is waiting', async () => { + const res = await app.request(`${ISSUER}/login`, {}, env); + expect(res.status).toBe(302); + expect(res.headers.get('Location')).toBe('/'); + }); +}); + +describe('security hardening', () => { + it('refuses a login that is pinned to a different GitHub account', async () => { + const login = memberWithAccount().github!; + expect((await signIn({ id: 100, login })).status).toBe(302); + // Same login, different GitHub user ID: a freed and re-registered name. + const res = await signIn({ id: 200, login }); + expect(res.status).toBe(403); + expect(await res.text()).toContain('previously used by a different GitHub account'); + // The original owner still signs in. + expect((await signIn({ id: 100, login: login.toUpperCase() })).status).toBe(302); + }); + + it('rate-limits /authorize per IP when the limiter is bound', async () => { + const limit = vi.fn(async () => ({ success: false })); + env = await createEnv({ RATE_LIMITER: { limit } as unknown as RateLimit }); + const res = await app.request( + `${ISSUER}/authorize?client_id=google-workspace&redirect_uri=${encodeURIComponent(GOOGLE_REDIRECT)}&response_type=code&scope=openid`, + { headers: { 'CF-Connecting-IP': '203.0.113.7' } }, + env + ); + expect(res.status).toBe(429); + expect(limit).toHaveBeenCalledWith({ key: '203.0.113.7' }); + }); + + it('decodes form-encoded Basic client credentials (RFC 6749 2.3.1)', async () => { + env = await createEnv({ OIDC_CLIENT_SECRET: 'a b:c+d' }); + const code = codeFrom(await signIn({ id: 5, login: memberWithAccount().github! })); + const encoded = `google-workspace:${encodeURIComponent('a b:c+d').replace(/%20/g, '+')}`; + const res = await app.request( + `${ISSUER}/token`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${btoa(encoded)}`, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: GOOGLE_REDIRECT, + code_verifier: VERIFIER, + }), + }, + env + ); + expect(res.status).toBe(200); + }); + + it('rejects a code_verifier when no challenge was sent (PKCE downgrade)', async () => { + const back = await signIn( + { id: 6, login: memberWithAccount().github! }, + { code_challenge: undefined, code_challenge_method: undefined } + ); + const res = await exchange(codeFrom(back)); + expect(res.status).toBe(400); + expect(((await res.json()) as { error: string }).error).toBe('invalid_grant'); + }); + + it('stamps auth_time at GitHub sign-in, revokes the GitHub token and clears the state cookie', async () => { + const before = Math.floor(Date.now() / 1000); + const back = await signIn({ id: 8, login: memberWithAccount().github! }); + expect(lastGitHub).toHaveBeenCalledWith( + 'https://api.github.com/applications/gh-client/token', + expect.objectContaining({ method: 'DELETE' }) + ); + expect(lastJar.header()).not.toMatch(/(^|; )state=/); + + const body = (await (await exchange(codeFrom(back))).json()) as { id_token: string }; + const jwks = (await (await app.request(`${ISSUER}/jwks`, {}, env)).json()) as { keys: never[] }; + const { payload } = await jwtVerify(body.id_token, createLocalJWKSet(jwks), { + issuer: ISSUER, + audience: 'google-workspace', + }); + expect(payload.auth_time).toBeGreaterThanOrEqual(before); + expect(payload.auth_time).toBeLessThanOrEqual(payload.iat!); + }); +}); + +describe('users.ts invariants the sign-in mapping relies on', () => { + it('has no two members with the same GitHub login or email prefix, ignoring case', () => { + const logins = MEMBERS.flatMap((m) => (m.github ? [m.github.toLowerCase()] : [])); + expect(new Set(logins).size).toBe(logins.length); + const prefixes = MEMBERS.flatMap((m) => + m.googleEmailPrefix ? [m.googleEmailPrefix.toLowerCase()] : [] + ); + expect(new Set(prefixes).size).toBe(prefixes.length); + }); +}); + +describe('rollout pilot', () => { + it('only lets the pilot logins sign in when PILOT_GITHUB_LOGINS is set', async () => { + const withAccount = MEMBERS.filter((m) => m.github && workspaceEmailFor(m.github, DOMAIN)); + const [pilot, other] = withAccount; + env = await createEnv({ PILOT_GITHUB_LOGINS: ` ${pilot!.github!.toUpperCase()} ` }); + + expect((await signIn({ id: 11, login: pilot!.github! })).status).toBe(302); + + const res = await signIn({ id: 12, login: other!.github! }); + expect(res.status).toBe(403); + expect(await res.text()).toContain("isn't enabled"); + }); + + it('maps every pilot login in wrangler.toml to a Workspace account', () => { + const toml = readFileSync(join(import.meta.dirname, '../wrangler.toml'), 'utf8'); + const match = toml.match(/^PILOT_GITHUB_LOGINS\s*=\s*"([^"]*)"/m); + const logins = (match?.[1] ?? '') + .split(',') + .map((l) => l.trim()) + .filter(Boolean); + for (const login of logins) { + expect(workspaceEmailFor(login, DOMAIN), login).not.toBeNull(); + } + }); +}); diff --git a/access-site/tsconfig.json b/access-site/tsconfig.json new file mode 100644 index 00000000..4eaab68e --- /dev/null +++ b/access-site/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types", "node"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src/**/*", "test/**/*", "vitest.config.ts"] +} diff --git a/access-site/vitest.config.ts b/access-site/vitest.config.ts new file mode 100644 index 00000000..14b00667 --- /dev/null +++ b/access-site/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +// Tests run in plain Node (22+), which provides WebCrypto, fetch, Request and +// Response. Cloudflare bindings (KV) are replaced with in-memory fakes. +export default defineConfig({ + test: { + environment: 'node', + include: ['test/**/*.test.ts'], + }, +}); diff --git a/access-site/wrangler.toml b/access-site/wrangler.toml new file mode 100644 index 00000000..f1a288d9 --- /dev/null +++ b/access-site/wrangler.toml @@ -0,0 +1,42 @@ +name = "access-site" +main = "src/index.ts" +compatibility_date = "2026-10-01" +workers_dev = false +# MCP Domain Account (same as cloudflare:accountId in Pulumi.prod.yaml). +account_id = "c39d6379ccfdb20a3627eeddffa80ec9" + +routes = [{ pattern = "access.modelcontextprotocol.io", custom_domain = true }] + +[observability] +enabled = true + +# Per-IP limit on the browser-facing sign-in endpoints (/authorize, /login). +[[ratelimits]] +name = "RATE_LIMITER" +namespace_id = "1001" +simple = { limit = 30, period = 60 } + +# Created on first deploy by scripts/deploy.sh, which fills in the ID. +[[d1_databases]] +binding = "DB" +database_name = "access-site" +database_id = "REPLACE_WITH_D1_DATABASE_ID" +migrations_dir = "migrations" + +[vars] +ISSUER = "https://access.modelcontextprotocol.io" +WORKSPACE_DOMAIN = "modelcontextprotocol.io" +# Client ID of the org-owned GitHub App (not secret). +GITHUB_CLIENT_ID = "REPLACE_WITH_GITHUB_APP_CLIENT_ID" +# Google Workspace is the only OIDC client. The client ID is chosen by us and +# entered in Google's custom OIDC SSO profile; the redirect URI is the one +# Google shows when the profile is created (comma-separate if several). +OIDC_CLIENT_ID = "google-workspace" +OIDC_REDIRECT_URIS = "REPLACE_WITH_GOOGLE_SSO_REDIRECT_URI" +# Rollout pilot: only these GitHub logins can sign in. Remove the line (by PR) +# to open sign-in to everyone with an account in users.ts. +PILOT_GITHUB_LOGINS = "sambhav" + +# Secrets are uploaded by scripts/deploy.sh from GitHub secrets in the +# `production` environment: OIDC_SIGNING_KEY, OIDC_CLIENT_SECRET and +# GITHUB_CLIENT_SECRET. OIDC_PREVIOUS_JWK is optional, during key rotation.