@wacry/codeks-bridge 0.0.0-stage → 0.0.3

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.
Files changed (104) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +40 -2
  4. package/catalog/README.md +63 -0
  5. package/catalog/claude-opus-5-5.json +194 -0
  6. package/catalog/claude-sonnet-5-5.json +194 -0
  7. package/catalog/deepseek-flash.json +203 -0
  8. package/catalog/gpt-5.6-sol.json +152 -0
  9. package/catalog/gpt-6-astra.json +184 -0
  10. package/catalog/gpt-6-luna.json +178 -0
  11. package/catalog/gpt-6-sol.json +182 -0
  12. package/catalog/gpt-6.1-sol.json +187 -0
  13. package/dist/adapter-options.d.ts +5 -0
  14. package/dist/adapter-options.js +41 -0
  15. package/dist/auth.d.ts +2 -0
  16. package/dist/auth.js +42 -0
  17. package/dist/backends/anthropic-tools.d.ts +13 -0
  18. package/dist/backends/anthropic-tools.js +41 -0
  19. package/dist/backends/anthropic.d.ts +9 -0
  20. package/dist/backends/anthropic.js +356 -0
  21. package/dist/backends/chat.d.ts +16 -0
  22. package/dist/backends/chat.js +284 -0
  23. package/dist/backends/deepseek.d.ts +6 -0
  24. package/dist/backends/deepseek.js +37 -0
  25. package/dist/backends/responses.d.ts +5 -0
  26. package/dist/backends/responses.js +227 -0
  27. package/dist/carrier.d.ts +22 -0
  28. package/dist/carrier.js +61 -0
  29. package/dist/catalog.d.ts +8 -0
  30. package/dist/catalog.js +40 -0
  31. package/dist/cli.d.ts +2 -0
  32. package/dist/cli.js +64 -0
  33. package/dist/compaction-handoff.d.ts +13 -0
  34. package/dist/compaction-handoff.js +126 -0
  35. package/dist/compaction-history.d.ts +7 -0
  36. package/dist/compaction-history.js +20 -0
  37. package/dist/compaction.d.ts +20 -0
  38. package/dist/compaction.js +134 -0
  39. package/dist/config.d.ts +6 -0
  40. package/dist/config.js +170 -0
  41. package/dist/errors.d.ts +43 -0
  42. package/dist/errors.js +141 -0
  43. package/dist/headers.d.ts +6 -0
  44. package/dist/headers.js +39 -0
  45. package/dist/history.d.ts +7 -0
  46. package/dist/history.js +85 -0
  47. package/dist/ids.d.ts +1 -0
  48. package/dist/ids.js +4 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.js +4 -0
  51. package/dist/inspect/capture.d.ts +119 -0
  52. package/dist/inspect/capture.js +370 -0
  53. package/dist/inspect/http.d.ts +7 -0
  54. package/dist/inspect/http.js +149 -0
  55. package/dist/inspect/view.d.ts +239 -0
  56. package/dist/inspect/view.js +388 -0
  57. package/dist/model-catalog.d.ts +18 -0
  58. package/dist/model-catalog.js +16 -0
  59. package/dist/profiles.d.ts +6 -0
  60. package/dist/profiles.js +63 -0
  61. package/dist/prompts.d.ts +5 -0
  62. package/dist/prompts.js +118 -0
  63. package/dist/server.d.ts +4 -0
  64. package/dist/server.js +232 -0
  65. package/dist/sse.d.ts +12 -0
  66. package/dist/sse.js +93 -0
  67. package/dist/tool-history.d.ts +4 -0
  68. package/dist/tool-history.js +73 -0
  69. package/dist/tools.d.ts +35 -0
  70. package/dist/tools.js +193 -0
  71. package/dist/types.d.ts +125 -0
  72. package/dist/types.js +1 -0
  73. package/dist/upstream.d.ts +3 -0
  74. package/dist/upstream.js +89 -0
  75. package/dist/writer.d.ts +95 -0
  76. package/dist/writer.js +430 -0
  77. package/docs/backends-anthropic.md +88 -0
  78. package/docs/backends-deepseek-chat.md +75 -0
  79. package/docs/backends-responses.md +33 -0
  80. package/docs/compaction.md +180 -0
  81. package/docs/config.md +271 -0
  82. package/docs/design.md +97 -0
  83. package/docs/inspect.md +148 -0
  84. package/docs/reasoning.md +72 -0
  85. package/docs/releasing.md +50 -0
  86. package/docs/responses-stream.md +90 -0
  87. package/docs/testing.md +177 -0
  88. package/docs/tools.md +68 -0
  89. package/examples/config.json +35 -0
  90. package/examples/gpt-claude.json +27 -0
  91. package/package.json +46 -4
  92. package/ui/api.js +64 -0
  93. package/ui/app.css +379 -0
  94. package/ui/app.js +489 -0
  95. package/ui/detail.js +713 -0
  96. package/ui/diff.js +281 -0
  97. package/ui/dom.js +258 -0
  98. package/ui/events.js +376 -0
  99. package/ui/favicon.svg +1 -0
  100. package/ui/index.html +58 -0
  101. package/ui/labels.js +106 -0
  102. package/ui/list.js +180 -0
  103. package/ui/structure.js +285 -0
  104. package/ui/tree.js +158 -0
