Phase D: typed LNbits error codes + re-pair UX (#52) #61

Merged
padreug merged 2 commits from phase-d-rekey-ux into dev 2026-06-21 10:43:12 +00:00
Owner

Phase D of the bunker migration (#52) — error handling. Stacked on #60 (Phase C); targets phase-c-bunker-bootstrap so this diff is only Phase D (2 commits). Once #60 merges to dev, I'll retarget this to dev.

Two self-contained pieces, both tested:

1. feat(lnbits) — typed nostr-transport error codes (fc2b5d9)

The error-handling layer from the 2026-05-26 cross-session handshake. LnbitsClient now rejects ERROR responses with a typed LnbitsRpcError carrying the machine-readable code + its retryPolicy, so callers branch on disposition, not string-matching.

  • error-codes.ts: LnbitsErrorCode (14 codes, signer/transport/app classes) mirroring the lnbits canonical enum; retryPolicyFor(); LnbitsRpcError.fromResponse().
  • Safe to land before lnbits emits codes: error_code is optional-additive on the wire — an absent/unknown code maps to internal_error (retry-once). No string-matching, no special parser paths.
  • invoice_already_paid flagged terminal-idempotent for the cash-out resume-after-reboot case.
  • 8 new tests.

2. feat(machine) — re-pair UX on bunker deauth (b59b4ea)

A revoked / TTL-expired / off-policy binding (now all enforced post-bind per nsecbunkerd#27) surfaces a dedicated "Pairing Required" screen instead of a raw error; a signer/relay timeout shows "Signer Unreachable" (transient).

  • Shared classifyInitError() maps the typed BunkerRejectedError / BunkerTimeoutError (by name, so it survives bundle boundaries) to maintenance-screen sentinels, used at every store init catch + the App.vue fallback.
  • App.vue's nested-ternary screen copy refactored to a keyed map.
  • 5 new tests.

Review focus

  • packages/lnbits/src/error-codes.ts — confirm the 14 codes + retry policies match the lnbits canonical enum / docs/devs/nostr-transport.md (this is the drift-detection surface).
  • apps/machine/src/services/init-error.ts — the name-based classification (deliberately not instanceof, to survive the dynamic-import boundary in App.vue's maintenance path).

Deliberately deferred (remaining Phase D, follow-up)

  • State-machine retry switch — wiring retryPolicy into the cash-out XState flow (retry transient operator_signer_unavailable / rate_limited / internal_error; terminal on the rest) + invoice_already_paid → success-equivalent at the dispense path. This touches the critical cash-out path and is best validated against live lnbits error emission, not mocks. The foundation (retryPolicy / isRetryable) is in place for it.
  • Mid-session re-pair detection — flipping to the re-pair screen when a sign fails during a live flow (this PR covers boot-time, the dominant restart-after-revoke case).

Checks

typecheck 12/12; 156 tests pass (lnbits 19, machine 29); full electron prod build clean.

Do NOT use the MCP merge endpoint — merge via the Forgejo UI after review.

🤖 Generated with Claude Code

Phase D of the bunker migration (#52) — error handling. **Stacked on #60** (Phase C); targets `phase-c-bunker-bootstrap` so this diff is *only* Phase D (2 commits). Once #60 merges to `dev`, I'll retarget this to `dev`. Two self-contained pieces, both tested: ### 1. `feat(lnbits)` — typed nostr-transport error codes (`fc2b5d9`) The error-handling layer from the 2026-05-26 cross-session handshake. `LnbitsClient` now rejects ERROR responses with a typed `LnbitsRpcError` carrying the machine-readable `code` + its `retryPolicy`, so callers branch on disposition, not string-matching. - `error-codes.ts`: `LnbitsErrorCode` (14 codes, signer/transport/app classes) mirroring the lnbits canonical enum; `retryPolicyFor()`; `LnbitsRpcError.fromResponse()`. - **Safe to land before lnbits emits codes:** `error_code` is optional-additive on the wire — an absent/unknown code maps to `internal_error` (retry-once). No string-matching, no special parser paths. - `invoice_already_paid` flagged `terminal-idempotent` for the cash-out resume-after-reboot case. - 8 new tests. ### 2. `feat(machine)` — re-pair UX on bunker deauth (`b59b4ea`) A revoked / TTL-expired / off-policy binding (now all enforced post-bind per nsecbunkerd#27) surfaces a dedicated **"Pairing Required"** screen instead of a raw error; a signer/relay timeout shows **"Signer Unreachable"** (transient). - Shared `classifyInitError()` maps the typed `BunkerRejectedError` / `BunkerTimeoutError` (by `name`, so it survives bundle boundaries) to maintenance-screen sentinels, used at every store init catch + the App.vue fallback. - App.vue's nested-ternary screen copy refactored to a keyed map. - 5 new tests. ### Review focus - `packages/lnbits/src/error-codes.ts` — confirm the 14 codes + retry policies match the lnbits canonical enum / `docs/devs/nostr-transport.md` (this is the drift-detection surface). - `apps/machine/src/services/init-error.ts` — the `name`-based classification (deliberately not `instanceof`, to survive the dynamic-import boundary in App.vue's maintenance path). ### Deliberately deferred (remaining Phase D, follow-up) - **State-machine retry switch** — wiring `retryPolicy` into the cash-out XState flow (retry transient `operator_signer_unavailable` / `rate_limited` / `internal_error`; terminal on the rest) + `invoice_already_paid` → success-equivalent at the dispense path. This touches the **critical cash-out path** and is best validated against *live* lnbits error emission, not mocks. The foundation (`retryPolicy` / `isRetryable`) is in place for it. - **Mid-session re-pair detection** — flipping to the re-pair screen when a sign fails *during* a live flow (this PR covers boot-time, the dominant restart-after-revoke case). ### Checks typecheck 12/12; 156 tests pass (lnbits 19, machine 29); full electron prod build clean. Do NOT use the MCP merge endpoint — merge via the Forgejo UI after review. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements the error-handling layer agreed in the 2026-05-26 cross-session
handshake (aiolabs/bitspire#52). LnbitsClient now rejects ERROR responses
with a typed LnbitsRpcError carrying the machine-readable code + its retry
disposition, so callers (and the state machine, Phase D.3) branch on
disposition rather than string-matching the human-readable message.

- error-codes.ts: LnbitsErrorCode (14 codes, signer/transport/app classes)
  mirroring the lnbits canonical enum; retryPolicyFor() classifier;
  LnbitsRpcError.fromResponse().
- error_code is optional-additive on the wire: an absent or unknown code
  maps to internal_error (retry-once), so this is safe to land before lnbits
  emits codes — no string-matching, no special parser paths.
- invoice_already_paid is flagged terminal-idempotent (isIdempotentSuccess)
  for the cash-out resume-after-reboot case.

Part of Phase D, aiolabs/bitspire#52.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A revoked / TTL-expired / off-policy bunker binding now surfaces a dedicated
"Pairing Required" screen instead of a raw error, and a signer/relay timeout
shows "Signer Unreachable" (transient). Shared classifyInitError() maps the
typed BunkerRejectedError / BunkerTimeoutError (by name, so it survives bundle
boundaries) to maintenance-screen sentinels, used at every store init catch +
the App.vue fallback. App.vue's nested-ternary screen copy refactored to a
keyed map (cleaner, and the new screens drop in).

Scope: boot-time detection (covers the dominant restart-after-revoke case).
Mid-session re-pair detection (flipping the screen when a sign fails during a
live flow) is a deliberate follow-up.

Part of Phase D, aiolabs/bitspire#52.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padreug changed target branch from phase-c-bunker-bootstrap to dev 2026-06-21 10:35:57 +00:00
padreug force-pushed phase-d-rekey-ux from b59b4ea1d4 to 78d54cdc94 2026-06-21 10:38:12 +00:00 Compare
padreug deleted branch phase-d-rekey-ux 2026-06-21 10:43:13 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/bitspire!61
No description provided.