Compare commits

...

4 commits

Author SHA1 Message Date
e39eaa632d startKey: decode bech32 nsec to hex before constructing NDKPrivateKeySigner
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
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) <noreply@anthropic.com>
2026-05-26 00:32:39 +02:00
42dbbd7536 disable pingOrDie watchdog — false-positives on non-public relays
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) <noreply@anthropic.com>
2026-05-26 00:29:53 +02:00
960b9399e8 Dockerfile: switch from npm to pnpm + drop --frozen-lockfile
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) <noreply@anthropic.com>
2026-05-26 00:29:41 +02:00
06272c8f2c pin @nostr-dev-kit/ndk to 2.8.1 instead of workspace:*
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) <noreply@anthropic.com>
2026-05-26 00:29:29 +02:00
4 changed files with 36 additions and 11 deletions

View file

@ -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 FROM node:20.11-bullseye AS build
WORKDIR /app WORKDIR /app
# Copy package files and install dependencies RUN npm install -g pnpm@9
COPY package*.json ./
RUN npm install # 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 application files
COPY . . COPY . .
# Generate prisma client and build the application # Generate prisma client and build the application
RUN npx prisma generate RUN npx prisma generate
RUN npm run build RUN pnpm run build
# Runtime stage # Runtime stage
FROM node:20.11-alpine as runtime FROM node:20.11-alpine AS runtime
WORKDIR /app WORKDIR /app
@ -22,11 +34,13 @@ RUN apk update && \
apk add --no-cache openssl && \ apk add --no-cache openssl && \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
RUN npm install -g pnpm@9
# Copy built files from the build stage # Copy built files from the build stage
COPY --from=build /app . COPY --from=build /app .
# Install only runtime dependencies # Install only runtime dependencies (pnpm respects the workspace protocol)
RUN npm install --only=production RUN pnpm install --prod --no-frozen-lockfile
EXPOSE 3000 EXPOSE 3000

View file

@ -39,7 +39,7 @@
"@fastify/view": "^8.2.0", "@fastify/view": "^8.2.0",
"@inquirer/password": "^1.1.2", "@inquirer/password": "^1.1.2",
"@inquirer/prompts": "^1.2.3", "@inquirer/prompts": "^1.2.3",
"@nostr-dev-kit/ndk": "workspace:*", "@nostr-dev-kit/ndk": "2.8.1",
"@prisma/client": "^5.4.1", "@prisma/client": "^5.4.1",
"@scure/base": "^1.1.1", "@scure/base": "^1.1.1",
"@types/yargs": "^17.0.24", "@types/yargs": "^17.0.24",

View file

@ -122,7 +122,12 @@ class AdminInterface {
this.rpc.on('request', (req) => this.handleRequest(req)); 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) => { }).catch((err) => {
console.log('❌ admin connection failed'); console.log('❌ admin connection failed');
console.log(err); console.log(err);

View file

@ -230,8 +230,14 @@ class Daemon {
if (nsec.startsWith('nsec1')) { if (nsec.startsWith('nsec1')) {
try { try {
const key = new NDKPrivateKeySigner(nsec); // NDK 2.8.1's NDKPrivateKeySigner constructor passes its
hexpk = key.privateKey!; // 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) { } catch(e) {
console.error(`Error loading key ${name}:`, e); console.error(`Error loading key ${name}:`, e);
return return