diff --git a/AGENTS.md b/AGENTS.md index 2505ee2..e8a04e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,15 +18,23 @@ A Cloudflare Worker that proxies OpenPGP Web Key Directory (WKD) requests to Pro ## Architecture Single-file Worker (`src/index.ts`) with no framework dependencies. Production -`DOMAINS` and routes stay in the Cloudflare dashboard. Do not put `DOMAINS` -in `wrangler.jsonc` `vars`. Tests set example domains in `vitest.config.ts`. +`DOMAINS` is a Cloudflare Worker **secret**, not a plain `vars` entry. Runtime +still reads `env.DOMAINS` as a comma-separated string; secrets bind the same +way. Dashboard routes stay out of git. Do not put `DOMAINS` in +`wrangler.jsonc` `vars`. Tests set example domains in `vitest.config.ts`. Local `wrangler dev` reads `.dev.vars`. +README.md **Production `DOMAINS` secret** is the primary user-facing guidance +for operators and forks. Follow that section for how to set or rotate the +secret (dashboard Secrets or `wrangler secret put DOMAINS`), never commit the +value, and keep example.com / example.org / example.net only in tests and +`.dev.vars.example`. + ## Key Files - `src/index.ts` - The entire Worker implementation - `test/index.spec.ts` - Tests using `@cloudflare/vitest-pool-workers` -- `wrangler.jsonc` - Wrangler config (`keep_vars`, no routes, no `DOMAINS` var) +- `wrangler.jsonc` - Wrangler config (`secrets.required` for `DOMAINS`, no `keep_vars`, no routes, no `DOMAINS` var) - `.github/workflows/deploy.yaml` - GitHub CI only: lint, test, `cf:check` ## Commands @@ -47,16 +55,17 @@ Cloudflare Workers Builds owns production deploy from `main`. GitHub Actions validates only. Do not run `wrangler deploy`, remote DNS writes, or use `CLOUDFLARE_API_TOKEN` from GitHub or a Cloud Agent. -The Builds deploy command is `pnpm deploy:cloudflare` (`wrangler deploy --keep-vars`). +The Builds deploy command is `pnpm deploy:cloudflare` (`wrangler deploy`). `workers_dev` is false and `route` / `routes` are omitted so dashboard routes stay. -`keep_vars` only preserves dashboard vars that are absent from `wrangler.jsonc`. -Do not commit `DOMAINS` in `vars`, including example values. This public -repo has no GitHub `DOMAINS` secret. Dashboard `DOMAINS` is the runtime -source. +`wrangler.jsonc` lists `DOMAINS` under `secrets.required` so Builds fails closed +when the secret is missing. Do not commit `DOMAINS` in `vars`, including +example values. This public repo has no GitHub `DOMAINS` secret. Agents must +not run `wrangler secret put` or use `CLOUDFLARE_API_TOKEN`. Operators set or +rotate from a workstation with Mark's token using the README procedure. Adding a domain is a workstation or dashboard procedure: update the `DOMAINS` -var, add the three route patterns, create the `openpgpkey` CNAME, and add a -root placeholder only when the zone has no A/AAAA/CNAME. +secret per README, add the three route patterns, create the `openpgpkey` CNAME, +and add a root placeholder only when the zone has no A/AAAA/CNAME. ## Code Standards diff --git a/CLAUDE.md b/CLAUDE.md index 96b3b4b..3dde4d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,15 +7,23 @@ A Cloudflare Worker that proxies OpenPGP Web Key Directory (WKD) requests to Pro ## Architecture Single-file Worker (`src/index.ts`) with no framework dependencies. Production -`DOMAINS` and routes stay in the Cloudflare dashboard. Do not put `DOMAINS` -in `wrangler.jsonc` `vars`. Tests set example domains in `vitest.config.ts`. +`DOMAINS` is a Cloudflare Worker **secret**, not a plain `vars` entry. Runtime +still reads `env.DOMAINS` as a comma-separated string; secrets bind the same +way. Dashboard routes stay out of git. Do not put `DOMAINS` in +`wrangler.jsonc` `vars`. Tests set example domains in `vitest.config.ts`. Local `wrangler dev` reads `.dev.vars`. +README.md **Production `DOMAINS` secret** is the primary user-facing guidance +for operators and forks. Follow that section for how to set or rotate the +secret (dashboard Secrets or `wrangler secret put DOMAINS`), never commit the +value, and keep example.com / example.org / example.net only in tests and +`.dev.vars.example`. + ## Key Files - `src/index.ts` - The entire Worker implementation - `test/index.spec.ts` - Tests using `@cloudflare/vitest-pool-workers` -- `wrangler.jsonc` - Wrangler config (`keep_vars`, no routes, no `DOMAINS` var) +- `wrangler.jsonc` - Wrangler config (`secrets.required` for `DOMAINS`, no `keep_vars`, no routes, no `DOMAINS` var) - `.github/workflows/deploy.yaml` - GitHub CI only: lint, test, `cf:check` ## Commands @@ -36,16 +44,17 @@ Cloudflare Workers Builds owns production deploy from `main`. GitHub Actions validates only. Do not run `wrangler deploy`, remote DNS writes, or use `CLOUDFLARE_API_TOKEN` from GitHub or a Cloud Agent. -The Builds deploy command is `pnpm deploy:cloudflare` (`wrangler deploy --keep-vars`). +The Builds deploy command is `pnpm deploy:cloudflare` (`wrangler deploy`). `workers_dev` is false and `route` / `routes` are omitted so dashboard routes stay. -`keep_vars` only preserves dashboard vars that are absent from `wrangler.jsonc`. -Do not commit `DOMAINS` in `vars`, including example values. This public -repo has no GitHub `DOMAINS` secret. Dashboard `DOMAINS` is the runtime -source. +`wrangler.jsonc` lists `DOMAINS` under `secrets.required` so Builds fails closed +when the secret is missing. Do not commit `DOMAINS` in `vars`, including +example values. This public repo has no GitHub `DOMAINS` secret. Agents must +not run `wrangler secret put` or use `CLOUDFLARE_API_TOKEN`. Operators set or +rotate from a workstation with Mark's token using the README procedure. Adding a domain is a workstation or dashboard procedure: update the `DOMAINS` -var, add the three route patterns, create the `openpgpkey` CNAME, and add a -root placeholder only when the zone has no A/AAAA/CNAME. +secret per README, add the three route patterns, create the `openpgpkey` CNAME, +and add a root placeholder only when the zone has no A/AAAA/CNAME. ## Code Standards diff --git a/README.md b/README.md index b366ab5..02fc9f4 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,48 @@ This Worker intercepts those requests via Cloudflare route patterns and proxies ## Features -- Supports unlimited custom domains via a single environment variable +- Supports unlimited custom domains via a single Worker secret - Handles both WKD direct (subdomain) and advanced (`.well-known` path) methods -- Dashboard-managed routes and `DOMAINS` so real domains stay out of this public repo +- Dashboard-managed routes and a `DOMAINS` Worker secret so real domains stay out of this public repo - One-time DNS setup for `openpgpkey.*` subdomains - 100% test coverage with Cloudflare Workers vitest integration - Full observability: structured logging, traces, and logpush +## Production `DOMAINS` secret + +This is the primary guidance for operators and anyone forking the Worker. + +Production `DOMAINS` is a **Cloudflare Worker secret**, not a plain Wrangler `vars` entry and not a GitHub Actions secret. The Worker reads `env.DOMAINS` at runtime as a comma-separated list of hostnames (no spaces required). Secrets bind the same way as vars, so existing code does not change. + +**Never commit the value.** Do not put real domains in git, in `wrangler.jsonc` `vars`, in docs examples, in tests, or in GitHub Actions secrets or variables. This public repository has no GitHub `DOMAINS` secret. + +### Set or rotate the secret + +Use either path. Do not put the value in the repo. + +1. Cloudflare dashboard: Worker `wkd-proxy-worker` → Settings → Secrets → add or update `DOMAINS`. +2. Workstation CLI (account token on that machine only): + +```bash +wrangler secret put DOMAINS +``` + +Paste the comma-separated list when prompted, for example: + +```text +example.com,example.org,example.net +``` + +That example is documentation only. Production uses your real custom domains in the secret, never in git. + +`wrangler.jsonc` lists `DOMAINS` under `secrets.required`. Workers Builds (`pnpm deploy:cloudflare`, which is `wrangler deploy`) fails closed if the secret is missing. A Worker that somehow ran without it would return HTTP 500 when `env.DOMAINS` is empty. + +If `DOMAINS` still exists as a dashboard **plain** variable, convert it to a **secret** before the next Builds deploy. Secrets survive deploys. Leftover plains do not, because this project no longer uses `keep_vars`. + +### Example domains only in tests and local files + +Tests (`vitest.config.ts`, `test/index.spec.ts`) and `.dev.vars.example` use only `example.com`, `example.org`, and `example.net`. Copy `.dev.vars.example` to `.dev.vars` for local `wrangler dev`. Never copy production domains into those files. + ## Quick Start ### 1. Fork This Repository @@ -40,10 +75,7 @@ In the Cloudflare dashboard, open Worker `wkd-proxy-worker` → Settings → Builds and connect this repository. Production branch: `main`. Deploy command: `pnpm deploy:cloudflare`. Leave non-production branch builds off. -Set the Worker runtime variable `DOMAINS` in Settings → Variables to a -comma-separated list of your custom domains. That list stays in the -Cloudflare dashboard. This public repository has no GitHub secret or -variable for `DOMAINS`, and GitHub Actions has no Cloudflare deploy token. +Set production `DOMAINS` as a Worker secret using [Production `DOMAINS` secret](#production-domains-secret). GitHub Actions has no Cloudflare deploy token. ### 4. Add routes and DNS once @@ -60,10 +92,9 @@ has no A, AAAA, or CNAME (common for email-only domains), add proxied placeholders `192.0.2.1` and `100::` so Cloudflare can intercept `.well-known` requests. Existing website records stay untouched. -`wrangler.jsonc` sets `workers_dev = false` and `keep_vars = true` and -omits `routes` and `vars`. Wrangler still applies any `vars` that are in -the config, so example `DOMAINS` must not appear there. Tests and local -dev supply example domains separately. +`wrangler.jsonc` sets `workers_dev = false` and `secrets.required` to +`["DOMAINS"]` and omits `routes` and `vars`. Wrangler still applies any +`vars` that are in the config, so `DOMAINS` must not appear there. ### 5. Push to Deploy @@ -85,7 +116,8 @@ pnpm run dev ``` This starts a local dev server. Copy `.dev.vars.example` to `.dev.vars` -so local `DOMAINS` uses the example list. +so local `DOMAINS` uses the example list (`example.com`, `example.org`, +`example.net`). See [Production `DOMAINS` secret](#production-domains-secret). ## Testing @@ -96,12 +128,16 @@ pnpm run typecheck # TypeScript strict mode pnpm run lint # ESLint strict type-checked ``` +Tests inject the same example list. They never use production domains. + ## How Deployment Works GitHub Actions validates the pull request. Cloudflare Workers Builds deploys from `main` with `pnpm deploy:cloudflare`. That command is -`wrangler deploy --keep-vars`. Dashboard routes stay because `routes` is -omitted. Production `DOMAINS` stays because it is not in `wrangler.jsonc`. +`wrangler deploy`. Dashboard routes stay because `routes` is omitted. + +Production `DOMAINS` is a Worker secret. Set and rotate it as described in +[Production `DOMAINS` secret](#production-domains-secret). The Worker only intercepts the three WKD route patterns. Other hostname traffic is unchanged. @@ -113,14 +149,13 @@ That org-required workflow uses hosted runners and no MNPPI secrets. ## Adding or Removing Domains -1. Update the Worker `DOMAINS` variable in the Cloudflare dashboard. +1. Update the Worker `DOMAINS` secret (dashboard Secrets or + `wrangler secret put DOMAINS`). Never commit the value. See + [Production `DOMAINS` secret](#production-domains-secret). 2. Add or remove the three route patterns for that domain. 3. Add or delete the `openpgpkey.*` CNAME. Add a root placeholder only when the zone has no A, AAAA, or CNAME. -Do not put real domains in git, in `wrangler.jsonc` `vars`, or in GitHub -Actions secrets. Production `DOMAINS` is a Cloudflare dashboard variable. - ## Prerequisites - Custom domains added to Cloudflare (DNS managed by Cloudflare) diff --git a/package.json b/package.json index eafc478..fe93196 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "deploy": "echo 'Refusing workstation production deploy. Workers Builds owns pnpm deploy:cloudflare.' >&2; exit 1", - "deploy:cloudflare": "wrangler deploy --keep-vars", + "deploy:cloudflare": "wrangler deploy", "cf:check": "wrangler deploy --dry-run", "dev": "wrangler dev", "start": "wrangler dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 845a899..4787293 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ overrides: browserslist@<4.28.7: 4.28.7 esbuild@>=0.27.3 <0.28.1: 0.28.1 flatted@<3.4.2: ^3.4.2 - fast-uri@>=3.0.0 <3.1.5: ^3.1.5 + fast-uri@>=3.0.0 <3.1.7: 3.1.7 hono@<4.13.5: 4.13.5 ip-address@<10.3.1: ^10.3.1 minimatch@<10: ^10.2.1 @@ -1502,8 +1502,8 @@ packages: fast-levenshtein@2.0.6: resolution: { integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== } - fast-uri@3.1.6: - resolution: { integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q== } + fast-uri@3.1.7: + resolution: { integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg== } fast-xml-builder@1.3.1: resolution: { integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug== } @@ -3354,7 +3354,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.6 + fast-uri: 3.1.7 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -3654,7 +3654,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.6: {} + fast-uri@3.1.7: {} fast-xml-builder@1.3.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 512051e..bd47832 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,7 +9,8 @@ overrides: browserslist@<4.28.7: 4.28.7 esbuild@>=0.27.3 <0.28.1: 0.28.1 flatted@<3.4.2: ^3.4.2 - fast-uri@>=3.0.0 <3.1.5: ^3.1.5 + # fast-uri ≥3.1.7 floor for GHSA-58mr-gqgx-xq4g / GHSA-qw65-cvwx-89v3 — REMOVE when lockfile resolves ≥3.1.7 without this pin and those alerts are gone. + fast-uri@>=3.0.0 <3.1.7: 3.1.7 # hono ≥4.13.5 floor for GHSA-crvj-82cr-hjcx / GHSA-gqvv-2mrq-wpjv — REMOVE when lockfile resolves ≥4.13.5 without this pin and those alerts are gone. hono@<4.13.5: 4.13.5 ip-address@<10.3.1: ^10.3.1 diff --git a/wrangler.jsonc b/wrangler.jsonc index 0b42270..08965b6 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -4,8 +4,10 @@ "main": "src/index.ts", "compatibility_date": "2025-03-16", "workers_dev": false, - "keep_vars": true, "logpush": true, + "secrets": { + "required": ["DOMAINS"], + }, "observability": { "enabled": true, "head_sampling_rate": 1,