fix(nix): build bcrypt's native binding again under pnpm 10 — unbreaks nsecbunkerd on aio-demo #54

Merged
padreug merged 2 commits from fix/pnpm10-bcrypt-native-build into dev 2026-09-05 19:00:02 +00:00
Owner

What broke

nsecbunkerd has been crashlooping on aio-demo (restart counter climbing past 21). Every restart dies with:

Error: Cannot find module '.../bcrypt@5.1.1/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node'
Require stack:
- .../node_modules/.pnpm/bcrypt@5.1.1/node_modules/bcrypt/bcrypt.js
- .../dist/daemon/index.js

aio-demo's flake.lock pins nsecbunkerd-dev at exactly 0d8c436f — the nodejs_20/pnpm_9 → nodejs_24/pnpm_10 bump. Its store path contains only binding.gyp and the C++ sources under bcrypt@5.1.1; there is no lib/binding/ and no .node file anywhere. The daemon requires bcrypt at load, so it dies in about a second, every time.

Why

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. Unlike pnpm 9, it refuses to run any dependency lifecycle script unless the package is allow-listed (onlyBuiltDependencies / pnpm approve-builds) — and it skips them silently, with the install still reporting success. So the bump turned that line into a no-op, the build kept passing, and the breakage only surfaced at boot on the deployed host.

Fix

d2ec84a — restore the native build, and make the failure loud

  • --config.dangerouslyAllowAllBuilds=true on the offline reinstall, restoring 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.
  • A doInstallCheck that require()s bcrypt from the installed $out tree, the same way dist/daemon/index.js does. This failure mode is invisible at build time and fatal at boot, so it has to break the build rather than the host.

e05e184 — stop the launcher erroring on every boot

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:

npm error syscall spawn sh
npm error enoent spawn sh ENOENT

Never fatal — ExecStartPre already applies migrations ("No pending migrations to apply") — but it's journal noise that made the real bcrypt crash harder to spot. A shell alone isn't enough either: pnpm's generated node_modules/.bin/prisma is itself a /bin/sh script resolving its basedir with dirname + sed. Ships bash + coreutils + gnused so the launcher stands on its own under any caller's environment.

Verification

Built against the same nixpkgs the deploy uses (da5ad661), both commits independently:

  • lib/binding/napi-v3/bcrypt_lib.node is produced
  • installCheckPhase prints bcrypt native binding loads OK
  • with PATH=/nonexistent, the wrapper applies all 25 migrations and the daemon reaches ✅ nsecBunker ready to serve requests. — zero MODULE_NOT_FOUND
  • pnpmDeps hash unchanged

After merge

The daemon cannot recover without a new build, so this needs a flake.lock bump of nsecbunkerd-dev in deploy/server-deploy and a redeploy to aio-demo.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QBjd9Rw4ct134JH3CnLVaw

## What broke `nsecbunkerd` has been crashlooping on **aio-demo** (restart counter climbing past 21). Every restart dies with: ``` Error: Cannot find module '.../bcrypt@5.1.1/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node' Require stack: - .../node_modules/.pnpm/bcrypt@5.1.1/node_modules/bcrypt/bcrypt.js - .../dist/daemon/index.js ``` `aio-demo`'s `flake.lock` pins `nsecbunkerd-dev` at exactly `0d8c436f` — the `nodejs_20`/`pnpm_9` → `nodejs_24`/`pnpm_10` bump. Its store path contains only `binding.gyp` and the C++ sources under `bcrypt@5.1.1`; there is no `lib/binding/` and no `.node` file anywhere. The daemon requires `bcrypt` at load, so it dies in about a second, every time. ## Why `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. Unlike pnpm 9, it refuses to run **any** dependency lifecycle script unless the package is allow-listed (`onlyBuiltDependencies` / `pnpm approve-builds`) — and it skips them **silently**, with the install still reporting success. So the bump turned that line into a no-op, the build kept passing, and the breakage only surfaced at boot on the deployed host. ## Fix **`d2ec84a` — restore the native build, and make the failure loud** - `--config.dangerouslyAllowAllBuilds=true` on the offline reinstall, restoring 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. - A `doInstallCheck` that `require()`s bcrypt from the **installed** `$out` tree, the same way `dist/daemon/index.js` does. This failure mode is invisible at build time and fatal at boot, so it has to break the build rather than the host. **`e05e184` — stop the launcher erroring on every boot** `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: ``` npm error syscall spawn sh npm error enoent spawn sh ENOENT ``` Never fatal — `ExecStartPre` already applies migrations ("No pending migrations to apply") — but it's journal noise that made the real bcrypt crash harder to spot. A shell alone isn't enough either: pnpm's generated `node_modules/.bin/prisma` is itself a `/bin/sh` script resolving its basedir with `dirname` + `sed`. Ships `bash` + `coreutils` + `gnused` so the launcher stands on its own under any caller's environment. ## Verification Built against the same nixpkgs the deploy uses (`da5ad661`), both commits independently: - `lib/binding/napi-v3/bcrypt_lib.node` is produced - `installCheckPhase` prints `bcrypt native binding loads OK` - with `PATH=/nonexistent`, the wrapper applies all 25 migrations and the daemon reaches `✅ nsecBunker ready to serve requests.` — zero `MODULE_NOT_FOUND` - `pnpmDeps` hash unchanged ## After merge The daemon cannot recover without a new build, so this needs a `flake.lock` bump of `nsecbunkerd-dev` in `deploy/server-deploy` and a redeploy to `aio-demo`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QBjd9Rw4ct134JH3CnLVaw
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBjd9Rw4ct134JH3CnLVaw
fix(nix): give the launcher a shell and coreutils/gnused on PATH
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
e05e184785
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBjd9Rw4ct134JH3CnLVaw
padreug deleted branch fix/pnpm10-bcrypt-native-build 2026-09-05 19:00:02 +00:00
Sign in to join this conversation.
No reviewers
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!54
No description provided.