90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
/**
|
|
* Azure Key Vault secret resolution for Node.js services + dashboards.
|
|
*
|
|
* Call resolveKeyVaultSecrets() BEFORE Zod config parsing to populate
|
|
* process.env from Key Vault. Falls back gracefully when AKV is unavailable.
|
|
*
|
|
* Requires: AZURE_KEYVAULT_URL env var (skip if not set).
|
|
* Auth: DefaultAzureCredential (managed identity in prod, az cli locally).
|
|
*/
|
|
|
|
export interface SecretMapping {
|
|
/** Azure Key Vault secret name (e.g. 'lysnr-cosmos-key') */
|
|
kvName: string;
|
|
/** Environment variable name to populate (e.g. 'COSMOS_KEY') */
|
|
envVar: string;
|
|
}
|
|
|
|
/**
|
|
* Resolve secrets from Azure Key Vault into process.env.
|
|
*
|
|
* - Only fetches secrets whose env var is empty/unset (env takes precedence).
|
|
* - Skips entirely if AZURE_KEYVAULT_URL is not set.
|
|
* - Logs warnings but does NOT throw — services fall back to .env values.
|
|
*
|
|
* @param secrets - Array of {kvName, envVar} mappings
|
|
* @param opts - Optional: custom vault URL override
|
|
*/
|
|
export async function resolveKeyVaultSecrets(
|
|
secrets: SecretMapping[],
|
|
opts?: { vaultUrl?: string },
|
|
): Promise<void> {
|
|
const vaultUrl = opts?.vaultUrl || process.env.AZURE_KEYVAULT_URL;
|
|
if (!vaultUrl) return; // No KV configured — use env vars as-is
|
|
|
|
// Filter to only secrets that are missing from env
|
|
const missing = secrets.filter((s) => !process.env[s.envVar]);
|
|
if (missing.length === 0) return; // All secrets already in env
|
|
|
|
try {
|
|
const { DefaultAzureCredential } = await import('@azure/identity');
|
|
const { SecretClient } = await import('@azure/keyvault-secrets');
|
|
|
|
const client = new SecretClient(vaultUrl, new DefaultAzureCredential());
|
|
|
|
const results = await Promise.allSettled(
|
|
missing.map(async (s) => {
|
|
const secret = await client.getSecret(s.kvName);
|
|
if (secret.value) {
|
|
process.env[s.envVar] = secret.value;
|
|
}
|
|
}),
|
|
);
|
|
|
|
const failures = results.filter((r) => r.status === 'rejected');
|
|
if (failures.length > 0) {
|
|
console.warn(
|
|
`[keyvault] ${failures.length}/${missing.length} secrets failed to resolve — falling back to env vars`,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.warn(`[keyvault] Unable to connect to Key Vault at ${vaultUrl} — using env vars`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Standard secret mappings used across all LysnrAI services.
|
|
* Services pick the subset they need.
|
|
*/
|
|
export const LYSNR_SECRETS = {
|
|
COSMOS_KEY: { kvName: 'lysnr-cosmos-key', envVar: 'COSMOS_KEY' },
|
|
COSMOS_ENDPOINT: { kvName: 'lysnr-cosmos-endpoint', envVar: 'COSMOS_ENDPOINT' },
|
|
JWT_SECRET: { kvName: 'lysnr-jwt-secret', envVar: 'JWT_SECRET' },
|
|
STRIPE_SECRET_KEY: { kvName: 'lysnr-stripe-secret-key', envVar: 'STRIPE_SECRET_KEY' },
|
|
STRIPE_WEBHOOK_SECRET: { kvName: 'lysnr-stripe-webhook-secret', envVar: 'STRIPE_WEBHOOK_SECRET' },
|
|
BILLING_INTERNAL_KEY: { kvName: 'lysnr-billing-internal-key', envVar: 'BILLING_INTERNAL_KEY' },
|
|
AZURE_BLOB_CONNECTION_STRING: {
|
|
kvName: 'lysnr-blob-connection-string',
|
|
envVar: 'AZURE_BLOB_CONNECTION_STRING',
|
|
},
|
|
AZURE_BLOB_ACCOUNT_KEY: { kvName: 'lysnr-blob-account-key', envVar: 'AZURE_BLOB_ACCOUNT_KEY' },
|
|
GEMINI_API_KEY: { kvName: 'lysnr-gemini-api-key', envVar: 'GEMINI_API_KEY' },
|
|
SEED_SECRET: { kvName: 'lysnr-seed-secret', envVar: 'SEED_SECRET' },
|
|
AZURE_SPEECH_KEY: { kvName: 'lysnr-azure-speech-key', envVar: 'AZURE_SPEECH_KEY' },
|
|
AZURE_OPENAI_KEY: { kvName: 'lysnr-azure-openai-key', envVar: 'AZURE_OPENAI_KEY' },
|
|
AZURE_OPENAI_ENDPOINT: {
|
|
kvName: 'lysnr-azure-openai-endpoint',
|
|
envVar: 'AZURE_OPENAI_ENDPOINT',
|
|
},
|
|
} as const satisfies Record<string, SecretMapping>;
|