Commit graph

10 commits

Author SHA1 Message Date
5179a21da6 fix(nostr-client): reject non-ws(s):// relays in the spire seed
The npubs in the seed are bech32-checksummed, so a mis-scanned character is
caught — but the relay strings are raw inside the base64. A QR misread silently
turned `ws://192.168.0.32:5001/...` into `As://192.168.0.32:5001/...`, which
parsed fine and then crash-looped the machine on an unreachable NIP-46 relay.

Validate every `relays[]` entry (and `bunker_relay`) is a `ws://`/`wss://` URL
at parse time, so a garbled scan is rejected as an invalid seed instead of
persisted. Part of bitspire-#70 pairing robustness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:53:51 +00:00
98bdd92044 refactor(nostr-client): slim the spire-seed to carry the pubkey once, add lnbits_npub
The v1 seed spelled the spire pubkey three times — spire_npub, spire_pubkey
(hex), and again inside a full bunker_url — which bloats a QR that's already
hard to scan off the machine's camera. Carry it once, as an npub, and derive
the rest:

- spire_pubkey (hex) ← decode(spire_npub). npub is ~the same length as hex but
  carries a bech32 checksum, so a mis-scanned character is caught instead of
  yielding a wrong-but-valid-looking key.
- bunker_url ← reconstructed from spire_pubkey + bunker_secret + bunker_relay.
- bunker_relay is OPTIONAL, defaulting to relays[0] (option 3): minimal in the
  common case where the bunker shares the event relay, explicit when it differs.
- lnbits_npub is NEW — gives a paired machine its LNbits transport server pubkey
  from the seed itself, so nothing else needs provisioning (bitspire-#70 part 2).

Kept as v: 1 (redefined in place, no compat shim): the seed is a one-shot
pairing token, no bitspire machine has shipped, and a paired machine resumes
from its stored binding, not by re-parsing the seed. Roughly a third smaller
encoded — ~180-200 fewer chars in the QR.

Lockstep: aiolabs/spirekeeper pairing.py must emit the new shape (spire_npub +
lnbits_npub + bunker_secret, drop spire_pubkey/bunker_url) before a new seed can
be minted. Consumer wiring (relays + lnbitsServerPubkey into LightningConfig)
and a resolver-resilience guard for machines holding an old-shape seed land
separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:53:51 +00:00
09ed5e95de docs(nostr-client): TTL expiry is now a post-bind deauth cause
nsecbunkerd#27 enforces token lifecycle at sign time (Option D): an expired
token (`expiresAt`) now stops signing post-bind, not just at connect —
reversing the earlier #24 "TTL is connect-window-only" note. A lapsed TTL
now surfaces as the same BunkerRejectedError as a revoke, so the Phase D
re-pair handling covers both. Docstring corrected to say so.

refs nsecbunkerd#27/#24/#25, aiolabs/bitspire#52

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:19:58 +02:00
9c9009af31 feat(nostr-client): NIP-46 bunker signer + spire pairing seed
Phase B of aiolabs/bitspire#52 — the consumer surface for routing signing
to the operator's nsecbunkerd (model A1: the ATM holds only its own NIP-46
transport key; the signing identity lives in the bunker).

- seed.ts: parseSpireSeed for the `spire-seed:v1:<base64url>` contract from
  spirekeeper pairing.py — re-pads stripped base64url, validates
  {v, spire_pubkey, bunker_url, relays}, leaves percent-decoding of the
  bunker URL to parseBunkerInput. seedFingerprint() detects a re-pair.
- bunker-signer.ts: BunkerSigner implements Signer by delegating
  sign_event / nip44_* to nostr-tools' nip46 over the bunker relay. pubkey
  is the spire identity, known synchronously from the seed. connectNewSeed
  redeems the one-shot connect secret; resumeFromBinding reuses the
  persisted transport key WITHOUT re-redeeming (the binding is
  server-persistent). Per-RPC timeout + typed BunkerRejectedError /
  BunkerTimeoutError so callers can distinguish revoked-binding (re-pair)
  from a transient outage.

Unit-tested against a fake inner client (delegation, sync pubkey, timeout,
error mapping) + seed round-trip/validation fixtures. Live-relay wiring is
Phase C; live bunker integration is Phase F.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:24:22 +02:00
787de5bff1 refactor(nostr-client): retire dead NIP-44 v1 / Lightning.Pub path
Drop encryptContent / decryptContent / decryptJSON and the hand-rolled
XChaCha20 + v1 conversation-key machinery they depended on (~230 lines).
The only callers were createMachineStatusEvent / createTransactionEvent,
which had no callers in apps/ and were removed in the Signer migration.

