Organize src/ into concern folders (http, auth, admin, plugin-host, ui); co-locate tests, move plugin-api barrel into plugin-host, sync docs + AGENTS layout

This commit is contained in:
2026-06-24 00:23:55 +02:00
parent 6d316c4888
commit de22f51c12
113 changed files with 282 additions and 265 deletions
+35
View File
@@ -0,0 +1,35 @@
// URL safety helpers. Two pure, dependency-free guards:
//
// safeUrl(value) — sanitise an untrusted URL before rendering it in an href/src attribute.
// Partials escape *text*, but a URL field is emitted verbatim, so a
// `javascript:`/`data:` URL from upstream/user data would be live XSS. The
// contract (docs/plugin-contract.md) is: a relative or http(s) URL is allowed,
// anything else collapses to "#". Exported to plugins via plugin-api.ts.
//
// localPath(value) — validate a redirect target is a *same-origin* path (the redirect-URI
// allowlist). Used for `return_to`: a host-relative "/a/b?x=1" passes, an
// absolute or protocol-relative ("//evil.com", "https://evil.com") is rejected
// so a crafted ?return_to= can't turn login completion into an open redirect.
// ASCII control chars + space that browsers strip/ignore when resolving a URL — strip them before
// the scheme check so "java\tscript:" / a leading space can't masquerade as relative.
const CONTROL_G = /[\u0000-\u0020\u007f]/g;
const CONTROL = /[\u0000-\u0020\u007f]/;
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i; // a URL scheme prefix, e.g. "javascript:", "http:"
const HTTP_SCHEME = /^https?:/i;
export function safeUrl(value: string): string {
const cleaned = value.replace(CONTROL_G, "");
if (!cleaned) return "#";
// A scheme present? Allow only http(s). No scheme ⇒ relative ⇒ safe. Return the original once
// deemed safe (EJS still HTML-escapes it into the attribute; the inert control chars don't matter).
if (HAS_SCHEME.test(cleaned) && !HTTP_SCHEME.test(cleaned)) return "#";
return value;
}
export function localPath(value: string | null | undefined): string | null {
if (!value || CONTROL.test(value)) return null;
if (!value.startsWith("/")) return null; // must be host-relative
if (value.startsWith("//") || value.startsWith("/\\")) return null; // protocol-relative ⇒ off-origin
return value;
}