feat(signer): nostr publish via resolve_for_wallet + door-scanner stats endpoint #24
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "signer-abstraction"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Two pieces of work that piggybacked on the
signer-abstractionbranch:Signer-abstraction migration —
nostr_hooks+nostr_publisherstop touching
account.prvkey. The extension now signs Nostrpublishes through
resolve_for_wallet(wallet_id)and an opaqueNostrSigner. Pre-cascade prerequisite foraiolabs/lnbits#17(signer abstraction phase 1), whose
m002startup job NULLs thelegacy
accounts.prvkeycolumn. After this lands, events workstransparently with
LocalSigner/RemoteBunkerSigner/ClientSideOnlySigner. Closesaiolabs/events#23.Door-scanner stats endpoint —
GET /events/api/v1/tickets/event/{event_id}/stats. HTTP mirror ofthe
events_list_event_ticketsnostr-transport RPC for callers thatdon't hold a raw user prvkey (the webapp post-#9 in particular —
useTicketScanner.refreshStatshad no working HTTP path). Withoutthis endpoint the activities scanner loaded initial counts via
fallbacks but every post-scan
refreshStatsreturned 404, leavingthe "Scanned" counter visually stuck at 0 even though registrations
landed correctly. Surfaced by aio-demo manual test on 2026-06-03.
Commits
66076d6feat(signer): migrate Nostr publishing off account.prvkey → resolve_for_wallet (#23)4238b41feat: GET /tickets/event/{event_id}/stats for door-scanner roster1fb96bfchore: bump config.json version to 1.6.1-aio.5Signer migration — what changed
nostr_hooks.py—publish_or_delete_nostr_eventWas: pulled
(account.pubkey, account.prvkey)from the wallet owner,passed both into the publisher. Hard-skipped publish when
account.prvkeywasNone.Now: calls
await resolve_for_wallet(event.wallet)(the DRY helperfrom
aiolabs/lnbits#23— wallet → account → signer →can_signcheck in one call, returns
Noneon any soft-fail). Passes theresolved
NostrSignerto the publisher. Soft-skip onNone(walletmissing, account unclassified, or
ClientSideOnlySignerwhere theserver has no signing authority) — matches the previous "no prvkey"
behavior.
nostr_publisher.py—publish_event_to_nostrWas: accepted
(account_pubkey, account_prvkey)and signed via alocal
sign_nostr_eventhelper that calledcoincurve.PrivateKey.sign_schnorron the plaintext nsec.Now: accepts
signer: NostrSigner. Builds the unsigned event dict(
kind/created_at/tags/content), hands it toawait signer.sign_event(...), reconstructs the localNostrEventmodel from the signed dict (
id/pubkey/sig). The signer backendis transparent.
Removed the
sign_nostr_eventhelper entirely + dropped thecoincurveimport — no direct crypto in this extension anymore.Door-scanner stats endpoint
Auth:
require_admin_key+ the event's wallet must be in the caller'suser.wallet_ids(matches the register endpoint's owner check).Response:
Only paid tickets are included (consistent with the door-scanner's
mental model — unpaid tickets don't exist on the roster).
Acceptance — signer migration
account.prvkey)publish_event_to_nostracceptsNostrSignerinstead of(pubkey, prvkey)sign_nostr_eventgone)grep -r account.prvkeyacrossevents/→ zero matchesCross-references
aiolabs/events#23— issue the signer migration closesaiolabs/lnbits#17— the cascading signer-abstraction PRaiolabs/lnbits#23— theresolve_for_wallethelper this consumesaiolabs/lnbits#26— phase 2.3 (sign_eventover bunker, validatedagainst
aiolabs/nsecbunkerd@fb1c239)aiolabs/lnbits#21— umbrella audit identifying 5 affectedextensions
Test plan
Signer migration:
LocalSigner-backedaccount → kind 31922/31923 publish lands on configured relays,
event.nostr_event_idpopulated.ClientSideOnlySigneraccount → publish soft-skips silently (no exception, no crash).
→ same soft-skip behavior as before.
aiolabs/lnbits#17'sm002NULLsaccounts.prvkey, runthe above three again → same outcomes (proves the migration is
not silently using the legacy column).
Door-scanner stats endpoint:
admin_keyagainst/tickets/event/{event_id}/stats→200 with the four scalar counters + per-ticket roster.
admin_key→ 401.admin_key(caller does not own the event's wallet)→ 403
"You do not own this event."event_id→ 404"Event does not exist."unregistered paid ones.
refreshStats→Scannedcounter increments without a page reload.🤖 Generated with Claude Code