Bump @nostr-dev-kit/ndk 2.8.1 → 3.0.3 — unlocks nip44 backend + 2yr of upstream #14

Closed
opened 2026-05-31 09:57:05 +00:00 by padreug · 0 comments
Owner

Why

Diagnosed against dev@f2a9697 on 2026-05-31 (see coord log 2026-05-31T08:35Z). Two coupled gaps in our NDK pin block the encrypt/decrypt path through nsecbunkerd:

Finding 1 — NDK 2.8.1 has no nip44_encrypt / nip44_decrypt backend handlers.

NDKNip46Backend.handlers in our pinned node_modules/.../@nostr-dev-kit/ndk@2.8.1/.../signers/nip46/backend/index.ts:119 registers only connect, sign_event, nip04_encrypt, nip04_decrypt, get_public_key, ping. When a wire RPC arrives with method: "nip44_decrypt" (the call shape lnbits's RemoteBunkerSigner now uses post-aiolabs/lnbits#38), dispatch falls through to sendResponse(id, remotePubkey, "error", undefined, "Not authorized") at backend/index.ts:179. lnbits maps that to NsecBunkerRpcErrorterminal, non-retry-eligible.

Finding 2 — even nip04 is silently broken: NDK 2.8.1 normalizes the wire method to encrypt / decrypt before calling pubkeyAllowed, while lnbits's policy stores wire names.

Nip04EncryptHandlingStrategy.encrypt() in 2.8.1 calls backend.pubkeyAllowed({ method: "encrypt", ... }). Upstream NDK (post-Oct 2025) passes the wire name verbatim. lnbits's _ensure_policy stores rules as nip04_encrypt / nip04_decrypt / nip44_encrypt / nip44_decrypt. Confirmed via sqlite3 ~/dev/local/docker/regtest/data/nsecbunker/nsecbunker.db 'SELECT method FROM PolicyRule' — 4 kind-less rules, all wire-named. With NDK 2.8.1 the auth check at src/daemon/lib/acl/index.ts receives method='encrypt' and finds zero matching PolicyRule rows → falls to requestAuthorization() → 15s timeout. So the encrypt/decrypt path through nsecbunkerd has never actually worked end-to-end; it just hadn't been exercised until phase 2.4 landed.

Fix

Pin @nostr-dev-kit/ndk to 3.0.3 (the current latest dist-tag, published 2026-02-23, stable for 3 months).

What this gets us beyond the immediate fix

  • nip44_encrypt / nip44_decrypt backend handlers registered by default
  • switch_relays NIP-46 support for client-side relay migration
  • Configurable NDKNip46Signer timeout (pairs with lnbits PR #38's matching client 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)
  • Signature verification moved in-house (off the legacy nostr-tools v1 path)
  • 2 years of security/perf updates in transitive @noble/* crypto primitives

Why 3.0.3 (and not 2.15.0, 2.18.0, beta, or HEAD)

Target Verdict
2.8.1 (current) Apr 2024, 2yr old, no nip44 backend
2.15.0 Oct 2025, predates nip44 backend handlers (added 344c313f Oct 8 2025)
2.18.0 Oct 2025, last 2.x — also predates nip44 default-encryption flip
71× 3.0.0-beta.X not stable
HEAD (4b86acd1) Apr 2026, untagged
3.0.3 latest dist-tag, 3 months stable

Migration surface (verified by reading NDK 3.x source at ~/dev/refs/repos/nostr/nostr-dev-kit/ndk)

Our usage maps cleanly onto NDK 3.x without API breakage. Only meaningful code change is in src/daemon/lib/acl/index.ts:

  • IMethod type: widen from connect | sign_event | encrypt | decrypt | ping to include the new wire names (nip04_encrypt, nip04_decrypt, nip44_encrypt, nip44_decrypt, switch_relays).
  • requestToSigningConditionQuery: update the switch to handle the new method names instead of the old normalized ones.
  • allowScopeToSigningConditionQuery: follow.

Once those align with the wire-name convention, the post-#11 live-policy join (step 4 of checkIfPubkeyAllowed) naturally matches lnbits's existing nip04_encrypt / nip44_decrypt policy rules — no further changes needed on lnbits's side. applyToken fan-out at src/daemon/backend/index.ts:97-112 and the permit callback at src/daemon/run.ts:101 need no changes — they pass rule.method and params.method through verbatim.

Plan

Single branch off dev, four commits:

  1. chore(deps): bump @nostr-dev-kit/ndk 2.8.1 → 3.0.3package.json + regenerated pnpm-lock.yaml.
  2. refactor(acl): align method-name convention with NDK 3.x wire-name semanticsIMethod + requestToSigningConditionQuery + allowScopeToSigningConditionQuery.
  3. fix(nix): adapt package derivation to NDK 3.x dep tree (only if needed — flake.lock / package.nix tweaks discovered during build).
  4. Coord log entry documenting the live regtest smoke result.

Test plan

  • pnpm install clean (no peer-dep warnings introduced); pnpm run build green.
  • Regtest: rebuild nsecbunker container (docker compose build --no-cache nsecbunker && docker compose up -d nsecbunker). Wait for satmachineadmin's next consumer poll. Verify Greg's pending v1.1 cassette-state event decrypts successfully (kind-30078 NIP-44 ciphertext from bitspire's Sintra). cassette_configs.state_event_id advances past f46b6dcf62f6... to the new v1.1 event ID.
  • Webapp regression: smoke a regular operator nostr-publish through the bunker (any sign_event flow) — verify nothing breaks in the post-bump path.
  • aiolabs/nsecbunkerd#11 regression: re-run the 8 live-policy auth cases on the bumped branch — they should pass identically since the acl/index.ts rewrite remains conceptually the same, just with widened method-name vocabulary.

Carries forward

Open question parked in coord log 2026-05-31T08:35Z: bunker boots with 🔑 Starting keys []. By design (only un-encrypted keys are auto-loaded; encrypted keys load as locked and unlock via admin RPC). Not blocking this bump but flagged for awareness in case Greg's bunker_name key needs to be unlocked before the smoke succeeds.

refs: coord log 2026-05-31T08:35Z (the diagnosis), aiolabs/lnbits PR #38 (the phase-2.4 surface that surfaced this), aiolabs/satmachineadmin PR #30 (the consumer migration), NDK CHANGELOG ~/dev/refs/repos/nostr/nostr-dev-kit/ndk/core/CHANGELOG.md, NDK MIGRATION ~/dev/refs/repos/nostr/nostr-dev-kit/ndk/core/MIGRATION-2.16.md

## Why Diagnosed against `dev@f2a9697` on 2026-05-31 (see coord log `2026-05-31T08:35Z`). Two coupled gaps in our NDK pin block the encrypt/decrypt path through nsecbunkerd: **Finding 1 — NDK 2.8.1 has no `nip44_encrypt` / `nip44_decrypt` backend handlers.** `NDKNip46Backend.handlers` in our pinned `node_modules/.../@nostr-dev-kit/ndk@2.8.1/.../signers/nip46/backend/index.ts:119` registers only `connect, sign_event, nip04_encrypt, nip04_decrypt, get_public_key, ping`. When a wire RPC arrives with `method: "nip44_decrypt"` (the call shape lnbits's `RemoteBunkerSigner` now uses post-`aiolabs/lnbits#38`), dispatch falls through to `sendResponse(id, remotePubkey, "error", undefined, "Not authorized")` at `backend/index.ts:179`. lnbits maps that to `NsecBunkerRpcError` — **terminal, non-retry-eligible**. **Finding 2 — even nip04 is silently broken: NDK 2.8.1 normalizes the wire method to `encrypt` / `decrypt` before calling `pubkeyAllowed`, while lnbits's policy stores wire names.** `Nip04EncryptHandlingStrategy.encrypt()` in 2.8.1 calls `backend.pubkeyAllowed({ method: "encrypt", ... })`. Upstream NDK (post-Oct 2025) passes the wire name verbatim. lnbits's `_ensure_policy` stores rules as `nip04_encrypt` / `nip04_decrypt` / `nip44_encrypt` / `nip44_decrypt`. Confirmed via `sqlite3 ~/dev/local/docker/regtest/data/nsecbunker/nsecbunker.db 'SELECT method FROM PolicyRule'` — 4 kind-less rules, all wire-named. With NDK 2.8.1 the auth check at `src/daemon/lib/acl/index.ts` receives `method='encrypt'` and finds zero matching `PolicyRule` rows → falls to `requestAuthorization()` → 15s timeout. So the encrypt/decrypt path through nsecbunkerd has never actually worked end-to-end; it just hadn't been exercised until phase 2.4 landed. ## Fix Pin `@nostr-dev-kit/ndk` to `3.0.3` (the current `latest` dist-tag, published 2026-02-23, stable for 3 months). ### What this gets us beyond the immediate fix - `nip44_encrypt` / `nip44_decrypt` backend handlers registered by default - `switch_relays` NIP-46 support for client-side relay migration - Configurable NDKNip46Signer timeout (pairs with lnbits PR #38's matching client 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`) - Signature verification moved in-house (off the legacy nostr-tools v1 path) - 2 years of security/perf updates in transitive `@noble/*` crypto primitives ### Why 3.0.3 (and not 2.15.0, 2.18.0, beta, or HEAD) | Target | Verdict | |---|---| | `2.8.1` (current) | Apr 2024, 2yr old, no nip44 backend | | `2.15.0` | Oct 2025, predates nip44 backend handlers (added `344c313f` Oct 8 2025) | | `2.18.0` | Oct 2025, last 2.x — also predates nip44 default-encryption flip | | 71× `3.0.0-beta.X` | not stable | | HEAD (`4b86acd1`) | Apr 2026, untagged | | **`3.0.3`** | `latest` dist-tag, 3 months stable | ### Migration surface (verified by reading NDK 3.x source at `~/dev/refs/repos/nostr/nostr-dev-kit/ndk`) Our usage maps cleanly onto NDK 3.x without API breakage. Only meaningful code change is in `src/daemon/lib/acl/index.ts`: - `IMethod` type: widen from `connect | sign_event | encrypt | decrypt | ping` to include the new wire names (`nip04_encrypt`, `nip04_decrypt`, `nip44_encrypt`, `nip44_decrypt`, `switch_relays`). - `requestToSigningConditionQuery`: update the `switch` to handle the new method names instead of the old normalized ones. - `allowScopeToSigningConditionQuery`: follow. Once those align with the wire-name convention, the post-`#11` live-policy join (step 4 of `checkIfPubkeyAllowed`) naturally matches lnbits's existing `nip04_encrypt` / `nip44_decrypt` policy rules — no further changes needed on lnbits's side. `applyToken` fan-out at `src/daemon/backend/index.ts:97-112` and the permit callback at `src/daemon/run.ts:101` need no changes — they pass `rule.method` and `params.method` through verbatim. ## Plan Single branch off `dev`, four commits: 1. `chore(deps): bump @nostr-dev-kit/ndk 2.8.1 → 3.0.3` — `package.json` + regenerated `pnpm-lock.yaml`. 2. `refactor(acl): align method-name convention with NDK 3.x wire-name semantics` — `IMethod` + `requestToSigningConditionQuery` + `allowScopeToSigningConditionQuery`. 3. `fix(nix): adapt package derivation to NDK 3.x dep tree` (only if needed — flake.lock / package.nix tweaks discovered during build). 4. Coord log entry documenting the live regtest smoke result. ## Test plan - `pnpm install` clean (no peer-dep warnings introduced); `pnpm run build` green. - Regtest: rebuild nsecbunker container (`docker compose build --no-cache nsecbunker && docker compose up -d nsecbunker`). Wait for satmachineadmin's next consumer poll. Verify Greg's pending v1.1 cassette-state event decrypts successfully (kind-30078 NIP-44 ciphertext from bitspire's Sintra). `cassette_configs.state_event_id` advances past `f46b6dcf62f6...` to the new v1.1 event ID. - Webapp regression: smoke a regular operator nostr-publish through the bunker (any sign_event flow) — verify nothing breaks in the post-bump path. - `aiolabs/nsecbunkerd#11` regression: re-run the 8 live-policy auth cases on the bumped branch — they should pass identically since the `acl/index.ts` rewrite remains conceptually the same, just with widened method-name vocabulary. ## Carries forward Open question parked in coord log `2026-05-31T08:35Z`: bunker boots with `🔑 Starting keys []`. By design (only un-encrypted keys are auto-loaded; encrypted keys load as locked and unlock via admin RPC). Not blocking this bump but flagged for awareness in case Greg's `bunker_name` key needs to be unlocked before the smoke succeeds. refs: coord log 2026-05-31T08:35Z (the diagnosis), aiolabs/lnbits PR #38 (the phase-2.4 surface that surfaced this), aiolabs/satmachineadmin PR #30 (the consumer migration), NDK CHANGELOG `~/dev/refs/repos/nostr/nostr-dev-kit/ndk/core/CHANGELOG.md`, NDK MIGRATION `~/dev/refs/repos/nostr/nostr-dev-kit/ndk/core/MIGRATION-2.16.md`
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/nsecbunkerd#14
No description provided.