index.ts173 lines · main
1import { resolve } from 'node:path';
2
3import { constantTimeEqual, resolveBuildIdentity } from '@briven/shared';
4import { createLogger } from '@briven/shared/observability';
5import { Hono } from 'hono';
6import pg from 'pg';
7import { z } from 'zod';
8
9import { env } from './env.js';
10
11const log = createLogger({
12 service: 'runtime',
13 env: env.BRIVEN_ENV,
14 level: env.BRIVEN_LOG_LEVEL,
15});
16import { handleInvoke } from './invoke.js';
17import { registerPoolGauges, renderPrometheus } from './metrics.js';
18import { getPool } from './runtime-bootstrap.js';
19import type { InvokeRequest } from './types.js';
20
21const BOOT_TIME = new Date().toISOString();
22// Same .git/HEAD fallback as apps/api + apps/realtime. From
23// /app/apps/runtime, the repo root's .git is two levels up.
24const { buildSha: BUILD_SHA, buildAt: BUILD_AT } = resolveBuildIdentity(
25 resolve(process.cwd(), '../../.git'),
26);
27
28const invokeSchema = z.object({
29 projectId: z.string().min(1),
30 functionName: z.string().min(1).max(128),
31 deploymentId: z.string().min(1),
32 requestId: z.string().min(1),
33 args: z.unknown(),
34 auth: z
35 .object({
36 userId: z.string().min(1),
37 tokenType: z.enum(['session', 'api_key']),
38 })
39 .nullable()
40 .default(null),
41 env: z.record(z.string(), z.string()).optional(),
42});
43
44const app = new Hono();
45
46app.get('/health', (c) =>
47 c.json({ status: 'ok', service: 'runtime', executor: env.BRIVEN_RUNTIME_EXECUTOR }),
48);
49
50app.get('/info', (c) =>
51 c.json({
52 service: 'runtime',
53 env: env.BRIVEN_ENV,
54 buildSha: BUILD_SHA,
55 buildAt: BUILD_AT,
56 bootedAt: BOOT_TIME,
57 uptimeSec: Math.floor(process.uptime()),
58 executor: env.BRIVEN_RUNTIME_EXECUTOR,
59 }),
60);
61
62app.get('/ready', async (c) => {
63 const [apiOk, dpOk] = await Promise.all([probeApi(), probeDataPlane()]);
64 const ready = apiOk && dpOk;
65 return c.json(
66 {
67 status: ready ? 'ready' : 'not_ready',
68 checks: {
69 api: apiOk ? 'ok' : 'unreachable',
70 data_plane_dolt: env.BRIVEN_DATA_PLANE_URL
71 ? dpOk
72 ? 'ok'
73 : 'unreachable'
74 : 'not_configured',
75 },
76 },
77 ready ? 200 : 503,
78 );
79});
80
81async function probeApi(): Promise<boolean> {
82 try {
83 const res = await fetch(`${env.BRIVEN_API_INTERNAL_URL}/health`, {
84 signal: AbortSignal.timeout(2000),
85 });
86 return res.ok;
87 } catch {
88 return false;
89 }
90}
91
92async function probeDataPlane(): Promise<boolean> {
93 if (!env.BRIVEN_DATA_PLANE_URL) return false;
94 // @README-BRIVEN ADR 0001: DoltGres (Postgres-wire) readiness probe via the
95 // `pg` driver (node-postgres works against DoltGres; postgres.js does not).
96 // Opens a single short-lived pool, runs `select 1`, then ends it — never
97 // leaves a connection in a long-lived pool.
98 const pool = new pg.Pool({
99 connectionString: env.BRIVEN_DATA_PLANE_URL,
100 max: 1,
101 connectionTimeoutMillis: 2000,
102 idleTimeoutMillis: 1000,
103 });
104 try {
105 await pool.query('select 1');
106 return true;
107 } catch {
108 return false;
109 } finally {
110 await pool.end();
111 }
112}
113
114app.post('/invoke', async (c) => {
115 // Fail CLOSED always: missing secret or bad bearer → 401. Never skip auth
116 // when BRIVEN_RUNTIME_SHARED_SECRET is unset (that was an open endpoint).
117 const expected = env.BRIVEN_RUNTIME_SHARED_SECRET;
118 if (!expected) {
119 return c.json(
120 { code: 'unauthorized', message: 'runtime shared secret not configured' },
121 401,
122 );
123 }
124 {
125 const auth = c.req.header('authorization');
126 const token = auth?.startsWith('Bearer ') ? auth.slice('Bearer '.length).trim() : null;
127 if (!token || !constantTimeEqual(token, expected)) {
128 return c.json({ code: 'unauthorized', message: 'runtime is not open to the public' }, 401);
129 }
130 }
131
132 const body = await c.req.json().catch(() => null);
133 const parsed = invokeSchema.safeParse(body);
134 if (!parsed.success) {
135 return c.json(
136 { code: 'validation_failed', message: 'invalid request body', issues: parsed.error.issues },
137 400,
138 );
139 }
140
141 const result = await handleInvoke(parsed.data as InvokeRequest);
142 // Always 200 — the business-level outcome is in the body. The runtime
143 // treats user-code failures as data, not HTTP errors, so the control
144 // plane can observe durations and error codes uniformly.
145 return c.json(result);
146});
147
148// `/metrics` is intentionally unauthenticated — Prometheus scrapers run
149// inside the swarm overlay network and the host firewall keeps the port
150// off the public internet. Adding bearer-auth here would force every
151// scrape config to manage a secret.
152app.get('/metrics', async (c) => {
153 const text = await renderPrometheus();
154 return c.text(text, 200, { 'content-type': 'text/plain; version=0.0.4' });
155});
156
157// Eagerly construct the pool on boot so the gauge provider has a stable
158// reference before the first scrape arrives. `getPool()` is idempotent —
159// the singleton is shared with `handleInvoke`.
160registerPoolGauges(getPool());
161
162app.notFound((c) => c.json({ code: 'not_found', message: 'route not found' }, 404));
163
164log.info('runtime_boot', {
165 port: env.BRIVEN_RUNTIME_PORT,
166 executor: env.BRIVEN_RUNTIME_EXECUTOR,
167 bundleDir: env.BRIVEN_RUNTIME_BUNDLE_DIR,
168});
169
170export default {
171 port: env.BRIVEN_RUNTIME_PORT,
172 fetch: app.fetch,
173};