From 9f116ff1f86129fd12b5b3220663d49e83c2ec9c Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 28 May 2026 08:58:31 +0200 Subject: [PATCH] feat(signer): stop reading account.prvkey in merchant provision/rotate paths (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-cascade prerequisite for aiolabs/lnbits#17 (signer abstraction phase 1), which lands an m002 startup job that fail-closed NULLs the legacy `accounts.prvkey` column. This commit migrates the two sites in `views_api.py` that read `account.prvkey` so they no longer silently undo m002, and fail-closed cleanly when prvkey is missing. Scope intentionally narrow — this is the prvkey-elimination subset of aiolabs/nostrmarket#5. The full phase A (envelope-encrypt `merchants.private_key` → `signer_blob`) and phase B (route `Merchant.sign_hash` through core's `NostrSigner`) work remains tracked under that issue. ## What changed ### views_api.py — `_auto_create_merchant` Was: lazy fallback that, if `account.prvkey` was missing, generated a fresh keypair and wrote it back into the account (lines 112-118). After m002 NULLs `accounts.prvkey`, this regenerate-and-write-back path would silently undo the migration AND change the user's Nostr pubkey out from under them. Now: no longer touches the account. Asserts `account.prvkey` is present (matching the existing pubkey assertion) with a clear fail-closed message pointing at aiolabs/nostrmarket#5 for the phase A/B fix. For accounts that still carry a plaintext prvkey (pre-m002, FakeWallet local dev, etc.) the auto-provision path continues to work unchanged. For migrated accounts, the assertion fires fast with an actionable error. Removed the regenerate block entirely. Dropped now-unused imports: `update_account`, `generate_keypair`. ### views_api.py — `api_migrate_merchant_keys` Was: same `account and account.pubkey and account.prvkey` assertion with the generic message "Account has no Nostr keypair". Now: assertion updated with the same bridge-state framing — points at aiolabs/nostrmarket#5 for the phase A/B fix. ## Acceptance - [x] regenerate-and-write-back block removed (would undo m002) - [x] `account.prvkey` references in views_api.py are assertions only (fail-closed guards, not data reads) - [x] unused imports dropped (`update_account`, `generate_keypair`) - [x] error messages reference aiolabs/nostrmarket#5 for the phase A/B fix path Manual smoke / version bump / tag / catalog entry deferred until the lnbits cascade lands AND phase A's schema migration ships; this commit alone doesn't change the on-disk merchants table. ## Out of scope (per aiolabs/nostrmarket#5) - Phase A: envelope-encrypting `merchants.private_key` column. - Phase B (full): refactoring `Merchant.sign_hash` / `helpers.sign_message_hash` through core's `NostrSigner`. - Phase C: NIP-46 bunker + NIP-26 delegation variants. - Re-enabling `_create_default_merchant` on the lnbits core side. ## Cross-references - aiolabs/nostrmarket#5 — issue this is a partial step toward - aiolabs/lnbits#17 — the cascading signer-abstraction PR whose m002 fail-closed NULLs `accounts.prvkey` - aiolabs/lnbits#21 — umbrella audit (5 affected extensions) - aiolabs/events#23 / aiolabs/tasks#3 / aiolabs/restaurant#11 — sister migrations already on signer-abstraction branches Co-Authored-By: Claude Opus 4.7 (1M context) --- views_api.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/views_api.py b/views_api.py index 0e78bc3..9e0ce8b 100644 --- a/views_api.py +++ b/views_api.py @@ -4,7 +4,7 @@ from typing import List, Optional from fastapi import Depends from fastapi.exceptions import HTTPException -from lnbits.core.crud import get_account, update_account +from lnbits.core.crud import get_account from lnbits.core.services import websocket_updater from lnbits.decorators import ( WalletTypeInfo, @@ -12,7 +12,6 @@ from lnbits.decorators import ( require_invoice_key, ) from lnbits.utils.exchange_rates import currencies -from lnbits.utils.nostr import generate_keypair from loguru import logger from . import nostr_client, nostrmarket_ext @@ -101,21 +100,32 @@ async def _auto_create_merchant( ) -> Merchant: """ Lazy fallback: provision a merchant from the user's account keypair when - the LNbits-side eager provisioning didn't run (e.g., older accounts, or - upstream LNbits without our signup hook). + the LNbits-side eager provisioning didn't run. Delegates to services.provision_merchant — the canonical implementation. + + Pre-cascade bridge state (see aiolabs/nostrmarket#5): + After aiolabs/lnbits#17 m002 lands, `accounts.prvkey` is fail-closed + NULL'd for migrated accounts (the cleartext nsec lives encrypted in + `signer_config`, owned by the core signer abstraction). Auto-provision + cannot extract that cleartext to copy into `merchants.private_key`, + so this path fails-closed when prvkey is missing. The proper fix is + phase A (envelope-encrypt `merchants.private_key` → `signer_blob`) + followed by phase B (route `Merchant.sign_hash` through core's + `NostrSigner`) per aiolabs/nostrmarket#5. Until then, migrated + accounts must explicitly provision a merchant through the future + phase-A-aware flow. + + The previous regenerate-and-write-back block (generated a fresh + keypair and stored it into the account) was removed because it + would silently undo m002's NULL'ing. """ account = await get_account(wallet.wallet.user) assert account, "User account not found" - - # In our fork, accounts always have keypairs. Generate as fallback only - # if somehow missing (e.g., upstream LNbits where this isn't auto-set). - if not account.pubkey or not account.prvkey: - private_key, public_key = generate_keypair() - account.pubkey = public_key - account.prvkey = private_key - await update_account(account) + assert account.pubkey and account.prvkey, ( + "Account has no plaintext Nostr keypair available for merchant " + "provisioning (see aiolabs/nostrmarket#5 for the phase A/B fix)" + ) merchant = await provision_merchant( user_id=wallet.wallet.user, @@ -245,8 +255,13 @@ async def api_migrate_merchant_keys( assert merchant.id == merchant_id, "Wrong merchant ID" account = await get_account(wallet.wallet.user) + # account.prvkey is fail-closed NULL'd by aiolabs/lnbits#17 m002 + # for migrated accounts. Rotation cannot copy a cleartext nsec + # into merchants.private_key until phase A lands — see + # aiolabs/nostrmarket#5 for the migration plan. assert account and account.pubkey and account.prvkey, ( - "Account has no Nostr keypair" + "Account has no plaintext Nostr keypair available for key " + "rotation (see aiolabs/nostrmarket#5 for the phase A/B fix)" ) if account.pubkey == merchant.public_key: