From 23cf973d541648e2611851f9dabe6cb3f9bc241f Mon Sep 17 00:00:00 2001 From: Bryan Fawcett Date: Fri, 31 Jul 2026 13:37:58 +0000 Subject: [PATCH 1/2] docs(deployment): update Agent Gateway page for the Fly.io migration Rewrite in full - the page still described the Vercel phase (domain, build files, port model, deploy commands) after the gateway moved to Fly.io. Covers fundi.nyuchi.com, the single-port fly.toml, the config.yaml chown fix and its durability caveat, dedicated IPs, and a History section. Adds a Fly.io subsection to the deployment overview stub and moves the cross-link there from Vercel. --- .../content/docs/deployment/agentgateway.mdx | 297 +++++++++--------- site/src/content/docs/deployment/overview.mdx | 23 +- 2 files changed, 165 insertions(+), 155 deletions(-) diff --git a/site/src/content/docs/deployment/agentgateway.mdx b/site/src/content/docs/deployment/agentgateway.mdx index 498f689..76ac6dd 100644 --- a/site/src/content/docs/deployment/agentgateway.mdx +++ b/site/src/content/docs/deployment/agentgateway.mdx @@ -1,114 +1,132 @@ --- -title: Agent Gateway on Vercel -description: How nyuchi/agentgateway ships as a Docker container on Vercel using Dockerfile.vercel and container image support. +title: Agent Gateway on Fly.io +description: How nyuchi/agentgateway ships as a Docker container on Fly.io, fronted by WorkOS OIDC, at fundi.nyuchi.com. --- import { Aside } from '@astrojs/starlight/components'; [`nyuchi/agentgateway`](https://github.com/nyuchi/agentgateway) is the agent gateway all Nyuchi agents route through. It runs the -[agentgateway](https://agentgateway.dev) Docker image and ships to Vercel -using Vercel's [container image support](https://vercel.com/docs/functions/container-images) — -a `Dockerfile.vercel` at the repo root deploys straight to a Vercel Function, -no separate container host needed. +[agentgateway](https://agentgateway.dev) Docker image on **Fly.io** +(app `agentgateway`, org `nyuchi-web-services`, region `iad`). -Live at **`mcp.nyuchi.com`**. +Live at **`fundi.nyuchi.com`**. -## Why Vercel, not a standalone Docker host - -Vercel Functions can now run OCI-compatible container images directly: a -`Dockerfile.vercel` (or `Containerfile.vercel`) at the project root is -auto-detected, built, and pushed to the Vercel Container Registry on deploy. -Functions scale to zero and bill on **Active CPU** — only while the container -is actually handling a request — which fits a gateway that mostly idles -between agent calls. `vercel dev` builds and runs the same image locally -(needs the `docker` CLI and daemon). + ## Repo layout | File | Does | | --- | --- | -| `Dockerfile.vercel` | `FROM cr.agentgateway.dev/agentgateway:v1.4.1`, copies in `config.yaml`, runs `agentgateway -f /config.yaml` | -| `config.yaml` | `config.adminAddr: "off"`, the gateway listener, plus one `routes` entry per namespaced MCP server | -| `vercel.json` | Sets `PORT=3000` | +| `Dockerfile.vercel` | `FROM cr.agentgateway.dev/agentgateway:v1.4.1`, copies in `config.yaml` with `--chown=65532:65532` (see below), runs `agentgateway -f /config.yaml`. Named `.vercel` for historical reasons — `fly.toml`'s `[build]` block still points at this exact filename. | +| `config.yaml` | `config.adminAddr: "off"`, the single gateway listener, one `routes` entry per namespaced MCP server, the `ui` OIDC policy | +| `fly.toml` | One `[[services]]` block, one port (`3000` internal → `80`/`443` external) | ```docker filename="Dockerfile.vercel" FROM cr.agentgateway.dev/agentgateway:v1.4.1 -COPY config.yaml /config.yaml +COPY --chown=65532:65532 config.yaml /config.yaml CMD ["-f", "/config.yaml"] ``` -## Port resolution + -A Vercel Function running a container image forwards traffic to a single -port — `80` by default, overridable with the `PORT` environment variable. -agentgateway's own default gateway listener is `3000`, so `vercel.json` pins -`PORT` to `3000` to match: +## One port, on purpose -```json filename="vercel.json" -{ - "env": { - "PORT": "3000" - } -} -``` +`fly.toml` declares exactly one `[[services]]` block, port `3000` internal, +forwarded externally on `80` (redirects to `443`) and `443`: -The dedicated admin port (normally `15000`) is **disabled outright** — -`config.adminAddr: "off"` in `config.yaml`. agentgateway has no built-in -auth on that port (`adminAddr` only controls *where* it binds, never *how* -it authenticates), and its own default, if left unset, is `localhost:15000` -— meaning that server would run inside the container regardless of whether -Vercel ever routes to it. Vercel only forwarding one port isn't access -control; it's incidental. +```toml filename="fly.toml" +[[services]] + internal_port = 3000 + protocol = "tcp" + auto_stop_machines = false + auto_start_machines = true + min_machines_running = 1 -Instead the admin **UI** is exposed on the same gateway listener as -everything else, gated by a real login — see the next section. + [[services.ports]] + port = 80 + handlers = ["http"] + force_https = true -```yaml filename="config.yaml" -config: - adminAddr: "off" + [[services.ports]] + port = 443 + handlers = ["tls", "http"] ``` +An earlier revision added a *second* `[[services]]` block exposing the +admin UI on its own port, `15000`. It was reverted: verified working over +Fly's internal WireGuard network, but the TLS handshake on `15000` reset +from every public-internet path tested. **OIDC is what actually gates the +UI, not the port** — a second exposed port bought no security and cost +real reachability. The base gateway on `443` now serves the proxy, the +namespaced MCP routes, and the OIDC-gated UI together. + + + ## Admin UI, gated by WorkOS OIDC -The `ui` block attaches the admin UI to the `default` gateway (port `3000` -— same one Vercel forwards) and requires a browser OIDC login before -serving anything: +The `ui` block has no explicit `gateways:` key, so it falls back to the +`default` gateway — same port as everything else — and requires a browser +OIDC login before serving anything: ```yaml filename="config.yaml" +config: + adminAddr: "off" + ui: policies: oidc: issuer: ${WORKOS_ISSUER} clientId: ${WORKOS_CLIENT_ID} clientSecret: ${WORKOS_CLIENT_SECRET} - redirectURI: https://mcp.nyuchi.com/oauth/callback + redirectURI: https://fundi.nyuchi.com/oauth/callback ``` +`config.adminAddr` stays `"off"` — agentgateway's dedicated admin port has +no built-in auth at all (`adminAddr` only controls *where* it binds, never +*how* it authenticates). + The `${WORKOS_*}` tokens are **not** placeholders we substitute — agentgateway resolves `$VAR`/`${VAR}` natively from its own process environment at startup, scanning the whole config file. That's confirmed the hard way: an -earlier revision of this setup tried routing secrets through a hand-rolled -`envsubst` entrypoint in a custom `debian:bookworm-slim` image (because the -official image is [Chainguard](https://www.chainguard.dev/) distroless — no -shell), and a stray literal `${WORKOS_*}` left in a *comment* made -agentgateway try to look up an env var literally named `WORKOS_*` and refuse -to boot. Once that surfaced agentgateway's native substitution, the whole -custom-image detour turned out to be unnecessary — the official -`cr.agentgateway.dev/agentgateway:v1.4.1` image handles this fine on its own. +earlier revision tried routing secrets through a hand-rolled `envsubst` +entrypoint in a custom `debian:bookworm-slim` image, and a stray literal +`${WORKOS_*}` left in a *comment* made agentgateway try to look up an env +var literally named `WORKOS_*` and refuse to boot. Once that surfaced +agentgateway's native substitution, the custom-image detour turned out to +be unnecessary. The OIDC identity is a **WorkOS Connect OAuth Application** (first-party, confidential — not Public, since agentgateway exchanges the code server-side), created in the "Nyuchi Identity" WorkOS project's Production -environment. This is the same pattern `mzizi-mcp` and `bushtrade-mcp` -already use for gating internal services against the same identity pool — -not a plain AuthKit Application (single product's own end-user clients) and -not an M2M app (no human user in an OIDC browser-redirect flow). +environment — the same pattern `mzizi-mcp` and `bushtrade-mcp` use for +gating internal services against the same identity pool. -**Four Vercel project env vars are required** (`WORKOS_CLIENT_SECRET` and -`OIDC_COOKIE_SECRET` marked Encrypted), across Production and Preview scopes: +**Four secrets, set with `flyctl secrets set`, not written to `config.yaml` +or committed to git:** | Var | Value | | --- | --- | @@ -122,26 +140,40 @@ not an M2M app (no human user in an OIDC browser-redirect flow). reads it directly from its process environment, unconditionally, the moment any `oidc` policy exists anywhere in the config. Skip it and agentgateway refuses to boot with `OIDC_COOKIE_SECRET is required when - oidc is configured` — confirmed against the agentgateway source - (`crates/agentgateway/src/config.rs`), not just the docs. It must decode - to exactly 32 bytes; anything else is a boot-time hard failure too. + oidc is configured`. It must decode to exactly 32 bytes; anything else is + a boot-time hard failure too. + + + With the OIDC policy active, agentgateway redirects **every unauthenticated request that doesn't match a more specific route** — including `/`, not -just `/ui/*` — to the WorkOS login. That's expected: `/` isn't matched by -any `routes` entry, so it falls under the same gateway the `ui` policy -protects. `/docs/mcp` is unaffected because it's an explicit route matched -first. +just `/ui/*` — to the WorkOS login. `/docs/mcp` is unaffected because it's +an explicit route matched first. + +### Config durability + +Fly machines have an ephemeral root filesystem. The `--chown` fix above +makes the admin UI's live saves to `/config.yaml` *succeed*, but nothing +currently backs that file with a Fly Volume — any edit made through the UI +is lost on the next `flyctl deploy` or machine restart, both of which +revert to whatever's baked into the image. Durable UI-driven config editing +needs a volume-backed config path; not done yet. ## Namespaces: one gateway, many MCP servers -This is the actual point of running an API gateway in front of the Nyuchi -agents: one domain, one path prefix per backend MCP server, instead of a -different worker hostname for each one. agentgateway's `mcp` backend type is -protocol-aware — it terminates the MCP session itself and re-issues calls to -the target, rather than doing a raw reverse proxy — so `routes` in -`config.yaml` just maps a `pathPrefix` to a target `host`: +The actual point of running an API gateway in front of the Nyuchi agents: +one domain, one path prefix per backend MCP server, instead of a different +worker hostname for each one. agentgateway's `mcp` backend type is +protocol-aware — it terminates the MCP session itself, rather than doing a +raw reverse proxy: ```yaml filename="config.yaml" routes: @@ -158,99 +190,68 @@ routes: ``` That puts [`nyuchi-docs-mcp`](/integrations/docs-mcp/) at -**`mcp.nyuchi.com/docs/mcp`**, still reachable directly at +**`fundi.nyuchi.com/docs/mcp`**, still reachable directly at `docs.nyuchi.com/mcp` and `nyuchi-docs-mcp.nyuchi.workers.dev/mcp` — the gateway is an additional front door, not a replacement: ```sh -curl -X POST https://mcp.nyuchi.com/docs/mcp \ +curl -X POST https://fundi.nyuchi.com/docs/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}' ``` Every request past `initialize` needs the `Mcp-Session-Id` the gateway hands -back — that's agentgateway enforcing normal MCP session semantics, not a -routing bug. Add another Nyuchi agent's MCP server the same way: a new -`routes` entry, a new namespace. - - +back. Add another Nyuchi agent's MCP server the same way: a new `routes` +entry, a new namespace, `flyctl deploy`. ## Deploying -The `nyuchi/agentgateway` Vercel project (team `nyuchi`) is **Git-connected** -— production branch `main` — so a push to `main` auto-builds -`Dockerfile.vercel` and deploys; other branches get preview deployments. - -The project was created without the dashboard, in three API/CLI calls (no -manual clicking): - -```sh -vercel link --scope nyuchi --project agentgateway --yes # create + link the project -vercel deploy --prod --scope nyuchi # first deploy, before Git was wired up -vercel git connect https://github.com/nyuchi/agentgateway --scope nyuchi -``` - -`vercel git connect` is the CLI wrapper around the (undocumented, but -stable — it's what the CLI itself calls) linking endpoint: - -```sh -curl -X POST "https://api.vercel.com/v9/projects/agentgateway/link?teamId=" \ - -H "Authorization: Bearer $VERCEL_TOKEN" \ - -d '{"type":"github","repo":"nyuchi/agentgateway"}' -``` - -It only works because the Vercel GitHub App is already installed for the -`nyuchi` org (shared with every other Vercel-hosted Nyuchi project) — there's -no API call that installs the GitHub App itself, that's a one-time dashboard -step done long before this project existed. - - - -Vercel rebuilds and re-pushes the image to the Vercel Container Registry on -every deploy that touches `Dockerfile.vercel` or the files it `COPY`s. - -### Domain - -`mcp.nyuchi.com` is a `CNAME` to `cname.vercel-dns.com` in the `nyuchi.com` -Cloudflare zone (DNS-only, not proxied — Vercel terminates TLS and issues its -own certificate for the domain). Added to the project with: +There's no CI/CD wired up yet — every deploy so far has been manual from a +checkout: ```sh -curl -X POST "https://api.vercel.com/v10/projects/agentgateway/domains?teamId=" \ - -H "Authorization: Bearer $VERCEL_TOKEN" \ - -d '{"name":"mcp.nyuchi.com"}' +flyctl deploy -a agentgateway --local-only ``` -## Limits worth knowing - -- **Secure Compute and Static IPs don't work with custom container images - yet.** Don't route anything through this gateway that assumes a fixed - egress IP. -- **Scale-to-zero**: an instance with no traffic for 5 minutes in production - (30 seconds in preview) is torn down. A scaled-down instance gets - `SIGTERM` with a 30-second grace period before a hard kill. -- **One process, one port.** agentgateway's own multi-port model (proxy + - MCP + admin) collapses to whatever `config.yaml` binds to the single port - Vercel forwards — there's no way to expose a second port from the same - Function. +`--local-only` builds with the local Docker daemon instead of Fly's remote +(Depot) builder. That matters in some sandboxed/proxied environments where +the remote builder's gRPC connection can't be tunneled through an HTTP +proxy — it isn't required from a normal machine with unrestricted network +access, but it's the flag that got a build through when the remote builder +timed out with a TLS handshake failure. + +### Networking + +- **Domain**: `fundi.nyuchi.com` — Cloudflare DNS (`nyuchi.com` zone), + `A`/`AAAA` records to the app's dedicated IPs, plus `_fly-ownership` and + `_acme-challenge` records, following the same pattern already used for + `api.nyuchi.com` and `auth.nyuchi.com`. +- **Dedicated IPs**: IPv4 (**$2/mo**) and IPv6 — `flyctl ips list -a agentgateway`. + Fly requires a dedicated IP once an app runs more than one `[[services]]` + block; kept even after collapsing back to one, since it's already + allocated and paid for. +- **TLS**: Let's Encrypt, issued and managed by Fly + (`flyctl certs list/check/add -a agentgateway`). + +## History + +Shipped on **Vercel** first — `Dockerfile.vercel` running as a Vercel +Function via [container image support](https://vercel.com/docs/functions/container-images), +admin UI folded into the single port Vercel forwards, domain +`mcp.nyuchi.com`. Migrated to **Fly.io** for full multi-service control. +The Vercel project, its domain attachment, and all its deployments were +deleted after the migration; `mcp.nyuchi.com` was retired in favor of +`fundi.nyuchi.com`. Two detours along the way, both reverted: a standalone +`debian:bookworm-slim` + `envsubst` entrypoint (unnecessary — agentgateway +substitutes env vars into its own config natively), and a dedicated +admin-UI port on `15000` (unreachable from the public internet from every +network tested). ## Cross-links - [`nyuchi/agentgateway`](https://github.com/nyuchi/agentgateway) — the repo. - [agentgateway.dev docs](https://agentgateway.dev/docs/standalone/latest/) — upstream configuration reference (gateways, listeners, backends). -- [Vercel: Container Images](https://vercel.com/docs/functions/container-images) — - the underlying Vercel feature. +- [Fly.io: Container Images / Machines](https://fly.io/docs/machines/) — + the underlying Fly.io deployment model. diff --git a/site/src/content/docs/deployment/overview.mdx b/site/src/content/docs/deployment/overview.mdx index e9cb777..8c1d7cb 100644 --- a/site/src/content/docs/deployment/overview.mdx +++ b/site/src/content/docs/deployment/overview.mdx @@ -9,11 +9,13 @@ import { Aside } from '@astrojs/starlight/components'; This section is a stub. Treat it as a TODO list, not a reference. -Nyuchi runs production workloads on three substrates: **Cloudflare** (edge, +Nyuchi runs production workloads on four substrates: **Cloudflare** (edge, Workers, R2, D1, KV, Hyperdrive), **Vercel** (Next.js apps and the public -marketing surface), and **Supabase** (Postgres, Auth-adjacent storage, edge -functions where Cloudflare isn't the right fit). This section captures the -patterns we've already paid for so a new project doesn't reinvent them. +marketing surface), **Supabase** (Postgres, Auth-adjacent storage, edge +functions where Cloudflare isn't the right fit), and **Fly.io** (long-running +Docker containers that need real multi-service control, not a single +serverless Function). This section captures the patterns we've already paid +for so a new project doesn't reinvent them. ## What needs to land here @@ -32,9 +34,16 @@ patterns we've already paid for so a new project doesn't reinvent them. - The Next.js version we standardise on and the App Router conventions. - Image / asset handling, edge runtime choices. - How we wire `docs.nyuchi.com` and `platform.nyuchi.com` once DNS is set up. -- Docker container images on Vercel Functions — see - [Agent Gateway on Vercel](/deployment/agentgateway/) for the pattern we - use to ship `nyuchi/agentgateway` as a `Dockerfile.vercel` image. + +### Fly.io + +- Multi-service Docker apps that outgrow a single serverless Function port + — see [Agent Gateway on Fly.io](/deployment/agentgateway/) for + `nyuchi/agentgateway`: dedicated IPs, Fly-managed Let's Encrypt certs, + and why it moved off Vercel's container image support (one port is fine + until you need a second one that also has to be reachable). +- Fly Volumes for anything that needs to survive a redeploy — the ephemeral + root filesystem gotcha is called out on that page; not yet solved there. ### Supabase From 552dd4b27283f0be12d8984a4ee19b58ab1f8c21 Mon Sep 17 00:00:00 2001 From: Bryan Fawcett Date: Fri, 31 Jul 2026 14:00:44 +0000 Subject: [PATCH 2/2] feat(site,mcp): gate internal docs behind WorkOS OIDC / bearer auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marks deployment/* and mzizi-tools/*, tools/* as `visibility: internal` in frontmatter (site/src/content.config.ts extends docsSchema with the field). scripts/generate-internal-paths.mjs scans that frontmatter at build time and emits the manifest both surfaces below read. - site: adds a `main` worker (src/worker/gate.ts) in front of the Workers Static Assets binding — internal pages redirect to a WorkOS OIDC login (PKCE, session cookie is the WorkOS ID token itself, re-verified on each request via JWKS) and fall through to assets on success; public pages are untouched. - nyuchi-docs-mcp-worker: verifies each MCP caller's own bearer token (src/auth.ts) before deciding whether to surface internal content. read_page denies internal paths outright for unauthenticated callers, and forwards a shared INTERNAL_FETCH_KEY on the internal fetch for authorized ones so that read skips the browser OIDC flow. search_docs/ask_docs filter internal URLs out of citations for unauthenticated callers as a defensive backstop — the AI Search index is built from a public crawl, so it shouldn't contain internal content in the first place. Known limitation, not solved here: if the AI Search crawler is ever allowlisted past the site gate, search_docs/ask_docs would need the crawler's ingestion to respect visibility too, not just this filter. Secrets needed post-merge (not committed): WORKOS_CLIENT_ID/SECRET + WORKOS_ISSUER for a new "Nyuchi Docs" WorkOS Connect app (redirect URI https://docs.nyuchi.com/oauth/callback) on the site worker, and a shared INTERNAL_FETCH_KEY on both workers. --- nyuchi-docs-mcp-worker/package.json | 4 +- nyuchi-docs-mcp-worker/src/auth.ts | 67 ++ nyuchi-docs-mcp-worker/src/mcp.ts | 93 ++- nyuchi-docs-mcp-worker/src/worker.ts | 8 +- nyuchi-docs-mcp-worker/wrangler.toml | 14 + pnpm-lock.yaml | 682 +++--------------- site/package.json | 5 +- site/public/internal-paths.json | 11 + site/scripts/generate-internal-paths.mjs | 73 ++ site/src/content.config.ts | 19 +- .../content/docs/deployment/agentgateway.mdx | 1 + site/src/content/docs/deployment/overview.mdx | 1 + site/src/content/docs/mzizi-tools/a2a.mdx | 1 + .../content/docs/mzizi-tools/dna-helix.mdx | 1 + .../src/content/docs/mzizi-tools/overview.mdx | 1 + .../docs/mzizi-tools/registry-health.mdx | 1 + site/src/content/docs/tools/index.mdx | 1 + site/src/worker/gate.ts | 193 +++++ site/src/worker/internal-paths.generated.ts | 12 + site/wrangler.toml | 21 +- 20 files changed, 621 insertions(+), 588 deletions(-) create mode 100644 nyuchi-docs-mcp-worker/src/auth.ts create mode 100644 site/public/internal-paths.json create mode 100644 site/scripts/generate-internal-paths.mjs create mode 100644 site/src/worker/gate.ts create mode 100644 site/src/worker/internal-paths.generated.ts diff --git a/nyuchi-docs-mcp-worker/package.json b/nyuchi-docs-mcp-worker/package.json index dabeb0d..87d44a3 100644 --- a/nyuchi-docs-mcp-worker/package.json +++ b/nyuchi-docs-mcp-worker/package.json @@ -10,7 +10,9 @@ "deploy": "wrangler deploy", "test": "vitest run" }, - "dependencies": {}, + "dependencies": { + "jose": "^5.9.6" + }, "devDependencies": { "@cloudflare/workers-types": "^5.20260728.1", "typescript": "^6.0.3", diff --git a/nyuchi-docs-mcp-worker/src/auth.ts b/nyuchi-docs-mcp-worker/src/auth.ts new file mode 100644 index 0000000..8e1b276 --- /dev/null +++ b/nyuchi-docs-mcp-worker/src/auth.ts @@ -0,0 +1,67 @@ +// Verifies a caller's own WorkOS-issued bearer token (Authorization: +// Bearer on the incoming MCP JSON-RPC request) so this worker +// knows *who is asking* before deciding whether to surface +// `visibility: internal` content (see nyuchi-docs's +// site/src/content.config.ts and scripts/generate-internal-paths.mjs). +// +// Deliberately checks only signature + issuer + expiry, not audience — +// any token from the Nyuchi Identity WorkOS project proves identity +// regardless of which app requested it, which is what "is this a real +// Nyuchi person" needs here. Contrast with agentgateway's OIDC policy, +// which *does* pin a specific client_id because it's terminating a +// browser login for one particular app, not accepting bearer tokens +// minted for arbitrary Nyuchi-internal clients. + +import { createRemoteJWKSet, jwtVerify } from 'jose'; + +let jwks: ReturnType | undefined; +let jwksIssuer: string | undefined; + +export async function verifyBearerAuth( + req: Request, + issuer: string | undefined +): Promise<{ authorized: boolean; subject?: string }> { + if (!issuer) return { authorized: false }; + const header = req.headers.get('authorization') ?? ''; + const match = header.match(/^Bearer\s+(.+)$/i); + if (!match) return { authorized: false }; + + try { + if (!jwks || jwksIssuer !== issuer) { + jwks = createRemoteJWKSet(new URL(`${issuer}/oauth2/jwks`)); + jwksIssuer = issuer; + } + const { payload } = await jwtVerify(match[1], jwks, { issuer }); + return { authorized: true, subject: typeof payload.sub === 'string' ? payload.sub : undefined }; + } catch { + return { authorized: false }; + } +} + +// The manifest is generated at nyuchi-docs build time (see +// scripts/generate-internal-paths.mjs) and served statically at +// /internal-paths.json — this worker is a separate deploy from that +// site, so it reads the list over HTTP rather than importing it, with a +// short in-isolate cache since it rarely changes. +let cachedPaths: readonly string[] | undefined; +let cachedAt = 0; +const CACHE_TTL_MS = 5 * 60 * 1000; + +export async function getInternalPaths(docsOrigin: string): Promise { + if (cachedPaths && Date.now() - cachedAt < CACHE_TTL_MS) return cachedPaths; + try { + const res = await fetch(`${docsOrigin}/internal-paths.json`); + if (!res.ok) return cachedPaths ?? []; + const data = (await res.json()) as { internalPaths?: string[] }; + cachedPaths = data.internalPaths ?? []; + cachedAt = Date.now(); + return cachedPaths; + } catch { + return cachedPaths ?? []; + } +} + +export function isInternalPath(paths: readonly string[], pathname: string): boolean { + const normalised = pathname.endsWith('/') ? pathname : `${pathname}/`; + return paths.some((p) => normalised === p || normalised.startsWith(p)); +} diff --git a/nyuchi-docs-mcp-worker/src/mcp.ts b/nyuchi-docs-mcp-worker/src/mcp.ts index fec14e2..d952d9b 100644 --- a/nyuchi-docs-mcp-worker/src/mcp.ts +++ b/nyuchi-docs-mcp-worker/src/mcp.ts @@ -12,6 +12,12 @@ import { type ChatMessage, type Env, } from './worker.js'; +import { getInternalPaths, isInternalPath } from './auth.js'; + +export interface CallerAuth { + authorized: boolean; + subject?: string; +} // MCP spec revisions this server speaks, newest first. Initialize // negotiates: a supported requested version is echoed back; anything @@ -147,11 +153,36 @@ async function callSearch(env: Env, query: string, topK: number) { return normaliseCitations(res.chunks ?? []); } -async function toolSearchDocs(env: Env, params: Record): Promise { +// Best-effort: AI Search's index is built by crawling the public site, and +// the crawler gets the same OIDC gate any other unauthenticated visitor +// does, so an internal page landing in this index at all would already be +// a bug elsewhere. This filter is the belt-and-suspenders backstop, not +// the actual access-control boundary — don't rely on it alone. +async function filterCitations( + citations: T[], + auth: CallerAuth +): Promise { + if (auth.authorized) return citations; + const internalPaths = await getInternalPaths(DOCS_ORIGIN); + if (internalPaths.length === 0) return citations; + return citations.filter((c) => { + try { + return !isInternalPath(internalPaths, new URL(c.url, DOCS_ORIGIN).pathname); + } catch { + return true; + } + }); +} + +async function toolSearchDocs( + env: Env, + params: Record, + auth: CallerAuth +): Promise { const query = str(params, 'query'); if (!query) return textResult('search_docs: query is required', true); const topK = Math.min(Math.max(Number(params.top_k) || Number(env.TOP_K ?? '5'), 1), 10); - const hits = await callSearch(env, query, topK); + const hits = await filterCitations(await callSearch(env, query, topK), auth); if (hits.length === 0) return textResult(`No documentation matches for "${query}".`); const lines = hits.map( (h) => `${h.index}. ${h.title}\n ${h.url}${h.snippet ? `\n ${h.snippet}` : ''}` @@ -159,7 +190,11 @@ async function toolSearchDocs(env: Env, params: Record): Promis return textResult(lines.join('\n\n')); } -async function toolAskDocs(env: Env, params: Record): Promise { +async function toolAskDocs( + env: Env, + params: Record, + auth: CallerAuth +): Promise { const question = str(params, 'question'); if (!question) return textResult('ask_docs: question is required', true); const messages: ChatMessage[] = [{ role: 'user', content: question }]; @@ -173,7 +208,7 @@ async function toolAskDocs(env: Env, params: Record): Promise `[${c.index}] ${c.title} — ${c.url}`) .join('\n'); if (!answer) return textResult('The docs assistant returned no answer for that question.', true); @@ -214,14 +249,30 @@ function htmlToText(html: string): string { .trim(); } -async function toolReadPage(params: Record): Promise { +async function toolReadPage( + env: Env, + params: Record, + auth: CallerAuth +): Promise { const raw = str(params, 'path'); if (!raw) return textResult('read_page: path is required', true); const url = resolveDocsUrl(raw); if (!url) return textResult(`read_page: only ${DOCS_ORIGIN} pages can be read`, true); - const res = await fetch(url.toString(), { - headers: { 'user-agent': 'nyuchi-docs-mcp/1.0' }, - }); + + const internalPaths = await getInternalPaths(DOCS_ORIGIN); + const internal = isInternalPath(internalPaths, url.pathname); + if (internal && !auth.authorized) { + return textResult( + `read_page: ${url.pathname} is internal-only. Provide a valid Authorization: Bearer to read it.`, + true + ); + } + + const headers: Record = { 'user-agent': 'nyuchi-docs-mcp/1.0' }; + if (internal && auth.authorized && env.INTERNAL_FETCH_KEY) { + headers['x-internal-fetch-key'] = env.INTERNAL_FETCH_KEY; + } + const res = await fetch(url.toString(), { headers }); if (!res.ok) return textResult(`read_page: ${url.pathname} responded ${res.status}`, true); const text = htmlToText(await res.text()); const clipped = @@ -290,14 +341,19 @@ async function toolRaiseIssue(env: Env, params: Record): Promis return textResult(`Issue queued for the docs team (ref ${key}).`); } -async function callTool(env: Env, name: string, args: Record): Promise { +async function callTool( + env: Env, + name: string, + args: Record, + auth: CallerAuth +): Promise { switch (name) { case 'search_docs': - return toolSearchDocs(env, args); + return toolSearchDocs(env, args, auth); case 'ask_docs': - return toolAskDocs(env, args); + return toolAskDocs(env, args, auth); case 'read_page': - return toolReadPage(args); + return toolReadPage(env, args, auth); case 'submit_feedback': return toolSubmitFeedback(env, args); case 'raise_issue': @@ -307,7 +363,11 @@ async function callTool(env: Env, name: string, args: Record): } } -async function handleMessage(env: Env, msg: JsonRpcRequest): Promise { +async function handleMessage( + env: Env, + msg: JsonRpcRequest, + auth: CallerAuth +): Promise { const { id, method, params = {} } = msg; // Notifications (no id) get no response body. @@ -333,7 +393,7 @@ async function handleMessage(env: Env, msg: JsonRpcRequest): Promise; try { - return rpcResult(id, await callTool(env, name, args)); + return rpcResult(id, await callTool(env, name, args, auth)); } catch (err) { const msg = err instanceof Error ? err.message : 'tool execution failed'; return rpcResult(id, textResult(`${name}: ${msg}`, true)); @@ -347,7 +407,8 @@ async function handleMessage(env: Env, msg: JsonRpcRequest): Promise + cors: Record, + auth: CallerAuth ): Promise { const jsonHeaders = { 'content-type': 'application/json', ...cors }; @@ -369,7 +430,7 @@ export async function handleMcp( } const messages = Array.isArray(parsed) ? (parsed as JsonRpcRequest[]) : [parsed as JsonRpcRequest]; - const responses = (await Promise.all(messages.map((m) => handleMessage(env, m)))).filter( + const responses = (await Promise.all(messages.map((m) => handleMessage(env, m, auth)))).filter( (r): r is Record => r !== null ); diff --git a/nyuchi-docs-mcp-worker/src/worker.ts b/nyuchi-docs-mcp-worker/src/worker.ts index 02c1f6f..aa5af22 100644 --- a/nyuchi-docs-mcp-worker/src/worker.ts +++ b/nyuchi-docs-mcp-worker/src/worker.ts @@ -14,6 +14,7 @@ // file real GitHub issues on nyuchi/nyuchi-docs). import { handleMcp } from './mcp.js'; +import { verifyBearerAuth } from './auth.js'; export interface ChatMessage { role: 'user' | 'assistant' | 'system'; @@ -68,6 +69,10 @@ export interface Env { FEEDBACK?: FeedbackStore; /** Optional secret — when set, raise_issue files real GitHub issues. */ GITHUB_TOKEN?: string; + /** WorkOS issuer used to verify a caller's own bearer token (see src/auth.ts). Unset = every caller is treated as unauthenticated (public-only). */ + WORKOS_ISSUER?: string; + /** Shared with nyuchi-docs's site worker — sent on internal-page fetches once a caller is verified, so the read skips the browser OIDC flow. */ + INTERNAL_FETCH_KEY?: string; } const WILDCARD_PATTERNS = [/^https:\/\/[a-z0-9-]+\.vercel\.app$/i]; @@ -139,7 +144,8 @@ export default { } if (url.pathname === '/mcp' || url.pathname === '/mcp/') { - return handleMcp(req, env, cors); + const auth = await verifyBearerAuth(req, env.WORKOS_ISSUER); + return handleMcp(req, env, cors, auth); } return new Response('Not found', { status: 404, headers: cors }); diff --git a/nyuchi-docs-mcp-worker/wrangler.toml b/nyuchi-docs-mcp-worker/wrangler.toml index 5814623..dfe9f45 100644 --- a/nyuchi-docs-mcp-worker/wrangler.toml +++ b/nyuchi-docs-mcp-worker/wrangler.toml @@ -15,6 +15,12 @@ routes = [ [vars] TOP_K = "5" ALLOWED_ORIGINS = "https://docs.nyuchi.com,https://docs.bundu.org" +# The "Nyuchi Docs" WorkOS Connect OAuth Application's issuer — used only +# to verify a caller's own bearer token (src/auth.ts), never to originate +# a login flow itself. Not a secret: an OIDC issuer URL is public by +# design (it's how discovery/JWKS work), but keep it in sync with the +# same value the site worker uses. +WORKOS_ISSUER = "https://identity.nyuchi.com" # Same AI Search instance the Ask-AI tab uses — read tools ride the # existing nyuchi-docs corpus; this worker adds no ingestion of its own. @@ -31,3 +37,11 @@ enabled = true [[kv_namespaces]] binding = "FEEDBACK" id = "9c5af0b28d5c4706840c492b2e3f47e8" + +# Secret (wrangler secret put INTERNAL_FETCH_KEY): shared with +# nyuchi-docs's site worker. Once this worker has verified a caller's own +# bearer token (WORKOS_ISSUER above), it sends this key on the internal +# fetch for read_page so that already-authorized read skips the site's +# browser OIDC flow. Any long random string; must match on both workers. +# Unset on either side = internal reads degrade to "always denied" rather +# than an open bypass. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ddef39..843f002 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,28 +29,32 @@ importers: devDependencies: vitest: specifier: '>=3.2.6' - version: 4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) + version: 4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) nyuchi-docs-mcp-worker: + dependencies: + jose: + specifier: ^5.9.6 + version: 5.10.0 devDependencies: '@cloudflare/workers-types': specifier: ^5.20260728.1 - version: 5.20260728.1 + version: 5.20260731.1 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: '>=3.2.6' - version: 4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) + version: 4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) wrangler: specifier: ^4.114.0 - version: 4.114.0(@cloudflare/workers-types@5.20260728.1)(@types/node@24.12.4) + version: 4.114.0(@cloudflare/workers-types@5.20260731.1)(@types/node@24.12.4) nyuchi-docs-search: dependencies: '@astrojs/starlight': specifier: ^0.39.0 || ^0.41.0 - version: 0.41.4(@astrojs/markdown-remark@7.2.1)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(typescript@6.0.3) + version: 0.41.4(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(typescript@6.0.3) devDependencies: '@sveltejs/kit': specifier: ^2.70.1 @@ -98,25 +102,25 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: ^5.20260728.1 - version: 5.20260728.1 + version: 5.20260731.1 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: '>=3.2.6' - version: 4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) + version: 4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) wrangler: specifier: ^4.114.0 - version: 4.114.0(@cloudflare/workers-types@5.20260728.1)(@types/node@24.12.4) + version: 4.114.0(@cloudflare/workers-types@5.20260731.1)(@types/node@24.12.4) site: dependencies: '@astrojs/starlight': specifier: ^0.41.4 - version: 0.41.4(@astrojs/markdown-remark@7.2.1)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(typescript@6.0.3) + version: 0.41.4(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(typescript@6.0.3) '@astrojs/svelte': specifier: ^9.0.1 - version: 9.0.1(@types/node@24.12.4)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(esbuild@0.28.1)(svelte@5.56.8)(tsx@4.22.3)(typescript@6.0.3) + version: 9.0.1(@types/node@24.12.4)(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(esbuild@0.28.1)(svelte@5.56.8)(tsx@4.22.3)(typescript@6.0.3) '@bundu/ui': specifier: ^0.1.1 version: 0.1.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -125,7 +129,10 @@ importers: version: link:../nyuchi-docs-search astro: specifier: ^7.1.4 - version: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) + version: 7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) + jose: + specifier: ^5.9.6 + version: 5.10.0 sharp: specifier: ^0.35.0 version: 0.35.3(@types/node@24.12.4) @@ -133,9 +140,12 @@ importers: specifier: ^5.56.8 version: 5.56.8 devDependencies: + '@cloudflare/workers-types': + specifier: ^5.20260726.1 + version: 5.20260726.1 wrangler: specifier: ^4.114.0 - version: 4.114.0(@cloudflare/workers-types@5.20260728.1)(@types/node@24.12.4) + version: 4.114.0(@cloudflare/workers-types@5.20260726.1)(@types/node@24.12.4) packages: @@ -225,12 +235,18 @@ packages: '@astrojs/internal-helpers@0.10.1': resolution: {integrity: sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q==} + '@astrojs/internal-helpers@0.10.2': + resolution: {integrity: sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ==} + '@astrojs/markdown-remark@7.2.1': resolution: {integrity: sha512-jPVNIqTvk+yKviikszv/Y1U4jGUSKpp/Nw48QZV4qjWgp70j4Lkq3lhSDRbWwCfgKvEyO9GHuVbV1dM2WYXy1w==} '@astrojs/markdown-satteri@0.3.4': resolution: {integrity: sha512-6Lvt/bQZEBW+zzdhPblvfZEy5PGEYJaUsUqaCgwHeRPxZJL1gc9I+DRLKWJjjYTWDzVUTzXlMq4WwSK+X34CVw==} + '@astrojs/markdown-satteri@0.3.5': + resolution: {integrity: sha512-CvWVEFAbay7YO+i9SaqDJubipA5ckiVB89QWoMJ5XC0m5CtFg8JwZ7Kau6X9sYY7FZURH0w2l03ISH2jOS/RDQ==} + '@astrojs/mdx@7.0.3': resolution: {integrity: sha512-RxyIwU0uFam5ftwqKOjpIdhnFxZ/kEikeimLyQy3eGXbHT8WgRGzzesOIHVU8+m9TY8ag5WVOyvV24/GyqPdPQ==} engines: {node: '>=22.12.0'} @@ -408,8 +424,11 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@5.20260728.1': - resolution: {integrity: sha512-rZqmesLEH40Xt67PpQSj+iJqwkYBzVr7U/UOa5twkToMsxO5pYsPw2QAaTKDdiCSiki9fWAgsWTj9OmFnbwLRQ==} + '@cloudflare/workers-types@5.20260726.1': + resolution: {integrity: sha512-fKgRSm3sDmOdak1LGWehS4vSPSj7/zeu0NfmE62VPjBMqWgODcOGljYvq6A75sL+7YfY3iGFGb0jVEDYq+hlmw==} + + '@cloudflare/workers-types@5.20260731.1': + resolution: {integrity: sha512-ly+eua642FCR2nbQGi/OxqlVaJI3fNdyqK7G4u6jWCAZ4WYhOBvZWyf0M7s5zurWbNQW3YjelOxuKqR0zYK90w==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -458,21 +477,12 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/core@2.0.0-alpha.3': - resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} - '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/runtime@2.0.0-alpha.3': - resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} - '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@emnapi/wasi-threads@2.0.1': - resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -840,8 +850,8 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@napi-rs/wasm-runtime@1.2.1': - resolution: {integrity: sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==} + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 @@ -853,9 +863,6 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - '@oxc-project/types@0.142.0': - resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} - '@pagefind/darwin-arm64@1.5.2': resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} cpu: [arm64] @@ -916,60 +923,30 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.2.1': - resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.2.1': - resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.1': - resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.2.1': - resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.2.1': - resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -977,13 +954,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.2.1': - resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -991,13 +961,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.2.1': - resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1005,13 +968,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.2.1': - resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1019,13 +975,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.1': - resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1033,13 +982,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.1': - resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1047,58 +989,29 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.2.1': - resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.2.1': - resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.2.1': - resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - '@rolldown/binding-win32-arm64-msvc@1.1.5': resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.2.1': - resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.1': - resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} @@ -1525,12 +1438,12 @@ packages: peerDependencies: astro: '>=6.4.6 <7' - astro@7.1.4: - resolution: {integrity: sha512-e0gkBReJECAZuuTgpEB5JMUc6J4mM6boD6wuVE1pBf/fMywG47f8qm9XQoA6kZB1RWHiHmUlLuQ2jd9Q5+72HQ==} + astro@7.1.6: + resolution: {integrity: sha512-83x9rYbHazMaZkYrAFRVZXSQx2moFkz0F7cjTDUF3GWfS0a3p2vZXG1ZdhV86rStHApQCodBJW+XTD37xISIrQ==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: - '@astrojs/markdown-remark': 7.2.1 + '@astrojs/markdown-remark': 7.2.2 peerDependenciesMeta: '@astrojs/markdown-remark': optional: true @@ -1702,9 +1615,6 @@ packages: devalue@5.8.2: resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} - devalue@5.9.0: - resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1988,6 +1898,9 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2322,8 +2235,8 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-mock-http@1.0.5: - resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -2420,10 +2333,6 @@ packages: resolution: {integrity: sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} - engines: {node: ^10 || ^12 || >=14} - pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -2565,11 +2474,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.2.1: - resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rollup@4.60.4: resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2582,8 +2486,8 @@ packages: satteri@0.9.5: resolution: {integrity: sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w==} - sax@1.6.1: - resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} saxes@6.0.0: @@ -2632,8 +2536,8 @@ packages: engines: {node: '>=20.19.5', npm: '>=10.8.2'} hasBin: true - smol-toml@1.7.1: - resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -2950,49 +2854,6 @@ packages: yaml: optional: true - vite@8.2.0: - resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.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 - vitefu@1.1.3: resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: @@ -3170,15 +3031,7 @@ snapshots: '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - - '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': - dependencies: - '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -3205,21 +3058,6 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-binding@0.3.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': - optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.3.2 - '@astrojs/compiler-binding-darwin-x64': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-musl': 0.3.2 - '@astrojs/compiler-binding-linux-x64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-x64-musl': 0.3.2 - '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) - '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.2 - '@astrojs/compiler-binding-win32-x64-msvc': 0.3.2 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) @@ -3227,14 +3065,18 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-rs@0.3.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@astrojs/internal-helpers@0.10.1': dependencies: - '@astrojs/compiler-binding': 0.3.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + js-yaml: 4.3.0 + picomatch: 4.0.5 + retext-smartypants: 6.2.0 + shiki: 4.3.1 + smol-toml: 1.7.0 + unified: 11.0.5 - '@astrojs/internal-helpers@0.10.1': + '@astrojs/internal-helpers@0.10.2': dependencies: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 @@ -3242,7 +3084,7 @@ snapshots: picomatch: 4.0.5 retext-smartypants: 6.2.0 shiki: 4.3.1 - smol-toml: 1.7.1 + smol-toml: 1.7.0 unified: 11.0.5 '@astrojs/markdown-remark@7.2.1': @@ -3275,35 +3117,21 @@ snapshots: hast-util-from-html: 2.0.3 satteri: 0.9.5 - '@astrojs/mdx@7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))': + '@astrojs/markdown-satteri@0.3.5': dependencies: - '@astrojs/internal-helpers': 0.10.1 - '@astrojs/markdown-remark': 7.2.1 - '@mdx-js/mdx': 3.1.1 - acorn: 8.17.0 - astro: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) - es-module-lexer: 2.3.1 - estree-util-visit: 2.0.0 - hast-util-to-html: 9.0.5 - piccolore: 0.1.3 - rehype-raw: 7.0.0 - remark-gfm: 4.0.1 - remark-smartypants: 3.0.2 - source-map: 0.7.6 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - optionalDependencies: - '@astrojs/markdown-satteri': 0.3.4 - transitivePeerDependencies: - - supports-color + '@astrojs/internal-helpers': 0.10.2 + '@astrojs/prism': 4.0.2 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + satteri: 0.9.5 - '@astrojs/mdx@7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))': + '@astrojs/mdx@7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))': dependencies: '@astrojs/internal-helpers': 0.10.1 '@astrojs/markdown-remark': 7.2.1 '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 - astro: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) + astro: 7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) es-module-lexer: 2.3.1 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -3329,55 +3157,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.4(@astrojs/markdown-remark@7.2.1)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(typescript@6.0.3)': - dependencies: - '@astrojs/markdown-satteri': 0.3.4 - '@astrojs/mdx': 7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)) - '@astrojs/sitemap': 3.7.2 - '@pagefind/default-ui': 1.5.2 - '@types/hast': 3.0.5 - '@types/js-yaml': 4.0.9 - '@types/mdast': 4.0.4 - astro: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) - astro-expressive-code: 0.44.1(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)) - bcp-47: 2.1.0 - hast-util-from-html: 2.0.3 - hast-util-select: 6.0.4 - hast-util-to-string: 3.0.1 - hastscript: 9.0.1 - i18next: 26.2.0(typescript@6.0.3) - js-yaml: 4.3.0 - klona: 2.0.6 - magic-string: 0.30.21 - mdast-util-directive: 3.1.0 - mdast-util-to-markdown: 2.1.2 - mdast-util-to-string: 4.0.0 - pagefind: 1.5.2 - rehype: 13.0.2 - rehype-format: 5.0.1 - remark-directive: 4.0.0 - satteri: 0.9.5 - ultrahtml: 1.7.0 - unified: 11.0.5 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - optionalDependencies: - '@astrojs/markdown-remark': 7.2.1 - transitivePeerDependencies: - - supports-color - - typescript - - '@astrojs/starlight@0.41.4(@astrojs/markdown-remark@7.2.1)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(typescript@6.0.3)': + '@astrojs/starlight@0.41.4(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(typescript@6.0.3)': dependencies: '@astrojs/markdown-satteri': 0.3.4 - '@astrojs/mdx': 7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)) + '@astrojs/mdx': 7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)) '@astrojs/sitemap': 3.7.2 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.5 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) - astro-expressive-code: 0.44.1(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)) + astro: 7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) + astro-expressive-code: 0.44.1(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -3399,16 +3189,14 @@ snapshots: unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 - optionalDependencies: - '@astrojs/markdown-remark': 7.2.1 transitivePeerDependencies: - supports-color - typescript - '@astrojs/svelte@9.0.1(@types/node@24.12.4)(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(esbuild@0.28.1)(svelte@5.56.8)(tsx@4.22.3)(typescript@6.0.3)': + '@astrojs/svelte@9.0.1(@types/node@24.12.4)(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3))(esbuild@0.28.1)(svelte@5.56.8)(tsx@4.22.3)(typescript@6.0.3)': dependencies: '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8)(vite@8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) - astro: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) + astro: 7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) svelte: 5.56.8 svelte2tsx: 0.7.58(svelte@5.56.8)(typescript@6.0.3) typescript: 6.0.3 @@ -3538,7 +3326,9 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260722.1': optional: true - '@cloudflare/workers-types@5.20260728.1': {} + '@cloudflare/workers-types@5.20260726.1': {} + + '@cloudflare/workers-types@5.20260731.1': {} '@cspotcode/source-map-support@0.8.1': dependencies: @@ -3576,32 +3366,16 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/core@2.0.0-alpha.3': - dependencies: - '@emnapi/wasi-threads': 2.0.1 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/runtime@2.0.0-alpha.3': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@2.0.1': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.28.1': optional: true @@ -3689,8 +3463,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.25 - postcss-nested: 6.2.0(postcss@8.5.25) + postcss: 8.5.21 + postcss-nested: 6.2.0(postcss@8.5.21) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -3874,26 +3648,17 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': - dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 - '@tybys/wasm-util': 0.10.3 - optional: true - '@oslojs/encoding@1.1.0': {} '@oxc-project/types@0.139.0': {} - '@oxc-project/types@0.142.0': {} - '@pagefind/darwin-arm64@1.5.2': optional: true @@ -3938,75 +3703,39 @@ snapshots: '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-android-arm64@1.2.1': - optional: true - '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.2.1': - optional: true - '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.2.1': - optional: true - '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.2.1': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.1': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.1': - optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.1': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.1': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.1': - optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.1': - optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.2.1': - optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.2.1': - optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: '@emnapi/core': 1.11.1 @@ -4014,25 +3743,12 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-wasm32-wasi@1.2.1': - dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 - '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) - optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.1': - optional: true - '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.1': - optional: true - '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@5.4.0(rollup@4.60.4)': @@ -4332,14 +4048,6 @@ snapshots: optionalDependencies: vite: 8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) - '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) - '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 @@ -4403,23 +4111,17 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.44.1(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)): - dependencies: - astro: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) - rehype-expressive-code: 0.44.1 - url-extras: 0.1.0 - - astro-expressive-code@0.44.1(astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)): + astro-expressive-code@0.44.1(astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3)): dependencies: - astro: 7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) + astro: 7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3) rehype-expressive-code: 0.44.1 url-extras: 0.1.0 - astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3): + astro@7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3): dependencies: '@astrojs/compiler-rs': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - '@astrojs/internal-helpers': 0.10.1 - '@astrojs/markdown-satteri': 0.3.4 + '@astrojs/internal-helpers': 0.10.2 + '@astrojs/markdown-satteri': 0.3.5 '@astrojs/telemetry': 3.3.3 '@capsizecss/unpack': 4.0.1 '@clack/prompts': 1.7.0 @@ -4432,100 +4134,7 @@ snapshots: clsx: 2.1.1 common-ancestor-path: 2.0.0 cookie: 2.0.1 - devalue: 5.9.0 - diff: 8.0.4 - dset: 3.1.4 - es-module-lexer: 2.3.1 - esbuild: 0.28.1 - flattie: 1.1.1 - fontace: 0.4.1 - get-tsconfig: 5.0.0-beta.4 - github-slugger: 2.0.0 - html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 - js-yaml: 4.3.0 - jsonc-parser: 3.3.1 - magic-string: 1.1.0 - magicast: 0.5.3 - mrmime: 2.0.1 - neotraverse: 1.0.1 - obug: 2.1.4 - p-limit: 7.3.1 - p-queue: 9.3.3 - package-manager-detector: 1.8.0 - piccolore: 0.1.3 - picomatch: 4.0.5 - semver: 7.8.5 - shiki: 4.3.1 - smol-toml: 1.7.1 - svgo: 4.0.2 - tinyclip: 0.1.15 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - ultrahtml: 1.7.0 - unifont: 0.7.4 - unstorage: 1.17.5 - vite: 8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) - vitefu: 1.1.3(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) - xxhash-wasm: 1.1.0 - yargs-parser: 22.0.0 - zod: 4.4.3 - optionalDependencies: - '@astrojs/markdown-remark': 7.2.1 - sharp: 0.35.3(@types/node@24.12.4) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@emnapi/core' - - '@emnapi/runtime' - - '@netlify/blobs' - - '@planetscale/database' - - '@types/node' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - '@vitejs/devtools' - - aws4fetch - - db0 - - idb-keyval - - ioredis - - jiti - - less - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - uploadthing - - yaml - - astro@7.1.4(@astrojs/markdown-remark@7.2.1)(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.12.4)(rollup@4.60.4)(tsx@4.22.3): - dependencies: - '@astrojs/compiler-rs': 0.3.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) - '@astrojs/internal-helpers': 0.10.1 - '@astrojs/markdown-satteri': 0.3.4 - '@astrojs/telemetry': 3.3.3 - '@capsizecss/unpack': 4.0.1 - '@clack/prompts': 1.7.0 - '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.4.0(rollup@4.60.4) - am-i-vibing: 0.4.0 - aria-query: 5.3.2 - axobject-query: 4.1.0 - ci-info: 4.4.0 - clsx: 2.1.1 - common-ancestor-path: 2.0.0 - cookie: 2.0.1 - devalue: 5.9.0 + devalue: 5.8.2 diff: 8.0.4 dset: 3.1.4 es-module-lexer: 2.3.1 @@ -4550,7 +4159,7 @@ snapshots: picomatch: 4.0.5 semver: 7.8.5 shiki: 4.3.1 - smol-toml: 1.7.1 + smol-toml: 1.7.0 svgo: 4.0.2 tinyclip: 0.1.15 tinyexec: 1.2.4 @@ -4558,13 +4167,12 @@ snapshots: ultrahtml: 1.7.0 unifont: 0.7.4 unstorage: 1.17.5 - vite: 8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) - vitefu: 1.1.3(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) + vite: 8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) + vitefu: 1.1.3(vite@8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 optionalDependencies: - '@astrojs/markdown-remark': 7.2.1 sharp: 0.35.3(@types/node@24.12.4) transitivePeerDependencies: - '@azure/app-configuration' @@ -4732,8 +4340,6 @@ snapshots: devalue@5.8.2: {} - devalue@5.9.0: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -4917,7 +4523,7 @@ snapshots: defu: 6.1.7 destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.5 + node-mock-http: 1.0.4 radix3: 1.1.2 ufo: 1.6.4 uncrypto: 0.1.3 @@ -5156,6 +4762,8 @@ snapshots: dependencies: '@types/estree': 1.0.9 + jose@5.10.0: {} + js-tokens@4.0.0: {} js-yaml@4.3.0: @@ -5761,7 +5369,7 @@ snapshots: node-fetch-native@1.6.7: {} - node-mock-http@1.0.5: {} + node-mock-http@1.0.4: {} normalize-path@3.0.0: {} @@ -5853,9 +5461,9 @@ snapshots: picomatch@4.0.5: {} - postcss-nested@6.2.0(postcss@8.5.25): + postcss-nested@6.2.0(postcss@8.5.21): dependencies: - postcss: 8.5.25 + postcss: 8.5.21 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -5869,12 +5477,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.25: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -6104,27 +5706,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 - rolldown@1.2.1: - dependencies: - '@oxc-project/types': 0.142.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.1 - '@rolldown/binding-darwin-arm64': 1.2.1 - '@rolldown/binding-darwin-x64': 1.2.1 - '@rolldown/binding-freebsd-x64': 1.2.1 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 - '@rolldown/binding-linux-arm64-gnu': 1.2.1 - '@rolldown/binding-linux-arm64-musl': 1.2.1 - '@rolldown/binding-linux-ppc64-gnu': 1.2.1 - '@rolldown/binding-linux-s390x-gnu': 1.2.1 - '@rolldown/binding-linux-x64-gnu': 1.2.1 - '@rolldown/binding-linux-x64-musl': 1.2.1 - '@rolldown/binding-openharmony-arm64': 1.2.1 - '@rolldown/binding-wasm32-wasi': 1.2.1 - '@rolldown/binding-win32-arm64-msvc': 1.2.1 - '@rolldown/binding-win32-x64-msvc': 1.2.1 - rollup@4.60.4: dependencies: '@types/estree': 1.0.8 @@ -6178,7 +5759,7 @@ snapshots: '@bruits/satteri-win32-arm64-msvc': 0.9.5 '@bruits/satteri-win32-x64-msvc': 0.9.5 - sax@1.6.1: {} + sax@1.6.0: {} saxes@6.0.0: dependencies: @@ -6251,9 +5832,9 @@ snapshots: '@types/node': 24.12.4 '@types/sax': 1.2.7 arg: 5.0.2 - sax: 1.6.1 + sax: 1.6.0 - smol-toml@1.7.1: {} + smol-toml@1.7.0: {} source-map-js@1.2.1: {} @@ -6335,7 +5916,7 @@ snapshots: css-what: 6.2.2 csso: 5.0.5 picocolors: 1.1.1 - sax: 1.6.1 + sax: 1.6.0 symbol-tree@3.2.4: {} @@ -6516,27 +6097,10 @@ snapshots: fsevents: 2.3.3 tsx: 4.22.3 - vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.2.1 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 24.12.4 - esbuild: 0.28.1 - fsevents: 2.3.3 - tsx: 4.22.3 - vitefu@1.1.3(vite@8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)): optionalDependencies: vite: 8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) - vitefu@1.1.3(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)): - optionalDependencies: - vite: 8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) - vitest@4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)): dependencies: '@vitest/expect': 4.1.10 @@ -6565,34 +6129,6 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.10(@types/node@24.12.4)(jsdom@29.1.1)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.2 - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vite: 8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(tsx@4.22.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.12.4 - jsdom: 29.1.1 - transitivePeerDependencies: - - msw - w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -6624,7 +6160,25 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260722.1 '@cloudflare/workerd-windows-64': 1.20260722.1 - wrangler@4.114.0(@cloudflare/workers-types@5.20260728.1)(@types/node@24.12.4): + wrangler@4.114.0(@cloudflare/workers-types@5.20260726.1)(@types/node@24.12.4): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260722.0(@types/node@24.12.4) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260722.1 + optionalDependencies: + '@cloudflare/workers-types': 5.20260726.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + wrangler@4.114.0(@cloudflare/workers-types@5.20260731.1)(@types/node@24.12.4): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) @@ -6635,7 +6189,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260722.1 optionalDependencies: - '@cloudflare/workers-types': 5.20260728.1 + '@cloudflare/workers-types': 5.20260731.1 fsevents: 2.3.3 transitivePeerDependencies: - '@types/node' diff --git a/site/package.json b/site/package.json index 43598e1..595f0a3 100644 --- a/site/package.json +++ b/site/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "astro dev", "build:deps": "pnpm --filter @nyuchi/nyuchi-docs-search build", - "build": "pnpm run build:deps && astro build", + "generate:internal-paths": "node scripts/generate-internal-paths.mjs", + "build": "pnpm run build:deps && pnpm run generate:internal-paths && astro build", "preview": "astro preview", "astro": "astro" }, @@ -15,11 +16,13 @@ "@astrojs/svelte": "^9.0.1", "@bundu/ui": "^0.1.1", "astro": "^7.1.4", + "jose": "^5.9.6", "sharp": "^0.34.5", "svelte": "^5.56.8", "@nyuchi/nyuchi-docs-search": "workspace:*" }, "devDependencies": { + "@cloudflare/workers-types": "^5.20260726.1", "wrangler": "^4.114.0" } } diff --git a/site/public/internal-paths.json b/site/public/internal-paths.json new file mode 100644 index 0000000..4fc2844 --- /dev/null +++ b/site/public/internal-paths.json @@ -0,0 +1,11 @@ +{ + "internalPaths": [ + "/deployment/agentgateway/", + "/deployment/overview/", + "/mzizi-tools/a2a/", + "/mzizi-tools/dna-helix/", + "/mzizi-tools/overview/", + "/mzizi-tools/registry-health/", + "/tools/" + ] +} diff --git a/site/scripts/generate-internal-paths.mjs b/site/scripts/generate-internal-paths.mjs new file mode 100644 index 0000000..42f3b48 --- /dev/null +++ b/site/scripts/generate-internal-paths.mjs @@ -0,0 +1,73 @@ +// Scans site/src/content/docs/**/*.mdx for `visibility: internal` in +// frontmatter and emits the list of gated URL paths that both the site's +// gate worker (src/worker/gate.ts) and nyuchi-docs-mcp-worker consume as +// their single source of truth for "which pages are private." +// +// Deliberately a standalone regex scan, not the Astro content loader — +// this needs to run before `astro build` (the gate worker imports its +// output) and outside Astro's own build graph. Keep the frontmatter key +// and default in sync with site/src/content.config.ts if either changes. +// +// Outputs: +// site/src/worker/internal-paths.generated.ts — bundled into the gate +// worker directly (no runtime fetch needed). +// site/public/internal-paths.json — served statically at +// /internal-paths.json so nyuchi-docs-mcp-worker (a separate deploy) +// can read the same list over HTTP without a cross-package import. + +import { readdir, readFile, mkdir, writeFile } from 'node:fs/promises'; +import { join, relative, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const DOCS_ROOT = join(SCRIPT_DIR, '..', 'src', 'content', 'docs'); +const WORKER_OUT = join(SCRIPT_DIR, '..', 'src', 'worker', 'internal-paths.generated.ts'); +const PUBLIC_OUT = join(SCRIPT_DIR, '..', 'public', 'internal-paths.json'); + +async function walk(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const full = join(dir, entry.name); + if (entry.isDirectory()) files.push(...(await walk(full))); + else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) files.push(full); + } + return files; +} + +// Mirrors Starlight's own slug derivation closely enough for this purpose: +// strip the docs root, drop the extension, `index` collapses to the parent, +// always ends in a trailing slash to match Starlight's URL convention. +function toUrlPath(file) { + const rel = relative(DOCS_ROOT, file).replace(/\.mdx?$/, ''); + const withoutIndex = rel.replace(/\/index$/, '').replace(/^index$/, ''); + return `/${withoutIndex}${withoutIndex ? '/' : ''}`; +} + +function isInternal(source) { + const fm = source.match(/^---\n([\s\S]*?)\n---/); + if (!fm) return false; + return /^visibility:\s*internal\s*$/m.test(fm[1]); +} + +const files = await walk(DOCS_ROOT); +const internalPaths = []; +for (const file of files) { + const source = await readFile(file, 'utf-8'); + if (isInternal(source)) internalPaths.push(toUrlPath(file)); +} +internalPaths.sort(); + +await mkdir(dirname(WORKER_OUT), { recursive: true }); +await writeFile( + WORKER_OUT, + `// Generated by scripts/generate-internal-paths.mjs — do not edit by hand.\n` + + `// Source of truth is the \`visibility: internal\` frontmatter field; re-run\n` + + `// the generator (wired into \`pnpm build\`) after changing any page's visibility.\n` + + `export const INTERNAL_PATHS: readonly string[] = ${JSON.stringify(internalPaths, null, 2)};\n` +); +await mkdir(dirname(PUBLIC_OUT), { recursive: true }); +await writeFile(PUBLIC_OUT, JSON.stringify({ internalPaths }, null, 2) + '\n'); + +console.log(`generate-internal-paths: ${internalPaths.length} internal path(s)`); +for (const p of internalPaths) console.log(` ${p}`); diff --git a/site/src/content.config.ts b/site/src/content.config.ts index 6a7b7a0..d335bcf 100644 --- a/site/src/content.config.ts +++ b/site/src/content.config.ts @@ -1,7 +1,24 @@ +import { z } from 'astro:content'; import { defineCollection } from 'astro:content'; import { docsLoader } from '@astrojs/starlight/loaders'; import { docsSchema } from '@astrojs/starlight/schema'; +// `visibility: internal` gates a page behind the site's WorkOS OIDC login +// (site/src/worker/gate.ts) and behind bearer-token auth on the docs MCP +// server (nyuchi-docs-mcp-worker). Everything defaults to `public` — mark +// a page internal explicitly, never the other way around. +// +// scripts/generate-internal-paths.mjs reads this frontmatter directly (via +// a regex, not the Astro content loader) to build the manifest both +// workers consume — keep the field name and default in sync with that +// script if either changes. export const collections = { - docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), + docs: defineCollection({ + loader: docsLoader(), + schema: docsSchema({ + extend: z.object({ + visibility: z.enum(['public', 'internal']).default('public'), + }), + }), + }), }; diff --git a/site/src/content/docs/deployment/agentgateway.mdx b/site/src/content/docs/deployment/agentgateway.mdx index 76ac6dd..c6381dc 100644 --- a/site/src/content/docs/deployment/agentgateway.mdx +++ b/site/src/content/docs/deployment/agentgateway.mdx @@ -1,6 +1,7 @@ --- title: Agent Gateway on Fly.io description: How nyuchi/agentgateway ships as a Docker container on Fly.io, fronted by WorkOS OIDC, at fundi.nyuchi.com. +visibility: internal --- import { Aside } from '@astrojs/starlight/components'; diff --git a/site/src/content/docs/deployment/overview.mdx b/site/src/content/docs/deployment/overview.mdx index 8c1d7cb..b6d0e8c 100644 --- a/site/src/content/docs/deployment/overview.mdx +++ b/site/src/content/docs/deployment/overview.mdx @@ -1,6 +1,7 @@ --- title: Deployment overview description: Cloudflare, Vercel, and Supabase patterns we standardise on at Nyuchi. +visibility: internal --- import { Aside } from '@astrojs/starlight/components'; diff --git a/site/src/content/docs/mzizi-tools/a2a.mdx b/site/src/content/docs/mzizi-tools/a2a.mdx index d2e0884..46f718a 100644 --- a/site/src/content/docs/mzizi-tools/a2a.mdx +++ b/site/src/content/docs/mzizi-tools/a2a.mdx @@ -1,6 +1,7 @@ --- title: A2A — delegating long runs to fundi description: Why mzizi-mcp will hand security, chaos, and accessibility runs to the fundi agent over A2A instead of blocking an MCP tool call. Design, not yet shipped. +visibility: internal --- import { Aside } from '@astrojs/starlight/components'; diff --git a/site/src/content/docs/mzizi-tools/dna-helix.mdx b/site/src/content/docs/mzizi-tools/dna-helix.mdx index ec3134b..e027c4b 100644 --- a/site/src/content/docs/mzizi-tools/dna-helix.mdx +++ b/site/src/content/docs/mzizi-tools/dna-helix.mdx @@ -1,6 +1,7 @@ --- title: The Mzizi DNA double helix description: The frontend architecture Mzizi actually serves — nodes on two backbones held by cross-cutting rungs. No axes, no outliers, no 3D. +visibility: internal --- import { Aside } from '@astrojs/starlight/components'; diff --git a/site/src/content/docs/mzizi-tools/overview.mdx b/site/src/content/docs/mzizi-tools/overview.mdx index 47f81d9..e01289a 100644 --- a/site/src/content/docs/mzizi-tools/overview.mdx +++ b/site/src/content/docs/mzizi-tools/overview.mdx @@ -1,6 +1,7 @@ --- title: Mzizi tools overview description: Using mzizi-mcp, mzizi-cli, and mzizi-skills from inside a Nyuchi project — endpoints, install commands, auth, and the authoring flow. +visibility: internal --- import { Aside } from '@astrojs/starlight/components'; diff --git a/site/src/content/docs/mzizi-tools/registry-health.mdx b/site/src/content/docs/mzizi-tools/registry-health.mdx index 211e04d..c176413 100644 --- a/site/src/content/docs/mzizi-tools/registry-health.mdx +++ b/site/src/content/docs/mzizi-tools/registry-health.mdx @@ -1,6 +1,7 @@ --- title: Mzizi registry health description: What the 64 registry tools actually return — which are broken, which serve retired data, and which to trust. Current known state, not an aspiration. +visibility: internal --- import { Aside } from '@astrojs/starlight/components'; diff --git a/site/src/content/docs/tools/index.mdx b/site/src/content/docs/tools/index.mdx index 622246c..f8adda7 100644 --- a/site/src/content/docs/tools/index.mdx +++ b/site/src/content/docs/tools/index.mdx @@ -1,6 +1,7 @@ --- title: Nyuchi tools directory description: Every skill, CLI, and MCP server across the Nyuchi and Bundu repos — what it is, where it lives, and how to install it. +visibility: internal --- import { Aside } from '@astrojs/starlight/components'; diff --git a/site/src/worker/gate.ts b/site/src/worker/gate.ts new file mode 100644 index 0000000..d137c92 --- /dev/null +++ b/site/src/worker/gate.ts @@ -0,0 +1,193 @@ +// Gates `visibility: internal` pages (see src/content.config.ts and +// scripts/generate-internal-paths.mjs) behind a WorkOS OIDC login, and +// falls through to the static asset router for everything else — public +// pages are never touched by any of this. +// +// Session model: the cookie *is* the WorkOS ID token. WorkOS already +// signs it; re-verifying that signature (+ exp) on each request is the +// session check, so there's no separate app-level session secret to +// manage or rotate. `docs_oauth_state` is a short-lived cookie carrying +// this request's PKCE verifier + CSRF state + where to return to; it only +// exists for the few minutes of the login redirect round-trip. +// +// Service-to-service bypass: nyuchi-docs-mcp-worker independently verifies +// each MCP caller's own bearer token before deciding whether to read an +// internal page, then forwards INTERNAL_FETCH_KEY on its own fetch to +// this worker so that already-authorized read doesn't have to go through +// a browser OIDC dance. The key is a Wrangler secret shared between the +// two workers; treat it like any other service credential. + +import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose'; +import { INTERNAL_PATHS } from './internal-paths.generated.js'; + +export interface Env { + ASSETS: Fetcher; + WORKOS_CLIENT_ID: string; + WORKOS_CLIENT_SECRET: string; + WORKOS_ISSUER: string; + /** Shared secret nyuchi-docs-mcp-worker sends to bypass the browser flow for callers it has already authorized itself. */ + INTERNAL_FETCH_KEY?: string; +} + +const SESSION_COOKIE = 'docs_session'; +const OAUTH_STATE_COOKIE = 'docs_oauth_state'; +const CALLBACK_PATH = '/oauth/callback'; + +function isInternalPath(pathname: string): boolean { + const normalised = pathname.endsWith('/') ? pathname : `${pathname}/`; + return INTERNAL_PATHS.some((p) => normalised === p || normalised.startsWith(p)); +} + +function parseCookies(req: Request): Record { + const header = req.headers.get('cookie') ?? ''; + const out: Record = {}; + for (const part of header.split(';')) { + const i = part.indexOf('='); + if (i === -1) continue; + out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim()); + } + return out; +} + +function setCookie(name: string, value: string, maxAgeSeconds: number): string { + return `${name}=${encodeURIComponent(value)}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${maxAgeSeconds}`; +} + +function clearCookie(name: string): string { + return `${name}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`; +} + +function base64url(bytes: Uint8Array): string { + return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +async function pkcePair(): Promise<{ verifier: string; challenge: string }> { + const verifierBytes = crypto.getRandomValues(new Uint8Array(32)); + const verifier = base64url(verifierBytes); + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)); + const challenge = base64url(new Uint8Array(digest)); + return { verifier, challenge }; +} + +// jose's remote JWKS caches keys internally; module-level so it survives +// across requests to the same isolate instead of re-fetching every time. +let jwks: ReturnType | undefined; +let jwksIssuer: string | undefined; + +async function verifySession(env: Env, token: string): Promise { + try { + if (!jwks || jwksIssuer !== env.WORKOS_ISSUER) { + jwks = createRemoteJWKSet(new URL(`${env.WORKOS_ISSUER}/oauth2/jwks`)); + jwksIssuer = env.WORKOS_ISSUER; + } + const { payload } = await jwtVerify(token, jwks, { + issuer: env.WORKOS_ISSUER, + audience: env.WORKOS_CLIENT_ID, + }); + return payload; + } catch { + return null; + } +} + +function redirectToLogin(env: Env, url: URL, verifier: string, challenge: string, state: string): Response { + const authorizeUrl = new URL(`${env.WORKOS_ISSUER}/oauth2/authorize`); + authorizeUrl.searchParams.set('response_type', 'code'); + authorizeUrl.searchParams.set('client_id', env.WORKOS_CLIENT_ID); + authorizeUrl.searchParams.set('redirect_uri', `${url.origin}${CALLBACK_PATH}`); + authorizeUrl.searchParams.set('scope', 'openid'); + authorizeUrl.searchParams.set('state', state); + authorizeUrl.searchParams.set('code_challenge', challenge); + authorizeUrl.searchParams.set('code_challenge_method', 'S256'); + + const stateCookiePayload = JSON.stringify({ state, verifier, redirectTo: url.pathname + url.search }); + return new Response(null, { + status: 302, + headers: { + location: authorizeUrl.toString(), + 'set-cookie': setCookie(OAUTH_STATE_COOKIE, stateCookiePayload, 300), + }, + }); +} + +async function handleCallback(env: Env, req: Request, url: URL): Promise { + const cookies = parseCookies(req); + const raw = cookies[OAUTH_STATE_COOKIE]; + if (!raw) return new Response('Login expired — go back and try again.', { status: 400 }); + + let saved: { state: string; verifier: string; redirectTo: string }; + try { + saved = JSON.parse(raw); + } catch { + return new Response('Malformed login state.', { status: 400 }); + } + + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + if (!code || !state || state !== saved.state) { + return new Response('Login state mismatch — possible CSRF, try again.', { status: 400 }); + } + + const tokenRes = await fetch(`${env.WORKOS_ISSUER}/oauth2/token`, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + client_id: env.WORKOS_CLIENT_ID, + client_secret: env.WORKOS_CLIENT_SECRET, + code, + code_verifier: saved.verifier, + redirect_uri: `${url.origin}${CALLBACK_PATH}`, + }), + }); + if (!tokenRes.ok) { + return new Response(`Login failed exchanging code: ${tokenRes.status}`, { status: 502 }); + } + const tokens = (await tokenRes.json()) as { id_token?: string }; + if (!tokens.id_token) return new Response('Login response had no id_token.', { status: 502 }); + + const payload = await verifySession(env, tokens.id_token); + if (!payload) return new Response('Login succeeded but the returned token failed verification.', { status: 502 }); + + const maxAge = payload.exp ? Math.max(60, payload.exp - Math.floor(Date.now() / 1000)) : 3600; + return new Response(null, { + status: 302, + headers: { + location: saved.redirectTo || '/', + 'set-cookie': setCookie(SESSION_COOKIE, tokens.id_token, maxAge), + }, + }); +} + +export default { + async fetch(req: Request, env: Env): Promise { + const url = new URL(req.url); + + if (url.pathname === CALLBACK_PATH) { + const res = await handleCallback(env, req, url); + // one-shot: the state cookie is spent whether the callback succeeded or not + res.headers.append('set-cookie', clearCookie(OAUTH_STATE_COOKIE)); + return res; + } + + if (!isInternalPath(url.pathname)) { + return env.ASSETS.fetch(req); + } + + // Trusted service-to-service caller (nyuchi-docs-mcp-worker, having + // already verified its own end user) — skip the browser flow entirely. + if (env.INTERNAL_FETCH_KEY && req.headers.get('x-internal-fetch-key') === env.INTERNAL_FETCH_KEY) { + return env.ASSETS.fetch(req); + } + + const cookies = parseCookies(req); + const session = cookies[SESSION_COOKIE]; + if (session && (await verifySession(env, session))) { + return env.ASSETS.fetch(req); + } + + const { verifier, challenge } = await pkcePair(); + const state = base64url(crypto.getRandomValues(new Uint8Array(16))); + return redirectToLogin(env, url, verifier, challenge, state); + }, +} satisfies ExportedHandler; diff --git a/site/src/worker/internal-paths.generated.ts b/site/src/worker/internal-paths.generated.ts new file mode 100644 index 0000000..f4a1817 --- /dev/null +++ b/site/src/worker/internal-paths.generated.ts @@ -0,0 +1,12 @@ +// Generated by scripts/generate-internal-paths.mjs — do not edit by hand. +// Source of truth is the `visibility: internal` frontmatter field; re-run +// the generator (wired into `pnpm build`) after changing any page's visibility. +export const INTERNAL_PATHS: readonly string[] = [ + "/deployment/agentgateway/", + "/deployment/overview/", + "/mzizi-tools/a2a/", + "/mzizi-tools/dna-helix/", + "/mzizi-tools/overview/", + "/mzizi-tools/registry-health/", + "/tools/" +]; diff --git a/site/wrangler.toml b/site/wrangler.toml index 6ed335c..261fc02 100644 --- a/site/wrangler.toml +++ b/site/wrangler.toml @@ -1,14 +1,27 @@ name = "nyuchi-docs" +main = "src/worker/gate.ts" compatibility_date = "2026-03-27" +compatibility_flags = ["nodejs_compat"] account_id = "125a2dfbc21f76a25c980609609e8218" -# Pure static site served via Workers Static Assets. No worker entry — the -# Cloudflare-managed asset router serves files from ./dist directly. SPA-style -# fallback is off: Pagefind URLs must resolve to real files, and missing routes -# should land on the Astro-built 404 page. +# Static assets from ./dist, bound as ASSETS so src/worker/gate.ts can +# fetch through to them. Gate covers `visibility: internal` pages behind +# a WorkOS OIDC login (scripts/generate-internal-paths.mjs bakes the list +# into src/worker/internal-paths.generated.ts at build time); everything +# else passes straight through, same as when this was assets-only. [assets] directory = "./dist" +binding = "ASSETS" not_found_handling = "404-page" [observability] enabled = true + +# Secrets (wrangler secret put, not committed): +# WORKOS_CLIENT_ID, WORKOS_CLIENT_SECRET, WORKOS_ISSUER — the "Nyuchi +# Docs" WorkOS Connect OAuth Application, redirect URI +# https://docs.nyuchi.com/oauth/callback registered in its dashboard. +# INTERNAL_FETCH_KEY — shared with nyuchi-docs-mcp-worker so it can +# read internal pages on behalf of a caller it has already +# authorized itself, bypassing the browser OIDC flow. Any long +# random string; must match on both workers.