JMR-devandClaude Opus 4.8 cb7d9c67a0 #10 Report lifecycle/status metadata (pending/removed/published)
Add a status layer over the ObjectStore so each stored report has a
lifecycle state, encoded in its object key as reports/<status>/<id>:
pending (new reports), removed (#11), published (#15). Encoding status in
the key prefix means "list pending" is a single prefix listing with no
secondary index to drift, so it returns exactly the pending reports by
construction.

Storage:
- Extend ObjectStore with List(ctx, prefix) and Delete(ctx, key); implement
  in MemoryStore (host) and the js/wasm R2Store. R2Store.List drives the R2
  binding's list() directly to page a prefix (the syumai helper takes no
  options), so a status with >1000 objects is still enumerated exactly.
- The ingest Sink now writes new reports under reports/pending/<id>, so
  accepted reports enter the lifecycle as pending. The <id> is stable across
  transitions.

lifecycle package:
- Manager over an ObjectStore: ListPending, GetPending(id), MarkRemoved(id),
  MarkPublished(id). A transition copies the opaque ciphertext frame to the
  destination status key and deletes the source key — bytes are never
  decrypted or re-encrypted; no key is needed to change status.
- Copy-then-delete is idempotent and retry-safe: Put(dest) before Delete(src)
  never loses a report, a retry converges (re-Put identical bytes, Delete the
  leftover source), and a transition of an id not in the source status returns
  ErrUnknownReport (unless it is already at the destination -> idempotent nil).

Tests (host, MemoryStore, no TinyGo):
- List-pending exactness across a mix of pending/removed/published.
- pending->removed and pending->published leave the pending set, appear under
  the target, and move byte-identical ciphertext that still decrypts.
- Idempotent retry and convergence from an interrupted (both-keys) state.
- Unknown/terminal-state ids error sensibly; new Sink reports list as pending.

Closes #10

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:25:11 -05:00

LibreMail Bug Report Ingest

Server-side infrastructure for LibreMail's debug bug-report pipeline. This repo is intentionally separate from the Android app repo — it owns the Cloudflare Worker and infrastructure-as-code, not the client.

What this is

Per JMR-dev/LibreMail#11:

  1. The LibreMail app lets a user opt in to submitting a debug report (LibreMail#33).
  2. A Cloudflare Worker in this repo receives the report over HTTPS, best-effort scrubs PII, and stores it encrypted in a Cloudflare R2 bucket (#34).
  3. Every Friday at 17:00 (Central Time, DST-aware), a scheduled job publishes any report not manually removed as a GitHub issue on the LibreMail repo (#35).

Stack

  • Worker: Go, compiled to WebAssembly with TinyGo and served through the syumai/workers runtime adapter
  • Infrastructure as code: Pulumi (Go)
  • Deployment: GitHub Actions
  • Secrets/key custody: Cloudflare Secret Manager
  • DNS: Google Cloud DNS

Build & run locally

The request-handling logic lives in internal/handler as plain, build-tag-free Go, so it is unit-tested and run locally with the standard Go toolchain — no TinyGo needed. Only the actual Wasm Worker build requires TinyGo.

Layout:

  • internal/handler/ — the core http.Handler (health/hello endpoints). No build tags; all request logic and its tests live here.
  • cmd/devserver/ — a plain net/http server that mounts the core handler for local dev without TinyGo.
  • worker/ — the Cloudflare Workers (Wasm) entrypoint, guarded by //go:build js && wasm, wiring the same core handler into the Workers runtime. Excluded from host builds and tests.

Test

go vet ./...
go test ./...

Run locally (no TinyGo)

go run ./cmd/devserver   # listens on :8787; override with ADDR, e.g. ADDR=:9000

Then, from another shell:

$ curl -s localhost:8787/
{"service":"libremail-bug-report-ingest","status":"ok","message":"hello from the LibreMail bug-report ingest Worker"}
$ curl -s localhost:8787/healthz
{"status":"ok"}

This runs the exact handler the deployed Worker uses, minus the Workers runtime.

Build & run the real Worker (requires TinyGo)

Node tooling is managed with pnpm; wrangler is a dev dependency. The Wasm build uses TinyGo 0.41.1 on the Go 1.26 toolchain.

Temporary toolchain patch. TinyGo 0.41.1 and earlier vendor a net/http js/wasm overlay (tinygo-org/net@e54965e) that fails to compile against Go 1.25+/1.26 with t.roundTrip undefined (see tinygo-org/tinygo#5467). CI applies the exact upstream fix (tinygo-org/net@1026408a, checked in as .ci/tinygo-net-roundtrip.patch) to the installed TinyGo before building. Building locally on Go 1.26 needs the same one-file patch until a TinyGo release later than 0.41.1 ships it, at which point the patch and the CI step are removed (tracked in #26).

pnpm install                 # install wrangler
pnpm run build               # workers-assets-gen + TinyGo -> ./build/app.wasm + ./build/worker.mjs
pnpm exec wrangler dev       # serve the Wasm Worker locally on :8787
pnpm exec wrangler deploy    # deploy (CI only)

pnpm run build runs, verbatim:

go run github.com/syumai/workers/cmd/workers-assets-gen && tinygo build -o ./build/app.wasm -target wasm -no-debug ./worker

TinyGo is not required for tests or the dev server; it is needed only for the Wasm build above and is installed in CI. The generated ./build/ output is git-ignored.

Why TinyGo + syumai/workers

Cloudflare Workers execute WebAssembly, not native binaries, so Go must be compiled to Wasm. Of the two options — the standard compiler's GOOS=js GOARCH=wasm output or TinyGo — TinyGo emits far smaller modules that sit comfortably inside the Worker size limit, which is why it is the standard path for Go on Workers. The syumai/workers package adapts Go's net/http handler model to the Workers fetch event, so a single http.Handler runs unchanged on the dev server and in the deployed Worker.

Status

Early bootstrap. See the project board and open issues for the current breakdown of work.

License

GNU AGPL v3.0.

S
Description
Server-side ingest pipeline (Cloudflare Worker + IaC) for LibreMail's opt-in bug-report feature. See JMR-dev/LibreMail#11.
Readme AGPL-3.0
467 KiB
Languages
Go 100%