// src/web/index.ts
async function parseJsonObjectBody(request) {
  let raw;
  try {
    raw = await request.json();
  } catch {
    return [null, Response.json({ error: "Invalid JSON body" }, { status: 400 })];
  }
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
    return [null, Response.json({ error: "Body must be a JSON object" }, { status: 400 })];
  }
  return [raw, null];
}
function requireString(body, field) {
  const v = body[field];
  if (typeof v !== "string" || v.length === 0) {
    return Response.json({ error: `Missing or non-string field: ${field}` }, { status: 400 });
  }
  return v;
}
function extractRequestContext(request) {
  const ipAddress = request.headers.get("CF-Connecting-IP") ?? request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim() ?? "0.0.0.0";
  return {
    ipAddress,
    userAgent: request.headers.get("User-Agent") ?? "",
    timestamp: (/* @__PURE__ */ new Date()).toISOString(),
    requestId: crypto.randomUUID()
  };
}
async function checkRateLimit(kv, key, limit, windowSeconds) {
  const now = Math.floor(Date.now() / 1e3);
  const windowStart = now - windowSeconds;
  const kvKey = `rl:${key}`;
  const raw = await kv.get(kvKey);
  const parsed = parseRateLimitState(raw);
  if (parsed === POISONED_STATE) {
    return { allowed: false, remaining: 0, resetAt: now + windowSeconds };
  }
  const valid = parsed.filter((t) => t > windowStart);
  if (valid.length >= limit) return { allowed: false, remaining: 0, resetAt: (valid[0] ?? now) + windowSeconds };
  valid.push(now);
  await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 });
  return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds };
}
var POISONED_STATE = /* @__PURE__ */ Symbol("rate-limit-poisoned-state");
function parseRateLimitState(raw) {
  if (raw === null) return [];
  let parsed;
  try {
    parsed = JSON.parse(raw);
  } catch {
    return POISONED_STATE;
  }
  if (!Array.isArray(parsed)) return POISONED_STATE;
  return parsed.filter((t) => typeof t === "number" && Number.isFinite(t));
}
function serializeCookie(value, opts) {
  if (opts.sameSite === "None" && !opts.secure) {
    throw new Error("SameSite=None cookies require secure: true (browsers reject them otherwise)");
  }
  const parts = [`${opts.name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? "/"}`];
  if (opts.httpOnly !== false) parts.push("HttpOnly");
  parts.push(`SameSite=${opts.sameSite ?? "Lax"}`);
  if (opts.maxAgeSeconds !== void 0) parts.push(`Max-Age=${opts.maxAgeSeconds}`);
  if (opts.secure) parts.push("Secure");
  return parts.join("; ");
}
function clearCookieHeader(opts) {
  return serializeCookie("", { ...opts, maxAgeSeconds: 0 });
}
function readCookieValue(cookieHeader, name) {
  if (!cookieHeader) return null;
  for (const part of cookieHeader.split(/;\s*/)) {
    const [cookieName, ...rest] = part.split("=");
    if (cookieName === name) {
      try {
        return decodeURIComponent(rest.join("="));
      } catch {
        return null;
      }
    }
  }
  return null;
}
function addSecurityHeaders(response, opts = {}) {
  response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("X-Frame-Options", "SAMEORIGIN");
  response.headers.set("Referrer-Policy", "same-origin");
  response.headers.set("X-XSS-Protection", "1; mode=block");
  if (opts.disclaimer) response.headers.set("X-AI-Disclaimer", opts.disclaimer);
  if (opts.retention) response.headers.set("X-Data-Retention", opts.retention);
  for (const [k, v] of Object.entries(opts.extra ?? {})) response.headers.set(k, v);
  return response;
}
var REJECTED_MEDIA_SCHEMES = ["file:", "data:", "blob:", "javascript:", "vbscript:"];
function assertMediaUrl(url, what = "media url") {
  const trimmed = url.trim();
  if (/^https?:\/\//i.test(trimmed)) return;
  if (trimmed.startsWith("/api/")) return;
  const shown = trimmed.length > 96 ? `${trimmed.slice(0, 96)}\u2026` : trimmed;
  const lower = trimmed.toLowerCase();
  if (REJECTED_MEDIA_SCHEMES.some((scheme) => lower.startsWith(scheme)) || lower.startsWith("/tmp/") || lower.startsWith("/home/")) {
    throw new Error(`${what} must reference a provider http(s) URL or a rooted /api/ path, not a local sandbox file (${shown})`);
  }
  throw new Error(`${what} must be http(s) or a rooted /api/ path (${shown})`);
}

export {
  parseJsonObjectBody,
  requireString,
  extractRequestContext,
  checkRateLimit,
  serializeCookie,
  clearCookieHeader,
  readCookieValue,
  addSecurityHeaders,
  assertMediaUrl
};
//# sourceMappingURL=chunk-HFC4BTWJ.js.map