From 21689ef59b0eb34b1a29eda739dc10d33d25b44a Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Fri, 10 Jul 2026 17:14:13 +0300 Subject: docs: Cloudflare Workers hosting pipeline (apex, content workers, CI, previews) (#2140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs-infra: scaffold Cloudflare workers workspace (versions.json v2, matrix, toolchain) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: record full Phase-0 plan decision in workers/PLAN.md πŸ€– Generated by [robots](https://vyos.io) * docs-infra: shared content worker β€” asset serving, cache classes, X-Docs-Build, canary no-store πŸ€– Generated by [robots](https://vyos.io) * docs-infra: run worker script before assets; test fetch entrypoint πŸ€– Generated by [robots](https://vyos.io) * docs-infra: apex manifest loader + dispatch map + runtime binding guard (TDD) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: apex redirects (aliases, PDF, trailing-slash) + special paths (TDD) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: PDF redirect honors pdf:null and preserves query πŸ€– Generated by [robots](https://vyos.io) * docs-infra: apex UA gate β€” allowlist-wins, log-only AI crawlers, empty block list at launch πŸ€– Generated by [robots](https://vyos.io) * docs-infra: apex router (pipeline Β§3.2), themed 404/503, /kb seam, env configs + congruence test πŸ€– Generated by [robots](https://vyos.io) * docs-infra: add missing-User-Agent regression test for apex UA gate πŸ€– Generated by [robots](https://vyos.io) * docs-infra: R2-streaming preview worker β€” MIME map, noindex, no-store (TDD) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: preview 404 no-store + fetch handler tests πŸ€– Generated by [robots](https://vyos.io) * docs-infra: bootstrap script β€” binding-target workers must exist before apex deploys πŸ€– Generated by [robots](https://vyos.io) * docs-infra: apex run_worker_first, lockfile for npm ci, PDF Location from manifest πŸ€– Generated by [robots](https://vyos.io) * docs-infra: derive html_baseurl from DOCS_VERSION_SLUG with RTD fallback (canonical gate prereq) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: version picker + status banner + language scaffold (vanilla JS, TDD pure core) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: picker preserves query+hash across switch; valid breadcrumb markup πŸ€– Generated by [robots](https://vyos.io) * docs-infra: Pagefind search wrapper with runtime base-path + preview prefix handling (TDD) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: pagefind wrapper β€” asset-failure notice + UI stylesheet load πŸ€– Generated by [robots](https://vyos.io) * docs-infra: gate Pagefind searchbox to CF builds (RTD keeps stock search until cutover) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: deploy sanity gates β€” limits, critical pages, count-delta, canonical (TDD) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: hermetic gate tests via fixture versions.json πŸ€– Generated by [robots](https://vyos.io) * docs-infra: docs-build workflow β€” candidate/smoke/promote two-stage deploy + registry + rollback Two-stage CF Workers pipeline: build in pinned container, assemble artifact, sanity gates, deploy candidate, scoped pre-traffic smoke via canary apex, promote (rollback-id capture, hostname purge, registry upload), post-promote probe + auto-rollback. DOCS_CF_LIVE repo variable gates every docs.vyos.io production interaction pre-cutover. scripts/docs_gates/smoke.py adds one authorized check beyond the spec: the version's index.html probe asserts the #vyos-search mount div is present in the response body, guarding CI silently forgetting DOCS_VERSION_SLUG (which would otherwise ship stock RTD search without the Pagefind gate noticing). πŸ€– Generated by [robots](https://vyos.io) * docs-infra: build docs image in-workflow with buildx cache (v4.1 β€” digest pin dropped) Plan v4.1 amendment: the ghcr.io digest-pinned image does not exist (workflow would hard-fail at the first docker step on every push). Replace the BUILD_IMAGE env placeholder with an in-workflow docker build from docker/Dockerfile via docker/setup-buildx-action@v3 + docker/build-push-action@v6 (context: docker/, load: true, tags: docs-build:local, GHA cache from/to). The checked-out commit is the pin; buildx GHA cache keeps repeat builds cheap. Sphinx-build step swaps to docs-build:local; inner script unchanged. πŸ€– Generated by [robots](https://vyos.io) * docs-infra: apex/preview deploy workflow β€” canary auto, production behind environment approval * docs-infra: apex-deploy concurrency guard (per-ref, cancel-in-progress) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: fork-safe PR preview pipeline β€” approval record, R2 prefixes, label consumption, cleanup * docs-infra: nightly preview sweep β€” pipefail + per-prefix failure isolation πŸ€– Generated by [robots](https://vyos.io) * docs-infra: nightly canary QA β€” per-entry sweep + URL-parity corpus vs RTD πŸ€– Generated by [robots](https://vyos.io) * docs-infra: parity sweep scoped to CF-built versions; transport-error resilience πŸ€– Generated by [robots](https://vyos.io) * docs-infra: one-off bootstrap workflow (binding targets β€” runs once on this push) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: remove one-off bootstrap workflow (bootstrap complete) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: one-off canary apex + preview deploy (route targets for Task 3.6 step 2c) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: remove one-off canary deploy workflow (targets live) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: address Phase-0 CodeRabbit findings (canonical gate, error caching, registry pointer, validation) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: strengthen manifest tests (full dispatch iteration, mutation-free validate) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: address GitHub CodeRabbit review (pointer-after-probe, fail-closed sweeps, block-precedence UA gate, preview hardening) πŸ€– Generated by [robots](https://vyos.io) * docs-infra: adversarial review fixes β€” error no-store, probe retry, PR-list membership, preview dotted-segment πŸ€– Generated by [robots](https://vyos.io) * docs-infra: serve oversized legacy PDF from R2 via apex (spec Β§5 fallback) The 1.3 PDF (29.2 MiB) exceeds the 25 MiB static-asset cap and is absent from the legacy content Worker's build, so /_/downloads/en/1.3/pdf/ (and the picker's PDF link) 301'd into a dead-end 404 post-cutover. Add the R2 object fallback spec Β§5 already documented but never implemented: a DOCS_PDFS R2 bucket binding on the apex Worker, a manifest pdf_r2_key field (1.3 only), and a router step ahead of version dispatch that streams the object with its own cache class (canary/error still force no-store). πŸ€– Generated by [robots](https://vyos.io) * docs-infra: PDF R2 fallback honors Range + If-None-Match, preserves ETag πŸ€– Generated by [robots](https://vyos.io) --- workers/.gitignore | 3 + workers/PLAN.md | 5 + workers/apex/assets/404.html | 23 + workers/apex/assets/503.html | 24 + workers/apex/assets/apple-touch-icon.png | Bin 0 -> 4128 bytes workers/apex/assets/favicon.ico | Bin 0 -> 766 bytes workers/apex/assets/robots.txt | 3 + workers/apex/assets/root.html | 30 + workers/apex/src/dispatch.ts | 15 + workers/apex/src/index.ts | 159 ++ workers/apex/src/manifest.ts | 55 + workers/apex/src/redirects.ts | 38 + workers/apex/src/special.ts | 55 + workers/apex/src/uagate.ts | 18 + workers/apex/test/dispatch.test.ts | 29 + workers/apex/test/manifest.test.ts | 129 + workers/apex/test/redirects.test.ts | 54 + workers/apex/test/router.test.ts | 334 +++ workers/apex/test/uagate.test.ts | 24 + workers/apex/ua-policy.json | 5 + workers/apex/wrangler.jsonc | 39 + workers/bootstrap.sh | 29 + workers/branch/src/index.ts | 44 + workers/branch/test/content.test.ts | 80 + workers/branch/wrangler.legacy.jsonc | 16 + workers/branch/wrangler.rolling.jsonc | 16 + workers/branch/wrangler.v14.jsonc | 16 + workers/branch/wrangler.v15.jsonc | 16 + workers/matrix.json | 5 + workers/package-lock.json | 3921 ++++++++++++++++++++++++++ workers/package.json | 16 + workers/picker-test/pagefind-wrapper.test.ts | 42 + workers/picker-test/picker.test.ts | 64 + workers/preview/src/index.ts | 61 + workers/preview/test/preview.test.ts | 93 + workers/preview/wrangler.jsonc | 11 + workers/versions.json | 19 + workers/vitest.config.ts | 15 + 38 files changed, 5506 insertions(+) create mode 100644 workers/.gitignore create mode 100644 workers/PLAN.md create mode 100644 workers/apex/assets/404.html create mode 100644 workers/apex/assets/503.html create mode 100644 workers/apex/assets/apple-touch-icon.png create mode 100644 workers/apex/assets/favicon.ico create mode 100644 workers/apex/assets/robots.txt create mode 100644 workers/apex/assets/root.html create mode 100644 workers/apex/src/dispatch.ts create mode 100644 workers/apex/src/index.ts create mode 100644 workers/apex/src/manifest.ts create mode 100644 workers/apex/src/redirects.ts create mode 100644 workers/apex/src/special.ts create mode 100644 workers/apex/src/uagate.ts create mode 100644 workers/apex/test/dispatch.test.ts create mode 100644 workers/apex/test/manifest.test.ts create mode 100644 workers/apex/test/redirects.test.ts create mode 100644 workers/apex/test/router.test.ts create mode 100644 workers/apex/test/uagate.test.ts create mode 100644 workers/apex/ua-policy.json create mode 100644 workers/apex/wrangler.jsonc create mode 100755 workers/bootstrap.sh create mode 100644 workers/branch/src/index.ts create mode 100644 workers/branch/test/content.test.ts create mode 100644 workers/branch/wrangler.legacy.jsonc create mode 100644 workers/branch/wrangler.rolling.jsonc create mode 100644 workers/branch/wrangler.v14.jsonc create mode 100644 workers/branch/wrangler.v15.jsonc create mode 100644 workers/matrix.json create mode 100644 workers/package-lock.json create mode 100644 workers/package.json create mode 100644 workers/picker-test/pagefind-wrapper.test.ts create mode 100644 workers/picker-test/picker.test.ts create mode 100644 workers/preview/src/index.ts create mode 100644 workers/preview/test/preview.test.ts create mode 100644 workers/preview/wrangler.jsonc create mode 100644 workers/versions.json create mode 100644 workers/vitest.config.ts (limited to 'workers') diff --git a/workers/.gitignore b/workers/.gitignore new file mode 100644 index 00000000..ef5b8080 --- /dev/null +++ b/workers/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.wrangler/ +dist/ diff --git a/workers/PLAN.md b/workers/PLAN.md new file mode 100644 index 00000000..c64cd382 --- /dev/null +++ b/workers/PLAN.md @@ -0,0 +1,5 @@ +# Cloudflare account entitlements (docs.vyos.io) + +- Plan: Workers Paid (pay-as-you-go); zone Enterprise on vyos.io ← from Phase-0 item 0.2; update on change +- Static-assets file cap in force: 100,000/version (Paid) β€” sanity gates assume this (scripts/docs_gates/limits.py) +- Plan-dependent features NOT assumed by config: Logpush, Bot Management, Observability retention diff --git a/workers/apex/assets/404.html b/workers/apex/assets/404.html new file mode 100644 index 00000000..6beae739 --- /dev/null +++ b/workers/apex/assets/404.html @@ -0,0 +1,23 @@ + + + + +Page not found β€” VyOS Documentation + + + + +
+

404

+

This page doesn't exist (or moved). Try the latest VyOS documentation.

+
+ + diff --git a/workers/apex/assets/503.html b/workers/apex/assets/503.html new file mode 100644 index 00000000..60fdc6cf --- /dev/null +++ b/workers/apex/assets/503.html @@ -0,0 +1,24 @@ + + + + +Service unavailable β€” VyOS Documentation + + + + +
+

503

+

This version of the documentation is temporarily unavailable. Please try again shortly, or visit + vyos.io in the meantime.

+
+ + diff --git a/workers/apex/assets/apple-touch-icon.png b/workers/apex/assets/apple-touch-icon.png new file mode 100644 index 00000000..82b8fb6b Binary files /dev/null and b/workers/apex/assets/apple-touch-icon.png differ diff --git a/workers/apex/assets/favicon.ico b/workers/apex/assets/favicon.ico new file mode 100644 index 00000000..42e8969e Binary files /dev/null and b/workers/apex/assets/favicon.ico differ diff --git a/workers/apex/assets/robots.txt b/workers/apex/assets/robots.txt new file mode 100644 index 00000000..6f35aa9f --- /dev/null +++ b/workers/apex/assets/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://docs.vyos.io/sitemap.xml diff --git a/workers/apex/assets/root.html b/workers/apex/assets/root.html new file mode 100644 index 00000000..4b266fbf --- /dev/null +++ b/workers/apex/assets/root.html @@ -0,0 +1,30 @@ + + + + +VyOS Documentation + + + + + + +
+

VyOS Documentation

+

Redirecting to the latest documentation…

+
+ + diff --git a/workers/apex/src/dispatch.ts b/workers/apex/src/dispatch.ts new file mode 100644 index 00000000..9ad1f15d --- /dev/null +++ b/workers/apex/src/dispatch.ts @@ -0,0 +1,15 @@ +export function resolveVersion( + pathname: string, + dispatch: Map, +): { slug: string; binding: string } | null { + const m = pathname.match(/^\/en\/([^/]+)\//); + if (!m) return null; + const binding = dispatch.get(m[1]); + return binding ? { slug: m[1], binding } : null; +} + +export function bindingGuard(env: Record, binding: string): Fetcher | null { + const b = env[binding]; + if (b && typeof (b as Fetcher).fetch === "function") return b as Fetcher; + return null; +} diff --git a/workers/apex/src/index.ts b/workers/apex/src/index.ts new file mode 100644 index 00000000..278794e2 --- /dev/null +++ b/workers/apex/src/index.ts @@ -0,0 +1,159 @@ +import { loadManifest, buildDispatch } from "./manifest"; +import { resolveVersion, bindingGuard } from "./dispatch"; +import { redirectFor } from "./redirects"; +import { specialPathFor } from "./special"; +import { uaVerdict } from "./uagate"; +import policy from "../ua-policy.json"; + +export interface ApexEnv extends Record { + ASSETS: Fetcher; + APEX_BUILD_SHA: string; + DOCS_ENV: "production" | "canary"; + DOCS_KB?: Fetcher; + // Β§5 apex PDF fallback β€” R2 bucket holding oversized legacy PDFs excluded from the + // content Worker's own asset tree (currently just the 1.3 PDF). Optional so the + // binding-guard path (503, not a crash) exercises on envs that omit it. + DOCS_PDFS?: R2Bucket; +} + +const manifest = loadManifest(); +const dispatch = buildDispatch(manifest); + +// Security headers only β€” safe on content pass-through (never touches +// Cache-Control or X-Docs-Build, which the content Worker owns). +function securityHeaders(resp: Response): Response { + const out = new Response(resp.body, resp); + out.headers.set("X-Content-Type-Options", "nosniff"); + out.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); + out.headers.set("Content-Security-Policy-Report-Only", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'"); + return out; +} + +const DEFAULT_CACHE_CLASS = "public, max-age=0, s-maxage=300, must-revalidate"; + +function apexHeaders(resp: Response, env: ApexEnv, cacheClass: string = DEFAULT_CACHE_CLASS): Response { + const out = securityHeaders(resp); + out.headers.set("X-Apex-Build", env.APEX_BUILD_SHA); + // Β§3.3 cache contract applies to apex-owned responses too. Error responses (4xx/5xx) + // must never carry the s-maxage cache class β€” the cache key excludes User-Agent, so a + // cached UA-gate 403 or themed 404/503 would poison the edge for every visitor for the + // full s-maxage window. Mirrors the branch worker's withDocsHeaders() precedence. + // `cacheClass` lets a specific caller (e.g. the Β§5 PDF R2 fallback) apply a + // differently-classed success cache-control; canary/error still always win. + out.headers.set( + "Cache-Control", + env.DOCS_ENV === "canary" || out.status >= 400 ? "no-store" : cacheClass, + ); + return out; +} + +async function themed(env: ApexEnv, status: 404 | 503): Promise { + const page = await env.ASSETS.fetch(new Request(`https://apex.internal/${status}.html`)); + return apexHeaders(new Response(page.body, { status, headers: { "content-type": "text/html; charset=utf-8" } }), env); +} + +export default { + async fetch(request: Request, env: ApexEnv): Promise { + const url = new URL(request.url); + + // 1. UA gate β€” production only (Β§3.2.1) + if (env.DOCS_ENV === "production") { + const v = uaVerdict(request.headers.get("user-agent") ?? "", policy); + if (v === "block") return apexHeaders(new Response("Forbidden", { status: 403 }), env); + if (v === "log") console.log(JSON.stringify({ event: "ua-log", ua: request.headers.get("user-agent"), path: url.pathname })); + } + + // 2. Special paths (Β§3.2.2) + const special = await specialPathFor(request, manifest, env as never); + if (special) return apexHeaders(special, env); + + // 2b. Legacy PDF R2 fallback (spec Β§5) β€” the 1.3 PDF (29.2 MiB) exceeds the 25 MiB + // static-asset cap and is excluded from the content Worker's own build. MUST run + // before version dispatch (step 6): the legacy Worker's asset tree lacks this file, + // so unconditional dispatch would 404 on the exact path the PDF 301 (Β§3.2.4) targets. + const pdfVersion = manifest.versions.find((v) => v.pdf_r2_key && v.pdf === url.pathname); + if (pdfVersion) { + const bucket = env.DOCS_PDFS; + if (!bucket) { + console.log(JSON.stringify({ event: "binding-missing", binding: "DOCS_PDFS" })); + return themed(env, 503); + } + let raw: R2ObjectBody | R2Object | null; + try { + // Forward Range + conditional (If-None-Match/If-Match/If-Modified-Since) headers + // straight through to R2 so a resumed download or a client with a fresh cached + // copy doesn't have to re-pull the full 29.2 MiB object. + raw = await bucket.get(pdfVersion.pdf_r2_key!, { + range: request.headers, + onlyIf: request.headers, + }); + } catch (e) { + console.log(JSON.stringify({ event: "binding-error", binding: "DOCS_PDFS", error: String(e) })); + return themed(env, 503); + } + if (!raw) return themed(env, 404); + + // Distinct (longer) cache class from apex's default control-response class β€” this + // is effectively content, just not content the legacy content Worker can serve. + // 304/206 are both <400 so apexHeaders() still applies this class, not "no-store". + const pdfCacheClass = "public, max-age=300, s-maxage=600, must-revalidate"; + const pdfHeaders: Record = { + etag: raw.httpEtag, + "accept-ranges": "bytes", + }; + + // A satisfied onlyIf precondition (e.g. If-None-Match matched the R2 object's current + // ETag) makes R2 hand back a body-less R2Object β€” just the validators, no content. + if (!("body" in raw) || !raw.body) { + return apexHeaders(new Response(null, { status: 304, headers: pdfHeaders }), env, pdfCacheClass); + } + const obj = raw as R2ObjectBody; + pdfHeaders["content-type"] = "application/pdf"; + + // A satisfied Range request β€” R2 echoes the actually-served byte range on `obj.range`; + // its absence means either no Range header was sent or R2 served the full object. + const range = obj.range; + if (range && "offset" in range) { + const start = range.offset ?? 0; + const length = range.length ?? obj.size - start; + pdfHeaders["content-range"] = `bytes ${start}-${start + length - 1}/${obj.size}`; + pdfHeaders["content-length"] = String(length); + return apexHeaders(new Response(obj.body, { status: 206, headers: pdfHeaders }), env, pdfCacheClass); + } + + pdfHeaders["content-length"] = String(obj.size); + return apexHeaders(new Response(obj.body, { status: 200, headers: pdfHeaders }), env, pdfCacheClass); + } + + // 3+4. Trailing-slash + alias/codename/PDF 301s (Β§3.2.3-4) + const redir = redirectFor(url, manifest); + if (redir) return apexHeaders(redir, env); + + // 5. /kb seam (Β§3.2.5) + if (url.pathname.startsWith("/kb/") || url.pathname === "/kb") { + if (env.DOCS_KB) return securityHeaders(await env.DOCS_KB.fetch(request)); + return themed(env, 404); + } + + // 6. Version dispatch (Β§3.2.6) + const hit = resolveVersion(url.pathname, dispatch); + if (hit) { + const fetcher = bindingGuard(env, hit.binding); + if (!fetcher) { // 7. runtime binding guard (Β§3.2.7) + console.log(JSON.stringify({ event: "binding-missing", binding: hit.binding })); + return themed(env, 503); + } + try { + const resp = await fetcher.fetch(request); + if (resp.status === 404) return themed(env, 404); + return securityHeaders(resp); // Β§3.3: security headers at apex; cache + X-Docs-Build stay content-owned + } catch (e) { + console.log(JSON.stringify({ event: "binding-error", binding: hit.binding, error: String(e) })); + return themed(env, 503); + } + } + + // 7. Fallback + return themed(env, 404); + }, +} satisfies ExportedHandler; diff --git a/workers/apex/src/manifest.ts b/workers/apex/src/manifest.ts new file mode 100644 index 00000000..396f29ed --- /dev/null +++ b/workers/apex/src/manifest.ts @@ -0,0 +1,55 @@ +import raw from "../../versions.json"; + +export interface VersionEntry { + slug: string; label: string; + status: "dev" | "lts" | "eol"; + binding: string; aliases: string[]; + pdf: string | null; + // R2 object key for the apex PDF fallback (spec Β§5) β€” set only on versions whose PDF + // exceeds the 25 MiB static-asset cap and is therefore absent from the content + // Worker's own asset tree (currently just 1.3). Optional; most versions omit it. + pdf_r2_key?: string; +} +export interface Manifest { + schema_version: number; + default_lang: string; default_version: string; + languages: { code: string; label: string }[]; + versions: VersionEntry[]; +} + +// Split out from loadManifest() so tests can validate synthetic manifests without +// touching the real ../../versions.json import (which loadManifest() is hardwired to). +export function validateManifest(m: Manifest): Manifest { + if (m.schema_version !== 2) throw new Error(`versions.json schema_version ${m.schema_version} != 2`); + const slugs = new Set(); + for (const v of m.versions) { + if (!/^DOCS_[A-Z0-9_]+$/.test(v.binding)) throw new Error(`bad binding for ${v.slug}`); + if (!["dev", "lts", "eol"].includes(v.status)) throw new Error(`bad status for ${v.slug}`); + if (slugs.has(v.slug)) throw new Error(`duplicate slug: ${v.slug}`); + // pdf_r2_key names an R2 fallback for the exact `pdf` URL β€” a null pdf has no URL + // for the fallback to ever be reached at, so the pairing is nonsensical. + if (v.pdf_r2_key && v.pdf === null) throw new Error(`pdf_r2_key set but pdf is null for ${v.slug}`); + slugs.add(v.slug); + } + // Separate pass: an alias must not collide with ANY canonical slug (not just an + // earlier one), so `slugs` must be fully populated before this check runs. + const aliases = new Set(); + for (const v of m.versions) { + for (const alias of v.aliases) { + if (slugs.has(alias)) throw new Error(`alias ${alias} (on ${v.slug}) collides with a canonical slug`); + if (aliases.has(alias)) throw new Error(`duplicate alias: ${alias}`); + aliases.add(alias); + } + } + if (!m.versions.some((v) => v.slug === m.default_version)) + throw new Error(`default_version ${m.default_version} not in versions[]`); + return m; +} + +export function loadManifest(): Manifest { + return validateManifest(raw as Manifest); +} + +export function buildDispatch(m: Manifest): Map { + return new Map(m.versions.map((v) => [v.slug, v.binding])); +} diff --git a/workers/apex/src/redirects.ts b/workers/apex/src/redirects.ts new file mode 100644 index 00000000..835b50f7 --- /dev/null +++ b/workers/apex/src/redirects.ts @@ -0,0 +1,38 @@ +import type { Manifest } from "./manifest"; + +function aliasMap(m: Manifest): Map { + const map = new Map(); + for (const v of m.versions) for (const a of v.aliases) map.set(a, v.slug); + return map; +} + +function r301(pathAndQuery: string): Response { + return new Response(null, { status: 301, headers: { Location: pathAndQuery } }); +} + +export function redirectFor(url: URL, m: Manifest): Response | null { + const { pathname, search } = url; // never touch url.hash β€” fragments don't reach the server + + // RTD PDF URLs: /_/downloads/en//pdf/* β†’ /en//vyos-documentation.pdf + const pdf = pathname.match(/^\/_\/downloads\/en\/([^/]+)\/pdf(?:\/|$)/); + if (pdf) { + const slug = aliasMap(m).get(pdf[1]) ?? pdf[1]; + const entry = m.versions.find((v) => v.slug === slug); + // pdf: null means no PDF artifact exists for this version β€” don't 301 into a dead-end 404. + // The manifest's pdf value is the source of truth (not a hardcoded filename) so a future + // R2-fallback path for 1.3 (or any other version) can move the target without a code change. + if (entry && entry.pdf !== null) return r301(`${entry.pdf}${search}`); + } + + // Alias / codename prefixes: /en//* β†’ /en//* + const seg = pathname.match(/^\/en\/([^/]+)(\/.*)?$/); + if (seg) { + const [, first, rest = ""] = seg; + const target = aliasMap(m).get(first); + if (target) return r301(`/en/${target}${rest || "/"}${search}`); + // Trailing-slash normalization on bare version roots: /en/ β†’ /en// + if (rest === "" && m.versions.some((v) => v.slug === first)) + return r301(`/en/${first}/${search}`); + } + return null; +} diff --git a/workers/apex/src/special.ts b/workers/apex/src/special.ts new file mode 100644 index 00000000..f1a843f0 --- /dev/null +++ b/workers/apex/src/special.ts @@ -0,0 +1,55 @@ +import type { Manifest } from "./manifest"; +import { bindingGuard } from "./dispatch"; + +// Static assets (404.html, 503.html, robots.txt, favicons, root) live in the apex +// ASSETS binding; versions.json is served from the imported manifest so the body +// always matches the dispatch map. +export async function specialPathFor( + request: Request, + m: Manifest, + env: Record & { ASSETS: Fetcher }, +): Promise { + const url = new URL(request.url); + const p = url.pathname; + + if (p === "/") // default-version redirect (Β§3.2.2) β€” query preserved, like alias redirects + return new Response(null, { status: 301, headers: { Location: `/en/${m.default_version}/${url.search}` } }); + + if (p === "/versions.json") + return new Response(JSON.stringify(m), { + headers: { "content-type": "application/json; charset=utf-8" }, + }); + + if (p === "/healthz") + return new Response(JSON.stringify({ status: "ok", versions: m.versions.length }), { + headers: { "content-type": "application/json" }, + }); + + if (p === "/sitemap.xml") { + const entries = m.versions + .map((v) => `https://docs.vyos.io/en/${v.slug}/sitemap.xml`) + .join(""); + return new Response( + `${entries}`, + { headers: { "content-type": "application/xml" } }, + ); + } + + if (p === "/llms.txt") { // direct body, not a redirect (Β§3.2.2) + const def = m.versions.find((v) => v.slug === m.default_version)!; + const b = bindingGuard(env, def.binding); + if (!b) // do NOT fall through β€” /llms.txt with a missing binding is a 503, not a 404 + return new Response("service unavailable", { status: 503, headers: { "content-type": "text/plain" } }); + // Forward the ORIGINAL request (method + conditional-GET headers), just retargeted + // to the versioned path β€” mirrors the robots.txt/favicon pass-through below. + const target = new URL(`/en/${def.slug}/llms.txt`, url); + return b.fetch(new Request(target, request)); + } + + if (p === "/robots.txt" || p === "/favicon.ico" || /^\/apple-touch-icon.*\.png$/.test(p)) + // Pass the ORIGINAL request through (not a re-synthesized `new Request(url)`) so + // conditional-GET headers (If-None-Match / If-Modified-Since) reach ASSETS intact. + return env.ASSETS.fetch(request); + + return null; +} diff --git a/workers/apex/src/uagate.ts b/workers/apex/src/uagate.ts new file mode 100644 index 00000000..822a7277 --- /dev/null +++ b/workers/apex/src/uagate.ts @@ -0,0 +1,18 @@ +export interface UaPolicy { + allow: string[]; + log: string[]; + block: string[]; +} + +export type UaVerdict = "allow" | "block" | "log"; + +export function uaVerdict(ua: string, policy: UaPolicy): UaVerdict { + const hit = (list: string[]) => list.some((n) => ua.toLowerCase().includes(n.toLowerCase())); + // Explicit blocks take precedence β€” a request-controlled UA string that spoofs an + // allow-listed substring (e.g. "Googlebot EvilScraper") must not be able to bypass a + // block entry just by also matching the allow list. + if (hit(policy.block)) return "block"; + if (hit(policy.allow)) return "allow"; + if (hit(policy.log)) return "log"; + return "allow"; // fail-open default +} diff --git a/workers/apex/test/dispatch.test.ts b/workers/apex/test/dispatch.test.ts new file mode 100644 index 00000000..9a61169d --- /dev/null +++ b/workers/apex/test/dispatch.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { loadManifest, buildDispatch } from "../src/manifest"; +import { resolveVersion, bindingGuard } from "../src/dispatch"; + +const dispatch = buildDispatch(loadManifest()); + +describe("version dispatch (Β§3.2.6)", () => { + it("resolves /en//... to its binding, path untouched", () => { + expect(resolveVersion("/en/rolling/cli/index.html", dispatch)) + .toEqual({ slug: "rolling", binding: "DOCS_ROLLING" }); + expect(resolveVersion("/en/1.5/", dispatch)) + .toEqual({ slug: "1.5", binding: "DOCS_V15" }); + }); + it("returns null for unknown version or non-version paths", () => { + expect(resolveVersion("/en/9.9/x", dispatch)).toBeNull(); + expect(resolveVersion("/kb/article", dispatch)).toBeNull(); + }); +}); + +describe("runtime binding guard (Β§3.2.7)", () => { + it("returns the fetcher when binding exists", () => { + const fake = { fetch: async () => new Response("ok") }; + expect(bindingGuard({ DOCS_ROLLING: fake } as never, "DOCS_ROLLING")).toBe(fake); + }); + it("returns null (β†’ themed 503) when binding missing or not a Fetcher", () => { + expect(bindingGuard({} as never, "DOCS_ROLLING")).toBeNull(); + expect(bindingGuard({ DOCS_ROLLING: 42 } as never, "DOCS_ROLLING")).toBeNull(); + }); +}); diff --git a/workers/apex/test/manifest.test.ts b/workers/apex/test/manifest.test.ts new file mode 100644 index 00000000..5569d081 --- /dev/null +++ b/workers/apex/test/manifest.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect } from "vitest"; +import { loadManifest, buildDispatch, validateManifest, type Manifest } from "../src/manifest"; +// The workers pool has no real filesystem (node:fs readFileSync is an unimplemented +// stub β€” see @cloudflare/vitest-pool-workers/dist/worker/lib/node/fs.mjs); import the +// config as a Vite `?raw` asset instead so the file content is inlined at bundle time. +// eslint-disable-next-line import/no-unresolved +import wranglerJsonc from "../wrangler.jsonc?raw"; +// eslint-disable-next-line import/no-unresolved +import rootHtml from "../assets/root.html?raw"; + +describe("versions.json v2 manifest (Β§3.4)", () => { + it("loads and validates schema_version 2 with 5 versions", () => { + const m = loadManifest(); + expect(m.schema_version).toBe(2); + expect(m.versions).toHaveLength(5); + expect(m.default_version).toBe("rolling"); + }); + it("every version carries a binding; statuses in dev|lts|eol", () => { + const m = loadManifest(); + for (const v of m.versions) { + expect(v.binding).toMatch(/^DOCS_[A-Z0-9_]+$/); + expect(["dev", "lts", "eol"]).toContain(v.status); + } + }); + it("dispatch map: every version slug maps to its binding (shared legacy included)", () => { + const m = loadManifest(); + const d = buildDispatch(m); + expect(d.size).toBe(m.versions.length); + for (const v of m.versions) { + expect(d.get(v.slug)).toBe(v.binding); + } + // aliases are redirect-layer concerns, never dispatch keys + for (const v of m.versions) { + for (const a of v.aliases) { + expect(d.has(a)).toBe(false); + } + } + }); + it("only 1.3 carries pdf_r2_key (Β§5 apex PDF R2 fallback)", () => { + const m = loadManifest(); + const withKey = m.versions.filter((v) => v.pdf_r2_key); + expect(withKey).toHaveLength(1); + expect(withKey[0]).toMatchObject({ + slug: "1.3", + pdf: "/en/1.3/vyos-documentation.pdf", + pdf_r2_key: "legacy/1.3/vyos-documentation.pdf", + }); + }); +}); + +function baseManifest(): Manifest { + return { + schema_version: 2, + default_lang: "en", + default_version: "rolling", + languages: [{ code: "en", label: "English" }], + versions: [ + { slug: "rolling", label: "Rolling", status: "dev", binding: "DOCS_ROLLING", + aliases: ["latest"], pdf: null }, + { slug: "1.5", label: "1.5", status: "lts", binding: "DOCS_V15", + aliases: ["stable", "lts"], pdf: null }, + ], + }; +} + +describe("validateManifest β€” duplicate/ambiguous slug + alias rejection", () => { + it("rejects a duplicate slug", () => { + const m = baseManifest(); + m.versions.push({ ...m.versions[0], binding: "DOCS_ROLLING2" }); + expect(() => validateManifest(m)).toThrow(/duplicate slug: rolling/); + }); + + it("rejects a duplicate alias across two versions", () => { + const m = baseManifest(); + m.versions[1].aliases.push("latest"); // "latest" already aliases rolling + expect(() => validateManifest(m)).toThrow(/duplicate alias: latest/); + }); + + it("rejects an alias that collides with a canonical slug", () => { + const m = baseManifest(); + m.versions[0].aliases.push("1.5"); // "1.5" is a real slug + expect(() => validateManifest(m)).toThrow(/alias 1\.5 .* collides with a canonical slug/); + }); + + it("accepts a well-formed manifest unchanged", () => { + const m = baseManifest(); + const snapshot = structuredClone(m); + expect(validateManifest(m)).toBe(m); + expect(m).toEqual(snapshot); // validation must not mutate the manifest + }); + + it("accepts pdf_r2_key when pdf is set", () => { + const m = baseManifest(); + m.versions[1].pdf = "/en/1.5/vyos-documentation.pdf"; + m.versions[1].pdf_r2_key = "legacy/1.5/vyos-documentation.pdf"; + expect(validateManifest(m)).toBe(m); + }); + + it("rejects pdf_r2_key set on a version whose pdf is null (no URL for the fallback to be reached at)", () => { + const m = baseManifest(); + m.versions[0].pdf_r2_key = "legacy/rolling/vyos-documentation.pdf"; // pdf stays null + expect(() => validateManifest(m)).toThrow(/pdf_r2_key set but pdf is null for rolling/); + }); +}); + +it("every versions.json binding exists in BOTH apex wrangler envs (Β§3.4 gate a)", () => { + const raw = wranglerJsonc.replace(/\/\/.*$/gm, ""); // strip line comments + const cfg = JSON.parse(raw); + const bindings = new Set(buildDispatch(loadManifest()).values()); + for (const envName of ["canary", "production"]) { + const services = new Set((cfg.env[envName].services as { binding: string }[]).map((s) => s.binding)); + for (const b of bindings) expect(services, `${envName} missing ${b}`).toContain(b); + } +}); + +it("root.html's hardcoded default-version references stay congruent with the manifest", () => { + // root.html is a static fallback shell (never templated at build time β€” see the + // comment in the file) so its `/en//` references must be hand-kept in sync + // with the manifest's default_version. This is the drift guard: it fails loudly if + // someone bumps default_version without also updating the static asset. + const expected = `/en/${loadManifest().default_version}/`; + // Strip HTML comments first β€” the explanatory comment in root.html mentions the + // literal placeholder text "/en//", which is documentation, not + // a real markup reference, and must not be asserted against the manifest. + const withoutComments = rootHtml.replace(//g, ""); + const refs = withoutComments.match(/\/en\/[^/"'\s]+\//g) ?? []; + expect(refs.length).toBeGreaterThan(0); + for (const ref of refs) expect(ref).toBe(expected); +}); diff --git a/workers/apex/test/redirects.test.ts b/workers/apex/test/redirects.test.ts new file mode 100644 index 00000000..5df03197 --- /dev/null +++ b/workers/apex/test/redirects.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { loadManifest } from "../src/manifest"; +import { redirectFor } from "../src/redirects"; + +const m = loadManifest(); +const loc = (u: string) => { + const r = redirectFor(new URL(u), m); + return r ? { status: r.status, location: r.headers.get("Location") } : null; +}; + +describe("alias + codename 301s (Β§3.2.4)", () => { + it("maps every alias, preserving path + query", () => { + expect(loc("https://docs.vyos.io/en/latest/cli/index.html?x=1")) + .toEqual({ status: 301, location: "/en/rolling/cli/index.html?x=1" }); + expect(loc("https://docs.vyos.io/en/stable/")).toEqual({ status: 301, location: "/en/1.5/" }); + expect(loc("https://docs.vyos.io/en/lts/a")).toEqual({ status: 301, location: "/en/1.5/a" }); + expect(loc("https://docs.vyos.io/en/circinus/a")).toEqual({ status: 301, location: "/en/1.5/a" }); + expect(loc("https://docs.vyos.io/en/sagitta/a")).toEqual({ status: 301, location: "/en/1.4/a" }); + expect(loc("https://docs.vyos.io/en/equuleus/a")).toEqual({ status: 301, location: "/en/1.3/a" }); + expect(loc("https://docs.vyos.io/en/crux/a")).toEqual({ status: 301, location: "/en/1.2/a" }); + }); + it("RTD PDF URLs β†’ PDF path taken from the manifest (source of truth, Β§3.4)", () => { + const v15pdf = m.versions.find((v) => v.slug === "1.5")!.pdf; + const rollingPdf = m.versions.find((v) => v.slug === "rolling")!.pdf; + expect(loc("https://docs.vyos.io/_/downloads/en/1.5/pdf/")) + .toEqual({ status: 301, location: v15pdf }); + expect(loc("https://docs.vyos.io/_/downloads/en/latest/pdf/")) + .toEqual({ status: 301, location: rollingPdf }); + }); + it("RTD PDF URLs for a pdf:null version β†’ no redirect (no dead-end 301)", () => { + expect(loc("https://docs.vyos.io/_/downloads/en/crux/pdf/")).toBeNull(); + expect(loc("https://docs.vyos.io/_/downloads/en/1.2/pdf/")).toBeNull(); + }); + it("RTD PDF URLs preserve the query string, appended to the manifest's pdf value", () => { + const v14pdf = m.versions.find((v) => v.slug === "1.4")!.pdf; + expect(loc("https://docs.vyos.io/_/downloads/en/sagitta/pdf/?x=1")) + .toEqual({ status: 301, location: `${v14pdf}?x=1` }); + }); + it("does not match /pdf-notes (only an exact /pdf segment)", () => { + expect(loc("https://docs.vyos.io/_/downloads/en/rolling/pdf-notes")).toBeNull(); + }); + it("trailing-slash normalization on bare version roots (Β§3.2.3)", () => { + expect(loc("https://docs.vyos.io/en/1.5")).toEqual({ status: 301, location: "/en/1.5/" }); + expect(loc("https://docs.vyos.io/en/rolling?q=1")).toEqual({ status: 301, location: "/en/rolling/?q=1" }); + }); + it("never emits a fragment in Location (Β§3.2.6)", () => { + const r = loc("https://docs.vyos.io/en/latest/page.html#section"); + expect(r!.location).not.toContain("#"); + }); + it("returns null for canonical paths (no redirect loop)", () => { + expect(loc("https://docs.vyos.io/en/rolling/")).toBeNull(); + expect(loc("https://docs.vyos.io/en/1.5/cli/")).toBeNull(); + }); +}); diff --git a/workers/apex/test/router.test.ts b/workers/apex/test/router.test.ts new file mode 100644 index 00000000..3ba1867c --- /dev/null +++ b/workers/apex/test/router.test.ts @@ -0,0 +1,334 @@ +import { describe, it, expect, vi } from "vitest"; +import worker from "../src/index"; + +function makeEnv(overrides: Record = {}) { + const html = (body: string, status = 200) => + new Response(body, { status, headers: { "content-type": "text/html", "X-Docs-Build": "sha-content" } }); + const fetcher = (tag: string) => ({ + fetch: async (req: Request) => { + const p = new URL(req.url).pathname; + if (p.endsWith("/missing.html")) return html("nope", 404); + return html(`${tag}:${p}`); + }, + }); + return { + DOCS_ROLLING: fetcher("rolling"), DOCS_V15: fetcher("v15"), + DOCS_V14: fetcher("v14"), DOCS_LEGACY: fetcher("legacy"), + ASSETS: { fetch: async () => new Response("

404

", { status: 200, headers: { "content-type": "text/html" } }) }, + APEX_BUILD_SHA: "apex-sha", DOCS_ENV: "canary", + ...overrides, + } as never; +} +const get = (path: string, env = makeEnv(), ua = "vitest") => + worker.fetch(new Request(`https://docs-next.vyos.io${path}`, { headers: { "user-agent": ua } }), env); + +describe("apex router (§3.2 order)", () => { + it("/ → 301 default version", async () => { + const r = await get("/"); + expect(r.status).toBe(301); + expect(r.headers.get("Location")).toBe("/en/rolling/"); + expect(r.headers.get("X-Apex-Build")).toBe("apex-sha"); + }); + it("/ redirect preserves the query string (like alias redirects)", async () => { + const r = await get("/?ref=email"); + expect(r.status).toBe(301); + expect(r.headers.get("Location")).toBe("/en/rolling/?ref=email"); + }); + it("/versions.json served from manifest with X-Apex-Build", async () => { + const r = await get("/versions.json"); + expect(r.status).toBe(200); + const body = await r.json() as { schema_version: number }; + expect(body.schema_version).toBe(2); + expect(r.headers.get("X-Apex-Build")).toBe("apex-sha"); + }); + it("dispatches /en/rolling/x to the binding with original path", async () => { + const r = await get("/en/rolling/cli/index.html"); + expect(await r.text()).toBe("rolling:/en/rolling/cli/index.html"); + expect(r.headers.get("X-Docs-Build")).toBe("sha-content"); + }); + it("content responses get apex security headers, content-owned headers untouched (§3.3)", async () => { + const r = await get("/en/rolling/cli/index.html"); + expect(r.headers.get("X-Content-Type-Options")).toBe("nosniff"); + expect(r.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin"); + expect(r.headers.get("X-Docs-Build")).toBe("sha-content"); // not overwritten + expect(r.headers.get("X-Apex-Build")).toBeNull(); // apex build header is apex-paths-only + }); + it("alias 301 before dispatch", async () => { + const r = await get("/en/latest/cli/"); + expect(r.status).toBe(301); + expect(r.headers.get("Location")).toBe("/en/rolling/cli/"); + }); + it("binding 404 → themed 404 with real 404 status (§3.2.7)", async () => { + const r = await get("/en/rolling/missing.html"); + expect(r.status).toBe(404); + expect(r.headers.get("X-Apex-Build")).toBe("apex-sha"); + }); + it("missing binding degrades to themed 503, not a crash", async () => { + const env = makeEnv({ DOCS_ROLLING: undefined }); + const r = await get("/en/rolling/", env); + expect(r.status).toBe(503); + }); + it("/kb/* → themed 404 while seam unbound; dispatches when DOCS_KB present", async () => { + expect((await get("/kb/article")).status).toBe(404); + const env = makeEnv({ DOCS_KB: { fetch: async () => new Response("kb!") } }); + const r = await get("/kb/article", env); + expect(await r.text()).toBe("kb!"); + // /kb passthrough gets the same security headers as any other content response. + expect(r.headers.get("X-Content-Type-Options")).toBe("nosniff"); + expect(r.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin"); + }); + it("unknown path → themed 404 + security headers", async () => { + const r = await get("/nope"); + expect(r.status).toBe(404); + expect(r.headers.get("X-Content-Type-Options")).toBe("nosniff"); + expect(r.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin"); + }); + it("UA gate blocks only in production env", async () => { + const prodEnv = makeEnv({ DOCS_ENV: "production" }); + // policy.block is empty at launch → craft env-independent check via log class: + const r = await get("/en/rolling/", prodEnv, "GPTBot/1.0"); + expect(r.status).toBe(200); // log-only, not blocked + }); + it("apex responses carry §3.3 cache headers (no-store canary; revalidate production)", async () => { + expect((await get("/versions.json")).headers.get("Cache-Control")).toBe("no-store"); + const prod = await get("/versions.json", makeEnv({ DOCS_ENV: "production" })); + expect(prod.headers.get("Cache-Control")).toBe("public, max-age=0, s-maxage=300, must-revalidate"); + }); + it("error responses stay no-store in production — cache key excludes UA, so a cached 4xx/5xx would poison the edge for everyone", async () => { + const prodEnv = makeEnv({ DOCS_ENV: "production" }); + + // themed 404 (unknown path) + const notFound = await get("/nope", prodEnv); + expect(notFound.status).toBe(404); + expect(notFound.headers.get("Cache-Control")).toBe("no-store"); + + // themed 503 (missing runtime binding) + const svcUnavailable = await get("/en/rolling/", makeEnv({ DOCS_ENV: "production", DOCS_ROLLING: undefined })); + expect(svcUnavailable.status).toBe(503); + expect(svcUnavailable.headers.get("Cache-Control")).toBe("no-store"); + + // UA-block 403 — force a block entry via a policy override, since the shipped + // ua-policy.json block list is empty at launch. + vi.resetModules(); + vi.doMock("../ua-policy.json", () => ({ + default: { allow: [], log: [], block: ["EvilScraper"] }, + })); + try { + const { default: freshWorker } = await import("../src/index"); + const blocked = await freshWorker.fetch( + new Request("https://docs-next.vyos.io/en/rolling/", { headers: { "user-agent": "EvilScraper/1.0" } }), + prodEnv, + ); + expect(blocked.status).toBe(403); + expect(blocked.headers.get("Cache-Control")).toBe("no-store"); + } finally { + vi.doUnmock("../ua-policy.json"); + vi.resetModules(); + } + }); + it("/llms.txt with missing default binding → 503, never 404", async () => { + const env = makeEnv({ DOCS_ROLLING: undefined }); + expect((await get("/llms.txt", env)).status).toBe(503); + }); + it("/llms.txt forwards the ORIGINAL request (method + conditional-GET headers preserved)", async () => { + let seenMethod: string | null = null; + let seenIfNoneMatch: string | null = null; + const env = makeEnv({ + DOCS_ROLLING: { + fetch: async (req: Request) => { + seenMethod = req.method; + seenIfNoneMatch = req.headers.get("if-none-match"); + return new Response("llms body", { headers: { "content-type": "text/plain" } }); + }, + }, + }); + const r = await worker.fetch( + new Request("https://docs-next.vyos.io/llms.txt", { + method: "HEAD", + headers: { "user-agent": "vitest", "if-none-match": '"xyz"' }, + }), + env, + ); + expect(r.status).toBe(200); + expect(seenMethod).toBe("HEAD"); + expect(seenIfNoneMatch).toBe('"xyz"'); + }); + it("robots.txt passthrough forwards the ORIGINAL request (conditional-GET headers preserved)", async () => { + let seenIfNoneMatch: string | null = null; + const env = makeEnv({ + ASSETS: { + fetch: async (req: Request) => { + seenIfNoneMatch = req.headers.get("if-none-match"); + return new Response("User-agent: *", { headers: { "content-type": "text/plain" } }); + }, + }, + }); + const r = await worker.fetch( + new Request("https://docs-next.vyos.io/robots.txt", { + headers: { "user-agent": "vitest", "if-none-match": '"abc123"' }, + }), + env, + ); + expect(r.status).toBe(200); + expect(seenIfNoneMatch).toBe('"abc123"'); + }); + + it("UA gate in production tolerates a missing User-Agent header (no crash, fail-open)", async () => { + const prodEnv = makeEnv({ DOCS_ENV: "production" }); + const r = await worker.fetch( + new Request("https://docs-next.vyos.io/en/rolling/", { headers: {} }), + prodEnv, + ); + expect(r.status).toBe(200); + }); + + describe("legacy PDF R2 fallback (spec §5) — runs before version dispatch", () => { + // Fake R2Bucket mock following the preview-worker precedent (apex/preview/test/preview.test.ts). + // Simulates R2's onlyIf (If-None-Match → body-less R2Object) + range (echoes the + // satisfied byte range on `.range`) behavior closely enough to exercise index.ts's + // handling of both without needing the real R2 binding. + const ETAG = '"pdf-etag-1"'; + function r2Env( + objects: Record, + overrides: Record = {}, + ) { + return makeEnv({ + DOCS_PDFS: { + get: async (key: string, options?: { onlyIf?: Headers; range?: Headers }) => { + const hit = objects[key]; + if (!hit) return null; + const etag = hit.etag ?? ETAG; + const size = hit.body.length; + + const ifNoneMatch = options?.onlyIf?.get?.("if-none-match"); + if (ifNoneMatch && ifNoneMatch === etag) { + return { httpEtag: etag, size }; // R2Object, no `body` — precondition matched + } + + const rangeHeader = options?.range?.get?.("range"); + const m = rangeHeader ? /^bytes=(\d+)-(\d+)$/.exec(rangeHeader) : null; + if (m) { + const offset = Number(m[1]); + const length = Number(m[2]) - offset + 1; + return { + httpEtag: etag, + size, + body: hit.body.slice(offset, offset + length), + range: { offset, length }, + }; + } + + return { httpEtag: etag, size, body: hit.body }; + }, + } as unknown as R2Bucket, + ...overrides, + }); + } + + it("served-from-R2 200: content-type application/pdf, PDF cache class, X-Apex-Build present, etag + accept-ranges", async () => { + const env = r2Env( + { "legacy/1.3/vyos-documentation.pdf": { body: "PDF-BYTES" } }, + { DOCS_ENV: "production" }, + ); + const r = await get("/en/1.3/vyos-documentation.pdf", env); + expect(r.status).toBe(200); + expect(await r.text()).toBe("PDF-BYTES"); + expect(r.headers.get("content-type")).toBe("application/pdf"); + expect(r.headers.get("Cache-Control")).toBe("public, max-age=300, s-maxage=600, must-revalidate"); + expect(r.headers.get("X-Apex-Build")).toBe("apex-sha"); + expect(r.headers.get("etag")).toBe(ETAG); + expect(r.headers.get("accept-ranges")).toBe("bytes"); + expect(r.headers.get("content-length")).toBe("9"); + }); + + it("If-None-Match matching R2's etag → 304, no body, etag present, PDF cache class", async () => { + const env = r2Env( + { "legacy/1.3/vyos-documentation.pdf": { body: "PDF-BYTES" } }, + { DOCS_ENV: "production" }, + ); + const r = await worker.fetch( + new Request("https://docs-next.vyos.io/en/1.3/vyos-documentation.pdf", { + headers: { "user-agent": "vitest", "if-none-match": ETAG }, + }), + env, + ); + expect(r.status).toBe(304); + expect(await r.text()).toBe(""); + expect(r.headers.get("etag")).toBe(ETAG); + expect(r.headers.get("Cache-Control")).toBe("public, max-age=300, s-maxage=600, must-revalidate"); + expect(r.headers.get("content-type")).toBeNull(); + }); + + it("Range: bytes=0-3 → 206 + Content-Range + partial body, PDF cache class", async () => { + const env = r2Env( + { "legacy/1.3/vyos-documentation.pdf": { body: "PDF-BYTES" } }, // length 9 + { DOCS_ENV: "production" }, + ); + const r = await worker.fetch( + new Request("https://docs-next.vyos.io/en/1.3/vyos-documentation.pdf", { + headers: { "user-agent": "vitest", range: "bytes=0-3" }, + }), + env, + ); + expect(r.status).toBe(206); + expect(await r.text()).toBe("PDF-"); + expect(r.headers.get("content-range")).toBe("bytes 0-3/9"); + expect(r.headers.get("content-length")).toBe("4"); + expect(r.headers.get("etag")).toBe(ETAG); + expect(r.headers.get("Cache-Control")).toBe("public, max-age=300, s-maxage=600, must-revalidate"); + }); + + it("canary env still forces no-store on the PDF response (canary rule wins)", async () => { + const env = r2Env({ "legacy/1.3/vyos-documentation.pdf": { body: "PDF-BYTES" } }); // default DOCS_ENV: canary + const r = await get("/en/1.3/vyos-documentation.pdf", env); + expect(r.status).toBe(200); + expect(r.headers.get("Cache-Control")).toBe("no-store"); + }); + + it("R2 miss → themed 404, no-store, never falls through to DOCS_LEGACY dispatch", async () => { + const env = r2Env({}, { DOCS_ENV: "production" }); // bucket present but empty + const r = await get("/en/1.3/vyos-documentation.pdf", env); + expect(r.status).toBe(404); + expect(r.headers.get("Cache-Control")).toBe("no-store"); + expect(await r.text()).not.toContain("legacy:"); // not the DOCS_LEGACY fetcher's echoed tag + }); + + it("R2 get() throwing → themed 503, no-store", async () => { + const env = makeEnv({ + DOCS_ENV: "production", + DOCS_PDFS: { get: async () => { throw new Error("R2 unavailable"); } } as unknown as R2Bucket, + }); + const r = await get("/en/1.3/vyos-documentation.pdf", env); + expect(r.status).toBe(503); + expect(r.headers.get("Cache-Control")).toBe("no-store"); + }); + + it("DOCS_PDFS binding missing → themed 503, not a crash", async () => { + const env = makeEnv({ DOCS_ENV: "production" }); // no DOCS_PDFS at all + const r = await get("/en/1.3/vyos-documentation.pdf", env); + expect(r.status).toBe(503); + }); + + it("other versions' PDF paths never touch DOCS_PDFS — fall through to normal dispatch", async () => { + // env carries a DOCS_PDFS bucket that would 500 if queried at all, proving the + // rolling/1.5/1.4 PDF paths (no pdf_r2_key on those manifest entries) skip it entirely. + const env = r2Env( + {}, + { + DOCS_ENV: "production", + DOCS_PDFS: { get: async () => { throw new Error("must not be called for non-1.3 PDFs"); } } as unknown as R2Bucket, + }, + ); + const r = await get("/en/rolling/vyos-documentation.pdf", env); + expect(r.status).toBe(200); + expect(await r.text()).toBe("rolling:/en/rolling/vyos-documentation.pdf"); + }); + + it("1.2 (pdf: null, no pdf_r2_key) falls through to normal dispatch unaffected", async () => { + const env = r2Env({}, { DOCS_ENV: "production" }); + const r = await get("/en/1.2/vyos-documentation.pdf", env); + expect(r.status).toBe(200); + expect(await r.text()).toBe("legacy:/en/1.2/vyos-documentation.pdf"); + }); + }); +}); diff --git a/workers/apex/test/uagate.test.ts b/workers/apex/test/uagate.test.ts new file mode 100644 index 00000000..f4c16c66 --- /dev/null +++ b/workers/apex/test/uagate.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { uaVerdict } from "../src/uagate"; +import policy from "../ua-policy.json"; + +describe("UA gate (§3.2.1) — ships log-only for AI crawlers", () => { + it("search engines always allowed", () => { + expect(uaVerdict("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", policy)).toBe("allow"); + expect(uaVerdict("Mozilla/5.0 (compatible; bingbot/2.0)", policy)).toBe("allow"); + }); + it("AI-training crawlers are log-only initially", () => { + expect(uaVerdict("GPTBot/1.0", policy)).toBe("log"); + expect(uaVerdict("CCBot/2.0", policy)).toBe("log"); + }); + it("named abusers blocked", () => { + expect(uaVerdict("EvilScraper/0.1", { ...policy, block: ["EvilScraper"] })).toBe("block"); + }); + it("unknown UA → allow (fail-open for humans)", () => { + expect(uaVerdict("Mozilla/5.0 (X11; Linux x86_64) Firefox/128.0", policy)).toBe("allow"); + }); + it("block takes precedence over allow on a UA matching both lists", () => { + const dualMatch = { ...policy, allow: ["Googlebot"], block: ["Googlebot EvilScraper"] }; + expect(uaVerdict("Mozilla/5.0 (compatible; Googlebot EvilScraper/1.0)", dualMatch)).toBe("block"); + }); +}); diff --git a/workers/apex/ua-policy.json b/workers/apex/ua-policy.json new file mode 100644 index 00000000..4a3534eb --- /dev/null +++ b/workers/apex/ua-policy.json @@ -0,0 +1,5 @@ +{ + "allow": ["Googlebot", "bingbot", "DuckDuckBot", "YandexBot", "Applebot", "UptimeRobot"], + "log": ["GPTBot", "CCBot", "ClaudeBot", "Google-Extended", "Bytespider", "PerplexityBot", "meta-externalagent"], + "block": [] +} diff --git a/workers/apex/wrangler.jsonc b/workers/apex/wrangler.jsonc new file mode 100644 index 00000000..71c831f3 --- /dev/null +++ b/workers/apex/wrangler.jsonc @@ -0,0 +1,39 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "vyos-docs-apex", + "main": "src/index.ts", + "compatibility_date": "2026-07-01", + "workers_dev": false, + "preview_urls": false, + "assets": { "directory": "assets", "binding": "ASSETS", "run_worker_first": true }, + "env": { + "canary": { + // Routes are MANAGED MANUALLY (v4.2 amendment): the account-owned CI token has no + // Workers Routes group, and — decisive — docs.vyos.io is ALREADY orange-cloud, so a + // config-managed production route would capture live traffic on first env deploy. + // Canary route (docs-next.vyos.io/* → this worker, env canary) is created once by the + // operator/dashboard (Task 3.6 step 2c); the production route IS the cutover (Task 6.2). + "vars": { "APEX_BUILD_SHA": "dev", "DOCS_ENV": "canary" }, + "services": [ + { "binding": "DOCS_ROLLING", "service": "vyos-docs-rolling-en-candidate" }, + { "binding": "DOCS_V15", "service": "vyos-docs-v15-en-candidate" }, + { "binding": "DOCS_V14", "service": "vyos-docs-v14-en-candidate" }, + { "binding": "DOCS_LEGACY", "service": "vyos-docs-legacy-candidate" } + ], + // §5 apex PDF fallback: same bucket as production — there's only one copy of the + // (single, immutable) oversized 1.3 PDF artifact, not a per-env candidate/live pair. + "r2_buckets": [{ "binding": "DOCS_PDFS", "bucket_name": "vyos-docs-artifacts" }] + }, + "production": { + // production route deliberately ABSENT — created manually at cutover (Task 6.2); see canary note above + "vars": { "APEX_BUILD_SHA": "dev", "DOCS_ENV": "production" }, + "services": [ + { "binding": "DOCS_ROLLING", "service": "vyos-docs-rolling-en" }, + { "binding": "DOCS_V15", "service": "vyos-docs-v15-en" }, + { "binding": "DOCS_V14", "service": "vyos-docs-v14-en" }, + { "binding": "DOCS_LEGACY", "service": "vyos-docs-legacy" } + ], + "r2_buckets": [{ "binding": "DOCS_PDFS", "bucket_name": "vyos-docs-artifacts" }] + } + } +} diff --git a/workers/bootstrap.sh b/workers/bootstrap.sh new file mode 100755 index 00000000..a7454f9f --- /dev/null +++ b/workers/bootstrap.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# One-time bootstrap: deploy placeholder versions of every service-binding +# target so the apex Worker (whose config binds all of them) can deploy. +# Requires CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID in the environment. +set -euo pipefail +cd "$(dirname "$0")" + +mkdir -p ../dist/assets +for slug in rolling 1.5 1.4 1.3 1.2; do + mkdir -p "../dist/assets/en/$slug" + printf 'bootstrap placeholder — real build pending' \ + > "../dist/assets/en/$slug/index.html" +done + +deploy() { # config-suffix worker-name + for suffix in "" "-candidate"; do + npx wrangler deploy --config "branch/wrangler.$1.jsonc" \ + --name "$2$suffix" \ + --var DOCS_BUILD_SHA:bootstrap \ + --var DOCS_ENV:$( [ -n "$suffix" ] && echo canary || echo production ) + done +} + +npm ci +deploy rolling vyos-docs-rolling-en +deploy v15 vyos-docs-v15-en +deploy v14 vyos-docs-v14-en +deploy legacy vyos-docs-legacy +echo "bootstrap complete — all 8 binding targets exist; apex can now deploy" diff --git a/workers/branch/src/index.ts b/workers/branch/src/index.ts new file mode 100644 index 00000000..d955d166 --- /dev/null +++ b/workers/branch/src/index.ts @@ -0,0 +1,44 @@ +export interface Env { + ASSETS: Fetcher; // static assets binding + DOCS_BUILD_SHA: string; // injected at deploy + DOCS_ENV: "production" | "canary"; +} + +export type CacheClass = "page" | "asset"; + +export function classifyPath(path: string): CacheClass { + if (path.endsWith(".pdf") || path.includes("/_static/")) return "asset"; + return "page"; // HTML, versions.json, sitemaps, robots/llms, pagefind index +} + +export function cacheHeaderFor(cls: CacheClass): string { + return cls === "asset" + ? "public, max-age=300, s-maxage=600, must-revalidate" + : "public, max-age=0, s-maxage=300, must-revalidate"; +} + +export function withDocsHeaders( + resp: Response, + path: string, + env: Pick, +): Response { + const out = new Response(resp.body, resp); + out.headers.set("X-Docs-Build", env.DOCS_BUILD_SHA); + out.headers.set( + "Cache-Control", + // Error responses (4xx/5xx) must never carry the page/asset cache class — a + // cached 404 would poison the edge for the full s-maxage window. + env.DOCS_ENV === "canary" || out.status >= 400 + ? "no-store" + : cacheHeaderFor(classifyPath(path)), + ); + return out; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + const resp = await env.ASSETS.fetch(request); + return withDocsHeaders(resp, url.pathname, env); + }, +} satisfies ExportedHandler; diff --git a/workers/branch/test/content.test.ts b/workers/branch/test/content.test.ts new file mode 100644 index 00000000..fcf068da --- /dev/null +++ b/workers/branch/test/content.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from "vitest"; +import worker, { classifyPath, cacheHeaderFor, withDocsHeaders, type Env } from "../src/index"; + +describe("cache classes (§3.3)", () => { + it("HTML + config class → max-age=0, s-maxage=300", () => { + for (const p of ["/en/rolling/index.html", "/en/rolling/versions.json", + "/en/rolling/sitemap.xml", "/en/rolling/pagefind/pagefind.js"]) { + expect(cacheHeaderFor(classifyPath(p))) + .toBe("public, max-age=0, s-maxage=300, must-revalidate"); + } + }); + it("PDF + _static class → max-age=300, s-maxage=600", () => { + for (const p of ["/en/rolling/vyos-documentation.pdf", "/en/rolling/_static/css/theme.css"]) { + expect(cacheHeaderFor(classifyPath(p))) + .toBe("public, max-age=300, s-maxage=600, must-revalidate"); + } + }); +}); + +describe("response headers", () => { + it("adds X-Docs-Build and cache-control; canary forces no-store", () => { + const base = new Response("ok", { headers: { "content-type": "text/html" } }); + const prod = withDocsHeaders(base, "/en/rolling/index.html", + { DOCS_BUILD_SHA: "abc123", DOCS_ENV: "production" }); + expect(prod.headers.get("X-Docs-Build")).toBe("abc123"); + expect(prod.headers.get("Cache-Control")).toBe("public, max-age=0, s-maxage=300, must-revalidate"); + const canary = withDocsHeaders(base, "/en/rolling/index.html", + { DOCS_BUILD_SHA: "abc123", DOCS_ENV: "canary" }); + expect(canary.headers.get("Cache-Control")).toBe("no-store"); + }); +}); + +describe("default fetch entrypoint", () => { + const makeEnv = (docsEnv: Env["DOCS_ENV"], seen: string[]): Env => ({ + ASSETS: { + fetch: async (req: Request) => { + seen.push(req.url); + return new Response("", { headers: { "content-type": "text/html" } }); + }, + } as unknown as Fetcher, + DOCS_BUILD_SHA: "testsha", + DOCS_ENV: docsEnv, + }); + + it("serves assets verbatim with docs headers; path is byte-stable", async () => { + const seen: string[] = []; + const url = "https://docs.vyos.io/en/rolling/index.html"; + const resp = await worker.fetch(new Request(url), makeEnv("production", seen)); + expect(resp.headers.get("X-Docs-Build")).toBe("testsha"); + expect(resp.headers.get("Cache-Control")) + .toBe("public, max-age=0, s-maxage=300, must-revalidate"); + expect(seen).toEqual([url]); // original request URL reached ASSETS unmodified + }); + + it("canary env forces no-store on the fetch path too", async () => { + const seen: string[] = []; + const resp = await worker.fetch( + new Request("https://docs.vyos.io/en/rolling/index.html"), + makeEnv("canary", seen), + ); + expect(resp.headers.get("Cache-Control")).toBe("no-store"); + expect(resp.headers.get("X-Docs-Build")).toBe("testsha"); + }); + + it("4xx/5xx responses are never cached, even in production", async () => { + const env: Env = { + ASSETS: { + fetch: async () => new Response("nope", { status: 404, headers: { "content-type": "text/html" } }), + } as unknown as Fetcher, + DOCS_BUILD_SHA: "testsha", + DOCS_ENV: "production", + }; + const resp = await worker.fetch( + new Request("https://docs.vyos.io/en/rolling/missing.html"), + env, + ); + expect(resp.status).toBe(404); + expect(resp.headers.get("Cache-Control")).toBe("no-store"); + }); +}); diff --git a/workers/branch/wrangler.legacy.jsonc b/workers/branch/wrangler.legacy.jsonc new file mode 100644 index 00000000..fd036ec5 --- /dev/null +++ b/workers/branch/wrangler.legacy.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "vyos-docs-legacy", + "main": "src/index.ts", + "compatibility_date": "2026-07-01", + "workers_dev": false, + "preview_urls": false, + "assets": { + "directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3 + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page", + "run_worker_first": true + }, + "vars": { "DOCS_BUILD_SHA": "dev", "DOCS_ENV": "production" } +} diff --git a/workers/branch/wrangler.rolling.jsonc b/workers/branch/wrangler.rolling.jsonc new file mode 100644 index 00000000..4595a06a --- /dev/null +++ b/workers/branch/wrangler.rolling.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "vyos-docs-rolling-en", + "main": "src/index.ts", + "compatibility_date": "2026-07-01", + "workers_dev": false, + "preview_urls": false, + "assets": { + "directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3 + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page", + "run_worker_first": true + }, + "vars": { "DOCS_BUILD_SHA": "dev", "DOCS_ENV": "production" } +} diff --git a/workers/branch/wrangler.v14.jsonc b/workers/branch/wrangler.v14.jsonc new file mode 100644 index 00000000..2c7310aa --- /dev/null +++ b/workers/branch/wrangler.v14.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "vyos-docs-v14-en", + "main": "src/index.ts", + "compatibility_date": "2026-07-01", + "workers_dev": false, + "preview_urls": false, + "assets": { + "directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3 + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page", + "run_worker_first": true + }, + "vars": { "DOCS_BUILD_SHA": "dev", "DOCS_ENV": "production" } +} diff --git a/workers/branch/wrangler.v15.jsonc b/workers/branch/wrangler.v15.jsonc new file mode 100644 index 00000000..45098aee --- /dev/null +++ b/workers/branch/wrangler.v15.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "vyos-docs-v15-en", + "main": "src/index.ts", + "compatibility_date": "2026-07-01", + "workers_dev": false, + "preview_urls": false, + "assets": { + "directory": "../../dist/assets", // populated by CI (Task 3.2); html_handling per §3.2.3 + "binding": "ASSETS", + "html_handling": "auto-trailing-slash", + "not_found_handling": "404-page", + "run_worker_first": true + }, + "vars": { "DOCS_BUILD_SHA": "dev", "DOCS_ENV": "production" } +} diff --git a/workers/matrix.json b/workers/matrix.json new file mode 100644 index 00000000..31ece863 --- /dev/null +++ b/workers/matrix.json @@ -0,0 +1,5 @@ +{ + "rolling": { "worker": "vyos-docs-rolling-en", "slug": "rolling" }, + "circinus": { "worker": "vyos-docs-v15-en", "slug": "1.5" }, + "sagitta": { "worker": "vyos-docs-v14-en", "slug": "1.4" } +} diff --git a/workers/package-lock.json b/workers/package-lock.json new file mode 100644 index 00000000..3840ab19 --- /dev/null +++ b/workers/package-lock.json @@ -0,0 +1,3921 @@ +{ + "name": "vyos-docs-workers", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vyos-docs-workers", + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.8.19", + "typescript": "^5.5.0", + "vitest": "~3.2.0", + "wrangler": "^4.34.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/vitest-pool-workers": { + "version": "0.8.71", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.71.tgz", + "integrity": "sha512-keu2HCLQfRNwbmLBCDXJgCFpANTaYnQpE01fBOo4CNwiWHUT7SZGN7w64RKiSWRHyYppStXBuE5Ng7F42+flpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "0.2.14", + "cjs-module-lexer": "^1.2.3", + "devalue": "^5.3.2", + "miniflare": "4.20250906.0", + "semver": "^7.7.1", + "wrangler": "4.35.0", + "zod": "^3.22.3" + }, + "peerDependencies": { + "@vitest/runner": "2.0.x - 3.2.x", + "@vitest/snapshot": "2.0.x - 3.2.x", + "vitest": "2.0.x - 3.2.x" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@cloudflare/unenv-preset": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.3.tgz", + "integrity": "sha512-tsQQagBKjvpd9baa6nWVIv399ejiqcrUBBW6SZx6Z22+ymm+Odv5+cFimyuCsD/fC1fQTwfRmwXBNpzvHSeGCw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.21", + "workerd": "^1.20250828.1" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/unenv": { + "version": "2.0.0-rc.21", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz", + "integrity": "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.7", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "ufo": "^1.6.1" + } + }, + "node_modules/@cloudflare/vitest-pool-workers/node_modules/wrangler": { + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.35.0.tgz", + "integrity": "sha512-HbyXtbrh4Fi3mU8ussY85tVdQ74qpVS1vctUgaPc+bPrXBTqfDLkZ6VRtHAVF/eBhz4SFmhJtCQpN1caY2Ak8A==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.0", + "@cloudflare/unenv-preset": "2.7.3", + "blake3-wasm": "2.1.5", + "esbuild": "0.25.4", + "miniflare": "4.20250906.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.21", + "workerd": "1.20250906.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250906.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250906.0.tgz", + "integrity": "sha512-E+X/YYH9BmX0ew2j/mAWFif2z05NMNuhCTlNYEGLkqMe99K15UewBqajL9pMcMUKxylnlrEoK3VNxl33DkbnPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250906.0.tgz", + "integrity": "sha512-X5apsZ1SFW4FYTM19ISHf8005FJMPfrcf4U5rO0tdj+TeJgQgXuZ57IG0WeW7SpLVeBo8hM6WC8CovZh41AfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250906.0.tgz", + "integrity": "sha512-rlKzWgsLnlQ5Nt9W69YBJKcmTmZbOGu0edUsenXPmc6wzULUxoQpi7ZE9k3TfTonJx4WoQsQlzCUamRYFsX+0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250906.0.tgz", + "integrity": "sha512-DdedhiQ+SeLzpg7BpcLrIPEZ33QKioJQ1wvL4X7nuLzEB9rWzS37NNNahQzc1+44rhG4fyiHbXBPOeox4B9XVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250906.0.tgz", + "integrity": "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/birpc": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", + "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", + "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "4.20250906.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250906.0.tgz", + "integrity": "sha512-T/RWn1sa0ien80s6NjU+Un/tj12gR6wqScZoiLeMJDD4/fK0UXfnbWXJDubnUED8Xjm7RPQ5ESYdE+mhPmMtuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", + "stoppable": "1.1.0", + "undici": "^7.10.0", + "workerd": "1.20250906.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/miniflare/node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20250906.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250906.0.tgz", + "integrity": "sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250906.0", + "@cloudflare/workerd-darwin-arm64": "1.20250906.0", + "@cloudflare/workerd-linux-64": "1.20250906.0", + "@cloudflare/workerd-linux-arm64": "1.20250906.0", + "@cloudflare/workerd-windows-64": "1.20250906.0" + } + }, + "node_modules/wrangler": { + "version": "4.109.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.109.0.tgz", + "integrity": "sha512-Ev6iIBNnRwvYeJv8B7377YsnY+PxEznHmz3r33cWgP+6A5HNqqqJhChBaVUWxxs0uQPKuYBy8tqrfyd7o8WmBg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "4.20260708.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260708.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260708.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260708.1.tgz", + "integrity": "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260708.1.tgz", + "integrity": "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260708.1.tgz", + "integrity": "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260708.1.tgz", + "integrity": "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260708.1.tgz", + "integrity": "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/miniflare": { + "version": "4.20260708.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.0.tgz", + "integrity": "sha512-ut32kKKTyZIoXhbtteDAMOwTVx9aCeehMeKb1xmJs4RLdBrF5flSZD1MOpWqmJycIV7YBHjcaZcVHLMr/g5RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.28.0", + "workerd": "1.20260708.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/wrangler/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20260708.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260708.1.tgz", + "integrity": "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260708.1", + "@cloudflare/workerd-darwin-arm64": "1.20260708.1", + "@cloudflare/workerd-linux-64": "1.20260708.1", + "@cloudflare/workerd-linux-arm64": "1.20260708.1", + "@cloudflare/workerd-windows-64": "1.20260708.1" + } + }, + "node_modules/wrangler/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/workers/package.json b/workers/package.json new file mode 100644 index 00000000..65082cb0 --- /dev/null +++ b/workers/package.json @@ -0,0 +1,16 @@ +{ + "name": "vyos-docs-workers", + "private": true, + "type": "module", + "engines": { "node": ">=20" }, + "scripts": { + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.8.19", + "typescript": "^5.5.0", + "vitest": "~3.2.0", + "wrangler": "^4.34.0" + } +} diff --git a/workers/picker-test/pagefind-wrapper.test.ts b/workers/picker-test/pagefind-wrapper.test.ts new file mode 100644 index 00000000..75e8be50 --- /dev/null +++ b/workers/picker-test/pagefind-wrapper.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from "vitest"; +// The workers pool has no real filesystem (node:fs readFileSync is an unimplemented +// stub — see @cloudflare/vitest-pool-workers/dist/worker/lib/node/fs.mjs, and confirmed +// empirically here: "readFileSync() is not yet implemented in Workers"). Same constraint +// documented in picker.test.ts and apex/test/manifest.test.ts; import as Vite `?raw` asset +// instead so content is inlined at bundle time — no runtime filesystem access needed. +// eslint-disable-next-line import/no-unresolved +import src from "../../docs/_static/js/pagefind-wrapper.js?raw"; + +const ns: Record = {}; +new Function("window", src)(ns as never); +const W = (ns as never as { VyOSSearch: Record }).VyOSSearch; + +describe("basePathFor (§9)", () => { + it("production/canary version path", () => { + expect(W.basePathFor("/en/rolling/search.html")) + .toEqual({ base: "/en/rolling/", prefix: "" }); + }); + it("PR preview path keeps /pr-/ prefix and reports it", () => { + expect(W.basePathFor("/pr-42/en/1.5/search.html")) + .toEqual({ base: "/pr-42/en/1.5/", prefix: "/pr-42" }); + }); + it("prefixes result URLs in previews", () => { + expect(W.prefixResultUrl("/en/1.5/cli/index.html", "/pr-42")).toBe("/pr-42/en/1.5/cli/index.html"); + expect(W.prefixResultUrl("/en/1.5/cli/index.html", "")).toBe("/en/1.5/cli/index.html"); + }); +}); + +describe("assetUrlsFor (css + js pair under the version base)", () => { + it("production/canary base", () => { + expect(W.assetUrlsFor("/en/rolling/")).toEqual({ + css: "/en/rolling/pagefind/pagefind-ui.css", + js: "/en/rolling/pagefind/pagefind-ui.js", + }); + }); + it("PR preview base keeps the /pr-/ prefix", () => { + expect(W.assetUrlsFor("/pr-42/en/1.5/")).toEqual({ + css: "/pr-42/en/1.5/pagefind/pagefind-ui.css", + js: "/pr-42/en/1.5/pagefind/pagefind-ui.js", + }); + }); +}); diff --git a/workers/picker-test/picker.test.ts b/workers/picker-test/picker.test.ts new file mode 100644 index 00000000..33ae2264 --- /dev/null +++ b/workers/picker-test/picker.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +// The workers pool has no real filesystem (node:fs readFileSync is an unimplemented +// stub — see @cloudflare/vitest-pool-workers/dist/worker/lib/node/fs.mjs, and confirmed +// empirically here: "readFileSync() is not yet implemented in Workers"). Same constraint +// documented in apex/test/manifest.test.ts; import as Vite `?raw` / native JSON assets +// instead so content is inlined at bundle time — no runtime filesystem access needed. +// eslint-disable-next-line import/no-unresolved +import src from "../../docs/_static/js/version-picker.js?raw"; +// eslint-disable-next-line import/no-unresolved +import manifest from "../versions.json"; + +// Evaluate the plain script and grab its namespace (no DOM access at module scope allowed). +const ns: Record = {}; +new Function("window", src)(ns as never); +const P = (ns as never as { VyOSVersionPicker: Record }).VyOSVersionPicker; + +describe("parseLocation", () => { + it("extracts lang/slug/rest from a docs path", () => { + expect(P.parseLocation("/en/1.5/cli/index.html")) + .toEqual({ lang: "en", slug: "1.5", rest: "cli/index.html" }); + }); + it("returns null off the version tree (e.g. previews without prefix knowledge)", () => { + expect(P.parseLocation("/kb/x")).toBeNull(); + }); +}); + +describe("bannerFor (§4)", () => { + it("dev → info banner", () => { + expect(P.bannerFor("rolling", manifest)).toMatchObject({ kind: "dev" }); + }); + it("newest lts → no banner; older lts → newer-lts notice naming 1.5", () => { + expect(P.bannerFor("1.5", manifest)).toBeNull(); + expect(P.bannerFor("1.4", manifest)).toMatchObject({ kind: "newer-lts", newest: "1.5" }); + }); + it("eol → warning linking newest LTS", () => { + expect(P.bannerFor("1.3", manifest)).toMatchObject({ kind: "eol", newest: "1.5" }); + }); +}); + +describe("targetUrlFor", () => { + it("same path on target version", () => { + expect(P.targetUrlFor({ lang: "en", slug: "1.5", rest: "cli/index.html" }, "1.4")) + .toBe("/en/1.4/cli/index.html"); + }); +}); + +describe("navUrlFor (query + fragment preserved across version switch)", () => { + const loc = { lang: "en", slug: "1.4", rest: "quick-start.html" }; + it("neither → bare target path", () => { + expect(P.navUrlFor(loc, "1.5", "", "")).toBe("/en/1.5/quick-start.html"); + }); + it("query-only", () => { + expect(P.navUrlFor(loc, "1.5", "?ref=x", "")) + .toBe("/en/1.5/quick-start.html?ref=x"); + }); + it("hash-only", () => { + expect(P.navUrlFor(loc, "1.5", "", "#section-3")) + .toBe("/en/1.5/quick-start.html#section-3"); + }); + it("both, in query-then-hash order", () => { + expect(P.navUrlFor(loc, "1.5", "?ref=x", "#section-3")) + .toBe("/en/1.5/quick-start.html?ref=x#section-3"); + }); +}); diff --git a/workers/preview/src/index.ts b/workers/preview/src/index.ts new file mode 100644 index 00000000..5c945175 --- /dev/null +++ b/workers/preview/src/index.ts @@ -0,0 +1,61 @@ +export interface Env { PREVIEWS: R2Bucket } + +const MIME: Record = { + html: "text/html; charset=utf-8", css: "text/css", js: "text/javascript", + json: "application/json", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", + gif: "image/gif", ico: "image/x-icon", txt: "text/plain; charset=utf-8", + xml: "application/xml", pdf: "application/pdf", woff2: "font/woff2", woff: "font/woff", +}; + +export function mimeFor(key: string): string { + const ext = key.split(".").pop() ?? ""; + return MIME[ext] ?? "application/octet-stream"; +} + +export function keyFor(pathname: string): string { + let key = pathname.replace(/^\//, ""); + if (key.endsWith("/") || key === "" ) key += "index.html"; + return key; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const key = keyFor(new URL(request.url).pathname); + let obj: R2ObjectBody | null = null; + let resolvedKey = key; + try { + obj = await env.PREVIEWS.get(key); + if (!obj && !key.split("/").pop()?.includes(".")) { + // Extensionless directory URL with no trailing slash (e.g. /en/rolling/cli) — + // keyFor() only appends index.html for trailing-slash/empty paths, so probe the + // directory's index.html before 404ing. Check the LAST path segment only — a dot + // anywhere earlier (e.g. version segment "1.4" in /pr-42/en/1.4/cli) must not skip + // the probe for an otherwise-extensionless final segment. + resolvedKey = `${key}/index.html`; + obj = await env.PREVIEWS.get(resolvedKey); + } + } catch { + // Transient R2/binding error on either probe — fail closed with a controlled 503 + // instead of letting an unhandled exception surface as a raw worker error. + return new Response("preview temporarily unavailable", { + status: 503, + headers: { "X-Robots-Tag": "noindex", "Cache-Control": "no-store" }, + }); + } + if (!obj) { + return new Response("preview not found", { + status: 404, + // no-store on the 404 too — a cached 404 would persist past the preview upload + headers: { "X-Robots-Tag": "noindex", "Cache-Control": "no-store" }, + }); + } + return new Response(obj.body, { + headers: { + "content-type": obj.httpMetadata?.contentType ?? mimeFor(resolvedKey), + "X-Robots-Tag": "noindex", + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff", + }, + }); + }, +} satisfies ExportedHandler; diff --git a/workers/preview/test/preview.test.ts b/workers/preview/test/preview.test.ts new file mode 100644 index 00000000..90530690 --- /dev/null +++ b/workers/preview/test/preview.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect } from "vitest"; +import worker, { mimeFor, keyFor } from "../src/index"; +import type { Env } from "../src/index"; + +describe("preview worker helpers (§10)", () => { + it("derives R2 key from path, defaulting directory to index.html", () => { + expect(keyFor("/pr-42/en/rolling/")).toBe("pr-42/en/rolling/index.html"); + expect(keyFor("/pr-42/en/rolling/cli/index.html")).toBe("pr-42/en/rolling/cli/index.html"); + }); + it("extension→MIME fallback map (nosniff-safe)", () => { + expect(mimeFor("a.html")).toBe("text/html; charset=utf-8"); + expect(mimeFor("a.css")).toBe("text/css"); + expect(mimeFor("a.js")).toBe("text/javascript"); + expect(mimeFor("a.json")).toBe("application/json"); + expect(mimeFor("a.svg")).toBe("image/svg+xml"); + expect(mimeFor("a.woff2")).toBe("font/woff2"); + expect(mimeFor("a.unknown")).toBe("application/octet-stream"); + }); +}); + +// Fake R2Bucket mock following the makeEnv() precedent in apex/test/router.test.ts — +// only the `get` surface the handler consumes; miniflare's real R2 not needed here. +function makeEnv(objects: Record): Env { + return { + PREVIEWS: { + get: async (key: string) => { + const hit = objects[key]; + if (!hit) return null; + return { + body: hit.body, + httpMetadata: hit.contentType ? { contentType: hit.contentType } : undefined, + }; + }, + } as unknown as R2Bucket, + }; +} + +const get = (path: string, env: Env) => + worker.fetch(new Request(`https://docs-preview.vyos.io${path}`), env); + +describe("preview worker fetch entrypoint (§10)", () => { + it("serves a found object with uploader contentType + noindex/no-store/nosniff headers", async () => { + const env = makeEnv({ + "pr-42/en/rolling/index.html": { body: "

preview

", contentType: "text/html; charset=utf-8" }, + }); + const r = await get("/pr-42/en/rolling/", env); + expect(r.status).toBe(200); + expect(await r.text()).toBe("

preview

"); + expect(r.headers.get("content-type")).toBe("text/html; charset=utf-8"); + expect(r.headers.get("X-Robots-Tag")).toBe("noindex"); + expect(r.headers.get("Cache-Control")).toBe("no-store"); + expect(r.headers.get("X-Content-Type-Options")).toBe("nosniff"); + }); + it("missing object β†’ 404 with noindex AND no-store (404s must never be cached)", async () => { + const r = await get("/pr-42/en/rolling/missing.html", makeEnv({})); + expect(r.status).toBe(404); + expect(r.headers.get("X-Robots-Tag")).toBe("noindex"); + expect(r.headers.get("Cache-Control")).toBe("no-store"); + }); + it("object without httpMetadata.contentType falls back to mimeFor(key)", async () => { + const env = makeEnv({ "pr-42/en/rolling/style.css": { body: "body{}" } }); + const r = await get("/pr-42/en/rolling/style.css", env); + expect(r.status).toBe(200); + expect(r.headers.get("content-type")).toBe("text/css"); + }); + it("extensionless directory URL with no trailing slash serves the directory's index.html", async () => { + const env = makeEnv({ + "pr-42/en/rolling/cli/index.html": { body: "

cli

", contentType: "text/html; charset=utf-8" }, + }); + const r = await get("/pr-42/en/rolling/cli", env); + expect(r.status).toBe(200); + expect(await r.text()).toBe("

cli

"); + expect(r.headers.get("content-type")).toBe("text/html; charset=utf-8"); + }); + it("extensionless directory URL with a dotted version segment earlier in the path still probes index.html (dot-check is last-segment-only)", async () => { + const env = makeEnv({ + "pr-42/en/1.4/cli/index.html": { body: "

cli 1.4

", contentType: "text/html; charset=utf-8" }, + }); + const r = await get("/pr-42/en/1.4/cli", env); + expect(r.status).toBe(200); + expect(await r.text()).toBe("

cli 1.4

"); + expect(r.headers.get("content-type")).toBe("text/html; charset=utf-8"); + }); + it("a transient R2/binding error on either probe degrades to a controlled 503 (never an unhandled exception)", async () => { + const throwingEnv: Env = { + PREVIEWS: { get: async () => { throw new Error("R2 unavailable"); } } as unknown as R2Bucket, + }; + const r = await get("/pr-42/en/rolling/cli", throwingEnv); + expect(r.status).toBe(503); + expect(r.headers.get("X-Robots-Tag")).toBe("noindex"); + expect(r.headers.get("Cache-Control")).toBe("no-store"); + }); +}); diff --git a/workers/preview/wrangler.jsonc b/workers/preview/wrangler.jsonc new file mode 100644 index 00000000..fbdb4aba --- /dev/null +++ b/workers/preview/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "vyos-docs-preview", + "main": "src/index.ts", + "compatibility_date": "2026-07-01", + "workers_dev": false, + "preview_urls": false, + // Route managed manually (v4.2 amendment β€” CI token has no Workers Routes group): + // operator creates docs-preview.vyos.io/* β†’ vyos-docs-preview once, alongside the canary route. + "r2_buckets": [{ "binding": "PREVIEWS", "bucket_name": "vyos-docs-previews" }] +} diff --git a/workers/versions.json b/workers/versions.json new file mode 100644 index 00000000..3e349bca --- /dev/null +++ b/workers/versions.json @@ -0,0 +1,19 @@ +{ + "schema_version": 2, + "default_lang": "en", + "default_version": "rolling", + "languages": [{ "code": "en", "label": "English" }], + "versions": [ + { "slug": "rolling", "label": "Rolling (development)", "status": "dev", "binding": "DOCS_ROLLING", + "aliases": ["latest"], "pdf": "/en/rolling/vyos-documentation.pdf" }, + { "slug": "1.5", "label": "1.5 (Circinus, LTS)", "status": "lts", "binding": "DOCS_V15", + "aliases": ["stable", "lts", "circinus"], "pdf": "/en/1.5/vyos-documentation.pdf" }, + { "slug": "1.4", "label": "1.4 (Sagitta, LTS)", "status": "lts", "binding": "DOCS_V14", + "aliases": ["sagitta"], "pdf": "/en/1.4/vyos-documentation.pdf" }, + { "slug": "1.3", "label": "1.3 (Equuleus, EOL)", "status": "eol", "binding": "DOCS_LEGACY", + "aliases": ["equuleus"], "pdf": "/en/1.3/vyos-documentation.pdf", + "pdf_r2_key": "legacy/1.3/vyos-documentation.pdf" }, + { "slug": "1.2", "label": "1.2 (Crux, EOL)", "status": "eol", "binding": "DOCS_LEGACY", + "aliases": ["crux"], "pdf": null } + ] +} diff --git a/workers/vitest.config.ts b/workers/vitest.config.ts new file mode 100644 index 00000000..70fd2248 --- /dev/null +++ b/workers/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; + +export default defineWorkersConfig({ + test: { + poolOptions: { + workers: { + // Each suite provides its own wrangler config via miniflare option overrides; + // apex tests read workers/apex/wrangler.jsonc to assert config congruence. + miniflare: { + compatibilityDate: "2026-07-01", + }, + }, + }, + }, +}); -- cgit v1.2.3