Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
9f116ff1f8 feat(signer): stop reading account.prvkey in merchant provision/rotate paths (#5)
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) <noreply@anthropic.com>
2026-05-28 08:58:31 +02:00

View file

@ -4,7 +4,7 @@ from typing import List, Optional
from fastapi import Depends from fastapi import Depends
from fastapi.exceptions import HTTPException 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.core.services import websocket_updater
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
@ -12,7 +12,6 @@ from lnbits.decorators import (
require_invoice_key, require_invoice_key,
) )
from lnbits.utils.exchange_rates import currencies from lnbits.utils.exchange_rates import currencies
from lnbits.utils.nostr import generate_keypair
from loguru import logger from loguru import logger
from . import nostr_client, nostrmarket_ext from . import nostr_client, nostrmarket_ext
@ -101,21 +100,32 @@ async def _auto_create_merchant(
) -> Merchant: ) -> Merchant:
""" """
Lazy fallback: provision a merchant from the user's account keypair when 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 the LNbits-side eager provisioning didn't run.
upstream LNbits without our signup hook).
Delegates to services.provision_merchant the canonical implementation. 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) account = await get_account(wallet.wallet.user)
assert account, "User account not found" assert account, "User account not found"
assert account.pubkey and account.prvkey, (
# In our fork, accounts always have keypairs. Generate as fallback only "Account has no plaintext Nostr keypair available for merchant "
# if somehow missing (e.g., upstream LNbits where this isn't auto-set). "provisioning (see aiolabs/nostrmarket#5 for the phase A/B fix)"
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)
merchant = await provision_merchant( merchant = await provision_merchant(
user_id=wallet.wallet.user, user_id=wallet.wallet.user,
@ -245,8 +255,13 @@ async def api_migrate_merchant_keys(
assert merchant.id == merchant_id, "Wrong merchant ID" assert merchant.id == merchant_id, "Wrong merchant ID"
account = await get_account(wallet.wallet.user) 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, ( 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: if account.pubkey == merchant.public_key: