agent-yes 1.144.0 → 1.145.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.
@@ -0,0 +1,280 @@
1
+ // CLI WebRTC remote: dial an `ay serve --share` room as a CLIENT peer over an
2
+ // end-to-end-encrypted WebRTC DataChannel, then expose it as a local HTTP
3
+ // endpoint (the "bridge") so every existing fetch-based remote command — ls,
4
+ // send, status, and streaming `tail -f` — works unchanged against a remote host
5
+ // that has no reachable HTTP port.
6
+ //
7
+ // The host side lives in ts/share.ts; this is its inverse. The host offers and
8
+ // responds; we answer and request. Crypto is reused verbatim from lab/ui/e2e.js
9
+ // so the AES-GCM / transcript-hash handshake is byte-identical to the browser
10
+ // console and the host.
11
+ import { RTCPeerConnection } from "node-datachannel/polyfill";
12
+ import {
13
+ deriveAuthToken,
14
+ deriveDirKeys,
15
+ computeTranscriptHash,
16
+ seal as e2eSeal,
17
+ open as e2eOpen,
18
+ packEnvelope,
19
+ unpackEnvelope,
20
+ randomHex,
21
+ FLAG_CONFIRM,
22
+ } from "../lab/ui/e2e.js";
23
+ import { SIGNAL_SUBPROTOCOL as SUB, parseWebrtcLink, type WebrtcLink } from "./webrtcLink.ts";
24
+
25
+ export { isWebrtcSpec, parseWebrtcLink } from "./webrtcLink.ts";
26
+ export type { WebrtcLink } from "./webrtcLink.ts";
27
+
28
+ const CONNECT_TIMEOUT_MS = 25_000;
29
+
30
+ interface PendingReq {
31
+ onRes: (status: number, ct: string) => void;
32
+ onData: (chunk: string) => void;
33
+ onEnd: (error?: string) => void;
34
+ }
35
+
36
+ /** A live, key-confirmed WebRTC connection to a single share room. */
37
+ class WebRtcConn {
38
+ private ws: WebSocket;
39
+ private pc: any = null;
40
+ private dc: any = null;
41
+ private readonly send = { sendCtr: 0n };
42
+ private readonly recv = { lastSeen: -1n };
43
+ private keyC2H: CryptoKey | null = null; // client encrypts host-bound frames
44
+ private keyH2C: CryptoKey | null = null; // client decrypts host-sent frames
45
+ private th: Uint8Array | null = null;
46
+ private hostPeer: string | null = null;
47
+ private readonly myNonce = randomHex(16);
48
+ private confirmedIn = false;
49
+ private confirmedOut = false;
50
+ private confirmed = false;
51
+ private sendChain: Promise<void> = Promise.resolve();
52
+ private recvChain: Promise<void> = Promise.resolve();
53
+ private idCounter = 0;
54
+ private readonly pending = new Map<string, PendingReq>();
55
+
56
+ readonly ready: Promise<void>;
57
+ private resolveReady!: () => void;
58
+ private rejectReady!: (e: Error) => void;
59
+
60
+ constructor(private readonly link: WebrtcLink) {
61
+ this.ready = new Promise<void>((res, rej) => {
62
+ this.resolveReady = res;
63
+ this.rejectReady = rej;
64
+ });
65
+ const timer = setTimeout(
66
+ () => this.rejectReady(new Error(`WebRTC connect timeout after ${CONNECT_TIMEOUT_MS}ms`)),
67
+ CONNECT_TIMEOUT_MS,
68
+ );
69
+ this.ready.then(
70
+ () => clearTimeout(timer),
71
+ () => clearTimeout(timer),
72
+ );
73
+ this.ws = this.dial();
74
+ }
75
+
76
+ private dial(): WebSocket {
77
+ const { room, host } = this.link;
78
+ const ws = new WebSocket(`wss://${host}/${room}`, [SUB]);
79
+ ws.onopen = async () => {
80
+ const authToken = await deriveAuthToken(this.link.s, room, host);
81
+ ws.send(JSON.stringify({ type: "hello", role: "client", v: 2, token: authToken }));
82
+ };
83
+ ws.onerror = (e: any) =>
84
+ this.rejectReady(new Error(`signaling error: ${String(e?.message ?? e)}`));
85
+ ws.onclose = () => {
86
+ if (!this.confirmed) this.rejectReady(new Error("signaling closed before connect"));
87
+ };
88
+ ws.onmessage = (ev: any) => void this.onSignal(ev);
89
+ return ws;
90
+ }
91
+
92
+ private async onSignal(ev: any): Promise<void> {
93
+ const m = JSON.parse(typeof ev.data === "string" ? ev.data : await ev.data.text());
94
+ if (m.type === "offer") {
95
+ this.hostPeer = m.from;
96
+ const pc = new RTCPeerConnection({ iceServers: m.iceServers || [] });
97
+ this.pc = pc;
98
+ pc.onicecandidate = (e: any) => {
99
+ if (e.candidate)
100
+ this.ws.send(
101
+ JSON.stringify({ type: "candidate", to: this.hostPeer, candidate: e.candidate }),
102
+ );
103
+ };
104
+ pc.ondatachannel = (e: any) => {
105
+ const dc = e.channel;
106
+ this.dc = dc;
107
+ dc.binaryType = "arraybuffer";
108
+ dc.onopen = () => this.enqueueSeal(FLAG_CONFIRM, { t: "confirm", nonce: this.myNonce });
109
+ dc.onmessage = (ev2: any) => {
110
+ this.recvChain = this.recvChain.then(() => this.onFrame(ev2.data)).catch(() => {});
111
+ };
112
+ };
113
+ await pc.setRemoteDescription({ type: "offer", sdp: m.sdp });
114
+ const answer = await pc.createAnswer();
115
+ await pc.setLocalDescription(answer);
116
+ this.th = await computeTranscriptHash(m.sdp, pc.localDescription.sdp);
117
+ const keys = await deriveDirKeys(this.link.s, this.th);
118
+ this.keyC2H = keys.keyC2H;
119
+ this.keyH2C = keys.keyH2C;
120
+ this.ws.send(JSON.stringify({ type: "answer", to: this.hostPeer, sdp: pc.localDescription.sdp }));
121
+ } else if (m.type === "candidate" && this.pc) {
122
+ await this.pc.addIceCandidate(m.candidate).catch(() => {});
123
+ }
124
+ }
125
+
126
+ private enqueueSeal(flags: number, obj: object): Promise<void> {
127
+ this.sendChain = this.sendChain.then(async () => {
128
+ if (!this.dc || this.dc.readyState !== "open" || !this.keyC2H || !this.th) return;
129
+ const frame = await e2eSeal(this.keyC2H, this.send, flags, this.th, packEnvelope(obj));
130
+ try {
131
+ this.dc.send(frame);
132
+ } catch {}
133
+ });
134
+ return this.sendChain;
135
+ }
136
+
137
+ private async onFrame(data: any): Promise<void> {
138
+ if (typeof data === "string" || !this.keyH2C || !this.th) return;
139
+ let env: any;
140
+ try {
141
+ const { plaintext } = await e2eOpen(this.keyH2C, data, this.th, this.recv);
142
+ env = unpackEnvelope(plaintext);
143
+ } catch {
144
+ return; // drop undecryptable frames
145
+ }
146
+ if (!this.confirmed) {
147
+ if (!env || env.t !== "confirm") return;
148
+ if (typeof env.nonce === "string" && !this.confirmedOut) {
149
+ await this.enqueueSeal(FLAG_CONFIRM, { t: "confirm", nonce: this.myNonce, echo: env.nonce });
150
+ this.confirmedOut = true;
151
+ }
152
+ if (env.echo && env.echo === this.myNonce) this.confirmedIn = true;
153
+ if (this.confirmedIn && this.confirmedOut) {
154
+ this.confirmed = true;
155
+ this.resolveReady();
156
+ }
157
+ return;
158
+ }
159
+ if (!env || env.t === "confirm") return;
160
+ const p = this.pending.get(env.id);
161
+ if (!p) return;
162
+ if (env.t === "res") p.onRes(env.status, env.ct);
163
+ else if (env.t === "data") p.onData(env.chunk);
164
+ else if (env.t === "end") p.onEnd(env.error);
165
+ }
166
+
167
+ /**
168
+ * Issue one request over the channel. Resolves once the response head (status,
169
+ * content-type) arrives; the body is a ReadableStream fed by `data` frames as
170
+ * they land, so streaming endpoints (SSE `tail`) flow without buffering.
171
+ */
172
+ request(
173
+ method: string,
174
+ path: string,
175
+ body?: string,
176
+ ): Promise<{ status: number; ct: string; stream: ReadableStream<Uint8Array> }> {
177
+ const id = String(++this.idCounter);
178
+ return new Promise((resolve, reject) => {
179
+ let controller: ReadableStreamDefaultController<Uint8Array>;
180
+ const enc = new TextEncoder();
181
+ let head = false;
182
+ const stream = new ReadableStream<Uint8Array>({
183
+ start: (c) => {
184
+ controller = c;
185
+ },
186
+ cancel: () => {
187
+ this.pending.delete(id);
188
+ void this.enqueueSeal(0, { t: "abort", id });
189
+ },
190
+ });
191
+ this.pending.set(id, {
192
+ onRes: (status, ct) => {
193
+ if (!head) {
194
+ head = true;
195
+ resolve({ status, ct, stream });
196
+ }
197
+ },
198
+ onData: (chunk) => {
199
+ try {
200
+ controller.enqueue(enc.encode(chunk));
201
+ } catch {}
202
+ },
203
+ onEnd: (error) => {
204
+ this.pending.delete(id);
205
+ if (error) {
206
+ if (!head) {
207
+ head = true;
208
+ reject(new Error(error));
209
+ }
210
+ try {
211
+ controller.error(new Error(error));
212
+ } catch {}
213
+ } else {
214
+ try {
215
+ controller.close();
216
+ } catch {}
217
+ }
218
+ },
219
+ });
220
+ void this.enqueueSeal(0, { t: "req", id, method, path, body });
221
+ });
222
+ }
223
+
224
+ close(): void {
225
+ try {
226
+ this.ws.close();
227
+ } catch {}
228
+ try {
229
+ this.pc?.close();
230
+ } catch {}
231
+ }
232
+ }
233
+
234
+ export interface WebrtcBridge {
235
+ baseUrl: string;
236
+ token: string;
237
+ close: () => void;
238
+ }
239
+
240
+ /**
241
+ * Connect to a share room and start a local HTTP server that forwards every
242
+ * request over the encrypted DataChannel. Returns the loopback base URL the
243
+ * existing remote commands can `fetch()` against. The process owns teardown:
244
+ * `ay` subcommands `process.exit()` when done, which closes the socket/peer.
245
+ */
246
+ export async function startWebrtcBridge(link: string): Promise<WebrtcBridge> {
247
+ const parsed = parseWebrtcLink(link);
248
+ if (!parsed) throw new Error(`not a WebRTC share link: ${link}`);
249
+ const conn = new WebRtcConn(parsed);
250
+ await conn.ready;
251
+
252
+ const server = Bun.serve({
253
+ port: 0,
254
+ hostname: "127.0.0.1",
255
+ idleTimeout: 0, // streaming tail can idle indefinitely between log lines
256
+ async fetch(req: Request) {
257
+ const u = new URL(req.url);
258
+ const pathWithQuery = u.pathname + u.search;
259
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
260
+ const body = hasBody ? await req.text() : undefined;
261
+ try {
262
+ const { status, ct, stream } = await conn.request(req.method, pathWithQuery, body);
263
+ return new Response(stream, { status, headers: ct ? { "content-type": ct } : {} });
264
+ } catch (e: any) {
265
+ return new Response(String(e?.message ?? e), { status: 502 });
266
+ }
267
+ },
268
+ });
269
+
270
+ return {
271
+ baseUrl: `http://127.0.0.1:${server.port}`,
272
+ token: "webrtc", // auth is the E2E secret; the host injects its own bearer
273
+ close: () => {
274
+ try {
275
+ server.stop(true);
276
+ } catch {}
277
+ conn.close();
278
+ },
279
+ };
280
+ }
@@ -1,8 +0,0 @@
1
- import "./ts-Qh0Z7nsZ.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-BjKAfiI-.js";
4
- import "./pidStore-fqXqTKkh.js";
5
- import "./globalPidIndex-DlmmJlO8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-D0dDlvLS.js";
7
-
8
- export { SUPPORTED_CLIS };