agent-yes 1.119.1 → 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/ts/serve.ts CHANGED
@@ -133,18 +133,32 @@ function ayServeArgv(args: string[]): string[] {
133
133
  return [...launcher, "serve", ...args];
134
134
  }
135
135
 
136
- // Register the daemon with the platform init system so it comes back after a
137
- // *reboot*, not just a crash. oxmgr wires launchd/systemd/Task Scheduler via
138
- // `oxmgr service install`; pm2 persists its process list with `pm2 save` (a
139
- // once-installed `pm2 startup` hook then resurrects it on boot). Idempotent and
140
- // best-effort: returns false on any failure without aborting the install — the
141
- // process is still crash-managed, just not guaranteed boot-persistent.
136
+ // Register the daemon to come up automatically. The *scope* is per-platform by
137
+ // design: Linux at system boot (before login); Windows → at user login.
138
+ // - Linux (oxmgr): `oxmgr service install` wires a systemd **--user** unit,
139
+ // which on its own only starts after the user logs in. To make it start at
140
+ // boot without requiring root (no system-scope unit, no sudo), we also
141
+ // `loginctl enable-linger`, which keeps the user's systemd instance — and
142
+ // thus our service — running from boot. Best-effort; linger failing just
143
+ // downgrades us to login-scope.
144
+ // - Windows (pm2): `pm2 save` persists the process list so the once-installed
145
+ // `pm2 startup` logon hook resurrects it at user login.
146
+ // Idempotent and best-effort: returns false on failure without aborting the
147
+ // install — the process is still crash-managed, just not boot/login-persistent.
142
148
  async function ensureBootAutostart(mgr: DaemonManager): Promise<boolean> {
143
149
  try {
150
+ if (mgr.id !== "oxmgr") {
151
+ // pm2 (Windows): logon-scoped resurrect via the saved process list.
152
+ return (await spawnExit([mgr.bin, "save"])) === 0;
153
+ }
144
154
  // oxmgr's --system defaults to "auto" (launchd/systemd/Task Scheduler); it's
145
155
  // a `service`-level flag, so it goes before the subcommand, not after.
146
- const cmd = mgr.id === "oxmgr" ? [mgr.bin, "service", "install"] : [mgr.bin, "save"];
147
- return (await Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] }).exited) === 0;
156
+ const installed = (await spawnExit([mgr.bin, "service", "install"])) === 0;
157
+ if (installed && process.platform === "linux") {
158
+ // Upgrade login-scope → boot-scope: linger starts the user manager at boot.
159
+ await spawnExit(["loginctl", "enable-linger", userInfo().username]);
160
+ }
161
+ return installed;
148
162
  } catch {
149
163
  return false;
150
164
  }
@@ -281,14 +295,14 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
281
295
  if (mgr.id === "oxmgr")
282
296
  process.stdout.write(
283
297
  onBoot
284
- ? `start-on-boot: enabled (oxmgr registered with the system init)\n`
285
- : `start-on-boot: not registered — run \`oxmgr service install\` to enable\n`,
298
+ ? `start-on-boot: enabled (systemd --user + linger, starts at boot)\n`
299
+ : `start-on-boot: not registered — needs a user systemd session; run \`oxmgr service install\` to enable\n`,
286
300
  );
287
301
  else
288
302
  process.stdout.write(
289
303
  onBoot
290
- ? `start-on-boot: pm2 list saved (run \`pm2 startup\` once for boot resurrect)\n`
291
- : `start-on-boot: \`pm2 save\` failed — run it manually to persist across reboots\n`,
304
+ ? `start-on-login: enabled (pm2 list saved; run \`pm2 startup\` once if logon resurrect is not yet installed)\n`
305
+ : `start-on-login: \`pm2 save\` failed — run it manually to persist across logins\n`,
292
306
  );
293
307
  process.stdout.write(`token: ${token}\n\n`);