@@ -0,0 +1,370 @@
1
+ // In-memory capture of complete exchanges for the inspector. A capture keeps
2
+ // every byte the bridge received and sent. Only credential header values are
3
+ // redacted; bodies are never filtered.
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { createHash } from "node:crypto";
6
+ const MIB = 1024 * 1024;
7
+ export const DEFAULT_CAPTURE_MIB = 256;
8
+ const scope = new AsyncLocalStorage();
9
+ // Segment match keeps diagnostics such as x-ratelimit-remaining-tokens visible.
10
+ const SENSITIVE = /(^|[-_])(auth|authorization|token|secret|password|passwd|cookie|key|apikey|credential|credentials|signature)([-_]|$)/i;
11
+ function redact(name, value, extra) {
12
+ if (!extra.has(name) && !SENSITIVE.test(name))
13
+ return value;
14
+ const digest = createHash("sha256").update(value).digest("hex").slice(0, 8);
15
+ return `[redacted: ${value.length} chars, sha256 ${digest}]`;
16
+ }
17
+ /** Lowercase names, join repeated values, and redact credentials. */
18
+ export function headerMap(headers, extra = new Set()) {
19
+ const result = {};
20
+ if (!headers)
21
+ return result;
22
+ const entries = headers instanceof Headers ? [...headers.entries()] : Object.entries(headers);
23
+ for (const [rawName, rawValue] of entries) {
24
+ if (rawValue === undefined)
25
+ continue;
26
+ const name = rawName.toLowerCase();
27
+ const value = Array.isArray(rawValue) ? rawValue.map(String).join(name === "set-cookie" ? "\n" : ", ") : String(rawValue);
28
+ result[name] = redact(name, value, extra);
29
+ }
30
+ return result;
31
+ }
32
+ function byteLength(chunk) {
33
+ return typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
34
+ }
35
+ function message(error) {
36
+ return error instanceof Error ? error.message : String(error);
37
+ }
38
+ /** One HTTP message after its start line: status, headers, and timed body parts. */
39
+ export class Message {
40
+ status;
41
+ statusText;
42
+ headers;
43
+ headersMs;
44
+ endedMs;
45
+ end = "active";
46
+ error;
47
+ parts = [];
48
+ bytes = 0;
49
+ decoder = new TextDecoder();
50
+ exchange;
51
+ constructor(exchange) { this.exchange = exchange; }
52
+ head(status, statusText, headers) {
53
+ if (this.exchange.closed)
54
+ return;
55
+ if (this.headersMs === undefined)
56
+ this.exchange.touch();
57
+ this.status = status;
58
+ this.statusText = statusText;
59
+ this.headers = headers;
60
+ this.headersMs ??= this.exchange.now();
61
+ }
62
+ push(chunk) {
63
+ if (this.exchange.closed || this.end !== "active")
64
+ return;
65
+ const text = typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true });
66
+ if (text)
67
+ this.parts.push({ t: this.exchange.now(), text });
68
+ const size = byteLength(chunk);
69
+ this.bytes += size;
70
+ this.exchange.account(size);
71
+ }
72
+ finish(end, error) {
73
+ if (this.exchange.closed || this.end !== "active")
74
+ return;
75
+ const rest = this.decoder.decode();
76
+ if (rest)
77
+ this.parts.push({ t: this.exchange.now(), text: rest });
78
+ this.end = end;
79
+ if (error !== undefined)
80
+ this.error = message(error);
81
+ this.endedMs = this.exchange.now();
82
+ this.exchange.touch();
83
+ }
84
+ text() {
85
+ return this.parts.map(part => part.text).join("");
86
+ }
87
+ discard() {
88
+ this.parts.length = 0;
89
+ this.bytes = 0;
90
+ this.headers = undefined;
91
+ this.error = undefined;
92
+ this.decoder.decode();
93
+ }
94
+ }
95
+ /** One request from the bridge to an upstream. */
96
+ export class Call {
97
+ index;
98
+ label;
99
+ startedMs;
100
+ method = "POST";
101
+ url;
102
+ headers;
103
+ text;
104
+ bytes;
105
+ error;
106
+ response;
107
+ exchange;
108
+ constructor(exchange, label, url, headers, text) {
109
+ this.exchange = exchange;
110
+ this.index = exchange.calls.length;
111
+ this.label = label;
112
+ this.startedMs = exchange.now();
113
+ this.url = url;
114
+ this.headers = headers;
115
+ this.text = text;
116
+ this.bytes = Buffer.byteLength(text);
117
+ exchange.account(this.bytes);
118
+ exchange.touch();
119
+ }
120
+ received(response) {
121
+ if (this.response || this.exchange.closed)
122
+ return;
123
+ this.response = new Message(this.exchange);
124
+ this.response.head(response.status, response.statusText, headerMap(response.headers));
125
+ }
126
+ chunk(value) {
127
+ this.response?.push(value);
128
+ }
129
+ done(end, error) {
130
+ this.response?.finish(end, error);
131
+ }
132
+ failed(error) {
133
+ if (this.exchange.closed || this.response || this.error !== undefined)
134
+ return;
135
+ this.error = message(error);
136
+ this.exchange.touch();
137
+ }
138
+ discard() {
139
+ this.text = "";
140
+ this.bytes = 0;
141
+ for (const name of Object.keys(this.headers))
142
+ delete this.headers[name];
143
+ this.error = undefined;
144
+ this.response?.discard();
145
+ }
146
+ }
147
+ function previewOf(request) {
148
+ if (typeof request.input === "string")
149
+ return request.input.slice(0, 200);
150
+ if (!Array.isArray(request.input))
151
+ return null;
152
+ for (let i = request.input.length - 1; i >= 0; i--) {
153
+ const item = request.input[i];
154
+ // A subagent's task arrives as an agent_message item after the inherited user turns.
155
+ const task = item?.type === "agent_message" || (item?.role === "user" && (item.type === undefined || item.type === "message"));
156
+ if (!task)
157
+ continue;
158
+ const text = typeof item.content === "string" ? item.content
159
+ : Array.isArray(item.content) ? item.content.filter((part) => typeof part?.text === "string").map((part) => part.text).join(" ") : "";
160
+ if (text.trim())
161
+ return text.trim().replace(/\s+/g, " ").slice(0, 200);
162
+ }
163
+ return null;
164
+ }
165
+ /** One client request and everything it caused. */
166
+ export class Exchange {
167
+ id;
168
+ seq;
169
+ rev = 0;
170
+ startedAt = Date.now();
171
+ durationMs;
172
+ closed = false;
173
+ method;
174
+ path;
175
+ headers;
176
+ requestText;
177
+ requestBytes = 0;
178
+ meta = { model: null, kind: "invalid", stream: null, input_items: null, preview: null };
179
+ route;
180
+ responsesLite;
181
+ calls = [];
182
+ response;
183
+ bytes = 0;
184
+ inspector;
185
+ t0 = performance.now();
186
+ constructor(inspector, seq, req, res) {
187
+ this.inspector = inspector;
188
+ this.seq = seq;
189
+ this.id = `x${seq}`;
190
+ this.method = req.method ?? "";
191
+ this.path = req.url ?? "";
192
+ this.headers = headerMap(req.headers);
193
+ this.response = new Message(this);
194
+ this.tap(res);
195
+ }
196
+ now() { return Math.round((performance.now() - this.t0) * 10) / 10; }
197
+ touch() { this.rev = ++this.inspector.rev; }
198
+ account(bytes) { this.inspector.account(this, bytes); }
199
+ received(text) {
200
+ if (this.closed)
201
+ return;
202
+ this.requestText = text;
203
+ this.requestBytes = Buffer.byteLength(text);
204
+ this.account(this.requestBytes);
205
+ }
206
+ parsed(request) {
207
+ if (this.closed)
208
+ return;
209
+ const input = request.input;
210
+ this.meta = {
211
+ model: typeof request.model === "string" ? request.model : null,
212
+ kind: Array.isArray(input) && input.some(item => item?.type === "compaction_trigger") ? "compaction" : "turn",
213
+ stream: typeof request.stream === "boolean" ? request.stream : null,
214
+ input_items: Array.isArray(input) ? input.length : null,
215
+ preview: previewOf(request),
216
+ };
217
+ this.touch();
218
+ }
219
+ routed(route, responsesLite) {
220
+ if (this.closed)
221
+ return;
222
+ this.route = route;
223
+ this.responsesLite = responsesLite;
224
+ this.touch();
225
+ }
226
+ /** Records what the bridge writes, at the same calls that write it. */
227
+ tap(res) {
228
+ const exchange = this;
229
+ const { writeHead, write, end } = res;
230
+ const head = (target, status, rest) => {
231
+ const text = typeof rest[0] === "string" ? rest[0] : undefined;
232
+ const extra = text === undefined ? rest[0] : rest[1];
233
+ const given = Array.isArray(extra) ? Object.fromEntries(extra) : extra ?? {};
234
+ exchange.response.head(status, text ?? target.statusMessage, headerMap({ ...target.getHeaders(), ...headerMap(given) }));
235
+ };
236
+ const chunk = (target, value, encoding) => {
237
+ if (exchange.response.headersMs === undefined)
238
+ head(target, target.statusCode, []);
239
+ if (typeof value === "string")
240
+ exchange.response.push(typeof encoding === "string" && encoding !== "utf8" && encoding !== "utf-8" ? Buffer.from(value, encoding) : value);
241
+ else if (value instanceof Uint8Array)
242
+ exchange.response.push(value);
243
+ };
244
+ res.writeHead = function (status, ...rest) {
245
+ head(this, status, rest);
246
+ return writeHead.call(this, status, ...rest);
247
+ };
248
+ res.write = function (value, ...rest) {
249
+ chunk(this, value, rest[0]);
250
+ return write.call(this, value, ...rest);
251
+ };
252
+ res.end = function (...args) {
253
+ if (args.length && typeof args[0] !== "function")
254
+ chunk(this, args[0], args[1]);
255
+ return end.apply(this, args);
256
+ };
257
+ res.once("close", () => this.close(res.writableFinished));
258
+ }
259
+ close(finished) {
260
+ if (this.closed)
261
+ return;
262
+ this.response.finish(finished ? "complete" : "cancelled", finished ? undefined : "Client disconnected");
263
+ for (const call of this.calls) {
264
+ if (!call.response)
265
+ call.failed(finished ? "No upstream response" : "Cancelled by client disconnect");
266
+ // The bridge stops reading after the terminal event; any remaining bytes stay unread.
267
+ else
268
+ call.done(finished ? "unread" : "cancelled");
269
+ }
270
+ this.durationMs = this.now();
271
+ this.closed = true;
272
+ this.touch();
273
+ this.inspector.evict();
274
+ }
275
+ /** Releases captured data without stopping the underlying HTTP exchange. */
276
+ discard() {
277
+ this.closed = true;
278
+ this.requestText = undefined;
279
+ this.requestBytes = 0;
280
+ this.meta = { model: null, kind: "invalid", stream: null, input_items: null, preview: null };
281
+ this.route = undefined;
282
+ for (const name of Object.keys(this.headers))
283
+ delete this.headers[name];
284
+ for (const call of this.calls)
285
+ call.discard();
286
+ this.calls.length = 0;
287
+ this.response.discard();
288
+ this.bytes = 0;
289
+ }
290
+ }
291
+ /** Process-wide capture store. Nothing is kept unless capture is on. */
292
+ export class Inspector {
293
+ capturing;
294
+ maxBytes;
295
+ bytes = 0;
296
+ evicted = 0;
297
+ rev = 0;
298
+ seq = 0;
299
+ startedAt = Date.now();
300
+ exchanges = new Map();
301
+ config;
302
+ redactions;
303
+ constructor(config) {
304
+ this.config = config;
305
+ this.capturing = config.inspect?.capture ?? false;
306
+ this.maxBytes = (config.inspect?.max_mib ?? DEFAULT_CAPTURE_MIB) * MIB;
307
+ this.redactions = new Set(Object.values(config.upstreams).flatMap(up => up.auth_header ? [up.auth_header.toLowerCase()] : []));
308
+ }
309
+ /** Runs one server request with a slot for its exchange. */
310
+ scope(fn) { return scope.run({}, fn); }
311
+ begin(req, res) {
312
+ const slot = scope.getStore();
313
+ if (!this.capturing || !slot)
314
+ return undefined;
315
+ const exchange = new Exchange(this, ++this.seq, req, res);
316
+ this.exchanges.set(exchange.id, exchange);
317
+ slot.exchange = exchange;
318
+ exchange.touch();
319
+ return exchange;
320
+ }
321
+ /** Records one upstream request made inside the current exchange. */
322
+ upstream(url, headers, text) {
323
+ const slot = scope.getStore();
324
+ const exchange = slot?.exchange;
325
+ if (!exchange || exchange.closed || this.exchanges.get(exchange.id) !== exchange)
326
+ return undefined;
327
+ const call = new Call(exchange, slot.label ?? "main", url, headerMap(headers, this.redactions), text);
328
+ exchange.calls.push(call);
329
+ return call;
330
+ }
331
+ account(exchange, bytes) {
332
+ if (this.exchanges.get(exchange.id) !== exchange)
333
+ return;
334
+ exchange.bytes += bytes;
335
+ this.bytes += bytes;
336
+ this.evict();
337
+ }
338
+ /** Rechecks the soft cap when bytes arrive or an active exchange closes. */
339
+ evict() {
340
+ if (this.bytes <= this.maxBytes)
341
+ return;
342
+ // The cap is soft: exchanges still streaming are never evicted.
343
+ for (const [id, candidate] of this.exchanges) {
344
+ if (this.bytes <= this.maxBytes)
345
+ break;
346
+ if (!candidate.closed)
347
+ continue;
348
+ this.exchanges.delete(id);
349
+ this.bytes -= candidate.bytes;
350
+ this.evicted++;
351
+ }
352
+ this.rev++;
353
+ }
354
+ clear() {
355
+ for (const exchange of this.exchanges.values())
356
+ exchange.discard();
357
+ this.exchanges.clear();
358
+ this.bytes = 0;
359
+ this.rev++;
360
+ }
361
+ }
362
+ /** Labels upstream calls made inside fn, such as native compaction or a handoff. */
363
+ export function withCaptureLabel(label, fn) {
364
+ const slot = scope.getStore();
365
+ return slot?.exchange ? scope.run({ ...slot, label }, fn) : fn();
366
+ }
367
+ /** Records an upstream request when the current server request is being captured. */
368
+ export function captureUpstream(url, headers, text) {
369
+ return scope.getStore()?.exchange?.inspector.upstream(url, headers, text);
370
+ }
@@ -0,0 +1,7 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Inspector } from "./capture.ts";
3
+ export declare function isInspectorPath(path: string): boolean;
4
+ export declare function serveInspector(req: IncomingMessage, res: ServerResponse, url: URL, inspector: Inspector, token: {
5
+ configured: boolean;
6
+ valid: () => boolean;
7
+ }): Promise<void>;
@@ -0,0 +1,149 @@
1
+ // HTTP routes for the inspector: static UI files and a small JSON API.
2
+ import { readFile } from "node:fs/promises";
3
+ import { extname } from "node:path";
4
+ import { detail, listView, stateView, writeExport } from "./view.js";
5
+ // src/inspect and dist/inspect are both two levels below the package root.
6
+ const UI_DIR = new URL("../../ui/", import.meta.url);
7
+ const TYPES = {
8
+ ".html": "text/html; charset=utf-8",
9
+ ".js": "text/javascript; charset=utf-8",
10
+ ".css": "text/css; charset=utf-8",
11
+ ".svg": "image/svg+xml",
12
+ ".png": "image/png",
13
+ ".ico": "image/x-icon",
14
+ ".json": "application/json; charset=utf-8",
15
+ };
16
+ const PAGE_HEADERS = {
17
+ "content-security-policy": "default-src 'self'; img-src 'self' data: blob:; style-src 'self'; script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
18
+ "x-content-type-options": "nosniff",
19
+ "referrer-policy": "no-referrer",
20
+ "cache-control": "no-cache",
21
+ };
22
+ const LOOPBACK = new Set(["127.0.0.1", "localhost", "[::1]"]);
23
+ const MAX_API_BODY = 64 * 1024;
24
+ export function isInspectorPath(path) {
25
+ return path === "/inspect" || path.startsWith("/inspect/");
26
+ }
27
+ function send(res, status, body) {
28
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" });
29
+ res.end(JSON.stringify(body));
30
+ }
31
+ function failure(res, status, code, message) {
32
+ send(res, status, { error: { code, message } });
33
+ }
34
+ /** Without a token, only same-origin pages served from a loopback name may use the API. */
35
+ function localPage(req) {
36
+ const host = req.headers.host;
37
+ if (!host)
38
+ return false;
39
+ let hostname;
40
+ try {
41
+ hostname = new URL(`http://${host}`).hostname;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ if (!LOOPBACK.has(hostname))
47
+ return false;
48
+ if (req.headers.origin !== undefined && req.headers.origin !== `http://${host}`)
49
+ return false;
50
+ const site = req.headers["sec-fetch-site"];
51
+ return site === undefined || site === "same-origin" || site === "none";
52
+ }
53
+ async function readJson(req) {
54
+ let size = 0;
55
+ const chunks = [];
56
+ for await (const chunk of req) {
57
+ size += chunk.length;
58
+ if (size > MAX_API_BODY)
59
+ return undefined;
60
+ chunks.push(chunk);
61
+ }
62
+ try {
63
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
69
+ async function serveFile(req, res, name) {
70
+ const type = TYPES[extname(name)];
71
+ if (!type || !/^[A-Za-z0-9][\w.-]*$/.test(name)) {
72
+ failure(res, 404, "not_found", "Not found");
73
+ return;
74
+ }
75
+ let body;
76
+ try {
77
+ body = await readFile(new URL(name, UI_DIR));
78
+ }
79
+ catch {
80
+ failure(res, 404, "not_found", "Not found");
81
+ return;
82
+ }
83
+ res.writeHead(200, { ...PAGE_HEADERS, "content-type": type, "content-length": String(body.length) });
84
+ res.end(req.method === "HEAD" ? undefined : body);
85
+ }
86
+ export async function serveInspector(req, res, url, inspector, token) {
87
+ const path = url.pathname;
88
+ const method = req.method ?? "GET";
89
+ if (!path.startsWith("/inspect/api/")) {
90
+ if (method !== "GET" && method !== "HEAD") {
91
+ failure(res, 405, "method_not_allowed", "Use GET");
92
+ return;
93
+ }
94
+ if (path === "/inspect") {
95
+ res.writeHead(308, { location: "/inspect/" + url.search });
96
+ res.end();
97
+ return;
98
+ }
99
+ await serveFile(req, res, path === "/inspect/" ? "index.html" : path.slice("/inspect/".length));
100
+ return;
101
+ }
102
+ if (token.configured ? !token.valid() : !localPage(req)) {
103
+ if (token.configured)
104
+ failure(res, 401, "invalid_api_key", "Enter the bridge token");
105
+ else
106
+ failure(res, 403, "forbidden", "Open the inspector from a loopback address, or configure token_env");
107
+ return;
108
+ }
109
+ const route = path.slice("/inspect/api/".length);
110
+ if (route === "state" && method === "GET") {
111
+ send(res, 200, stateView(inspector));
112
+ return;
113
+ }
114
+ if (route === "capture" && method === "POST") {
115
+ const body = await readJson(req);
116
+ if (typeof body?.enabled !== "boolean") {
117
+ failure(res, 400, "invalid_request", "Send {\"enabled\": true|false}");
118
+ return;
119
+ }
120
+ inspector.capturing = body.enabled;
121
+ inspector.rev++;
122
+ send(res, 200, stateView(inspector));
123
+ return;
124
+ }
125
+ if (route === "exchanges" && method === "DELETE") {
126
+ inspector.clear();
127
+ send(res, 200, stateView(inspector));
128
+ return;
129
+ }
130
+ if (route === "exchanges" && method === "GET") {
131
+ const since = Number(url.searchParams.get("since") ?? 0);
132
+ send(res, 200, listView(inspector, Number.isFinite(since) ? since : 0));
133
+ return;
134
+ }
135
+ if (route.startsWith("exchanges/") && method === "GET") {
136
+ const exchange = inspector.exchanges.get(route.slice("exchanges/".length));
137
+ if (!exchange) {
138
+ failure(res, 404, "not_found", "Exchange not found; it may have been cleared or evicted");
139
+ return;
140
+ }
141
+ send(res, 200, detail(exchange));
142
+ return;
143
+ }
144
+ if (route === "export" && method === "GET") {
145
+ await writeExport(inspector, res);
146
+ return;
147
+ }
148
+ failure(res, 404, "not_found", "Unknown inspector API route");
149
+ }