53 lines
1.6 KiB
TypeScript
53 lines
1.6 KiB
TypeScript
import { SecurityError } from "./errors.ts";
|
|
|
|
const TRUSTED_HTML = Symbol("wrnexus.trusted-html");
|
|
|
|
export interface TrustedHtmlPolicy {
|
|
/** Stable policy name used in diagnostics and CSP/Trusted Types integration. */
|
|
name: string;
|
|
/** Application-supplied, reviewed sanitizer. It must return sanitized HTML. */
|
|
sanitize(input: string): string;
|
|
}
|
|
|
|
export interface TrustedHtmlValue {
|
|
readonly policy: string;
|
|
readonly value: string;
|
|
readonly [TRUSTED_HTML]: true;
|
|
}
|
|
|
|
export function createTrustedHtml(input: string, policy: TrustedHtmlPolicy): TrustedHtmlValue {
|
|
if (!policy?.name?.trim() || typeof policy.sanitize !== "function") {
|
|
throw new SecurityError(
|
|
"WRN-SEC-TRUSTED-HTML-POLICY",
|
|
"Trusted HTML requires a named sanitizer policy.",
|
|
);
|
|
}
|
|
const value = policy.sanitize(String(input));
|
|
if (typeof value !== "string") {
|
|
throw new SecurityError(
|
|
"WRN-SEC-TRUSTED-HTML-RESULT",
|
|
`Trusted HTML policy '${policy.name}' must return a string.`,
|
|
);
|
|
}
|
|
return Object.freeze({ policy: policy.name.trim(), value, [TRUSTED_HTML]: true as const });
|
|
}
|
|
|
|
export function isTrustedHtml(value: unknown): value is TrustedHtmlValue {
|
|
return Boolean(
|
|
value &&
|
|
typeof value === "object" &&
|
|
(value as Partial<TrustedHtmlValue>)[TRUSTED_HTML] === true &&
|
|
typeof (value as Partial<TrustedHtmlValue>).value === "string",
|
|
);
|
|
}
|
|
|
|
export function unwrapTrustedHtml(value: TrustedHtmlValue): string {
|
|
if (!isTrustedHtml(value)) {
|
|
throw new SecurityError(
|
|
"WRN-SEC-TRUSTED-HTML-REQUIRED",
|
|
"Raw HTML must be produced by createTrustedHtml().",
|
|
);
|
|
}
|
|
return value.value;
|
|
}
|