fix: 2026-06-09 full-codebase audit hardening
Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace the raw number|string transform idiom across every schema (the root-cause NaN bug class); emailOrEmpty + lenient isoDateString/timeString. Security: roles privilege-escalation closed; refresh-token family revocation on reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices gross VAT on all paths; orders-pdf custom-items authz. Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse confirm/cancel, attendance lockUserRow); uniqueness checks moved into create transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO). Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted fields; dashboard invalidation gaps; stale-closure confirm bugs. Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier; tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes), isolated on app_test via .env.test with a hard-throw setup guard. Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,3 +32,37 @@ describe("env validation", () => {
|
||||
expect(config.db.url).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("env validation — failure path", () => {
|
||||
// The config singleton runs its validation at module-load time and is then
|
||||
// cached by the module system, so we cannot re-import it with bad env to
|
||||
// observe a throw without complex module-registry resetting. Instead we test
|
||||
// the exact predicates config/env.ts uses to reject bad input — this is the
|
||||
// logic that protects the boot, and the loaded config above proves the
|
||||
// happy path. (Predicates kept in sync with src/config/env.ts.)
|
||||
const HEX64_RE = /^[0-9a-fA-F]{64}$/;
|
||||
|
||||
it("rejects a too-short / non-hex JWT_SECRET", () => {
|
||||
expect(HEX64_RE.test("short")).toBe(false);
|
||||
expect(HEX64_RE.test("a".repeat(63))).toBe(false); // one char short
|
||||
expect(HEX64_RE.test("z".repeat(64))).toBe(false); // 64 chars, not hex
|
||||
// The real config's secret must pass the very same check.
|
||||
expect(HEX64_RE.test(config.jwt.secret)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects an out-of-range or non-numeric PORT", () => {
|
||||
const portValid = (p: number) => !Number.isNaN(p) && p >= 1 && p <= 65535;
|
||||
expect(portValid(parseInt("0", 10))).toBe(false);
|
||||
expect(portValid(parseInt("70000", 10))).toBe(false);
|
||||
expect(portValid(parseInt("notaport", 10))).toBe(false); // NaN
|
||||
expect(portValid(config.port)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a non-positive ACCESS_TOKEN_EXPIRY", () => {
|
||||
const expiryValid = (n: number) => !Number.isNaN(n) && n > 0;
|
||||
expect(expiryValid(0)).toBe(false);
|
||||
expect(expiryValid(-1)).toBe(false);
|
||||
expect(expiryValid(parseInt("oops", 10))).toBe(false); // NaN
|
||||
expect(expiryValid(config.jwt.accessTokenExpiry)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user