Fork of satmachineadmin's v2-bitspire line into its own repo. Renames
both identifiers so this extension is fully independent of the original
satmachineadmin install (which remains in service):
- extension id satmachineadmin -> spirekeeper
(router prefix, static path/static_url_for, module symbols, task
names, templates dir, config/manifest paths)
- database name satoshimachine -> spirekeeper
(Database(ext_spirekeeper), all schema-qualified table refs)
Also resets versioning to 0.1.0, sets the display name + manifest to
spirekeeper/aiolabs, and fixes the placeholder pyproject description.
Historical aiolabs/satmachineadmin#N issue references in comments are
left pointing at the original repo where those issues live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layer 2 prep: a second consumer (fee_transport.py for #39) is about to
land that uses the same operator-signer + NIP-44 v2 + nostrclient publish
flow as cassette_transport.py. Extracting shared primitives now rather
than duplicating ~100 lines.
New `nostr_publish.py` module:
- Error hierarchy: NostrPublishError base + OperatorIdentityMissing,
SignerUnavailable, RelayUnavailable subclasses (all transport-layer
failures, domain-agnostic).
- `resolve_operator_signer(operator_user_id)` — fetch account + resolve
to NostrSigner, with the can-sign + has-pubkey checks.
- `sign_as_operator(operator_user_id, event)` — wrap signer.sign_event,
set created_at before signing.
- `nip44_encrypt_via_signer` + `nip44_decrypt_via_signer` — transitional
LocalSigner → RemoteBunkerSigner cascade (bunker handles natively;
LocalSigner falls back to hand-rolled NIP-44 v2 against the stored
prvkey).
- `publish_signed_event(signed)` — nostrclient relay-manager publish
with lazy import + RelayUnavailable on missing extension.
- High-level `publish_encrypted_kind_30078(operator_user_id,
recipient_pubkey_hex, d_tag, payload)` — builds event, encrypts via
signer, signs, publishes. The whole flow in one call; callers
(cassette_transport, soon fee_transport) just specify domain.
`cassette_transport.py`:
- Imports from nostr_publish; CassetteTransportError becomes a subclass
of NostrPublishError so existing catches still work.
- `publish_to_atm` reduces to a thin wrapper that builds the
cassette-specific payload + d-tag and delegates to
`publish_encrypted_kind_30078`.
- Consumer path (`decrypt_and_parse_state_event`) still owns
cassette-specific decode/transient distinctions; uses imported
`nip44_decrypt_via_signer`.
- Re-exports OperatorIdentityMissing / SignerUnavailable /
RelayUnavailable so views_api can keep importing from
cassette_transport without change.
`tasks.py` — cassette bootstrap consumer imports `resolve_operator_signer`
from nostr_publish directly instead of the cassette_transport
underscore-prefixed name.
164/164 tests green; behavior unchanged.
Refs: aiolabs/satmachineadmin#37 (parent), #39 (Layer 2, this commit
is prep).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pulls the kind:30078 per-machine config + fleet roster publish path
introduced at 131ff92. The default-public posture leaked operator
fleet composition (which npubs they run, where they're located, fiat
codes) to whatever relays nostrclient was configured with — a robbery
/ competitor-intel / extortion target surface the operator never
opted into.
Privacy by default is the operator's stated preference: nothing about
the fleet goes on relays unless the operator explicitly opts in via a
future toggle. Roster lookups now read from satmachineadmin's local
DB only (the S6 LNbits-side roster-gating becomes a local-DB-read
story, not a public-relay subscription).
Pre-launch — no external consumer to coordinate with, so the rip-out
is clean. Future opt-in publishing tracked in follow-up issue.
Removed:
- nostr_publish.py (publish_machine_config / publish_fleet_roster /
tombstone_machine_config / _sign_as_operator hybrid)
- The three publish call sites in api_create_machine /
api_update_machine / api_delete_machine.
Heartbeat-style public metadata (the kind of info bitSpire already
emits about machine liveness, location, active state) is still a
legitimate publish target — but that's the ATM's job, not the
operator's. Designed in the follow-up issue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Responds to the lnbits session's 19:30Z coordination-log flag: PR #17
will NULL `accounts.prvkey` on cascade via the m002 classify job, which
would break the S4 fleet-roster publishing path (`131ff92`) — it reads
`account.prvkey` directly.
Hybrid migration in `_sign_as_operator`:
1. Try `from lnbits.core.signers import resolve_signer` — post-#17
lnbits provides this; routes through the per-account signer that
understands LocalSigner (envelope-encrypted nsec at rest),
ClientSideOnlySigner (server can't sign — soft-fail), and the
future RemoteBunkerSigner (lnbits#18; phase 2).
2. On ImportError, fall through to the direct `account.prvkey` read
identical to the pre-#17 implementation. Same wire-level signed
event either way; the fallback exists only to avoid a hard
ordering dependency between this commit and the lnbits #17
cascade landing on the host.
Soft-failure surfaces (all log + skip, don't break machine CRUD):
- operator has no pubkey on file → skip.
- signer resolve fails (unclassified account, etc.) → skip.
- `signer.can_sign()` False (ClientSideOnlySigner) → skip.
- `SignerUnavailableError` raised at sign time → skip.
Why hybrid instead of waiting for #17 to land first: pre-#17 lnbits is
what's currently in production / dev. If we ship a hard `from
lnbits.core.signers import ...` now, satmachineadmin breaks at import
time on every host running the current nostr-transport branch. The
try/except guard is the same shape lnbits core uses for cross-extension
imports (nostrmarket / nostrrelay).
Sister migrations on other extensions (nostrmarket, restaurant, tasks,
events) are tracked at `aiolabs/lnbits#21` umbrella + per-extension
issues that the lnbits session filed in the 2026-05-26T20:00Z audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closesaiolabs/satmachineadmin#18 (S4 — NIP-78 per-machine config +
fleet roster). On every machine create/update/delete, publish two
operator-signed kind:30078 (NIP-78 addressable) events via the
`nostrclient` LNbits extension:
- `bitspire-config:<machine_id>` — per-machine config event, one
per machine. Tagged with `p=<atm_npub>` so external observers
can filter by ATM pubkey: `{"#p": ["<atm_npub>"]}`.
- `bitspire-fleet` — aggregate roster across the operator's
active fleet. Lists every machine's atm_pubkey + display fields.
Tagged with `p=<atm_npub>` per active machine.
Delete path tombstones the per-machine config (replaceable kind:30078
with `content.deleted=true`) and re-publishes the roster without the
machine — external readers see the tombstone OR the absence from the
roster.
Implementation choice — direct in-process singleton import (path b
from the pre-flight check, not the WebSocket path a):
from nostrclient.router import nostr_client
nostr_client.relay_manager.publish_message(json.dumps(["EVENT", e]))
Bypasses the public/private WebSocket entirely. Cleaner than going
through `wss://localhost/nostrclient/api/v1/<encrypted_ws_id>`. Same
cross-extension import pattern lnbits core uses for
nostrmarket.services + nostrrelay.crud (guarded by try/except).
Soft-failure throughout:
- nostrclient extension not installed → log warning + skip.
- Operator account has no Nostr keypair on file (account never went
through Nostr-login flow, or post-bunker future where nsec is
moved off-disk per lnbits#18) → log warning + skip.
- The settlement / distribution path does NOT depend on the publish
— these events exist for external observers, not internal flow
control.
Out of scope (intentionally):
- ATM-side consumer in lamassu-next (forward-looking, will read
`#p=<atm_npub>` to learn its operator's config).
- LNbits-server-side roster-gating in the nostr-transport handler
(S6 / lnbits#14 Item 3 — needs satmachineadmin to publish first;
this commit lays the groundwork).
- Operator's NIP-65 relay list as the publish target (today we use
whatever nostrclient is configured with; future per-operator
relay lists can live on accounts.relays or similar).
m006 (the canonical-vocabulary rename migration shipped at d717a6e)
ran cleanly against the regtest container on lnbits restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>