feat(#14): bump @nostr-dev-kit/ndk 2.8.1 → 3.0.3 + nostr-tools v1 → v2.20 + acl wire-name vocabulary #15

Merged
padreug merged 4 commits from issue-14-ndk-bump into dev 2026-05-31 11:49:30 +00:00

4 commits

Author SHA1 Message Date
e8f245c917 fix(deps): cap nostr-tools at ~2.20.0 (regtest Node 20 / curves v2 ESM-only) (#14)
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
Caught during regtest dogfood after the previous three commits
landed. With `nostr-tools: ^2.17.2` pnpm resolved to 2.23.5, which
in turn pulls `@noble/curves@2.0.1` — ESM-only. The regtest
Dockerfile runs on Node 20.11.1, where CJS `require()` of pure-ESM
modules is hard-blocked:

  Error [ERR_REQUIRE_ESM]: require() of ES Module
  /app/node_modules/.pnpm/@noble+curves@2.0.1/.../secp256k1.js
  from /app/node_modules/.pnpm/nostr-tools@2.23.5/.../index.js
  not supported.

nostr-tools 2.21.0 was the cutover — that release flipped
`@noble/curves` from `1.2.0` to `2.0.1`. 2.20.0 is the last
nostr-tools 2.x release that's still CJS-friendly via @noble/curves
1.2.0. Capping our pin at `~2.20.0` keeps us within the
"nostr-tools >= 2.17.2" range NDK 3.0.3 asks for in its
peerDependency while sidestepping the ESM/CJS hazard.

This isn't a regression we introduce — it's a CJS-output footgun
unique to the regtest container's Node 20 + tsup-default-CJS
combination. Long-term fix paths (out of scope here):

  * Bump the container's Node base image to >= 22 (where
    `--experimental-require-module` is on by default for `.js`
    files inside `package.json type: "commonjs"`)
  * Switch tsup output to ESM (`tsup --format esm`) — wider
    surface change across the daemon, the client CLI, and the
    Dockerfile entrypoint
  * Accept the cap forever (small downside: 2.21+ patch fixes
    won't reach us until we fix one of the above)

The cap is intentionally tight (`~2.20.0` allows 2.20.x patches,
nothing newer) so a future `pnpm update` doesn't silently jump us
back over the 2.21 edge. Revisit when one of the long-term paths
above lands.

Refs aiolabs/nsecbunkerd#14, regtest dogfood 2026-05-31.
2026-05-31 13:43:37 +02:00
db1a834587 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).
2026-05-31 12:15:57 +02:00
94b5d55376 refactor: adapt source to NDK 3.0.3 / nostr-tools v2 surface (#14)
Mechanical adjustments to the source after the dep bump in the
previous commit. No semantic changes — every site adapts to API
drift between the pinned versions.

Surface changes addressed:

  * `NDKKind` strict numeric enum (was wider in 2.8.1). 18 sites
    passed the literal `24134` (NIP-46 admin-RPC response kind) to
    `rpc.sendResponse` / `rpc.sendRequest`; NDK 3's `NDKKind` enum
    omits 24134. Introduced `src/daemon/admin/kinds.ts` exporting
    `NIP46_ADMIN_RESPONSE_KIND = 24134 as NDKKind` so the cast lives
    once, and routed all 18 sites through the named constant.

  * `NDKPrivateKeySigner` constructor now accepts nsec1 or hex
    directly (the `@ai-guardrail` in NDK 3 source explicitly tells
    callers not to `nip19.decode` ahead of construction). Simplified
    `Daemon.startKey` and `createNewKey` accordingly — the bech32
    decode workaround for #8 was tied to NDK 2.8.1's old behavior
    and is no longer needed.

  * `NDKPrivateKeySigner.privateKey` is `string` (hex) on the public
    surface, not `Uint8Array`. `nostr-tools` v2's `nip19.nsecEncode`
    wants `Uint8Array`. Replaced `nip19.nsecEncode(key.privateKey!)`
    with `key.nsec` (NDK 3 exposes the getter directly), avoiding
    both the type mismatch and the unnecessary round-trip. For the
    one remaining hex-string-from-config call site, used
    `nostrUtils.hexToBytes` to convert before encoding.

  * `NDKPool` event rename: `'relay:notice'` → `'notice'`, with
    flipped arg order `(notice, relay)` → `(relay, notice)`.

  * `NDKUser.fromNip05` now requires the `ndk` instance as a 2nd
    positional arg (was implicit-global before).

  * `Nip46PermitCallbackParams.params` narrowed to `string |
    NostrEvent`; type guards added at the two access sites
    (`authorize.ts` and `acl/index.ts:requestToSigningConditionQuery`).

  * `req.params` is now `(string | undefined)[]` instead of `any[]`;
    `create_account.ts` `authorizationWithPayload` branch now
    explicitly throws on missing username/domain before passing to
    `createAccountReal` (validates what was implicit before).

  * Removed `src/daemon/backend/publish-event.ts` (defined a strategy
    that's never registered — wiring is commented out in
    `backend/index.ts:22`; in NDK 3 the file refs the removed
    `NDKNip46Backend.signEvent`). Dead since at least NDK 2.x; the
    bump just made the breakage visible.

Pre-existing `tsc` errors at `src/db.ts` and `src/daemon/authorize.ts`
on `'PrismaClient'` / `'Request'` exports are unrelated to this PR —
the regtest container's nix derivation can't reach the prisma engine
binary store on this host (`nsecbunkerd#14` parked separately).
`pnpm run build` (tsup) is green; the Docker container runs
`prisma generate` against its own engine at image-build time and
resolves these at runtime.

#11's wire-name policy convention adoption is the next commit —
this one is purely keep-it-compiling work.

Refs aiolabs/nsecbunkerd#14.
2026-05-31 12:14:29 +02:00
041f431bc2 chore(deps): bump @nostr-dev-kit/ndk 2.8.1 → 3.0.3 + nostr-tools v1 → v2 (#14)
NDK 2.8.1 (Apr 2024) is 2 years old and predates NIP-46 backend-side
nip44 support. With aiolabs/lnbits#38's phase-2.4 client-side migration
to bunker-mediated nip44_*, the bunker's lack of a `nip44_decrypt`
strategy registration causes wire RPCs to fall through to
`sendResponse(id, remotePubkey, "error", undefined, "Not authorized")`
at NDK 2.8.1's backend/index.ts:179. Even nip04 was silently broken:
2.8.1 normalizes the wire method to `encrypt`/`decrypt` for
`pubkeyAllowed` while lnbits's policy stores wire names. The
encrypt/decrypt path through nsecbunkerd has never actually worked
end-to-end; it just hadn't been exercised until phase 2.4 landed.

3.0.3 (Feb 2026) is the current `latest` dist-tag and ships:

  - `nip44_encrypt` / `nip44_decrypt` backend handlers registered
    by default + wire-name `pubkeyAllowed` semantics (the immediate fix)
  - `switch_relays` NIP-46 support for client-side relay migration
  - Configurable NDKNip46Signer timeout (pairs with lnbits PR #38's
    matching client-side config)
  - NIP-44 default outgoing encryption with NIP-04 compat fallback
  - Async error handling fix in backend dispatch — failed strategies
    report errors instead of silent drop (deb7f93d)
  - "Not enough relays received this event" race-condition fix on
    publish (relevant to open #7 — may close that one too)
  - Signature verification moved in-house (off legacy nostr-tools v1
    path)
  - 2 years of security/perf updates in transitive @noble/* crypto
    primitives

`nostr-tools` bumped from ^1.17.0 to ^2.17.2 alongside because NDK
3.x's `NDKPrivateKeySigner` imports `finalizeEvent` / `generateSecretKey`
+ uses the `./nip49` subpath, none of which exist in nostr-tools v1.17.
With v1.17 installed, `require('@nostr-dev-kit/ndk')` fails with
"Package subpath './nip49' is not defined". Confirmed against the
post-install module graph.

Source migrations for NDK 3 / nostr-tools v2 API surface land in the
follow-up commit; this commit is intentionally just the dep bump so
the diff stays reviewable. Refs aiolabs/nsecbunkerd#14 +
coord-log 2026-05-31T09:55Z.
2026-05-31 12:02:03 +02:00