From 63f2369d42aee2d93a27cdcd90d7a22b9f3c8e7a Mon Sep 17 00:00:00 2001 From: Hendrik Brombeer Date: Mon, 3 Aug 2026 10:36:18 +0200 Subject: [PATCH] feat!: serve the reference from a Cloudflare Worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reference was published as an nginx container image that grounds-pulumi had to pin, deploy, and route. Hosting the same static build on a Cloudflare Worker removes that chain: the build output is uploaded with the Worker and served from Cloudflare's edge, so a release is a deploy rather than an image bump in another repository. worker/index.ts reproduces the routing contract the nginx configuration provided — base-path redirect, health endpoint, cache classes, application shell fallback, and 404 outside /docs — and worker/index.test.ts covers it. The asset server is configured to resolve exact paths only, because its HTML handling redirects unknown paths to /, which escapes the /docs base path. BREAKING CHANGE: the container image ghcr.io/groundsgg/api-reference is no longer built or published. Deployments move to the Cloudflare Worker apidocs-demo, and the release workflow needs the CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID repository secrets. --- .dockerignore | 9 - .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 67 +- .gitignore | 1 + Dockerfile | 16 - README.md | 52 +- eslint.config.js | 2 +- nginx/default.conf | 38 -- package-lock.json | 1146 +++++++++++++++++++++++++++++++-- package.json | 8 +- scripts/container-smoke.sh | 73 --- tsconfig.json | 3 +- tsconfig.worker.json | 20 + worker/index.test.ts | 103 +++ worker/index.ts | 86 +++ wrangler.jsonc | 20 + 16 files changed, 1363 insertions(+), 285 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 nginx/default.conf delete mode 100755 scripts/container-smoke.sh create mode 100644 tsconfig.worker.json create mode 100644 worker/index.test.ts create mode 100644 worker/index.ts create mode 100644 wrangler.jsonc diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 921683b..0000000 --- a/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -.git -.github -.idea -.vscode -coverage -dist -node_modules -npm-debug.log* -*.local diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ce42e2..9c76af1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,5 +45,5 @@ jobs: - name: Build run: npm run build - - name: Test container - run: bash scripts/container-smoke.sh + - name: Validate Worker bundle + run: npx wrangler deploy --dry-run --outdir "${RUNNER_TEMP}/worker" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9bce10..165c500 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,17 +13,13 @@ on: permissions: contents: read - packages: write jobs: metadata: name: Resolve release metadata runs-on: ubuntu-latest outputs: - commit: ${{ steps.metadata.outputs.commit }} ref: ${{ steps.release.outputs.ref }} - timestamp: ${{ steps.metadata.outputs.timestamp }} - version: ${{ steps.release.outputs.version }} steps: - name: Check out workflow revision uses: actions/checkout@v6 @@ -57,20 +53,8 @@ jobs: echo "ref=${ref}" >> "${GITHUB_OUTPUT}" echo "version=${version}" >> "${GITHUB_OUTPUT}" - - name: Check out release ref - uses: actions/checkout@v6 - with: - fetch-depth: 0 - ref: ${{ steps.release.outputs.ref }} - - - name: Read commit metadata - id: metadata - run: | - echo "commit=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" - echo "timestamp=$(git show --no-patch --format=%cI HEAD)" >> "${GITHUB_OUTPUT}" - - image: - name: Publish container image + worker: + name: Deploy Worker needs: metadata runs-on: ubuntu-latest steps: @@ -79,40 +63,21 @@ jobs: with: ref: ${{ needs.metadata.outputs.ref }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Log in to GHCR - uses: docker/login-action@v4 + - name: Set up Node.js + uses: actions/setup-node@v6 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + node-version: 24 + cache: npm - - name: Generate image metadata - id: image-metadata - uses: docker/metadata-action@v6 - with: - images: ghcr.io/groundsgg/api-reference - tags: | - type=raw,value=${{ needs.metadata.outputs.version }} - type=raw,value=latest - labels: | - org.opencontainers.image.created=${{ needs.metadata.outputs.timestamp }} - org.opencontainers.image.revision=${{ needs.metadata.outputs.commit }} - org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - org.opencontainers.image.title=Grounds API Reference - org.opencontainers.image.version=${{ needs.metadata.outputs.version }} + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build - - name: Build and publish image - uses: docker/build-push-action@v7 + - name: Deploy to Cloudflare + uses: cloudflare/wrangler-action@v3 with: - cache-from: type=gha - cache-to: type=gha,mode=max - context: . - platforms: linux/amd64 - provenance: mode=max - push: true - sbom: true - tags: ${{ steps.image-metadata.outputs.tags }} - labels: ${{ steps.image-metadata.outputs.labels }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy diff --git a/.gitignore b/.gitignore index 7bc4ccd..cf5e841 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ coverage/ +.wrangler/ .DS_Store .idea/ .vscode/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ae5040e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS build - -WORKDIR /app - -COPY package.json package-lock.json ./ -RUN npm ci - -COPY . . -RUN npm run build - -FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24@sha256:44e36330f74d4f3a1d4e222acca9e23b401fb87811a7597024502bb759c4dd49 - -COPY nginx/default.conf /etc/nginx/conf.d/default.conf -COPY --from=build /app/dist /usr/share/nginx/html/docs - -EXPOSE 8080 diff --git a/README.md b/README.md index f92c6be..b16bfee 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ # Grounds API Reference -This repository builds the public, central OpenAPI reference for Grounds services. It renders versioned specification snapshots with Scalar and is designed to be served at [api.grounds.gg/docs](https://api.grounds.gg/docs). +This repository builds the public, central OpenAPI reference for Grounds services. It renders versioned specification snapshots with Scalar and is deployed as a Cloudflare Worker at [apidocs-demo.grounds.workers.dev/docs](https://apidocs-demo.grounds.workers.dev/docs/). The application is self-contained: it never fetches specifications from running services and does not require service credentials at runtime. Until the first API is published, the empty registry produces a supported empty state. ## Architecture -Service repositories generate and validate their own OpenAPI documents. A reviewable integration pull request copies a released snapshot into this repository and updates the source registry. CI validates the registry and every referenced document before building the static application and its unprivileged container image. +Service repositories generate and validate their own OpenAPI documents. A reviewable integration pull request copies a released snapshot into this repository and updates the source registry. CI validates the registry and every referenced document before building the static application and the Worker that serves it. ```text -service release -> snapshot pull request -> api-reference image -> grounds-pulumi -> /docs +service release -> snapshot pull request -> api-reference build -> Cloudflare Worker -> /docs ``` +The Worker in `worker/index.ts` owns routing. Static assets are uploaded with the Worker and read through its `ASSETS` binding: the Worker redirects `/` to `/docs/`, strips the `/docs` base path before reading an asset, answers `/docs/healthz`, falls back to the application shell for client-routed paths, and returns `404` outside `/docs`. + ## Local development Requirements: - Node.js 24 - npm -- Docker, when building or testing the runtime image +- A Cloudflare account with Workers access, when deploying Install dependencies and start Vite: @@ -91,41 +93,47 @@ Live service URLs, internal hostnames, private repository URLs, credentials, and Service pipelines may automate creation of the integration pull request later, but the snapshot and registry changes remain reviewable repository changes. -## Container +## Worker -Build and run the same unprivileged image used by the release workflow: +Build the application and run the deployed Worker locally: ```shell -docker build --tag api-reference:local . -docker run --rm --publish 8080:8080 api-reference:local +npm run preview:worker ``` -Available endpoints: +Available endpoints, identical locally and in production: -- `http://localhost:8080/docs` redirects permanently to `/docs/`. -- `http://localhost:8080/docs/` serves the application. -- `http://localhost:8080/docs/specs/registry.json` serves the source registry. -- `http://localhost:8080/docs/healthz` returns `ok`. +- `/` and `/docs` redirect permanently to `/docs/`. +- `/docs/` serves the application, with `Cache-Control: no-cache`. +- `/docs/assets/…` serves hashed bundles as `public, max-age=31536000, immutable`. +- `/docs/specs/registry.json` serves the source registry as `public, max-age=300`. +- `/docs/healthz` returns `ok`. - Paths outside `/docs` return `404`. -Run `bash scripts/container-smoke.sh` to build a disposable image and verify the routing, cache headers, health endpoint, and unprivileged runtime user. +`worker/index.test.ts` covers this routing contract; CI additionally builds the Worker bundle with `wrangler deploy --dry-run`. + +Deploy manually from a Cloudflare-authenticated shell: + +```shell +npm run deploy +``` ## Releases -Release Please creates releases from Conventional Commits. Tags follow `api-reference-v` and publish these images: +Release Please creates releases from Conventional Commits. Tags follow `api-reference-v` and deploy the Worker `apidocs-demo` from the tagged revision. -```text -ghcr.io/groundsgg/api-reference: -ghcr.io/groundsgg/api-reference:latest -``` +The release workflow needs two repository secrets: + +- `CLOUDFLARE_API_TOKEN` with the `Workers Scripts: Edit` permission +- `CLOUDFLARE_ACCOUNT_ID` ## Responsibility boundaries - **Service repositories** own OpenAPI generation, service-level validation, and the source revision represented by a snapshot. -- **This repository** owns the public registry, snapshot validation, Scalar rendering, the static runtime image, and its release pipeline. -- **`grounds-pulumi`** owns the image pin, Kubernetes resources, probes, security policy, and routing `api.grounds.gg/docs` to this image. +- **This repository** owns the public registry, snapshot validation, Scalar rendering, the Worker that serves it, and its release pipeline. +- **Cloudflare** owns hosting and TLS. Custom domains and Worker routes are configured on the account, not in this repository. - **`groundsgg/docs`** owns the Mintlify navigation entry that links to the deployed API reference. -Kubernetes resources, DNS, Mintlify navigation, service-specific generation, cross-repository pull-request automation, SDK generation, AsyncAPI, and authenticated API requests are outside this repository. +DNS, Cloudflare account configuration, Mintlify navigation, service-specific generation, cross-repository pull-request automation, SDK generation, AsyncAPI, and authenticated API requests are outside this repository. Implementation is tracked in [GitHub issue #1](https://github.com/groundsgg/api-reference/issues/1) and the [API01.1 implementation plan](https://grounds.atlassian.net/wiki/spaces/gkd/pages/242909186). diff --git a/eslint.config.js b/eslint.config.js index 13c88c6..64c1428 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,7 @@ import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ["dist", "coverage", "node_modules"] }, + { ignores: [".wrangler", "dist", "coverage", "node_modules"] }, ...tseslint.configs.recommended, { files: ["**/*.{ts,tsx}"], diff --git a/nginx/default.conf b/nginx/default.conf deleted file mode 100644 index 740565c..0000000 --- a/nginx/default.conf +++ /dev/null @@ -1,38 +0,0 @@ -server { - listen 8080; - listen [::]:8080; - server_name _; - server_tokens off; - absolute_redirect off; - - root /usr/share/nginx/html; - - location = /docs { - return 308 /docs/; - } - - location = /docs/healthz { - access_log off; - default_type text/plain; - return 200 "ok"; - } - - location /docs/assets/ { - try_files $uri =404; - add_header Cache-Control "public, max-age=31536000, immutable" always; - } - - location /docs/specs/ { - try_files $uri =404; - add_header Cache-Control "public, max-age=300" always; - } - - location /docs/ { - try_files $uri $uri/ /docs/index.html; - add_header Cache-Control "no-cache" always; - } - - location / { - return 404; - } -} diff --git a/package-lock.json b/package-lock.json index d0f1a7d..45290d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "zod": "4.4.3" }, "devDependencies": { + "@cloudflare/workers-types": "5.20260801.1", "@scalar/openapi-parser": "0.28.8", "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", @@ -31,7 +32,8 @@ "typescript": "6.0.3", "typescript-eslint": "8.65.0", "vite": "8.1.5", - "vitest": "4.1.10" + "vitest": "4.1.10", + "wrangler": "4.118.0" }, "engines": { "node": ">=24.0.0 <25.0.0" @@ -437,6 +439,124 @@ "specificity": "bin/cli.js" } }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.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/@cloudflare/workerd-darwin-64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260730.1.tgz", + "integrity": "sha512-+MBHmPaiTe2KajryW0T24rZvWFxb41hD3d8anNzQqHzft6vSEb18+sp0znSwxgij7ApPhSM1+vhkNg4f3YMguA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260730.1.tgz", + "integrity": "sha512-SBHKntPkKvNPgaCrTe99xC1CAl8ygJDzlYfK0LbuJ1muKadIw35WnhO0wu894fKBtllsVQdNzDLee+cm0ppLSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260730.1.tgz", + "integrity": "sha512-ouyPOSMbiKPeSwUJUvxtMcxGAXs2J4aPE4T5ABIYX5ClcQx5j5bbHTmnqOQEY8sAuLTPjH7dY+iB6UI5ISlwwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260730.1.tgz", + "integrity": "sha512-YQ+Mi78U3TPdgBPtwq+Sm6rJU+Ihl2y0pjYtuuKkdmUbYzL7oLR6Xqq9wljhasnuCFICssDJaqhMep5WizYoEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260730.1.tgz", + "integrity": "sha512-27fAN+vUECW1oYVc1KOcHYpkL8COM2Uxtxql7TL595kxbjoqS5yckw7NLz7bTf2pALFCZWjqXDjZGJ/xbG4ZKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "5.20260801.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260801.1.tgz", + "integrity": "sha512-XCv5xWi47WQOK0LpLa6997Mrpz8Ct+nZmp/M5Xp8Z4BFsarf7nYjkznGOcOoYK5m1GfbMFEEuQ2OIZnbIWoe9A==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, "node_modules/@codemirror/autocomplete": { "version": "6.20.3", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", @@ -591,6 +711,30 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", @@ -1430,112 +1574,687 @@ "vue-demi-switch": "bin/vue-demi-switch.js" }, "engines": { - "node": ">=12" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@headlessui/tailwindcss": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz", + "integrity": "sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "tailwindcss": "^3.0 || ^4.0" + } + }, + "node_modules/@headlessui/vue": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", + "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", + "license": "MIT", + "dependencies": { + "@tanstack/vue-virtual": "^3.0.0-beta.60" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", + "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", + "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", + "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", + "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", + "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", + "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", + "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", + "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", + "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", + "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", + "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", + "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", + "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", + "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", + "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", + "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", + "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", + "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", + "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" } }, - "node_modules/@headlessui/tailwindcss": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz", - "integrity": "sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw==", - "license": "MIT", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", + "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=20.9.0" }, - "peerDependencies": { - "tailwindcss": "^3.0 || ^4.0" - } - }, - "node_modules/@headlessui/vue": { - "version": "1.7.23", - "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", - "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", - "license": "MIT", - "dependencies": { - "@tanstack/vue-virtual": "^3.0.0-beta.60" - }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependencies": { - "vue": "^3.2.0" + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.1" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@img/sharp-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", + "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", "dev": true, - "license": "Apache-2.0", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "@humanfs/types": "^0.15.0" + "@emnapi/runtime": "^1.11.1" }, "engines": { - "node": ">=18.18.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", + "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "Apache-2.0", + "optional": true, "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" + "@img/sharp-wasm32": "0.35.2" }, "engines": { - "node": ">=18.18.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", + "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", + "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" + "node": "^20.9.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", + "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": ">=20.9.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, "node_modules/@internationalized/date": { @@ -1745,6 +2464,35 @@ "integrity": "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==", "license": "MIT" }, + "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/@replit/codemirror-css-color-picker": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@replit/codemirror-css-color-picker/-/codemirror-css-color-picker-6.3.0.tgz", @@ -2570,6 +3318,26 @@ "node": ">=22" } }, + "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.23", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.23.tgz", + "integrity": "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3698,6 +4466,13 @@ "require-from-string": "^2.0.2" } }, + "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/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", @@ -3853,6 +4628,20 @@ "dev": true, "license": "MIT" }, + "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/crelt": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", @@ -4032,6 +4821,16 @@ "dev": true, "license": "ISC" }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-module-lexer": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", @@ -5274,6 +6073,16 @@ "json-buffer": "3.0.1" } }, + "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/leven": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-4.1.0.tgz", @@ -6483,6 +7292,24 @@ "node": ">=4" } }, + "node_modules/miniflare": { + "version": "5.20260730.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260730.0-alpha.tgz", + "integrity": "sha512-8/dspSXDshP6nSkCpjKO7BYc2qZoYSXm7iM+QxY7qJyJpAB3onnQSaiu0cvKJlfuMGwULl55hG69FJCcCMXU1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.28.0", + "workerd": "1.20260730.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6679,6 +7506,13 @@ "node": ">=8" } }, + "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", @@ -7208,6 +8042,51 @@ "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", "license": "MIT" }, + "node_modules/sharp": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7357,6 +8236,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/swrv": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.2.0.tgz", @@ -7698,6 +8590,16 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, "node_modules/unhead": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.16.tgz", @@ -8244,6 +9146,85 @@ "node": ">=0.10.0" } }, + "node_modules/workerd": { + "version": "1.20260730.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260730.1.tgz", + "integrity": "sha512-zmfNIjwYSWFY5chGBOjWtH3xAE7p97FTC6vR4Ep98290ho6AeAR/NVcBD274YCLEUYzqm8yxdtZlxMybU8a3jA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260730.1", + "@cloudflare/workerd-darwin-arm64": "1.20260730.1", + "@cloudflare/workerd-linux-64": "1.20260730.1", + "@cloudflare/workerd-linux-arm64": "1.20260730.1", + "@cloudflare/workerd-windows-64": "1.20260730.1" + } + }, + "node_modules/wrangler": { + "version": "4.118.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.118.0.tgz", + "integrity": "sha512-9pkBw/b8zWqGx2S+oLhgHMR1M/4VOE8SynUFABnGWiSFGlcOQ4xiI/B71Xf66RYP2xzngU37IQFPtUruij3lYw==", + "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": "5.20260730.0-alpha", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260730.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.20260730.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -8296,6 +9277,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 5a10276..63dce2b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "lint": "eslint .", "typecheck": "tsc -b", "test": "vitest run", - "build": "npm run validate:specs && npm run typecheck && vite build" + "build": "npm run validate:specs && npm run typecheck && vite build", + "preview:worker": "npm run build && wrangler dev", + "deploy": "npm run build && wrangler deploy" }, "dependencies": { "@scalar/api-reference-react": "0.9.59", @@ -21,6 +23,7 @@ "zod": "4.4.3" }, "devDependencies": { + "@cloudflare/workers-types": "5.20260801.1", "@scalar/openapi-parser": "0.28.8", "@testing-library/jest-dom": "7.0.0", "@testing-library/react": "16.3.2", @@ -38,6 +41,7 @@ "typescript": "6.0.3", "typescript-eslint": "8.65.0", "vite": "8.1.5", - "vitest": "4.1.10" + "vitest": "4.1.10", + "wrangler": "4.118.0" } } diff --git a/scripts/container-smoke.sh b/scripts/container-smoke.sh deleted file mode 100755 index ca958b9..0000000 --- a/scripts/container-smoke.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -image_tag="api-reference:smoke" -container_name="api-reference-smoke-${RANDOM}-$$" -container_id="" - -cleanup() { - if [[ -n "${container_id}" ]]; then - docker rm --force "${container_id}" >/dev/null 2>&1 || true - fi -} -trap cleanup EXIT - -docker build --tag "${image_tag}" . - -container_id="$( - docker run \ - --detach \ - --name "${container_name}" \ - --publish 127.0.0.1::8080 \ - "${image_tag}" -)" - -published_address="$(docker port "${container_id}" 8080/tcp)" -port="${published_address##*:}" -base_url="http://127.0.0.1:${port}" - -for _ in {1..50}; do - if curl --fail --silent "${base_url}/docs/healthz" >/dev/null; then - break - fi - sleep 0.2 -done - -test "$(curl --silent --output /dev/null --write-out '%{http_code}' "${base_url}/docs")" = "308" -test "$(curl --silent --head "${base_url}/docs" | tr -d '\r' | sed -n 's/^Location: //p')" = "/docs/" -index_html="$(curl --fail --silent "${base_url}/docs/")" -printf '%s' "${index_html}" | grep "Grounds API Reference" >/dev/null -curl --fail --silent "${base_url}/docs/specs/registry.json" | grep '"schemaVersion": 1' >/dev/null -test "$(curl --fail --silent "${base_url}/docs/healthz")" = "ok" -test "$(curl --silent --output /dev/null --write-out '%{http_code}' "${base_url}/")" = "404" -runtime_user="$(docker image inspect --format '{{.Config.User}}' "${image_tag}")" -test -n "${runtime_user}" -if [[ "${runtime_user}" == "root" || "${runtime_user}" == root:* || "${runtime_user}" == "0" || "${runtime_user}" == 0:* ]]; then - echo "Container image uses root runtime user (user=${runtime_user})" >&2 - exit 1 -fi - -asset_path="$(printf '%s' "${index_html}" | sed -n 's/.*src="\([^"]*\/assets\/[^"]*\)".*/\1/p')" -test -n "${asset_path}" - -curl --fail --silent --head "${base_url}${asset_path}" \ - | tr -d '\r' \ - | grep --ignore-case '^Cache-Control: public, max-age=31536000, immutable$' >/dev/null - -curl --fail --silent --head "${base_url}/docs/specs/registry.json" \ - | tr -d '\r' \ - | grep --ignore-case '^Cache-Control: public, max-age=300$' >/dev/null - -curl --fail --silent --head "${base_url}/docs/" \ - | tr -d '\r' \ - | grep --ignore-case '^Cache-Control: no-cache$' >/dev/null - -if curl --silent --head "${base_url}/docs/healthz" \ - | tr -d '\r' \ - | grep --ignore-case '^Server: nginx/' >/dev/null; then - echo "Server header exposes the nginx version" >&2 - exit 1 -fi - -echo "Container smoke test passed (image=${image_tag})" diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..8950e1f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "references": [ { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.worker.json" } ] } diff --git a/tsconfig.worker.json b/tsconfig.worker.json new file mode 100644 index 0000000..64ddca7 --- /dev/null +++ b/tsconfig.worker.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "types": ["@cloudflare/workers-types"] + }, + "include": ["api-reference.config.ts", "worker"] +} diff --git a/worker/index.test.ts b/worker/index.test.ts new file mode 100644 index 0000000..1a4e60f --- /dev/null +++ b/worker/index.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; + +import worker, { type WorkerEnv } from "./index"; + +const ORIGIN = "https://apidocs-demo.grounds.workers.dev"; + +const assets = new Map([ + ["/index.html", "Grounds API Reference"], + ["/assets/index-abc123.js", "console.log('bundle');"], + ["/specs/registry.json", '{ "schemaVersion": 1, "sources": [] }'], +]); + +function createEnv(): WorkerEnv { + return { + ASSETS: { + // Mirrors the deployed asset server, which is configured to resolve exact + // paths only (`html_handling: "none"`). + async fetch(input: Request | string) { + const url = new URL(typeof input === "string" ? input : input.url); + const body = assets.get(url.pathname); + + if (body === undefined) { + return new Response("Not Found", { status: 404 }); + } + + return new Response(body, { status: 200 }); + }, + }, + } as unknown as WorkerEnv; +} + +async function get(pathname: string): Promise { + return worker.fetch(new Request(`${ORIGIN}${pathname}`), createEnv()); +} + +describe("API reference Worker", () => { + it("redirects the origin root to the base path", async () => { + const response = await get("/"); + + expect(response.status).toBe(308); + expect(response.headers.get("location")).toBe("/docs/"); + }); + + it("redirects the base path without a trailing slash", async () => { + const response = await get("/docs"); + + expect(response.status).toBe(308); + expect(response.headers.get("location")).toBe("/docs/"); + }); + + it("serves the application shell below the base path", async () => { + const response = await get("/docs/"); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-cache"); + await expect(response.text()).resolves.toContain("Grounds API Reference"); + }); + + it("serves hashed bundles as immutable", async () => { + const response = await get("/docs/assets/index-abc123.js"); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe( + "public, max-age=31536000, immutable", + ); + }); + + it("serves specification snapshots with a short cache lifetime", async () => { + const response = await get("/docs/specs/registry.json"); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("public, max-age=300"); + }); + + it("answers the health endpoint", async () => { + const response = await get("/docs/healthz"); + + expect(response.status).toBe(200); + await expect(response.text()).resolves.toBe("ok"); + }); + + it("falls back to the application shell for client-routed paths", async () => { + const response = await get("/docs/permissions"); + + expect(response.status).toBe(200); + await expect(response.text()).resolves.toContain("Grounds API Reference"); + }); + + it("does not fall back for missing bundles or specifications", async () => { + await expect( + get("/docs/assets/missing.js").then((response) => response.status), + ).resolves.toBe(404); + await expect( + get("/docs/specs/missing.json").then((response) => response.status), + ).resolves.toBe(404); + }); + + it("rejects paths outside the base path", async () => { + const response = await get("/internal"); + + expect(response.status).toBe(404); + }); +}); diff --git a/worker/index.ts b/worker/index.ts new file mode 100644 index 0000000..3088c1e --- /dev/null +++ b/worker/index.ts @@ -0,0 +1,86 @@ +import { API_REFERENCE_BASE_PATH } from "../api-reference.config"; + +export type WorkerEnv = { + ASSETS: Fetcher; +}; + +const BASE_PATH = API_REFERENCE_BASE_PATH.replace(/\/$/, ""); +const ASSET_PREFIX = "/assets/"; +const SPEC_PREFIX = "/specs/"; + +function cacheControlFor(assetPathname: string): string { + if (assetPathname.startsWith(ASSET_PREFIX)) { + return "public, max-age=31536000, immutable"; + } + + if (assetPathname.startsWith(SPEC_PREFIX)) { + return "public, max-age=300"; + } + + return "no-cache"; +} + +function withCacheControl(response: Response, assetPathname: string): Response { + const result = new Response(response.body, response); + result.headers.set("cache-control", cacheControlFor(assetPathname)); + return result; +} + +async function fetchAsset( + env: WorkerEnv, + request: Request, + url: URL, + assetPathname: string, +): Promise { + const assetUrl = new URL(url); + assetUrl.pathname = assetPathname; + assetUrl.search = ""; + + return env.ASSETS.fetch(new Request(assetUrl, request)); +} + +export default { + async fetch(request, env) { + const url = new URL(request.url); + const { pathname } = url; + + if (pathname === "/" || pathname === BASE_PATH) { + return new Response(null, { + status: 308, + headers: { location: `${BASE_PATH}/` }, + }); + } + + if (pathname === `${BASE_PATH}/healthz`) { + return new Response("ok", { + headers: { + "cache-control": "no-store", + "content-type": "text/plain; charset=utf-8", + }, + }); + } + + if (!pathname.startsWith(`${BASE_PATH}/`)) { + return new Response("Not Found", { status: 404 }); + } + + const assetPathname = pathname.slice(BASE_PATH.length); + const assetResponse = await fetchAsset(env, request, url, assetPathname); + + if (assetResponse.status !== 404) { + return withCacheControl(assetResponse, assetPathname); + } + + // Hashed bundles and specification snapshots must fail loudly instead of + // resolving to the application shell. + if ( + assetPathname.startsWith(ASSET_PREFIX) || + assetPathname.startsWith(SPEC_PREFIX) + ) { + return assetResponse; + } + + const indexResponse = await fetchAsset(env, request, url, "/index.html"); + return withCacheControl(indexResponse, "/index.html"); + }, +} satisfies ExportedHandler; diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..8b78e0e --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,20 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "apidocs-demo", + "main": "worker/index.ts", + "compatibility_date": "2026-08-01", + "observability": { + "enabled": true + }, + "assets": { + "directory": "./dist", + "binding": "ASSETS", + // The asset server must not rewrite or redirect on its own: its HTML + // handling redirects unknown paths to `/`, which escapes the base path. + "html_handling": "none", + "not_found_handling": "none", + // The Worker owns routing: it redirects `/` to the base path and strips the + // base path before reading an asset, so assets must not answer first. + "run_worker_first": true + } +}