{"version":3,"sources":["../src/request-origin.ts"],"sourcesContent":["/**\n * Shared request-origin primitives.\n *\n * Server actions and integration auth routes both need to decide whether a\n * state-changing request actually came from the app's own origin. The matching\n * rules (Origin/Referer resolution, Host fallback, wildcard subdomain patterns)\n * are security-sensitive and must not drift between the two, so they live here\n * and are consumed by both rather than reimplemented.\n *\n * These helpers never throw for untrusted input: callers map the failure\n * reasons onto their own error shapes.\n */\n\nexport type RequestOriginFailureReason = \"opaque-origin\" | \"invalid-origin\";\n\nexport type RequestSourceOriginResult =\n  /** `origin` is null when the request carried no Origin or Referer header. */\n  { ok: true; origin: string | null } | { ok: false; reason: RequestOriginFailureReason };\n\n/** Resolve the origin a request claims to come from, preferring Origin over Referer. */\nexport function getRequestSourceOrigin(request: Request): RequestSourceOriginResult {\n  const origin = request.headers.get(\"origin\")?.trim();\n  if (origin) {\n    return parseSourceOrigin(origin);\n  }\n\n  const referer = request.headers.get(\"referer\")?.trim();\n  if (referer) {\n    return parseSourceOrigin(referer);\n  }\n\n  return { ok: true, origin: null };\n}\n\nfunction parseSourceOrigin(value: string): RequestSourceOriginResult {\n  if (value === \"null\") {\n    return { ok: false, reason: \"opaque-origin\" };\n  }\n\n  try {\n    const parsed = new URL(value);\n    if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n      return { ok: false, reason: \"invalid-origin\" };\n    }\n    return { ok: true, origin: parsed.origin };\n  } catch {\n    return { ok: false, reason: \"invalid-origin\" };\n  }\n}\n\n/**\n * Accept a source origin whose host matches the `Host` header *and* whose\n * scheme matches the rebuilt `request.url` scheme. The scheme check blocks\n * browser-driven protocol-downgrade CSRF and also rejects proxy-rebuilt\n * `request.url` values whose scheme differs from the browser origin. For\n * TLS-terminating proxies that leave `request.url` as `http:`, enable\n * `trustProxy` with a proxy-emitted `X-Forwarded-Proto: https`, or add the\n * browser origin to `serverActions.allowedOrigins`.\n */\nexport function matchesHostHeader(sourceOrigin: string, request: Request): boolean {\n  const host = request.headers.get(\"host\")?.trim().toLowerCase();\n  if (!host) return false;\n\n  try {\n    const source = new URL(sourceOrigin);\n    const target = new URL(request.url);\n    return source.protocol === target.protocol && source.host.toLowerCase() === host;\n  } catch {\n    return false;\n  }\n}\n\nexport function matchesAllowedOrigin(sourceOrigin: string, pattern: string): boolean {\n  const source = new URL(sourceOrigin);\n  if (!pattern.includes(\"*\")) {\n    return pattern.includes(\"://\") ? source.origin === pattern : source.host === pattern;\n  }\n\n  const schemeEnd = pattern.indexOf(\"://\");\n  const scheme = schemeEnd === -1 ? null : pattern.slice(0, schemeEnd + 1);\n  const hostPattern = pattern.slice(schemeEnd === -1 ? 0 : schemeEnd + 3);\n  const [wildcardHost, port] = splitHostAndPort(hostPattern);\n  const baseHost = wildcardHost.slice(2);\n\n  if (scheme && source.protocol !== scheme) return false;\n  if (port && getEffectivePort(source) !== port) return false;\n  if (!port && source.port) return false;\n\n  return source.hostname.endsWith(`.${baseHost}`) && source.hostname !== baseHost;\n}\n\n/**\n * Validate and canonicalize a configured origin pattern. `label` names the\n * configuration field so the thrown message points at the user's own setting.\n */\nexport function normalizeAllowedOriginPattern(value: string, label: string): string {\n  const pattern = value.trim().toLowerCase();\n  if (!pattern) {\n    throw new TypeError(`${label} cannot contain empty values`);\n  }\n\n  if (pattern.includes(\"*\")) {\n    if (!/^(?:https?:\\/\\/)?\\*\\.[a-z0-9.-]+(?::\\d+)?$/.test(pattern)) {\n      throw new TypeError(`Invalid ${label} pattern: ${JSON.stringify(value)}`);\n    }\n    return pattern;\n  }\n\n  if (pattern.includes(\"://\")) {\n    let parsed: URL;\n    try {\n      parsed = new URL(pattern);\n    } catch {\n      throw new TypeError(`Invalid ${label} value: ${JSON.stringify(value)}`);\n    }\n\n    if (\n      (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") ||\n      parsed.username ||\n      parsed.password ||\n      parsed.pathname !== \"/\" ||\n      parsed.search ||\n      parsed.hash\n    ) {\n      throw new TypeError(`${label} must contain origins without paths: ${JSON.stringify(value)}`);\n    }\n    return parsed.origin;\n  }\n\n  if (/[/@?#]/.test(pattern)) {\n    throw new TypeError(`${label} must contain origins or hosts: ${JSON.stringify(value)}`);\n  }\n\n  try {\n    return new URL(`http://${pattern}`).host;\n  } catch {\n    throw new TypeError(`Invalid ${label} value: ${JSON.stringify(value)}`);\n  }\n}\n\nfunction splitHostAndPort(value: string): [string, string | null] {\n  const separator = value.lastIndexOf(\":\");\n  if (separator === -1) return [value, null];\n  return [value.slice(0, separator), value.slice(separator + 1)];\n}\n\nfunction getEffectivePort(url: URL): string {\n  if (url.port) return url.port;\n  if (url.protocol === \"https:\") return \"443\";\n  if (url.protocol === \"http:\") return \"80\";\n  return \"\";\n}\n"],"mappings":"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","names":[]}