Files
LibreMail-Bug-Report-Ingest/wrangler.jsonc
T
JMR-devandClaude Opus 4.8 f9ec7978b8 Add internal/publish: the real schedule.Publisher that turns pending
encrypted reports into labeled GitHub issues.

- GitHub REST client on net/http (host-testable via httptest; works under
  TinyGo js/wasm per #26). Encodes ADR #6 §3.2: serial mutations spaced
  >=1s, honour Retry-After, wait until x-ratelimit-reset, >=60s floor for
  secondary-limit 403s, full-jitter exponential backoff (base 1s, cap 60s,
  <=5 attempts). Ensures the three ADR #6 labels (create-or-ignore).
- Publisher: GetPending -> crypto.Open -> format -> CreateIssue per id,
  with the ADR #6 per-run cap (50) and 65,536-char body cap (truncate).
  Per-report failures are isolated and surfaced, never abort the batch.
- onPublished(ctx, id) seam, called only after a confirmed 201, default
  no-op: #15 wires it to lifecycle.MarkPublished to complete cross-run
  de-dup. #14 does not implement the mark-published transition.
- Issue body wraps report free-text in a length-adaptive code fence and
  metadata in inline code, neutralising Markdown/@mention injection.
- Worker: swap schedule.LogPublisher for the real publisher in
  scheduled_wasm.go; read GITHUB_TOKEN (Secrets Store) + GITHUB_REPO (var);
  pre-gate so the sibling cron fire does no secret I/O. worker/main.go
  untouched. Export storage.GetSecret for the token read.
- wrangler.jsonc: add GITHUB_TOKEN secret + GITHUB_REPO var (my keys only).

Tests (host, httptest mock, virtual clock): N reports -> N labeled issues
+ onPublished per success; >65,536-char body truncated; transient 5xx and
Retry-After retried per policy; persistent failure isolated (no
onPublished); permission 403 not retried; per-run cap; decrypt failure
isolated. go vet + go test ./... green; GOOS=js GOARCH=wasm build compiles.

Closes #14

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:30:33 -05:00

97 lines
4.5 KiB
JSON

{
// Cloudflare Worker config for local dev and deploy.
// Docs: https://developers.cloudflare.com/workers/wrangler/configuration/
"name": "libremail-bug-report-ingest",
// Entry module. The TinyGo build (see package.json "build") produces the Wasm
// binary at ./build/app.wasm and workers-assets-gen produces this shim, which
// instantiates the Wasm module. The ./build/ artifacts are git-ignored and are
// created by `pnpm run build` (which requires TinyGo).
"main": "./build/worker.mjs",
"compatibility_date": "2025-06-01",
// Wrangler runs this before dev/deploy to (re)build the Wasm + shim.
// Requires TinyGo locally; TinyGo is installed in CI.
"build": {
"command": "pnpm run build"
},
// R2 bucket for the encrypted-at-rest bug-report objects (ADR #5 / issue #9).
// The Worker encrypts each scrubbed report with AES-256-GCM before writing, so
// only ciphertext is ever stored here. "binding" is the JS var the Worker code
// reads (internal/storage.BucketBinding); "bucket_name" matches the bucket
// provisioned by infra/ (defaultR2BucketName = "libremail-bug-reports").
"r2_buckets": [
{
"binding": "REPORTS_BUCKET",
"bucket_name": "libremail-bug-reports"
}
],
// Cloudflare Secrets Store secrets. "binding" is the JS var the Worker reads at
// runtime via env.<binding>.get(). Replace "<store-id>" with the account's
// Secrets Store id at deploy time; secrets are not needed for `pnpm run build`
// (Wasm compile) or the devserver, so CI does not require them.
//
// - BUGREPORT_ENC_KEYRING (ADR #5, Key custody): the versioned encryption
// keyring, a single JSON secret {active, keys{ver: base64-32B}}, read via
// internal/storage.KeyringBinding.
// - ADMIN_TOKEN (#11): the maintainer admin API shared secret (see the
// detailed note on its entry below).
// - GITHUB_TOKEN (#14): the token the weekly publish job authenticates to the
// GitHub API with to create issues on the target repo. Least privilege: a
// fine-grained PAT with Issues: read/write (and Metadata: read) on
// JMR-dev/LibreMail. Read via worker/scheduled_wasm.go githubTokenBinding.
"secrets_store_secrets": [
{
"binding": "BUGREPORT_ENC_KEYRING",
"store_id": "<store-id>",
"secret_name": "bugreport-enc-keyring"
},
// Maintainer admin API shared secret (issue #11). The Worker reads it per
// request via env.ADMIN_TOKEN.get() (internal/storage.AdminTokenBinding) to
// authenticate GET /v1/admin/reports and POST /v1/admin/reports/{id}/remove
// with a constant-time-compared Bearer token. Fail-closed: if this binding is
// absent or empty the admin routes reject every request (401/503). Replace
// "<store-id>" with the account's Secrets Store id at deploy time; not needed
// for `pnpm run build` (Wasm compile) or the devserver (which uses ADMIN_TOKEN
// from the environment instead).
{
"binding": "ADMIN_TOKEN",
"store_id": "<store-id>",
"secret_name": "bugreport-admin-token"
},
{
"binding": "GITHUB_TOKEN",
"store_id": "<store-id>",
"secret_name": "github-token"
}
],
// Plain (non-secret) vars. GITHUB_REPO is the "owner/repo" the weekly publish
// job (#14) files issues on; it is configurable here without a code change and
// defaults to "JMR-dev/LibreMail" in worker/scheduled_wasm.go if unset. Read via
// cloudflare.Getenv(GITHUB_REPO).
"vars": {
"GITHUB_REPO": "JMR-dev/LibreMail"
},
// Cron Triggers for the weekly publish job (#13): "Friday 17:00 America/Chicago
// (Central), DST-correct". Cloudflare evaluates crons in UTC only and has no
// timezone support, and 17:00 Central is a different UTC hour depending on DST:
// 22:00 UTC during Central Daylight Time (CDT, UTC-5, summer) and 23:00 UTC
// during Central Standard Time (CST, UTC-6, winter). A single UTC cron therefore
// cannot express it, so we register BOTH candidate Friday UTC hours below. The
// Worker's scheduled handler (worker/scheduled_wasm.go -> internal/schedule)
// gates each fire on the real America/Chicago rule, so exactly one of the two
// does the publish work on any given Friday and the other is a no-op. This makes
// the trigger fire at 17:00 Central year-round with no manual DST maintenance.
"triggers": {
"crons": [
"0 22 * * 5", // Fridays 22:00 UTC == 17:00 Central during CDT (summer)
"0 23 * * 5" // Fridays 23:00 UTC == 17:00 Central during CST (winter)
]
}
}