An authenticated admin path for the single maintainer to review the pending
queue and pull a report before Friday's publish run. Built on the lifecycle
Manager from #10.
Endpoints (added to the existing internal/handler)
remove calls lifecycle.Manager.MarkRemoved (#10), transitioning the report pending -> removed, so #13's ListPending no longer returns it and it is
excluded from the next weekly publish. Remove is idempotent.
Status codes: 200 list/remove, 404 unknown/never-pending id, 401
missing/malformed/wrong token and unset server secret (fail-closed), 405
wrong method (with Allow).
Auth choice — shared-secret Bearer token
Chosen over Cloudflare Access for a single-maintainer, low-volume tool: no Zero
Trust org/policy to provision, trivially callable from curl/scripts/CI, and
injects cleanly for tests + dev server. Full rationale + alternatives in docs/decisions/admin-auth.md (ADR 0003).
Authorization: Bearer <token>, compared with crypto/subtle.ConstantTimeCompare.
Fail-closed: if the server secret is unset/empty, every request is rejected
(401) regardless of what the client sends — a missing binding can never silently
disable auth.
Injected via a new AdminBackend param on handler.New:
dev server + tests:handler.NewManagerBackend(lifecycle.New(store), token)
over an in-memory store; token from the ADMIN_TOKEN env var.
Worker:workerAdminBackend reads ADMIN_TOKEN from Secrets Store and
builds an R2-backed Manager per request (both are only available in-request,
mirroring how WorkerSink loads its keyring).
Tests
Go unit tests (internal/handler, httptest) — all pass
go build ./..., go vet ./..., and full go test ./... are green (host, no
TinyGo). The Worker package also type-checks under GOOS=js GOARCH=wasm go build.
Bruno API tests (api-tests/, OpenCollection YAML) — 19/19 pass
Run against ADMIN_TOKEN=local-dev-admin-token go run ./cmd/devserver, then cd api-tests && bru run --env local:
admin-seed-report (202 Accepted) ✓ seed report is accepted (202)
admin-list-authed (200 OK) ✓ authenticated list returns 200
✓ body has status ok and a reports array
✓ at least one pending report is listed (the seed)
admin-remove-authed (200 OK) ✓ authenticated remove returns 200
✓ body confirms the removed id
admin-list-excludes-removed (200 OK) ✓ list returns 200
✓ the removed id is no longer pending
admin-unauthorized-no-token (401) ✓ a request with no bearer token returns 401
✓ 401 advertises the Bearer challenge
admin-unauthorized-bad-token (401) ✓ a request with the wrong bearer token returns 401
📊 Requests 11 (11 Passed) · Tests 19/19 · Status ✓ PASS
(The 6 pre-existing ingest-contract tests also still pass in the same run.)
worker/main.go: one-line change — added the workerAdminBackend{} arg to the
existing handler.New(...) call (plus a comment). New logic lives in the new worker/admin.go. Triggers/cron untouched (owned by #13).
wrangler.jsonc: added only the ADMIN_TOKENsecrets_store_secrets
entry. Did not touch triggers.
#13 also edits worker/main.go; the overlap is limited to the single handler.New(...) line, so any conflict is trivial to resolve.
Out-of-scope notes
The Worker admin backend is unverified locally (no TinyGo here) but compiles
under the standard GOOS=js GOARCH=wasm toolchain and uses only existing
exported helpers; CI's pnpm run build is the authoritative check.
## What
An authenticated admin path for the single maintainer to review the pending
queue and pull a report before Friday's publish run. Built on the lifecycle
Manager from #10.
### Endpoints (added to the existing `internal/handler`)
| Method + path | Result |
| --- | --- |
| `GET /v1/admin/reports` | `200 {"status":"ok","reports":[<id>...]}` — pending report ids |
| `POST /v1/admin/reports/{id}/remove` | `200 {"status":"removed","id":<id>}` |
| `DELETE /v1/admin/reports/{id}` | `200` — REST alias of the remove above |
`remove` calls `lifecycle.Manager.MarkRemoved` (#10), transitioning the report
`pending -> removed`, so **#13's `ListPending` no longer returns it and it is
excluded from the next weekly publish**. Remove is idempotent.
Status codes: **200** list/remove, **404** unknown/never-pending id, **401**
missing/malformed/wrong token *and* unset server secret (fail-closed), **405**
wrong method (with `Allow`).
## Auth choice — shared-secret Bearer token
Chosen over Cloudflare Access for a single-maintainer, low-volume tool: no Zero
Trust org/policy to provision, trivially callable from curl/scripts/CI, and
injects cleanly for tests + dev server. Full rationale + alternatives in
`docs/decisions/admin-auth.md` (ADR 0003).
- `Authorization: Bearer <token>`, compared with `crypto/subtle.ConstantTimeCompare`.
- **Fail-closed:** if the server secret is unset/empty, every request is rejected
(401) regardless of what the client sends — a missing binding can never silently
disable auth.
- Injected via a new `AdminBackend` param on `handler.New`:
- **dev server + tests:** `handler.NewManagerBackend(lifecycle.New(store), token)`
over an in-memory store; token from the `ADMIN_TOKEN` env var.
- **Worker:** `workerAdminBackend` reads `ADMIN_TOKEN` from Secrets Store and
builds an R2-backed Manager per request (both are only available in-request,
mirroring how `WorkerSink` loads its keyring).
## Tests
### Go unit tests (`internal/handler`, httptest) — all pass
```
--- PASS: TestAdminListReturnsPendingIDs
--- PASS: TestAdminListEmptyIsArray
--- PASS: TestAdminRemoveExcludesFromPending
--- PASS: TestAdminRemoveViaDelete
--- PASS: TestAdminRemoveIdempotent
--- PASS: TestAdminRemoveUnknownIs404
--- PASS: TestAdminMissingTokenIs401
--- PASS: TestAdminInvalidTokenIs401
--- PASS: TestAdminMalformedAuthHeaderIs401
--- PASS: TestAdminUnsetSecretFailsClosed
--- PASS: TestAdminNilBackendFailsClosed
--- PASS: TestAdminWrongMethodIs405
ok github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/handler
```
`go build ./...`, `go vet ./...`, and full `go test ./...` are green (host, no
TinyGo). The Worker package also type-checks under `GOOS=js GOARCH=wasm go build`.
### Bruno API tests (`api-tests/`, OpenCollection YAML) — 19/19 pass
Run against `ADMIN_TOKEN=local-dev-admin-token go run ./cmd/devserver`, then
`cd api-tests && bru run --env local`:
```
admin-seed-report (202 Accepted) ✓ seed report is accepted (202)
admin-list-authed (200 OK) ✓ authenticated list returns 200
✓ body has status ok and a reports array
✓ at least one pending report is listed (the seed)
admin-remove-authed (200 OK) ✓ authenticated remove returns 200
✓ body confirms the removed id
admin-list-excludes-removed (200 OK) ✓ list returns 200
✓ the removed id is no longer pending
admin-unauthorized-no-token (401) ✓ a request with no bearer token returns 401
✓ 401 advertises the Bearer challenge
admin-unauthorized-bad-token (401) ✓ a request with the wrong bearer token returns 401
📊 Requests 11 (11 Passed) · Tests 19/19 · Status ✓ PASS
```
(The 6 pre-existing ingest-contract tests also still pass in the same run.)
## Worker / wrangler (conflict-awareness with #13)
- `worker/main.go`: one-line change — added the `workerAdminBackend{}` arg to the
existing `handler.New(...)` call (plus a comment). New logic lives in the new
`worker/admin.go`. **Triggers/cron untouched** (owned by #13).
- `wrangler.jsonc`: added **only** the `ADMIN_TOKEN` `secrets_store_secrets`
entry. Did not touch triggers.
- `#13` also edits `worker/main.go`; the overlap is limited to the single
`handler.New(...)` line, so any conflict is trivial to resolve.
## Out-of-scope notes
- The Worker admin backend is unverified locally (no TinyGo here) but compiles
under the standard `GOOS=js GOARCH=wasm` toolchain and uses only existing
exported helpers; CI's `pnpm run build` is the authoritative check.
Closes #11
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
An authenticated admin path for the single maintainer to review the pending
queue and pull a report before Friday's publish run. Built on the lifecycle
Manager from #10.
Endpoints (added to the existing
internal/handler)GET /v1/admin/reports200 {"status":"ok","reports":[<id>...]}— pending report idsPOST /v1/admin/reports/{id}/remove200 {"status":"removed","id":<id>}DELETE /v1/admin/reports/{id}200— REST alias of the remove aboveremovecallslifecycle.Manager.MarkRemoved(#10), transitioning the reportpending -> removed, so #13'sListPendingno longer returns it and it isexcluded from the next weekly publish. Remove is idempotent.
Status codes: 200 list/remove, 404 unknown/never-pending id, 401
missing/malformed/wrong token and unset server secret (fail-closed), 405
wrong method (with
Allow).Auth choice — shared-secret Bearer token
Chosen over Cloudflare Access for a single-maintainer, low-volume tool: no Zero
Trust org/policy to provision, trivially callable from curl/scripts/CI, and
injects cleanly for tests + dev server. Full rationale + alternatives in
docs/decisions/admin-auth.md(ADR 0003).Authorization: Bearer <token>, compared withcrypto/subtle.ConstantTimeCompare.(401) regardless of what the client sends — a missing binding can never silently
disable auth.
AdminBackendparam onhandler.New:handler.NewManagerBackend(lifecycle.New(store), token)over an in-memory store; token from the
ADMIN_TOKENenv var.workerAdminBackendreadsADMIN_TOKENfrom Secrets Store andbuilds an R2-backed Manager per request (both are only available in-request,
mirroring how
WorkerSinkloads its keyring).Tests
Go unit tests (
internal/handler, httptest) — all passgo build ./...,go vet ./..., and fullgo test ./...are green (host, noTinyGo). The Worker package also type-checks under
GOOS=js GOARCH=wasm go build.Bruno API tests (
api-tests/, OpenCollection YAML) — 19/19 passRun against
ADMIN_TOKEN=local-dev-admin-token go run ./cmd/devserver, thencd api-tests && bru run --env local:(The 6 pre-existing ingest-contract tests also still pass in the same run.)
Worker / wrangler (conflict-awareness with #13)
worker/main.go: one-line change — added theworkerAdminBackend{}arg to theexisting
handler.New(...)call (plus a comment). New logic lives in the newworker/admin.go. Triggers/cron untouched (owned by #13).wrangler.jsonc: added only theADMIN_TOKENsecrets_store_secretsentry. Did not touch triggers.
#13also editsworker/main.go; the overlap is limited to the singlehandler.New(...)line, so any conflict is trivial to resolve.Out-of-scope notes
under the standard
GOOS=js GOARCH=wasmtoolchain and uses only existingexported helpers; CI's
pnpm run buildis the authoritative check.Closes #11