diff --git a/scripts/build/assembleStandalone.mjs b/scripts/build/assembleStandalone.mjs index 16ee2426c2..05e3d2a82d 100644 --- a/scripts/build/assembleStandalone.mjs +++ b/scripts/build/assembleStandalone.mjs @@ -163,6 +163,43 @@ const EXTRA_MODULE_ENTRIES = [ dest: ["node_modules", "pino-pretty"], }, { label: "split2", src: ["node_modules", "split2"], dest: ["node_modules", "split2"] }, + { + // ioredis is a deliberately LAZY dependency (Redis is optional — see the + // #6559 comment in src/shared/utils/rateLimiter.ts) — reached only via a + // runtime `await import("ioredis")` in rateLimiter.ts, + // warmupScheduler/circuitBreakerFactory.ts and quota/redisQuotaStore.ts, + // never through a static top-level import. The standalone tracer only + // follows statically-analyzable imports, so it never sees these call + // sites and drops ioredis from node_modules/ entirely. Any self-hosted + // deployment that actually sets REDIS_URL crashes the first time it + // reaches one of those call sites with "Cannot find module 'ioredis'" — + // reproduced on a production Docker deployment (REDIS_URL configured, + // v3.8.49) where the standalone image shipped ioredis/package.json but + // none of its own dependencies or built/ output. + label: "ioredis (dynamic import — #6559)", + src: ["node_modules", "ioredis"], + dest: ["node_modules", "ioredis"], + }, + { + // bcryptjs IS statically imported by src/lib/auth/managementPassword.ts, + // so the main server bundle is fine — Next's server compiler inlines the + // small pure-JS package directly into the compiled route chunk instead of + // leaving it as an external node_modules dependency. bin/cli/settings- + // store.mjs (the `omniroute reset-password` / bin/reset-password.mjs + // CLI, used to recover a lost dashboard password) is a separate, + // unbundled entrypoint that does a plain runtime `import bcrypt from + // "bcryptjs"` and needs the real package physically present in + // node_modules/ — which nothing else requires as a loose runtime + // dependency, so it is never copied. Reproduced on a production + // deployment: `node bin/reset-password.mjs --password-stdin` failed with + // "Cannot find package 'bcryptjs' imported from + // /app/bin/cli/settings-store.mjs" (ERR_MODULE_NOT_FOUND) even though the + // same container's dashboard login (which also depends on bcryptjs) was + // working normally. + label: "bcryptjs (bin/cli/settings-store.mjs — reset-password CLI)", + src: ["node_modules", "bcryptjs"], + dest: ["node_modules", "bcryptjs"], + }, { label: "migrations", src: ["src", "lib", "db", "migrations"], dest: ["migrations"] }, { label: "MITM server", src: ["src", "mitm", "server.cjs"], dest: ["src", "mitm", "server.cjs"] }, { diff --git a/tests/unit/build/assemble-standalone.test.ts b/tests/unit/build/assemble-standalone.test.ts index 5894c3d82b..2adc67ff9d 100644 --- a/tests/unit/build/assemble-standalone.test.ts +++ b/tests/unit/build/assemble-standalone.test.ts @@ -40,6 +40,10 @@ function seedSidecarSources(root: string) { "node_modules/pino-abstract-transport/index.js", "node_modules/pino-pretty/index.js", "node_modules/split2/index.js", + "node_modules/ioredis/package.json", + "node_modules/ioredis/built/index.js", + "node_modules/bcryptjs/package.json", + "node_modules/bcryptjs/index.js", "node_modules/playwright-core/index.js", "node_modules/sql.js/package.json", "node_modules/sql.js/dist/sql-wasm.js", @@ -173,6 +177,19 @@ test("async and sync sidecar copy paths produce identical bundle trees", async ( ]) { assert.ok(asyncTree.includes(sqlJsFile), `sql.js runtime file copied: ${sqlJsFile}`); } + // #6559 / reset-password CLI: both are only reachable at runtime (a dynamic + // `import("ioredis")`, or a separate unbundled bin/cli/ entrypoint for + // bcryptjs), so the standalone tracer never picks them up on its own — + // regression guard for the two "Cannot find module/package" crashes + // reproduced on a real self-hosted deployment. + for (const runtimeOnlyFile of [ + "node_modules/ioredis/package.json", + "node_modules/ioredis/built/index.js", + "node_modules/bcryptjs/package.json", + "node_modules/bcryptjs/index.js", + ]) { + assert.ok(asyncTree.includes(runtimeOnlyFile), `runtime-only dep copied: ${runtimeOnlyFile}`); + } fs.rmSync(tmp, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); });