From d2ec84a18e80d1dd601342ea074c13de9c3aa6fb Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 5 Sep 2026 20:58:17 +0200 Subject: [PATCH 1/2] fix(nix): build bcrypt's native binding again under pnpm 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0d8c436 (nodejs_20/pnpm_9 -> nodejs_24/pnpm_10) shipped a package with no compiled bcrypt, and nsecbunkerd has been crashlooping on aio-demo ever since: dist/daemon/index.js requires bcrypt at load, the store path has only binding.gyp and the C++ sources under bcrypt@5.1.1, and the daemon dies instantly with Cannot find module '.../bcrypt/lib/binding/napi-v3/bcrypt_lib.node' buildPhase re-runs `pnpm install --force --offline` specifically to fire bcrypt's node-gyp postinstall, because configHook installs with --ignore-scripts. pnpm 10 changed that contract: it refuses to run *any* dependency lifecycle script unless the package is allow-listed, and it skips them silently — the install still reports success. So the bump turned that line into a no-op, the build kept passing, and the failure only surfaced at boot on the deployed host. Pass --config.dangerouslyAllowAllBuilds=true to restore the pnpm 9 semantics this build has always relied on. We run inside the nix sandbox against a store-seeded offline cache, so "all builds" is the same closed set of scripts pnpm 9 already ran. Add an installCheckPhase that requires bcrypt from the *installed* $out tree, the same way the daemon does. This failure mode is invisible at build time and fatal at boot, so it has to break the build instead of the host. Verified against the nixpkgs the deploy uses (da5ad661): lib/binding/napi-v3/bcrypt_lib.node is produced, installCheck prints "bcrypt native binding loads OK", and the daemon reaches "nsecBunker ready to serve requests." pnpmDeps hash is unchanged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBjd9Rw4ct134JH3CnLVaw --- package.nix | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/package.nix b/package.nix index 8270cdc..3dc355e 100644 --- a/package.nix +++ b/package.nix @@ -80,7 +80,21 @@ stdenv.mkDerivation (finalAttrs: { # configHook ran with --ignore-scripts; re-run install to trigger # native-module postinstall (bcrypt). --offline keeps it inside the # store seeded by configHook. - pnpm install --force --offline --frozen-lockfile --reporter=append-only + # + # `dangerouslyAllowAllBuilds` is load-bearing under pnpm 10: unlike + # pnpm 9, pnpm 10 refuses to run *any* dependency lifecycle script + # unless the package is allow-listed (`onlyBuiltDependencies` / + # `pnpm approve-builds`), and it skips them **silently** — the install + # still reports success. Without this, bcrypt's node-gyp postinstall + # never runs, `lib/binding/napi-v3/bcrypt_lib.node` is never produced, + # and the daemon dies at boot with MODULE_NOT_FOUND. That is exactly + # what shipped in 0d8c436 (the nodejs_20/pnpm_9 -> nodejs_24/pnpm_10 + # bump) and took down nsecbunkerd on aio-demo. The flag restores the + # pnpm 9 semantics this build has always relied on; we are inside the + # nix sandbox against a store-seeded offline cache, so "all builds" + # is the same closed set of scripts pnpm 9 ran. + pnpm install --force --offline --frozen-lockfile --reporter=append-only \ + --config.dangerouslyAllowAllBuilds=true pnpm prisma generate pnpm build @@ -132,6 +146,22 @@ stdenv.mkDerivation (finalAttrs: { runHook postInstall ''; + doInstallCheck = true; + + # The bcrypt failure mode is silent at build time and fatal at boot, so + # assert the native binding loads from the *installed* tree exactly the + # way `dist/daemon/index.js` loads it. A build that can't require bcrypt + # must fail here rather than on the deployed host. + installCheckPhase = '' + runHook preInstallCheck + + ${lib.getExe nodejs_24} -e \ + "require('$out/share/nsecbunkerd/node_modules/bcrypt'); \ + console.log('bcrypt native binding loads OK')" + + runHook postInstallCheck + ''; + passthru = { inherit prisma-engines; }; From e05e184785da1303023bbd98fae2ad00a6bc1333 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 5 Sep 2026 20:58:26 +0200 Subject: [PATCH 2/2] fix(nix): give the launcher a shell and coreutils/gnused on PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/start.js shells out to `npm run prisma:migrate`, but the wrapper only put nodejs and openssl on PATH, so npm could not spawn a shell at all and every boot logged npm error syscall spawn sh npm error enoent spawn sh ENOENT This was never fatal — the systemd unit's ExecStartPre already applies migrations — but it is pure noise in the journal and it made the real bcrypt crash harder to spot. A shell alone is not enough: pnpm's generated node_modules/.bin/prisma is itself a /bin/sh script that resolves its basedir with `dirname` and `sed`. Ship bash, coreutils and gnused so the launcher stands on its own under any caller's environment — the systemd unit's PATH, docker compose, or a bare shell — rather than depending on what the caller happens to export. Verified: with PATH set to /nonexistent, the wrapper now applies all 25 migrations and starts the daemon cleanly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBjd9Rw4ct134JH3CnLVaw --- package.nix | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/package.nix b/package.nix index 3dc355e..27d8cc5 100644 --- a/package.nix +++ b/package.nix @@ -9,6 +9,9 @@ # 7.x in nixpkgs which doesn't ship libquery_engine.node, so we'd fail # at postinstall. prisma-engines_6, + bash, + coreutils, + gnused, openssl, sqlite, python311, @@ -131,7 +134,22 @@ stdenv.mkDerivation (finalAttrs: { makeWrapper ${lib.getExe nodejs_24} $out/bin/nsecbunkerd \ --add-flags $out/share/nsecbunkerd/scripts/start.js \ --set NODE_ENV production \ - --prefix PATH : ${lib.makeBinPath [ openssl nodejs_24 ]} \ + --prefix PATH : ${ + lib.makeBinPath [ + openssl + nodejs_24 + # scripts/start.js shells out to `npm run prisma:migrate`, and + # npm needs a shell to spawn at all. The pnpm-generated + # `node_modules/.bin/prisma` shim is itself a /bin/sh script + # that resolves its basedir with `dirname` + `sed`. Ship all + # three so the launcher works under any caller's environment + # (the systemd unit's PATH, docker compose, a bare shell) + # rather than depending on what the caller happens to export. + bash + coreutils + gnused + ] + } \ ${ lib.concatStringsSep " \\\n " ( lib.mapAttrsToList (n: v: "--set ${n} ${lib.escapeShellArg v}") prismaEnv