import { createLogger } from "./logger/index.js";
const logger = createLogger("shared").child({ component: "origin-check" });
const DEFAULT_DEV_APP_URL = "http://localhost:3000";
class AppOriginConfigurationError extends Error {
  constructor(message) {
    super(message);
    this.name = "AppOriginConfigurationError";
  }
}
class AppOriginRejectedError extends Error {
  constructor(message) {
    super(message);
    this.name = "AppOriginRejectedError";
  }
}
function parseBaseUrl(raw) {
  const value = raw?.trim();
  if (!value) return null;
  try {
    const url = new URL(value);
    url.hash = "";
    url.search = "";
    return url;
  } catch {
    return null;
  }
}
function normalizeBaseUrl(raw) {
  const url = parseBaseUrl(raw);
  return url ? url.toString().replace(/\/$/, "") : null;
}
function normalizeOrigin(raw) {
  const url = parseBaseUrl(raw);
  return url ? url.origin : null;
}
function readCsv(value) {
  return (value ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0);
}
function readAllowedOrigins(env) {
  const origins = /* @__PURE__ */ new Set();
  for (const raw of [env.APP_URL, env.NEXT_PUBLIC_APP_URL, ...readCsv(env.APP_ALLOWED_ORIGINS)]) {
    const origin = normalizeOrigin(raw);
    if (origin) origins.add(origin);
  }
  return origins;
}
function readFirstHeaderValue(value) {
  const first = value?.split(",")[0]?.trim();
  return first && first.length > 0 ? first : null;
}
function originFromHost(protocol, rawHost) {
  const host = readFirstHeaderValue(rawHost);
  if (!host) return null;
  return normalizeOrigin(`${protocol}//${host}`);
}
function readRequestOriginCandidates(input) {
  const candidates = {
    urlOrigin: null,
    headerOrigins: /* @__PURE__ */ new Set()
  };
  if (!input) return candidates;
  const requestUrl = typeof input === "string" ? input : input.url;
  let parsedUrl;
  try {
    parsedUrl = new URL(requestUrl);
  } catch {
    return candidates;
  }
  candidates.urlOrigin = normalizeOrigin(parsedUrl.origin);
  if (typeof input === "string") return candidates;
  const forwardedProto = readFirstHeaderValue(input.headers.get("x-forwarded-proto")) ?? parsedUrl.protocol.replace(/:$/, "");
  const protocol = forwardedProto.endsWith(":") ? forwardedProto : `${forwardedProto}:`;
  const hostOrigin = originFromHost(protocol, input.headers.get("host"));
  const forwardedHostOrigin = originFromHost(protocol, input.headers.get("x-forwarded-host"));
  if (hostOrigin) candidates.headerOrigins.add(hostOrigin);
  if (forwardedHostOrigin) candidates.headerOrigins.add(forwardedHostOrigin);
  return candidates;
}
function requestOrigins(input) {
  const origins = /* @__PURE__ */ new Set();
  const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input);
  if (urlOrigin) origins.add(urlOrigin);
  for (const origin of headerOrigins) origins.add(origin);
  return origins;
}
function logOriginDebugContext(input, allowedOrigins, rejectedOrigin, level = "error", nodeEnv) {
  if (level === "warn" && nodeEnv === "test") return;
  const log = (msg, fields) => level === "warn" ? logger.warn(msg, fields) : logger.error(msg, fields);
  if (typeof input === "string") {
    log("Origin check rejected string input", {
      requestUrl: input,
      rejectedOrigin: rejectedOrigin ?? null,
      allowedOrigins: Array.from(allowedOrigins)
    });
    return;
  }
  if (!input) {
    log("Origin check rejected empty input", {
      rejectedOrigin: rejectedOrigin ?? null,
      allowedOrigins: Array.from(allowedOrigins)
    });
    return;
  }
  log("Origin check rejected request", {
    requestUrl: input.url,
    host: input.headers.get("host"),
    forwardedHost: input.headers.get("x-forwarded-host"),
    forwardedProto: input.headers.get("x-forwarded-proto"),
    derivedOrigins: Array.from(requestOrigins(input)),
    rejectedOrigin: rejectedOrigin ?? null,
    allowedOrigins: Array.from(allowedOrigins)
  });
}
function isLoopbackHostname(hostname) {
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
}
function isLoopbackOrigin(origin) {
  try {
    return isLoopbackHostname(new URL(origin).hostname);
  } catch {
    return false;
  }
}
function normalizeOriginPort(url) {
  if (url.port) return url.port;
  if (url.protocol === "https:") return "443";
  if (url.protocol === "http:") return "80";
  return "";
}
function isEquivalentLoopbackOrigin(origin, allowedOrigin) {
  try {
    const candidateUrl = new URL(origin);
    const allowedUrl = new URL(allowedOrigin);
    if (!isLoopbackHostname(candidateUrl.hostname) || !isLoopbackHostname(allowedUrl.hostname)) {
      return false;
    }
    return normalizeOriginPort(candidateUrl) === normalizeOriginPort(allowedUrl);
  } catch {
    return false;
  }
}
function shouldAllowLoopbackOrigin(origin, allowedOrigins, env) {
  if (!isLoopbackOrigin(origin)) return false;
  for (const allowedOrigin of allowedOrigins) {
    if (!isLoopbackOrigin(allowedOrigin)) continue;
    if (env.NODE_ENV !== "production") return true;
    if (isEquivalentLoopbackOrigin(origin, allowedOrigin)) return true;
  }
  return false;
}
function assertAllowedAppOrigin(input, env = process.env) {
  const allowedOrigins = readAllowedOrigins(env);
  if (allowedOrigins.size === 0) {
    if (env.NODE_ENV === "production") {
      logOriginDebugContext(input, allowedOrigins);
      throw new AppOriginConfigurationError("APP_URL must be configured in production");
    }
    return;
  }
  const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input);
  const hasAllowedHeaderOrigin = Array.from(headerOrigins).some((origin) => allowedOrigins.has(origin));
  for (const origin of headerOrigins) {
    if (allowedOrigins.has(origin)) continue;
    if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) continue;
    logOriginDebugContext(input, allowedOrigins, origin, "warn", env.NODE_ENV);
    throw new AppOriginRejectedError("Request origin is not allowed");
  }
  if (!urlOrigin) return;
  if (allowedOrigins.has(urlOrigin)) return;
  if (shouldAllowLoopbackOrigin(urlOrigin, allowedOrigins, env)) return;
  if (isLoopbackOrigin(urlOrigin) && hasAllowedHeaderOrigin) return;
  logOriginDebugContext(input, allowedOrigins, urlOrigin, "warn", env.NODE_ENV);
  throw new AppOriginRejectedError("Request origin is not allowed");
}
function resolveRequestOrigin(req) {
  const url = new URL(req.url);
  const proto = req.headers.get("x-forwarded-proto") || url.protocol.replace(":", "");
  const host = req.headers.get("x-forwarded-host") || req.headers.get("host") || url.host;
  return `${proto}://${host}`;
}
function getAppBaseUrl(req) {
  return process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL || resolveRequestOrigin(req);
}
function toAbsoluteUrl(req, path) {
  return new URL(path, getAppBaseUrl(req)).toString();
}
function getSecurityEmailBaseUrl(input, env = process.env) {
  const configuredAppUrl = normalizeBaseUrl(env.APP_URL);
  if (!configuredAppUrl) {
    if (env.NODE_ENV === "production") {
      throw new AppOriginConfigurationError("APP_URL must be configured in production");
    }
    return DEFAULT_DEV_APP_URL;
  }
  assertAllowedAppOrigin(input, env);
  return configuredAppUrl;
}
function toSecurityEmailUrl(input, path, env = process.env) {
  const base = getSecurityEmailBaseUrl(input, env);
  return `${base}/${path.replace(/^\/+/, "")}`;
}
function mapSecurityEmailUrlError(error, mapping) {
  if (error instanceof AppOriginRejectedError) {
    return { status: 400, body: { error: "Invalid request origin" } };
  }
  if (error instanceof AppOriginConfigurationError) {
    logger.error("APP_URL is required in production", { scope: mapping.scope });
    return { status: 500, body: { error: mapping.configMessage } };
  }
  return null;
}
export {
  AppOriginConfigurationError,
  AppOriginRejectedError,
  assertAllowedAppOrigin,
  getAppBaseUrl,
  getSecurityEmailBaseUrl,
  mapSecurityEmailUrlError,
  resolveRequestOrigin,
  toAbsoluteUrl,
  toSecurityEmailUrl
};
//# sourceMappingURL=url.js.map
