Files
portal/src/lib/host.ts
T
sharang e7a1290246
ci / shared (push) Successful in 4s
ci / test (push) Successful in 26s
ci / e2e (push) Has been skipped
ci / image (push) Has been skipped
feat(app): Next.js 16 + Auth.js v5 portal skeleton
Next.js 16 + Auth.js v5 skeleton: host→slug middleware, tenant-context layout, OIDC sign-in flow against breakpilot-dev realm. 100% coverage on src/lib. Bumps next to 16.2.6 to clear trivy CVEs in 15.0.3.
2026-05-19 09:35:05 +00:00

42 lines
1.4 KiB
TypeScript

// Host → tenant slug parser for the portal middleware.
//
// In dev we serve at <slug>.localhost:3000 (e.g. acme.localhost:3000). In
// prod we serve at <slug>.breakpilot.com. Backstage lives at the apex —
// no subdomain — and resolves to a fixed `__backstage__` slug.
export type HostMatch =
| { kind: "tenant"; slug: string }
| { kind: "backstage" }
| { kind: "apex" }
| { kind: "unknown" };
// Longest-first so `stage.breakpilot.com` is matched before `breakpilot.com`.
const APEX_HOSTS = ["stage.breakpilot.com", "breakpilot.com", "localhost"];
const APEX_SET = new Set(APEX_HOSTS);
export function parseHost(host: string | null | undefined): HostMatch {
if (!host) return { kind: "unknown" };
const hostNoPort = host.split(":")[0].toLowerCase();
if (APEX_SET.has(hostNoPort)) return { kind: "apex" };
// Strip the known apex suffix to extract the subdomain.
for (const apex of APEX_HOSTS) {
const suffix = `.${apex}`;
if (hostNoPort.endsWith(suffix)) {
const sub = hostNoPort.slice(0, -suffix.length);
if (!sub) return { kind: "apex" };
// Backstage is reserved.
if (sub === "backstage") return { kind: "backstage" };
// Slugs are [a-z0-9-]{2,40} per the tenant-registry schema check.
if (/^[a-z0-9-]{2,40}$/.test(sub)) {
return { kind: "tenant", slug: sub };
}
return { kind: "unknown" };
}
}
return { kind: "unknown" };
}