Implement the storage path: for each accepted report, scrub PII (#8), encrypt with AES-256-GCM (ADR #5), and write only ciphertext to R2, wired in as the real ingest Sink replacing NopSink. - internal/crypto: AES-256-GCM in the exact ADR #5 wire format (magic "LMB1" || version || key_id BE16 || nonce(12) || ct || tag(16); the 7-byte header is the GCM AAD). Provider-independent framing shared by a host crypto/aes+crypto/cipher impl (tests, devserver) and a Wasm SubtleCrypto impl (syscall/js, //go:build js && wasm) per the TinyGo constraint; both produce byte-identical frames. Versioned keyring with key_id rotation; ParseKeyring reads the Secrets Store JSON secret. - internal/storage: ObjectStore interface with an in-memory fake (tests, devserver) and a Wasm R2Store (syumai/workers R2 binding). Sink ties scrub -> Seal -> Put under a unique reports/<ts>-<rand> key. WorkerSink loads the keyring from Secrets Store (BUGREPORT_ENC_KEYRING), cached for the isolate lifetime. - handler.New now takes an injectable ingest.Sink; the Worker uses the real R2/Secrets-Store sink, the devserver a memory + throwaway-key sink. - wrangler.jsonc: add REPORTS_BUCKET (R2) and BUGREPORT_ENC_KEYRING (Secrets Store) bindings. Tests (host, no TinyGo): encrypt/decrypt roundtrip; ciphertext != plaintext; wrong key + tamper (ct/tag/nonce/header-AAD) fail; exact wire layout plus a known-answer vector; key_id rotation with retained keys; full sink path (PII scrubbed then encrypted, readback requires the key and yields the scrubbed content). Existing ingest/handler behavior preserved (202 on valid POST). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 lines
1.8 KiB
JSON
46 lines
1.8 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 secret holding the versioned encryption keyring
|
|
// (ADR #5, Key custody): a single JSON secret {active, keys{ver: base64-32B}}.
|
|
// "binding" is read at runtime via env.BUGREPORT_ENC_KEYRING.get()
|
|
// (internal/storage.KeyringBinding). Replace "<store-id>" with the account's
|
|
// Secrets Store id at deploy time; it is not needed for `pnpm run build`
|
|
// (Wasm compile) or the devserver, so CI does not require it.
|
|
"secrets_store_secrets": [
|
|
{
|
|
"binding": "BUGREPORT_ENC_KEYRING",
|
|
"store_id": "<store-id>",
|
|
"secret_name": "bugreport-enc-keyring"
|
|
}
|
|
]
|
|
}
|