project-env-cache.ts40 lines · main
1import { env } from './env.js';
2
3/**
4 * Per-project env var cache for the Deno isolate executor. The control
5 * plane decrypts and serves env vars over `/v1/internal/projects/:id/env`
6 * (shared-secret-protected); we cache the response for 60s so a hot
7 * project doesn't pay a fetch on every cold-start.
8 *
9 * Invalidation strategy: pure TTL. When users update an env var via the
10 * dashboard, the next cold-start within 60s still uses the old value.
11 * That's the correct trade-off for Phase 1 — env edits are rare and the
12 * worst case is a one-minute delay, not a security hole.
13 */
14const cache = new Map<string, { values: Record<string, string>; expiresAt: number }>();
15const TTL_MS = 60_000;
16
17export async function fetchProjectEnv(projectId: string): Promise<Record<string, string>> {
18 const cached = cache.get(projectId);
19 if (cached && cached.expiresAt > Date.now()) return cached.values;
20
21 const headers: Record<string, string> = { accept: 'application/json' };
22 if (env.BRIVEN_RUNTIME_SHARED_SECRET) {
23 headers['authorization'] = `Bearer ${env.BRIVEN_RUNTIME_SHARED_SECRET}`;
24 }
25 const url = `${env.BRIVEN_API_INTERNAL_URL}/v1/internal/projects/${projectId}/env`;
26 const res = await fetch(url, { headers });
27 if (res.status === 404) {
28 cache.set(projectId, { values: {}, expiresAt: Date.now() + TTL_MS });
29 return {};
30 }
31 if (!res.ok) throw new Error(`fetchProjectEnv ${projectId}: ${res.status}`);
32 const values = (await res.json()) as Record<string, string>;
33 cache.set(projectId, { values, expiresAt: Date.now() + TTL_MS });
34 return values;
35}
36
37/** Test hook — clears the in-memory cache so unit tests don't see staleness. */
38export function clearProjectEnvCache(): void {
39 cache.clear();
40}