invoke.ts76 lines · main
1import { Hono } from 'hono';
2
3import { projectRateLimit } from '../middleware/rate-limit.js';
4import { requireProjectAuth, requireProjectRole } from '../middleware/project-auth.js';
5import { invoke } from '../services/invoke.js';
6import { getQuotaState } from '../services/tier-enforcement.js';
7import type { ProjectAppEnv as AppEnv } from '../types/app-env.js';
8
9export const invokeRouter = new Hono<AppEnv>();
10
11// Tier-aware burst limit. Runs BEFORE auth so an unauthenticated flood
12// can't drive the auth path; the helper returns null when the project
13// doesn't exist, which the middleware treats as a pass-through (the
14// auth check below 401s anyway).
15invokeRouter.use('/v1/projects/:id/functions/:name', projectRateLimit('invoke'));
16
17// Functions are project-scoped resources, so they share project-auth with
18// deployments and api-keys: either a session-bound owner or a matching brk_.
19invokeRouter.use('/v1/projects/:id/functions/:name', requireProjectAuth());
20
21// Suspension gating happens once at app level (apps/api/src/index.ts) via
22// blockIfProjectSuspended mounted on /v1/projects/:id/*.
23
24// Function invocations require developer minimum (writes through user code).
25invokeRouter.post(
26 '/v1/projects/:id/functions/:name',
27 requireProjectRole('developer'),
28 async (c) => {
29 const projectId = c.req.param('id');
30 const functionName = c.req.param('name');
31 const user = c.get('user');
32 const apiKeyId = c.get('apiKeyId');
33 const requestId = c.get('requestId');
34
35 const raw = await c.req.text();
36 let args: unknown = null;
37 if (raw.length > 0) {
38 try {
39 args = JSON.parse(raw);
40 } catch {
41 return c.json({ code: 'invalid_json', message: 'request body is not valid json' }, 400);
42 }
43 }
44
45 // Monthly cap enforcement. Cached (60s ttl in tier-enforcement),
46 // so this adds at most one DB roundtrip per minute per project.
47 const quota = await getQuotaState(projectId);
48 if (quota.exceeded) {
49 return c.json(
50 {
51 code: 'monthly_quota_exceeded',
52 message: `monthly invocation quota exceeded (${quota.current} / ${quota.limit} on ${quota.tier} tier)`,
53 current: quota.current,
54 limit: quota.limit,
55 tier: quota.tier,
56 },
57 429,
58 );
59 }
60
61 const result = await invoke({
62 projectId,
63 functionName,
64 args,
65 requestId,
66 auth: user
67 ? { userId: user.id, tokenType: apiKeyId ? 'api_key' : 'session' }
68 : apiKeyId
69 ? { userId: `key:${apiKeyId}`, tokenType: 'api_key' }
70 : null,
71 });
72
73 const status = result.ok ? 200 : 500;
74 return c.json(result, status);
75 },
76);