Commit graph

5 commits

Author SHA1 Message Date
a059e3f596 refactor: rename extension identity to spirekeeper
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>
2026-06-13 22:30:05 +02:00
aeaee1f568 refactor(v2): extract kind-30078 publish primitives to nostr_publish.py (#39 1/3)
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>
2026-06-01 19:54:08 +02:00
dcd08748a7 revert(v2): drop NIP-78 fleet publishing (privacy by default)
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>
2026-05-26 23:20:24 +02:00
e13178d3ac feat(v2): route nostr_publish signing through lnbits#17 signer abstraction (hybrid)
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>
2026-05-26 22:24:29 +02:00
131ff92aa8 feat(v2): publish operator-signed kind:30078 fleet roster + per-machine config (S4)
Closes aiolabs/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>
2026-05-26 20:28:26 +02:00