refactor(acl): align IMethod with NIP-46 wire-name vocabulary (#14)

NDK 3.x's `NDKNip46Backend` passes the wire method name verbatim
to `pubkeyAllowed` — `nip04_encrypt`, `nip04_decrypt`,
`nip44_encrypt`, `nip44_decrypt`, etc. NDK 2.8.1 normalized these to
`encrypt`/`decrypt` before calling the permit callback; that
normalization was the root of why our encrypt/decrypt path had
never worked end-to-end against lnbits's bunker-backed signer
(lnbits stores `PolicyRule.method` using wire names, our auth
lookup looked for the normalized name → no match → request fell
through to the never-resolved admin prompt and timed out at 15s).

Source `IMethod` directly from NDK's exported `NIP46Method` union so
it can't drift across future bumps. If NDK adds a new method
(e.g. `nip60_*`) we pick it up for free. Drop the `method as IMethod`
cast at the `signingAuthorizationCallback` call site — both sides
now share the same vocabulary by construction.

This is the substantive win that aiolabs/nsecbunkerd#14 is filed for.
With this commit:

- `sign_event` policy rules with kinds continue to match exactly as
  before (kind stringification path unchanged).
- `nip04_encrypt` / `nip04_decrypt` / `nip44_encrypt` / `nip44_decrypt`
  policy rules — kind-less — now match the live-policy join (step 4
  of `checkIfPubkeyAllowed`) by their method-name alone. lnbits's
  bunker-mediated `signer.nip44_decrypt` and `signer.nip44_encrypt`
  calls (per `aiolabs/lnbits` PR #38 phase 2.4) start succeeding
  end-to-end against any operator account whose Policy carries those
  rules — which `_ensure_policy`'s self-heal already ensures for
  every newly-bound operator (per coord log 2026-05-30T22:00Z).
- `switch_relays` (new in NDK 3) flows through the auth check the
  same way as any other method.

`requestToSigningConditionQuery` needs no further change — the
existing `sign_event` switch case covers the only method that
discriminates on kind; all other methods use the default
`{ method }` query against the override layer, which is correct
for the kind-less wire names too.

Refs aiolabs/nsecbunkerd#14, aiolabs/nsecbunkerd#11 (whose live-policy
join this finally puts to use).
This commit is contained in:
Padreug 2026-05-31 12:15:57 +02:00
commit db1a834587
2 changed files with 23 additions and 7 deletions

View file

@ -1,4 +1,4 @@
import { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk';
import { NDKEvent, NostrEvent, NIP46Method } from '@nostr-dev-kit/ndk';
import prisma from '../../../db.js';
/**
@ -114,7 +114,26 @@ export async function checkIfPubkeyAllowed(
return undefined;
}
export type IMethod = "connect" | "sign_event" | "encrypt" | "decrypt" | "ping";
/**
* Sign-time auth method names follow the NIP-46 wire convention as
* NDK 3.x's `NDKNip46Backend` passes them through to `pubkeyAllowed`
* verbatim (it stopped normalizing `nip04_encrypt`/`nip04_decrypt`
* to `encrypt`/`decrypt` somewhere between 2.8.1 and current
* upstream).
*
* lnbits's `_ensure_policy` writes `PolicyRule.method` using the same
* wire-name vocabulary (`nip04_encrypt`, `nip04_decrypt`,
* `nip44_encrypt`, `nip44_decrypt`, `sign_event`, `get_public_key`,
* `connect`, `ping`). With the wire-name vocabulary on both sides,
* the post-#11 live-policy join (step 4 of `checkIfPubkeyAllowed`)
* naturally matches lnbits's stored rules no `encrypt → nip04_encrypt`
* adapter layer needed.
*
* Source the type from NDK itself so it can't drift across future
* NDK bumps; if NDK adds a new method (e.g. `nip60_*`) we pick it up
* for free.
*/
export type IMethod = NIP46Method;
export type IAllowScope = {
kind?: number | 'all';

View file

@ -1,10 +1,7 @@
import NDK, { NDKPrivateKeySigner, Nip46PermitCallback, Nip46PermitCallbackParams } from '@nostr-dev-kit/ndk';
import { nip19, utils as nostrUtils } from 'nostr-tools';
import { Backend } from './backend/index.js';
import {
IMethod,
checkIfPubkeyAllowed,
} from './lib/acl/index.js';
import { checkIfPubkeyAllowed } from './lib/acl/index.js';
import AdminInterface from './admin/index.js';
import { IConfig } from '../config/index.js';
import { NDKRpcRequest } from '@nostr-dev-kit/ndk';
@ -107,7 +104,7 @@ function signingAuthorizationCallback(keyName: string, adminInterface: AdminInte
}
try {
const keyAllowed = await checkIfPubkeyAllowed(keyName, remotePubkey, method as IMethod, payload);
const keyAllowed = await checkIfPubkeyAllowed(keyName, remotePubkey, method, payload);
if (keyAllowed === true || keyAllowed === false) {
console.log(`🔎 ${nip19.npubEncode(remotePubkey)} is ${keyAllowed ? 'allowed' : 'denied'} to ${method} with key ${keyName}`);