@agentproto/acp 0.4.0 → 0.5.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/LICENSE +202 -21
- package/dist/client/index.d.ts +21 -2
- package/dist/client/index.mjs +120 -17
- package/dist/client/index.mjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/manifest/index.d.ts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/tunnel/index.d.ts +268 -3
- package/dist/tunnel/index.mjs +331 -4
- package/dist/tunnel/index.mjs.map +1 -1
- package/dist/{types-Sxd1kKk2.d.ts → types-DQCJO97x.d.ts} +41 -1
- package/package.json +4 -3
package/dist/tunnel/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { createCipheriv, randomUUID, createDecipheriv } from 'crypto';
|
|
1
2
|
import { spawn } from 'child_process';
|
|
2
3
|
import { platform, hostname } from 'os';
|
|
3
4
|
import { EventEmitter } from 'events';
|
|
4
5
|
import { Readable, Writable } from 'stream';
|
|
5
|
-
import { randomUUID } from 'crypto';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @agentproto/acp v0.1.0-alpha
|
|
@@ -32,7 +32,9 @@ var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
|
32
32
|
"stdout",
|
|
33
33
|
"stderr",
|
|
34
34
|
"exit",
|
|
35
|
-
"reconnect_soon"
|
|
35
|
+
"reconnect_soon",
|
|
36
|
+
"e2e",
|
|
37
|
+
"e2e_handshake"
|
|
36
38
|
]);
|
|
37
39
|
function parseFrame(raw) {
|
|
38
40
|
let parsed;
|
|
@@ -58,6 +60,321 @@ function encodeData(bytes) {
|
|
|
58
60
|
function decodeData(data) {
|
|
59
61
|
return Buffer.from(data, "base64");
|
|
60
62
|
}
|
|
63
|
+
var E2eError = class extends Error {
|
|
64
|
+
code;
|
|
65
|
+
constructor(code, message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "E2eError";
|
|
68
|
+
this.code = code;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var DEFAULT_E2E_MAX_FRAMES = 4294967296;
|
|
72
|
+
var TAG_LEN = 16;
|
|
73
|
+
var NONCE_LEN = 12;
|
|
74
|
+
function nonceFor(n) {
|
|
75
|
+
const nonce = Buffer.alloc(NONCE_LEN);
|
|
76
|
+
nonce.writeBigUInt64BE(BigInt(n), NONCE_LEN - 8);
|
|
77
|
+
return nonce;
|
|
78
|
+
}
|
|
79
|
+
function aadFor(n) {
|
|
80
|
+
const aad = Buffer.alloc(8);
|
|
81
|
+
aad.writeBigUInt64BE(BigInt(n));
|
|
82
|
+
return aad;
|
|
83
|
+
}
|
|
84
|
+
function wrapE2E(inner, keys, opts = {}) {
|
|
85
|
+
const maxFrames = opts.maxFrames ?? DEFAULT_E2E_MAX_FRAMES;
|
|
86
|
+
const sendKey = Buffer.from(keys.sendKey);
|
|
87
|
+
const recvKey = Buffer.from(keys.recvKey);
|
|
88
|
+
const frameHandlers = /* @__PURE__ */ new Set();
|
|
89
|
+
const closeHandlers = /* @__PURE__ */ new Set();
|
|
90
|
+
let sentCount = 0;
|
|
91
|
+
let recvExpected = 0;
|
|
92
|
+
let recvCount = 0;
|
|
93
|
+
let open = inner.isOpen;
|
|
94
|
+
let failed = false;
|
|
95
|
+
const fail = (err) => {
|
|
96
|
+
if (failed) return;
|
|
97
|
+
failed = true;
|
|
98
|
+
opts.onSecurityError?.(err);
|
|
99
|
+
closeChannel(err.message);
|
|
100
|
+
};
|
|
101
|
+
const closeChannel = (reason) => {
|
|
102
|
+
if (!open) return;
|
|
103
|
+
open = false;
|
|
104
|
+
inner.close(reason);
|
|
105
|
+
for (const h of closeHandlers) h(reason);
|
|
106
|
+
frameHandlers.clear();
|
|
107
|
+
closeHandlers.clear();
|
|
108
|
+
};
|
|
109
|
+
const onInner = (frame) => {
|
|
110
|
+
if (!open) return;
|
|
111
|
+
if (frame.t !== "e2e") {
|
|
112
|
+
fail(
|
|
113
|
+
new E2eError(
|
|
114
|
+
"not_e2e",
|
|
115
|
+
`expected an e2e envelope, received a plaintext "${frame.t}" frame`
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const n = frame.n;
|
|
121
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0) {
|
|
122
|
+
fail(new E2eError("decode", "e2e envelope has an invalid counter"));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (n < recvExpected) {
|
|
126
|
+
fail(new E2eError("replay", `replayed frame: counter ${n} < expected ${recvExpected}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (n > recvExpected) {
|
|
130
|
+
fail(new E2eError("reorder", `out-of-order or dropped frame: counter ${n} > expected ${recvExpected}`));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let buf;
|
|
134
|
+
try {
|
|
135
|
+
buf = Buffer.from(frame.d, "base64");
|
|
136
|
+
} catch {
|
|
137
|
+
fail(new E2eError("decode", "e2e envelope payload is not valid base64"));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (buf.length < TAG_LEN) {
|
|
141
|
+
fail(new E2eError("decode", "e2e envelope payload is too short to hold a tag"));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const ct = buf.subarray(0, buf.length - TAG_LEN);
|
|
145
|
+
const tag = buf.subarray(buf.length - TAG_LEN);
|
|
146
|
+
let plaintext;
|
|
147
|
+
try {
|
|
148
|
+
const decipher = createDecipheriv("aes-256-gcm", recvKey, nonceFor(n));
|
|
149
|
+
decipher.setAAD(aadFor(n));
|
|
150
|
+
decipher.setAuthTag(tag);
|
|
151
|
+
plaintext = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
152
|
+
} catch {
|
|
153
|
+
fail(new E2eError("auth", `frame ${n} failed authentication \u2014 tampered ciphertext or wrong key`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
recvExpected = n + 1;
|
|
157
|
+
recvCount += 1;
|
|
158
|
+
const decoded = parseFrame(plaintext.toString("utf8"));
|
|
159
|
+
if (!decoded) {
|
|
160
|
+
fail(new E2eError("decode", "decrypted payload was not a valid tunnel frame"));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const h of frameHandlers) h(decoded);
|
|
164
|
+
};
|
|
165
|
+
const unsubInner = inner.onFrame(onInner);
|
|
166
|
+
const unsubClose = inner.onClose((reason) => {
|
|
167
|
+
if (!open) return;
|
|
168
|
+
open = false;
|
|
169
|
+
for (const h of closeHandlers) h(reason);
|
|
170
|
+
frameHandlers.clear();
|
|
171
|
+
closeHandlers.clear();
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
get isOpen() {
|
|
175
|
+
return open;
|
|
176
|
+
},
|
|
177
|
+
get sentCount() {
|
|
178
|
+
return sentCount;
|
|
179
|
+
},
|
|
180
|
+
get recvCount() {
|
|
181
|
+
return recvCount;
|
|
182
|
+
},
|
|
183
|
+
send(frame) {
|
|
184
|
+
if (!open || failed) return;
|
|
185
|
+
if (sentCount >= maxFrames) {
|
|
186
|
+
fail(
|
|
187
|
+
new E2eError(
|
|
188
|
+
"overflow",
|
|
189
|
+
`send counter reached the rekey limit (${maxFrames}); a rekey is required`
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const n = sentCount;
|
|
195
|
+
const plaintext = Buffer.from(encodeFrame(frame), "utf8");
|
|
196
|
+
const cipher = createCipheriv("aes-256-gcm", sendKey, nonceFor(n));
|
|
197
|
+
cipher.setAAD(aadFor(n));
|
|
198
|
+
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
199
|
+
const tag = cipher.getAuthTag();
|
|
200
|
+
sentCount += 1;
|
|
201
|
+
inner.send({ t: "e2e", n, d: Buffer.concat([ct, tag]).toString("base64") });
|
|
202
|
+
},
|
|
203
|
+
close(reason) {
|
|
204
|
+
unsubInner();
|
|
205
|
+
unsubClose();
|
|
206
|
+
closeChannel(reason);
|
|
207
|
+
},
|
|
208
|
+
onFrame(handler) {
|
|
209
|
+
frameHandlers.add(handler);
|
|
210
|
+
return () => frameHandlers.delete(handler);
|
|
211
|
+
},
|
|
212
|
+
onClose(handler) {
|
|
213
|
+
closeHandlers.add(handler);
|
|
214
|
+
return () => closeHandlers.delete(handler);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
219
|
+
function b64(bytes) {
|
|
220
|
+
return Buffer.from(bytes).toString("base64");
|
|
221
|
+
}
|
|
222
|
+
function awaitHandshakeFrame(sink, timeoutMs) {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
let settled = false;
|
|
225
|
+
const finish = (fn) => {
|
|
226
|
+
if (settled) return;
|
|
227
|
+
settled = true;
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
unsubFrame();
|
|
230
|
+
unsubClose();
|
|
231
|
+
fn();
|
|
232
|
+
};
|
|
233
|
+
const timer = setTimeout(
|
|
234
|
+
() => finish(() => reject(new E2eError("decode", "handshake timed out"))),
|
|
235
|
+
timeoutMs
|
|
236
|
+
);
|
|
237
|
+
const unsubFrame = sink.onFrame((frame) => {
|
|
238
|
+
if (frame.t !== "e2e_handshake") {
|
|
239
|
+
finish(
|
|
240
|
+
() => reject(
|
|
241
|
+
new E2eError(
|
|
242
|
+
"not_e2e",
|
|
243
|
+
`expected an e2e_handshake frame, received "${frame.t}"`
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
finish(() => resolve(Buffer.from(frame.d, "base64")));
|
|
250
|
+
});
|
|
251
|
+
const unsubClose = sink.onClose(
|
|
252
|
+
(reason) => finish(
|
|
253
|
+
() => reject(new E2eError("decode", `transport closed during handshake: ${reason ?? "unknown"}`))
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function clientHandshakeOverSink(sink, hello, deriveKeys, opts = {}) {
|
|
259
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
260
|
+
const replyPromise = awaitHandshakeFrame(sink, timeoutMs);
|
|
261
|
+
sink.send({ t: "e2e_handshake", d: b64(hello) });
|
|
262
|
+
const reply = await replyPromise;
|
|
263
|
+
let keys;
|
|
264
|
+
try {
|
|
265
|
+
keys = deriveKeys(reply);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
sink.close("handshake failed");
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
return wrapE2E(sink, keys, opts.wrap);
|
|
271
|
+
}
|
|
272
|
+
async function daemonHandshakeOverSink(sink, respond, opts = {}) {
|
|
273
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
|
|
274
|
+
const hello = await awaitHandshakeFrame(sink, timeoutMs);
|
|
275
|
+
let result;
|
|
276
|
+
try {
|
|
277
|
+
result = respond(hello);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
sink.close("handshake failed");
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
sink.send({ t: "e2e_handshake", d: b64(result.reply) });
|
|
283
|
+
return wrapE2E(sink, result.keys, opts.wrap);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/tunnel/tunnel-e2e.ts
|
|
287
|
+
var DEFAULT_TUNNEL_E2E_TIMEOUT_MS = 8e3;
|
|
288
|
+
function b642(bytes) {
|
|
289
|
+
return Buffer.from(bytes).toString("base64");
|
|
290
|
+
}
|
|
291
|
+
function awaitFirstFrame(sink, timeoutMs) {
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
let settled = false;
|
|
294
|
+
const finish = (fn) => {
|
|
295
|
+
if (settled) return;
|
|
296
|
+
settled = true;
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
unsubFrame();
|
|
299
|
+
unsubClose();
|
|
300
|
+
fn();
|
|
301
|
+
};
|
|
302
|
+
const timer = setTimeout(() => finish(() => resolve(null)), timeoutMs);
|
|
303
|
+
const unsubFrame = sink.onFrame((frame) => finish(() => resolve(frame)));
|
|
304
|
+
const unsubClose = sink.onClose(
|
|
305
|
+
(reason) => finish(
|
|
306
|
+
() => reject(new E2eError("decode", `transport closed during e2e negotiation: ${reason ?? "unknown"}`))
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async function connectSinkE2E(sink, offer, deriveKeys, opts = {}) {
|
|
312
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TUNNEL_E2E_TIMEOUT_MS;
|
|
313
|
+
const firstPromise = awaitFirstFrame(sink, timeoutMs);
|
|
314
|
+
sink.send({ t: "e2e_handshake", d: b642(offer) });
|
|
315
|
+
const first = await firstPromise;
|
|
316
|
+
if (first === null) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
if (first.t !== "e2e_handshake") {
|
|
320
|
+
sink.close("e2e negotiation: unexpected frame");
|
|
321
|
+
throw new E2eError(
|
|
322
|
+
"not_e2e",
|
|
323
|
+
`expected an e2e_handshake accept, received "${first.t}"`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
let keys;
|
|
327
|
+
try {
|
|
328
|
+
keys = deriveKeys(Buffer.from(first.d, "base64"));
|
|
329
|
+
} catch (err) {
|
|
330
|
+
sink.close("e2e handshake failed");
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
return wrapE2E(sink, keys, opts.wrap);
|
|
334
|
+
}
|
|
335
|
+
async function acceptSinkE2E(sink, respond, opts = {}) {
|
|
336
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TUNNEL_E2E_TIMEOUT_MS;
|
|
337
|
+
const first = await awaitFirstFrame(sink, timeoutMs);
|
|
338
|
+
if (first === null) {
|
|
339
|
+
return { e2e: false, sink };
|
|
340
|
+
}
|
|
341
|
+
if (first.t !== "e2e_handshake") {
|
|
342
|
+
return { e2e: false, sink: prependFrame(sink, first) };
|
|
343
|
+
}
|
|
344
|
+
let result;
|
|
345
|
+
try {
|
|
346
|
+
result = respond(Buffer.from(first.d, "base64"));
|
|
347
|
+
} catch (err) {
|
|
348
|
+
sink.close("e2e handshake failed");
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
351
|
+
sink.send({ t: "e2e_handshake", d: b642(result.reply) });
|
|
352
|
+
return { e2e: true, sink: wrapE2E(sink, result.keys, opts.wrap) };
|
|
353
|
+
}
|
|
354
|
+
function prependFrame(inner, frame) {
|
|
355
|
+
let replayed = false;
|
|
356
|
+
return {
|
|
357
|
+
get isOpen() {
|
|
358
|
+
return inner.isOpen;
|
|
359
|
+
},
|
|
360
|
+
send(f) {
|
|
361
|
+
inner.send(f);
|
|
362
|
+
},
|
|
363
|
+
close(reason) {
|
|
364
|
+
inner.close(reason);
|
|
365
|
+
},
|
|
366
|
+
onFrame(handler) {
|
|
367
|
+
if (!replayed) {
|
|
368
|
+
replayed = true;
|
|
369
|
+
handler(frame);
|
|
370
|
+
}
|
|
371
|
+
return inner.onFrame(handler);
|
|
372
|
+
},
|
|
373
|
+
onClose(handler) {
|
|
374
|
+
return inner.onClose(handler);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
61
378
|
var DEFAULT_WS_DIAL_TIMEOUT_MS = 1e4;
|
|
62
379
|
var DEFAULT_HTTP_FORWARD_TIMEOUT_MS = 3e4;
|
|
63
380
|
function createTunnelServer(opts) {
|
|
@@ -71,7 +388,8 @@ function createTunnelServer(opts) {
|
|
|
71
388
|
capabilities: {
|
|
72
389
|
pty: opts.pty === true,
|
|
73
390
|
wsForward: opts.dialUpstreamWs !== void 0 && !!opts.httpUpstream,
|
|
74
|
-
...opts.tools && opts.tools.length ? { tools: opts.tools } : {}
|
|
391
|
+
...opts.tools && opts.tools.length ? { tools: opts.tools } : {},
|
|
392
|
+
...opts.e2e === true ? { e2e: true } : {}
|
|
75
393
|
},
|
|
76
394
|
label: opts.label,
|
|
77
395
|
daemon: {
|
|
@@ -549,6 +867,15 @@ function createTunnelServer(opts) {
|
|
|
549
867
|
if (HOP_BY_HOP.has(k.toLowerCase())) continue;
|
|
550
868
|
headers[k] = v;
|
|
551
869
|
}
|
|
870
|
+
if (opts.httpInjectHeaders) {
|
|
871
|
+
for (const [k, v] of Object.entries(opts.httpInjectHeaders)) {
|
|
872
|
+
const lower = k.toLowerCase();
|
|
873
|
+
for (const existing of Object.keys(headers)) {
|
|
874
|
+
if (existing.toLowerCase() === lower) delete headers[existing];
|
|
875
|
+
}
|
|
876
|
+
headers[k] = v;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
552
879
|
const url = `${opts.httpUpstream.replace(/\/$/, "")}${req.path}`;
|
|
553
880
|
const controller = new AbortController();
|
|
554
881
|
const timeoutMs = req.timeoutMs ?? opts.httpForwardTimeoutMs ?? DEFAULT_HTTP_FORWARD_TIMEOUT_MS;
|
|
@@ -1363,6 +1690,6 @@ function wrapWebSocket(ws) {
|
|
|
1363
1690
|
};
|
|
1364
1691
|
}
|
|
1365
1692
|
|
|
1366
|
-
export { DEFAULT_HTTP_FORWARD_TIMEOUT_MS, DEFAULT_WS_DIAL_TIMEOUT_MS, TUNNEL_VERSION, createTunnelClient, createTunnelServer, decodeData, encodeData, encodeFrame, parseFrame, wrapWebSocket };
|
|
1693
|
+
export { DEFAULT_E2E_MAX_FRAMES, DEFAULT_HTTP_FORWARD_TIMEOUT_MS, DEFAULT_TUNNEL_E2E_TIMEOUT_MS, DEFAULT_WS_DIAL_TIMEOUT_MS, E2eError, TUNNEL_VERSION, acceptSinkE2E, clientHandshakeOverSink, connectSinkE2E, createTunnelClient, createTunnelServer, daemonHandshakeOverSink, decodeData, encodeData, encodeFrame, parseFrame, wrapE2E, wrapWebSocket };
|
|
1367
1694
|
//# sourceMappingURL=index.mjs.map
|
|
1368
1695
|
//# sourceMappingURL=index.mjs.map
|