Files
HealthLog/src/lib/auth/step-up-factors.ts
T
Marc-André Bombeck 3e8846929c test(guards): load no module graph inside a guard's test body
step-up-elevation-guard timed out under load in three separate gates
and passed every time on its own. One of its tests imported
@/lib/auth/step-up inside the test body to read the fresh-factor set,
and that module reaches the database client and the generated Prisma
client: the import alone ran against the test timeout, and how long it
took depended on the machine, not on the code under test.

The fresh-factor definitions (StepUpMethod, FRESH_FACTOR_METHODS,
isFreshFactorMethod) move to step-up-factors.ts, which imports nothing;
the guard imports it statically and the test is synchronous. Two more
guards did the same: next-image-optimizer-guard loaded next.config and
its plugins per test, and proxy-relative-redirect-guard loaded the
redirect helper with next/server. Both import statically now.

guard-dynamic-import-guard.test.ts parses every file of the guard
project with the TypeScript parser and fails on any import() call, so
a guard that needs a value imports it with the file or reads the
source; a test that must load late belongs to the isolated project.
2026-10-23 22:51:51 +02:00

46 lines
2.0 KiB
TypeScript

/**
* Which re-proved factors satisfy a fresh-factor gate.
*
* On its own, with nothing imported, because it is an authorisation input
* the structural guards read directly: the definition is pinned by
* `step-up-elevation-guard.test.ts`, and reaching it through `step-up.ts`
* meant loading the database client and the generated Prisma client inside
* a test body.
*/
/**
* Which factor was re-proved. This is an AUTHORISATION INPUT — see
* `FRESH_FACTOR_METHODS` and the file header.
*
* password — the account password. Reaches what a plain cookie session
* reaches, and no further.
* totp — a current code from the enrolled authenticator.
* webauthn — an assertion from a registered second-factor security key.
* passkey — an assertion from a primary passkey. At parity with
* `/api/auth/passkey/login-verify`, which stamps `mfaVerifiedAt`
* on the web for the very same ceremony.
*/
export type StepUpMethod = "password" | "totp" | "webauthn" | "passkey";
/**
* The methods that satisfy a fresh-factor gate — exactly the set for which the
* web writes `mfaVerifiedAt`. `password` is deliberately absent: a password
* login has never satisfied step-up on the web and must not start here.
*
* A recovery code is also absent, and that IS a narrowing relative to the web,
* where `/api/auth/mfa/verify` accepts one and does stamp the session. The
* consequence is that an account which has lost its authenticator cannot
* disable its second factor from the app — only from the web. That asymmetry is
* deliberate and flagged rather than quietly closed, because admitting a
* recovery code here means spending one break-glass credential to authorise
* spending another.
*/
export const FRESH_FACTOR_METHODS: ReadonlySet<StepUpMethod> = new Set([
"totp",
"webauthn",
"passkey",
]);
export function isFreshFactorMethod(method: StepUpMethod): boolean {
return FRESH_FACTOR_METHODS.has(method);
}