@securecoms/agent-bridge 0.11.2 → 0.15.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/README.md +75 -0
- package/dist/bin.js +2657 -93
- package/dist/bridge-agent.d.ts +331 -0
- package/dist/config.d.ts +3 -0
- package/dist/errors.d.ts +1 -1
- package/dist/http-server.d.ts +4 -4
- package/dist/index.js +2674 -112
- package/dist/memory-taps.d.ts +6 -2
- package/dist/rate-limit.d.ts +10 -0
- package/dist/sodium-ready.d.ts +16 -8
- package/dist/summarize.d.ts +54 -0
- package/dist/voice/fast-client.d.ts +13 -0
- package/dist/voice/fast-prompt.d.ts +53 -0
- package/dist/voice/persona.d.ts +23 -0
- package/dist/voice/prompt-compiler.d.ts +4 -0
- package/dist/voice/settings.d.ts +31 -0
- package/package.json +7 -1
- package/dist/attachments.js +0 -49
- package/dist/auth.js +0 -56
- package/dist/bin.js.pre-c1-f803007 +0 -5795
- package/dist/bin.js.pre-c3-096a9a3 +0 -5832
- package/dist/bin.js.pre-ffrebuild-b162d4a +0 -5639
- package/dist/bin.js.pre-kpheal-20260708-150924 +0 -5531
- package/dist/bin.js.pre-kppersist-20260708-152341 +0 -5543
- package/dist/bin.js.pre-p0-20260708-102415 +0 -5196
- package/dist/bin.js.pre-p1b-02f47e1 +0 -5639
- package/dist/bin.js.pre-warmfix-20260708-153705 +0 -5613
- package/dist/bin.js.pretrace-113205 +0 -5395
- package/dist/bridge-agent.js +0 -831
- package/dist/config.js +0 -74
- package/dist/coordination.js +0 -63
- package/dist/errors.js +0 -25
- package/dist/http-server.js +0 -220
- package/dist/index.js.pre-p0-20260708-102415 +0 -4879
- package/dist/latency.js +0 -87
- package/dist/rate-limit.js +0 -56
- package/dist/service/config.js +0 -26
- package/dist/service/install.js +0 -60
- package/dist/service/launchd.js +0 -30
- package/dist/service/systemd.js +0 -25
- package/dist/ws-server.js +0 -254
package/dist/config.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
const REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
4
|
-
export function loadConfig(opts) {
|
|
5
|
-
const raw = readFileSync(opts.credentialPath, "utf8");
|
|
6
|
-
// Perms check (advisory): warn to STDERR if not owner-only.
|
|
7
|
-
try {
|
|
8
|
-
const mode = statSync(opts.credentialPath).mode & 0o777;
|
|
9
|
-
if (mode & 0o077)
|
|
10
|
-
process.stderr.write(`[bridge] WARNING: credential file ${opts.credentialPath} permission ${mode.toString(8)} is not 0600 — tighten it (chmod 600).\n`);
|
|
11
|
-
}
|
|
12
|
-
catch { /* stat best-effort */ }
|
|
13
|
-
let json;
|
|
14
|
-
try {
|
|
15
|
-
json = JSON.parse(raw);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
throw new Error("credential file is not valid JSON");
|
|
19
|
-
}
|
|
20
|
-
for (const k of REQUIRED)
|
|
21
|
-
if (typeof json[k] !== "string" || !json[k])
|
|
22
|
-
throw new Error(`credential file missing required field: ${k}`);
|
|
23
|
-
const storePath = opts.storePath ?? join(dirname(opts.credentialPath), "bridge-state.db");
|
|
24
|
-
// Optional agent name for the room @mention filter: prefer agent_name, else display_name.
|
|
25
|
-
const agentName = typeof json.agent_name === "string" && json.agent_name
|
|
26
|
-
? json.agent_name
|
|
27
|
-
: typeof json.display_name === "string" && json.display_name
|
|
28
|
-
? json.display_name
|
|
29
|
-
: undefined;
|
|
30
|
-
const rateLimitCapacity = typeof json.rate_limit_capacity === "number" ? json.rate_limit_capacity : 20;
|
|
31
|
-
const rateLimitRefillPerMinute = typeof json.rate_limit_refill_per_minute === "number" ? json.rate_limit_refill_per_minute : 20;
|
|
32
|
-
return {
|
|
33
|
-
apiKey: json.api_key, identityId: json.identity_id, deviceId: json.device_id,
|
|
34
|
-
signingPrivateKey: json.signing_private_key, enclaveId: json.enclave_id,
|
|
35
|
-
apiBase: json.api_base, storePath, agentName,
|
|
36
|
-
rateLimitCapacity, rateLimitRefillPerMinute,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function arg(argv, flag) {
|
|
40
|
-
const i = argv.indexOf(flag);
|
|
41
|
-
return i >= 0 ? argv[i + 1] : undefined;
|
|
42
|
-
}
|
|
43
|
-
function args(argv, flag) {
|
|
44
|
-
const out = [];
|
|
45
|
-
for (let i = 0; i < argv.length; i++)
|
|
46
|
-
if (argv[i] === flag && argv[i + 1])
|
|
47
|
-
out.push(argv[i + 1]);
|
|
48
|
-
return out;
|
|
49
|
-
}
|
|
50
|
-
export function parseDaemonArgs(argv, env) {
|
|
51
|
-
const tlsCert = arg(argv, "--tls-cert") ?? env.SECURECOMS_TLS_CERT;
|
|
52
|
-
const tlsKey = arg(argv, "--tls-key") ?? env.SECURECOMS_TLS_KEY;
|
|
53
|
-
return {
|
|
54
|
-
port: Number(arg(argv, "--port") ?? env.SECURECOMS_PORT ?? 7077),
|
|
55
|
-
bind: arg(argv, "--bind") ?? env.SECURECOMS_BIND ?? "127.0.0.1",
|
|
56
|
-
tokenFile: arg(argv, "--token-file") ?? env.SECURECOMS_TOKEN_FILE,
|
|
57
|
-
token: arg(argv, "--token") ?? env.SECURECOMS_LOCAL_TOKEN,
|
|
58
|
-
tls: Boolean(tlsCert && tlsKey),
|
|
59
|
-
tlsCert,
|
|
60
|
-
tlsKey,
|
|
61
|
-
allowOrigins: args(argv, "--allow-origin"),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
export function isLoopback(host) {
|
|
65
|
-
return (host === "localhost" ||
|
|
66
|
-
host === "::1" ||
|
|
67
|
-
host === "[::1]" ||
|
|
68
|
-
/^127\./.test(host));
|
|
69
|
-
}
|
|
70
|
-
export function validateBind(bind, tls) {
|
|
71
|
-
if (!isLoopback(bind) && !tls) {
|
|
72
|
-
throw new Error(`refusing non-loopback bind '${bind}' without TLS — supply --tls-cert and --tls-key for cross-host, or bind a loopback address`);
|
|
73
|
-
}
|
|
74
|
-
}
|
package/dist/coordination.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// Multi-agent ROOM coordination floor — @mention filter.
|
|
2
|
-
//
|
|
3
|
-
// Ported faithfully from the gen2 OpenClaw plugin
|
|
4
|
-
// (packages/plugin/src/openclaw-entry.ts: _parseMentions / _shouldProcessRoomMessage /
|
|
5
|
-
// _ownRoomNames). Pure functions only — no rate limiter (a later slice), no globals,
|
|
6
|
-
// no console output. Semantics must match gen2 exactly.
|
|
7
|
-
/** Extract @mention names from plaintext. Returns lowercased names. */
|
|
8
|
-
export function parseMentions(text) {
|
|
9
|
-
const mentions = [];
|
|
10
|
-
// Match @word at a word boundary — supports multiple via sequential matching.
|
|
11
|
-
const re = /@(\w[\w]*)/gi;
|
|
12
|
-
let match;
|
|
13
|
-
while ((match = re.exec(text)) !== null) {
|
|
14
|
-
mentions.push(match[1].toLowerCase());
|
|
15
|
-
}
|
|
16
|
-
return mentions;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Lower-cased set of names that count as "this agent" in a room: framework
|
|
20
|
-
* agentName + its first word, accountId + its first word, and (when known) the
|
|
21
|
-
* AgentVault room display_name + its first word. First word = split on
|
|
22
|
-
* whitespace or "(".
|
|
23
|
-
*/
|
|
24
|
-
export function ownRoomNames(agentName, accountId, roomDisplayName) {
|
|
25
|
-
const names = new Set();
|
|
26
|
-
const add = (n) => {
|
|
27
|
-
if (!n)
|
|
28
|
-
return;
|
|
29
|
-
const lower = n.toLowerCase();
|
|
30
|
-
names.add(lower);
|
|
31
|
-
names.add(lower.split(/[\s(]/)[0]);
|
|
32
|
-
};
|
|
33
|
-
add(agentName);
|
|
34
|
-
add(accountId);
|
|
35
|
-
add(roomDisplayName);
|
|
36
|
-
return names;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Whether this agent should process a room message based on its @mentions.
|
|
40
|
-
*
|
|
41
|
-
* Loop prevention is the rate-limiter's job (a later slice), so this filter does
|
|
42
|
-
* NOT gate on senderIsAgent — every message is processed unless it @mentions a
|
|
43
|
-
* *different* agent specifically:
|
|
44
|
-
* - no mentions → true (broadcast)
|
|
45
|
-
* - @all / @everyone → true
|
|
46
|
-
* - this agent is mentioned → true
|
|
47
|
-
* - else (a different agent is mentioned) → false
|
|
48
|
-
*/
|
|
49
|
-
export function shouldProcessRoomMessage(plaintext, agentName, accountId, opts) {
|
|
50
|
-
const mentions = parseMentions(plaintext);
|
|
51
|
-
// No mentions → broadcast to all agents.
|
|
52
|
-
if (mentions.length === 0)
|
|
53
|
-
return true;
|
|
54
|
-
// @all / @everyone → all agents process.
|
|
55
|
-
if (mentions.includes("all") || mentions.includes("everyone"))
|
|
56
|
-
return true;
|
|
57
|
-
// This agent addressed by framework name / accountId / room display_name.
|
|
58
|
-
const ownNames = ownRoomNames(agentName, accountId, opts?.roomDisplayName);
|
|
59
|
-
if (mentions.some((m) => ownNames.has(m)))
|
|
60
|
-
return true;
|
|
61
|
-
// Has @mentions but none match this agent — skip.
|
|
62
|
-
return false;
|
|
63
|
-
}
|
package/dist/errors.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export const BRIDGE_ERRORS = {
|
|
2
|
-
unauthorized: { http: 401, message: "missing or invalid bearer token" },
|
|
3
|
-
forbidden_origin: { http: 403, message: "origin or host not allowed" },
|
|
4
|
-
not_found: { http: 404, message: "no such route" },
|
|
5
|
-
invalid_request: { http: 400, message: "request failed validation" },
|
|
6
|
-
invalid_json: { http: 400, message: "request body is not valid JSON" },
|
|
7
|
-
body_too_large: { http: 400, message: "request body exceeds the size limit" },
|
|
8
|
-
channel_pending: { http: 503, message: "channel not yet admitted; retry shortly" },
|
|
9
|
-
rate_limited: { http: 429, message: "rate limit exceeded; retry shortly" },
|
|
10
|
-
internal: { http: 500, message: "internal error" },
|
|
11
|
-
};
|
|
12
|
-
/** The shared error payload (used as-is by HTTP, and spread into the WS frame). */
|
|
13
|
-
export function errorBody(code, detail) {
|
|
14
|
-
const body = { error: code, message: BRIDGE_ERRORS[code].message };
|
|
15
|
-
if (detail !== undefined)
|
|
16
|
-
body.detail = detail;
|
|
17
|
-
return body;
|
|
18
|
-
}
|
|
19
|
-
export function httpStatus(code) {
|
|
20
|
-
return BRIDGE_ERRORS[code].http;
|
|
21
|
-
}
|
|
22
|
-
/** WebSocket error frame: the shared body under a `{type:"error"}` envelope. */
|
|
23
|
-
export function wsErrorFrame(code, detail) {
|
|
24
|
-
return { type: "error", ...errorBody(code, detail) };
|
|
25
|
-
}
|
package/dist/http-server.js
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { createServer as createHttp } from "node:http";
|
|
2
|
-
import { createServer as createHttps } from "node:https";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { ChannelPendingError, RateLimitedError, InvalidPathError } from "./bridge-agent.js";
|
|
5
|
-
import { bearerFrom, tokenMatches, originHostAllowed } from "./auth.js";
|
|
6
|
-
import { errorBody, httpStatus } from "./errors.js";
|
|
7
|
-
export function writeSse(res, msgs) {
|
|
8
|
-
for (let i = 0; i < msgs.length; i++) {
|
|
9
|
-
if (res.destroyed || res.writableEnded)
|
|
10
|
-
return msgs.slice(i);
|
|
11
|
-
try {
|
|
12
|
-
res.write(`event: message\ndata: ${JSON.stringify(msgs[i])}\n\n`);
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return msgs.slice(i);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
/** The daemon's self-describing capability contract (A2A-style discovery). */
|
|
21
|
-
export function capabilities(opts) {
|
|
22
|
-
return {
|
|
23
|
-
api_version: "1",
|
|
24
|
-
receive_modes: ["longpoll", "sse", ...(opts.websocket ? ["websocket"] : [])],
|
|
25
|
-
send_types: ["text", "agent_status", "a2a"],
|
|
26
|
-
webhook_push: false, // B3: A2A-style push-notification config (ciphertext/wake-only)
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
const SEND_CAP = 64 * 1024;
|
|
30
|
-
const OTHER_CAP = 8 * 1024;
|
|
31
|
-
const a2aSchema = z.object({
|
|
32
|
-
a2a_version: z.string().min(1),
|
|
33
|
-
kind: z.enum(["message", "task", "artifact"]),
|
|
34
|
-
object: z.record(z.unknown()),
|
|
35
|
-
});
|
|
36
|
-
const sendAttachmentSchema = z.object({
|
|
37
|
-
channel_id: z.string().min(1),
|
|
38
|
-
path: z.string().min(1),
|
|
39
|
-
caption: z.string().optional(),
|
|
40
|
-
});
|
|
41
|
-
const sendSchema = z.object({
|
|
42
|
-
channel_id: z.string().min(1),
|
|
43
|
-
text: z.string().optional(),
|
|
44
|
-
idempotency_key: z.string().optional(),
|
|
45
|
-
a2a: a2aSchema.optional(),
|
|
46
|
-
}).refine((d) => typeof d.text === "string" || d.a2a !== undefined, {
|
|
47
|
-
message: "send requires either text or a2a",
|
|
48
|
-
});
|
|
49
|
-
const statusSchema = z.object({
|
|
50
|
-
channel_id: z.string().min(1),
|
|
51
|
-
state: z.enum(["idle", "working", "stalled", "error"]),
|
|
52
|
-
detail: z.string().optional(),
|
|
53
|
-
});
|
|
54
|
-
function json(res, status, body) {
|
|
55
|
-
const s = JSON.stringify(body);
|
|
56
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
57
|
-
res.end(s);
|
|
58
|
-
}
|
|
59
|
-
/** Render a canonical error to its mapped HTTP status + shared body. */
|
|
60
|
-
function jsonError(res, code, detail) {
|
|
61
|
-
json(res, httpStatus(code), errorBody(code, detail));
|
|
62
|
-
}
|
|
63
|
-
function readBody(req, cap) {
|
|
64
|
-
return new Promise((resolve, reject) => {
|
|
65
|
-
let size = 0;
|
|
66
|
-
let done = false;
|
|
67
|
-
const chunks = [];
|
|
68
|
-
const onData = (c) => {
|
|
69
|
-
if (done)
|
|
70
|
-
return;
|
|
71
|
-
size += c.length;
|
|
72
|
-
if (size > cap) {
|
|
73
|
-
// Stop consuming and let the response (400) flush; do NOT destroy the
|
|
74
|
-
// socket here, that races with the response write and surfaces to the
|
|
75
|
-
// client as "other side closed" instead of a clean 400.
|
|
76
|
-
done = true;
|
|
77
|
-
req.removeListener("data", onData);
|
|
78
|
-
req.pause();
|
|
79
|
-
reject(new Error("body_too_large"));
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
chunks.push(c);
|
|
83
|
-
};
|
|
84
|
-
req.on("data", onData);
|
|
85
|
-
req.on("end", () => { if (!done) {
|
|
86
|
-
done = true;
|
|
87
|
-
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
88
|
-
} });
|
|
89
|
-
req.on("error", (e) => { if (!done) {
|
|
90
|
-
done = true;
|
|
91
|
-
reject(e);
|
|
92
|
-
} });
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
export function createHttpServer(agent, opts) {
|
|
96
|
-
// SSE state is wired in the next task; declared here so closeAllSseStreams exists.
|
|
97
|
-
let closeSse = () => { };
|
|
98
|
-
let sse = null;
|
|
99
|
-
const tearDownSse = () => {
|
|
100
|
-
if (!sse)
|
|
101
|
-
return;
|
|
102
|
-
sse.unsubscribe();
|
|
103
|
-
clearInterval(sse.keepalive);
|
|
104
|
-
try {
|
|
105
|
-
sse.res.end();
|
|
106
|
-
}
|
|
107
|
-
catch { /* already closed */ }
|
|
108
|
-
sse = null;
|
|
109
|
-
};
|
|
110
|
-
closeSse = tearDownSse; // replaces the no-op placeholder
|
|
111
|
-
const handler = async (req, res) => {
|
|
112
|
-
try {
|
|
113
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
114
|
-
const path = url.pathname;
|
|
115
|
-
const method = req.method ?? "GET";
|
|
116
|
-
if (method === "GET" && path === "/healthz") {
|
|
117
|
-
return json(res, 200, { ...agent.health(), capabilities: capabilities(opts) });
|
|
118
|
-
}
|
|
119
|
-
// Auth + origin/host for everything else.
|
|
120
|
-
if (!tokenMatches(bearerFrom(req.headers.authorization), opts.token)) {
|
|
121
|
-
return jsonError(res, "unauthorized");
|
|
122
|
-
}
|
|
123
|
-
if (!originHostAllowed({ origin: req.headers.origin, host: req.headers.host }, opts.allowOrigins)) {
|
|
124
|
-
return jsonError(res, "forbidden_origin");
|
|
125
|
-
}
|
|
126
|
-
if (method === "GET" && path === "/v1/channels") {
|
|
127
|
-
return json(res, 200, await agent.listChannels());
|
|
128
|
-
}
|
|
129
|
-
// SecureComs-envelope latency percentiles (ms), messaging hops only —
|
|
130
|
-
// excludes LLM time. Behind the bearer (timing aggregates leak rate).
|
|
131
|
-
if (method === "GET" && path === "/v1/metrics") {
|
|
132
|
-
return json(res, 200, agent.metrics());
|
|
133
|
-
}
|
|
134
|
-
if (method === "POST" && path === "/v1/send") {
|
|
135
|
-
const parsed = sendSchema.safeParse(JSON.parse(await readBody(req, SEND_CAP)));
|
|
136
|
-
if (!parsed.success)
|
|
137
|
-
return jsonError(res, "invalid_request", parsed.error.issues);
|
|
138
|
-
const r = parsed.data.a2a
|
|
139
|
-
? await agent.sendA2A(parsed.data.channel_id, parsed.data.a2a, parsed.data.idempotency_key)
|
|
140
|
-
// `text` is guaranteed present here by the sendSchema refine (text | a2a)
|
|
141
|
-
: await agent.send(parsed.data.channel_id, parsed.data.text, parsed.data.idempotency_key);
|
|
142
|
-
return json(res, 200, r);
|
|
143
|
-
}
|
|
144
|
-
if (method === "POST" && path === "/v1/send-attachment") {
|
|
145
|
-
const parsed = sendAttachmentSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
146
|
-
if (!parsed.success)
|
|
147
|
-
return jsonError(res, "invalid_request", parsed.error.issues);
|
|
148
|
-
const r = await agent.sendAttachment(parsed.data.channel_id, parsed.data.path, parsed.data.caption);
|
|
149
|
-
return json(res, 200, r);
|
|
150
|
-
}
|
|
151
|
-
if (method === "POST" && path === "/v1/status") {
|
|
152
|
-
const parsed = statusSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
153
|
-
if (!parsed.success)
|
|
154
|
-
return jsonError(res, "invalid_request", parsed.error.issues);
|
|
155
|
-
await agent.reportStatus(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
|
|
156
|
-
return json(res, 200, { ok: true });
|
|
157
|
-
}
|
|
158
|
-
if (method === "GET" && path === "/v1/messages") {
|
|
159
|
-
const channel = url.searchParams.get("channel_id") ?? undefined;
|
|
160
|
-
const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
|
|
161
|
-
return json(res, 200, await agent.drain(channel, wait));
|
|
162
|
-
}
|
|
163
|
-
if (method === "GET" && path === "/v1/events") {
|
|
164
|
-
const channel = url.searchParams.get("channel_id") ?? undefined;
|
|
165
|
-
tearDownSse(); // single consumer: newest wins
|
|
166
|
-
res.writeHead(200, {
|
|
167
|
-
"content-type": "text/event-stream",
|
|
168
|
-
"cache-control": "no-cache",
|
|
169
|
-
connection: "keep-alive",
|
|
170
|
-
});
|
|
171
|
-
res.write(":connected\n\n"); // deterministic open signal (clients ignore comments)
|
|
172
|
-
const drainToClient = () => {
|
|
173
|
-
const left = writeSse(res, agent.takeBuffered(channel));
|
|
174
|
-
if (left.length)
|
|
175
|
-
agent.requeueFront(left);
|
|
176
|
-
};
|
|
177
|
-
const unsubscribe = agent.subscribe(drainToClient);
|
|
178
|
-
const keepalive = setInterval(() => { try {
|
|
179
|
-
res.write(":keepalive\n\n");
|
|
180
|
-
}
|
|
181
|
-
catch { /* gone */ } }, 15_000);
|
|
182
|
-
sse = { res, unsubscribe, keepalive };
|
|
183
|
-
drainToClient(); // flush anything already buffered
|
|
184
|
-
req.on("close", () => { if (sse?.res === res)
|
|
185
|
-
tearDownSse(); });
|
|
186
|
-
return; // keep the connection open
|
|
187
|
-
}
|
|
188
|
-
return jsonError(res, "not_found");
|
|
189
|
-
}
|
|
190
|
-
catch (e) {
|
|
191
|
-
if (e.message === "body_too_large") {
|
|
192
|
-
// The request stream was paused mid-body (readBody stops consuming at the
|
|
193
|
-
// cap). Send `Connection: close` so Node flushes the 400 and then closes
|
|
194
|
-
// this socket — discarding the unread body. This avoids both the
|
|
195
|
-
// keep-alive stall (a paused, unconsumed stream blocks the next pipelined
|
|
196
|
-
// request) and the response-truncation race that destroying the socket
|
|
197
|
-
// directly would cause. (review issue 1)
|
|
198
|
-
if (!res.headersSent) {
|
|
199
|
-
res.writeHead(httpStatus("body_too_large"), { "content-type": "application/json", connection: "close" });
|
|
200
|
-
}
|
|
201
|
-
res.end(JSON.stringify(errorBody("body_too_large")));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (e instanceof ChannelPendingError)
|
|
205
|
-
return jsonError(res, "channel_pending");
|
|
206
|
-
if (e instanceof RateLimitedError)
|
|
207
|
-
return jsonError(res, "rate_limited");
|
|
208
|
-
if (e instanceof InvalidPathError)
|
|
209
|
-
return jsonError(res, "invalid_request", e.message);
|
|
210
|
-
if (e instanceof SyntaxError)
|
|
211
|
-
return jsonError(res, "invalid_json");
|
|
212
|
-
process.stderr.write(`[bridge] request error: ${e?.message ?? e}\n`);
|
|
213
|
-
return jsonError(res, "internal");
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
const server = opts.tls
|
|
217
|
-
? createHttps({ cert: opts.tls.cert, key: opts.tls.key }, (req, res) => void handler(req, res))
|
|
218
|
-
: createHttp((req, res) => void handler(req, res));
|
|
219
|
-
return { server, closeAllSseStreams: () => closeSse() };
|
|
220
|
-
}
|