refactor(pairing): use lnbits' public ensure_policy, drop fork duplicate (#9)
Some checks failed
ci.yml / refactor(pairing): use lnbits' public ensure_policy, drop fork duplicate (#9) (pull_request) Failing after 0s
Some checks failed
ci.yml / refactor(pairing): use lnbits' public ensure_policy, drop fork duplicate (#9) (pull_request) Failing after 0s
Adopts aiolabs/lnbits#55 (merged b5fba561): pair_spire now calls the public ensure_policy(client, name='spirekeeper-spire', rules=..., methods_no_kind=...) instead of spirekeeper's cache-free _ensure_spire_policy copy. #55 re-keyed _POLICY_ID_CACHE on (admin_pubkey, policy_name), so the shared helper no longer returns the wrong (lnbits-default) id for a non-default policy name — the exact reason the duplicate existed. Net -45 LOC, one less fork-divergent reimplementation to keep in sync. Requires lnbits >= the #55 merge (ensure_policy importable) — already true on dev/demo. Tests: FakeBunker gains admin_pubkey; an autouse fixture clears lnbits' _POLICY_ID_CACHE between tests (the shared helper caches, unlike the old local one). 203 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
761f078053
commit
9c5f07c72e
2 changed files with 21 additions and 51 deletions
59
pairing.py
59
pairing.py
|
|
@ -14,7 +14,7 @@ reference for the admin chain):
|
|||
spirekeeper (here) spire, at first boot (bitspire#52)
|
||||
────────────────── ──────────────────────────────────
|
||||
1. create_new_key 5. NIP-46 connect — redeem the token with a
|
||||
2. _ensure_spire_policy freshly-generated *client* keypair; bunker
|
||||
2. ensure_policy freshly-generated *client* keypair; bunker
|
||||
3. create_new_token binds (client_pubkey → spire key). The
|
||||
4. get_key_tokens ─ seed ─► client_nsec stays on the spire; the
|
||||
(package token in URL) signing key never leaves the bunker.
|
||||
|
|
@ -46,6 +46,7 @@ from lnbits.core.services.nsec_bunker import (
|
|||
NsecBunkerNotConfiguredError,
|
||||
npub_to_hex,
|
||||
)
|
||||
from lnbits.core.signers.remote_bunker import ensure_policy
|
||||
from lnbits.settings import settings
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -103,55 +104,6 @@ def spire_key_name(machine_id: str) -> str:
|
|||
return f"spire-{machine_id}"
|
||||
|
||||
|
||||
async def _ensure_spire_policy(client: NsecBunkerAdminClient) -> int:
|
||||
"""Find-or-create the shared `spirekeeper-spire` policy and reconcile
|
||||
(add-only) any missing rules. Returns its bunker-assigned id.
|
||||
|
||||
Mirrors lnbits' `_ensure_policy` but with the spire rule set and **no
|
||||
admin-pubkey cache** — lnbits' cache is keyed on `admin_pubkey` alone
|
||||
(remote_bunker.py:157), so reusing its helper for a second policy name
|
||||
would return the wrong (cached lnbits-default) id. Pairing is an
|
||||
infrequent operator action, so the extra `get_policies` round-trip is
|
||||
cheap and correct. (Filed as an lnbits follow-up: cache key should
|
||||
include policy_name.)
|
||||
|
||||
Add-only, never remove: the policy may be shared across spires (and in
|
||||
principle other consumers); removing a rule would affect them all.
|
||||
"""
|
||||
policies = await client.get_policies()
|
||||
existing = [p for p in policies if p.get("name") == SPIRE_POLICY_NAME]
|
||||
if existing:
|
||||
policy = max(existing, key=lambda p: int(p["id"]))
|
||||
policy_id = int(policy["id"])
|
||||
live_rules = policy.get("rules") or []
|
||||
else:
|
||||
policy_id = await client.create_new_policy(
|
||||
SPIRE_POLICY_NAME, SPIRE_POLICY_RULES
|
||||
)
|
||||
live_rules = list(SPIRE_POLICY_RULES)
|
||||
|
||||
def _norm(method: str, kind) -> tuple[str, str | None]:
|
||||
# nsecbunkerd stores kind as a string; None = kind-less rule.
|
||||
return (method, str(kind) if kind is not None else None)
|
||||
|
||||
live_keys = {
|
||||
_norm(r["method"], r.get("kind"))
|
||||
for r in live_rules
|
||||
if isinstance(r, dict) and r.get("method")
|
||||
}
|
||||
for rule in SPIRE_POLICY_RULES:
|
||||
key = _norm(rule["method"], rule.get("kind"))
|
||||
if key not in live_keys:
|
||||
await client.add_policy_rule(policy_id, rule)
|
||||
live_keys.add(key)
|
||||
for method in SPIRE_POLICY_METHODS_NO_KIND:
|
||||
key = _norm(method, None)
|
||||
if key not in live_keys:
|
||||
await client.add_policy_rule(policy_id, {"method": method})
|
||||
live_keys.add(key)
|
||||
return policy_id
|
||||
|
||||
|
||||
def _recover_token(tokens: list[dict], client_name: str) -> str:
|
||||
"""Pull the freshly-issued `<npub>#<secret>` token out of the bunker's
|
||||
`get_key_tokens` response. Match by client name when the bunker
|
||||
|
|
@ -238,7 +190,12 @@ async def pair_spire(
|
|||
try:
|
||||
spire_npub = await admin_client.create_new_key(key_name, passphrase)
|
||||
spire_pubkey_hex = npub_to_hex(spire_npub)
|
||||
policy_id = await _ensure_spire_policy(admin_client)
|
||||
policy_id = await ensure_policy(
|
||||
admin_client,
|
||||
name=SPIRE_POLICY_NAME,
|
||||
rules=SPIRE_POLICY_RULES,
|
||||
methods_no_kind=SPIRE_POLICY_METHODS_NO_KIND,
|
||||
)
|
||||
await admin_client.create_new_token(key_name, client_name, policy_id)
|
||||
tokens = await admin_client.get_key_tokens(key_name)
|
||||
except NsecBunkerNotConfiguredError as exc:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,17 @@ _BUNKER_RELAY = "wss://bunker.internal/relay"
|
|||
_PASSPHRASE = "keystore-pass" # pragma: allowlist secret
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_policy_cache():
|
||||
# lnbits' ensure_policy caches resolved policy ids on
|
||||
# (admin_pubkey, name); clear between tests so each FakeBunker's
|
||||
# canned policy state is honoured rather than a stale cached id.
|
||||
from lnbits.core.signers import remote_bunker
|
||||
|
||||
remote_bunker._POLICY_ID_CACHE.clear()
|
||||
yield
|
||||
|
||||
|
||||
def _machine(mid: str = "m1") -> Machine:
|
||||
return Machine(
|
||||
id=mid,
|
||||
|
|
@ -56,6 +67,8 @@ def _machine(mid: str = "m1") -> Machine:
|
|||
class FakeBunker:
|
||||
"""Records calls; returns canned bunker responses."""
|
||||
|
||||
admin_pubkey = "fake-admin-pubkey"
|
||||
|
||||
# pragma: allowlist secret
|
||||
def __init__(self, *, policies=None, token_secret="s3cr3t"):
|
||||
self._policies = policies or []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue