import { t as sql } from "./esm-yFaYC5wd.mjs";
import { readFileSync } from "fs";
//#region subgraphs/vetra-cloud-secrets/openbao-transit.ts
const SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
const DEFAULT_KEY_NAME_PREFIX = "vetra-tenant-";
const TOKEN_EXPIRY_BUFFER_MS = 300 * 1e3;
var OpenBaoTransitClient = class {
	addr;
	role;
	keyNamePrefix;
	readToken;
	tokenCache = null;
	constructor(opts) {
		this.addr = opts.addr.replace(/\/$/, "");
		this.role = opts.role;
		this.keyNamePrefix = opts.keyNamePrefix ?? DEFAULT_KEY_NAME_PREFIX;
		this.readToken = opts.tokenReader ?? readFileSync;
	}
	keyFor(tenantId) {
		return `${this.keyNamePrefix}${tenantId}`;
	}
	async ensureTenantKey(tenantId) {
		const key = this.keyFor(tenantId);
		const token = await this.getToken();
		const res = await fetch(`${this.addr}/v1/transit/keys/${encodeURIComponent(key)}`, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"X-Vault-Token": token
			},
			body: JSON.stringify({
				type: "aes256-gcm96",
				deletion_allowed: false
			})
		});
		if (res.status === 204) return;
		const text = await res.text();
		if (res.status === 400 && /existing key/i.test(text)) return;
		if (!res.ok) throw new Error(`OpenBao ensureTenantKey(${tenantId}) failed (${res.status}): ${text}`);
	}
	async encrypt(tenantId, plaintext) {
		const key = this.keyFor(tenantId);
		const token = await this.getToken();
		const res = await fetch(`${this.addr}/v1/transit/encrypt/${encodeURIComponent(key)}`, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"X-Vault-Token": token
			},
			body: JSON.stringify({ plaintext: Buffer.from(plaintext, "utf8").toString("base64") })
		});
		if (!res.ok) {
			const text = await res.text();
			throw new Error(`OpenBao transit encrypt(${tenantId}) failed (${res.status}): ${text}`);
		}
		return (await res.json()).data.ciphertext;
	}
	async decrypt(tenantId, ciphertext) {
		const key = this.keyFor(tenantId);
		const token = await this.getToken();
		const res = await fetch(`${this.addr}/v1/transit/decrypt/${encodeURIComponent(key)}`, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"X-Vault-Token": token
			},
			body: JSON.stringify({ ciphertext })
		});
		if (!res.ok) {
			const text = await res.text();
			throw new Error(`OpenBao transit decrypt(${tenantId}) failed (${res.status}): ${text}`);
		}
		const json = await res.json();
		return Buffer.from(json.data.plaintext, "base64").toString("utf8");
	}
	async authenticate() {
		const saToken = this.readToken(SA_TOKEN_PATH, "utf8").trim();
		const res = await fetch(`${this.addr}/v1/auth/kubernetes/login`, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({
				jwt: saToken,
				role: this.role
			})
		});
		if (!res.ok) {
			const text = await res.text();
			throw new Error(`OpenBao authentication failed (${res.status}): ${text}`);
		}
		const json = await res.json();
		const ttlMs = Math.max(json.auth.lease_duration, 60) * 1e3;
		this.tokenCache = {
			token: json.auth.client_token,
			expiresAt: Date.now() + ttlMs - TOKEN_EXPIRY_BUFFER_MS
		};
		return json.auth.client_token;
	}
	async getToken() {
		if (this.tokenCache && this.tokenCache.expiresAt > Date.now()) return this.tokenCache.token;
		return this.authenticate();
	}
};
//#endregion
//#region subgraphs/vetra-cloud-secrets/services/secrets-service.ts
/**
* Shared service for reading + writing tenant env vars and secrets. Used by:
*   - The GraphQL resolvers (`vetra-cloud-secrets` subgraph) — the public
*     surface that the vetra.to Configuration UI talks to.
*   - The `vetra-cloud-environment` gitops processor — when emitting tenant
*     values.yaml, it routes env entries through here so secrets land in
*     the encrypted `tenant_secrets` table (and never in plaintext in the
*     synced values.yaml).
*
* All writes pg_notify('vetra_secrets_changed', tenantId) inside the
* same transaction so the standalone `vetra-secrets-controller` pod
* picks up the change and materializes the tenant ConfigMap/Secret.
*/
const KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
const NOTIFY_CHANNEL = "vetra_secrets_changed";
var InvalidSecretsKeyError = class extends Error {
	constructor(key) {
		super(`Invalid key "${key}": key must match ^[A-Z][A-Z0-9_]*$ (e.g. MY_VAR, API_KEY)`);
		this.name = "InvalidSecretsKeyError";
	}
};
/**
* Keys reserved for another write path. PH_CONNECT_CONFIG_JSON is owned by the
* vetra-cloud-environment document model (SET_RUNTIME_CONFIG operation) and is
* rendered onto the connect pod's env via the gitops processor — it must not be
* set as a raw env var here (via the resolvers or the processor's env routing).
*/
const RESERVED_KEYS = new Set(["PH_CONNECT_CONFIG_JSON"]);
function validateKey(key) {
	if (!KEY_PATTERN.test(key)) throw new InvalidSecretsKeyError(key);
	if (RESERVED_KEYS.has(key)) throw new Error(`Reserved key "${key}" cannot be set via setEnvVar; set it through the vetra-cloud-environment document's SET_RUNTIME_CONFIG operation instead`);
}
function createSecretsService({ db, transit }) {
	return {
		async listEnvVars(tenantId) {
			return db.selectFrom("tenant_env_vars").select(["key", "value"]).where("tenantId", "=", tenantId).orderBy("key", "asc").execute();
		},
		async listSecretKeys(tenantId) {
			return db.selectFrom("tenant_secrets").select("key").where("tenantId", "=", tenantId).orderBy("key", "asc").execute();
		},
		async setEnvVar(tenantId, key, value) {
			validateKey(key);
			const now = (/* @__PURE__ */ new Date()).toISOString();
			await db.transaction().execute(async (trx) => {
				await trx.insertInto("tenant_env_vars").values({
					tenantId,
					key,
					value,
					updatedAt: now
				}).onConflict((oc) => oc.columns(["tenantId", "key"]).doUpdateSet({
					value,
					updatedAt: now
				})).execute();
				await sql`SELECT pg_notify(${NOTIFY_CHANNEL}, ${tenantId})`.execute(trx);
			});
			return {
				key,
				value
			};
		},
		async deleteEnvVar(tenantId, key) {
			return await db.transaction().execute(async (trx) => {
				const result = await trx.deleteFrom("tenant_env_vars").where("tenantId", "=", tenantId).where("key", "=", key).executeTakeFirst();
				const deleted = Number(result.numDeletedRows) > 0;
				if (deleted) await sql`SELECT pg_notify(${NOTIFY_CHANNEL}, ${tenantId})`.execute(trx);
				return deleted;
			});
		},
		async setSecret(tenantId, key, value) {
			validateKey(key);
			await transit.ensureTenantKey(tenantId);
			const ciphertext = await transit.encrypt(tenantId, value);
			const now = (/* @__PURE__ */ new Date()).toISOString();
			await db.transaction().execute(async (trx) => {
				await trx.insertInto("tenant_secrets").values({
					tenantId,
					key,
					updatedAt: now,
					ciphertext
				}).onConflict((oc) => oc.columns(["tenantId", "key"]).doUpdateSet({
					updatedAt: now,
					ciphertext
				})).execute();
				await sql`SELECT pg_notify(${NOTIFY_CHANNEL}, ${tenantId})`.execute(trx);
			});
			return { key };
		},
		async setSecrets(tenantId, entries) {
			if (entries.length === 0) return;
			for (const e of entries) validateKey(e.key);
			await transit.ensureTenantKey(tenantId);
			const now = (/* @__PURE__ */ new Date()).toISOString();
			const rows = await Promise.all(entries.map(async (e) => ({
				tenantId,
				key: e.key,
				updatedAt: now,
				ciphertext: await transit.encrypt(tenantId, e.value)
			})));
			await db.transaction().execute(async (trx) => {
				await trx.insertInto("tenant_secrets").values(rows).onConflict((oc) => oc.columns(["tenantId", "key"]).doUpdateSet((eb) => ({
					updatedAt: eb.ref("excluded.updatedAt"),
					ciphertext: eb.ref("excluded.ciphertext")
				}))).execute();
				await sql`SELECT pg_notify(${NOTIFY_CHANNEL}, ${tenantId})`.execute(trx);
			});
		},
		async deleteSecret(tenantId, key) {
			return await db.transaction().execute(async (trx) => {
				const result = await trx.deleteFrom("tenant_secrets").where("tenantId", "=", tenantId).where("key", "=", key).executeTakeFirst();
				const deleted = Number(result.numDeletedRows) > 0;
				if (deleted) await sql`SELECT pg_notify(${NOTIFY_CHANNEL}, ${tenantId})`.execute(trx);
				return deleted;
			});
		}
	};
}
//#endregion
export { OpenBaoTransitClient as n, createSecretsService as t };

//# sourceMappingURL=secrets-service-TFqI9ubU.mjs.map