@crewhaus/gateway-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@crewhaus/gateway-server",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Bun.serve daemon speaking gateway-protocol — JWT auth + per-tenant routing + budget enforcement",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "scripts": {
12
+ "test": "bun test src"
13
+ },
14
+ "dependencies": {
15
+ "@crewhaus/audit-log": "0.0.0",
16
+ "@crewhaus/errors": "0.0.0",
17
+ "@crewhaus/gateway-protocol": "0.0.0",
18
+ "@crewhaus/tenancy": "0.0.0"
19
+ },
20
+ "license": "Apache-2.0",
21
+ "author": {
22
+ "name": "Max Meier",
23
+ "email": "max@studiomax.io",
24
+ "url": "https://studiomax.io"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/crewhaus/factory.git",
29
+ "directory": "packages/gateway-server"
30
+ },
31
+ "homepage": "https://github.com/crewhaus/factory/tree/main/packages/gateway-server#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/crewhaus/factory/issues"
34
+ },
35
+ "publishConfig": {
36
+ "access": "restricted"
37
+ },
38
+ "files": [
39
+ "src",
40
+ "README.md",
41
+ "LICENSE",
42
+ "NOTICE"
43
+ ]
44
+ }
@@ -0,0 +1,221 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { type Tenant, buildTenant } from "@crewhaus/tenancy";
6
+ import { createGatewayServer, signJwt, verifyJwt } from "./index";
7
+
8
+ let tmp: string;
9
+
10
+ beforeEach(() => {
11
+ tmp = mkdtempSync(join(tmpdir(), "gateway-server-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ rmSync(tmp, { recursive: true, force: true });
16
+ });
17
+
18
+ const SECRET = "test-secret-do-not-use-in-prod";
19
+
20
+ function makeServer(
21
+ handler: Parameters<typeof createGatewayServer>[0]["handler"] = async () => ({ ok: true }),
22
+ ): {
23
+ server: ReturnType<typeof createGatewayServer>;
24
+ tenantA: Tenant;
25
+ tenantB: Tenant;
26
+ } {
27
+ const tenantA = buildTenant("tenant-a", { tenantsRoot: tmp });
28
+ const tenantB = buildTenant("tenant-b", { tenantsRoot: tmp });
29
+ const server = createGatewayServer({
30
+ jwtSecret: SECRET,
31
+ tenantsRoot: tmp,
32
+ handler,
33
+ tenantOverrides: { "tenant-a": tenantA, "tenant-b": tenantB },
34
+ });
35
+ return { server, tenantA, tenantB };
36
+ }
37
+
38
+ describe("JWT round-trip", () => {
39
+ test("sign + verify with the same secret", () => {
40
+ const token = signJwt({ tenant_id: "tenant-a" }, SECRET);
41
+ const claims = verifyJwt(token, SECRET);
42
+ expect(claims.tenant_id).toBe("tenant-a");
43
+ });
44
+
45
+ test("rejects wrong secret", () => {
46
+ const token = signJwt({ tenant_id: "tenant-a" }, SECRET);
47
+ expect(() => verifyJwt(token, "wrong-secret")).toThrow(/signature mismatch/);
48
+ });
49
+
50
+ test("rejects malformed token", () => {
51
+ expect(() => verifyJwt("not.a.jwt.too.many.parts", SECRET)).toThrow(/3 segments/);
52
+ });
53
+
54
+ test("rejects expired token", () => {
55
+ const exp = Math.floor((Date.now() - 60_000) / 1000);
56
+ const token = signJwt({ tenant_id: "tenant-a", exp }, SECRET);
57
+ expect(() => verifyJwt(token, SECRET)).toThrow(/expired/);
58
+ });
59
+
60
+ test("rejects invalid tenant_id (path traversal)", () => {
61
+ const token = signJwt({ tenant_id: "../etc" }, SECRET);
62
+ expect(() => verifyJwt(token, SECRET)).toThrow(/invalid tenantId/);
63
+ });
64
+ });
65
+
66
+ describe("server.handle (T2/T3 contract)", () => {
67
+ test("authenticated runs.create dispatches to handler", async () => {
68
+ let received: unknown;
69
+ const { server } = makeServer(async ({ method, params, tenant }) => {
70
+ received = { method, params, tenantId: tenant.id };
71
+ return { runId: "run_x", sessionId: "sess_x", tenantId: tenant.id };
72
+ });
73
+ const token = signJwt({ tenant_id: "tenant-a" }, SECRET);
74
+ const res = await server.handle({
75
+ bearer: token,
76
+ body: {
77
+ protocol: "crewhaus.v1",
78
+ id: "1",
79
+ method: "runs.create",
80
+ params: { spec: "s", input: "hi" },
81
+ },
82
+ });
83
+ expect(res).toEqual({
84
+ protocol: "crewhaus.v1",
85
+ id: "1",
86
+ result: { runId: "run_x", sessionId: "sess_x", tenantId: "tenant-a" },
87
+ });
88
+ expect(received).toEqual({
89
+ method: "runs.create",
90
+ params: { spec: "s", input: "hi" },
91
+ tenantId: "tenant-a",
92
+ });
93
+ });
94
+
95
+ test("missing bearer → 401 unauthorized", async () => {
96
+ const { server } = makeServer();
97
+ const res = await server.handle({
98
+ body: {
99
+ protocol: "crewhaus.v1",
100
+ id: "1",
101
+ method: "runs.create",
102
+ params: { spec: "s", input: "" },
103
+ },
104
+ });
105
+ expect(res).toMatchObject({ error: { code: "unauthorized" } });
106
+ });
107
+
108
+ test("expired JWT → 401 unauthorized", async () => {
109
+ const { server } = makeServer();
110
+ const exp = Math.floor((Date.now() - 60_000) / 1000);
111
+ const token = signJwt({ tenant_id: "tenant-a", exp }, SECRET);
112
+ const res = await server.handle({
113
+ bearer: token,
114
+ body: {
115
+ protocol: "crewhaus.v1",
116
+ id: "1",
117
+ method: "runs.create",
118
+ params: { spec: "s", input: "" },
119
+ },
120
+ });
121
+ expect(res).toMatchObject({
122
+ error: { code: "unauthorized", message: expect.stringMatching(/expired/) },
123
+ });
124
+ });
125
+
126
+ test("malformed envelope → 400 bad_request", async () => {
127
+ const { server } = makeServer();
128
+ const token = signJwt({ tenant_id: "tenant-a" }, SECRET);
129
+ const res = await server.handle({
130
+ bearer: token,
131
+ body: { protocol: "crewhaus.v0", id: "1", method: "x", params: {} },
132
+ });
133
+ expect(res).toMatchObject({ error: { code: "bad_request" } });
134
+ });
135
+ });
136
+
137
+ describe("budget enforcement", () => {
138
+ test("recordUsage increments cumulative usage", async () => {
139
+ const { server } = makeServer();
140
+ server.recordUsage("tenant-a", { input: 1000, output: 200 });
141
+ server.recordUsage("tenant-a", { input: 500, output: 100 });
142
+ expect(server.usage("tenant-a")).toEqual({ input: 1500, output: 300 });
143
+ });
144
+
145
+ test("exhausted input budget → 429 budget_exceeded", async () => {
146
+ const tenantA = buildTenant("tenant-a", { tenantsRoot: tmp });
147
+ const tinyA: Tenant = { ...tenantA, budget: { maxInputTokens: 100, maxOutputTokens: 100 } };
148
+ const server = createGatewayServer({
149
+ jwtSecret: SECRET,
150
+ tenantsRoot: tmp,
151
+ handler: async () => ({ ok: true }),
152
+ tenantOverrides: { "tenant-a": tinyA },
153
+ });
154
+ server.recordUsage("tenant-a", { input: 999, output: 0 });
155
+ const token = signJwt({ tenant_id: "tenant-a" }, SECRET);
156
+ const res = await server.handle({
157
+ bearer: token,
158
+ body: {
159
+ protocol: "crewhaus.v1",
160
+ id: "1",
161
+ method: "runs.create",
162
+ params: { spec: "s", input: "" },
163
+ },
164
+ });
165
+ expect(res).toMatchObject({
166
+ error: { code: "budget_exceeded", message: expect.stringMatching(/input tokens/) },
167
+ });
168
+ });
169
+ });
170
+
171
+ describe("tenancy isolation", () => {
172
+ test("tenant-a's tokens never resolve to tenant-b's context", async () => {
173
+ let seen: string | undefined;
174
+ const { server } = makeServer(async ({ tenant }) => {
175
+ seen = tenant.id;
176
+ return { ok: true };
177
+ });
178
+ const tokenA = signJwt({ tenant_id: "tenant-a" }, SECRET);
179
+ await server.handle({
180
+ bearer: tokenA,
181
+ body: {
182
+ protocol: "crewhaus.v1",
183
+ id: "1",
184
+ method: "runs.create",
185
+ params: { spec: "s", input: "" },
186
+ },
187
+ });
188
+ expect(seen).toBe("tenant-a");
189
+ const tokenB = signJwt({ tenant_id: "tenant-b" }, SECRET);
190
+ await server.handle({
191
+ bearer: tokenB,
192
+ body: {
193
+ protocol: "crewhaus.v1",
194
+ id: "1",
195
+ method: "runs.create",
196
+ params: { spec: "s", input: "" },
197
+ },
198
+ });
199
+ expect(seen).toBe("tenant-b");
200
+ });
201
+ });
202
+
203
+ describe("audit log", () => {
204
+ test("every authenticated request writes a gateway_request audit row", async () => {
205
+ const { server, tenantA } = makeServer();
206
+ const token = signJwt({ tenant_id: "tenant-a" }, SECRET);
207
+ await server.handle({
208
+ bearer: token,
209
+ body: {
210
+ protocol: "crewhaus.v1",
211
+ id: "1",
212
+ method: "runs.create",
213
+ params: { spec: "s", input: "" },
214
+ },
215
+ });
216
+ const log = await server.getAuditLog(tenantA);
217
+ const rows: unknown[] = [];
218
+ for await (const r of log.read()) rows.push(r);
219
+ expect(rows.length).toBe(1);
220
+ });
221
+ });
package/src/index.ts ADDED
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Catalog R16 `gateway-server` — Bun.serve daemon speaking
3
+ * `@crewhaus/gateway-protocol` over JSON-over-HTTP.
4
+ *
5
+ * Auth: HS256 JWT bearer tokens. Token claims must include
6
+ * `tenant_id`, an `iat` not in the future, and an `exp` in the future.
7
+ * The signing secret is verified against `opts.jwtSecret`. Tokens are
8
+ * NEVER minted by the daemon — they're issued by an external IDP and
9
+ * the daemon only verifies. (For the smoke test we mint with the same
10
+ * helper for convenience; production has a separate IDP.)
11
+ *
12
+ * Per-tenant scoping: every authenticated request runs inside
13
+ * `withTenant(tenant, ...)` so storage adapters rebase under the
14
+ * tenant root and cross-tenant reads throw at the storage layer
15
+ * (defense in depth — `policy-engine` would refuse before that, but
16
+ * the storage guard is the floor).
17
+ *
18
+ * Budget enforcement: each tenant has `budget.maxInputTokens` /
19
+ * `maxOutputTokens`. The daemon tracks cumulative usage in memory
20
+ * (file-backed in production) and refuses with `429
21
+ * budget_exceeded` once the limit is reached. Run handlers report
22
+ * usage via `recordUsage(tenantId, { input, output })`.
23
+ *
24
+ * Layer R16. Pairs with `gateway-protocol`, `tenancy`, `audit-log`.
25
+ */
26
+
27
+ import { createHmac, timingSafeEqual } from "node:crypto";
28
+ import { type AppendInput, type AuditLog, openAuditLog } from "@crewhaus/audit-log";
29
+ import { CrewhausError } from "@crewhaus/errors";
30
+ import {
31
+ ErrorCode,
32
+ GatewayProtocolError,
33
+ type MethodT,
34
+ PROTOCOL_VERSION,
35
+ decodeRequest,
36
+ encodeError,
37
+ encodeSuccess,
38
+ } from "@crewhaus/gateway-protocol";
39
+ import { type Tenant, buildTenant, validateTenantId, withTenant } from "@crewhaus/tenancy";
40
+
41
+ export class GatewayServerError extends CrewhausError {
42
+ override readonly name = "GatewayServerError";
43
+ constructor(message: string, cause?: unknown) {
44
+ super("config", message, cause);
45
+ }
46
+ }
47
+
48
+ export type JwtClaims = {
49
+ readonly tenant_id: string;
50
+ readonly iat?: number;
51
+ readonly exp?: number;
52
+ readonly sub?: string;
53
+ };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // HS256 JWT — minimal verifier and signer (no external deps).
57
+ // ---------------------------------------------------------------------------
58
+
59
+ function b64urlEncode(input: Uint8Array | string): string {
60
+ const buf = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
61
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
62
+ }
63
+
64
+ function b64urlDecode(input: string): Buffer {
65
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((input.length + 3) % 4);
66
+ return Buffer.from(padded, "base64");
67
+ }
68
+
69
+ export function signJwt(claims: JwtClaims, secret: string): string {
70
+ const header = b64urlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" }));
71
+ const body = b64urlEncode(JSON.stringify(claims));
72
+ const data = `${header}.${body}`;
73
+ const sig = createHmac("sha256", secret).update(data).digest();
74
+ return `${data}.${b64urlEncode(sig)}`;
75
+ }
76
+
77
+ export function verifyJwt(token: string, secret: string, now: () => number = Date.now): JwtClaims {
78
+ const parts = token.split(".");
79
+ if (parts.length !== 3) {
80
+ throw new GatewayServerError("malformed JWT — expected 3 segments");
81
+ }
82
+ const [headerB64, bodyB64, sigB64] = parts as [string, string, string];
83
+ const data = `${headerB64}.${bodyB64}`;
84
+ const expected = createHmac("sha256", secret).update(data).digest();
85
+ let actual: Buffer;
86
+ try {
87
+ actual = b64urlDecode(sigB64);
88
+ } catch (err) {
89
+ throw new GatewayServerError("malformed JWT signature segment", err);
90
+ }
91
+ if (
92
+ expected.length !== actual.length ||
93
+ !timingSafeEqual(new Uint8Array(expected), new Uint8Array(actual))
94
+ ) {
95
+ throw new GatewayServerError("JWT signature mismatch");
96
+ }
97
+ let claims: JwtClaims;
98
+ try {
99
+ claims = JSON.parse(b64urlDecode(bodyB64).toString("utf8")) as JwtClaims;
100
+ } catch (err) {
101
+ throw new GatewayServerError("malformed JWT body", err);
102
+ }
103
+ if (typeof claims.tenant_id !== "string") {
104
+ throw new GatewayServerError("JWT missing tenant_id claim");
105
+ }
106
+ validateTenantId(claims.tenant_id);
107
+ const nowMs = now();
108
+ if (claims.exp !== undefined && claims.exp * 1000 < nowMs) {
109
+ throw new GatewayServerError("JWT expired");
110
+ }
111
+ if (claims.iat !== undefined && claims.iat * 1000 > nowMs + 60_000) {
112
+ throw new GatewayServerError("JWT iat in the future");
113
+ }
114
+ return claims;
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Server contract.
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export type RunHandler = (args: {
122
+ readonly method: MethodT;
123
+ readonly params: unknown;
124
+ readonly tenant: Tenant;
125
+ }) => Promise<unknown>;
126
+
127
+ export type CreateGatewayServerOptions = {
128
+ readonly jwtSecret: string;
129
+ readonly tenantsRoot?: string;
130
+ readonly handler: RunHandler;
131
+ /**
132
+ * Optional: pre-built tenants override the default `buildTenant`
133
+ * (used by tests + the smoke to inject in-memory budgets without
134
+ * reading them from disk).
135
+ */
136
+ readonly tenantOverrides?: Readonly<Record<string, Tenant>>;
137
+ readonly now?: () => number;
138
+ };
139
+
140
+ export type UsageDelta = {
141
+ readonly input: number;
142
+ readonly output: number;
143
+ };
144
+
145
+ export interface GatewayServer {
146
+ /**
147
+ * Start listening on `port`. Returns the bound port (useful when
148
+ * caller passed 0 to ask the kernel for a free port).
149
+ */
150
+ listen(port: number, host?: string): Promise<{ port: number; close: () => Promise<void> }>;
151
+ /**
152
+ * Single-request entrypoint — used by tests to drive the daemon
153
+ * without HTTP overhead. Verifies `bearer` exactly the same way the
154
+ * HTTP layer does.
155
+ */
156
+ handle(request: { readonly bearer?: string; readonly body: unknown }): Promise<unknown>;
157
+ /** Record token usage against a tenant's running total. */
158
+ recordUsage(tenantId: string, delta: UsageDelta): void;
159
+ /** Read current usage (mostly for tests). */
160
+ usage(tenantId: string): { input: number; output: number };
161
+ /** Get or build the audit log for a tenant. Memoised. */
162
+ getAuditLog(tenant: Tenant): Promise<AuditLog>;
163
+ }
164
+
165
+ export function createGatewayServer(opts: CreateGatewayServerOptions): GatewayServer {
166
+ const usageByTenant = new Map<string, { input: number; output: number }>();
167
+ const auditLogByTenant = new Map<string, AuditLog>();
168
+ const now = opts.now ?? Date.now;
169
+
170
+ function tenantFor(claims: JwtClaims): Tenant {
171
+ const override = opts.tenantOverrides?.[claims.tenant_id];
172
+ if (override !== undefined) return override;
173
+ return buildTenant(claims.tenant_id, {
174
+ ...(opts.tenantsRoot !== undefined ? { tenantsRoot: opts.tenantsRoot } : {}),
175
+ });
176
+ }
177
+
178
+ async function getAuditLog(tenant: Tenant): Promise<AuditLog> {
179
+ const cached = auditLogByTenant.get(tenant.id);
180
+ if (cached !== undefined) return cached;
181
+ const log = await openAuditLog({ rootDir: tenant.auditRoot });
182
+ auditLogByTenant.set(tenant.id, log);
183
+ return log;
184
+ }
185
+
186
+ function bumpUsage(tenantId: string, delta: UsageDelta): void {
187
+ const cur = usageByTenant.get(tenantId) ?? { input: 0, output: 0 };
188
+ usageByTenant.set(tenantId, {
189
+ input: cur.input + delta.input,
190
+ output: cur.output + delta.output,
191
+ });
192
+ }
193
+
194
+ function checkBudget(tenant: Tenant): void {
195
+ const used = usageByTenant.get(tenant.id) ?? { input: 0, output: 0 };
196
+ if (used.input >= tenant.budget.maxInputTokens) {
197
+ throw new GatewayServerError(
198
+ `budget exceeded: input tokens ${used.input}/${tenant.budget.maxInputTokens}`,
199
+ );
200
+ }
201
+ if (used.output >= tenant.budget.maxOutputTokens) {
202
+ throw new GatewayServerError(
203
+ `budget exceeded: output tokens ${used.output}/${tenant.budget.maxOutputTokens}`,
204
+ );
205
+ }
206
+ }
207
+
208
+ async function handleEnvelope(envelope: unknown, bearer: string | undefined): Promise<unknown> {
209
+ let id = "?";
210
+ try {
211
+ if (typeof bearer !== "string" || bearer === "") {
212
+ return encodeError("?", ErrorCode.Unauthorized, "missing bearer token");
213
+ }
214
+ const claims = verifyJwt(bearer, opts.jwtSecret, now);
215
+ const tenant = tenantFor(claims);
216
+ const decoded = decodeRequest(envelope);
217
+ id = decoded.id;
218
+ checkBudget(tenant);
219
+ // Audit every authenticated gateway request.
220
+ const log = await getAuditLog(tenant);
221
+ const requestPayload: AppendInput["payload"] = {
222
+ method: decoded.method,
223
+ tenantId: tenant.id,
224
+ sub: claims.sub,
225
+ };
226
+ await log.append({ kind: "gateway_request", payload: requestPayload });
227
+ const result = await withTenant(tenant, () =>
228
+ opts.handler({ method: decoded.method, params: decoded.params, tenant }),
229
+ );
230
+ return encodeSuccess(id, result);
231
+ } catch (err) {
232
+ if (err instanceof GatewayProtocolError) {
233
+ return encodeError(id, ErrorCode.BadRequest, err.message);
234
+ }
235
+ if (err instanceof GatewayServerError) {
236
+ if (err.message.startsWith("budget exceeded")) {
237
+ return encodeError(id, ErrorCode.BudgetExceeded, err.message);
238
+ }
239
+ // JWT failures and tenant-id failures both map to 401.
240
+ if (
241
+ err.message.startsWith("JWT ") ||
242
+ err.message.startsWith("malformed JWT") ||
243
+ err.message.startsWith("invalid tenantId")
244
+ ) {
245
+ return encodeError(id, ErrorCode.Unauthorized, err.message);
246
+ }
247
+ return encodeError(id, ErrorCode.BadRequest, err.message);
248
+ }
249
+ // Tenancy validateTenantId throws TenancyError; map to 401.
250
+ if (err instanceof Error && err.name === "TenancyError") {
251
+ return encodeError(id, ErrorCode.Unauthorized, err.message);
252
+ }
253
+ const msg = err instanceof Error ? err.message : String(err);
254
+ return encodeError(id, ErrorCode.InternalError, msg);
255
+ }
256
+ }
257
+
258
+ return {
259
+ async listen(port, host = "127.0.0.1"): Promise<{ port: number; close: () => Promise<void> }> {
260
+ const server = Bun.serve({
261
+ port,
262
+ hostname: host,
263
+ fetch: async (req): Promise<Response> => {
264
+ const auth = req.headers.get("authorization") ?? "";
265
+ const bearer = auth.startsWith("Bearer ") ? auth.slice(7) : undefined;
266
+ let body: unknown;
267
+ try {
268
+ body = await req.json();
269
+ } catch {
270
+ return Response.json(
271
+ encodeError("?", ErrorCode.BadRequest, "request body must be JSON"),
272
+ { status: 400 },
273
+ );
274
+ }
275
+ const out = await handleEnvelope(body, bearer);
276
+ // Map error codes back to HTTP status for ergonomics.
277
+ const status =
278
+ out !== null && typeof out === "object" && "error" in out
279
+ ? statusFor((out as { error: { code: string } }).error.code ?? "")
280
+ : 200;
281
+ return Response.json(out, { status });
282
+ },
283
+ });
284
+ return {
285
+ port: server.port ?? port,
286
+ async close(): Promise<void> {
287
+ server.stop();
288
+ },
289
+ };
290
+ },
291
+ handle(req): Promise<unknown> {
292
+ return handleEnvelope(req.body, req.bearer);
293
+ },
294
+ recordUsage(tenantId, delta): void {
295
+ bumpUsage(tenantId, delta);
296
+ },
297
+ usage(tenantId): { input: number; output: number } {
298
+ return usageByTenant.get(tenantId) ?? { input: 0, output: 0 };
299
+ },
300
+ getAuditLog,
301
+ };
302
+ }
303
+
304
+ function statusFor(code: string): number {
305
+ switch (code) {
306
+ case ErrorCode.Unauthorized:
307
+ return 401;
308
+ case ErrorCode.Forbidden:
309
+ return 403;
310
+ case ErrorCode.NotFound:
311
+ return 404;
312
+ case ErrorCode.BadRequest:
313
+ return 400;
314
+ case ErrorCode.BudgetExceeded:
315
+ return 429;
316
+ case ErrorCode.InternalError:
317
+ return 500;
318
+ default:
319
+ return 200;
320
+ }
321
+ }
322
+
323
+ export { PROTOCOL_VERSION };