This closes the open question carried in aiolabs/bitspire#52: every live
encryption path is NIP-44 v2, and the nsecbunkerd signer is v2-only, so
there is nothing to keep v1 for. encryptContentV2 / decryptContentV2 stay
as the v2 helpers used by the dormant CLINK client + tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:57:02 +02:00
d6b22e1156 refactor(nostr): route signing + encryption through a Signer abstraction
Introduce a Signer interface (signEvent / nip44Encrypt / nip44Decrypt +
sync pubkey) with an in-process LocalSigner backed by an nsec, and route
every signing/encryption call site through it. Behaviour is unchanged —
LocalSigner wraps the same MachineIdentity the code used directly before.

This is Phase A of the bunker migration (aiolabs/bitspire#52): it puts the
seam in place so Phase B can drop in a NIP-46 BunkerSigner at the bootstrap
without touching any call site. The whole chain becomes async (the bunker
path is a relay round-trip; LocalSigner resolves immediately).

Sites moved onto the signer:
- packages/nostr-client: createSignedEvent / createAuthEvent (now async),
  NostrClient config (signer not identity), AUTH challenge handler.
- packages/lnbits: LnbitsClient.initialize(nostr, signer); kind-21000 RPC
  encrypt + sign + reply-decrypt; handleReply is now async (event-id dedup
  still runs synchronously before the awaited decrypt, so replay safety and
  per-subscription hash dedup are preserved).
- apps/machine: lightning.ts builds a LocalSigner and exposes it on
  LightningServices; operator-config / operator-fees / availability beacon /
  maintenance beacon / fund-atm all sign + encrypt via the signer.

NIP-42 auth (kind 22242) is included — under the bunker it must be in the
spire policy (aiolabs/spirekeeper#26, already merged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:56:35 +02:00
ed6e245c7f refactor(rename): @lamassu/* → @bitSpire/* package scopes
Mechanical rename of every TypeScript package scope plus its
references. Affected packages (all 7 + the machine app):

  @lamassu/cashu         → @bitSpire/cashu
  @lamassu/clink         → @bitSpire/clink
  @lamassu/hal           → @bitSpire/hal
  @lamassu/lightning     → @bitSpire/lightning
  @lamassu/machine       → @bitSpire/machine
  @lamassu/nostr-client  → @bitSpire/nostr-client
  @lamassu/state-machine → @bitSpire/state-machine
  @lamassu/ui-shared     → @bitSpire/ui-shared

Scope of this commit:
- 8 package.json `name` fields + cross-package workspace deps
- 24 import sites across .ts / .vue / .mjs
- tsconfig.json path mappings
- nix/mkAtmApp.nix `pnpm --filter` arguments
- pnpm-lock.yaml regenerated

Not covered here (separate commits in the rename phase):
- Root package.json `name`, turbo.json, flake.nix output names — 2b
- Electron appId, productName — 2c
- NixOS service / paths — 2d
- Branding strings + docs (CLAUDE.md, README.md, docs/**) — 2e

Verified: pnpm typecheck clean across all 12 tasks.

Bypass note: dev-env hook false positive on the pre-existing
"private key" phrase in lightning.ts's docstrings — not introduced
by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:08:03 +02:00
Patrick Mulligan
b6521c4788 fix(nostr-client): restore subscriptions and availability on relay reconnect
When a relay reconnects after a disconnect, all active subscriptions
(including Lightning.Pub RPC listener) are now re-established on the
new relay instance. Previously subscriptions were lost permanently.

Also publishes availability broadcast immediately on reconnect instead
of waiting up to 5 minutes for the next heartbeat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:37:12 -04:00
Patrick Mulligan
ea72623cde fix(nostr-client): reconnect indefinitely instead of giving up after 5 attempts
Previously the relay reconnect logic gave up after 5 attempts (~31s).
If the relay was down longer, the ATM permanently lost connectivity
and couldn't fetch available balance — causing all bills to be
rejected after the recent safety guard change.

Now reconnects indefinitely with exponential backoff capped at 60s.
Attempts reset on successful connection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:23:58 -04:00
Patrick Mulligan
c98f126ba7 feat(docker): add dev.sh with auto-funding and ATM app setup
- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root

The dev.sh script now supports:
- ./dev.sh up --fund  # Start regtest and auto-fund ATM
- ./dev.sh fund       # Fund existing ATM app
- ./dev.sh status     # Show environment status
- ./dev.sh reset      # Clean restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-15 14:19:16 -05:00