import { readFileSync } from "fs"; const OPENBAO_ADDR = process.env.OPENBAO_ADDR || "http://openbao.openbao.svc:8200"; const SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"; const K8S_AUTH_ROLE = process.env.OPENBAO_K8S_ROLE || "pieced-portal"; const K8S_AUTH_MOUNT = process.env.OPENBAO_K8S_MOUNT || "kubernetes"; let cachedToken: { token: string; expiresAt: number } | null = null; async function authenticate(): Promise { if (cachedToken && Date.now() < cachedToken.expiresAt - 30_000) { return cachedToken.token; } const jwt = readFileSync(SA_TOKEN_PATH, "utf-8").trim(); const res = await fetch( `${OPENBAO_ADDR}/v1/auth/${K8S_AUTH_MOUNT}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role: K8S_AUTH_ROLE, jwt }), } ); if (!res.ok) { const body = await res.text(); throw new Error(`OpenBao K8s auth failed: ${res.status} ${body}`); } const data = await res.json(); const token = data.auth.client_token as string; const leaseDuration = (data.auth.lease_duration as number) || 3600; cachedToken = { token, expiresAt: Date.now() + leaseDuration * 1000, }; return token; } /** * Read a KV v2 secret. Path relative to KV mount. * Returns .data.data object or null if 404. */ export async function readSecret( path: string ): Promise | null> { const token = await authenticate(); const res = await fetch(`${OPENBAO_ADDR}/v1/secret/data/${path}`, { headers: { "X-Vault-Token": token }, }); if (res.status === 404) return null; if (!res.ok) { const body = await res.text(); throw new Error(`OpenBao read failed: ${res.status} ${body}`); } const json = await res.json(); return json.data?.data ?? null; } export async function writePackageSecrets( tenantId: string, packageId: string, secrets: Record ): Promise { const token = await authenticate(); const path = `secret/data/tenants/${tenantId}/${packageId}`; const res = await fetch(`${OPENBAO_ADDR}/v1/${path}`, { method: "POST", headers: { "Content-Type": "application/json", "X-Vault-Token": token, }, body: JSON.stringify({ data: secrets }), }); if (!res.ok) { const body = await res.text(); throw new Error(`OpenBao write failed: ${res.status} ${body}`); } } export async function deletePackageSecrets( tenantId: string, packageId: string ): Promise { const token = await authenticate(); const path = `secret/metadata/tenants/${tenantId}/${packageId}`; const res = await fetch(`${OPENBAO_ADDR}/v1/${path}`, { method: "DELETE", headers: { "X-Vault-Token": token }, }); if (!res.ok && res.status !== 404) { const body = await res.text(); throw new Error(`OpenBao delete failed: ${res.status} ${body}`); } }