Commit Graph
39 Commits
Author SHA1 Message Date
JMR-devandClaude Opus 4.8 e650e5f161 #13 Cloudflare Cron Trigger: Friday 17:00 America/Chicago, DST-correct
Add a weekly Cron Trigger that fires at 17:00 America/Chicago (Central) every
Friday year-round, correct across the CST/CDT DST transition, and on fire lists
the pending reports (#10 Manager.ListPending) and hands their ids to the publish
step.

Cloudflare crons are UTC-only, and 17:00 Central is 22:00 UTC under CDT (summer)
and 23:00 UTC under CST (winter), so no single UTC cron expresses it. Register
BOTH Friday UTC hours in wrangler.jsonc (`0 22 * * 5` and `0 23 * * 5`) and gate
each fire: only the fire that is actually 17:00 Central does the work, so
publishing runs exactly once per Friday.

TinyGo/Wasm may lack the IANA tz database, so the gate does not call
time.LoadLocation. Instead internal/schedule computes the US Central DST rule
from first principles (CDT from the 2nd Sunday of March 02:00 to the 1st Sunday
of November 02:00, else CST) behind a pure func IsFriday1700Central(time.Time),
host-testable without TinyGo and cross-checked against the real America/Chicago
zone (via a test-only time/tzdata import) over a 20-year sweep.

- internal/schedule: pure DST gate + Run orchestrator; Publisher/PendingLister
  seams; LogPublisher no-op default (the seam #14 replaces).
- worker/scheduled_wasm.go: js/wasm-only adapter registering the scheduled task
  via init()+cron.ScheduleTaskNonBlock, wiring the R2-backed lifecycle Manager to
  schedule.Run. worker/main.go is untouched.
- wrangler.jsonc: add triggers.crons (only the triggers section changed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:52:39 -05:00
Jason Ross c9f8489350 Merge pull request #37 from JMR-dev/ticket-36-deps-security
#36 Security: patch form-data + uuid (Dependabot)
2026-07-02 15:44:59 -05:00
JMR-devandClaude Opus 4.8 52ce6e9aca #36 Security: patch vulnerable transitive deps form-data + uuid
Force patched versions of two vulnerable transitive dev-tooling deps
flagged by Dependabot. Both are dev-only (pulled in transitively by
wrangler / @usebruno/cli) and are not part of the Go/Wasm Worker:

- form-data 4.0.4 -> 4.0.6  (HIGH, CRLF injection; vuln >=4.0.0 <4.0.6)
- uuid      10.0.0 -> 14.0.1 (MEDIUM, buffer bounds; vuln <11.1.1)

The pnpm overrides live in pnpm-workspace.yaml (the `overrides:` key)
rather than package.json's `pnpm.overrides` because pnpm 11 no longer
reads the "pnpm" field in package.json (it warns and ignores it). This
sits alongside the existing allowBuilds config in the same file. The
lockfile was regenerated so form-data resolves to a single 4.0.6 and
uuid to 14.0.1 (the >=11.1.1 override resolves to the latest published
uuid, which is well above the vulnerable <11.1.1 range).

Verified locally:
- pnpm install and pnpm install --frozen-lockfile exit 0
- pnpm run test:api (Bruno suite) passes 8/8 against go devserver
- pnpm exec wrangler --version -> 4.106.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:42:41 -05:00
Jason Ross 9557075bdb Merge pull request #35 from JMR-dev/ticket-10-lifecycle-metadata
#10 Report lifecycle/status metadata (pending/removed/published)
2026-07-02 15:30:04 -05:00
JMR-devandClaude Opus 4.8 a03da6ede9 storage(r2): build R2 list options via Object/Set for TinyGo parity
Construct the list() options with js.Global().Get("Object").New()+Set instead
of js.ValueOf(map[string]any), keeping the JS-interop surface identical to the
rest of the wasm build. No behavior change; wasm-only file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:27:31 -05:00
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
Jason Ross 786eb225b7 Merge pull request #34 from JMR-dev/ticket-9-encrypted-r2-storage
#9 Encrypted-at-rest R2 storage for scrubbed reports
2026-07-02 15:10:42 -05:00
JMR-devandClaude Opus 4.8 bd7fe21d97 #9 Encrypted-at-rest R2 storage for scrubbed reports
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>
2026-07-02 15:08:21 -05:00
Jason Ross 3d320a8cea Merge pull request #33 from JMR-dev/ticket-32-ci-cache-infra
#32 CI: cache infra/ Go module deps
2026-07-02 15:02:37 -05:00
JMR-devandClaude Opus 4.8 98ee51b21a CI: cache infra/ Go module deps (Pulumi SDKs) to speed up runs
setup-go's built-in module cache defaults to keying only on the root
go.sum, so the infra/ module's heavy Pulumi SDK dependencies
(pulumi/sdk, pulumi-cloudflare, pulumi-gcp) re-downloaded on every run.

Set cache-dependency-path to hash both go.sum and infra/go.sum so
infra/'s deps are restored from cache. The cache warms on the first
(cold) run; the speedup lands on subsequent (warm) runs.

Closes #32

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:00:50 -05:00
Jason Ross da31bc4b24 Merge pull request #31 from JMR-dev/ticket-12-privacy-doc
#12 Document data flow & privacy posture
2026-07-02 14:52:56 -05:00
JMR-devandClaude Opus 4.8 80db59d9f1 Correct privacy doc status after #7/#8/#2 merged to main
Bring docs/privacy.md's implementation status current with main, which now
includes the ingest endpoint (#7), the scrub library (#8), and the Pulumi
infra (#2).

- Stop claiming the ingest endpoint is unimplemented: POST /v1/reports (size
  cap, v1 schema validation, and the 202/400/413/415/405/503 contract) and the
  PII-scrub library are now implemented in the repo.
- Replace the granular per-stage status table with one concise
  "Current implementation status" note that is less prone to going stale.
- Keep the honest nuance: the endpoint is wired to a no-op sink, so accepted
  reports are not yet retained, scrubbed in-line, encrypted, or published;
  scrub is not yet invoked on the live path. Encrypted storage (#9), lifecycle
  (#10), manual removal (#11), cron (#13), and publish (#14/#15) remain not yet
  built, and the edge rate-limit ruleset is reserved but not yet provisioned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:47:43 -05:00
JMR-devandClaude Opus 4.8 cb1b6536f2 Document data flow & privacy posture (#12)
Add docs/privacy.md describing the end-to-end bug-report pipeline and its
privacy posture, so it can be linked from LibreMail's README / F-Droid
metadata.

Covers: opt-in / user-initiated-only submission; HTTPS ingest (POST
/v1/reports, size-limited, validated); best-effort PII scrub and its
documented limits; encrypted-at-rest R2 storage (AES-256-GCM, key in
Cloudflare Secrets Store); manual review/removal window; and the weekly
publish to GitHub. States plainly that scrubbing is best-effort (not a
guarantee) and marks stages that are designed but not yet implemented.

Links ADR #5 (encryption) and ADR #6 (labels/abuse).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:47:43 -05:00
Jason Ross dd443bbe37 Merge pull request #27 from JMR-dev/ticket-2-pulumi-scaffold
#2 Pulumi IaC scaffolding: Worker, R2 bucket, Google Cloud DNS
2026-07-02 14:36:23 -05:00
Jason Ross 1a605fdda6 Merge branch 'main' into ticket-2-pulumi-scaffold 2026-07-02 14:32:03 -05:00
Jason Ross ab81b3a36e Merge pull request #29 from JMR-dev/ticket-7-ingest-endpoint
#7 Ingest HTTPS endpoint: accept report POST, size limit
2026-07-02 14:30:41 -05:00
Jason Ross 9a2d9df944 Merge branch 'main' into ticket-2-pulumi-scaffold 2026-07-02 14:29:02 -05:00
Jason Ross cd59c79521 Merge branch 'main' into ticket-7-ingest-endpoint 2026-07-02 14:29:00 -05:00
Jason Ross 16941e1c9d Merge pull request #28 from JMR-dev/ticket-8-pii-redaction
#8 PII anonymization/redaction pass before storage
2026-07-02 14:27:30 -05:00
Jason Ross 653b981146 Merge branch 'main' into ticket-7-ingest-endpoint 2026-07-02 14:25:41 -05:00
Jason Ross 1c1119cd30 Merge branch 'main' into ticket-8-pii-redaction 2026-07-02 14:25:37 -05:00
Jason Ross 62ad4ce907 Merge branch 'main' into ticket-2-pulumi-scaffold 2026-07-02 14:25:34 -05:00
Jason Ross 92655c58cb Merge pull request #25 from JMR-dev/ticket-3-ci
#3 CI: build, lint, test + working TinyGo/Wasm build
2026-07-02 14:24:32 -05:00
Jason Ross c0c2925a5a Merge branch 'main' into ticket-3-ci 2026-07-02 14:22:41 -05:00
JMR-devandClaude Opus 4.8 47f9babe35 #26 Fix TinyGo net/http wasm build via pinned upstream patch (Go 1.26)
TinyGo 0.41.1 and earlier vendor tinygo-org/net@e54965e, whose net/http
js/wasm overlay (roundtrip_js.go) calls the private t.roundTrip fallback
removed from Go 1.25+/1.26 net/http, so `pnpm run build` fails to compile
on Go 1.26 (tinygo-org/tinygo#5467). No released TinyGo carries the fix
yet: it landed in tinygo-org/net@1026408a on 2026-04-27, after 0.41.1
shipped 2026-04-22, and is already on TinyGo's dev branch.

Keep Go 1.26 and apply the exact upstream fix in CI before the build:
- .ci/tinygo-net-roundtrip.patch: byte-exact tinygo-org/net@1026408a diff
  (its parent e54965e is the commit 0.41.1 ships), targeting
  src/net/http/roundtrip_js.go.
- ci.yml: new "Patch TinyGo net/http (temporary)" step applies it to
  $(tinygo env TINYGOROOT) via `git apply`, failing loudly on drift.
- .gitattributes: force LF on *.patch so `git apply` works on the Linux
  runner regardless of the committer's platform.
- README: document the temporary patch and its removal condition.

Temporary: remove the patch and the CI step once a TinyGo release later
than 0.41.1 ships the net fix. Tracking #26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:06:08 -05:00
JMR-devandClaude Opus 4.8 b31455f1c3 #7 Approve protobufjs build script so pnpm install exits 0
@usebruno/cli (the API-test runner added in this PR) pulls in protobufjs,
whose postinstall build script pnpm 11 leaves un-approved by default. That
makes `pnpm install` exit non-zero (ERR_PNPM_IGNORED_BUILDS), which also
aborts `pnpm exec` / `pnpm run` via their verify-deps-before-run precheck
and would break CI's pnpm install once this lands on main.

Add protobufjs to the existing allowBuilds allowlist in pnpm-workspace.yaml
(same mechanism already used for esbuild/sharp/workerd). With it, pnpm
install exits 0 and the Bruno OpenCollection YAML suite runs green through
the pnpm wrapper (pnpm exec bru run / pnpm run test:api), 8/8 tests passing
against `go run ./cmd/devserver`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:02:04 -05:00
JMR-devandClaude Opus 4.8 bd3637d8a6 Revert Go downgrade; keep Go 1.26 per maintainer mandate
Undoes the go.mod/setup-go pin to 1.25 from the previous commit. The
maintainer requires Go 1.26. The TinyGo net/http wasm build failure is
an upstream toolchain bug (tinygo-org/tinygo#5467) and is being resolved
separately without changing the Go version. Not pushed pending the
toolchain-fix decision (issue #26).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:53:46 -05:00
JMR-devandClaude Opus 4.8 4c22056e66 #7 Ingest HTTPS endpoint: accept report POST, size limit
Add internal/ingest implementing POST /v1/reports, wired into the core
build-tag-free handler so the same route serves on the dev server and the
Cloudflare Worker.

Response contract (ADR #6 §2.4):
- 202 Accepted for valid JSON within the 256 KiB cap ({"status":"accepted"})
- 413 for oversized bodies (Content-Length fast path AND a MaxBytesReader
  hard cap, so a missing/lying Content-Length cannot bypass the limit)
- 415 when Content-Type is not application/json
- 400 for malformed JSON or failed schema validation (generic error body,
  never echoes request content)
- 405 with Allow: POST for any non-POST method
- 503 when the storage Sink fails

Storage is decoupled behind a small Sink interface (Store(ctx, raw)) with a
NopSink default and a MemorySink for tests, so PII scrubbing (#8) and
encrypted R2 storage (#9) can slot in without touching the HTTP contract.
Rate limiting (429) and volumetric shedding stay a Cloudflare-edge/Pulumi
concern per #2 and are intentionally not implemented in the Worker.

Tests:
- Go unit tests (net/http/httptest) for every response code, including 413
  via both Content-Length and an oversized streamed body, plus boundary,
  storage-failure, and no-content-echo cases.
- Bruno API tests in OpenCollection YAML format under api-tests/, asserting
  the full contract against the local dev server via @usebruno/cli.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:52:04 -05:00
JMR-devandClaude Opus 4.8 6508679d86 Add best-effort PII redaction pass (internal/scrub) (#8)
Introduce package internal/scrub, a schema-agnostic, regex/heuristic
based redaction pass to run over raw bug-report payloads before storage
(#9). It masks (never deletes) matches with bracketed placeholders so
payload structure is preserved for triage.

Categories:
- Emails: robust address regex; ignores @handles and "meet @ 3pm".
- Auth tokens/secrets: Authorization/Proxy-Authorization header values,
  standalone Bearer tokens, eyJ-anchored JWTs, well-known provider key
  formats (GitHub, GitLab, Slack, Stripe, OpenAI, Google, AWS), and
  values under secret-named keys (password, api_key, token, ...).
- IP addresses: octet-validated IPv4 and comprehensive IPv6 (full,
  compressed, loopback, IPv4-mapped), ordered for correct extraction.
- Names: deliberately weak, key-directed heuristic (name/user/...),
  \b-anchored to avoid filename/hostname collisions. Documented in code
  as best-effort and NOT to be relied upon.

API: Scrub([]byte) []byte, ScrubString(string) string, plus composable
per-category RedactEmails/RedactTokens/RedactIPs/RedactNames and exported
Placeholder* constants. Non-mutating and idempotent. Build-tag-free so it
compiles for host and the Wasm target.

Tests cover each category with positive and over-redaction-guard cases
(89 passing checks); go test ./... is green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:46:45 -05:00
JMR-devandClaude Opus 4.8 21665b172d #2 Pulumi IaC scaffolding: Worker, R2 bucket, Google Cloud DNS
Add an infra/ Pulumi (Go) program in its own module
(github.com/JMR-dev/LibreMail-Bug-Report-Ingest/infra) that declares the
three pieces of edge/DNS infrastructure for the bug-report ingest pipeline:

- Cloudflare Worker script (libremail-bug-report-ingest, built in #1)
- Cloudflare R2 bucket (libremail-bug-reports) for encrypted reports (ADR 0001)
- Google Cloud DNS record (CNAME) pointing the ingest hostname at the Worker,
  referencing an existing managed zone by name

Per-environment stacks (dev/prod) via Pulumi.<stack>.yaml + pulumi.Config;
account id, zone, domain, etc. are parameterized through config and secrets
are kept out of git (documented in infra/README.md). Worker content is a
documented placeholder because the real TinyGo->Wasm artifact is produced by
the build pipeline.

Mock-based unit tests (pulumi.RunErr + pulumi.WithMocks) assert the registered
resources and their inputs; go build + go vet + go test all pass without the
Pulumi CLI. Structured so the #7 Cloudflare Rate Limiting ruleset can be added
later (reserved cloudflareZoneId config + insertion point in deploy.go).

Providers: pulumi-cloudflare v6.17.0, pulumi-gcp v8.41.1, pulumi/sdk v3.250.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:45:07 -05:00
JMR-devandClaude Opus 4.8 f27ad42c24 #26 Pin Go 1.25 + TinyGo 0.41.1 so the Wasm build compiles
TinyGo 0.41.1's bundled net/http override (roundtrip_js.go) fails to
compile against the Go 1.26 stdlib:

  net/http/roundtrip_js.go:73:12: t.roundTrip undefined (type *Transport
  has no field or method roundTrip, but does have method RoundTrip)

This is tinygo-org/tinygo#5467 (closed 2026-06-20, but not in any tagged
TinyGo release as of 0.41.1, released 2026-04-22). Go 1.25.x is the
newest line TinyGo 0.41.1 fully supports; syumai/workers v0.33.0 needs
only go 1.21.3 and the handler uses only net/http + encoding/json, so
downgrading is safe:

- go.mod: go 1.26.2 -> go 1.25.0 (so GOTOOLCHAIN won't auto-upgrade past
  what TinyGo supports)
- ci.yml: setup-go go-version 1.26 -> 1.25 (TinyGo pin stays 0.41.1)
- README: document the pinned TinyGo/Go matrix and the #5467 rationale

go vet ./..., go test ./..., and actionlint stay green locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:42:17 -05:00
Jason Ross 27d707d704 Merge pull request #24 from JMR-dev/ticket-23-autoupdate-pat
#23 autoupdate: use STATUS_CHECKS_RETRIGGER_TOKEN so branch updates re-trigger checks
2026-07-02 13:34:02 -05:00
JMR-devandClaude Opus 4.8 4c2d0ad43f autoupdate: use STATUS_CHECKS_RETRIGGER_TOKEN so branch updates re-trigger checks
The autoupdate workflow authenticated its branch-update pushes with the
default GITHUB_TOKEN. Pushes made with GITHUB_TOKEN do not re-trigger
downstream workflow runs, so status checks were not re-run on updated PR
branches.

Source the token from the STATUS_CHECKS_RETRIGGER_TOKEN PAT (scoped to the
"production" environment) instead. The action still reads GITHUB_TOKEN from
env, so only the value changes. Add `environment: production` to the job so
the environment-scoped secret is accessible, and update the explanatory
comment accordingly.

Closes #23

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:30:53 -05:00
JMR-devandClaude Opus 4.8 610b02ba83 #3 GitHub Actions CI: build, lint, test
Add .github/workflows/ci.yml running on pull_request (targeting main) and
push to main. A single ubuntu-latest job "ci":
- checks out the repo, sets up Go 1.26, pnpm 10 + Node 22 (pnpm store
  cache), and TinyGo 0.41.1 (Binaryen/wasm-opt included);
- runs pnpm install --frozen-lockfile, go vet ./..., go test ./...;
- conditionally vets/tests an infra/ Go module if infra/go.mod exists
  (no-op until ticket #2 adds it);
- runs pnpm run build to confirm the TinyGo/Wasm Worker builds end to end.

Every action is pinned by full commit SHA with a "# vX.Y.Z" comment,
matching the supply-chain style of .github/workflows/autoupdate.yml.
Validated with actionlint (clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:30:50 -05:00
Jason RossandClaude Opus 4.8 8e1dc66c54 CI: auto-update open PR branches via autoupdate Action (#20) (#22)
Add .github/workflows/autoupdate.yml. On every push to main, the
chinthakagodawita/autoupdate action merges main into all open PRs that
target it (PR_FILTER: "all"), keeping branches current as PRs merge.

The action is pinned to commit 0707656 (v1.7.0) for supply-chain safety.
Uses the default GITHUB_TOKEN with minimal contents:write and
pull-requests:write permissions.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:22:52 -05:00
Jason RossandClaude Opus 4.8 499bf7f655 Bootstrap Go module + Cloudflare Worker build tooling (#21)
Initialize the Go module and the Go -> Cloudflare Workers (TinyGo/Wasm) build
path, structured so `go test` and a local dev server run on plain Go without
TinyGo, while the real Wasm entrypoint is isolated behind build tags.

- go.mod/go.sum: module github.com/JMR-dev/LibreMail-Bug-Report-Ingest (Go 1.26),
  requiring github.com/syumai/workers.
- internal/handler: build-tag-free core http.Handler (GET / and GET /healthz,
  JSON responses, 404/405 handling) with net/http/httptest unit tests.
- cmd/devserver: plain net/http server mounting the core handler for local dev
  without TinyGo (listens on :8787, override with ADDR).
- worker/main.go: Cloudflare Workers (Wasm) entrypoint behind
  //go:build js && wasm, wiring the same handler via github.com/syumai/workers;
  excluded from host builds/tests.
- package.json + pnpm-lock.yaml + pnpm-workspace.yaml: wrangler dev dependency
  managed with pnpm, with toolchain build scripts approved.
- wrangler.jsonc: name=libremail-bug-report-ingest, main=./build/worker.mjs,
  build via `pnpm run build` (TinyGo).
- README: "Build & run locally" section with exact commands and the rationale
  for the TinyGo + syumai/workers path.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:22:48 -05:00
Jason RossandClaude Opus 4.8 e93b6a2266 Add encryption ADR: Worker-side AES-256-GCM + Secrets Store key custody (#19)
Documents the decision for #5: Worker-side authenticated encryption (AES-256-GCM) applied in the Worker before writing to R2, so R2 never receives plaintext or the key. Key material is held as a versioned keyring in Cloudflare Secrets Store (shared by the ingest and weekly-publish Workers). Rotation is data-loss-free via a key-id/version in each object header plus retained old key versions. Unblocks #9.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:17:15 -05:00
Jason RossandClaude Opus 4.8 041c7758c1 #6 Decision: GitHub labels + abuse/rate-limit policy ADR (#18)
Add docs/decisions/labels-and-abuse.md fixing concrete values that unblock
#14 and inform #7:

- Labels on auto-published issues (JMR-dev/LibreMail): bug-report, automated,
  needs-triage. #14 must create any missing.
- Ingest policy: 256 KiB payload cap (413); per-IP 15/60s + 100/1h rate limits
  (429 + Retry-After) via Cloudflare Rate Limiting rules in Pulumi; full
  response-code contract (202/400/413/415/405/429/5xx).
- Weekly publish job: 50 issues/run cap; serial creation, 1s spacing,
  Retry-After honoured, exponential backoff (base 1s, cap 60s, jitter,
  max 5 attempts), mark-published-on-confirm de-dup.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:13:39 -05:00
JMR-devandClaude Sonnet 5 72bdfb2974 Bootstrap repo: README, AGPL-3.0 license, Go .gitignore
Initial commit for the LibreMail bug-report ingest pipeline
(JMR-dev/LibreMail#11), split from the app repo into its own
infrastructure repo per the issue's separation-of-concerns spec.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 12:39:29 -05:00