From b231eb6a27c7fbfb7c4f6a88eca21424dff530e1 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 8 May 2026 16:57:41 +0700 Subject: [PATCH] Load worker secrets from Vault --- Dockerfile | 1 - src/config.ts | 4 +++ src/hatchet/worker.ts | 2 +- src/vault/env.ts | 65 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/vault/env.ts diff --git a/Dockerfile b/Dockerfile index 3b485c9..7de5e5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,6 @@ RUN npm ci FROM node:22-alpine AS build WORKDIR /app -ENV DATABASE_URL="postgresql://mapflow:mapflow@localhost:5432/mapflow" COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run prisma:generate diff --git a/src/config.ts b/src/config.ts index 0cc2456..56e3f35 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,9 @@ import 'dotenv/config'; +import { loadVaultEnvironment } from './vault/env.js'; + +await loadVaultEnvironment(); + export const config = { databaseUrl: process.env.DATABASE_URL ?? '', workerName: process.env.HATCHET_WORKER_NAME ?? 'mapflow-hatchet-worker', diff --git a/src/hatchet/worker.ts b/src/hatchet/worker.ts index 8a74c22..0d7ffb1 100644 --- a/src/hatchet/worker.ts +++ b/src/hatchet/worker.ts @@ -1,7 +1,6 @@ import 'dotenv/config'; import { config } from '../config.js'; -import { hatchet } from './hatchet-client.js'; import { processVoiceExperienceWorkflow } from './workflows/process-voice-experience.js'; function resolveWorkerSlots(): number { @@ -14,6 +13,7 @@ async function main() { throw new Error('HATCHET_CLIENT_TOKEN is required for hatchet worker'); } + const { hatchet } = await import('./hatchet-client.js'); const worker = await hatchet.worker(config.workerName, { workflows: [processVoiceExperienceWorkflow], slots: resolveWorkerSlots(), diff --git a/src/vault/env.ts b/src/vault/env.ts new file mode 100644 index 0000000..0db6ad0 --- /dev/null +++ b/src/vault/env.ts @@ -0,0 +1,65 @@ +type VaultConfig = { + address: string; + token: string; + mount: string; + sharedPath: string; + projectPath: string; +}; + +function requireEnv(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} is required when VAULT_ENABLED=true.`); + } + return value; +} + +function vaultConfig(): VaultConfig { + return { + address: requireEnv('VAULT_ADDR').replace(/\/$/, ''), + token: requireEnv('VAULT_TOKEN'), + mount: requireEnv('VAULT_KV_MOUNT'), + sharedPath: requireEnv('VAULT_SHARED_PATH'), + projectPath: requireEnv('VAULT_PROJECT_PATH'), + }; +} + +async function readVaultPath(config: VaultConfig, path: string) { + const response = await fetch( + `${config.address}/v1/${config.mount}/data/${path}`, + { headers: { 'X-Vault-Token': config.token } }, + ); + + if (!response.ok) { + throw new Error(`Vault read failed for ${path}: ${response.status}.`); + } + + const payload = (await response.json()) as { + data?: { data?: Record }; + }; + const data = payload.data?.data; + if (!data) { + throw new Error(`Vault path ${path} has no KV v2 data.`); + } + + return data; +} + +function applyEnvironment(values: Record) { + for (const [key, value] of Object.entries(values)) { + if (typeof value !== 'string') { + throw new Error(`Vault value ${key} must be a string.`); + } + process.env[key] = value; + } +} + +export async function loadVaultEnvironment() { + if (process.env.VAULT_ENABLED !== 'true') { + return; + } + + const config = vaultConfig(); + applyEnvironment(await readVaultPath(config, config.sharedPath)); + applyEnvironment(await readVaultPath(config, config.projectPath)); +}