src/portal-host.ts
66 lines and 6 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| isPortalHost | 25 to 27 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| portalBase | 30 to 32 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| portalCookiePath | 36 to 38 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| publicPortalPath | 41 to 45 | 4 | 100% | OkCRAP 4 | No data: mutation testing has not run on this repo yet |
| isSameOriginPath | 49 to 51 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| portalHostRequest | 55 to 74 | 8 | 100% | WatchCRAP 8 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by src/digest.ts, src/index.ts, src/ops-feed.ts, src/portal-actions.ts, src/portal-app.ts, src/portal-auth.ts, src/push.ts, src/routes.ts.
Source
// Capsid Portal on its own host ("one address pattern for the family"):
// https://portal.dustinedwards.info, a Custom Domain on this Worker. It is a
// separate origin from every public site, and its cookies are host-only.
//
// The Portal's routes are written once, under /portal (src/routes.ts). On the Portal host a
// request for /x is handled as /portal/x, so every route, the gate and every check apply
// unchanged; the workers.dev /portal keeps answering as before until the new address is
// verified live, and is then retired by a follow-up PR. Two exceptions on the Portal host:
// - a browser asking for /portal/... is sent to the same path at the root (308), so an
// old link lands on the new address;
// - /portal/assets/... is served as it is, not redirected. The built app names its
// hashed files by that absolute path (dashboard/vite.config.ts base), on both hosts.
// Nothing else is reachable on the Portal host: /mcp, /health and the /ops routes are on
// mcp.dustinedwards.info (src/mcp-host.ts), and on this host they are Portal paths behind
// the gate.
// The same values as portal-auth's PORTAL_PATH and PORTAL_PREFIX, stated here rather than
// imported, because portal-auth imports this module.
const PORTAL_PATH = "/portal";
const PORTAL_PREFIX = "/portal/";
export const PORTAL_HOST = "portal.dustinedwards.info";
const PORTAL_ASSETS_PREFIX = "/portal/assets/";
export function isPortalHost(url: URL): boolean {
return url.hostname === PORTAL_HOST;
}
/** The Portal's path as the browser sees it: "" on the Portal host, "/portal" elsewhere. */
export function portalBase(url: URL): string {
return isPortalHost(url) ? "" : PORTAL_PATH;
}
/** Where the Portal's cookies are scoped: the whole Portal host, or /portal elsewhere.
* Never a Domain attribute, so no other subdomain of the family can read or set them. */
export function portalCookiePath(url: URL): string {
return isPortalHost(url) ? "/" : PORTAL_PATH;
}
/** An internal Portal path (/portal, /portal/x) as the browser on this host names it. */
export function publicPortalPath(url: URL, internal: string): string {
if (!isPortalHost(url)) return internal;
if (internal === PORTAL_PATH) return "/";
return internal.startsWith(PORTAL_PREFIX) ? internal.slice(PORTAL_PATH.length) : internal;
}
/** A path the browser may be sent to on this origin: one leading slash and no backslash.
* "//host/x" and "/\\host/x" are protocol-relative to a browser and would leave the origin. */
export function isSameOriginPath(path: string): boolean {
return path.startsWith("/") && !path.startsWith("//") && !path.includes("\\");
}
/** On the Portal host: a redirect for an old /portal path, or the request rewritten to its
* internal /portal path. Anywhere else: the request unchanged. */
export function portalHostRequest(request: Request): { redirect: Response } | { request: Request } {
const url = new URL(request.url);
if (!isPortalHost(url)) return { request };
const path = url.pathname;
const underPortal = path === PORTAL_PATH || path.startsWith(PORTAL_PREFIX);
if (underPortal && !path.startsWith(PORTAL_ASSETS_PREFIX)) {
const publicPath = publicPortalPath(url, path);
// /portal//attacker.example would otherwise become a 308 to https://attacker.example/,
// carrying an OAuth code or a phishing visitor off the host (Grok audit 2026-10, C1).
if (!isSameOriginPath(publicPath)) return { redirect: new Response("Not found", { status: 404 }) };
const target = new URL(publicPath + url.search, url.origin);
// 308 keeps the method and body, so a stray POST to an old address is not turned
// into a GET.
return { redirect: new Response(null, { status: 308, headers: { Location: target.toString(), "Cache-Control": "no-store" } }) };
}
if (underPortal) return { request };
const inner = new URL(url);
inner.pathname = path === "/" ? PORTAL_PREFIX : PORTAL_PATH + path;
return { request: new Request(inner.toString(), request) };
}