/admin is wallet-gated. The configured ADMIN_PUBLISHER is a BNS name;
only the on-chain owner of that name can sign in. The sign-in flow is
BIP-137 (same primitive publishers use): the login page shows a fresh
one-shot challenge, the admin signs it in their wallet, pastes the
signature, and the service verifies that it recovers to the current
on-chain owner. Session is a 24-hour ed25519-signed cookie minted by
the service key.
Pages:
/admin dashboard — totals, mode state, recent listings + audit
/admin/login BIP-137 challenge sign-in
/admin/listings all listings, with a Withdraw button per row
/admin/modes global + per-category publish-mode toggle
/admin/audit the hash-chained audit log
The three publish modes exist as toggles:
`append` — any valid listing goes in (launch default)
`scan-first` — infected uploads are rejected before they land
`review` — clean listings wait for human approval
Global mode is the fallback; a per-category override takes precedence
when set. The toggle persists; the review-queue enforcement lands in a
later milestone (the enforcement sketch is in modes.js's header).
Audit log:
Every admin action (login, set_mode, clear_mode, withdraw_listing) is
an append-only row with a sha256 chain over the previous row's hash.
The chain does not prevent edits; it makes retroactive ones loud —
row N's `prev_hash` disagreeing with row N-1's `this_hash` is the
tamper signal.
Withdrawal:
Soft — `withdrawn_at` on the listing row. Browse hides withdrawn
listings (already the case) and the detail page returns 404 for them.
Nothing in the chain is undone; this is Prometheus's own view only.
Session design:
Cookie is `prom_admin=<payload b64url>.<signature b64url>`, signed with
the ed25519 service key, HttpOnly, SameSite=Strict, Path=/admin,
24-hour expiry. Reconfiguring ADMIN_PUBLISHER invalidates in-flight
sessions by name mismatch. Challenges live in a 10-min TTL map and
are tied to a specific name so one admin's challenge cannot authenticate
another.
Added:
service/admin.js route handlers + dispatch
service/admin-pages.js HTML templates for the four pages
service/admin-session.js challenge + cookie mint/verify + readSession
service/audit.js hash-chained append helper
service/modes.js publish-mode read/write with override semantics
schema: settings + audit tables
scripts/test-admin-roundtrip.mjs drives the whole flow (unauth redirect
→ challenge scrape → sign → login → dashboard → set mode → audit
inspection) with the same deterministic test key the listing-roundtrip
script uses. Withdraw-flow spot-check: create a listing, log in as
admin, withdraw it, confirm browse hides it and the detail page returns
404. All seven checks pass.