bundle-store.ts76 lines · main
1import { mkdir, rm, writeFile } from 'node:fs/promises';
2import { dirname, resolve } from 'node:path';
3
4import { env } from './env.js';
5import type { Bundle } from './types.js';
6
7interface BundleResponse {
8 deploymentId: string;
9 projectId: string;
10 functionNames: string[];
11 bundle: Record<string, string>;
12}
13
14/**
15 * In-memory cache keyed by deploymentId. A new deployment for the same
16 * project gets a new id, so cache lookup is always either a hit (same
17 * deployment) or a miss (new deployment), never stale.
18 */
19const cache = new Map<string, Bundle>();
20
21/**
22 * Fetch the deployment's bundle from apps/api over the swarm overlay
23 * network, write each function file under
24 * <BRIVEN_RUNTIME_BUNDLE_DIR>/<projectId>/<deploymentId>/functions/<name>.ts
25 * and return a Bundle handle the executor can read from disk.
26 */
27export async function loadDeployment(
28 projectId: string,
29 deploymentId: string,
30): Promise<Bundle | null> {
31 const cached = cache.get(deploymentId);
32 if (cached) return cached;
33
34 const headers: Record<string, string> = { accept: 'application/json' };
35 if (env.BRIVEN_RUNTIME_SHARED_SECRET) {
36 headers['authorization'] = `Bearer ${env.BRIVEN_RUNTIME_SHARED_SECRET}`;
37 }
38
39 const url = `${env.BRIVEN_API_INTERNAL_URL}/v1/internal/deployments/${projectId}/${deploymentId}/bundle`;
40 const res = await fetch(url, { headers });
41 if (res.status === 404) return null;
42 if (!res.ok) {
43 throw new Error(`bundle fetch failed: ${res.status}`);
44 }
45
46 const payload = (await res.json()) as BundleResponse;
47 const dir = resolve(env.BRIVEN_RUNTIME_BUNDLE_DIR, projectId, deploymentId);
48
49 // Wipe any prior on-disk content for this deployment (idempotent restart).
50 await rm(dir, { recursive: true, force: true });
51 await mkdir(dir, { recursive: true });
52
53 for (const [relPath, source] of Object.entries(payload.bundle)) {
54 if (relPath.includes('..') || relPath.startsWith('/')) {
55 // Refuse path traversal — the api should never send these but the
56 // runtime is the last line of defense.
57 continue;
58 }
59 const target = resolve(dir, 'functions', relPath);
60 await mkdir(dirname(target), { recursive: true });
61 await writeFile(target, source, 'utf8');
62 }
63
64 const bundle: Bundle = {
65 projectId,
66 deploymentId,
67 functionNames: payload.functionNames.map(stripExt),
68 directory: dir,
69 };
70 cache.set(deploymentId, bundle);
71 return bundle;
72}
73
74function stripExt(name: string): string {
75 return name.endsWith('.ts') ? name.slice(0, -'.ts'.length) : name;
76}