294
308
  if (httpish) {
@@ -299,8 +313,9 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
299
313
  process.stdout.write(` ay serve uninstall # remove daemon\n`);
300
314
  if (webrtcish) {
301
315
  process.stdout.write(
302
- `\nthe WebRTC share link is printed by the daemon see: ay serve logs\n` +
303
- `(the room persists in ~/.agent-yes/.share-room, so the link survives restarts)\n`,
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`,
304
319
  );
305
320
  }
306
321
  }
@@ -988,6 +1003,8 @@ export async function cmdServe(rest: string[]): Promise<number> {
988
1003
  return serveUiFile("room-client.js", "text/javascript; charset=utf-8");
989
1004
  if (req.method === "GET" && p === "/console-logic.js")
990
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");
991
1008
  if (req.method === "GET" && p === "/favicon.ico") return new Response(null, { status: 204 });
992
1009
  return apiFetch(req);
993
1010
  };
@@ -1049,18 +1066,37 @@ export async function cmdServe(rest: string[]): Promise<number> {
1049
1066
  const { startShare, loadOrCreateShareRoom } = await import("./share.ts");
1050
1067
  // No explicit webrtc:// URL → reuse the persisted room (minted once and
1051
1068
  // saved like the serve token), so the link is stable across restarts.
1052
- const { link, close } = await startShare({
1069
+ const { room, link, close } = await startShare({
1053
1070
  url: explicitUrl ?? (await loadOrCreateShareRoom()),
1054
1071
  localFetch: apiFetch,
1055
1072
  apiToken: token,
1056
1073
  });
1057
1074
  closeShare = close;
1058
- process.stdout.write(
1059
- `${wantHttp ? "\n" : ""}shared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n` +
1060
- (explicitUrl
1061
- ? "\n"
1062
- : ` (persistent room — same link across restarts; delete ~/.agent-yes/.share-room to rotate)\n\n`),
1063
- );
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
+ }
1064
1100
  } catch (e) {
1065
1101
  process.stderr.write(`ay serve --webrtc failed: ${(e as Error).message}\n`);
1066
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://")) return url;
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 token = randomBytes(32).toString("hex");
47
- const url = `webrtc://${room}:${token}@${sighost}`;
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}:${token}${suffix}`;
190
+ const link = `${ui}/#${room}:${MARKER}${S}${suffix}`;
158
191
 
159
- type Peer = { pc: any; aborts: Map<number, AbortController> };
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
- await peers.get(m.from)?.pc.setRemoteDescription({ type: "answer", sdp: m.sdp });
180
- else if (m.type === "candidate")
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
- const aborts = new Map<number, AbortController>();
200
- peers.set(peerId, { pc, aborts });
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.onmessage = (e: any) => onReq(dc, aborts, JSON.parse(e.data));
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
- function send(dc: any, obj: object) {
230
- // readyState alone is racy: node-datachannel can still report "open" for a
231
- // tick after a dropped peer's channel is torn down underneath, so dc.send()
232
- // throws "DataChannel is closed". Swallow it — the frame is for a peer that's
233
- // already gone (closePeer aborts its in-flight requests right behind this).
234
- if (dc.readyState !== "open") return;
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
- dc.send(JSON.stringify(obj));
369
+ const { plaintext } = await e2eOpen(peer.keyC2H, data, peer.th, peer.recv);
370
+ env = unpackEnvelope(plaintext);
237
371
  } catch {
238
- /* peer vanished mid-send; dropping the frame is correct */
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, aborts: Map<number, AbortController>, req: 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
- send(dc, { t: "res", id, status: res.status, ct: res.headers.get("content-type") ?? "" });
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
- send(dc, { t: "data", id, chunk: text.slice(i, i + MAX_CHUNK) });
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
- send(dc, { t: "end", id });
443
+ enqueueSeal(peerId, dc, peer, 0, { t: "end", id, seq });
276
444
  } catch (e) {
277
- if ((e as Error).name !== "AbortError") send(dc, { t: "end", id, error: String(e) });
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-VrgyWwNH.js";
2
- import "./logger-B9h0djqx.js";
3
- import "./versionChecker-BjZOppZJ.js";
4
- import "./pidStore-B5vBu8Px.js";
5
- import "./globalPidIndex-gZuTvTBs.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DwPmzY8B.js";
7
-
8
- export { SUPPORTED_CLIS };