clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / package / testing / safety.ts
safety.ts 2.8K
1/**2 * A JSON-like value restricted to booleans, finite numbers, arrays, and plain3 * objects. Strings are excluded so state snapshots record shape and counts but4 * never user values or credentials.5 */6export type ValueFreeState =7 | null8 | boolean9 | number10 | { readonly [key: string]: ValueFreeState }11 | readonly ValueFreeState[];1213const JSON_SECRET_ASSIGNMENT =14 /(\"(?:password|passwd|secret|token|authorization|cookie|credential|session)\"\s*:\s*)\"(?:\\.|[^\"])*\"/gi;15const SECRET_ASSIGNMENT =16 /\b(password|passwd|secret|token|authorization|cookie|credential|session)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']+)/gi;17const BEARER_VALUE = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;18const URL_VALUE = /https?:\/\/[^\s"'<>]+/gi;1920function safeUrl(value: string): string {21 try {22 const url = new URL(value);23 url.username = "";24 url.password = "";25 url.search = "";26 url.hash = "";27 return url.toString();28 } catch {29 return value;30 }31}3233/** Redacts common credential forms before diagnostics are retained in memory. */34export function redactDiagnosticText(35 value: string,36 secretValues: readonly string[] = [],37): string {38 let redacted = value39 .replaceAll(URL_VALUE, (url) => safeUrl(url))40 .replaceAll(JSON_SECRET_ASSIGNMENT, '$1"[redacted]"')41 .replaceAll(BEARER_VALUE, "Bearer [redacted]")42 .replaceAll(SECRET_ASSIGNMENT, (_match, key: string) => `${key}=[redacted]`);43 for (const secret of secretValues) {44 if (secret.length > 0) redacted = redacted.replaceAll(secret, "[redacted]");45 }46 return redacted;47}4849/**50 * State artifacts intentionally reject strings and non-finite numbers. Callers51 * can record shape, counts and booleans, but not user values or credentials.52 */53export function assertValueFreeState(54 value: unknown,55 path = "state",56): asserts value is ValueFreeState {57 if (value === null || typeof value === "boolean") return;58 if (typeof value === "number" && Number.isFinite(value)) return;59 if (typeof value === "string") {60 throw new TypeError(`${path} contains a string value; state artifacts must be value-free`);61 }62 if (Array.isArray(value)) {63 value.forEach((entry, index) => assertValueFreeState(entry, `${path}[${index}]`));64 return;65 }66 if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {67 for (const [key, entry] of Object.entries(value)) {68 assertValueFreeState(entry, `${path}.${key}`);69 }70 return;71 }72 throw new TypeError(`${path} contains an unsupported value`);73}7475/**76 * Normalize a label into a filesystem-safe artifact directory name, falling77 * back to `"failure"` when nothing usable remains.78 */79export function artifactName(value: string): string {80 const safe = value.toLowerCase().replaceAll(/[^a-z0-9._-]+/g, "-").replaceAll(/^-+|-+$/g, "");81 return safe || "failure";82}