Skip to content

Add audit trail for admin-issued preload UCAN signing operations #231

@rabble

Description

@rabble

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-430generate_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions