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>
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>