feat(pairing): slim the spire seed + carry lnbits_npub (bitspire-#70)
Some checks failed
ci.yml / feat(pairing): slim the spire seed + carry lnbits_npub (bitspire-#70) (pull_request) Failing after 0s
Some checks failed
ci.yml / feat(pairing): slim the spire seed + carry lnbits_npub (bitspire-#70) (pull_request) Failing after 0s
Mint the new-shape seed the bitspire consumer now expects: the pubkey rides
once as spire_npub (consumer derives the hex + reconstructs bunker_url from
bunker_secret + bunker_relay|relays[0]), and lnbits_npub is embedded so a paired
machine reaches this lnbits' nostr-transport with nothing else provisioned.
- build_seed_url emits {spire_npub, lnbits_npub, bunker_secret, relays} and
bunker_relay only when it differs from relays[0] (omitted in the common case).
Drops spire_pubkey + the full bunker_url from the payload.
- pair_spire reads settings.nostr_transport_public_key, hex_to_npub's it, and
raises PairingError when it's empty (transport not running → can't mint a
self-sufficient seed). bunker_url is still returned in PairResult for operator
display / audit; only the seed stops embedding it.
Consumer side: bitspire packages/nostr-client/src/seed.ts + the #70 machine
wiring. Kept as v: 1 (redefined in place; no shipped seed to preserve).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b93e3fb698
commit
9dc4d09973
2 changed files with 105 additions and 21 deletions
57
pairing.py
57
pairing.py
|
|
@ -23,15 +23,21 @@ We deliberately do NOT run the connect/eager-bind step here: the spire is the
|
|||
NIP-46 client, so the binding must happen spire-side with the spire's own
|
||||
client keypair. spirekeeper only mints + packages.
|
||||
|
||||
Seed URL wire format (contract shared with bitspire#52):
|
||||
Seed URL wire format (contract shared with bitspire#52, slimmed in bitspire#70):
|
||||
|
||||
spire-seed:v1:<base64url(json)> json = {
|
||||
"v": 1,
|
||||
"spire_npub": "npub1…", # the bunker-minted spire identity
|
||||
"spire_pubkey": "<64-hex>", # same key, hex (consumer convenience)
|
||||
"bunker_url": "bunker://<spire_pubkey>?relay=<bunker_relay>&secret=<sec>",
|
||||
"relays": ["wss://…"], # relays for the spire's own events
|
||||
"spire_npub": "npub1…", # the bunker-minted spire identity
|
||||
"lnbits_npub": "npub1…", # this lnbits' nostr-transport server id
|
||||
"bunker_secret": "<sec>", # one-shot NIP-46 connect token
|
||||
"relays": ["wss://…"], # relays for the spire's own events
|
||||
"bunker_relay": "wss://…", # OPTIONAL — omitted when == relays[0]
|
||||
}
|
||||
|
||||
The pubkey is carried ONCE, as an npub: the consumer derives spire_pubkey (hex)
|
||||
and reconstructs the bunker:// URL from spire_npub + bunker_relay|relays[0] +
|
||||
bunker_secret. `lnbits_npub` lets a paired machine reach this lnbits' transport
|
||||
with nothing else provisioned. See bitspire packages/nostr-client/src/seed.ts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -48,6 +54,7 @@ from lnbits.core.services.nsec_bunker import (
|
|||
)
|
||||
from lnbits.core.signers.remote_bunker import ensure_policy
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.nostr import hex_to_npub
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .models import Machine
|
||||
|
|
@ -136,15 +143,27 @@ def _recover_token(tokens: list[dict], client_name: str) -> str:
|
|||
|
||||
|
||||
def build_seed_url(
|
||||
*, spire_npub: str, spire_pubkey_hex: str, bunker_url: str, relays: list[str]
|
||||
*,
|
||||
spire_npub: str,
|
||||
lnbits_npub: str,
|
||||
bunker_secret: str,
|
||||
relays: list[str],
|
||||
bunker_relay: str | None = None,
|
||||
) -> str:
|
||||
payload = {
|
||||
"""Build the slim seed URL (bitspire#70). The pubkey rides once as
|
||||
`spire_npub`; the consumer derives the hex + reconstructs `bunker_url` from
|
||||
`bunker_secret` + `bunker_relay` (or `relays[0]`). `bunker_relay` is emitted
|
||||
only when it differs from `relays[0]`, keeping the common case one field
|
||||
lighter."""
|
||||
payload: dict = {
|
||||
"v": 1,
|
||||
"spire_npub": spire_npub,
|
||||
"spire_pubkey": spire_pubkey_hex,
|
||||
"bunker_url": bunker_url,
|
||||
"lnbits_npub": lnbits_npub,
|
||||
"bunker_secret": bunker_secret,
|
||||
"relays": relays,
|
||||
}
|
||||
if bunker_relay and relays and bunker_relay != relays[0]:
|
||||
payload["bunker_relay"] = bunker_relay
|
||||
blob = (
|
||||
base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode())
|
||||
.decode()
|
||||
|
|
@ -233,15 +252,31 @@ async def pair_spire(
|
|||
token = _recover_token(tokens, client_name)
|
||||
_, _, secret = token.partition("#")
|
||||
|
||||
# The spire needs THIS lnbits' nostr-transport server identity to reach the
|
||||
# backend from the seed alone (bitspire#70). The transport sets
|
||||
# `settings.nostr_transport_public_key` at startup; if it's empty the
|
||||
# transport isn't running, so we can't mint a self-sufficient seed.
|
||||
lnbits_pubkey_hex = settings.nostr_transport_public_key
|
||||
if not lnbits_pubkey_hex:
|
||||
raise PairingError(
|
||||
"LNbits nostr transport has no server pubkey "
|
||||
"(settings.nostr_transport_public_key is empty) — is the transport "
|
||||
"running? Cannot mint a self-sufficient seed."
|
||||
)
|
||||
lnbits_npub = hex_to_npub(lnbits_pubkey_hex)
|
||||
|
||||
# bunker_url is still returned in PairResult (operator display / audit); the
|
||||
# seed itself no longer embeds it — the consumer reconstructs it.
|
||||
bunker_url = (
|
||||
f"bunker://{spire_pubkey_hex}?relay={quote(relay, safe='')}"
|
||||
f"&secret={quote(secret, safe='')}"
|
||||
)
|
||||
seed_url = build_seed_url(
|
||||
spire_npub=spire_npub,
|
||||
spire_pubkey_hex=spire_pubkey_hex,
|
||||
bunker_url=bunker_url,
|
||||
lnbits_npub=lnbits_npub,
|
||||
bunker_secret=secret,
|
||||
relays=relays,
|
||||
bunker_relay=relay,
|
||||
)
|
||||
return PairResult(
|
||||
spire_npub=spire_npub,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue