import crypto from "node:crypto";
import { createLogger } from "../logger/index.js";
import { parseNumberWithDefault } from "../number.js";
const logger = createLogger("auth").child({ component: "jwt" });
function base64url(input) {
  return (typeof input === "string" ? Buffer.from(input) : input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
const DEFAULT_ISSUER = "open-mercato";
const DEFAULT_STAFF_AUDIENCE = "staff";
const AUDIENCE_SECRET_LABEL = "open-mercato:jwt:v1";
const LEGACY_GRACE_DEFAULT_MINUTES = 480;
const LEGACY_TOKEN_CLOCK_SKEW_SECONDS = 60;
function getLegacyGraceMinutes() {
  const raw = process.env.JWT_LEGACY_GRACE_MINUTES;
  const normalized = typeof raw === "string" ? raw.trim().toLowerCase() : raw;
  if (normalized === "false" || normalized === "off") return 0;
  return parseNumberWithDefault(normalized, LEGACY_GRACE_DEFAULT_MINUTES, { min: 0, integer: true });
}
function getLegacyCutoverEpochSeconds() {
  const raw = process.env.JWT_LEGACY_CUTOVER_AT;
  if (!raw || !raw.trim()) return null;
  const parsed = Date.parse(raw.trim());
  if (Number.isNaN(parsed)) {
    warnOnce(
      "jwt-legacy-cutover-unparseable",
      "JWT_LEGACY_CUTOVER_AT is not a valid ISO-8601 instant \u2014 legacy JWT fallback remains disabled."
    );
    return null;
  }
  return Math.floor(parsed / 1e3);
}
const MIN_SECRET_LENGTH = 32;
const PLACEHOLDER_SECRETS = /* @__PURE__ */ new Set([
  "jwt",
  "jwt-secret",
  "jwtsecret",
  "secret",
  "password",
  "changeme",
  "change-me",
  "change-me-dev-secret",
  "change-me-dev-auth-secret",
  "your-strong-jwt-secret",
  "your-secure-jwt-secret-change-me",
  "dev",
  "development",
  "test"
]);
const warnedKeys = /* @__PURE__ */ new Set();
function warnOnce(key, message) {
  if (warnedKeys.has(key)) return;
  warnedKeys.add(key);
  logger.warn(message);
}
function isProduction() {
  return process.env.NODE_ENV === "production";
}
function inspectSecret(secret) {
  const value = typeof secret === "string" ? secret.trim() : "";
  if (!value) return "missing";
  if (PLACEHOLDER_SECRETS.has(value.toLowerCase())) return "placeholder";
  if (value.length < MIN_SECRET_LENGTH) return "too_short";
  return null;
}
function describeViolation(name, violation) {
  switch (violation) {
    case "missing":
      return `${name} is not set. Generate one with \`openssl rand -hex 32\`.`;
    case "placeholder":
      return `${name} is set to a placeholder value published in this repository's examples, so anyone can forge tokens for this deployment. Generate a real one with \`openssl rand -hex 32\`.`;
    case "too_short":
      return `${name} is shorter than ${MIN_SECRET_LENGTH} characters. Generate a stronger one with \`openssl rand -hex 32\`.`;
  }
}
function enforceSecretPolicy(name, secret) {
  const violation = inspectSecret(secret);
  if (!violation) return;
  const message = describeViolation(name, violation);
  if (isProduction()) {
    throw new Error(`[auth.jwt] Refusing to run in production with an unsafe signing secret: ${message}`);
  }
  warnOnce(`secret-policy:${name}:${violation}`, `${message} This is tolerated outside production only.`);
}
function assertJwtSecretPolicy() {
  enforceSecretPolicy("JWT_SECRET", process.env.JWT_SECRET);
  for (const [key, value] of Object.entries(process.env)) {
    if (!/^JWT_[A-Z0-9]+(?:_[A-Z0-9]+)*_SECRET$/.test(key)) continue;
    enforceSecretPolicy(key, value);
  }
}
function readBaseSecret(explicit) {
  const secret = explicit ?? process.env.JWT_SECRET;
  if (!secret) throw new Error("JWT_SECRET is not set");
  if (explicit === void 0) enforceSecretPolicy("JWT_SECRET", secret);
  return secret;
}
function normalizeAudience(audience) {
  return audience.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_");
}
const derivedSecretCache = /* @__PURE__ */ new Map();
function deriveAudienceSecretFromBase(normalized, base) {
  const cacheKey = `${normalized}::${base}`;
  const cached = derivedSecretCache.get(cacheKey);
  if (cached !== void 0) return cached;
  const label = `${AUDIENCE_SECRET_LABEL}:${normalized}`;
  const derived = crypto.createHmac("sha256", base).update(label).digest("hex");
  derivedSecretCache.set(cacheKey, derived);
  return derived;
}
function deriveJwtAudienceSecret(audience, baseSecret) {
  const normalized = normalizeAudience(audience);
  if (!normalized) throw new Error("Audience is required to derive a JWT secret");
  const overrideName = `JWT_${normalized.toUpperCase()}_SECRET`;
  const override = process.env[overrideName];
  if (override && override.trim().length > 0) {
    enforceSecretPolicy(overrideName, override);
    return override;
  }
  const base = readBaseSecret(baseSecret);
  return deriveAudienceSecretFromBase(normalized, base);
}
function isSignOptions(value) {
  return typeof value === "object" && value !== null;
}
function isVerifyOptions(value) {
  return typeof value === "object" && value !== null;
}
function toSignOptions(secretOrOptions, expiresInSec) {
  if (isSignOptions(secretOrOptions)) {
    const audience = secretOrOptions.audience ?? DEFAULT_STAFF_AUDIENCE;
    const secret = secretOrOptions.secret ?? deriveJwtAudienceSecret(audience);
    if (!secret) throw new Error("JWT_SECRET is not set");
    return {
      secret,
      expiresInSec: secretOrOptions.expiresInSec ?? 60 * 60 * 8,
      audience,
      issuer: secretOrOptions.issuer ?? DEFAULT_ISSUER
    };
  }
  if (typeof secretOrOptions === "string") {
    if (!secretOrOptions) throw new Error("JWT_SECRET is not set");
    return {
      secret: secretOrOptions,
      expiresInSec: expiresInSec ?? 60 * 60 * 8
    };
  }
  return {
    secret: deriveJwtAudienceSecret(DEFAULT_STAFF_AUDIENCE),
    expiresInSec: expiresInSec ?? 60 * 60 * 8,
    audience: DEFAULT_STAFF_AUDIENCE,
    issuer: DEFAULT_ISSUER
  };
}
function toVerifyOptions(secretOrOptions) {
  if (isVerifyOptions(secretOrOptions)) {
    const audience = secretOrOptions.audience ?? DEFAULT_STAFF_AUDIENCE;
    const secret = secretOrOptions.secret ?? deriveJwtAudienceSecret(audience);
    if (!secret) throw new Error("JWT_SECRET is not set");
    return {
      secret,
      audience,
      issuer: secretOrOptions.issuer ?? DEFAULT_ISSUER
    };
  }
  if (typeof secretOrOptions === "string") {
    if (!secretOrOptions) throw new Error("JWT_SECRET is not set");
    return { secret: secretOrOptions };
  }
  return {
    secret: deriveJwtAudienceSecret(DEFAULT_STAFF_AUDIENCE),
    audience: DEFAULT_STAFF_AUDIENCE,
    issuer: DEFAULT_ISSUER
  };
}
function signJwt(payload, secretOrOptions, expiresInSec) {
  const options = toSignOptions(secretOrOptions, expiresInSec);
  const header = { alg: "HS256", typ: "JWT" };
  const now = Math.floor(Date.now() / 1e3);
  const body = { iat: now, exp: now + options.expiresInSec, ...payload };
  if (options.issuer && body.iss === void 0) body.iss = options.issuer;
  if (options.audience && body.aud === void 0) body.aud = options.audience;
  const encHeader = base64url(JSON.stringify(header));
  const encBody = base64url(JSON.stringify(body));
  const data = `${encHeader}.${encBody}`;
  const sig = crypto.createHmac("sha256", options.secret).update(data).digest();
  const encSig = base64url(sig);
  return `${data}.${encSig}`;
}
function verifyWithOptions(token, options) {
  const parts = token.split(".");
  if (parts.length !== 3) return null;
  const [h, p, s] = parts;
  const data = `${h}.${p}`;
  const expected = base64url(crypto.createHmac("sha256", options.secret).update(data).digest());
  const providedSignature = Buffer.from(s);
  const expectedSignature = Buffer.from(expected);
  if (providedSignature.length !== expectedSignature.length) return null;
  if (!crypto.timingSafeEqual(providedSignature, expectedSignature)) return null;
  let payload;
  try {
    payload = JSON.parse(Buffer.from(p, "base64").toString("utf8"));
  } catch {
    return null;
  }
  const now = Math.floor(Date.now() / 1e3);
  if (payload.exp && now > payload.exp) return null;
  if (options.audience !== void 0) {
    if (payload.aud !== options.audience) return null;
  }
  if (options.issuer !== void 0) {
    if (payload.iss !== options.issuer) return null;
  }
  return payload;
}
function isWithinLegacyWindow(payload, graceMinutes) {
  const cutoverAt = getLegacyCutoverEpochSeconds();
  const now = Math.floor(Date.now() / 1e3);
  if (cutoverAt === null || now >= cutoverAt) return false;
  const issuedAt = payload.iat;
  if (typeof issuedAt !== "number" || !Number.isFinite(issuedAt)) return false;
  if (issuedAt > now + LEGACY_TOKEN_CLOCK_SKEW_SECONDS) return false;
  return now - issuedAt <= graceMinutes * 60;
}
function verifyJwt(token, secretOrOptions) {
  const options = toVerifyOptions(secretOrOptions);
  const result = verifyWithOptions(token, options);
  if (result) {
    if (result._legacyToken !== void 0) delete result._legacyToken;
    return result;
  }
  if (secretOrOptions === void 0) {
    const graceMinutes = getLegacyGraceMinutes();
    const rawSecret = process.env.JWT_SECRET;
    if (graceMinutes > 0 && rawSecret) {
      const legacyResult = verifyWithOptions(token, { secret: rawSecret });
      if (legacyResult && isWithinLegacyWindow(legacyResult, graceMinutes)) {
        legacyResult._legacyToken = true;
        return legacyResult;
      }
    }
  }
  return null;
}
function signAudienceJwt(audience, payload, expiresInSec = 60 * 60 * 8) {
  return signJwt(payload, { audience, expiresInSec });
}
function verifyAudienceJwt(audience, token) {
  return verifyJwt(token, { audience });
}
export {
  assertJwtSecretPolicy,
  deriveJwtAudienceSecret,
  signAudienceJwt,
  signJwt,
  verifyAudienceJwt,
  verifyJwt
};
//# sourceMappingURL=jwt.js.map
