fix(validation): stop unrecognised boolean strings coercing to a silent false
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s

checkField in packages/validation/src/index.ts (and its browser mirror in
runtime.ts) treated any string other than "true"/"on" as false with no
error, so typos like "treu" or values like "yes"/"1"/"TRUE" silently
passed as false.

Now:
- true/false booleans pass through unchanged
- recognised true strings (case-insensitive, trimmed): true, on, 1, yes
- recognised false strings: false, off, 0, no
- numeric 1/0 coerce (JSON payloads)
- undefined/null/"" still coerce to false (unchecked-checkbox semantics)
- anything else is now a type error (desc.typeMessage or "Must be true or
  false") instead of a silent false

Locked-in behaviours preserved: a required boolean given false still
errors, and parseEnv DEBUG: "true" coercion still works.

Added coverage for recognised strings, numeric 1/0, the type-error
regression guard, absent/empty handling, the required+false case, and a
client/server parity test driving both checkField and the browser runtime
through the same inputs.

Blast radius: searched packages/, examples/, services/ for v.boolean()
usage; all existing call sites (auth consent/rememberDevice, db 'active'
default, example consent checkboxes) feed true/false/'on'/absent values,
none of which change behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 21:26:20 +05:30
co-authored by Claude Opus 5
parent 3ae5d7cf97
commit 281615a4b0
4 changed files with 193 additions and 2 deletions
+22 -1
View File
@@ -114,7 +114,28 @@ export function checkField(
}
if (desc.type === "boolean") {
const value = raw === true || raw === "true" || raw === "on";
const empty = raw === undefined || raw === null || raw === "";
let value: boolean;
if (typeof raw === "boolean") {
value = raw;
} else if (empty) {
value = false;
} else if (raw === 1) {
value = true;
} else if (raw === 0) {
value = false;
} else if (typeof raw === "string") {
const norm = raw.trim().toLowerCase();
if (norm === "true" || norm === "on" || norm === "1" || norm === "yes") {
value = true;
} else if (norm === "false" || norm === "off" || norm === "0" || norm === "no") {
value = false;
} else {
return { value: raw, error: desc.typeMessage ?? "Must be true or false" };
}
} else {
return { value: raw, error: desc.typeMessage ?? "Must be true or false" };
}
if (!desc.optional && !value) {
return { value, error: desc.requiredMessage || "Required" };
}
+22 -1
View File
@@ -45,7 +45,28 @@ export const VALIDATE_RUNTIME = String.raw`
return (missing && !desc.optional) ? (desc.requiredMessage || "Required") : null;
}
if (desc.type === "boolean") {
var b = raw === true || raw === "true" || raw === "on";
var bEmpty = raw === undefined || raw === null || raw === "";
var b;
if (typeof raw === "boolean") {
b = raw;
} else if (bEmpty) {
b = false;
} else if (raw === 1) {
b = true;
} else if (raw === 0) {
b = false;
} else if (typeof raw === "string") {
var bNorm = raw.trim().toLowerCase();
if (bNorm === "true" || bNorm === "on" || bNorm === "1" || bNorm === "yes") {
b = true;
} else if (bNorm === "false" || bNorm === "off" || bNorm === "0" || bNorm === "no") {
b = false;
} else {
return desc.typeMessage || "Must be true or false";
}
} else {
return desc.typeMessage || "Must be true or false";
}
return (!desc.optional && !b) ? (desc.requiredMessage || "Required") : null;
}
var pre = desc.trim && typeof raw === "string" ? raw.trim() : raw;