From 06272c8f2c498dc9e7717ae983ad005cf7f80f0c Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 26 May 2026 00:29:29 +0200 Subject: [PATCH 1/4] pin @nostr-dev-kit/ndk to 2.8.1 instead of workspace:* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream declares the dependency as workspace:*, but the repo has no pnpm-workspace.yaml and no sibling @nostr-dev-kit/ndk package — so pnpm install fails with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND on a clean clone. The shipped pnpm-lock.yaml was resolving to ndk 2.8.1, so pin to that exact version to match what the lockfile already expects. Fixes #3. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4232435..297fd2a 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@fastify/view": "^8.2.0", "@inquirer/password": "^1.1.2", "@inquirer/prompts": "^1.2.3", - "@nostr-dev-kit/ndk": "workspace:*", + "@nostr-dev-kit/ndk": "2.8.1", "@prisma/client": "^5.4.1", "@scure/base": "^1.1.1", "@types/yargs": "^17.0.24", From 960b9399e8000449fcc3440ca16e29a65a35448b Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 26 May 2026 00:29:41 +0200 Subject: [PATCH 2/4] Dockerfile: switch from npm to pnpm + drop --frozen-lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two upstream-rot issues fixed in one commit (same root cause: the upstream Dockerfile predates the move to pnpm and the lockfile has drifted): - npm install can't resolve workspace:* deps (which package.json used to declare for @nostr-dev-kit/ndk — see prior commit for the pin). Switching to pnpm@9 matches the lockfile that ships in-repo. - pnpm-lock.yaml is out of date vs package.json (likely from generation-time vs commit-time drift), so --frozen-lockfile fails with ERR_PNPM_OUTDATED_LOCKFILE. Drop the flag in both build and runtime stages to let pnpm resolve fresh, at the cost of giving up determinism — to be restored once the lockfile is regenerated. Also reorders the build stage to COPY lockfile + manifest before the source, so the install layer caches across source-only edits. Fixes #1, #2. Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1eb99be..1168d8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,32 @@ +# Patched from upstream kind-0/nsecbunkerd Dockerfile to use pnpm — the +# upstream version uses `npm install` but package.json declares +# `@nostr-dev-kit/ndk` as `workspace:*`, which only pnpm understands. +# A clean clone of upstream fails to build with `EUNSUPPORTEDPROTOCOL` +# under npm. Switching to pnpm matches the lockfile that ships in-repo. +# Also drops `--frozen-lockfile` because the upstream pnpm-lock.yaml is +# out of date vs. package.json (ERR_PNPM_OUTDATED_LOCKFILE) — bug to +# file upstream once we've verified the rest of the stack works. + FROM node:20.11-bullseye AS build WORKDIR /app -# Copy package files and install dependencies -COPY package*.json ./ -RUN npm install +RUN npm install -g pnpm@9 + +# Copy lockfile + manifest first so the install layer caches across +# source changes. +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --no-frozen-lockfile # Copy application files COPY . . # Generate prisma client and build the application RUN npx prisma generate -RUN npm run build +RUN pnpm run build # Runtime stage -FROM node:20.11-alpine as runtime +FROM node:20.11-alpine AS runtime WORKDIR /app @@ -22,11 +34,13 @@ RUN apk update && \ apk add --no-cache openssl && \ rm -rf /var/cache/apk/* +RUN npm install -g pnpm@9 + # Copy built files from the build stage COPY --from=build /app . -# Install only runtime dependencies -RUN npm install --only=production +# Install only runtime dependencies (pnpm respects the workspace protocol) +RUN pnpm install --prod --no-frozen-lockfile EXPOSE 3000 From 42dbbd753663f4698e930582a7c8d72666e7344e Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 26 May 2026 00:29:53 +0200 Subject: [PATCH 3/4] =?UTF-8?q?disable=20pingOrDie=20watchdog=20=E2=80=94?= =?UTF-8?q?=20false-positives=20on=20non-public=20relays?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NDK 2.8.1's outbox model doesn't reliably deliver self-published events back through subscriptions when the configured relay set is a single custom (non-public) relay. The pingOrDie self-watchdog publishes a kind-24133 event to its own pubkey every 20s and exits the bunker if it doesn't see the echo within 50s — which means on a private relay channel (e.g. LNbits's nostrrelay extension), the bunker exits cleanly every 50s even though admin RPCs over that same channel are working fine. Plain-WebSocket round-trips to the same relay echo correctly in <1s, so the issue is on NDK's side, not the relay's. Commenting out the watchdog is the minimum patch to keep the daemon alive. Real fix is either an env-flag opt-out, a simpler connectivity check that doesn't depend on self-echo, or an NDK upgrade that fixes the outbox-vs-subscribe race. Fixes #4. See also #7 for the underlying NDK echo investigation. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/daemon/admin/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts index 4db9cc2..75dfc4b 100644 --- a/src/daemon/admin/index.ts +++ b/src/daemon/admin/index.ts @@ -122,7 +122,12 @@ class AdminInterface { this.rpc.on('request', (req) => this.handleRequest(req)); - pingOrDie(this.ndk); + // pingOrDie disabled — NDK 2.8.1 outbox model doesn't echo + // self-published events back through subscriptions on + // non-public relay channels, so the watchdog fires false + // positives and exits the bunker every 50s on private relays. + // See aiolabs/nsecbunkerd#4 + #7. + // pingOrDie(this.ndk); }).catch((err) => { console.log('❌ admin connection failed'); console.log(err); From e39eaa632db16c0d2abdd78710467639e74ddcf4 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 26 May 2026 00:32:39 +0200 Subject: [PATCH 4/4] startKey: decode bech32 nsec to hex before constructing NDKPrivateKeySigner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NDK 2.8.1's NDKPrivateKeySigner constructor forwards its arg straight to nostr-tools getPublicKey() which requires 32-byte hex/bytes/bigint and throws on bech32 input. Every key loaded through startKey (i.e. every key created via create_new_key, plus boot-time reloads of any plain-nsec entries in the config) was failing silently with the nostr-tools type error. The try/catch caught the throw and returned without loading the key, so the bunker would happily report create_new_key as successful, the key would persist encrypted on disk, but the runtime keystore would not have a signer for it. NIP-46 connect / sign_event against any admin-provisioned target therefore silently timed out from the client side — blocking essentially every signing flow. Sister bug to #5 (getKeys iterator) in a different code path. The fix matches the existing pattern in create_new_key.ts:16: hexpk = nip19.decode(nsec).data as string; Verified against the local spike harness: create_new_key now loads the target into runtime; get_keys returns the new entry (assuming #5 is patched separately for the iterator path). Fixes #8. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/daemon/run.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/daemon/run.ts b/src/daemon/run.ts index 262a150..6637986 100644 --- a/src/daemon/run.ts +++ b/src/daemon/run.ts @@ -230,8 +230,14 @@ class Daemon { if (nsec.startsWith('nsec1')) { try { - const key = new NDKPrivateKeySigner(nsec); - hexpk = key.privateKey!; + // NDK 2.8.1's NDKPrivateKeySigner constructor passes its + // arg straight to nostr-tools getPublicKey() which requires + // 32-byte hex / bytes / bigint, not bech32. Without this + // decode, every key created via create_new_key fails to + // load with the nostr-tools getPublicKey type error, so + // the bunker can never sign for any target it provisions. + // See aiolabs/nsecbunkerd#8. + hexpk = nip19.decode(nsec).data as string; } catch(e) { console.error(`Error loading key ${name}:`, e); return