diff --git a/Dockerfile b/Dockerfile index 9ace24a..1168d8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,20 +39,10 @@ RUN npm install -g pnpm@9 # Copy built files from the build stage COPY --from=build /app . -# Install all dependencies (including devDeps). The prisma CLI lives in -# devDependencies but scripts/start.js invokes `prisma migrate deploy` -# at boot, so it must be available at runtime. Dropping --prod adds the -# CLI tooling to the runtime image — a modest size cost for the -# correctness of the migration step. -RUN pnpm install --no-frozen-lockfile +# Install only runtime dependencies (pnpm respects the workspace protocol) +RUN pnpm install --prod --no-frozen-lockfile EXPOSE 3000 -# Run via scripts/start.js so `prisma migrate deploy` applies pending -# migrations before the daemon spawns. The upstream Dockerfile invokes -# ./dist/index.js directly, which silently bypasses the migration step -# and leaves the SQLite db empty on first boot — every command that -# touches Policy/KeyUser/Token/etc. then throws "table does not exist." -# Caught during aiolabs/nsecbunkerd#7 diagnosis 2026-05-27. -ENTRYPOINT [ "node", "./scripts/start.js" ] +ENTRYPOINT [ "node", "./dist/index.js" ] CMD ["start"] diff --git a/package.nix b/package.nix index adeb62b..5c49bf7 100644 --- a/package.nix +++ b/package.nix @@ -13,17 +13,12 @@ }: let - # Fork commit `06272c8` ("pin @nostr-dev-kit/ndk to 2.8.1 instead of - # workspace:*") changed package.json to a pinned `"2.8.1"`, but the - # pnpm-lock.yaml still expresses the spec as `"^2.8.1"` (the way - # `pnpm add` originally generated it). pnpm with --frozen-lockfile - # rejects that mismatch. Patching package.json to use the caret form - # is non-semantic (2.8.1 is still the resolved version) and aligns - # both files. Same fix the Dockerfile-side already handles via - # `--no-frozen-lockfile`; in nix we prefer frozen + a targeted patch. + # package.json pins `@nostr-dev-kit/ndk: "workspace:*"` but the lockfile + # resolves `^2.8.1`. With --frozen-lockfile pnpm refuses the mismatch, + # so rewrite the spec to match the lockfile. patchNdk = '' substituteInPlace package.json \ - --replace-fail '"@nostr-dev-kit/ndk": "2.8.1"' \ + --replace-fail '"@nostr-dev-kit/ndk": "workspace:*"' \ '"@nostr-dev-kit/ndk": "^2.8.1"' ''; @@ -82,12 +77,7 @@ stdenv.mkDerivation (finalAttrs: { pnpm prisma generate pnpm build - # Do NOT `pnpm prune --prod` here — the prisma CLI lives in - # devDependencies and `scripts/start.js` invokes it at boot via - # `npx prisma migrate deploy`. Without the CLI, the migration step - # silently fails (npx falls back to downloading prisma fresh, which - # OOMs on most containers) and the SQLite db stays empty. See - # `aiolabs/nsecbunkerd#7` diagnosis 2026-05-27. + pnpm prune --prod --ignore-scripts find node_modules -xtype l -delete runHook postBuild @@ -97,24 +87,14 @@ stdenv.mkDerivation (finalAttrs: { runHook preInstall mkdir -p $out/{bin,share/nsecbunkerd} - # scripts/ MUST be copied — it contains the start.js launcher that - # runs `prisma migrate deploy` before spawning the daemon. The - # upstream packaging (and the upstream Dockerfile) bypassed this by - # invoking dist/index.js directly, leaving migrations unapplied. - cp -r dist node_modules prisma scripts templates package.json \ + cp -r dist node_modules prisma templates package.json \ $out/share/nsecbunkerd/ - # Wrapper invokes scripts/start.js, which runs `prisma migrate deploy` - # then spawns dist/index.js. start.js resolves sibling paths from - # __dirname, so the caller (systemd unit, docker compose, etc.) can - # set its own WorkingDirectory for the writable state dir without - # interfering with how the launcher finds its own package files. - # NSEC_BUNKER_CONFIG_DIR can override the config directory location; - # by default it's `./config` relative to cwd. makeWrapper ${lib.getExe nodejs_20} $out/bin/nsecbunkerd \ - --add-flags $out/share/nsecbunkerd/scripts/start.js \ + --chdir $out/share/nsecbunkerd \ + --add-flags $out/share/nsecbunkerd/dist/index.js \ --set NODE_ENV production \ - --prefix PATH : ${lib.makeBinPath [ openssl nodejs_20 ]} \ + --prefix PATH : ${lib.makeBinPath [ openssl ]} \ ${ lib.concatStringsSep " \\\n " ( lib.mapAttrsToList (n: v: "--set ${n} ${lib.escapeShellArg v}") prismaEnv diff --git a/scripts/start.js b/scripts/start.js index 603d5b2..c3899f8 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -1,32 +1,20 @@ const { execSync, spawn } = require('child_process'); const fs = require('fs'); -const path = require('path'); - -// Resolve sibling paths from this script's location so the launcher -// works whether cwd is /app (docker), the nix store, or a writable -// state dir set by systemd's WorkingDirectory. The prisma CLI and -// dist/index.js live alongside this file in `/share/nsecbunkerd/` -// (nix) or `/app/` (docker). The migration-side env knobs: -// NSEC_BUNKER_CONFIG_DIR — directory holding nsecbunker.{json,db}; -// defaults to ./config relative to cwd. -// DATABASE_URL — prisma's source of truth for the sqlite -// path; honor whatever the caller set. -const pkgRoot = path.resolve(__dirname, '..'); -const configDir = process.env.NSEC_BUNKER_CONFIG_DIR || path.resolve(process.cwd(), 'config'); try { - console.log(`Running migrations`); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); + console.log(`Running migrations`); + // check if config folder exists + if (!fs.existsSync('./config')) { + execSync(`mkdir config`); } - execSync('npm run prisma:migrate', { cwd: pkgRoot, stdio: 'inherit' }); + execSync('npm run prisma:migrate'); } catch (error) { - console.log(error); + console.log(error); // Handle any potential migration errors here } const args = process.argv.slice(2); -const childProcess = spawn('node', [path.join(pkgRoot, 'dist/index.js'), ...args], { +const childProcess = spawn('node', ['./dist/index.js', ...args], { stdio: 'inherit', }); diff --git a/src/daemon/admin/commands/create_account.ts b/src/daemon/admin/commands/create_account.ts index f3c026a..e4632d6 100644 --- a/src/daemon/admin/commands/create_account.ts +++ b/src/daemon/admin/commands/create_account.ts @@ -1,4 +1,4 @@ -import { Hexpubkey, NDKPrivateKeySigner, NDKRpcRequest, NDKUserProfile } from "@nostr-dev-kit/ndk"; +import { Hexpubkey, NDKKind, NDKPrivateKeySigner, NDKRpcRequest, NDKUserProfile } from "@nostr-dev-kit/ndk"; import AdminInterface from ".."; import { nip19 } from 'nostr-tools'; import { setupSkeletonProfile } from "../../lib/profile"; @@ -136,7 +136,7 @@ export default async function createAccount(admin: AdminInterface, req: NDKRpcRe } /** - * This is where the real work of creating the private key, wallet, nip-05, granting access, etc happen — pragma: allowlist secret + * This is where the real work of creating the private key, wallet, nip-05, granting access, etc happen */ export async function createAccountReal( admin: AdminInterface, @@ -209,18 +209,11 @@ export async function createAccountReal( // access it without having to go through an approval flow await grantPermissions(req, keyName); - // NDKKind.NostrConnectAdmin doesn't exist in NDK 2.8.1 — it resolves - // to `undefined` and sendResponse defaults to NDKKind.NostrConnect - // (24133), sending the response on the wrong channel. Mirror the - // request's kind so the response goes back on the same channel the - // client subscribed for. Filed as part of aiolabs/nsecbunkerd#7 - // diagnosis 2026-05-27. - const originalKind = req.event.kind!; - return admin.rpc.sendResponse(req.id, req.pubkey, generatedUser.pubkey, originalKind); + return admin.rpc.sendResponse(req.id, req.pubkey, generatedUser.pubkey, NDKKind.NostrConnectAdmin); } catch (e: any) { console.trace('error', e); - const originalKind = req.event.kind!; - return admin.rpc.sendResponse(req.id, req.pubkey, "error", originalKind, e.message); + return admin.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin, + e.message); } } diff --git a/src/daemon/admin/commands/create_new_token.ts b/src/daemon/admin/commands/create_new_token.ts index 04588c4..df145a2 100644 --- a/src/daemon/admin/commands/create_new_token.ts +++ b/src/daemon/admin/commands/create_new_token.ts @@ -7,19 +7,15 @@ export default async function createNewToken(admin: AdminInterface, req: NDKRpcR if (!clientName || !policyId) throw new Error("Invalid params"); - const policyIdInt = parseInt(policyId); - const policy = await prisma.policy.findUnique({ where: { id: policyIdInt }, include: { rules: true } }); + const policy = await prisma.policy.findUnique({ where: { id: parseInt(policyId) }, include: { rules: true } }); if (!policy) throw new Error("Policy not found"); console.log({clientName, policy, durationInHours}); const token = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); - // policyId must be Int per the Prisma schema (Token.policyId references - // Policy.id which is autoincrement Int). Upstream passes the raw string - // from the wire — caught during aiolabs/nsecbunkerd#7 diagnosis 2026-05-27. const data: any = { - keyName, clientName, policyId: policyIdInt, + keyName, clientName, policyId, createdBy: req.pubkey, token }; diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts index 5adafe8..75dfc4b 100644 --- a/src/daemon/admin/index.ts +++ b/src/daemon/admin/index.ts @@ -111,28 +111,8 @@ class AdminInterface { return; } - const debugTransport = process.env.NSEC_BUNKER_DEBUG_TRANSPORT === '1'; - - // Per-relay publish-status logging for diagnosing aiolabs/nsecbunkerd#7. - // NDKNostrRpc.sendResponse calls event.publish() and discards the - // returned Set, so a silent outbox-drop is invisible without - // hooking the underlying per-relay events. Gated by env flag so - // production deployments stay quiet. - const attachRelayLogging = (relay: any) => { - relay.on('published', (event: NDKEvent) => { - console.log(`📤 PUBLISHED relay=${relay.url} kind=${event.kind} id=${event.id?.slice(0,8)}`); - }); - relay.on('publish:failed', (event: NDKEvent, err: any) => { - console.log(`❌ PUBLISH_FAILED relay=${relay.url} kind=${event.kind} id=${event.id?.slice(0,8)} err=${err?.message ?? err}`); - }); - }; - - this.ndk.pool.on('relay:connect', (relay: any) => { - console.log('✅ nsecBunker Admin Interface ready'); - if (debugTransport) attachRelayLogging(relay); - }); + this.ndk.pool.on('relay:connect', () => console.log('✅ nsecBunker Admin Interface ready')); this.ndk.pool.on('relay:disconnect', () => console.log('❌ admin disconnected')); - this.ndk.connect(2500).then(() => { // connect for whitelisted admins this.rpc.subscribe({ @@ -140,33 +120,7 @@ class AdminInterface { "#p": [this.signerUser!.pubkey] }); - // Attach per-relay logging to relays that connected before our - // 'relay:connect' listener was registered above (NDK can connect - // synchronously inside .connect() under some paths). - if (debugTransport) { - this.ndk.pool.relays.forEach((relay: any) => attachRelayLogging(relay)); - - // Wrap sendResponse to log id + kind + elapsed time so we - // can correlate REQUEST_IN → RESPONSE_SENT → PUBLISHED. - const originalSendResponse = this.rpc.sendResponse.bind(this.rpc); - this.rpc.sendResponse = async (id: string, remotePubkey: string, result: string, kind?: number, error?: string) => { - const start = Date.now(); - try { - await originalSendResponse(id, remotePubkey, result, kind, error); - console.log(`📨 RESPONSE_SENT id=${id} remote=${remotePubkey.slice(0,8)} kind=${kind ?? NDKKind.NostrConnect} elapsed=${Date.now()-start}ms`); - } catch (e: any) { - console.log(`❌ RESPONSE_SEND_FAILED id=${id} remote=${remotePubkey.slice(0,8)} kind=${kind ?? NDKKind.NostrConnect} err=${e?.message ?? e}`); - throw e; - } - }; - } - - this.rpc.on('request', (req) => { - if (debugTransport) { - console.log(`📥 REQUEST_IN method=${req.method} id=${req.id} from=${req.pubkey?.slice(0,8)} kind=${req.event?.kind}`); - } - this.handleRequest(req); - }); + this.rpc.on('request', (req) => this.handleRequest(req)); // pingOrDie disabled — NDK 2.8.1 outbox model doesn't echo // self-published events back through subscriptions on @@ -209,15 +163,7 @@ class AdminInterface { } } catch (err: any) { debug(`Error handling request ${req.method}: ${err?.message??err}`, req.params); - // NDKKind.NostrConnectAdmin doesn't exist in NDK 2.8.1 — using it - // makes sendResponse fall through to its default of 24133, which - // sends the error on a different channel than the request came in - // on. Mirror req.event.kind so the response goes back where the - // client is listening. Filed as part of aiolabs/nsecbunkerd#7 - // diagnosis 2026-05-27. - const originalKind = req.event.kind!; - console.log(`⚠️ HANDLE_REQUEST_ERROR method=${req.method} id=${req.id} kind=${originalKind} err=${err?.message ?? err}`); - return this.rpc.sendResponse(req.id, req.pubkey, "error", originalKind, err?.message); + return this.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin, err?.message); } }