agent-yes 1.120.0 → 1.121.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/dist/SUPPORTED_CLIS-CegJgoEf.js +8 -0
- package/dist/{SUPPORTED_CLIS-BD8zWc7O.js → SUPPORTED_CLIS-O57LGUEG.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-6RqphTG0.js → serve-D2czcYNC.js} +18 -9
- package/dist/{setup-B5TPF2MV.js → setup-f1FIFcZm.js} +2 -2
- package/dist/share-B6QVr5D1.js +522 -0
- package/dist/{subcommands-CCgzXvQ-.js → subcommands-CzpZQHO6.js} +3 -3
- package/dist/{subcommands-BVcos4UW.js → subcommands-DobVXouH.js} +1 -1
- package/dist/{ts-C78N0K4F.js → ts-D91dm1E0.js} +2 -2
- package/dist/{versionChecker-CYZtJKMG.js → versionChecker-CAtpgnoQ.js} +2 -2
- package/lab/ui/blog/e2ee-share-links/index.html +299 -0
- package/lab/ui/e2e.d.ts +47 -0
- package/lab/ui/e2e.js +245 -0
- package/lab/ui/index.html +180 -26
- package/package.json +6 -2
- package/scripts/check-e2e.ts +40 -0
- package/ts/e2e-crypto.spec.ts +235 -0
- package/ts/serve.ts +31 -9
- package/ts/share.ts +205 -32
- package/dist/SUPPORTED_CLIS-CoYWGWbP.js +0 -8
- package/dist/share-B7J79Wq9.js +0 -254
package/ts/serve.ts
CHANGED
|
@@ -313,8 +313,9 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
313
313
|
process.stdout.write(` ay serve uninstall # remove daemon\n`);
|
|
314
314
|
if (webrtcish) {
|
|
315
315
|
process.stdout.write(
|
|
316
|
-
`\nthe WebRTC share link
|
|
317
|
-
`
|
|
316
|
+
`\nthe WebRTC share link carries a secret, so the daemon does NOT log it —\n` +
|
|
317
|
+
`read it from ~/.agent-yes/.share-link (mode 0600). The room persists in\n` +
|
|
318
|
+
`~/.agent-yes/.share-room, so the link survives restarts.\n`,
|
|
318
319
|
);
|
|
319
320
|
}
|
|
320
321
|
}
|
|
@@ -1002,6 +1003,8 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1002
1003
|
return serveUiFile("room-client.js", "text/javascript; charset=utf-8");
|
|
1003
1004
|
if (req.method === "GET" && p === "/console-logic.js")
|
|
1004
1005
|
return serveUiFile("console-logic.js", "text/javascript; charset=utf-8");
|
|
1006
|
+
if (req.method === "GET" && p === "/e2e.js")
|
|
1007
|
+
return serveUiFile("e2e.js", "text/javascript; charset=utf-8");
|
|
1005
1008
|
if (req.method === "GET" && p === "/favicon.ico") return new Response(null, { status: 204 });
|
|
1006
1009
|
return apiFetch(req);
|
|
1007
1010
|
};
|
|
@@ -1063,18 +1066,37 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1063
1066
|
const { startShare, loadOrCreateShareRoom } = await import("./share.ts");
|
|
1064
1067
|
// No explicit webrtc:// URL → reuse the persisted room (minted once and
|
|
1065
1068
|
// saved like the serve token), so the link is stable across restarts.
|
|
1066
|
-
const { link, close } = await startShare({
|
|
1069
|
+
const { room, link, close } = await startShare({
|
|
1067
1070
|
url: explicitUrl ?? (await loadOrCreateShareRoom()),
|
|
1068
1071
|
localFetch: apiFetch,
|
|
1069
1072
|
apiToken: token,
|
|
1070
1073
|
});
|
|
1071
1074
|
closeShare = close;
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1075
|
+
const persistNote = explicitUrl
|
|
1076
|
+
? "\n"
|
|
1077
|
+
: ` (persistent room — same link across restarts; delete ~/.agent-yes/.share-room to rotate)\n\n`;
|
|
1078
|
+
if (process.stdout.isTTY) {
|
|
1079
|
+
process.stdout.write(
|
|
1080
|
+
`${wantHttp ? "\n" : ""}shared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n` +
|
|
1081
|
+
persistNote,
|
|
1082
|
+
);
|
|
1083
|
+
} else {
|
|
1084
|
+
// Non-TTY (daemon/journal/CI): the link embeds the room secret S, so never
|
|
1085
|
+
// write it to a log stream. Stash it in a 0600 file and point there instead.
|
|
1086
|
+
const linkFile = path.join(
|
|
1087
|
+
process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes"),
|
|
1088
|
+
".share-link",
|
|
1089
|
+
);
|
|
1090
|
+
try {
|
|
1091
|
+
await writeFile(linkFile, link + "\n", { mode: 0o600 });
|
|
1092
|
+
} catch {
|
|
1093
|
+
/* best effort */
|
|
1094
|
+
}
|
|
1095
|
+
process.stdout.write(
|
|
1096
|
+
`${wantHttp ? "\n" : ""}shared over WebRTC · room ${room} — the link carries a secret, so it is NOT logged.\n` +
|
|
1097
|
+
` read it from ${linkFile} (mode 0600); delete ~/.agent-yes/.share-room to rotate\n\n`,
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1078
1100
|
} catch (e) {
|
|
1079
1101
|
process.stderr.write(`ay serve --webrtc failed: ${(e as Error).message}\n`);
|
|
1080
1102
|
if (!wantHttp) return 1; // nothing else is running
|
package/ts/share.ts
CHANGED
|
@@ -8,10 +8,24 @@ import { randomBytes } from "crypto";
|
|
|
8
8
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import path from "path";
|
|
11
|
+
import {
|
|
12
|
+
CONFIRM_TIMEOUT_MS,
|
|
13
|
+
FLAG_CONFIRM,
|
|
14
|
+
MARKER,
|
|
15
|
+
MAX_CHUNK,
|
|
16
|
+
computeTranscriptHash,
|
|
17
|
+
deriveAuthToken,
|
|
18
|
+
deriveDirKeys,
|
|
19
|
+
open as e2eOpen,
|
|
20
|
+
seal as e2eSeal,
|
|
21
|
+
packEnvelope,
|
|
22
|
+
parseSecret,
|
|
23
|
+
randomHex,
|
|
24
|
+
unpackEnvelope,
|
|
25
|
+
} from "../lab/ui/e2e.js";
|
|
11
26
|
|
|
12
27
|
const SUB = "ay-signal-1";
|
|
13
28
|
const ICE = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
14
|
-
const MAX_CHUNK = 15_000; // keep DataChannel messages under the SCTP limit
|
|
15
29
|
const DEFAULT_SIGHOST = "s.agent-yes.com";
|
|
16
30
|
|
|
17
31
|
export interface ShareOpts {
|
|
@@ -38,13 +52,20 @@ function shareRoomPath(): string {
|
|
|
38
52
|
export async function loadOrCreateShareRoom(sighost = DEFAULT_SIGHOST): Promise<string> {
|
|
39
53
|
try {
|
|
40
54
|
const url = (await readFile(shareRoomPath(), "utf-8")).trim();
|
|
41
|
-
if (url.startsWith("webrtc://"))
|
|
55
|
+
if (url.startsWith("webrtc://")) {
|
|
56
|
+
// A v2 (encrypted) room carries the e1. marker on its secret — reuse it.
|
|
57
|
+
// A legacy markerless room is rotated to a fresh encrypted room below: the
|
|
58
|
+
// signaling DO has pinned the old room to its plaintext token, so we must
|
|
59
|
+
// mint a NEW room name. This is a one-time, deliberate security upgrade —
|
|
60
|
+
// old share links stop working; re-open the new printed link.
|
|
61
|
+
if (parseShareUrl(url).token.startsWith(MARKER)) return url;
|
|
62
|
+
}
|
|
42
63
|
} catch {
|
|
43
64
|
/* not yet minted */
|
|
44
65
|
}
|
|
45
66
|
const room = "r" + randomBytes(3).toString("hex");
|
|
46
|
-
const
|
|
47
|
-
const url = `webrtc://${room}:${
|
|
67
|
+
const s = randomBytes(32).toString("hex");
|
|
68
|
+
const url = `webrtc://${room}:${MARKER}${s}@${sighost}`;
|
|
48
69
|
await mkdir(path.dirname(shareRoomPath()), { recursive: true });
|
|
49
70
|
await writeFile(shareRoomPath(), url, { mode: 0o600 });
|
|
50
71
|
return url;
|
|
@@ -146,17 +167,46 @@ export async function startShare(
|
|
|
146
167
|
? parseShareUrl(opts.url)
|
|
147
168
|
: {
|
|
148
169
|
room: "r" + randomBytes(3).toString("hex"),
|
|
149
|
-
token: randomBytes(32).toString("hex")
|
|
170
|
+
token: `${MARKER}${randomBytes(32).toString("hex")}`,
|
|
150
171
|
host: sighost,
|
|
151
172
|
};
|
|
152
173
|
|
|
174
|
+
// E2E: the URL secret S splits into authToken (the only value the server sees,
|
|
175
|
+
// for room matching) and per-connection AES keys the server never sees. We
|
|
176
|
+
// refuse to host a legacy plaintext room — old rooms are auto-rotated to v2 by
|
|
177
|
+
// loadOrCreateShareRoom (delete ~/.agent-yes/.share-room to force a rotation).
|
|
178
|
+
const { s: S, v2 } = parseSecret(token);
|
|
179
|
+
if (!v2) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
"refusing to host an unencrypted room — delete ~/.agent-yes/.share-room to rotate to an encrypted link",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const authToken = await deriveAuthToken(S, room, host);
|
|
185
|
+
|
|
153
186
|
const RTCPeerConnection = await importRTC();
|
|
154
187
|
const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
|
|
155
188
|
const ui = host === "s.agent-yes.com" ? "https://agent-yes.com" : "http://localhost:7778";
|
|
156
189
|
const suffix = host === "s.agent-yes.com" ? "" : "@" + host;
|
|
157
|
-
const link = `${ui}/#${room}:${
|
|
190
|
+
const link = `${ui}/#${room}:${MARKER}${S}${suffix}`;
|
|
158
191
|
|
|
159
|
-
type Peer = {
|
|
192
|
+
type Peer = {
|
|
193
|
+
pc: any;
|
|
194
|
+
aborts: Map<string, AbortController>;
|
|
195
|
+
send: { sendCtr: bigint };
|
|
196
|
+
recv: { lastSeen: bigint };
|
|
197
|
+
th?: Uint8Array;
|
|
198
|
+
keyH2C?: CryptoKey; // host encrypts with H2C, decrypts with C2H
|
|
199
|
+
keyC2H?: CryptoKey;
|
|
200
|
+
keysReady: Promise<void>;
|
|
201
|
+
resolveKeys: () => void;
|
|
202
|
+
myNonce: string;
|
|
203
|
+
confirmedIn: boolean; // peer echoed our nonce
|
|
204
|
+
confirmedOut: boolean; // we echoed peer's nonce
|
|
205
|
+
confirmed: boolean;
|
|
206
|
+
confirmTimer?: ReturnType<typeof setTimeout>;
|
|
207
|
+
recvChain: Promise<void>; // serialize decrypts so the replay counter stays ordered
|
|
208
|
+
sendChain: Promise<void>; // serialize seals so wire order == counter order
|
|
209
|
+
};
|
|
160
210
|
const peers = new Map<string, Peer>();
|
|
161
211
|
let closed = false; // set by close(); stops signaling reconnect + new peers
|
|
162
212
|
let currentWs: WebSocket | undefined; // the live rendezvous socket, for close()
|
|
@@ -167,7 +217,7 @@ export async function startShare(
|
|
|
167
217
|
currentWs = ws;
|
|
168
218
|
let ready = false;
|
|
169
219
|
ws.onopen = () => {
|
|
170
|
-
ws.send(JSON.stringify({ type: "hello", role: "host", token }));
|
|
220
|
+
ws.send(JSON.stringify({ type: "hello", role: "host", v: 2, token: authToken }));
|
|
171
221
|
ready = true;
|
|
172
222
|
onReady();
|
|
173
223
|
};
|
|
@@ -175,17 +225,44 @@ export async function startShare(
|
|
|
175
225
|
if (closed) return;
|
|
176
226
|
const m = JSON.parse(ev.data as string);
|
|
177
227
|
if (m.type === "peer-join") startPeer(ws, m.peer);
|
|
178
|
-
else if (m.type === "answer")
|
|
179
|
-
|
|
180
|
-
|
|
228
|
+
else if (m.type === "answer") {
|
|
229
|
+
const peer = peers.get(m.from);
|
|
230
|
+
if (!peer) return;
|
|
231
|
+
try {
|
|
232
|
+
await peer.pc.setRemoteDescription({ type: "answer", sdp: m.sdp });
|
|
233
|
+
// Derive per-connection keys the moment both descriptions are stable —
|
|
234
|
+
// before the DataChannel can open and deliver a frame. Host's offer is
|
|
235
|
+
// local, the browser's answer is remote.
|
|
236
|
+
peer.th = await computeTranscriptHash(
|
|
237
|
+
peer.pc.localDescription.sdp,
|
|
238
|
+
peer.pc.remoteDescription.sdp,
|
|
239
|
+
);
|
|
240
|
+
const { keyH2C, keyC2H } = await deriveDirKeys(S, peer.th);
|
|
241
|
+
peer.keyH2C = keyH2C;
|
|
242
|
+
peer.keyC2H = keyC2H;
|
|
243
|
+
peer.resolveKeys();
|
|
244
|
+
} catch {
|
|
245
|
+
closePeer(m.from);
|
|
246
|
+
}
|
|
247
|
+
} else if (m.type === "candidate")
|
|
181
248
|
await peers
|
|
182
249
|
.get(m.from)
|
|
183
250
|
?.pc.addIceCandidate(m.candidate)
|
|
184
251
|
.catch(() => {});
|
|
185
252
|
else if (m.type === "peer-leave") closePeer(m.peer);
|
|
186
253
|
};
|
|
187
|
-
ws.onclose = () => {
|
|
254
|
+
ws.onclose = (ev: any) => {
|
|
188
255
|
if (closed) return; // shutting down — don't resurrect the rendezvous
|
|
256
|
+
// The signaling server pins a room to its first host's authToken. A 1008
|
|
257
|
+
// means a different generation already owns this room — don't hot-loop;
|
|
258
|
+
// tell the operator to rotate. (Secret-free message.)
|
|
259
|
+
if (ev?.code === 1008) {
|
|
260
|
+
closed = true;
|
|
261
|
+
process.stderr.write(
|
|
262
|
+
"[share] room rejected by signaling server — delete ~/.agent-yes/.share-room to rotate the room\n",
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
189
266
|
// Keep established WebRTC peers; just re-establish the rendezvous so new
|
|
190
267
|
// browsers can still join. Backoff a little to avoid hot-looping.
|
|
191
268
|
setTimeout(() => connectSignaling(() => {}), ready ? 1500 : 4000);
|
|
@@ -196,8 +273,23 @@ export async function startShare(
|
|
|
196
273
|
|
|
197
274
|
function startPeer(ws: WebSocket, peerId: string) {
|
|
198
275
|
const pc = new RTCPeerConnection({ iceServers: ICE });
|
|
199
|
-
|
|
200
|
-
|
|
276
|
+
let resolveKeys!: () => void;
|
|
277
|
+
const keysReady = new Promise<void>((r) => (resolveKeys = r));
|
|
278
|
+
const peer: Peer = {
|
|
279
|
+
pc,
|
|
280
|
+
aborts: new Map<string, AbortController>(),
|
|
281
|
+
send: { sendCtr: 0n },
|
|
282
|
+
recv: { lastSeen: -1n },
|
|
283
|
+
keysReady,
|
|
284
|
+
resolveKeys,
|
|
285
|
+
myNonce: randomHex(16),
|
|
286
|
+
confirmedIn: false,
|
|
287
|
+
confirmedOut: false,
|
|
288
|
+
confirmed: false,
|
|
289
|
+
recvChain: Promise.resolve(),
|
|
290
|
+
sendChain: Promise.resolve(),
|
|
291
|
+
};
|
|
292
|
+
peers.set(peerId, peer);
|
|
201
293
|
pc.onicecandidate = (e: any) => {
|
|
202
294
|
if (e.candidate)
|
|
203
295
|
ws.send(JSON.stringify({ type: "candidate", to: peerId, candidate: e.candidate }));
|
|
@@ -206,7 +298,26 @@ export async function startShare(
|
|
|
206
298
|
if (["failed", "closed", "disconnected"].includes(pc.connectionState)) closePeer(peerId);
|
|
207
299
|
};
|
|
208
300
|
const dc = pc.createDataChannel("api");
|
|
209
|
-
dc.
|
|
301
|
+
dc.binaryType = "arraybuffer";
|
|
302
|
+
dc.onopen = async () => {
|
|
303
|
+
try {
|
|
304
|
+
await peer.keysReady; // keys derived in the answer handler
|
|
305
|
+
// Open the mandatory bidirectional key-confirmation handshake. Nothing
|
|
306
|
+
// the peer sends is acted on until BOTH directions confirm (see onFrame).
|
|
307
|
+
enqueueSeal(peerId, dc, peer, FLAG_CONFIRM, { t: "confirm", nonce: peer.myNonce });
|
|
308
|
+
peer.confirmTimer = setTimeout(() => {
|
|
309
|
+
if (!peer.confirmed) closePeer(peerId);
|
|
310
|
+
}, CONFIRM_TIMEOUT_MS);
|
|
311
|
+
} catch {
|
|
312
|
+
closePeer(peerId);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
// Serialize decrypts: WebCrypto open() is async, and a reliable+ordered
|
|
316
|
+
// channel must be processed in order or the monotonic replay check would
|
|
317
|
+
// spuriously reject a reordered await.
|
|
318
|
+
dc.onmessage = (e: any) => {
|
|
319
|
+
peer.recvChain = peer.recvChain.then(() => onFrame(peerId, dc, peer, e.data)).catch(() => {});
|
|
320
|
+
};
|
|
210
321
|
pc.createOffer()
|
|
211
322
|
.then((o: any) => pc.setLocalDescription(o))
|
|
212
323
|
.then(() =>
|
|
@@ -217,6 +328,7 @@ export async function startShare(
|
|
|
217
328
|
function closePeer(peerId: string) {
|
|
218
329
|
const p = peers.get(peerId);
|
|
219
330
|
if (!p) return;
|
|
331
|
+
if (p.confirmTimer) clearTimeout(p.confirmTimer);
|
|
220
332
|
for (const a of p.aborts.values()) a.abort();
|
|
221
333
|
try {
|
|
222
334
|
p.pc.close();
|
|
@@ -226,29 +338,72 @@ export async function startShare(
|
|
|
226
338
|
peers.delete(peerId);
|
|
227
339
|
}
|
|
228
340
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
341
|
+
// Seal an envelope and send it, serialized per peer so the wire order matches
|
|
342
|
+
// the nonce-counter order (so the receiver's monotonic check never trips).
|
|
343
|
+
function enqueueSeal(peerId: string, dc: any, peer: Peer, flags: number, obj: object) {
|
|
344
|
+
peer.sendChain = peer.sendChain.then(async () => {
|
|
345
|
+
if (dc.readyState !== "open" || !peer.keyH2C || !peer.th) return;
|
|
346
|
+
let frame: ArrayBuffer;
|
|
347
|
+
try {
|
|
348
|
+
frame = await e2eSeal(peer.keyH2C, peer.send, flags, peer.th, packEnvelope(obj));
|
|
349
|
+
} catch {
|
|
350
|
+
closePeer(peerId); // counter overflow — fail closed
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
dc.send(frame);
|
|
355
|
+
} catch {
|
|
356
|
+
/* peer vanished mid-send; dropping the frame is correct */
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return peer.sendChain;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Decrypt + route one inbound frame. Fail-closed: any decryption failure,
|
|
363
|
+
// replay, pre-confirmation app frame, or string frame closes the peer.
|
|
364
|
+
async function onFrame(peerId: string, dc: any, peer: Peer, data: any) {
|
|
365
|
+
if (!peers.has(peerId)) return;
|
|
366
|
+
if (typeof data === "string" || !peer.keyC2H || !peer.th) return closePeer(peerId);
|
|
367
|
+
let env: any;
|
|
235
368
|
try {
|
|
236
|
-
|
|
369
|
+
const { plaintext } = await e2eOpen(peer.keyC2H, data, peer.th, peer.recv);
|
|
370
|
+
env = unpackEnvelope(plaintext);
|
|
237
371
|
} catch {
|
|
238
|
-
|
|
372
|
+
return closePeer(peerId); // bad version/epoch/tag/AAD or replay
|
|
373
|
+
}
|
|
374
|
+
if (!peer.confirmed) {
|
|
375
|
+
if (!env || env.t !== "confirm") return closePeer(peerId);
|
|
376
|
+
if (typeof env.nonce === "string" && !peer.confirmedOut) {
|
|
377
|
+
// Flush our echo before marking confirmed-out (so no app frame is acted on
|
|
378
|
+
// until the peer can also complete its side).
|
|
379
|
+
await enqueueSeal(peerId, dc, peer, FLAG_CONFIRM, {
|
|
380
|
+
t: "confirm",
|
|
381
|
+
nonce: peer.myNonce,
|
|
382
|
+
echo: env.nonce,
|
|
383
|
+
});
|
|
384
|
+
peer.confirmedOut = true;
|
|
385
|
+
}
|
|
386
|
+
if (env.echo && env.echo === peer.myNonce) peer.confirmedIn = true;
|
|
387
|
+
if (peer.confirmedIn && peer.confirmedOut) {
|
|
388
|
+
peer.confirmed = true;
|
|
389
|
+
if (peer.confirmTimer) clearTimeout(peer.confirmTimer);
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
239
392
|
}
|
|
393
|
+
if (!env || env.t === "confirm") return; // stray confirm after handshake — ignore
|
|
394
|
+
onReq(peerId, dc, peer, env);
|
|
240
395
|
}
|
|
241
396
|
|
|
242
|
-
async function onReq(dc: any,
|
|
397
|
+
async function onReq(peerId: string, dc: any, peer: Peer, req: any) {
|
|
243
398
|
if (req.t === "abort") {
|
|
244
|
-
aborts.get(req.id)?.abort();
|
|
245
|
-
aborts.delete(req.id);
|
|
399
|
+
peer.aborts.get(req.id)?.abort();
|
|
400
|
+
peer.aborts.delete(req.id);
|
|
246
401
|
return;
|
|
247
402
|
}
|
|
248
403
|
if (req.t !== "req") return;
|
|
249
404
|
const { id, method, path: p, body } = req;
|
|
250
405
|
const ac = new AbortController();
|
|
251
|
-
aborts.set(id, ac);
|
|
406
|
+
peer.aborts.set(id, ac);
|
|
252
407
|
try {
|
|
253
408
|
// The host part is a placeholder — the handler only routes on the path.
|
|
254
409
|
const res = await opts.localFetch(
|
|
@@ -262,21 +417,39 @@ export async function startShare(
|
|
|
262
417
|
signal: ac.signal,
|
|
263
418
|
}),
|
|
264
419
|
);
|
|
265
|
-
|
|
420
|
+
enqueueSeal(peerId, dc, peer, 0, {
|
|
421
|
+
t: "res",
|
|
422
|
+
id,
|
|
423
|
+
status: res.status,
|
|
424
|
+
ct: res.headers.get("content-type") ?? "",
|
|
425
|
+
});
|
|
266
426
|
const reader = res.body!.getReader();
|
|
267
427
|
const dec = new TextDecoder();
|
|
428
|
+
let seq = 0;
|
|
268
429
|
for (;;) {
|
|
269
430
|
const { done, value } = await reader.read();
|
|
270
431
|
if (done) break;
|
|
271
432
|
const text = dec.decode(value, { stream: true });
|
|
433
|
+
// Slice on UTF-16 boundaries: JSON round-trips lone surrogates as \uXXXX,
|
|
434
|
+
// so the receiver reassembles the exact text by concatenating in seq order.
|
|
272
435
|
for (let i = 0; i < text.length; i += MAX_CHUNK)
|
|
273
|
-
|
|
436
|
+
enqueueSeal(peerId, dc, peer, 0, {
|
|
437
|
+
t: "data",
|
|
438
|
+
id,
|
|
439
|
+
seq: seq++,
|
|
440
|
+
chunk: text.slice(i, i + MAX_CHUNK),
|
|
441
|
+
});
|
|
274
442
|
}
|
|
275
|
-
|
|
443
|
+
enqueueSeal(peerId, dc, peer, 0, { t: "end", id, seq });
|
|
276
444
|
} catch (e) {
|
|
277
|
-
if ((e as Error).name !== "AbortError")
|
|
445
|
+
if ((e as Error).name !== "AbortError")
|
|
446
|
+
enqueueSeal(peerId, dc, peer, 0, {
|
|
447
|
+
t: "end",
|
|
448
|
+
id,
|
|
449
|
+
error: String((e as Error).message ?? e),
|
|
450
|
+
});
|
|
278
451
|
} finally {
|
|
279
|
-
aborts.delete(id);
|
|
452
|
+
peer.aborts.delete(id);
|
|
280
453
|
}
|
|
281
454
|
}
|
|
282
455
|
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-C78N0K4F.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-CYZtJKMG.js";
|
|
4
|
-
import "./pidStore-B5vBu8Px.js";
|
|
5
|
-
import "./globalPidIndex-gZuTvTBs.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BD8zWc7O.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|
package/dist/share-B7J79Wq9.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
-
import { homedir } from "os";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { randomBytes } from "crypto";
|
|
5
|
-
|
|
6
|
-
//#region ts/share.ts
|
|
7
|
-
const SUB = "ay-signal-1";
|
|
8
|
-
const ICE = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
9
|
-
const MAX_CHUNK = 15e3;
|
|
10
|
-
const DEFAULT_SIGHOST = "s.agent-yes.com";
|
|
11
|
-
function shareRoomPath() {
|
|
12
|
-
const home = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
13
|
-
return path.join(home, ".share-room");
|
|
14
|
-
}
|
|
15
|
-
async function loadOrCreateShareRoom(sighost = DEFAULT_SIGHOST) {
|
|
16
|
-
try {
|
|
17
|
-
const url = (await readFile(shareRoomPath(), "utf-8")).trim();
|
|
18
|
-
if (url.startsWith("webrtc://")) return url;
|
|
19
|
-
} catch {}
|
|
20
|
-
const url = `webrtc://${"r" + randomBytes(3).toString("hex")}:${randomBytes(32).toString("hex")}@${sighost}`;
|
|
21
|
-
await mkdir(path.dirname(shareRoomPath()), { recursive: true });
|
|
22
|
-
await writeFile(shareRoomPath(), url, { mode: 384 });
|
|
23
|
-
return url;
|
|
24
|
-
}
|
|
25
|
-
function parseShareUrl(s) {
|
|
26
|
-
const m = /^webrtc:\/\/([^:@/]+):([^@/]+)@(.+)$/.exec(s);
|
|
27
|
-
if (!m) throw new Error(`bad --share url: ${s} (want webrtc://room:token@host)`);
|
|
28
|
-
return {
|
|
29
|
-
room: m[1],
|
|
30
|
-
token: m[2],
|
|
31
|
-
host: m[3]
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
async function linkFromBunCache() {
|
|
35
|
-
const { existsSync, symlinkSync, mkdirSync, readdirSync } = await import("fs");
|
|
36
|
-
const path = (await import("path")).default;
|
|
37
|
-
const { createRequire } = await import("module");
|
|
38
|
-
const require = createRequire(import.meta.url);
|
|
39
|
-
const pkg = path.dirname(require.resolve("node-datachannel/package.json"));
|
|
40
|
-
const bin = path.join(pkg, "build", "Release", "node_datachannel.node");
|
|
41
|
-
const cacheRoot = path.join((await import("os")).homedir(), ".bun", "install", "cache");
|
|
42
|
-
if (existsSync(bin) && existsSync(cacheRoot)) for (const d of readdirSync(cacheRoot)) {
|
|
43
|
-
if (!d.startsWith("node-datachannel@")) continue;
|
|
44
|
-
const dst = path.join(cacheRoot, d, "build", "Release");
|
|
45
|
-
mkdirSync(dst, { recursive: true });
|
|
46
|
-
const link = path.join(dst, "node_datachannel.node");
|
|
47
|
-
if (!existsSync(link)) symlinkSync(bin, link);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
async function ndPackageDir() {
|
|
51
|
-
try {
|
|
52
|
-
const path = (await import("path")).default;
|
|
53
|
-
const { createRequire } = await import("module");
|
|
54
|
-
const require = createRequire(import.meta.url);
|
|
55
|
-
return path.dirname(require.resolve("node-datachannel/package.json"));
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async function ensureAddon(ndDir) {
|
|
61
|
-
const { existsSync } = await import("fs");
|
|
62
|
-
const path = (await import("path")).default;
|
|
63
|
-
if (existsSync(path.join(ndDir, "build", "Release", "node_datachannel.node"))) return;
|
|
64
|
-
try {
|
|
65
|
-
const { createRequire } = await import("module");
|
|
66
|
-
const binJs = createRequire(import.meta.url).resolve("prebuild-install/bin.js", { paths: [ndDir] });
|
|
67
|
-
const { spawnSync } = await import("child_process");
|
|
68
|
-
process.stderr.write("fetching node-datachannel prebuilt binary (one-time)…\n");
|
|
69
|
-
spawnSync(process.execPath, [
|
|
70
|
-
binJs,
|
|
71
|
-
"-r",
|
|
72
|
-
"napi"
|
|
73
|
-
], {
|
|
74
|
-
cwd: ndDir,
|
|
75
|
-
stdio: "ignore"
|
|
76
|
-
});
|
|
77
|
-
} catch {}
|
|
78
|
-
}
|
|
79
|
-
async function importRTC() {
|
|
80
|
-
const ndDir = await ndPackageDir();
|
|
81
|
-
if (ndDir) await ensureAddon(ndDir);
|
|
82
|
-
try {
|
|
83
|
-
return (await import("node-datachannel/polyfill")).RTCPeerConnection;
|
|
84
|
-
} catch (firstErr) {
|
|
85
|
-
await linkFromBunCache().catch(() => {});
|
|
86
|
-
try {
|
|
87
|
-
return (await import("node-datachannel/polyfill")).RTCPeerConnection;
|
|
88
|
-
} catch {
|
|
89
|
-
throw firstErr;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/** Start the share bridge. Resolves once signaling is connected; runs until the
|
|
94
|
-
* process exits, reconnecting signaling on drop. Returns the shareable link. */
|
|
95
|
-
async function startShare(opts) {
|
|
96
|
-
opts.url;
|
|
97
|
-
const sighost = opts.sighost ?? DEFAULT_SIGHOST;
|
|
98
|
-
const { room, token, host } = opts.url ? parseShareUrl(opts.url) : {
|
|
99
|
-
room: "r" + randomBytes(3).toString("hex"),
|
|
100
|
-
token: randomBytes(32).toString("hex"),
|
|
101
|
-
host: sighost
|
|
102
|
-
};
|
|
103
|
-
const RTCPeerConnection = await importRTC();
|
|
104
|
-
const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
|
|
105
|
-
const link = `${host === "s.agent-yes.com" ? "https://agent-yes.com" : "http://localhost:7778"}/#${room}:${token}${host === "s.agent-yes.com" ? "" : "@" + host}`;
|
|
106
|
-
const peers = /* @__PURE__ */ new Map();
|
|
107
|
-
let closed = false;
|
|
108
|
-
let currentWs;
|
|
109
|
-
const connectSignaling = (onReady) => {
|
|
110
|
-
if (closed) return;
|
|
111
|
-
const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
|
|
112
|
-
currentWs = ws;
|
|
113
|
-
let ready = false;
|
|
114
|
-
ws.onopen = () => {
|
|
115
|
-
ws.send(JSON.stringify({
|
|
116
|
-
type: "hello",
|
|
117
|
-
role: "host",
|
|
118
|
-
token
|
|
119
|
-
}));
|
|
120
|
-
ready = true;
|
|
121
|
-
onReady();
|
|
122
|
-
};
|
|
123
|
-
ws.onmessage = async (ev) => {
|
|
124
|
-
if (closed) return;
|
|
125
|
-
const m = JSON.parse(ev.data);
|
|
126
|
-
if (m.type === "peer-join") startPeer(ws, m.peer);
|
|
127
|
-
else if (m.type === "answer") await peers.get(m.from)?.pc.setRemoteDescription({
|
|
128
|
-
type: "answer",
|
|
129
|
-
sdp: m.sdp
|
|
130
|
-
});
|
|
131
|
-
else if (m.type === "candidate") await peers.get(m.from)?.pc.addIceCandidate(m.candidate).catch(() => {});
|
|
132
|
-
else if (m.type === "peer-leave") closePeer(m.peer);
|
|
133
|
-
};
|
|
134
|
-
ws.onclose = () => {
|
|
135
|
-
if (closed) return;
|
|
136
|
-
setTimeout(() => connectSignaling(() => {}), ready ? 1500 : 4e3);
|
|
137
|
-
};
|
|
138
|
-
ws.onerror = () => {};
|
|
139
|
-
return ws;
|
|
140
|
-
};
|
|
141
|
-
function startPeer(ws, peerId) {
|
|
142
|
-
const pc = new RTCPeerConnection({ iceServers: ICE });
|
|
143
|
-
const aborts = /* @__PURE__ */ new Map();
|
|
144
|
-
peers.set(peerId, {
|
|
145
|
-
pc,
|
|
146
|
-
aborts
|
|
147
|
-
});
|
|
148
|
-
pc.onicecandidate = (e) => {
|
|
149
|
-
if (e.candidate) ws.send(JSON.stringify({
|
|
150
|
-
type: "candidate",
|
|
151
|
-
to: peerId,
|
|
152
|
-
candidate: e.candidate
|
|
153
|
-
}));
|
|
154
|
-
};
|
|
155
|
-
pc.onconnectionstatechange = () => {
|
|
156
|
-
if ([
|
|
157
|
-
"failed",
|
|
158
|
-
"closed",
|
|
159
|
-
"disconnected"
|
|
160
|
-
].includes(pc.connectionState)) closePeer(peerId);
|
|
161
|
-
};
|
|
162
|
-
const dc = pc.createDataChannel("api");
|
|
163
|
-
dc.onmessage = (e) => onReq(dc, aborts, JSON.parse(e.data));
|
|
164
|
-
pc.createOffer().then((o) => pc.setLocalDescription(o)).then(() => ws.send(JSON.stringify({
|
|
165
|
-
type: "offer",
|
|
166
|
-
to: peerId,
|
|
167
|
-
sdp: pc.localDescription.sdp
|
|
168
|
-
})));
|
|
169
|
-
}
|
|
170
|
-
function closePeer(peerId) {
|
|
171
|
-
const p = peers.get(peerId);
|
|
172
|
-
if (!p) return;
|
|
173
|
-
for (const a of p.aborts.values()) a.abort();
|
|
174
|
-
try {
|
|
175
|
-
p.pc.close();
|
|
176
|
-
} catch {}
|
|
177
|
-
peers.delete(peerId);
|
|
178
|
-
}
|
|
179
|
-
function send(dc, obj) {
|
|
180
|
-
if (dc.readyState !== "open") return;
|
|
181
|
-
try {
|
|
182
|
-
dc.send(JSON.stringify(obj));
|
|
183
|
-
} catch {}
|
|
184
|
-
}
|
|
185
|
-
async function onReq(dc, aborts, req) {
|
|
186
|
-
if (req.t === "abort") {
|
|
187
|
-
aborts.get(req.id)?.abort();
|
|
188
|
-
aborts.delete(req.id);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (req.t !== "req") return;
|
|
192
|
-
const { id, method, path: p, body } = req;
|
|
193
|
-
const ac = new AbortController();
|
|
194
|
-
aborts.set(id, ac);
|
|
195
|
-
try {
|
|
196
|
-
const res = await opts.localFetch(new Request(`http://ay.local${p}`, {
|
|
197
|
-
method,
|
|
198
|
-
headers: {
|
|
199
|
-
Authorization: `Bearer ${opts.apiToken}`,
|
|
200
|
-
...body ? { "Content-Type": "application/json" } : {}
|
|
201
|
-
},
|
|
202
|
-
body: body ?? void 0,
|
|
203
|
-
signal: ac.signal
|
|
204
|
-
}));
|
|
205
|
-
send(dc, {
|
|
206
|
-
t: "res",
|
|
207
|
-
id,
|
|
208
|
-
status: res.status,
|
|
209
|
-
ct: res.headers.get("content-type") ?? ""
|
|
210
|
-
});
|
|
211
|
-
const reader = res.body.getReader();
|
|
212
|
-
const dec = new TextDecoder();
|
|
213
|
-
for (;;) {
|
|
214
|
-
const { done, value } = await reader.read();
|
|
215
|
-
if (done) break;
|
|
216
|
-
const text = dec.decode(value, { stream: true });
|
|
217
|
-
for (let i = 0; i < text.length; i += MAX_CHUNK) send(dc, {
|
|
218
|
-
t: "data",
|
|
219
|
-
id,
|
|
220
|
-
chunk: text.slice(i, i + MAX_CHUNK)
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
send(dc, {
|
|
224
|
-
t: "end",
|
|
225
|
-
id
|
|
226
|
-
});
|
|
227
|
-
} catch (e) {
|
|
228
|
-
if (e.name !== "AbortError") send(dc, {
|
|
229
|
-
t: "end",
|
|
230
|
-
id,
|
|
231
|
-
error: String(e)
|
|
232
|
-
});
|
|
233
|
-
} finally {
|
|
234
|
-
aborts.delete(id);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
await new Promise((resolve) => connectSignaling(resolve));
|
|
238
|
-
const close = () => {
|
|
239
|
-
closed = true;
|
|
240
|
-
try {
|
|
241
|
-
currentWs?.close();
|
|
242
|
-
} catch {}
|
|
243
|
-
for (const peerId of [...peers.keys()]) closePeer(peerId);
|
|
244
|
-
};
|
|
245
|
-
return {
|
|
246
|
-
room,
|
|
247
|
-
link,
|
|
248
|
-
close
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
//#endregion
|
|
253
|
-
export { loadOrCreateShareRoom, startShare };
|
|
254
|
-
//# sourceMappingURL=share-B7J79Wq9.js.map
|