68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
/**
|
|
* CORS-allowlist matcher retained for the retired browser-capture drain response.
|
|
*
|
|
* `/api/leads/capture` no longer writes and always returns 410. The allowlist
|
|
* only permits known stale browser deployments to read that structured response
|
|
* cross-origin during the R-LEADS drain window. Native intake is authenticated
|
|
* server-to-server at `/v1/platform/leads` and does not use this helper.
|
|
*/
|
|
|
|
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);
|
|
}
|