export interface RouteMatch { ns: string; id: string; rest: string; } const NAMESPACE = /^\/[^/\s]+$/; export function validateNamespace(ns: string): string { if (!NAMESPACE.test(ns)) { throw new Error( `namespace must be '/' plus a single path segment: ${JSON.stringify(ns)}`, ); } return ns; } // ids are client-generated names, not capabilities; restricting them to the // unreserved URI charset closes weird-key/path-confusion holes at the boundary. const ID_CHARSET = /^[A-Za-z0-9._~-]+$/; const ID_MAX_LENGTH = 256; export function invalidIdReason(id: string): string | null { if (id === "") return "id must be non-empty"; if (id.length > ID_MAX_LENGTH) { return `id must be at most ${ID_MAX_LENGTH} characters`; } if (!ID_CHARSET.test(id)) return "id must contain only [A-Za-z0-9._~-]"; return null; } export function parseRoute(pathname: string): RouteMatch | null { if (!pathname.startsWith("/")) return null; const idStart = pathname.indexOf("/", 1); if (idStart <= 1) return null; const ns = pathname.slice(0, idStart); const remainder = pathname.slice(idStart); const idEnd = remainder.indexOf("/", 1); const id = idEnd === -1 ? remainder.slice(1) : remainder.slice(1, idEnd); const rest = idEnd === -1 ? "" : remainder.slice(idEnd); return { ns, id, rest }; }