Context
PR [feat/preload-sign-after-claim] removes the is_unclaimed gate at api/src/api/http/nostr_rpc.rs:359 so admin-issued preload UCANs can sign for users after they claim their accounts. This unblocks publishing additional legacy Vine archives discovered after handover.
That change expands what an admin token can do, but does not add corresponding observability. We currently have no durable record of which admin signed what as which user. The mint side logs (tracing::info!("User token generated for pubkey: ... by admin: ...")) but those are ephemeral — they roll out of Cloud Logging retention and are not queryable months later.
If a user reports a surprise event in their feed, we have no way to trace it back to which admin issued the signing UCAN that produced it.
What we need
Per-sign audit row written from the signing path, ideally without slowing the hot path measurably.
Schema (extend existing admin_audit_events table)
-- existing table per recent commits 135dc74, 95caa55
INSERT INTO admin_audit_events
(action, admin_pubkey, target_pubkey, tenant_id, metadata, created_at)
VALUES
('preload_sign', '<from UCAN issued_by_admin fact>', '<user pubkey>', $tenant,
'{"event_id": "...", "kind": 1, "ucan_jti": "..."}'::jsonb, NOW());
metadata JSON should include at minimum:
event_id — the signed event id (so we can correlate with what landed on relays)
kind — Nostr event kind
ucan_jti or fingerprint — to group all signs from a single issued UCAN
created_at of the signed event (vs. row insert time, in case of backdating)
Insertion site
Best place: inside HttpRpcHandler::sign_event (or whatever the equivalent is for encrypt/decrypt — those should be audited too) when the handler was created from a server-signed preload UCAN. The handler already knows whether it was loaded via load_preloaded_user_handler vs load_handler_on_demand (OAuth path); we should plumb a tag through so we only audit preload-mode operations, not OAuth operations (those have their own per-authorization records).
The issued_by_admin field is already in the UCAN facts (set at admin.rs:413 in generate_preload_ucan), so we can read it from the UCAN at handler-load time and stash it on the handler struct.
Performance
- The cache hit path (
get_handler fast path, nostr_rpc.rs:439) cannot afford a synchronous DB write per sign. Either:
- Async fire-and-forget: spawn a tokio task to insert; accept that we may lose audit rows on crash.
- Buffered batch writer: queue audit rows in memory, flush every N seconds or M rows. Lower-overhead but more complex; needs a graceful-shutdown flush.
- Either way, audit insert failures should NOT fail the sign operation. Log and continue.
UI
A /admin (full admin only) view to list/search audit rows — at minimum:
- Filter by
target_pubkey (so we can answer "what did we sign for user X")
- Filter by
admin_pubkey (for "what did admin Y do")
- Filter by date range
- Show event_id with link to the event on a relay UI
Why we punted
We needed the gate dropped immediately to publish the next batch of legacy Vines. Audit logging was scoped out to land separately rather than block the unblock.
Related
- PR
feat/preload-sign-after-claim — drops the gate this issue compensates for
api/src/api/http/admin.rs:396-430 — generate_preload_ucan, where issued_by_admin is set
core/src/repositories/admin_audit_events.rs (or wherever the existing helper lives) — extend rather than re-invent
Context
PR [feat/preload-sign-after-claim] removes the
is_unclaimedgate atapi/src/api/http/nostr_rpc.rs:359so admin-issued preload UCANs can sign for users after they claim their accounts. This unblocks publishing additional legacy Vine archives discovered after handover.That change expands what an admin token can do, but does not add corresponding observability. We currently have no durable record of which admin signed what as which user. The mint side logs (
tracing::info!("User token generated for pubkey: ... by admin: ...")) but those are ephemeral — they roll out of Cloud Logging retention and are not queryable months later.If a user reports a surprise event in their feed, we have no way to trace it back to which admin issued the signing UCAN that produced it.
What we need
Per-sign audit row written from the signing path, ideally without slowing the hot path measurably.
Schema (extend existing
admin_audit_eventstable)metadataJSON should include at minimum:event_id— the signed event id (so we can correlate with what landed on relays)kind— Nostr event kinducan_jtior fingerprint — to group all signs from a single issued UCANcreated_atof the signed event (vs. row insert time, in case of backdating)Insertion site
Best place: inside
HttpRpcHandler::sign_event(or whatever the equivalent is forencrypt/decrypt— those should be audited too) when the handler was created from a server-signed preload UCAN. The handler already knows whether it was loaded viaload_preloaded_user_handlervsload_handler_on_demand(OAuth path); we should plumb a tag through so we only audit preload-mode operations, not OAuth operations (those have their own per-authorization records).The
issued_by_adminfield is already in the UCAN facts (set atadmin.rs:413ingenerate_preload_ucan), so we can read it from the UCAN at handler-load time and stash it on the handler struct.Performance
get_handlerfast path,nostr_rpc.rs:439) cannot afford a synchronous DB write per sign. Either:UI
A
/admin(full admin only) view to list/search audit rows — at minimum:target_pubkey(so we can answer "what did we sign for user X")admin_pubkey(for "what did admin Y do")Why we punted
We needed the gate dropped immediately to publish the next batch of legacy Vines. Audit logging was scoped out to land separately rather than block the unblock.
Related
feat/preload-sign-after-claim— drops the gate this issue compensates forapi/src/api/http/admin.rs:396-430—generate_preload_ucan, whereissued_by_adminis setcore/src/repositories/admin_audit_events.rs(or wherever the existing helper lives) — extend rather than re-invent