Files
accounted/lib/env/public-flags.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

66 lines
3.0 KiB
TypeScript

/**
* Runtime reads of `NEXT_PUBLIC_*` boolean flags.
*
* The Docker image is generic: it is built once with sentinel values
* (`ENV NEXT_PUBLIC_SELF_HOSTED=__NEXT_PUBLIC_SELF_HOSTED__` in the Dockerfile)
* and `docker-entrypoint.sh` seds the operator's real values into `.next` at
* container start. That contract has one requirement nobody wrote down: the
* sentinel must still BE in the build output for sed to find.
*
* Writing `process.env.NEXT_PUBLIC_SELF_HOSTED === 'true'` breaks it. The
* bundler inlines the sentinel, leaving `"__NEXT_PUBLIC_SELF_HOSTED__" ===
* 'true'`, which the minifier constant-folds to `false` and then
* dead-code-eliminates. Variable name and sentinel both disappear, sed has
* nothing to replace, and the flag is permanently false no matter what the
* operator configures.
*
* That shipped: every Docker self-host ran with the entitlement paywall live,
* so `ai`, `bank_sync`, `skatteverket` and `email_send` went dark 30 days after
* company creation (diagnosed 2026-08-17 against a running NAS instance, where
* the compiled gate read `function r(){return"true"!==process.env.FORCE_PAYWALL
* &&"true"===process.env.DISABLE_PAYWALL}` with the self-hosted branch gone).
* The un-prefixed `FORCE_PAYWALL`/`DISABLE_PAYWALL` survived precisely because
* they are never inlined.
*
* The fix is to keep the sentinel out of a foldable comparison. Reading it as a
* VALUE (a function argument) preserves the string literal in the bundle; the
* comparison then happens at runtime, after sed has done its work. The Set
* lookup is the belt to that suspenders: a minifier can fold `x === 'true'`,
* but not `SET.has(x)`.
*
* Use `flagEnabled(process.env.NEXT_PUBLIC_WHATEVER)` for any public boolean
* flag. `scripts/checks/no-new-antipatterns.mjs` (folded-public-flag) fails the
* build on a direct comparison so this cannot regress silently again.
*/
/**
* Values that mean "on". Deliberately a Set: `x === 'true'` is foldable when
* `x` is a build-time constant, `TRUTHY_VALUES.has(x)` is not.
*/
const TRUTHY_VALUES = new Set(['true'])
/**
* Whether a `NEXT_PUBLIC_*` flag is switched on.
*
* Pass the env read as an argument, never compare it in place:
*
* flagEnabled(process.env.NEXT_PUBLIC_BANKID_ENABLED) // correct
* process.env.NEXT_PUBLIC_BANKID_ENABLED === 'true' // folded away in Docker
*/
export function flagEnabled(value: string | undefined): boolean {
return value !== undefined && TRUTHY_VALUES.has(value)
}
/**
* Whether this is a self-hosted deployment (Docker), as opposed to the hosted
* product. Self-hosted disables forced MFA, session timeouts, analytics and the
* entitlement paywall, and lifts the hosted upload ceiling.
*
* Named accessor rather than a bare `flagEnabled` call because five modules ask
* this same question and the answer decides legal-ish behaviour (what an AGPL
* operator's own instance is allowed to do without paying us).
*/
export function isSelfHosted(): boolean {
return flagEnabled(process.env.NEXT_PUBLIC_SELF_HOSTED)
}