Source: utils/vaultUtils.js

/**
 * Vault write utilities for backend operations
 * Uses native fetch (Node 18+), reads token the same way as @commtool/vault-secrets
 */

import fs from 'fs';

function getVaultTokenAndAddr() {
    const vaultAddr = process.env.VAULT_ADDR || 'https://vault.commtool.org';

    let token = process.env.VAULT_TOKEN;
    if (!token) {
        const tokenPath = '/vault-token';
        if (fs.existsSync(tokenPath)) {
            token = fs.readFileSync(tokenPath, 'utf8').trim();
        }
    }
    if (!token) {
        throw new Error(
            'No Vault token available for write operations. ' +
            'Set VAULT_TOKEN or mount token at /vault-token.'
        );
    }
    return { token, vaultAddr };
}

/**
 * Write (or update) a secret in Vault KV v2.
 * Merges the new data on top of any existing secret (patch semantics via POST).
 *
 * @param {string} secretPath - Vault KV v2 path including the "data" segment,
 *   e.g. "orgas/data/UUID-xxxx/domains"
 * @param {Record<string, unknown>} data - Key/value data to store
 * @returns {Promise<void>}
 */
export async function saveSecretsToVault(secretPath, data) {
    const { token, vaultAddr } = getVaultTokenAndAddr();
    const prefix = secretPath.startsWith('/') ? '' : '/';
    const apiPath = `${vaultAddr}/v1${prefix}${secretPath}`;

    const response = await fetch(apiPath, {
        method: 'POST',
        headers: {
            'X-Vault-Token': token,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ data }),
    });

    if (!response.ok) {
        const errorText = await response.text().catch(() => '');
        throw new Error(`Vault write failed (${response.status}) at ${apiPath}: ${errorText}`);
    }
}