Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/leads/originCheck.ts
T

71 lines
2.5 KiB
TypeScript

/**
* CORS-allowlist matcher for the marketing-funnel capture endpoint (Phase 05 #78).
*
* The endpoint is intentionally public so external landing-pages can ingest
* leads, but the Origin header is matched against `MARKETING_ALLOWED_ORIGINS`
* (comma-separated env list). Wildcards are supported via a single leading `*`
* in the host (e.g. `https://*.toolctrl.de`).
*
* Server-to-server bots can spoof Origin — this layer is one of three (origin
* allowlist, rate-limit, optional Turnstile). It does NOT replace rate-limiting.
*/
export function parseAllowedOrigins(raw: string | undefined | null): string[] {
if (!raw) return [];
return raw
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
/**
* Normalise an origin string to `https://host` (no trailing slash, no path).
* Returns null when the input is not a valid origin URL.
*/
function normaliseOrigin(value: string | null | undefined): string | null {
if (!value) return null;
try {
const u = new URL(value);
return `${u.protocol}//${u.host}`;
} catch {
return null;
}
}
/**
* Match the request `Origin` header against an allowlist with wildcard support.
*
* - Empty allowlist + non-null origin → reject (fail-closed default).
* - Empty allowlist + null/missing origin → reject (server-to-server bot).
* - Wildcard entries match the host portion only (e.g. `https://*.example.com`
* matches `https://app.example.com` but not `https://example.com`).
*/
export function matchAllowedOrigin(origin: string | null | undefined, allowed: readonly string[]): boolean {
const normalised = normaliseOrigin(origin);
if (!normalised) return false;
if (allowed.length === 0) return false;
for (const entry of allowed) {
const entryNorm = normaliseOrigin(entry);
if (!entryNorm) {
// Allow bare host wildcard like `https://*.example.com` which is not a
// valid URL because of the literal `*`.
if (entry.includes("*")) {
if (wildcardMatch(entry, normalised)) return true;
}
continue;
}
if (entryNorm === normalised) return true;
if (entry.includes("*") && wildcardMatch(entry, normalised)) return true;
}
return false;
}
function wildcardMatch(pattern: string, candidate: string): boolean {
// Convert wildcard pattern (`https://*.example.com`) to anchored regex.
// Only `*` is special; everything else is escaped.
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[a-zA-Z0-9-]+");
const re = new RegExp(`^${escaped}$`);
return re.test(candidate);
}