From 6bfc571e4ed12f554ade1f385875ef89a963c35e Mon Sep 17 00:00:00 2001 From: mindsers Date: Sun, 11 Oct 2026 07:49:02 +0000 Subject: [PATCH] fix(infra): keep the boot checks in the production web build package.json lists the modules that have side effects, and the bundler drops a bare import of any module that is not on the list. boot.server was not on it, so the built web server ran neither validateEnv() nor the Row-Level Security guard. It started with a short or placeholder session secret, without DB_RUNTIME_URL, or as a database role that bypasses RLS: everything the self-hosting guide says it refuses. The dev server and the worker are not bundled and kept running the checks, which is why nothing noticed. boot.server is now declared, along with the worker's locale module, the other bare import of an app module. A test fails when a side-effect-only import of an app module is missing from the list. The health-monitoring guide notes what follows for probes: the web process needs the database once, at startup. --- docs/self-hosting/health-monitoring.md | 6 +++ package.json | 4 +- scripts/side-effect-imports.test.ts | 57 ++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 scripts/side-effect-imports.test.ts diff --git a/docs/self-hosting/health-monitoring.md b/docs/self-hosting/health-monitoring.md index 4e79a6d0..a4c53cea 100644 --- a/docs/self-hosting/health-monitoring.md +++ b/docs/self-hosting/health-monitoring.md @@ -25,6 +25,12 @@ back; readiness only takes the pods out of the load balancer until the dependenc Neither endpoint needs to be public: the orchestrator reaches them on the container port. +The web process checks the database itself once, when it starts: outside development it exits if it +cannot verify that Row-Level Security is enforced (see +[Row-Level Security](../development/row-level-security.md)). An instance started while PostgreSQL is +unreachable exits before `/livez` can answer; with a restart policy it comes back once the database +does. + ### Example Usage ```bash diff --git a/package.json b/package.json index 0ba936f5..23329584 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "**/pdf-worker.ts", "**/pdf-globals-shim.ts", "**/pdf-upsert-shim.ts", - "**/env.server.ts" + "**/env.server.ts", + "**/boot.server.ts", + "**/worker-locale.server.ts" ], "type": "module", "scripts": { diff --git a/scripts/side-effect-imports.test.ts b/scripts/side-effect-imports.test.ts new file mode 100644 index 00000000..7103e626 --- /dev/null +++ b/scripts/side-effect-imports.test.ts @@ -0,0 +1,57 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { dirname, extname, join, relative, resolve } from 'node:path' + +import { describe, expect, it } from 'vitest' + +const ROOT = resolve(import.meta.dirname, '..') +const APP_DIR = join(ROOT, 'app') + +// `import './x'` with no binding: the module is imported for what it does when it is evaluated. +const BARE_IMPORT = /^import\s+['"]((?:~|\.{1,2})\/[^'"]+)['"]/gm +const SOURCE_FILE = /\.tsx?$/ +const TEST_FILE = /\.test\.tsx?$/ +const GLOB_SPECIAL_CHARS = /[.+^${}()|[\]\\]/g +const GLOBSTAR_SEGMENT = /\*\*\//g +const GLOB_WILDCARD = /\*/g + +function sourceFiles(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap(entry => { + const path = join(dir, entry.name) + if (entry.isDirectory()) return entry.name === 'generated' || entry.name === 'paraglide' ? [] : sourceFiles(path) + return SOURCE_FILE.test(entry.name) && !TEST_FILE.test(entry.name) ? [path] : [] + }) +} + +function importedFile(importer: string, specifier: string): string { + const target = specifier.startsWith('~/') ? join(APP_DIR, specifier.slice(2)) : resolve(dirname(importer), specifier) + return relative(ROOT, extname(target) === '.css' ? target : `${target}.ts`) +} + +function globToRegExp(glob: string): RegExp { + const pattern = glob + .replace(GLOB_SPECIAL_CHARS, '\\$&') + .replace(GLOBSTAR_SEGMENT, '\u0000') + .replace(GLOB_WILDCARD, '[^/]*') + .replaceAll('\u0000', '(?:.*/)?') + return new RegExp(`^${pattern}$`) +} + +describe('side-effect-only imports', () => { + // package.json lists the modules that have side effects, and the production build drops a bare + // import of any module that is not on the list. That is how the boot checks (environment + // validation, the Row-Level Security guard) went missing from the built web server while every + // test, the dev server and the worker kept running them. + it('are all declared in package.json "sideEffects", or the production build drops them', () => { + const { sideEffects } = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')) as { sideEffects: string[] } + const declared = sideEffects.map(globToRegExp) + + const undeclared = sourceFiles(APP_DIR).flatMap(file => + [...readFileSync(file, 'utf8').matchAll(BARE_IMPORT)] + .map(match => importedFile(file, match[1] as string)) + .filter(imported => !declared.some(pattern => pattern.test(imported))) + .map(imported => `${relative(ROOT, file)} imports ${imported}`), + ) + + expect(undeclared).toEqual([]) + }) +})