agent-yes 1.302.2 → 1.303.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-C5Uhdl61.js → SUPPORTED_CLIS-BvWBFlrN.js} +3 -3
- package/dist/{SUPPORTED_CLIS-FBshN0v2.js → SUPPORTED_CLIS-CqmE-ESB.js} +2 -2
- package/dist/{agentShare-CNgCQMY9.js → agentShare-Gb4vxJzE.js} +4 -4
- package/dist/{callback-ayc9yXqJ.js → callback-CuIY-75l.js} +3 -3
- package/dist/{callback-CbZ-H_Pn.js → callback-ZzRPkYjf.js} +4 -4
- package/dist/cli.js +5 -5
- package/dist/{cmdPs-CDj-l5Pk.js → cmdPs-DSKYjOKS.js} +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-DZbd5fTj.js → notifyDaemon-C_gTWqYZ.js} +4 -4
- package/dist/{parentPingLoop-Bim6u0OH.js → parentPingLoop-BhVR1EQm.js} +2 -2
- package/dist/{remotes-ByjpYjQ9.js → remotes-C36aI8Ty.js} +2 -2
- package/dist/{remotes-D10E_xUn.js → remotes-COBEhmlr.js} +6 -6
- package/dist/{rustBinary-DklBSefL.js → rustBinary-Ep0x7WrS.js} +2 -2
- package/dist/{rustBinary-Btf18sk5.js → rustBinary-eOcvZ5yS.js} +2 -2
- package/dist/{schedule-DzqTosvJ.js → schedule-E2QlsME5.js} +4 -4
- package/dist/{serve-DdHbUWtG.js → serve-BWy7zL7j.js} +43 -26
- package/dist/{serveRust-BEzTqFRy.js → serveRust-BgY5Te5E.js} +50 -6
- package/dist/{setup-CzKkVYp8.js → setup-CD9-RyhT.js} +2 -2
- package/dist/{shareCmd-DinezAHY.js → shareCmd-Drxb-SjM.js} +7 -7
- package/dist/{subcommands-oIARj_mJ.js → subcommands-C627cuOl.js} +3 -3
- package/dist/{subcommands-fX4Ygx3a.js → subcommands-Cc6bmEnB.js} +30 -27
- package/dist/{terminal-DlW0KNym.js → terminal-BddOL2fx.js} +2 -2
- package/dist/trayApp-CdLbJK5F.js +5 -0
- package/dist/{trayApp-DWVdr0Pr.js → trayApp-D0UYVT6n.js} +2 -2
- package/dist/{ts-D29CMeWZ.js → ts-Bm6FhYS4.js} +3 -3
- package/dist/{versionChecker-DrjMn-XI.js → versionChecker-CFyZKym2.js} +2 -2
- package/dist/webrtcLink-CHLgPKWj.js +4 -0
- package/dist/{webrtcLink-CKnjNGaV.js → webrtcLink-bWxiCBTV.js} +26 -9
- package/dist/webrtcRemote-RmkbzY6Y.js +683 -0
- package/dist/{widget-CvcvPWF9.js → widget-C6-P26Ue.js} +3 -3
- package/dist/{ws-Cbx5sfsd.js → ws-DCcdDRXH.js} +2 -2
- package/dist/{ws-DDB8jh0f.js → ws-DmiSL50p.js} +4 -4
- package/lab/ui/index.html +71 -3
- package/package.json +1 -1
- package/ts/remotes.ts +11 -4
- package/ts/serve.ts +24 -2
- package/ts/serveRust.spec.ts +23 -6
- package/ts/serveRust.ts +34 -7
- package/ts/subcommands.ts +4 -1
- package/ts/webrtcLink.spec.ts +26 -1
- package/ts/webrtcLink.ts +32 -5
- package/ts/webrtcRemote.ts +406 -102
- package/ts/webrtcSignal.spec.ts +40 -0
- package/ts/webrtcSignal.ts +59 -3
- package/dist/trayApp-F_Xs3C2N.js +0 -5
- package/dist/webrtcLink-CAdvnRM0.js +0 -4
- package/dist/webrtcRemote-BYy2lTTl.js +0 -408
package/ts/webrtcSignal.spec.ts
CHANGED
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
reduceSignal,
|
|
7
7
|
socketError,
|
|
8
8
|
timeoutError,
|
|
9
|
+
memberOf,
|
|
10
|
+
mergeMemberLs,
|
|
11
|
+
otherMembers,
|
|
12
|
+
splitMemberKeyword,
|
|
9
13
|
type SignalState,
|
|
10
14
|
} from "./webrtcSignal.ts";
|
|
11
15
|
|
|
@@ -118,3 +122,39 @@ describe("describeConnectFailure", () => {
|
|
|
118
122
|
).toContain("not a sandbox");
|
|
119
123
|
});
|
|
120
124
|
});
|
|
125
|
+
|
|
126
|
+
describe("rooms with several hosts", () => {
|
|
127
|
+
const a = { peer: "h-aaaa", name: "alpha" };
|
|
128
|
+
const b = { peer: "h-bbbb", name: "beta" };
|
|
129
|
+
|
|
130
|
+
it("names the sender of a host frame, defaulting to the one host of an older server", () => {
|
|
131
|
+
expect(memberOf({ type: "offer", from: "h-aaaa" })).toBe("h-aaaa");
|
|
132
|
+
expect(memberOf({ type: "offer" })).toBe("host");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("reads a leading member name off a keyword", () => {
|
|
136
|
+
expect(splitMemberKeyword("beta:1111", [a, b])).toEqual({ member: b, keyword: "1111" });
|
|
137
|
+
expect(splitMemberKeyword("h-aaaa:repo", [a, b])).toEqual({ member: a, keyword: "repo" });
|
|
138
|
+
expect(splitMemberKeyword("alpha", [a, b])).toEqual({ member: a, keyword: undefined });
|
|
139
|
+
expect(splitMemberKeyword("gamma:1111", [a, b])).toBeNull();
|
|
140
|
+
expect(splitMemberKeyword("1111", [a, b])).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("leaves out this machine's own member unless it is the only one", () => {
|
|
144
|
+
expect(otherMembers([a, b], "alpha")).toEqual([b]);
|
|
145
|
+
expect(otherMembers([a], "alpha")).toEqual([a]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("tags every merged row with its member", () => {
|
|
149
|
+
expect(
|
|
150
|
+
mergeMemberLs([
|
|
151
|
+
{ member: a, rows: [{ pid: 1111 }] },
|
|
152
|
+
{ member: b, rows: [{ pid: 2222 }] },
|
|
153
|
+
{ member: { peer: "h-cccc", name: "" }, rows: "not a list" },
|
|
154
|
+
]),
|
|
155
|
+
).toEqual([
|
|
156
|
+
{ pid: 1111, member: "alpha" },
|
|
157
|
+
{ pid: 2222, member: "beta" },
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
160
|
+
});
|
package/ts/webrtcSignal.ts
CHANGED
|
@@ -133,11 +133,67 @@ export function describeConnectFailure(
|
|
|
133
133
|
return Promise.resolve(
|
|
134
134
|
`WebRTC handshake with the host behind ${host} failed: ${cause}\n` +
|
|
135
135
|
` Signaling worked and the host answered, so this is not a sandbox or proxy\n` +
|
|
136
|
-
` problem — the two machines could not open a data channel. Usual
|
|
137
|
-
`
|
|
138
|
-
` and
|
|
136
|
+
` problem — the two machines could not open a data channel. Usual cause: a\n` +
|
|
137
|
+
` host behind Docker Desktop or another symmetric NAT. There, publish one UDP\n` +
|
|
138
|
+
` port and set AGENT_YES_ICE_UDP_PORT + AGENT_YES_ICE_PUBLIC_IPS, or set\n` +
|
|
139
|
+
` AGENT_YES_SIGNAL_RELAY=1 on both ends to relay through the signaling server.`,
|
|
139
140
|
);
|
|
140
141
|
default:
|
|
141
142
|
return sandboxHint(cause);
|
|
142
143
|
}
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Rooms with several hosts (lab/ui/cf/roomRouting.ts)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/** One host serving a room: its signaling peer id and display name. */
|
|
151
|
+
export interface RoomMember {
|
|
152
|
+
peer: string;
|
|
153
|
+
name: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The sender of a host frame. A signaling server without multi-host rooms
|
|
158
|
+
* does not tag host frames; its one host is "host".
|
|
159
|
+
*/
|
|
160
|
+
export function memberOf(m: any): string {
|
|
161
|
+
return typeof m?.from === "string" && m.from ? m.from : "host";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Which members a `<alias>:<keyword>` reference names, when the keyword starts
|
|
166
|
+
* with a member's name (`<alias>:<member>:<keyword>`). Returns the member and
|
|
167
|
+
* the rest of the keyword, or null when the first segment names no member.
|
|
168
|
+
*/
|
|
169
|
+
export function splitMemberKeyword(
|
|
170
|
+
keyword: string,
|
|
171
|
+
members: RoomMember[],
|
|
172
|
+
): { member: RoomMember; keyword: string | undefined } | null {
|
|
173
|
+
const i = keyword.indexOf(":");
|
|
174
|
+
const head = i === -1 ? keyword : keyword.slice(0, i);
|
|
175
|
+
const member = members.find((m) => m.name === head || m.peer === head);
|
|
176
|
+
if (!member) return null;
|
|
177
|
+
const rest = i === -1 ? "" : keyword.slice(i + 1);
|
|
178
|
+
return { member, keyword: rest || undefined };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The members a viewer should talk to: everyone but the machine it runs on
|
|
183
|
+
* (a member listing its own room would show its agents twice), unless that
|
|
184
|
+
* would leave nobody.
|
|
185
|
+
*/
|
|
186
|
+
export function otherMembers(members: RoomMember[], selfName: string): RoomMember[] {
|
|
187
|
+
const others = members.filter((m) => m.name !== selfName);
|
|
188
|
+
return others.length ? others : members;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `/api/ls` rows from several members, each row tagged with its member. */
|
|
192
|
+
export function mergeMemberLs(parts: { member: RoomMember; rows: unknown }[]): any[] {
|
|
193
|
+
const out: any[] = [];
|
|
194
|
+
for (const { member, rows } of parts)
|
|
195
|
+
if (Array.isArray(rows))
|
|
196
|
+
for (const r of rows)
|
|
197
|
+
if (r && typeof r === "object") out.push({ ...r, member: member.name || member.peer });
|
|
198
|
+
return out;
|
|
199
|
+
}
|
package/dist/trayApp-F_Xs3C2N.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-BdLf8DvY.js";
|
|
2
|
-
import { i as parseWebrtcLink, n as SIGNAL_SUBPROTOCOL, r as isWebrtcSpec } from "./webrtcLink-CKnjNGaV.js";
|
|
3
|
-
import { RTCPeerConnection } from "node-datachannel/polyfill";
|
|
4
|
-
|
|
5
|
-
//#region ts/signalingHint.ts
|
|
6
|
-
/**
|
|
7
|
-
* Reach the signaling host over ordinary HTTPS. ANY HTTP response counts as
|
|
8
|
-
* reachable — a 404 or a 400 still proves egress, TLS and DNS all worked, which
|
|
9
|
-
* is the whole question. Never throws; a failure IS the answer.
|
|
10
|
-
*/
|
|
11
|
-
async function probeSignalingHost(host, timeoutMs = 4e3, doFetch = fetch) {
|
|
12
|
-
try {
|
|
13
|
-
await doFetch(`https://${host}/`, {
|
|
14
|
-
method: "HEAD",
|
|
15
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
16
|
-
redirect: "manual"
|
|
17
|
-
});
|
|
18
|
-
return "https-ok";
|
|
19
|
-
} catch {
|
|
20
|
-
return "https-failed";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* The operator-facing explanation, given the original error and what the probe
|
|
25
|
-
* found. Pure, so the wording is testable without a network.
|
|
26
|
-
*
|
|
27
|
-
* Every branch keeps the original error text: it is the only part that is
|
|
28
|
-
* certainly true, and a hint that replaces evidence is worse than no hint.
|
|
29
|
-
*/
|
|
30
|
-
function signalingFailureHint(cause, probe, host) {
|
|
31
|
-
const head = `cannot reach the signaling host ${host}: ${cause}`;
|
|
32
|
-
switch (probe) {
|
|
33
|
-
case "https-ok": return `${head}\n HTTPS to ${host} works from here, so DNS, TLS and egress are fine and the\n server is up — only the WebSocket did not connect. Something in between is\n allowing HTTPS and refusing the upgrade, which is what a coding-agent\n sandbox or an intercepting proxy does.\n If you are running inside a sandbox (Codex and similar): re-run with\n escalated / external-network permission. If you are behind a corporate\n proxy: it must allow WebSocket upgrades to ${host}.`;
|
|
34
|
-
case "https-failed": return `${head}\n HTTPS to ${host} does not work from here either, so this is not specific to\n WebSockets — either this environment has no outbound network, or the host\n is genuinely unreachable.\n If you are running inside a sandbox (Codex and similar): re-run with\n escalated / external-network permission. Otherwise check connectivity and\n whether ${host} is up.`;
|
|
35
|
-
default: return `${head}\n If you are running inside a coding-agent sandbox, outbound WebSockets are\n often blocked — re-run with escalated / external-network permission.`;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region ts/webrtcSignal.ts
|
|
41
|
-
var ConnectError = class extends Error {
|
|
42
|
-
constructor(phase, message) {
|
|
43
|
-
super(message);
|
|
44
|
-
this.phase = phase;
|
|
45
|
-
this.name = "ConnectError";
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
/** Classify one signaling frame against the current state. */
|
|
49
|
-
function reduceSignal(state, m) {
|
|
50
|
-
if (!m || typeof m !== "object") return { kind: "ignore" };
|
|
51
|
-
switch (m.type) {
|
|
52
|
-
case "welcome":
|
|
53
|
-
if (m.hostPresent === false) return {
|
|
54
|
-
kind: "fail",
|
|
55
|
-
error: new ConnectError("no-host", "the room is open but no host is attached to it")
|
|
56
|
-
};
|
|
57
|
-
return { kind: "ignore" };
|
|
58
|
-
case "offer":
|
|
59
|
-
if (state.offered) return { kind: "duplicate-offer" };
|
|
60
|
-
return {
|
|
61
|
-
kind: "offer",
|
|
62
|
-
sdp: m.sdp,
|
|
63
|
-
iceServers: m.iceServers || [],
|
|
64
|
-
from: m.from
|
|
65
|
-
};
|
|
66
|
-
case "candidate": return {
|
|
67
|
-
kind: "candidate",
|
|
68
|
-
candidate: m.candidate,
|
|
69
|
-
queue: !state.offered
|
|
70
|
-
};
|
|
71
|
-
default: return { kind: "ignore" };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/** The error for a socket that closed before the channel was confirmed. */
|
|
75
|
-
function closeError(state, code, reason) {
|
|
76
|
-
const why = `signaling closed (code ${code}${reason ? `: ${reason}` : ""})`;
|
|
77
|
-
if (!state.opened) return new ConnectError("ws-not-open", `${why} before the socket opened`);
|
|
78
|
-
if (code === 1008 || reason) return new ConnectError("rejected", why);
|
|
79
|
-
if (!state.offered) return new ConnectError("no-host", `${why} before the host offered`);
|
|
80
|
-
return new ConnectError("ice-timeout", `${why} during the WebRTC handshake`);
|
|
81
|
-
}
|
|
82
|
-
/** The error for the connect deadline expiring. */
|
|
83
|
-
function timeoutError(state, timeoutMs) {
|
|
84
|
-
const s = `${Math.round(timeoutMs / 1e3)}s`;
|
|
85
|
-
if (!state.opened) return new ConnectError("ws-not-open", `signaling did not open within ${s}`);
|
|
86
|
-
if (!state.offered) return new ConnectError("no-host", `no host answered the join within ${s}`);
|
|
87
|
-
return new ConnectError("ice-timeout", `no WebRTC data channel came up within ${s}`);
|
|
88
|
-
}
|
|
89
|
-
/** The error for a WebSocket `error` event. */
|
|
90
|
-
function socketError(state, detail) {
|
|
91
|
-
return state.opened ? new ConnectError("rejected", `signaling error after connect: ${detail}`) : new ConnectError("ws-not-open", `signaling error: ${detail}`);
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* The operator-facing explanation. `sandboxHint` is the HTTPS-probe-derived
|
|
95
|
-
* note from signalingHint.ts and is consulted ONLY for a socket that never
|
|
96
|
-
* opened: every later phase proves the WebSocket upgrade worked, so blaming a
|
|
97
|
-
* sandbox or proxy there would be wrong — and sent one reader down exactly
|
|
98
|
-
* that path.
|
|
99
|
-
*/
|
|
100
|
-
function describeConnectFailure(err, host, sandboxHint) {
|
|
101
|
-
const phase = err instanceof ConnectError ? err.phase : "ws-not-open";
|
|
102
|
-
const cause = err.message;
|
|
103
|
-
switch (phase) {
|
|
104
|
-
case "no-host": return Promise.resolve(`no host in the room at ${host}: ${cause}\n The signaling server is reachable and accepted the join — the daemon on the\n other machine is not attached to this room. There, run \`ay share status\`:\n if it prints a different link, the room rotated (or the daemon was\n reinstalled) and this saved remote is stale — re-add it with the new link.`);
|
|
105
|
-
case "rejected": return Promise.resolve(`join rejected by the signaling host ${host}: ${cause}\n The room id or secret in this saved remote no longer matches what the host\n serves (it rotated, or the link was copied from a different machine).\n On the host run \`ay share status\` and re-add the remote with that link.`);
|
|
106
|
-
case "ice-timeout": return Promise.resolve(`WebRTC handshake with the host behind ${host} failed: ${cause}\n Signaling worked and the host answered, so this is not a sandbox or proxy\n problem — the two machines could not open a data channel. Usual causes: a\n second host answering in the same room (on the host: \`ay serve status\`,\n and kill any stray \`ayrs serve\`), or ICE blocked between the networks.`);
|
|
107
|
-
default: return sandboxHint(cause);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
//#endregion
|
|
112
|
-
//#region ts/webrtcRemote.ts
|
|
113
|
-
const CONNECT_TIMEOUT_MS = 25e3;
|
|
114
|
-
/** A live, key-confirmed WebRTC connection to a single share room. */
|
|
115
|
-
var WebRtcConn = class {
|
|
116
|
-
ws;
|
|
117
|
-
pc = null;
|
|
118
|
-
dc = null;
|
|
119
|
-
send = { sendCtr: 0n };
|
|
120
|
-
recv = { lastSeen: -1n };
|
|
121
|
-
keyC2H = null;
|
|
122
|
-
keyH2C = null;
|
|
123
|
-
th = null;
|
|
124
|
-
hostPeer = null;
|
|
125
|
-
myNonce = randomHex(16);
|
|
126
|
-
confirmedIn = false;
|
|
127
|
-
confirmedOut = false;
|
|
128
|
-
confirmed = false;
|
|
129
|
-
sig = {
|
|
130
|
-
opened: false,
|
|
131
|
-
offered: false,
|
|
132
|
-
confirmed: false
|
|
133
|
-
};
|
|
134
|
-
earlyCandidates = [];
|
|
135
|
-
warnedDuplicateHost = false;
|
|
136
|
-
sendChain = Promise.resolve();
|
|
137
|
-
recvChain = Promise.resolve();
|
|
138
|
-
idCounter = 0;
|
|
139
|
-
pending = /* @__PURE__ */ new Map();
|
|
140
|
-
ready;
|
|
141
|
-
resolveReady;
|
|
142
|
-
rejectReady;
|
|
143
|
-
constructor(link) {
|
|
144
|
-
this.link = link;
|
|
145
|
-
this.ready = new Promise((res, rej) => {
|
|
146
|
-
this.resolveReady = res;
|
|
147
|
-
this.rejectReady = rej;
|
|
148
|
-
});
|
|
149
|
-
const timer = setTimeout(() => this.rejectReady(timeoutError(this.sig, CONNECT_TIMEOUT_MS)), CONNECT_TIMEOUT_MS);
|
|
150
|
-
this.ready.then(() => clearTimeout(timer), () => clearTimeout(timer));
|
|
151
|
-
this.ws = this.dial();
|
|
152
|
-
}
|
|
153
|
-
dial() {
|
|
154
|
-
const { room, host } = this.link;
|
|
155
|
-
const ws = new WebSocket(`wss://${host}/${room}`, [SIGNAL_SUBPROTOCOL]);
|
|
156
|
-
ws.onopen = async () => {
|
|
157
|
-
this.sig.opened = true;
|
|
158
|
-
const authToken = await deriveAuthToken(this.link.s, room, host);
|
|
159
|
-
ws.send(JSON.stringify({
|
|
160
|
-
type: "hello",
|
|
161
|
-
role: "client",
|
|
162
|
-
v: 2,
|
|
163
|
-
token: authToken
|
|
164
|
-
}));
|
|
165
|
-
};
|
|
166
|
-
ws.onerror = (e) => this.rejectReady(socketError(this.sig, String(e?.message ?? e)));
|
|
167
|
-
ws.onclose = (ev) => {
|
|
168
|
-
if (!this.confirmed) this.rejectReady(closeError(this.sig, Number(ev?.code ?? 0), String(ev?.reason ?? "")));
|
|
169
|
-
};
|
|
170
|
-
ws.onmessage = (ev) => void this.onSignal(ev);
|
|
171
|
-
return ws;
|
|
172
|
-
}
|
|
173
|
-
async onSignal(ev) {
|
|
174
|
-
const m = JSON.parse(typeof ev.data === "string" ? ev.data : await ev.data.text());
|
|
175
|
-
const action = reduceSignal(this.sig, m);
|
|
176
|
-
if (action.kind === "fail") {
|
|
177
|
-
this.rejectReady(action.error);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (action.kind === "duplicate-offer") {
|
|
181
|
-
if (!this.warnedDuplicateHost) {
|
|
182
|
-
this.warnedDuplicateHost = true;
|
|
183
|
-
process.stderr.write(`[webrtc] a second host offered in room ${this.link.room} — two hosts serve this room; on the host, stop the stray one (\`ay serve status\`)\n`);
|
|
184
|
-
}
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (action.kind === "candidate") {
|
|
188
|
-
if (action.queue) this.earlyCandidates.push(action.candidate);
|
|
189
|
-
else if (this.pc) await this.pc.addIceCandidate(action.candidate).catch(() => {});
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (action.kind === "offer") {
|
|
193
|
-
this.sig.offered = true;
|
|
194
|
-
this.hostPeer = action.from ?? null;
|
|
195
|
-
const pc = new RTCPeerConnection({ iceServers: action.iceServers || [] });
|
|
196
|
-
this.pc = pc;
|
|
197
|
-
let held = [];
|
|
198
|
-
const sendCandidate = (candidate) => this.ws.send(JSON.stringify({
|
|
199
|
-
type: "candidate",
|
|
200
|
-
to: this.hostPeer,
|
|
201
|
-
candidate
|
|
202
|
-
}));
|
|
203
|
-
pc.onicecandidate = (e) => {
|
|
204
|
-
if (!e.candidate) return;
|
|
205
|
-
if (held) held.push(e.candidate);
|
|
206
|
-
else sendCandidate(e.candidate);
|
|
207
|
-
};
|
|
208
|
-
pc.ondatachannel = (e) => {
|
|
209
|
-
const dc = e.channel;
|
|
210
|
-
this.dc = dc;
|
|
211
|
-
dc.binaryType = "arraybuffer";
|
|
212
|
-
dc.onopen = () => this.enqueueSeal(FLAG_CONFIRM, {
|
|
213
|
-
t: "confirm",
|
|
214
|
-
nonce: this.myNonce
|
|
215
|
-
});
|
|
216
|
-
dc.onmessage = (ev2) => {
|
|
217
|
-
this.recvChain = this.recvChain.then(() => this.onFrame(ev2.data)).catch(() => {});
|
|
218
|
-
};
|
|
219
|
-
};
|
|
220
|
-
await pc.setRemoteDescription({
|
|
221
|
-
type: "offer",
|
|
222
|
-
sdp: action.sdp
|
|
223
|
-
});
|
|
224
|
-
const early = this.earlyCandidates;
|
|
225
|
-
this.earlyCandidates = [];
|
|
226
|
-
for (const c of early) await pc.addIceCandidate(c).catch(() => {});
|
|
227
|
-
const answer = await pc.createAnswer();
|
|
228
|
-
await pc.setLocalDescription(answer);
|
|
229
|
-
this.th = await computeTranscriptHash(action.sdp, pc.localDescription.sdp);
|
|
230
|
-
const keys = await deriveDirKeys(this.link.s, this.th);
|
|
231
|
-
this.keyC2H = keys.keyC2H;
|
|
232
|
-
this.keyH2C = keys.keyH2C;
|
|
233
|
-
this.ws.send(JSON.stringify({
|
|
234
|
-
type: "answer",
|
|
235
|
-
to: this.hostPeer,
|
|
236
|
-
sdp: pc.localDescription.sdp
|
|
237
|
-
}));
|
|
238
|
-
const queued = held;
|
|
239
|
-
held = null;
|
|
240
|
-
for (const c of queued) sendCandidate(c);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
enqueueSeal(flags, obj) {
|
|
244
|
-
this.sendChain = this.sendChain.then(async () => {
|
|
245
|
-
if (!this.dc || this.dc.readyState !== "open" || !this.keyC2H || !this.th) return;
|
|
246
|
-
const frame = await seal(this.keyC2H, this.send, flags, this.th, packEnvelope(obj));
|
|
247
|
-
try {
|
|
248
|
-
this.dc.send(frame);
|
|
249
|
-
} catch {}
|
|
250
|
-
});
|
|
251
|
-
return this.sendChain;
|
|
252
|
-
}
|
|
253
|
-
async onFrame(data) {
|
|
254
|
-
if (typeof data === "string" || !this.keyH2C || !this.th) return;
|
|
255
|
-
let env;
|
|
256
|
-
try {
|
|
257
|
-
const { plaintext } = await open(this.keyH2C, data, this.th, this.recv);
|
|
258
|
-
env = unpackEnvelope(plaintext);
|
|
259
|
-
} catch {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (!this.confirmed) {
|
|
263
|
-
if (!env || env.t !== "confirm") return;
|
|
264
|
-
if (typeof env.nonce === "string" && !this.confirmedOut) {
|
|
265
|
-
await this.enqueueSeal(FLAG_CONFIRM, {
|
|
266
|
-
t: "confirm",
|
|
267
|
-
nonce: this.myNonce,
|
|
268
|
-
echo: env.nonce
|
|
269
|
-
});
|
|
270
|
-
this.confirmedOut = true;
|
|
271
|
-
}
|
|
272
|
-
if (env.echo && env.echo === this.myNonce) this.confirmedIn = true;
|
|
273
|
-
if (this.confirmedIn && this.confirmedOut) {
|
|
274
|
-
this.confirmed = true;
|
|
275
|
-
this.sig.confirmed = true;
|
|
276
|
-
this.resolveReady();
|
|
277
|
-
}
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (!env || env.t === "confirm") return;
|
|
281
|
-
const p = this.pending.get(env.id);
|
|
282
|
-
if (!p) return;
|
|
283
|
-
if (env.t === "res") p.onRes(env.status, env.ct);
|
|
284
|
-
else if (env.t === "data") p.onData(env.chunk);
|
|
285
|
-
else if (env.t === "end") p.onEnd(env.error);
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Issue one request over the channel. Resolves once the response head (status,
|
|
289
|
-
* content-type) arrives; the body is a ReadableStream fed by `data` frames as
|
|
290
|
-
* they land, so streaming endpoints (SSE `tail`) flow without buffering.
|
|
291
|
-
*/
|
|
292
|
-
request(method, path, body) {
|
|
293
|
-
const id = String(++this.idCounter);
|
|
294
|
-
return new Promise((resolve, reject) => {
|
|
295
|
-
let controller;
|
|
296
|
-
const enc = new TextEncoder();
|
|
297
|
-
let head = false;
|
|
298
|
-
const stream = new ReadableStream({
|
|
299
|
-
start: (c) => {
|
|
300
|
-
controller = c;
|
|
301
|
-
},
|
|
302
|
-
cancel: () => {
|
|
303
|
-
this.pending.delete(id);
|
|
304
|
-
this.enqueueSeal(0, {
|
|
305
|
-
t: "abort",
|
|
306
|
-
id
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
this.pending.set(id, {
|
|
311
|
-
onRes: (status, ct) => {
|
|
312
|
-
if (!head) {
|
|
313
|
-
head = true;
|
|
314
|
-
resolve({
|
|
315
|
-
status,
|
|
316
|
-
ct,
|
|
317
|
-
stream
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
onData: (chunk) => {
|
|
322
|
-
try {
|
|
323
|
-
controller.enqueue(enc.encode(chunk));
|
|
324
|
-
} catch {}
|
|
325
|
-
},
|
|
326
|
-
onEnd: (error) => {
|
|
327
|
-
this.pending.delete(id);
|
|
328
|
-
if (error) {
|
|
329
|
-
if (!head) {
|
|
330
|
-
head = true;
|
|
331
|
-
reject(new Error(error));
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
controller.error(new Error(error));
|
|
335
|
-
} catch {}
|
|
336
|
-
} else try {
|
|
337
|
-
controller.close();
|
|
338
|
-
} catch {}
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
this.enqueueSeal(0, {
|
|
342
|
-
t: "req",
|
|
343
|
-
id,
|
|
344
|
-
method,
|
|
345
|
-
path,
|
|
346
|
-
body
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
close() {
|
|
351
|
-
try {
|
|
352
|
-
this.ws.close();
|
|
353
|
-
} catch {}
|
|
354
|
-
try {
|
|
355
|
-
this.pc?.close();
|
|
356
|
-
} catch {}
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
/**
|
|
360
|
-
* Connect to a share room and start a local HTTP server that forwards every
|
|
361
|
-
* request over the encrypted DataChannel. Returns the loopback base URL the
|
|
362
|
-
* existing remote commands can `fetch()` against. The process owns teardown:
|
|
363
|
-
* `ay` subcommands `process.exit()` when done, which closes the socket/peer.
|
|
364
|
-
*/
|
|
365
|
-
async function startWebrtcBridge(link) {
|
|
366
|
-
const parsed = parseWebrtcLink(link);
|
|
367
|
-
if (!parsed) throw new Error(`not a WebRTC share link: ${link}`);
|
|
368
|
-
const conn = new WebRtcConn(parsed);
|
|
369
|
-
try {
|
|
370
|
-
await conn.ready;
|
|
371
|
-
} catch (e) {
|
|
372
|
-
const err = e instanceof Error ? e : new Error(String(e));
|
|
373
|
-
throw new Error(await describeConnectFailure(err, parsed.host, async (cause) => signalingFailureHint(cause, await probeSignalingHost(parsed.host), parsed.host)));
|
|
374
|
-
}
|
|
375
|
-
const server = Bun.serve({
|
|
376
|
-
port: 0,
|
|
377
|
-
hostname: "127.0.0.1",
|
|
378
|
-
idleTimeout: 0,
|
|
379
|
-
async fetch(req) {
|
|
380
|
-
const u = new URL(req.url);
|
|
381
|
-
const pathWithQuery = u.pathname + u.search;
|
|
382
|
-
const body = req.method !== "GET" && req.method !== "HEAD" ? await req.text() : void 0;
|
|
383
|
-
try {
|
|
384
|
-
const { status, ct, stream } = await conn.request(req.method, pathWithQuery, body);
|
|
385
|
-
return new Response(stream, {
|
|
386
|
-
status,
|
|
387
|
-
headers: ct ? { "content-type": ct } : {}
|
|
388
|
-
});
|
|
389
|
-
} catch (e) {
|
|
390
|
-
return new Response(String(e?.message ?? e), { status: 502 });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
return {
|
|
395
|
-
baseUrl: `http://127.0.0.1:${server.port}`,
|
|
396
|
-
token: "webrtc",
|
|
397
|
-
close: () => {
|
|
398
|
-
try {
|
|
399
|
-
server.stop(true);
|
|
400
|
-
} catch {}
|
|
401
|
-
conn.close();
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
//#endregion
|
|
407
|
-
export { startWebrtcBridge };
|
|
408
|
-
//# sourceMappingURL=webrtcRemote-BYy2lTTl.js.map
|