import { SecurityError } from "./errors.ts"; import { assertSafeObject, isDangerousObjectKey } from "./object.ts"; export interface SecureSerializeOptions { maxDepth?: number; maxKeys?: number; maxBytes?: number; redact?: RegExp | string[]; redactedValue?: string; } const DEFAULT_SECRET = /(?:password|passwd|secret|token|api[-_]?key|private[-_]?key|otp|authorization|cookie)/i; function shouldRedact(key: string, rule: SecureSerializeOptions["redact"]): boolean { if (Array.isArray(rule)) return rule.includes(key); return (rule ?? DEFAULT_SECRET).test(key); } export function secureJsonStringify(value: unknown, options: SecureSerializeOptions = {}): string { assertSafeObject(value, { maxDepth: options.maxDepth, maxKeys: options.maxKeys }); const redactedValue = options.redactedValue ?? "[REDACTED]"; const json = JSON.stringify(value, function secureReplacer(key, current) { if (isDangerousObjectKey(key)) { throw new SecurityError( "WRN-SEC-PROTOTYPE-POLLUTION", `Dangerous key '${key}' is not allowed.`, ); } if (key && shouldRedact(key, options.redact)) return redactedValue; if (typeof current === "bigint") return current.toString(); if (typeof current === "function" || typeof current === "symbol") { throw new SecurityError( "WRN-SEC-SERIALIZE-TYPE", `Cannot serialize ${typeof current} values.`, ); } return current; }); if (json === undefined) { throw new SecurityError("WRN-SEC-SERIALIZE-EMPTY", "Value cannot be serialized."); } const safe = json .replace(/&/g, "\\u0026") .replace(//g, "\\u003e") .replace(/\u2028/g, "\\u2028") .replace(/\u2029/g, "\\u2029"); const bytes = new TextEncoder().encode(safe).byteLength; const maxBytes = options.maxBytes ?? 256 * 1024; if (bytes > maxBytes) { throw new SecurityError( "WRN-SEC-SERIALIZE-SIZE", `Serialized payload exceeds ${maxBytes} bytes.`, 413, ); } return safe; } export function serializeForHtml(value: unknown, options: SecureSerializeOptions = {}): string { return secureJsonStringify(value, options); }