import crypto from "node:crypto";
import { generateDek, hashForLookup } from "./aes.js";
import { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from "./toggles.js";
import { parseBooleanToken } from "../boolean.js";
import { createLogger } from "../logger/index.js";
import { fetchWithTimeout, resolveTimeoutMs } from "../http/fetchWithTimeout.js";
const logger = createLogger("shared").child({ component: "kms" });
const DEFAULT_VAULT_REQUEST_TIMEOUT_MS = 1e3;
const DEFAULT_VAULT_RECOVERY_COOLDOWN_MS = 3e4;
function resolveVaultRequestTimeoutMs() {
  const raw = process.env.VAULT_REQUEST_TIMEOUT_MS;
  const parsed = raw ? Number.parseInt(raw, 10) : void 0;
  return resolveTimeoutMs(parsed, DEFAULT_VAULT_REQUEST_TIMEOUT_MS);
}
function resolveVaultRecoveryCooldownMs() {
  const raw = process.env.VAULT_RECOVERY_COOLDOWN_MS;
  const parsed = raw ? Number.parseInt(raw, 10) : void 0;
  return resolveTimeoutMs(parsed, DEFAULT_VAULT_RECOVERY_COOLDOWN_MS);
}
class FallbackKmsService {
  constructor(primary, fallback, onFallback) {
    this.primary = primary;
    this.fallback = fallback;
    this.onFallback = onFallback;
    this.notified = false;
  }
  isHealthy() {
    return this.primary.isHealthy() || Boolean(this.fallback?.isHealthy?.());
  }
  notifyFallback() {
    if (this.notified) return;
    this.notified = true;
    this.onFallback?.();
  }
  async fromPrimary(op) {
    try {
      return await op();
    } catch (err) {
      logger.warn("Primary KMS failed, will try fallback", { err });
      return null;
    }
  }
  async getTenantDek(tenantId) {
    if (this.primary.isHealthy()) {
      const dek = await this.fromPrimary(() => this.primary.getTenantDek(tenantId));
      if (dek) return dek;
    }
    if (this.fallback?.isHealthy()) {
      this.notifyFallback();
      return this.fallback.getTenantDek(tenantId);
    }
    return null;
  }
  async createTenantDek(tenantId) {
    if (this.primary.isHealthy()) {
      const dek = await this.fromPrimary(() => this.primary.createTenantDek(tenantId));
      if (dek) return dek;
    }
    if (this.fallback?.isHealthy()) {
      this.notifyFallback();
      return this.fallback.createTenantDek(tenantId);
    }
    return null;
  }
  invalidateDek(tenantId) {
    this.primary.invalidateDek?.(tenantId);
    this.fallback?.invalidateDek?.(tenantId);
  }
}
function normalizeEnv(value) {
  if (!value) return "";
  return value.trim().replace(/(?:^['"]|['"]$)/g, "");
}
function resolveDerivedKeySecret() {
  const candidates = [
    { value: process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY ?? null, envName: "TENANT_DATA_ENCRYPTION_FALLBACK_KEY" },
    { value: process.env.TENANT_DATA_ENCRYPTION_KEY ?? null, envName: "TENANT_DATA_ENCRYPTION_KEY" }
  ];
  for (const raw of candidates) {
    const normalized = normalizeEnv(raw.value ?? void 0);
    if (normalized) return { secret: normalized, source: "explicit", envName: raw.envName };
  }
  if (process.env.NODE_ENV !== "production" && parseBooleanToken(process.env.ALLOW_DERIVED_KMS_FALLBACK) === true) {
    return { secret: "om-dev-tenant-encryption", source: "dev-default", envName: "DEV_DEFAULT" };
  }
  return null;
}
class NoopKmsService {
  isHealthy() {
    return !isTenantDataEncryptionEnabled();
  }
  async getTenantDek() {
    return null;
  }
  async createTenantDek() {
    return null;
  }
}
class DerivedKmsService {
  constructor(secret) {
    this.root = crypto.createHash("sha256").update(secret).digest();
  }
  isHealthy() {
    return true;
  }
  deriveKey(tenantId) {
    const iterations = 31e4;
    const keyLength = 32;
    const derived = crypto.pbkdf2Sync(this.root, tenantId, iterations, keyLength, "sha512");
    return derived.toString("base64");
  }
  async getTenantDek(tenantId) {
    if (!tenantId) return null;
    return { tenantId, key: this.deriveKey(tenantId), fetchedAt: Date.now() };
  }
  async createTenantDek(tenantId) {
    return this.getTenantDek(tenantId);
  }
}
class HashicorpVaultKmsService {
  constructor(opts = {}) {
    this.cache = /* @__PURE__ */ new Map();
    this.healthy = true;
    // Sticky terminal failure (missing VAULT_ADDR/VAULT_TOKEN): no amount of
    // re-probing fixes a misconfiguration, so this never self-heals — only a
    // restart with corrected config does.
    this.misconfigured = false;
    // Timestamp of the last transient failure (timeout / network blip / 5xx).
    // Drives the half-open circuit breaker in isHealthy(): after the cooldown the
    // instance reports healthy again so the next call re-probes Vault.
    this.lastTransientFailureAt = null;
    this.vaultAddr = normalizeEnv(opts.vaultAddr || process.env.VAULT_ADDR || "");
    this.vaultToken = normalizeEnv(opts.vaultToken || process.env.VAULT_TOKEN || "");
    this.mountPath = (opts.mountPath || process.env.VAULT_KV_PATH || "secret/data").replace(/\/+$/, "");
    this.ttlMs = opts.ttlMs ?? 15 * 60 * 1e3;
    this.requestTimeoutMs = resolveTimeoutMs(opts.requestTimeoutMs, resolveVaultRequestTimeoutMs());
    this.recoveryCooldownMs = resolveTimeoutMs(opts.recoveryCooldownMs, resolveVaultRecoveryCooldownMs());
    this.debugEnabled = isEncryptionDebugEnabled();
    if (!this.vaultAddr || !this.vaultToken) {
      this.healthy = false;
      this.misconfigured = true;
      if (this.debugEnabled) {
        logger.warn("Vault misconfigured (missing VAULT_ADDR or VAULT_TOKEN)");
      }
    }
    if (this.healthy && !HashicorpVaultKmsService.loggedInit && this.debugEnabled) {
      HashicorpVaultKmsService.loggedInit = true;
      if (this.debugEnabled) {
        logger.info("Hashicorp Vault KMS enabled");
      }
    }
  }
  static {
    this.loggedInit = false;
  }
  isHealthy() {
    if (this.misconfigured) return false;
    if (this.healthy) return true;
    if (this.lastTransientFailureAt === null) return false;
    return this.now() - this.lastTransientFailureAt >= this.recoveryCooldownMs;
  }
  now() {
    return Date.now();
  }
  // Vault responded successfully (or is provably reachable): close the breaker.
  markHealthy() {
    this.healthy = true;
    this.lastTransientFailureAt = null;
  }
  // Transient infra failure (timeout / network blip / 5xx): open the breaker and
  // start the recovery cooldown so a later call can re-probe and self-heal.
  markTransientFailure() {
    this.healthy = false;
    this.lastTransientFailureAt = this.now();
  }
  cacheHit(tenantId) {
    const entry = this.cache.get(tenantId);
    if (!entry) return null;
    if (this.now() - entry.fetchedAt > this.ttlMs) {
      this.cache.delete(tenantId);
      return null;
    }
    return entry;
  }
  async readVault(path) {
    if (!this.vaultAddr || !this.vaultToken) {
      this.healthy = false;
      this.misconfigured = true;
      return null;
    }
    try {
      const res = await fetchWithTimeout(`${this.vaultAddr}/v1/${path}`, {
        method: "GET",
        headers: { "X-Vault-Token": this.vaultToken },
        timeoutMs: this.requestTimeoutMs
      });
      if (!res.ok) {
        if (res.status >= 500) this.markTransientFailure();
        else this.markHealthy();
        logger.warn("Vault read failed", { path, status: res.status });
        return null;
      }
      this.markHealthy();
      if (this.debugEnabled) {
        logger.info("Vault read ok", { path });
      }
      return await res.json();
    } catch (err) {
      this.markTransientFailure();
      logger.warn("Vault read error", { path, err, timeoutMs: this.requestTimeoutMs });
      return null;
    }
  }
  async writeVault(path, key, opts) {
    if (!this.vaultAddr || !this.vaultToken) {
      this.healthy = false;
      this.misconfigured = true;
      return "error";
    }
    const body = { data: { key } };
    if (typeof opts?.cas === "number") body.options = { cas: opts.cas };
    try {
      const res = await fetchWithTimeout(`${this.vaultAddr}/v1/${path}`, {
        method: "POST",
        headers: {
          "X-Vault-Token": this.vaultToken,
          "Content-Type": "application/json"
        },
        body: JSON.stringify(body),
        timeoutMs: this.requestTimeoutMs
      });
      if (res.ok) {
        this.markHealthy();
        return "ok";
      }
      if (typeof opts?.cas === "number" && res.status === 400) {
        this.markHealthy();
        logger.warn("Vault write CAS conflict (concurrent DEK create)", { path, status: res.status });
        return "conflict";
      }
      this.markTransientFailure();
      logger.warn("Vault write failed", { path, status: res.status });
      return "error";
    } catch (err) {
      this.markTransientFailure();
      logger.warn("Vault write error", { path, err, timeoutMs: this.requestTimeoutMs });
      return "error";
    }
  }
  buildKeyPath(tenantId) {
    const suffix = `tenant_key_${tenantId}`;
    const normalizedMount = this.mountPath.replace(/^\/+/, "");
    return `${normalizedMount}/${suffix}`;
  }
  remember(entry) {
    this.cache.set(entry.tenantId, entry);
    return entry;
  }
  async getTenantDek(tenantId) {
    const cached = this.cacheHit(tenantId);
    if (cached) return cached;
    const path = this.buildKeyPath(tenantId);
    const res = await this.readVault(path);
    const key = res?.data?.data?.key;
    if (!key) {
      logger.warn("No tenant DEK found in Vault", { tenantId, path });
      return null;
    }
    const dek = { tenantId, key, fetchedAt: this.now() };
    return this.remember(dek);
  }
  async createTenantDek(tenantId) {
    const path = this.buildKeyPath(tenantId);
    const existing = await this.readVault(path);
    const existingKey = existing?.data?.data?.key;
    if (existingKey) {
      return this.remember({ tenantId, key: existingKey, fetchedAt: this.now() });
    }
    if (!this.healthy) return null;
    const key = generateDek();
    const outcome = await this.writeVault(path, key, { cas: 0 });
    if (outcome === "ok") {
      logger.info("Stored tenant DEK in Vault", { tenantId, path });
      return this.remember({ tenantId, key, fetchedAt: this.now() });
    }
    if (outcome === "conflict") {
      const winner = await this.readVault(path);
      const winnerKey = winner?.data?.data?.key;
      if (winnerKey) {
        logger.info("Adopted concurrently-created tenant DEK", { tenantId, path });
        return this.remember({ tenantId, key: winnerKey, fetchedAt: this.now() });
      }
    }
    logger.warn("Failed to store tenant DEK in Vault", { tenantId, path });
    return null;
  }
  invalidateDek(tenantId) {
    this.cache.delete(tenantId);
  }
}
let loggedDerivedKeyFallbackBanner = false;
function fingerprintSecret(secret) {
  return crypto.createHash("sha256").update(secret, "utf8").digest("hex").slice(0, 16);
}
function buildDerivedKeyFallbackBannerLines(opts) {
  const sourceLine = opts.source === "explicit" ? `Source: ${opts.envName}` : "Source: dev default secret (do NOT use in production)";
  return [
    "\u{1F6A8} Using derived tenant encryption keys (Vault unavailable / no DEK)",
    sourceLine,
    `Secret fingerprint (sha256, truncated): ${fingerprintSecret(opts.secret)}`,
    "Persist this secret securely. Without it, encrypted tenant data cannot be recovered after restart."
  ];
}
function logDerivedKeyFallbackBanner(opts) {
  if (process.env.NODE_ENV === "test" || loggedDerivedKeyFallbackBanner) return;
  loggedDerivedKeyFallbackBanner = true;
  const redBg = "\x1B[41m";
  const white = "\x1B[97m";
  const reset = "\x1B[0m";
  const width = 110;
  const border = `${redBg}${white}${"\u2501".repeat(width)}${reset}`;
  const body = buildDerivedKeyFallbackBannerLines(opts);
  const bannerLines = [
    border,
    ...body.map((line) => `${redBg}${white} ${line.padEnd(width - 2, " ")} ${reset}`),
    border
  ];
  process.stderr.write(bannerLines.join("\n") + "\n");
  logger.warn("Using derived tenant encryption keys (Vault unavailable / no DEK)", {
    secretFingerprint: fingerprintSecret(opts.secret)
  });
}
function createKmsService() {
  if (!isTenantDataEncryptionEnabled()) return new NoopKmsService();
  const primary = new HashicorpVaultKmsService();
  const derived = resolveDerivedKeySecret();
  const fallback = derived ? new DerivedKmsService(derived.secret) : null;
  const notifyFallback = derived ? () => {
    logDerivedKeyFallbackBanner(derived);
  } : void 0;
  if (!primary.isHealthy()) {
    if (fallback) {
      notifyFallback?.();
      return fallback;
    }
    logger.warn("Vault not healthy or misconfigured (missing VAULT_ADDR/VAULT_TOKEN) and no fallback secret provided; falling back to noop KMS");
    return new NoopKmsService();
  }
  if (fallback) {
    return new FallbackKmsService(primary, fallback, notifyFallback);
  }
  return primary;
}
export {
  HashicorpVaultKmsService,
  NoopKmsService,
  buildDerivedKeyFallbackBannerLines,
  createKmsService,
  hashForLookup
};
//# sourceMappingURL=kms.js.map
