@crewhaus/gateway-server 0.1.4 → 0.1.6
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/dist/index.d.ts +128 -0
- package/dist/index.js +290 -0
- package/package.json +13 -10
- package/src/index.test.ts +0 -815
- package/src/index.ts +0 -408
package/src/index.ts
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
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 { type BudgetStore, InMemoryBudgetStore } from "@crewhaus/durable-state";
|
|
30
|
-
import { CrewhausError } from "@crewhaus/errors";
|
|
31
|
-
import {
|
|
32
|
-
ErrorCode,
|
|
33
|
-
GatewayProtocolError,
|
|
34
|
-
type MethodT,
|
|
35
|
-
PROTOCOL_VERSION,
|
|
36
|
-
decodeRequest,
|
|
37
|
-
encodeError,
|
|
38
|
-
encodeSuccess,
|
|
39
|
-
} from "@crewhaus/gateway-protocol";
|
|
40
|
-
import { type Tenant, buildTenant, validateTenantId, withTenant } from "@crewhaus/tenancy";
|
|
41
|
-
|
|
42
|
-
export class GatewayServerError extends CrewhausError {
|
|
43
|
-
override readonly name = "GatewayServerError";
|
|
44
|
-
constructor(message: string, cause?: unknown) {
|
|
45
|
-
super("config", message, cause);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type JwtClaims = {
|
|
50
|
-
readonly tenant_id: string;
|
|
51
|
-
readonly iat?: number;
|
|
52
|
-
readonly exp?: number;
|
|
53
|
-
readonly sub?: string;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// HS256 JWT — minimal verifier and signer (no external deps).
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/** Only HS256 is accepted — guards against `alg` confusion (e.g. `none`). */
|
|
61
|
-
const JWT_ALG = "HS256";
|
|
62
|
-
/** Only compact JWS bearer tokens are accepted. */
|
|
63
|
-
const JWT_TYP = "JWT";
|
|
64
|
-
/** Reject tokens whose lifetime (`exp - iat`) exceeds this when `iat` is present. */
|
|
65
|
-
const MAX_JWT_LIFETIME_SECONDS = 24 * 60 * 60;
|
|
66
|
-
/** Allowed clock skew when checking `iat` is not in the future. */
|
|
67
|
-
const IAT_SKEW_MS = 60_000;
|
|
68
|
-
|
|
69
|
-
type JwtHeader = {
|
|
70
|
-
readonly alg?: string;
|
|
71
|
-
readonly typ?: string;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function b64urlEncode(input: Uint8Array | string): string {
|
|
75
|
-
const buf = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
|
|
76
|
-
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function b64urlDecode(input: string): Buffer {
|
|
80
|
-
const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((input.length + 3) % 4);
|
|
81
|
-
return Buffer.from(padded, "base64");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function signJwt(claims: JwtClaims, secret: string): string {
|
|
85
|
-
// Convenience minter (tests + smoke only). Default `iat`/`exp` so emitted
|
|
86
|
-
// tokens satisfy the verifier's mandatory-`exp` + bounded-lifetime contract;
|
|
87
|
-
// production tokens come from an external IDP.
|
|
88
|
-
const iat = claims.iat ?? Math.floor(Date.now() / 1000);
|
|
89
|
-
const exp = claims.exp ?? iat + 60 * 60;
|
|
90
|
-
const header = b64urlEncode(JSON.stringify({ alg: JWT_ALG, typ: JWT_TYP }));
|
|
91
|
-
const body = b64urlEncode(JSON.stringify({ ...claims, iat, exp }));
|
|
92
|
-
const data = `${header}.${body}`;
|
|
93
|
-
const sig = createHmac("sha256", secret).update(data).digest();
|
|
94
|
-
return `${data}.${b64urlEncode(sig)}`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function verifyJwt(token: string, secret: string, now: () => number = Date.now): JwtClaims {
|
|
98
|
-
const parts = token.split(".");
|
|
99
|
-
if (parts.length !== 3) {
|
|
100
|
-
throw new GatewayServerError("malformed JWT — expected 3 segments");
|
|
101
|
-
}
|
|
102
|
-
const [headerB64, bodyB64, sigB64] = parts as [string, string, string];
|
|
103
|
-
// Validate the header (alg/typ) BEFORE spending an HMAC — rejects
|
|
104
|
-
// `alg: none` / algorithm-confusion tokens up front.
|
|
105
|
-
let header: JwtHeader;
|
|
106
|
-
try {
|
|
107
|
-
header = JSON.parse(b64urlDecode(headerB64).toString("utf8")) as JwtHeader;
|
|
108
|
-
} catch (err) {
|
|
109
|
-
throw new GatewayServerError("malformed JWT header", err);
|
|
110
|
-
}
|
|
111
|
-
if (header.alg !== JWT_ALG) {
|
|
112
|
-
throw new GatewayServerError(`JWT unsupported alg — expected ${JWT_ALG}`);
|
|
113
|
-
}
|
|
114
|
-
if (header.typ !== JWT_TYP) {
|
|
115
|
-
throw new GatewayServerError(`JWT unsupported typ — expected ${JWT_TYP}`);
|
|
116
|
-
}
|
|
117
|
-
const data = `${headerB64}.${bodyB64}`;
|
|
118
|
-
const expected = createHmac("sha256", secret).update(data).digest();
|
|
119
|
-
let actual: Buffer;
|
|
120
|
-
try {
|
|
121
|
-
actual = b64urlDecode(sigB64);
|
|
122
|
-
} catch (err) {
|
|
123
|
-
throw new GatewayServerError("malformed JWT signature segment", err);
|
|
124
|
-
}
|
|
125
|
-
if (
|
|
126
|
-
expected.length !== actual.length ||
|
|
127
|
-
!timingSafeEqual(new Uint8Array(expected), new Uint8Array(actual))
|
|
128
|
-
) {
|
|
129
|
-
throw new GatewayServerError("JWT signature mismatch");
|
|
130
|
-
}
|
|
131
|
-
let claims: JwtClaims;
|
|
132
|
-
try {
|
|
133
|
-
claims = JSON.parse(b64urlDecode(bodyB64).toString("utf8")) as JwtClaims;
|
|
134
|
-
} catch (err) {
|
|
135
|
-
throw new GatewayServerError("malformed JWT body", err);
|
|
136
|
-
}
|
|
137
|
-
if (typeof claims.tenant_id !== "string") {
|
|
138
|
-
throw new GatewayServerError("JWT missing tenant_id claim");
|
|
139
|
-
}
|
|
140
|
-
validateTenantId(claims.tenant_id);
|
|
141
|
-
const nowMs = now();
|
|
142
|
-
// `exp` is mandatory — an absent (or non-numeric) `exp` must not mean
|
|
143
|
-
// "never expires" (CWE-613).
|
|
144
|
-
if (typeof claims.exp !== "number" || !Number.isFinite(claims.exp)) {
|
|
145
|
-
throw new GatewayServerError("JWT missing exp claim");
|
|
146
|
-
}
|
|
147
|
-
if (claims.exp * 1000 <= nowMs) {
|
|
148
|
-
throw new GatewayServerError("JWT expired");
|
|
149
|
-
}
|
|
150
|
-
if (claims.iat !== undefined) {
|
|
151
|
-
if (typeof claims.iat !== "number" || !Number.isFinite(claims.iat)) {
|
|
152
|
-
throw new GatewayServerError("JWT malformed iat claim");
|
|
153
|
-
}
|
|
154
|
-
if (claims.iat * 1000 > nowMs + IAT_SKEW_MS) {
|
|
155
|
-
throw new GatewayServerError("JWT iat in the future");
|
|
156
|
-
}
|
|
157
|
-
// Bound the maximum lifetime — a token cannot outlive its `iat` by more
|
|
158
|
-
// than the configured ceiling.
|
|
159
|
-
if (claims.exp - claims.iat > MAX_JWT_LIFETIME_SECONDS) {
|
|
160
|
-
throw new GatewayServerError("JWT lifetime exceeds maximum");
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return claims;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// Server contract.
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
export type RunHandler = (args: {
|
|
171
|
-
readonly method: MethodT;
|
|
172
|
-
readonly params: unknown;
|
|
173
|
-
readonly tenant: Tenant;
|
|
174
|
-
}) => Promise<unknown>;
|
|
175
|
-
|
|
176
|
-
export type CreateGatewayServerOptions = {
|
|
177
|
-
readonly jwtSecret: string;
|
|
178
|
-
readonly tenantsRoot?: string;
|
|
179
|
-
readonly handler: RunHandler;
|
|
180
|
-
/**
|
|
181
|
-
* Optional: pre-built tenants override the default `buildTenant`
|
|
182
|
-
* (used by tests + the smoke to inject in-memory budgets without
|
|
183
|
-
* reading them from disk).
|
|
184
|
-
*/
|
|
185
|
-
readonly tenantOverrides?: Readonly<Record<string, Tenant>>;
|
|
186
|
-
readonly now?: () => number;
|
|
187
|
-
/**
|
|
188
|
-
* Optional per-request cost estimate. It is RESERVED against the tenant's
|
|
189
|
-
* budget before the handler runs and released after — closing the TOCTOU
|
|
190
|
-
* where concurrent requests all pass `checkBudget` (which only sees
|
|
191
|
-
* already-recorded usage) before any of them records its usage, each then
|
|
192
|
-
* running to full cost. A generic gateway can't know token costs, so supply
|
|
193
|
-
* a realistic estimate here to bound in-flight spend; the default reserves
|
|
194
|
-
* nothing (behavior-preserving). Actual usage is still recorded out-of-band
|
|
195
|
-
* via `recordUsage`.
|
|
196
|
-
*/
|
|
197
|
-
readonly estimateUsage?: (args: {
|
|
198
|
-
readonly method: MethodT;
|
|
199
|
-
readonly params: unknown;
|
|
200
|
-
readonly tenant: Tenant;
|
|
201
|
-
}) => UsageDelta;
|
|
202
|
-
/**
|
|
203
|
-
* Pluggable budget accounting (audit follow-up R3). Default: in-memory —
|
|
204
|
-
* per-process semantics identical to before the seam existed. Multi-process
|
|
205
|
-
* single-host deployments pass a `SqliteBudgetStore` (or a spec-built store
|
|
206
|
-
* via `createBudgetStore("sqlite:<path>")`) so every replica reserves and
|
|
207
|
-
* records against the SAME counters; multi-host deployments implement
|
|
208
|
-
* `BudgetStore` against a network store. Without this, N replicas multiply
|
|
209
|
-
* every tenant budget by N.
|
|
210
|
-
*/
|
|
211
|
-
readonly budgetStore?: BudgetStore;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
export type UsageDelta = {
|
|
215
|
-
readonly input: number;
|
|
216
|
-
readonly output: number;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
export interface GatewayServer {
|
|
220
|
-
/**
|
|
221
|
-
* Start listening on `port`. Returns the bound port (useful when
|
|
222
|
-
* caller passed 0 to ask the kernel for a free port).
|
|
223
|
-
*/
|
|
224
|
-
listen(port: number, host?: string): Promise<{ port: number; close: () => Promise<void> }>;
|
|
225
|
-
/**
|
|
226
|
-
* Single-request entrypoint — used by tests to drive the daemon
|
|
227
|
-
* without HTTP overhead. Verifies `bearer` exactly the same way the
|
|
228
|
-
* HTTP layer does.
|
|
229
|
-
*/
|
|
230
|
-
handle(request: { readonly bearer?: string; readonly body: unknown }): Promise<unknown>;
|
|
231
|
-
/**
|
|
232
|
-
* Record token usage against a tenant's running total. Async since the
|
|
233
|
-
* budget store may be durable (audit R3); await it so usage is committed
|
|
234
|
-
* before the response is considered complete.
|
|
235
|
-
*/
|
|
236
|
-
recordUsage(tenantId: string, delta: UsageDelta): Promise<void>;
|
|
237
|
-
/** Read current usage (mostly for tests). */
|
|
238
|
-
usage(tenantId: string): Promise<{ input: number; output: number }>;
|
|
239
|
-
/** Get or build the audit log for a tenant. Memoised. */
|
|
240
|
-
getAuditLog(tenant: Tenant): Promise<AuditLog>;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const ZERO_USAGE: UsageDelta = { input: 0, output: 0 };
|
|
244
|
-
|
|
245
|
-
export function createGatewayServer(opts: CreateGatewayServerOptions): GatewayServer {
|
|
246
|
-
// Budget accounting (recorded usage + in-flight reservations) lives behind
|
|
247
|
-
// the BudgetStore seam; the in-memory default preserves the pre-seam
|
|
248
|
-
// per-process semantics verbatim.
|
|
249
|
-
const budget = opts.budgetStore ?? new InMemoryBudgetStore();
|
|
250
|
-
const auditLogByTenant = new Map<string, AuditLog>();
|
|
251
|
-
const now = opts.now ?? Date.now;
|
|
252
|
-
|
|
253
|
-
function tenantFor(claims: JwtClaims): Tenant {
|
|
254
|
-
const override = opts.tenantOverrides?.[claims.tenant_id];
|
|
255
|
-
if (override !== undefined) return override;
|
|
256
|
-
return buildTenant(claims.tenant_id, {
|
|
257
|
-
...(opts.tenantsRoot !== undefined ? { tenantsRoot: opts.tenantsRoot } : {}),
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function getAuditLog(tenant: Tenant): Promise<AuditLog> {
|
|
262
|
-
const cached = auditLogByTenant.get(tenant.id);
|
|
263
|
-
if (cached !== undefined) return cached;
|
|
264
|
-
const log = await openAuditLog({ rootDir: tenant.auditRoot });
|
|
265
|
-
auditLogByTenant.set(tenant.id, log);
|
|
266
|
-
return log;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async function handleEnvelope(envelope: unknown, bearer: string | undefined): Promise<unknown> {
|
|
270
|
-
let id = "?";
|
|
271
|
-
try {
|
|
272
|
-
if (typeof bearer !== "string" || bearer === "") {
|
|
273
|
-
return encodeError("?", ErrorCode.Unauthorized, "missing bearer token");
|
|
274
|
-
}
|
|
275
|
-
const claims = verifyJwt(bearer, opts.jwtSecret, now);
|
|
276
|
-
const tenant = tenantFor(claims);
|
|
277
|
-
const decoded = decodeRequest(envelope);
|
|
278
|
-
id = decoded.id;
|
|
279
|
-
// Atomically reserve the estimated cost against recorded + in-flight
|
|
280
|
-
// usage (the store refuses when the total would exceed the budget on
|
|
281
|
-
// either dimension) — then release once the request finishes (actual
|
|
282
|
-
// usage is recorded out-of-band via recordUsage in the meantime). The
|
|
283
|
-
// check-and-reserve is a single atomic store operation so concurrent
|
|
284
|
-
// requests — including ones in OTHER processes sharing a durable
|
|
285
|
-
// store — can't all slip past the cap.
|
|
286
|
-
const estimate =
|
|
287
|
-
opts.estimateUsage?.({ method: decoded.method, params: decoded.params, tenant }) ??
|
|
288
|
-
ZERO_USAGE;
|
|
289
|
-
const reservation = await budget.tryReserve(tenant.id, estimate, tenant.budget);
|
|
290
|
-
if (!reservation.ok) {
|
|
291
|
-
throw new GatewayServerError(
|
|
292
|
-
`budget exceeded: ${reservation.reason} tokens ${reservation.total}/${reservation.limit}`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
try {
|
|
296
|
-
// Audit every authenticated gateway request.
|
|
297
|
-
const log = await getAuditLog(tenant);
|
|
298
|
-
const requestPayload: AppendInput["payload"] = {
|
|
299
|
-
method: decoded.method,
|
|
300
|
-
tenantId: tenant.id,
|
|
301
|
-
sub: claims.sub,
|
|
302
|
-
};
|
|
303
|
-
await log.append({ kind: "gateway_request", payload: requestPayload });
|
|
304
|
-
const result = await withTenant(tenant, () =>
|
|
305
|
-
opts.handler({ method: decoded.method, params: decoded.params, tenant }),
|
|
306
|
-
);
|
|
307
|
-
return encodeSuccess(id, result);
|
|
308
|
-
} finally {
|
|
309
|
-
await budget.release(tenant.id, estimate);
|
|
310
|
-
}
|
|
311
|
-
} catch (err) {
|
|
312
|
-
if (err instanceof GatewayProtocolError) {
|
|
313
|
-
return encodeError(id, ErrorCode.BadRequest, err.message);
|
|
314
|
-
}
|
|
315
|
-
if (err instanceof GatewayServerError) {
|
|
316
|
-
if (err.message.startsWith("budget exceeded")) {
|
|
317
|
-
return encodeError(id, ErrorCode.BudgetExceeded, err.message);
|
|
318
|
-
}
|
|
319
|
-
// JWT failures and tenant-id failures both map to 401.
|
|
320
|
-
if (
|
|
321
|
-
err.message.startsWith("JWT ") ||
|
|
322
|
-
err.message.startsWith("malformed JWT") ||
|
|
323
|
-
err.message.startsWith("invalid tenantId")
|
|
324
|
-
) {
|
|
325
|
-
return encodeError(id, ErrorCode.Unauthorized, err.message);
|
|
326
|
-
}
|
|
327
|
-
return encodeError(id, ErrorCode.BadRequest, err.message);
|
|
328
|
-
}
|
|
329
|
-
// Tenancy validateTenantId throws TenancyError; map to 401.
|
|
330
|
-
if (err instanceof Error && err.name === "TenancyError") {
|
|
331
|
-
return encodeError(id, ErrorCode.Unauthorized, err.message);
|
|
332
|
-
}
|
|
333
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
334
|
-
return encodeError(id, ErrorCode.InternalError, msg);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
async listen(port, host = "127.0.0.1"): Promise<{ port: number; close: () => Promise<void> }> {
|
|
340
|
-
const server = Bun.serve({
|
|
341
|
-
port,
|
|
342
|
-
hostname: host,
|
|
343
|
-
fetch: async (req): Promise<Response> => {
|
|
344
|
-
const auth = req.headers.get("authorization") ?? "";
|
|
345
|
-
const bearer = auth.startsWith("Bearer ") ? auth.slice(7) : undefined;
|
|
346
|
-
let body: unknown;
|
|
347
|
-
try {
|
|
348
|
-
body = await req.json();
|
|
349
|
-
} catch {
|
|
350
|
-
return Response.json(
|
|
351
|
-
encodeError("?", ErrorCode.BadRequest, "request body must be JSON"),
|
|
352
|
-
{ status: 400 },
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
const out = await handleEnvelope(body, bearer);
|
|
356
|
-
// Map error codes back to HTTP status for ergonomics.
|
|
357
|
-
const status =
|
|
358
|
-
out !== null && typeof out === "object" && "error" in out
|
|
359
|
-
? statusFor((out as { error: { code: string } }).error.code ?? "")
|
|
360
|
-
: 200;
|
|
361
|
-
return Response.json(out, { status });
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
return {
|
|
365
|
-
port: server.port ?? port,
|
|
366
|
-
async close(): Promise<void> {
|
|
367
|
-
server.stop();
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
},
|
|
371
|
-
handle(req): Promise<unknown> {
|
|
372
|
-
return handleEnvelope(req.body, req.bearer);
|
|
373
|
-
},
|
|
374
|
-
recordUsage(tenantId, delta): Promise<void> {
|
|
375
|
-
return budget.recordUsage(tenantId, delta);
|
|
376
|
-
},
|
|
377
|
-
usage(tenantId): Promise<{ input: number; output: number }> {
|
|
378
|
-
return budget.usage(tenantId);
|
|
379
|
-
},
|
|
380
|
-
getAuditLog,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Map a wire `ErrorCode` to its HTTP status. Exported so reference clients
|
|
386
|
-
* and embedders can render the same status the daemon's HTTP layer does.
|
|
387
|
-
* Exhaustive over {@link ErrorCode}; unknown codes fall back to `200`.
|
|
388
|
-
*/
|
|
389
|
-
export function statusFor(code: string): number {
|
|
390
|
-
switch (code) {
|
|
391
|
-
case ErrorCode.Unauthorized:
|
|
392
|
-
return 401;
|
|
393
|
-
case ErrorCode.Forbidden:
|
|
394
|
-
return 403;
|
|
395
|
-
case ErrorCode.NotFound:
|
|
396
|
-
return 404;
|
|
397
|
-
case ErrorCode.BadRequest:
|
|
398
|
-
return 400;
|
|
399
|
-
case ErrorCode.BudgetExceeded:
|
|
400
|
-
return 429;
|
|
401
|
-
case ErrorCode.InternalError:
|
|
402
|
-
return 500;
|
|
403
|
-
default:
|
|
404
|
-
return 200;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
export { PROTOCOL_VERSION };
|