opencode-router 0.11.77 → 0.11.79
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/bridge.js +1456 -0
- package/dist/cli.js +553 -0
- package/dist/config.js +195 -0
- package/dist/db.js +196 -0
- package/dist/events.js +11 -0
- package/dist/health.js +499 -0
- package/dist/logger.js +14 -0
- package/dist/opencode.js +34 -0
- package/dist/slack.js +169 -0
- package/dist/telegram.js +78 -0
- package/dist/text.js +41 -0
- package/package.json +1 -1
package/dist/health.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
export async function startHealthServer(port, getStatus, logger, handlers = {}) {
|
|
3
|
+
const server = http.createServer((req, res) => {
|
|
4
|
+
void (async () => {
|
|
5
|
+
const requestOrigin = req.headers.origin;
|
|
6
|
+
if (requestOrigin) {
|
|
7
|
+
res.setHeader("Access-Control-Allow-Origin", requestOrigin);
|
|
8
|
+
res.setHeader("Vary", "Origin");
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
12
|
+
}
|
|
13
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
14
|
+
const requestHeaders = req.headers["access-control-request-headers"];
|
|
15
|
+
if (Array.isArray(requestHeaders)) {
|
|
16
|
+
res.setHeader("Access-Control-Allow-Headers", requestHeaders.join(", "));
|
|
17
|
+
}
|
|
18
|
+
else if (typeof requestHeaders === "string" && requestHeaders.trim()) {
|
|
19
|
+
res.setHeader("Access-Control-Allow-Headers", requestHeaders);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
23
|
+
}
|
|
24
|
+
if (req.headers["access-control-request-private-network"] === "true") {
|
|
25
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
26
|
+
}
|
|
27
|
+
if (req.method === "OPTIONS") {
|
|
28
|
+
res.writeHead(204);
|
|
29
|
+
res.end();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const pathname = req.url ? new URL(req.url, "http://localhost").pathname : "";
|
|
33
|
+
if (!pathname || pathname === "/" || pathname === "/health") {
|
|
34
|
+
const snapshot = getStatus();
|
|
35
|
+
res.writeHead(snapshot.ok ? 200 : 503, {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
});
|
|
38
|
+
res.end(JSON.stringify(snapshot));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Legacy alias: POST /config/telegram-token -> upsert telegram identity "default".
|
|
42
|
+
if (pathname === "/config/telegram-token" && req.method === "POST") {
|
|
43
|
+
if (!handlers.upsertTelegramIdentity) {
|
|
44
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
45
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let raw = "";
|
|
49
|
+
for await (const chunk of req) {
|
|
50
|
+
raw += chunk.toString();
|
|
51
|
+
if (raw.length > 1024 * 1024) {
|
|
52
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
53
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const payload = JSON.parse(raw || "{}");
|
|
59
|
+
const token = typeof payload.token === "string" ? payload.token.trim() : "";
|
|
60
|
+
if (!token) {
|
|
61
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
62
|
+
res.end(JSON.stringify({ ok: false, error: "Token is required" }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const result = await handlers.upsertTelegramIdentity({ id: "default", token, enabled: true });
|
|
66
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
67
|
+
res.end(JSON.stringify({ ok: true, telegram: result }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const statusRaw = error?.status;
|
|
72
|
+
const status = typeof statusRaw === "number" && statusRaw >= 400 && statusRaw < 600 ? statusRaw : 500;
|
|
73
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
74
|
+
res.end(JSON.stringify({ ok: false, error: String(error instanceof Error ? error.message : error) }));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Legacy alias: POST /config/slack-tokens -> upsert slack identity "default".
|
|
79
|
+
if (pathname === "/config/slack-tokens" && req.method === "POST") {
|
|
80
|
+
if (!handlers.upsertSlackIdentity) {
|
|
81
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
82
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let raw = "";
|
|
86
|
+
for await (const chunk of req) {
|
|
87
|
+
raw += chunk.toString();
|
|
88
|
+
if (raw.length > 1024 * 1024) {
|
|
89
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
90
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const payload = JSON.parse(raw || "{}");
|
|
96
|
+
const botToken = typeof payload.botToken === "string" ? payload.botToken.trim() : "";
|
|
97
|
+
const appToken = typeof payload.appToken === "string" ? payload.appToken.trim() : "";
|
|
98
|
+
if (!botToken || !appToken) {
|
|
99
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
100
|
+
res.end(JSON.stringify({ ok: false, error: "Slack botToken and appToken are required" }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const result = await handlers.upsertSlackIdentity({ id: "default", botToken, appToken, enabled: true });
|
|
104
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
105
|
+
res.end(JSON.stringify({ ok: true, slack: result }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const statusRaw = error?.status;
|
|
110
|
+
const status = typeof statusRaw === "number" && statusRaw >= 400 && statusRaw < 600 ? statusRaw : 500;
|
|
111
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
112
|
+
res.end(JSON.stringify({ ok: false, error: String(error instanceof Error ? error.message : error) }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// GET /identities/telegram
|
|
117
|
+
if (pathname === "/identities/telegram" && req.method === "GET") {
|
|
118
|
+
if (!handlers.listTelegramIdentities) {
|
|
119
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
120
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const result = await handlers.listTelegramIdentities();
|
|
125
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
126
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const statusRaw = error?.status;
|
|
131
|
+
const status = typeof statusRaw === "number" && statusRaw >= 400 && statusRaw < 600 ? statusRaw : 500;
|
|
132
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
133
|
+
res.end(JSON.stringify({ ok: false, error: String(error instanceof Error ? error.message : error) }));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// POST /identities/telegram
|
|
138
|
+
if (pathname === "/identities/telegram" && req.method === "POST") {
|
|
139
|
+
if (!handlers.upsertTelegramIdentity) {
|
|
140
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
141
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let raw = "";
|
|
145
|
+
for await (const chunk of req) {
|
|
146
|
+
raw += chunk.toString();
|
|
147
|
+
if (raw.length > 1024 * 1024) {
|
|
148
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
149
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const payload = JSON.parse(raw || "{}");
|
|
155
|
+
const token = typeof payload.token === "string" ? payload.token.trim() : "";
|
|
156
|
+
const id = typeof payload.id === "string" ? payload.id.trim() : undefined;
|
|
157
|
+
const directory = typeof payload.directory === "string" ? payload.directory.trim() : undefined;
|
|
158
|
+
const enabled = payload.enabled === undefined ? undefined : payload.enabled === true || payload.enabled === "true";
|
|
159
|
+
if (!token) {
|
|
160
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
161
|
+
res.end(JSON.stringify({ ok: false, error: "token is required" }));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const result = await handlers.upsertTelegramIdentity({
|
|
165
|
+
id,
|
|
166
|
+
token,
|
|
167
|
+
...(enabled === undefined ? {} : { enabled }),
|
|
168
|
+
...(directory ? { directory } : {}),
|
|
169
|
+
});
|
|
170
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
171
|
+
res.end(JSON.stringify({ ok: true, telegram: result }));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
176
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// DELETE /identities/telegram/:id
|
|
181
|
+
if (pathname.startsWith("/identities/telegram/") && req.method === "DELETE") {
|
|
182
|
+
if (!handlers.deleteTelegramIdentity) {
|
|
183
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
184
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const id = pathname.slice("/identities/telegram/".length).trim();
|
|
188
|
+
if (!id) {
|
|
189
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
190
|
+
res.end(JSON.stringify({ ok: false, error: "id is required" }));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const result = await handlers.deleteTelegramIdentity(id);
|
|
195
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
196
|
+
res.end(JSON.stringify({ ok: true, telegram: result }));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
201
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// GET /identities/slack
|
|
206
|
+
if (pathname === "/identities/slack" && req.method === "GET") {
|
|
207
|
+
if (!handlers.listSlackIdentities) {
|
|
208
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
209
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const result = await handlers.listSlackIdentities();
|
|
214
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
215
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
220
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// POST /identities/slack
|
|
225
|
+
if (pathname === "/identities/slack" && req.method === "POST") {
|
|
226
|
+
if (!handlers.upsertSlackIdentity) {
|
|
227
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
228
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
let raw = "";
|
|
232
|
+
for await (const chunk of req) {
|
|
233
|
+
raw += chunk.toString();
|
|
234
|
+
if (raw.length > 1024 * 1024) {
|
|
235
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
236
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const payload = JSON.parse(raw || "{}");
|
|
242
|
+
const botToken = typeof payload.botToken === "string" ? payload.botToken.trim() : "";
|
|
243
|
+
const appToken = typeof payload.appToken === "string" ? payload.appToken.trim() : "";
|
|
244
|
+
const id = typeof payload.id === "string" ? payload.id.trim() : undefined;
|
|
245
|
+
const directory = typeof payload.directory === "string" ? payload.directory.trim() : undefined;
|
|
246
|
+
const enabled = payload.enabled === undefined ? undefined : payload.enabled === true || payload.enabled === "true";
|
|
247
|
+
if (!botToken || !appToken) {
|
|
248
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
249
|
+
res.end(JSON.stringify({ ok: false, error: "botToken and appToken are required" }));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const result = await handlers.upsertSlackIdentity({
|
|
253
|
+
id,
|
|
254
|
+
botToken,
|
|
255
|
+
appToken,
|
|
256
|
+
...(enabled === undefined ? {} : { enabled }),
|
|
257
|
+
...(directory ? { directory } : {}),
|
|
258
|
+
});
|
|
259
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
260
|
+
res.end(JSON.stringify({ ok: true, slack: result }));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
265
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// DELETE /identities/slack/:id
|
|
270
|
+
if (pathname.startsWith("/identities/slack/") && req.method === "DELETE") {
|
|
271
|
+
if (!handlers.deleteSlackIdentity) {
|
|
272
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
273
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const id = pathname.slice("/identities/slack/".length).trim();
|
|
277
|
+
if (!id) {
|
|
278
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
279
|
+
res.end(JSON.stringify({ ok: false, error: "id is required" }));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const result = await handlers.deleteSlackIdentity(id);
|
|
284
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
285
|
+
res.end(JSON.stringify({ ok: true, slack: result }));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
290
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// GET /config/groups - get current groups setting
|
|
295
|
+
if (pathname === "/config/groups" && req.method === "GET") {
|
|
296
|
+
if (!handlers.getGroupsEnabled) {
|
|
297
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
298
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const groupsEnabled = handlers.getGroupsEnabled();
|
|
302
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
303
|
+
res.end(JSON.stringify({ ok: true, groupsEnabled }));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// POST /config/groups - set groups enabled
|
|
307
|
+
if (pathname === "/config/groups" && req.method === "POST") {
|
|
308
|
+
if (!handlers.setGroupsEnabled) {
|
|
309
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
310
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
let raw = "";
|
|
314
|
+
for await (const chunk of req) {
|
|
315
|
+
raw += chunk.toString();
|
|
316
|
+
if (raw.length > 1024 * 1024) {
|
|
317
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
318
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const payload = JSON.parse(raw || "{}");
|
|
324
|
+
const enabled = payload.enabled === true || payload.enabled === "true";
|
|
325
|
+
const result = await handlers.setGroupsEnabled(enabled);
|
|
326
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
327
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
332
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (pathname === "/bindings" && req.method === "GET") {
|
|
337
|
+
if (!handlers.listBindings) {
|
|
338
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
339
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const parsed = req.url ? new URL(req.url, "http://localhost") : null;
|
|
344
|
+
const channel = typeof parsed?.searchParams.get("channel") === "string" ? parsed?.searchParams.get("channel") ?? undefined : undefined;
|
|
345
|
+
const identityId = typeof parsed?.searchParams.get("identityId") === "string" ? parsed?.searchParams.get("identityId") ?? undefined : undefined;
|
|
346
|
+
const result = await handlers.listBindings({
|
|
347
|
+
...(channel?.trim() ? { channel: channel.trim() } : {}),
|
|
348
|
+
...(identityId?.trim() ? { identityId: identityId.trim() } : {}),
|
|
349
|
+
});
|
|
350
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
351
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
356
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (pathname === "/bindings" && req.method === "POST") {
|
|
361
|
+
if (!handlers.setBinding && !handlers.clearBinding) {
|
|
362
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
363
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
let raw = "";
|
|
367
|
+
for await (const chunk of req) {
|
|
368
|
+
raw += chunk.toString();
|
|
369
|
+
if (raw.length > 1024 * 1024) {
|
|
370
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
371
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const payload = JSON.parse(raw || "{}");
|
|
377
|
+
const channel = typeof payload.channel === "string" ? payload.channel.trim() : "";
|
|
378
|
+
const identityId = typeof payload.identityId === "string" ? payload.identityId.trim() : "";
|
|
379
|
+
const peerId = typeof payload.peerId === "string" ? payload.peerId.trim() : "";
|
|
380
|
+
const directory = typeof payload.directory === "string" ? payload.directory.trim() : "";
|
|
381
|
+
if (!channel || !peerId) {
|
|
382
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
383
|
+
res.end(JSON.stringify({ ok: false, error: "channel and peerId are required" }));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!directory) {
|
|
387
|
+
if (!handlers.clearBinding) {
|
|
388
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
389
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
await handlers.clearBinding({ channel, identityId: identityId || undefined, peerId });
|
|
393
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
394
|
+
res.end(JSON.stringify({ ok: true }));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (!handlers.setBinding) {
|
|
398
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
399
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
await handlers.setBinding({ channel, identityId: identityId || undefined, peerId, directory });
|
|
403
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
404
|
+
res.end(JSON.stringify({ ok: true }));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
409
|
+
res.end(JSON.stringify({ ok: false, error: String(error) }));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (pathname === "/send" && req.method === "POST") {
|
|
414
|
+
if (!handlers.sendMessage) {
|
|
415
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
416
|
+
res.end(JSON.stringify({ ok: false, error: "Not supported" }));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
let raw = "";
|
|
420
|
+
for await (const chunk of req) {
|
|
421
|
+
raw += chunk.toString();
|
|
422
|
+
if (raw.length > 1024 * 1024) {
|
|
423
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
424
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const payload = JSON.parse(raw || "{}");
|
|
430
|
+
const channel = typeof payload.channel === "string" ? payload.channel.trim() : "";
|
|
431
|
+
const identityId = typeof payload.identityId === "string" ? payload.identityId.trim() : "";
|
|
432
|
+
const directory = typeof payload.directory === "string" ? payload.directory.trim() : "";
|
|
433
|
+
const peerId = typeof payload.peerId === "string" ? payload.peerId.trim() : "";
|
|
434
|
+
const autoBind = payload.autoBind === true;
|
|
435
|
+
const text = typeof payload.text === "string" ? payload.text : "";
|
|
436
|
+
if (!channel || !text.trim() || (!directory && !peerId)) {
|
|
437
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
438
|
+
res.end(JSON.stringify({ ok: false, error: "channel, text, and either directory or peerId are required" }));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const result = await handlers.sendMessage({
|
|
442
|
+
channel,
|
|
443
|
+
...(identityId ? { identityId } : {}),
|
|
444
|
+
...(directory ? { directory } : {}),
|
|
445
|
+
...(peerId ? { peerId } : {}),
|
|
446
|
+
...(autoBind ? { autoBind: true } : {}),
|
|
447
|
+
text,
|
|
448
|
+
});
|
|
449
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
450
|
+
res.end(JSON.stringify({ ok: true, ...result }));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
const statusRaw = error?.status;
|
|
455
|
+
const status = typeof statusRaw === "number" && statusRaw >= 400 && statusRaw < 600 ? statusRaw : 500;
|
|
456
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
457
|
+
res.end(JSON.stringify({ ok: false, error: String(error instanceof Error ? error.message : error) }));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
462
|
+
res.end(JSON.stringify({ ok: false, error: "Not found" }));
|
|
463
|
+
})().catch((error) => {
|
|
464
|
+
logger.error({ error }, "health server request failed");
|
|
465
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
466
|
+
res.end(JSON.stringify({ ok: false, error: "Internal error" }));
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
const host = (process.env.OPENCODE_ROUTER_HEALTH_HOST ?? "").trim() || "127.0.0.1";
|
|
470
|
+
try {
|
|
471
|
+
await new Promise((resolve, reject) => {
|
|
472
|
+
const onError = (error) => {
|
|
473
|
+
server.removeListener("listening", onListening);
|
|
474
|
+
reject(error);
|
|
475
|
+
};
|
|
476
|
+
const onListening = () => {
|
|
477
|
+
server.removeListener("error", onError);
|
|
478
|
+
resolve();
|
|
479
|
+
};
|
|
480
|
+
server.once("error", onError);
|
|
481
|
+
server.once("listening", onListening);
|
|
482
|
+
server.listen(port, host);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
const code = error?.code;
|
|
487
|
+
if (code === "EADDRINUSE") {
|
|
488
|
+
throw new Error(`Failed to start health server on ${host}:${port}. Port is in use. ` +
|
|
489
|
+
`Set OPENCODE_ROUTER_HEALTH_PORT, OPENCODE_ROUTER_HEALTH_PORT, or PORT to a free port.`);
|
|
490
|
+
}
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
const address = server.address();
|
|
494
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
495
|
+
logger.info({ host, port: actualPort }, "health server listening");
|
|
496
|
+
return () => {
|
|
497
|
+
server.close();
|
|
498
|
+
};
|
|
499
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pino from "pino";
|
|
4
|
+
export function createLogger(level, options) {
|
|
5
|
+
if (options?.logFile) {
|
|
6
|
+
fs.mkdirSync(path.dirname(options.logFile), { recursive: true });
|
|
7
|
+
const destination = pino.destination({ dest: options.logFile, sync: false });
|
|
8
|
+
return pino({ level, base: undefined }, destination);
|
|
9
|
+
}
|
|
10
|
+
return pino({
|
|
11
|
+
level,
|
|
12
|
+
base: undefined,
|
|
13
|
+
});
|
|
14
|
+
}
|
package/dist/opencode.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
3
|
+
export function createClient(config, directory) {
|
|
4
|
+
const headers = {};
|
|
5
|
+
if (config.opencodeUsername && config.opencodePassword) {
|
|
6
|
+
const token = Buffer.from(`${config.opencodeUsername}:${config.opencodePassword}`).toString("base64");
|
|
7
|
+
headers.Authorization = `Basic ${token}`;
|
|
8
|
+
}
|
|
9
|
+
return createOpencodeClient({
|
|
10
|
+
baseUrl: config.opencodeUrl,
|
|
11
|
+
directory: directory ?? config.opencodeDirectory,
|
|
12
|
+
headers: Object.keys(headers).length ? headers : undefined,
|
|
13
|
+
responseStyle: "data",
|
|
14
|
+
throwOnError: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function buildPermissionRules(mode) {
|
|
18
|
+
if (mode === "deny") {
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
permission: "*",
|
|
22
|
+
pattern: "*",
|
|
23
|
+
action: "deny",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
permission: "*",
|
|
30
|
+
pattern: "*",
|
|
31
|
+
action: "allow",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
}
|