fence-attrs.ts1.6 KBView on GitHub /**
* Shared attribute handling for the coaching fence nodes.
*
* Both nodes carry their verbatim fence body in a percent-encoded HTML attribute and render
* agent-authored links, so both need the same two guards. See apps/server/docs/coaching-agent.md
* §3.2 part E.
*/
/**
* Decode a fence body out of its `data-*` attribute.
*
* `decodeURIComponent` throws a `URIError` on a malformed sequence (a lone `%`), and this runs
* inside ProseMirror's DOM parse — an exception there takes down the whole editor rather than the
* one node. Everything else in these fences degrades to a raw render, so this does too.
*/
export function decodeFenceAttr(raw: string | null): string {
if (!raw) return '';
try {
return decodeURIComponent(raw);
} catch {
return raw;
}
}
/** Schemes safe to put in an `href`. `javascript:` and `data:` execute; nothing else here does. */
const SAFE_SCHEMES = new Set(['http:', 'https:', 'mailto:']);
/**
* Pass through a link only when its scheme is safe, otherwise drop it.
*
* The fence body is document content written by an agent, so a `recording:` value is untrusted
* input — a `javascript:` URL there would execute on click.
*/
export function safeHref(raw: string | undefined): string | undefined {
if (!raw) return undefined;
const trimmed = raw.trim();
try {
// A relative URL resolves against the base and inherits its scheme, so it stays allowed.
const { protocol } = new URL(trimmed, 'https://cedar.invalid');
return SAFE_SCHEMES.has(protocol) ? trimmed : undefined;
} catch {
return undefined;
}
}