import crypto from "node:crypto";
const GOOGLE_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs";
const CERT_CACHE_TTL_MS = 60 * 60 * 1e3;
const CERT_FETCH_TIMEOUT_MS = 5e3;
const GOOGLE_ACCEPTED_ISSUERS = /* @__PURE__ */ new Set(["https://accounts.google.com", "accounts.google.com"]);
class GmailPubSubJwtError extends Error {
  constructor(message, code) {
    super(message);
    this.name = "GmailPubSubJwtError";
    this.code = code;
  }
}
class FetchGmailPubSubVerifier {
  constructor() {
    this.certCache = null;
  }
  async verify(input) {
    const token = extractBearer(input.authorizationHeader);
    if (!token) throw new GmailPubSubJwtError("Missing Authorization bearer token", "missing_token");
    const parts = token.split(".");
    if (parts.length !== 3) {
      throw new GmailPubSubJwtError("JWT must have three dot-separated parts", "invalid_format");
    }
    const [headerB64, payloadB64, signatureB64] = parts;
    let header;
    let claims;
    try {
      header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf-8"));
      claims = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf-8"));
    } catch {
      throw new GmailPubSubJwtError("JWT header/payload not parseable", "invalid_format");
    }
    const kid = typeof header.kid === "string" ? header.kid : null;
    const alg = typeof header.alg === "string" ? header.alg : null;
    if (!kid || alg !== "RS256") {
      throw new GmailPubSubJwtError(`Unsupported JWT alg/kid: alg=${alg ?? "?"} kid=${kid ?? "?"}`, "invalid_format");
    }
    const certs = await this.getCerts();
    const cert = certs[kid];
    if (!cert) {
      this.certCache = null;
      const refreshed = await this.getCerts();
      const fresh = refreshed[kid];
      if (!fresh) throw new GmailPubSubJwtError(`No cert for kid=${kid}`, "invalid_signature");
      verifySignature(`${headerB64}.${payloadB64}`, signatureB64, fresh);
    } else {
      verifySignature(`${headerB64}.${payloadB64}`, signatureB64, cert);
    }
    validateClaims(claims, input);
    return claims;
  }
  async getCerts() {
    if (this.certCache && Date.now() - this.certCache.fetchedAt < CERT_CACHE_TTL_MS) {
      return this.certCache.certs;
    }
    let res;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), CERT_FETCH_TIMEOUT_MS);
    try {
      res = await fetch(GOOGLE_CERTS_URL, { signal: controller.signal });
    } catch (err) {
      throw new GmailPubSubJwtError(
        `Failed to fetch Google certs: ${err instanceof Error ? err.message : String(err)}`,
        "fetch_certs_failed"
      );
    } finally {
      clearTimeout(timer);
    }
    if (!res.ok) {
      throw new GmailPubSubJwtError(`Google certs endpoint returned ${res.status}`, "fetch_certs_failed");
    }
    let parsed;
    try {
      parsed = await res.json();
    } catch (err) {
      throw new GmailPubSubJwtError(
        `Google certs endpoint returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
        "fetch_certs_failed"
      );
    }
    const certs = toCertMap(parsed);
    if (!certs) {
      throw new GmailPubSubJwtError("Google certs endpoint returned an unexpected shape", "fetch_certs_failed");
    }
    this.certCache = { certs, fetchedAt: Date.now() };
    return certs;
  }
}
function toCertMap(value) {
  if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
  const entries = Object.entries(value);
  if (entries.length === 0) return null;
  const certs = {};
  for (const [kid, pem] of entries) {
    if (typeof pem !== "string" || pem.length === 0) return null;
    certs[kid] = pem;
  }
  return certs;
}
function extractBearer(header) {
  if (!header) return null;
  const match = /^Bearer\s+(.+)$/i.exec(header.trim());
  return match ? match[1].trim() : null;
}
function verifySignature(input, signatureB64, cert) {
  const verifier = crypto.createVerify("RSA-SHA256");
  verifier.update(input);
  verifier.end();
  const signature = Buffer.from(signatureB64, "base64url");
  const ok = verifier.verify(cert, signature);
  if (!ok) {
    throw new GmailPubSubJwtError("JWT signature verification failed", "invalid_signature");
  }
}
function validateClaims(claims, input) {
  const now = Math.floor(Date.now() / 1e3);
  if (typeof claims.exp !== "number" || claims.exp < now - 5) {
    throw new GmailPubSubJwtError("JWT expired or missing exp", "expired");
  }
  if (typeof claims.iat === "number" && claims.iat > now + 5) {
    throw new GmailPubSubJwtError("JWT issued in the future", "expired");
  }
  if (typeof claims.iss !== "string" || !GOOGLE_ACCEPTED_ISSUERS.has(claims.iss)) {
    throw new GmailPubSubJwtError(
      `JWT issuer not accepted (got ${typeof claims.iss === "string" ? claims.iss : "none"})`,
      "wrong_issuer"
    );
  }
  const audOk = Array.isArray(claims.aud) ? claims.aud.includes(input.expectedAudience) : claims.aud === input.expectedAudience;
  if (!audOk) {
    throw new GmailPubSubJwtError(
      `JWT audience mismatch (expected ${input.expectedAudience})`,
      "wrong_audience"
    );
  }
  const emailVerified = claims.emailVerified === true || claims.email_verified === true;
  if (!emailVerified || claims.email !== input.expectedEmail) {
    throw new GmailPubSubJwtError(
      `JWT email mismatch (expected ${input.expectedEmail})`,
      "wrong_email"
    );
  }
}
let cachedVerifier = null;
function getGmailPubSubVerifier() {
  if (!cachedVerifier) cachedVerifier = new FetchGmailPubSubVerifier();
  return cachedVerifier;
}
function setGmailPubSubVerifier(verifier) {
  cachedVerifier = verifier;
}
function decodeGmailPubSubBody(rawBody) {
  let envelope;
  try {
    envelope = JSON.parse(rawBody);
  } catch {
    throw new GmailPubSubJwtError("Body is not valid JSON", "invalid_format");
  }
  if (!envelope.message?.data) {
    throw new GmailPubSubJwtError("Envelope missing message.data", "invalid_format");
  }
  let payloadText;
  try {
    payloadText = Buffer.from(envelope.message.data, "base64").toString("utf-8");
  } catch {
    throw new GmailPubSubJwtError("message.data not base64 decodable", "invalid_format");
  }
  let payload;
  try {
    payload = JSON.parse(payloadText);
  } catch {
    throw new GmailPubSubJwtError("message.data JSON not parseable", "invalid_format");
  }
  if (!payload.emailAddress || payload.historyId === void 0) {
    throw new GmailPubSubJwtError("message.data missing emailAddress or historyId", "invalid_format");
  }
  return payload;
}
export {
  GmailPubSubJwtError,
  decodeGmailPubSubBody,
  getGmailPubSubVerifier,
  setGmailPubSubVerifier
};
//# sourceMappingURL=gmail-pubsub-jwt.js.map
