# M13a W5 Verification How to prove the W5 milestone (JWT gate on routerd / archiverd / deliverd-fcm / deliverd-telegram) is done. ## What "W5 done" means W3 shipped the JWT gate on admind and the shared `authd.NewFromEnv()` helper, but left the other four HTTP services out-of-scope (they were NATS-only consumers with no admin HTTP). W5 picks up the work: add a real admin route to each service, gate it with the same JWT middleware, ship tests and smoke coverage. Concretely, the **W5 exit criteria** are: 1. Each of `cmd/routerd`, `cmd/archiverd`, `cmd/deliverd-fcm`, `cmd/deliverd-telegram` has a `wireAdminRoutes(mux, …, logger)` helper that: - No-ops when `BA_AUTHD_JWT_SECRET` is unset (the LAN deploy path keeps working unchanged). - Wires the gate with `ad.RequireAuth` / `ad.RequireRole` when the secret is set. 2. Each service exposes at least one real admin route: - `routerd`: `GET /v1/admin/dedupe/state` (read) + `POST /v1/admin/dedupe/flush` (destructive, admin role). - `archiverd`: `POST /v1/admin/archiver/run` (any authenticated user; the archiver is idempotent). - `deliverd-fcm`: `GET /v1/admin/dlq` + `GET /v1/admin/dlq/{id}` (channel=fcm, cross-channel safety: non-fcm row returns 404). - `deliverd-telegram`: same shape (channel=telegram). 3. Unit tests cover: no-secret → 404 (route unregistered), with-secret + no-token → 401, role policy per service. routerd also has an end-to-end flush test against a real `dedupe.Collapser`. archiverd has a coalescing test (pre-filled channel → 202). 4. `go build ./...`, `go vet ./...`, `go test -count=1 ./...` all clean. 5. `scripts/m13a_smoke.sh` covers the W5 routes end-to-end against a running stack (checks 9–12). 6. `docker-compose.yml` passes `BA_AUTHD_JWT_SECRET` to all four new services (sharing the same secret authd uses). ## Step-by-step ### 1. Build + test ```bash cd /root/.openclaw/workspace/broad-announce go build ./... go vet ./... go test -count=1 ./cmd/routerd ./cmd/archiverd ./cmd/deliverd-fcm \ ./cmd/deliverd-telegram ./internal/dlq ``` Expected: ``` ok git3.techno-world.net/lrosales/broad-announce/cmd/routerd ok git3.techno-world.net/lrosales/broad-announce/cmd/archiverd ok git3.techno-world.net/lrosales/broad-announce/cmd/deliverd-fcm ok git3.techno-world.net/lrosales/broad-announce/cmd/deliverd-telegram ? git3.techno-world.net/lrosales/broad-announce/internal/dlq [no test files] ``` Test counts: | Package | Tests | Subtests | Notes | |--------------------------|--------|----------|----------------------------------| | cmd/routerd | 5 | 4 | incl. end-to-end Collapser flush | | cmd/archiverd | 5 | 3 | incl. coalescing path | | cmd/deliverd-fcm | 3 | 3 | any-authenticated-user can read | | cmd/deliverd-telegram | 3 | 3 | any-authenticated-user can read | | **Total new (W5)** | **16** | **13** | | ### 2. Run the gate unit tests in isolation ```bash cd /root/.openclaw/workspace/broad-announce go test -count=1 -v -run 'TestWireAdminRoutes|TestAdminDedupe|TestAdminRunNow|TestAdminRolePolicy|TestAdminDLQ' \ ./cmd/routerd ./cmd/archiverd ./cmd/deliverd-fcm ./cmd/deliverd-telegram ``` Each of the four services should show: - `TestWireAdminRoutes_NoSecret_Disabled` — when `BA_AUTHD_JWT_SECRET` is unset, the admin routes return 404 (they're not registered). This is the LAN-deploy backward-compat path. - `TestWireAdminRoutes_WithSecret_Gated` — when the secret is set, the admin routes return 401 without a token. routerd adds: - `TestAdminDedupeState_Auth` — an authenticated user can read the dedupe state JSON. - `TestAdminRolePolicy_ViewerReadOnly_AdminCanFlush` — viewer can read state (200), cannot flush (403); tenant_admin and super_admin can flush (200). - `TestAdminDedupeFlush_DrainsPending` — end-to-end: a pending collapse is observed, the handler is called, the pending count drops to 0. archiverd adds: - `TestAdminRunNow_Auth_Fires` — an authenticated user can trigger a run; the trigger channel receives the signal. - `TestAdminRunNow_Coalesces` — a second concurrent request returns 202 with `triggered=false, reason=already_pending`. - `TestAdminRolePolicy_ViewerCanTrigger` — viewer, tenant_admin, super_admin can all fire (idempotent op). deliverd-fcm and deliverd-telegram add: - `TestAdminDLQ_AnyAuthenticatedUser_CanList` — viewer, tenant_admin, super_admin can all read the per-channel DLQ (the gate passes; the handler then runs the SQL query). ### 3. Run the full M13a smoke (covers W5 end-to-end) ```bash cd /root/.openclaw/workspace/broad-announce docker compose up -d --build bash scripts/bootstrap-super-admin.sh bash scripts/m13a_smoke.sh ``` Expected: all 12 sections green, summary shows `$PASS passed, $FAIL failed` with `0 failed`. The W5-specific checks (9–12) verify: - **9.** routerd `/v1/admin/dedupe/state` and `/v1/admin/dedupe/flush`: - No auth → 401. - With super_admin Bearer → 200. - **10.** archiverd `/v1/admin/archiver/run`: - No auth → 401. - With Bearer → 200 (or 202 if a run is already in flight; both are accepted by the smoke check). - **11.** deliverd-fcm `/v1/admin/dlq`: - No auth → 401. - With Bearer → 200, body contains `"channel":"fcm"`. - **12.** deliverd-telegram `/v1/admin/dlq`: - No auth → 401. - With Bearer → 200, body contains `"channel":"telegram"`. ### 4. Backward-compat verification (LAN deploy) To confirm the W5 gate doesn't break the LAN-only deploy (no `BA_AUTHD_JWT_SECRET`): ```bash cd /root/.openclaw/workspace/broad-announce # Comment out the BA_AUTHD_JWT_SECRET env on the four # new services in docker-compose.yml (or just leave it # empty), then: docker compose up -d --build curl -s -o /dev/null -w "%{http_code}\n" \ http://127.0.0.1:8801/v1/admin/dedupe/state # expect: 404 (route unregistered) ``` ## Notes - The `internal/dlq/query.go` package is the shared SQL helper used by both per-channel admin endpoints. The `List` query has a 30-day hard cap on `created_at` (same as admind's global DLQ endpoint). The `Get` query returns the full row including payload, with a `pgx.ErrNoRows` → `(nil, nil)` idiom for idempotent callers. - The archiverd `runLoop` got a new `triggerCh` parameter (buffered to size 1). The loop now selects on `{ctx.Done, tick.C, triggerCh}`. The default cadence (`BA_ARCHIVERD_RUN_EVERY_SECONDS`, 3600s) is unchanged; the admin route is a side door that fires a run on demand. - The cross-channel safety net in the per-channel DLQ endpoints (`row.Channel != "fcm"` → 404) prevents a deliverd-fcm from accidentally exposing telegram-channel DLQ rows. The global `/v1/dlq` in admind doesn't need this; it serves the full cross-channel view. ## See also - `M13a_PLAN.md` § W5 — the design rationale and exit criteria. - `scripts/m13a_smoke.sh` — the E2E smoke, including the W5 checks 9–12. - `internal/authd/middleware.go` — `RequireAuth` and `RequireRole` (W2). The W5 admin routes are thin wrappers around these.