@a-t-h-i/bot-lobby 0.6.9 → 0.6.10
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 +7 -0
- package/package.json +2 -1
- package/src/excalidraw/client.ts +62 -3
package/README.md
CHANGED
|
@@ -388,6 +388,13 @@ deletion), and one call draws at most 100 shapes and removes at most 50.
|
|
|
388
388
|
- Rooms are Excalidraw's own collaboration protocol, end-to-end encrypted with the
|
|
389
389
|
key in the link, over `oss-collab.excalidraw.com`. A self-hosted collaboration
|
|
390
390
|
server is used when `BOT_LOBBY_EXCALIDRAW_SERVER` names it.
|
|
391
|
+
- **When `t` says it could not reach the server**, the reason in brackets says
|
|
392
|
+
why: a name that did not resolve (the network or DNS), a refused or timed-out
|
|
393
|
+
connection, a certificate a firewall replaced (point `NODE_EXTRA_CA_CERTS` at
|
|
394
|
+
its certificate), or a refusal with an HTTP status. Where only a proxy lets
|
|
395
|
+
traffic out, set `HTTPS_PROXY` (`NO_PROXY` exempts hosts); agents then join
|
|
396
|
+
through it, and the message names it. A network that blocks websockets but not
|
|
397
|
+
HTTPS still works: the seat falls back to long-polling, as a browser does.
|
|
391
398
|
|
|
392
399
|
Common keys: `tab` switches tabs, `esc` browses (arrows, single-key
|
|
393
400
|
commands), `ctrl+f` searches, `ctrl+s` saves the plan, `alt+o` browses
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a-t-h-i/bot-lobby",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "Structured multi-agent software engineering orchestrator for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"typescript": "^5.7.0"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"https-proxy-agent": "^9.1.0",
|
|
59
60
|
"socket.io-client": "4.8.4"
|
|
60
61
|
}
|
|
61
62
|
}
|
package/src/excalidraw/client.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* itself. And it does not draw while nobody else is in the room, because
|
|
12
12
|
* nothing keeps a scene for a room but the browsers in it.
|
|
13
13
|
*/
|
|
14
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
14
15
|
import { io, type Socket } from "socket.io-client";
|
|
15
16
|
import { DEFAULT_SERVER, seal, unseal, type RoomLink } from "./room.ts";
|
|
16
17
|
import { describeScene, mergeElements, plainText, planDraw, sceneBounds, visible, type DrawOutcome, type DrawRequest, type Scene, type SceneElement } from "./scene.ts";
|
|
@@ -51,7 +52,63 @@ const SCENE_WAIT_MS = 5000;
|
|
|
51
52
|
const MAX_MESSAGE_CHARS = 900_000;
|
|
52
53
|
|
|
53
54
|
function defaultConnect(server: string): SocketLike {
|
|
54
|
-
|
|
55
|
+
const proxy = proxyFor(server);
|
|
56
|
+
// Where a network lets plain HTTPS through but not a websocket, the seat falls back to long-polling, as a browser does.
|
|
57
|
+
// socket.io types `agent` for browsers (string | boolean); in Node it is passed on to the http and ws requests as an http.Agent.
|
|
58
|
+
const agent = proxy ? { agent: new HttpsProxyAgent(proxy) as unknown as string } : {};
|
|
59
|
+
return io(server, { transports: ["websocket", "polling"], tryAllTransports: true, autoUnref: true, timeout: 8000, ...agent });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The proxy the environment names for this server (`HTTPS_PROXY`, `HTTP_PROXY`,
|
|
64
|
+
* `ALL_PROXY`, in either case), unless `NO_PROXY` exempts its host. Node's own
|
|
65
|
+
* sockets ignore these variables, so without this a seat on a network that only
|
|
66
|
+
* lets traffic out through a proxy never reaches the server.
|
|
67
|
+
*/
|
|
68
|
+
export function proxyFor(server: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
69
|
+
let url: URL;
|
|
70
|
+
try {
|
|
71
|
+
url = new URL(server);
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const pick = (...names: string[]) => names.map((name) => env[name]?.trim()).find((value) => value);
|
|
76
|
+
const proxy = url.protocol === "http:" ? pick("http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY") : pick("https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY");
|
|
77
|
+
if (!proxy) return undefined;
|
|
78
|
+
const host = url.hostname.toLowerCase();
|
|
79
|
+
const exempt = (pick("no_proxy", "NO_PROXY") ?? "").split(/[\s,]+/).filter(Boolean);
|
|
80
|
+
for (const entry of exempt) {
|
|
81
|
+
if (entry === "*") return undefined;
|
|
82
|
+
const name = entry.toLowerCase().replace(/:\d+$/, "").replace(/^\*?\./, "");
|
|
83
|
+
if (host === name || host.endsWith(`.${name}`)) return undefined;
|
|
84
|
+
}
|
|
85
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(proxy) ? proxy : `http://${proxy}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Why a connection failed, in words a person can act on. socket.io reports
|
|
90
|
+
* every transport failure as "websocket error" or "xhr poll error" and keeps
|
|
91
|
+
* the cause (a refused upgrade, an unknown host, a certificate) underneath.
|
|
92
|
+
*/
|
|
93
|
+
export function connectFailure(error: unknown): string {
|
|
94
|
+
const outer = error as { message?: string; description?: unknown; context?: { responseText?: unknown } } | undefined;
|
|
95
|
+
const description = outer?.description as { message?: unknown } | number | string | undefined;
|
|
96
|
+
// A websocket keeps its cause in `description`; long-polling keeps the HTTP status there (0 when no response came) and the cause in the request's text.
|
|
97
|
+
const polled = typeof outer?.context?.responseText === "string" ? outer.context.responseText.split("\n")[0]!.replace(/^Error: /, "") : "";
|
|
98
|
+
const text =
|
|
99
|
+
typeof description === "object" && typeof description?.message === "string" && description.message
|
|
100
|
+
? description.message
|
|
101
|
+
: typeof description === "number" && description > 0
|
|
102
|
+
? `Unexpected server response: ${description}`
|
|
103
|
+
: typeof description === "string" && description
|
|
104
|
+
? description
|
|
105
|
+
: polled || outer?.message || "";
|
|
106
|
+
const status = /Unexpected server response: (\d{3})/.exec(text)?.[1];
|
|
107
|
+
if (status) return `the connection was refused with HTTP ${status}`;
|
|
108
|
+
if (/ENOTFOUND|EAI_AGAIN/.test(text)) return `${text}: the server's name did not resolve, so check the network or DNS`;
|
|
109
|
+
if (/ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH/.test(text)) return `${text}: check the network, or set HTTPS_PROXY if it only lets traffic out through a proxy`;
|
|
110
|
+
if (/certificate|CERT_|SSL|TLS/i.test(text)) return `${text}: a proxy or firewall may be intercepting HTTPS; NODE_EXTRA_CA_CERTS can point at its certificate`;
|
|
111
|
+
return text;
|
|
55
112
|
}
|
|
56
113
|
|
|
57
114
|
export function collaborationServer(env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -122,11 +179,13 @@ export class ExcalidrawRoom {
|
|
|
122
179
|
done = true;
|
|
123
180
|
this.opening = undefined;
|
|
124
181
|
this.close();
|
|
125
|
-
|
|
182
|
+
// Only the proxy's host is named: its URL may carry a password.
|
|
183
|
+
const via = this.options.connect ? undefined : proxyFor(server);
|
|
184
|
+
reject(new Error(`could not reach the Excalidraw collaboration server ${server}${via ? ` through the proxy ${new URL(via).host}` : ""}${lastError ? ` (${lastError})` : ""}`));
|
|
126
185
|
}, this.options.connectTimeoutMs ?? CONNECT_TIMEOUT_MS),
|
|
127
186
|
);
|
|
128
187
|
socket.on("connect_error", (error: Error) => {
|
|
129
|
-
lastError = error
|
|
188
|
+
lastError = connectFailure(error) || lastError;
|
|
130
189
|
});
|
|
131
190
|
// Every (re)connection is greeted with init-room; the seat answers by joining.
|
|
132
191
|
socket.on("init-room", () => {
|