Files
LibreMail-Bug-Report-Ingest/cmd/devserver/main.go
T
JMR-devandClaude Opus 4.8 047391d01c #11 Manual review/removal path for maintainers
Add an authenticated admin API to the ingest Worker so the single maintainer
can review the pending queue and pull a report before Friday's publish run.

Endpoints (on the existing handler):
  GET    /v1/admin/reports              list pending report ids
  POST   /v1/admin/reports/{id}/remove  mark a report removed
  DELETE /v1/admin/reports/{id}         remove alias

Remove calls lifecycle.MarkRemoved (#10), transitioning pending -> removed so
#13's ListPending excludes it from the next publish. Codes: 200 list/remove,
404 unknown id, 401 missing/bad/unset-secret token, 405 wrong method.

Auth: shared-secret Bearer token compared with crypto/subtle.ConstantTimeCompare,
fail-closed when the secret is unset. Injected via handler.New's new AdminBackend
arg: the dev server and tests wire a memory-backed lifecycle.Manager + ADMIN_TOKEN
env; the Worker reads ADMIN_TOKEN from Secrets Store and builds an R2-backed
Manager per request. Choice documented in docs/decisions/admin-auth.md.

Tests: Go httptest unit tests (list, remove+exclusion, 404, 401 incl. fail-closed,
405) and a Bruno api-tests flow (seed, authed list/remove, exclusion, no/bad
token 401). wrangler.jsonc gains only the ADMIN_TOKEN secret binding; worker
triggers untouched (owned by #13).

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

67 lines
2.7 KiB
Go

// Command devserver runs the core bug-report ingest handler on a plain
// net/http server for local development.
//
// It deliberately does not depend on TinyGo or the Cloudflare Workers runtime,
// so `go run ./cmd/devserver` works with the standard Go toolchain and exercises
// the exact same handler that the deployed Wasm Worker serves. It listens on
// :8787 by default (matching wrangler dev's default port); override with ADDR.
//
// The ingest path is wired with the real scrub+encrypt storage Sink (#9) backed
// by an in-memory object store and a throwaway per-run AES-256 key, so a POST
// /v1/reports exercises the full pipeline locally. Stored objects live only for
// the process lifetime.
//
// The maintainer admin API (#11) is wired over a lifecycle.Manager sharing that
// same in-memory store, so reports ingested via POST /v1/reports are immediately
// listable and removable under /v1/admin/reports. Its shared-secret bearer token
// comes from the ADMIN_TOKEN env var; if unset, the admin routes fail closed
// (every request 401s), matching production's fail-closed behaviour. Set
// ADMIN_TOKEN=... to exercise the admin API (the Bruno api-tests do this).
package main
import (
"log"
"net/http"
"os"
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/crypto"
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/handler"
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/lifecycle"
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/storage"
)
func main() {
addr := os.Getenv("ADDR")
if addr == "" {
addr = ":8787"
}
// A throwaway keyring: a single random key generated at startup. Reports are
// scrubbed, encrypted under it, and held in memory; nothing is persisted.
key, err := crypto.GenerateKey()
if err != nil {
log.Fatalf("devserver: generate key: %v", err)
}
keyring, err := crypto.NewKeyring(1, map[uint16][]byte{1: key})
if err != nil {
log.Fatalf("devserver: build keyring: %v", err)
}
// One shared in-memory store backs both the ingest Sink and the admin
// lifecycle Manager, so an ingested report is visible to the admin API.
store := storage.NewMemoryStore()
sink := storage.NewSink(store, keyring)
adminToken := os.Getenv("ADMIN_TOKEN")
admin := handler.NewManagerBackend(lifecycle.New(store), adminToken)
if adminToken == "" {
log.Print("devserver: ADMIN_TOKEN is unset; /v1/admin routes fail closed (401). Set ADMIN_TOKEN to enable them.")
} else {
log.Print("devserver: admin API enabled at /v1/admin/reports (bearer token from ADMIN_TOKEN)")
}
log.Printf("devserver listening on %s (try GET / and GET /healthz)", addr)
if err := http.ListenAndServe(addr, handler.New(sink, admin)); err != nil {
log.Fatalf("devserver: %v", err)
}
}