agent-shim 8.0.0 → 8.0.2
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/cli.cjs +304 -253
- package/dist/index.cjs +56 -16
- package/dist/index.mjs +56 -16
- package/dist/types/index.d.ts +12 -3
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -61039,7 +61039,7 @@ function registerIdentityCommand(program2, deps) {
|
|
|
61039
61039
|
// package.json
|
|
61040
61040
|
var package_default = {
|
|
61041
61041
|
name: "agent-shim",
|
|
61042
|
-
version: "8.0.
|
|
61042
|
+
version: "8.0.2",
|
|
61043
61043
|
description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
61044
61044
|
license: "MIT",
|
|
61045
61045
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -64665,14 +64665,11 @@ ${request6.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
64665
64665
|
};
|
|
64666
64666
|
const sessions = config2.interceptHosts.map((host) => {
|
|
64667
64667
|
if (config2.tapHosts.includes(host)) {
|
|
64668
|
-
const
|
|
64669
|
-
config2.
|
|
64670
|
-
|
|
64671
|
-
|
|
64672
|
-
|
|
64673
|
-
TAP_ALPN_PROTOCOLS
|
|
64674
|
-
);
|
|
64675
|
-
return { host, tlsAcceptor: tlsAcceptor2 };
|
|
64668
|
+
const serveSecure2 = (secure) => {
|
|
64669
|
+
pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config2.capture?.tapStream?.(host));
|
|
64670
|
+
};
|
|
64671
|
+
const tlsAcceptor2 = effects.createTlsAcceptor(config2.leafFor(host), serveSecure2, TAP_ALPN_PROTOCOLS);
|
|
64672
|
+
return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
|
|
64676
64673
|
}
|
|
64677
64674
|
const handler = (request6, response) => {
|
|
64678
64675
|
if (host === config2.routedHost && servedByPipeline(request6.url)) {
|
|
@@ -64686,19 +64683,29 @@ ${request6.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
64686
64683
|
effects.forwardHttp(config2.upstreamFor(host), request6, response, config2.capture?.observePassthrough(request6));
|
|
64687
64684
|
};
|
|
64688
64685
|
const httpSession = effects.createHttpSession(handler, onUpgrade(host));
|
|
64689
|
-
const
|
|
64690
|
-
tunnelTokens.set(secure,
|
|
64686
|
+
const serveSecure = (secure, capability) => {
|
|
64687
|
+
tunnelTokens.set(secure, capability);
|
|
64691
64688
|
httpSession.serve(secure);
|
|
64692
|
-
}
|
|
64693
|
-
|
|
64689
|
+
};
|
|
64690
|
+
const tlsAcceptor = effects.createTlsAcceptor(config2.leafFor(host), serveSecure);
|
|
64691
|
+
return { host, serveSecure, httpSession, tlsAcceptor };
|
|
64694
64692
|
});
|
|
64695
64693
|
const ledger = createConnectionLedger(config2.limits, config2.isLiveCapability);
|
|
64696
64694
|
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
64697
64695
|
handleConnect(socket, { config: config2, effects, sessions, ledger });
|
|
64698
64696
|
});
|
|
64699
64697
|
const apiSession = sessions.find((session) => session.host === config2.routedHost);
|
|
64698
|
+
const sessionByHost = new Map(sessions.map((session) => [session.host, session]));
|
|
64699
|
+
const sniAcceptor = effects.createSniTlsAcceptor(config2.interceptHosts, config2.leafFor, (secure, host) => {
|
|
64700
|
+
const session = sessionByHost.get(host);
|
|
64701
|
+
if (session === void 0) {
|
|
64702
|
+
secure.destroy();
|
|
64703
|
+
return;
|
|
64704
|
+
}
|
|
64705
|
+
session.serveSecure(secure, config2.transparentCapability ?? "");
|
|
64706
|
+
});
|
|
64700
64707
|
const transparent = config2.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback(config2.transparentPort, (socket) => {
|
|
64701
|
-
|
|
64708
|
+
sniAcceptor.accept(socket);
|
|
64702
64709
|
});
|
|
64703
64710
|
return {
|
|
64704
64711
|
port: listener.port,
|
|
@@ -64707,6 +64714,7 @@ ${request6.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
64707
64714
|
ledger.close();
|
|
64708
64715
|
await listener.close();
|
|
64709
64716
|
await transparent?.close();
|
|
64717
|
+
sniAcceptor.close();
|
|
64710
64718
|
for (const { httpSession, tlsAcceptor } of sessions) {
|
|
64711
64719
|
tlsAcceptor.close();
|
|
64712
64720
|
httpSession?.close();
|
|
@@ -65036,6 +65044,38 @@ function realConnectEffects() {
|
|
|
65036
65044
|
}
|
|
65037
65045
|
};
|
|
65038
65046
|
},
|
|
65047
|
+
createSniTlsAcceptor: (hosts, leafFor, onSecure) => {
|
|
65048
|
+
const contexts = new Map(
|
|
65049
|
+
hosts.map((host) => {
|
|
65050
|
+
const leaf = leafFor(host);
|
|
65051
|
+
return [host, tls.createSecureContext({ key: leaf.keyPem, cert: leaf.certPem })];
|
|
65052
|
+
})
|
|
65053
|
+
);
|
|
65054
|
+
const server = tls.createServer(
|
|
65055
|
+
{
|
|
65056
|
+
// No default key or cert exists, so the SNI selection is the only source of credentials: an unserved name fails its handshake here, and a client that sent no SNI at all never reaches the selection and fails for want of any certificate, which is exactly the fail-closed shape this surface wants.
|
|
65057
|
+
SNICallback: (servername, callback) => {
|
|
65058
|
+
const context = contexts.get(servername);
|
|
65059
|
+
callback(context === void 0 ? new Error(`the transparent surface serves no host named ${servername}`) : null, context);
|
|
65060
|
+
}
|
|
65061
|
+
},
|
|
65062
|
+
(secure) => {
|
|
65063
|
+
onSecure(secure, typeof secure.servername === "string" ? secure.servername : "");
|
|
65064
|
+
}
|
|
65065
|
+
);
|
|
65066
|
+
server.on("clientError", (_error, socket) => {
|
|
65067
|
+
socket.destroy();
|
|
65068
|
+
});
|
|
65069
|
+
return {
|
|
65070
|
+
// The same hand-off the single-leaf acceptor uses: emitting the connection event runs the TLS server's own connection listener on a socket this process already owns (the redirect half of the connection) rather than one the server accepted itself.
|
|
65071
|
+
accept: (socket) => {
|
|
65072
|
+
server.emit("connection", socket);
|
|
65073
|
+
},
|
|
65074
|
+
close: () => {
|
|
65075
|
+
server.close();
|
|
65076
|
+
}
|
|
65077
|
+
};
|
|
65078
|
+
},
|
|
65039
65079
|
createHttpSession: (handler, onUpgrade) => {
|
|
65040
65080
|
const server = http.createServer(handler);
|
|
65041
65081
|
server.on("clientError", (_error, socket) => {
|
|
@@ -68037,13 +68077,231 @@ function registerDoctorCommand(program2, deps) {
|
|
|
68037
68077
|
}
|
|
68038
68078
|
|
|
68039
68079
|
// src/headroom/commands.ts
|
|
68040
|
-
var
|
|
68080
|
+
var import_node_fs16 = __toESM(require("node:fs"), 1);
|
|
68041
68081
|
var import_node_http2 = __toESM(require("node:http"), 1);
|
|
68042
|
-
var
|
|
68082
|
+
var import_node_path36 = __toESM(require("node:path"), 1);
|
|
68043
68083
|
var import_node_child_process5 = require("node:child_process");
|
|
68044
68084
|
|
|
68045
|
-
// src/
|
|
68085
|
+
// src/frontdoor/realFrontDoorPort.ts
|
|
68086
|
+
var import_node_fs15 = __toESM(require("node:fs"), 1);
|
|
68087
|
+
|
|
68088
|
+
// src/frontdoor/ensure.ts
|
|
68046
68089
|
var import_node_crypto12 = require("node:crypto");
|
|
68090
|
+
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
68091
|
+
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
68092
|
+
var FrontDoorStartError = class extends CliError {
|
|
68093
|
+
constructor(message) {
|
|
68094
|
+
super(message);
|
|
68095
|
+
this.name = "FrontDoorStartError";
|
|
68096
|
+
}
|
|
68097
|
+
};
|
|
68098
|
+
function ensureFrontDoor(params) {
|
|
68099
|
+
const { paths, ports } = params;
|
|
68100
|
+
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
68101
|
+
let spawned = false;
|
|
68102
|
+
let replaced = false;
|
|
68103
|
+
const distrusted = /* @__PURE__ */ new Set();
|
|
68104
|
+
let lastRejection;
|
|
68105
|
+
for (; ; ) {
|
|
68106
|
+
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
68107
|
+
const supervisorPid = state?.supervisorPid;
|
|
68108
|
+
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
68109
|
+
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
68110
|
+
const verdict = ports.verifyListener(state.port);
|
|
68111
|
+
if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
|
|
68112
|
+
ports.stopSupervisor(supervisorPid);
|
|
68113
|
+
while (ports.isRunning(supervisorPid)) {
|
|
68114
|
+
if (ports.now() >= deadline) {
|
|
68115
|
+
throw new FrontDoorStartError(`agent-shim: the front door (pid ${String(supervisorPid)}, protocol ${String(state.protocol ?? 0)}) is older than this launcher (protocol ${String(FRONT_DOOR_PROTOCOL)}) and did not exit when asked. Daemon log: ${paths.frontdoorLogPath}`);
|
|
68116
|
+
}
|
|
68117
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
68118
|
+
}
|
|
68119
|
+
replaced = true;
|
|
68120
|
+
continue;
|
|
68121
|
+
}
|
|
68122
|
+
if (verdict.ok) {
|
|
68123
|
+
const token = (0, import_node_crypto12.randomUUID)();
|
|
68124
|
+
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
68125
|
+
return { port: state.port, connectPort: state.connectPort, token };
|
|
68126
|
+
}
|
|
68127
|
+
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
68128
|
+
if (spawned) {
|
|
68129
|
+
throw new FrontDoorStartError(`agent-shim: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
68130
|
+
}
|
|
68131
|
+
distrusted.add(supervisorPid);
|
|
68132
|
+
continue;
|
|
68133
|
+
}
|
|
68134
|
+
if (state?.lastError !== void 0) {
|
|
68135
|
+
throw new FrontDoorStartError(`agent-shim: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
68136
|
+
}
|
|
68137
|
+
} else if (!spawned) {
|
|
68138
|
+
ports.fs.mkdirp(paths.frontdoorDir);
|
|
68139
|
+
const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
68140
|
+
`);
|
|
68141
|
+
if (created) {
|
|
68142
|
+
ports.spawnSupervisor(paths);
|
|
68143
|
+
spawned = true;
|
|
68144
|
+
} else {
|
|
68145
|
+
const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
|
|
68146
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
68147
|
+
ports.fs.removeRecursive(paths.frontdoorLockFile);
|
|
68148
|
+
continue;
|
|
68149
|
+
}
|
|
68150
|
+
}
|
|
68151
|
+
}
|
|
68152
|
+
if (ports.now() >= deadline) {
|
|
68153
|
+
const lastError = state?.lastError;
|
|
68154
|
+
throw new FrontDoorStartError(
|
|
68155
|
+
`agent-shim: the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
|
|
68156
|
+
);
|
|
68157
|
+
}
|
|
68158
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
68159
|
+
}
|
|
68160
|
+
}
|
|
68161
|
+
|
|
68162
|
+
// src/frontdoor/probe.ts
|
|
68163
|
+
var import_node_worker_threads = require("node:worker_threads");
|
|
68164
|
+
var PROBE_TIMEOUT_MS = 2e3;
|
|
68165
|
+
var PROBE_WAIT_MARGIN_MS = 1e3;
|
|
68166
|
+
var REASON_BYTES = 1024;
|
|
68167
|
+
var HEADER_SLOTS = 2;
|
|
68168
|
+
var INT32_BYTES = 4;
|
|
68169
|
+
var VERDICT_PENDING = 0;
|
|
68170
|
+
var VERDICT_OK = 1;
|
|
68171
|
+
var VERDICT_FAILED = 2;
|
|
68172
|
+
var PROBE_SOURCE = `
|
|
68173
|
+
const { workerData } = require("node:worker_threads");
|
|
68174
|
+
const https = require("node:https");
|
|
68175
|
+
const { port, ca, timeoutMs, shared, ok, failed, headerSlots } = workerData;
|
|
68176
|
+
const header = new Int32Array(shared, 0, headerSlots);
|
|
68177
|
+
const reason = new Uint8Array(shared, headerSlots * 4);
|
|
68178
|
+
let done = false;
|
|
68179
|
+
const finish = (verdict, message) => {
|
|
68180
|
+
if (done) return;
|
|
68181
|
+
done = true;
|
|
68182
|
+
const bytes = Buffer.from(String(message), "utf8").subarray(0, reason.length);
|
|
68183
|
+
reason.set(bytes);
|
|
68184
|
+
Atomics.store(header, 1, bytes.length);
|
|
68185
|
+
Atomics.store(header, 0, verdict);
|
|
68186
|
+
Atomics.notify(header, 0);
|
|
68187
|
+
};
|
|
68188
|
+
const request = https.get({ host: "127.0.0.1", port, path: "/healthz", ca: [ca], agent: false }, (response) => {
|
|
68189
|
+
let body = "";
|
|
68190
|
+
response.setEncoding("utf8");
|
|
68191
|
+
response.on("data", (chunk) => { body += chunk; });
|
|
68192
|
+
response.on("end", () => {
|
|
68193
|
+
if (response.statusCode === 200 && body === "ok") finish(ok, "");
|
|
68194
|
+
else finish(failed, "the listener answered its health probe with HTTP " + String(response.statusCode));
|
|
68195
|
+
});
|
|
68196
|
+
response.on("error", (error) => finish(failed, error.message));
|
|
68197
|
+
});
|
|
68198
|
+
// A hard deadline over the whole exchange, handshake included: a listener that accepts the connection and then says nothing must still fail the probe.
|
|
68199
|
+
setTimeout(() => { request.destroy(new Error("no answer within " + String(timeoutMs) + "ms")); }, timeoutMs);
|
|
68200
|
+
request.on("error", (error) => finish(failed, (error.code ? error.code + ": " : "") + error.message));
|
|
68201
|
+
`;
|
|
68202
|
+
function probeFrontDoorSync(port, caPem) {
|
|
68203
|
+
const shared = new SharedArrayBuffer(HEADER_SLOTS * INT32_BYTES + REASON_BYTES);
|
|
68204
|
+
const header = new Int32Array(shared, 0, HEADER_SLOTS);
|
|
68205
|
+
const worker = new import_node_worker_threads.Worker(PROBE_SOURCE, {
|
|
68206
|
+
eval: true,
|
|
68207
|
+
workerData: { port, ca: caPem, timeoutMs: PROBE_TIMEOUT_MS, shared, ok: VERDICT_OK, failed: VERDICT_FAILED, headerSlots: HEADER_SLOTS }
|
|
68208
|
+
});
|
|
68209
|
+
worker.unref();
|
|
68210
|
+
try {
|
|
68211
|
+
const waited = Atomics.wait(header, 0, VERDICT_PENDING, PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS);
|
|
68212
|
+
const verdict = Atomics.load(header, 0);
|
|
68213
|
+
if (waited === "timed-out" || verdict === VERDICT_PENDING) {
|
|
68214
|
+
return { ok: false, reason: `the probe did not finish within ${String(PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS)}ms` };
|
|
68215
|
+
}
|
|
68216
|
+
if (verdict === VERDICT_OK) {
|
|
68217
|
+
return { ok: true };
|
|
68218
|
+
}
|
|
68219
|
+
const length = Atomics.load(header, 1);
|
|
68220
|
+
return { ok: false, reason: Buffer.from(new Uint8Array(shared, HEADER_SLOTS * INT32_BYTES, length)).toString("utf8") };
|
|
68221
|
+
} finally {
|
|
68222
|
+
void worker.terminate();
|
|
68223
|
+
}
|
|
68224
|
+
}
|
|
68225
|
+
|
|
68226
|
+
// src/frontdoor/trust.ts
|
|
68227
|
+
var import_node_crypto13 = require("node:crypto");
|
|
68228
|
+
var import_node_path35 = __toESM(require("node:path"), 1);
|
|
68229
|
+
var BUNDLE_NAME_HEX_DIGITS = 16;
|
|
68230
|
+
function resolveTrustBundle(params) {
|
|
68231
|
+
const { caCertFile, bundlesDir, variable, inherited, fs: fs19 } = params;
|
|
68232
|
+
if (inherited === void 0 || inherited === "" || import_node_path35.default.resolve(inherited) === import_node_path35.default.resolve(caCertFile)) {
|
|
68233
|
+
return { path: caCertFile };
|
|
68234
|
+
}
|
|
68235
|
+
const caPem = fs19.readFileUtf8(caCertFile);
|
|
68236
|
+
let inheritedPem;
|
|
68237
|
+
try {
|
|
68238
|
+
inheritedPem = fs19.readFileUtf8(inherited);
|
|
68239
|
+
} catch (error62) {
|
|
68240
|
+
return {
|
|
68241
|
+
path: caCertFile,
|
|
68242
|
+
warning: `agent-shim: ${variable} names ${inherited}, which could not be read (${error62 instanceof Error ? error62.message : String(error62)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
|
|
68243
|
+
};
|
|
68244
|
+
}
|
|
68245
|
+
if (inheritedPem.includes(caPem.trim())) {
|
|
68246
|
+
return { path: inherited };
|
|
68247
|
+
}
|
|
68248
|
+
const combined = `${inheritedPem.trimEnd()}
|
|
68249
|
+
${caPem}`;
|
|
68250
|
+
const bundle = import_node_path35.default.join(bundlesDir, `${(0, import_node_crypto13.createHash)("sha256").update(combined).digest("hex").slice(0, BUNDLE_NAME_HEX_DIGITS)}.pem`);
|
|
68251
|
+
if (!fs19.exists(bundle)) {
|
|
68252
|
+
fs19.mkdirp(bundlesDir);
|
|
68253
|
+
fs19.writeFileAtomic(bundle, combined);
|
|
68254
|
+
}
|
|
68255
|
+
return { path: bundle };
|
|
68256
|
+
}
|
|
68257
|
+
|
|
68258
|
+
// src/frontdoor/realFrontDoorPort.ts
|
|
68259
|
+
var realTrustBundleFs = {
|
|
68260
|
+
readFileUtf8: (file2) => import_node_fs15.default.readFileSync(file2, "utf8"),
|
|
68261
|
+
exists: (file2) => import_node_fs15.default.existsSync(file2),
|
|
68262
|
+
mkdirp: (dir) => {
|
|
68263
|
+
import_node_fs15.default.mkdirSync(dir, { recursive: true });
|
|
68264
|
+
},
|
|
68265
|
+
writeFileAtomic: (file2, contents) => {
|
|
68266
|
+
realFarmFs.writeFilePrivate(file2, contents);
|
|
68267
|
+
}
|
|
68268
|
+
};
|
|
68269
|
+
function realFrontDoorPort(paths, spawnDaemon) {
|
|
68270
|
+
return {
|
|
68271
|
+
ensure: (inheritedExtraCaCerts) => {
|
|
68272
|
+
const up = ensureFrontDoor({
|
|
68273
|
+
paths,
|
|
68274
|
+
launcherPid: process.pid,
|
|
68275
|
+
ports: {
|
|
68276
|
+
fs: realFarmFs,
|
|
68277
|
+
isRunning: realIsProcessRunning,
|
|
68278
|
+
now: () => Date.now(),
|
|
68279
|
+
sleep: realSleepSync,
|
|
68280
|
+
spawnSupervisor: (layout) => spawnDaemon(layout, "__frontdoor-supervisor", layout.frontdoorLogPath),
|
|
68281
|
+
stopSupervisor: (pid) => {
|
|
68282
|
+
process.kill(pid, "SIGTERM");
|
|
68283
|
+
},
|
|
68284
|
+
// Read fresh for each probe: a replacement supervisor may have regenerated an unparseable CA, and the probe must trust exactly what the serving door's leaf chains to.
|
|
68285
|
+
verifyListener: (port) => probeFrontDoorSync(port, import_node_fs15.default.readFileSync(paths.frontdoorCaCertFile, "utf8"))
|
|
68286
|
+
}
|
|
68287
|
+
});
|
|
68288
|
+
const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, variable: "NODE_EXTRA_CA_CERTS", inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
|
|
68289
|
+
return {
|
|
68290
|
+
port: up.port,
|
|
68291
|
+
connectPort: up.connectPort,
|
|
68292
|
+
trustBundlePath: trust.path,
|
|
68293
|
+
...trust.warning === void 0 ? {} : { trustWarning: trust.warning },
|
|
68294
|
+
sessionToken: up.token
|
|
68295
|
+
};
|
|
68296
|
+
},
|
|
68297
|
+
release: () => {
|
|
68298
|
+
removeFrontDoorSession(realFarmFs, paths.frontdoorSessionsDir, process.pid);
|
|
68299
|
+
}
|
|
68300
|
+
};
|
|
68301
|
+
}
|
|
68302
|
+
|
|
68303
|
+
// src/headroom/settings.ts
|
|
68304
|
+
var import_node_crypto14 = require("node:crypto");
|
|
68047
68305
|
function settingsOf(config2) {
|
|
68048
68306
|
return {
|
|
68049
68307
|
...config2.mode === void 0 ? {} : { mode: config2.mode },
|
|
@@ -68068,7 +68326,7 @@ function settingsEnv(settings2) {
|
|
|
68068
68326
|
return settings2.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings2.rolloutChannel };
|
|
68069
68327
|
}
|
|
68070
68328
|
function hashSettings(settings2) {
|
|
68071
|
-
return (0,
|
|
68329
|
+
return (0, import_node_crypto14.createHash)("sha256").update(JSON.stringify([...settingsArgs(settings2), JSON.stringify(settingsEnv(settings2))])).digest("hex");
|
|
68072
68330
|
}
|
|
68073
68331
|
|
|
68074
68332
|
// src/headroom/supervisor.ts
|
|
@@ -68413,7 +68671,14 @@ var READY_FETCH_TIMEOUT_MS = 2e3;
|
|
|
68413
68671
|
function headroomPidRunning(pid) {
|
|
68414
68672
|
return !exitedHeadroom.has(pid) && realIsProcessRunning(pid);
|
|
68415
68673
|
}
|
|
68416
|
-
|
|
68674
|
+
var HEADROOM_CA_BUNDLE = "HEADROOM_CA_BUNDLE";
|
|
68675
|
+
function resolveHeadroomTrustBundle(params) {
|
|
68676
|
+
if (!params.fs.exists(params.caCertFile)) {
|
|
68677
|
+
return void 0;
|
|
68678
|
+
}
|
|
68679
|
+
return resolveTrustBundle({ ...params, variable: HEADROOM_CA_BUNDLE });
|
|
68680
|
+
}
|
|
68681
|
+
function headroomSpawnEnv(parentEnv, allowlist, settings2, trustBundlePath) {
|
|
68417
68682
|
const inherited = { ...parentEnv };
|
|
68418
68683
|
delete inherited.HEADROOM_HOST;
|
|
68419
68684
|
delete inherited.HEADROOM_PORT;
|
|
@@ -68421,8 +68686,8 @@ function headroomSpawnEnv(parentEnv, allowlist, settings2, caBundle) {
|
|
|
68421
68686
|
...inherited,
|
|
68422
68687
|
HEADROOM_ALLOWED_BASE_URLS: allowlist.join(","),
|
|
68423
68688
|
HEADROOM_HTTP2: parentEnv.HEADROOM_HTTP2 ?? "0",
|
|
68424
|
-
//
|
|
68425
|
-
...
|
|
68689
|
+
// The bundle already holds any inherited HEADROOM_CA_BUNDLE's certificates, so replacing the variable loses nothing. An inherited SSL_CERT_FILE or REQUESTS_CA_BUNDLE passes through untouched and, by headroom's own precedence, still replaces everything: an explicit choice in the parent environment wins.
|
|
68690
|
+
...trustBundlePath === void 0 ? {} : { [HEADROOM_CA_BUNDLE]: trustBundlePath },
|
|
68426
68691
|
...settingsEnv(settings2)
|
|
68427
68692
|
};
|
|
68428
68693
|
}
|
|
@@ -68449,12 +68714,12 @@ function readInstalledHeadroomCommit() {
|
|
|
68449
68714
|
if (toolDir.status !== 0) {
|
|
68450
68715
|
return void 0;
|
|
68451
68716
|
}
|
|
68452
|
-
const libDir =
|
|
68717
|
+
const libDir = import_node_path36.default.join(toolDir.stdout.trim(), HEADROOM_TOOL_NAME, "lib");
|
|
68453
68718
|
for (const python of realFarmFs.readdir(libDir)) {
|
|
68454
|
-
const sitePackages =
|
|
68719
|
+
const sitePackages = import_node_path36.default.join(libDir, python, "site-packages");
|
|
68455
68720
|
for (const entry of realFarmFs.readdir(sitePackages)) {
|
|
68456
68721
|
if (entry.startsWith("headroom_ai-") && entry.endsWith(".dist-info")) {
|
|
68457
|
-
const directUrl = realFarmFs.readFileUtf8(
|
|
68722
|
+
const directUrl = realFarmFs.readFileUtf8(import_node_path36.default.join(sitePackages, entry, "direct_url.json"));
|
|
68458
68723
|
return directUrl === void 0 ? void 0 : parseInstalledCommit(directUrl);
|
|
68459
68724
|
}
|
|
68460
68725
|
}
|
|
@@ -68477,14 +68742,18 @@ function realSupervisorPorts(paths) {
|
|
|
68477
68742
|
socketTrust: realHeadroomSocketTrust,
|
|
68478
68743
|
readConfig: () => resolveSupervisorConfig(readGlobalConfig(paths)?.headroom ?? {}),
|
|
68479
68744
|
spawnHeadroom: (socketPath, allowlist, settings2) => {
|
|
68480
|
-
const
|
|
68481
|
-
|
|
68482
|
-
|
|
68745
|
+
const trust = resolveHeadroomTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: process.env[HEADROOM_CA_BUNDLE], fs: realTrustBundleFs });
|
|
68746
|
+
import_node_fs16.default.mkdirSync(paths.logsDir, { recursive: true });
|
|
68747
|
+
if (trust?.warning !== void 0) {
|
|
68748
|
+
import_node_fs16.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${trust.warning}
|
|
68749
|
+
`);
|
|
68750
|
+
}
|
|
68751
|
+
const logFd = import_node_fs16.default.openSync(paths.headroomLogPath, "a");
|
|
68483
68752
|
try {
|
|
68484
68753
|
const child = (0, import_node_child_process5.spawn)("headroom", headroomProxyArgs(socketPath, settings2), {
|
|
68485
68754
|
detached: true,
|
|
68486
68755
|
stdio: ["ignore", logFd, logFd],
|
|
68487
|
-
env: headroomSpawnEnv(process.env, allowlist, settings2,
|
|
68756
|
+
env: headroomSpawnEnv(process.env, allowlist, settings2, trust?.path)
|
|
68488
68757
|
});
|
|
68489
68758
|
if (child.pid === void 0) {
|
|
68490
68759
|
throw new Error("spawning headroom returned no pid");
|
|
@@ -68494,12 +68763,12 @@ function realSupervisorPorts(paths) {
|
|
|
68494
68763
|
child.once("exit", (code, signal) => {
|
|
68495
68764
|
ownedHeadroom.delete(pid);
|
|
68496
68765
|
exitedHeadroom.add(pid);
|
|
68497
|
-
|
|
68766
|
+
import_node_fs16.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} agent-shim headroom supervisor: headroom exited (${signal ?? `code ${String(code)}`})
|
|
68498
68767
|
`);
|
|
68499
68768
|
});
|
|
68500
68769
|
return pid;
|
|
68501
68770
|
} finally {
|
|
68502
|
-
|
|
68771
|
+
import_node_fs16.default.closeSync(logFd);
|
|
68503
68772
|
}
|
|
68504
68773
|
},
|
|
68505
68774
|
stopProcess: (pid) => {
|
|
@@ -68512,7 +68781,7 @@ function realSupervisorPorts(paths) {
|
|
|
68512
68781
|
now: () => Date.now()
|
|
68513
68782
|
});
|
|
68514
68783
|
if (outcome === "still-running") {
|
|
68515
|
-
|
|
68784
|
+
import_node_fs16.default.appendFileSync(
|
|
68516
68785
|
paths.headroomLogPath,
|
|
68517
68786
|
`${(/* @__PURE__ */ new Date()).toISOString()} agent-shim headroom supervisor: pid ${String(pid)} survived SIGKILL within its grace; it is stuck uninterruptibly
|
|
68518
68787
|
`
|
|
@@ -68526,10 +68795,10 @@ function realSupervisorPorts(paths) {
|
|
|
68526
68795
|
return { ok: false, error: `uv is not installed or not on PATH (${result.error.message})` };
|
|
68527
68796
|
}
|
|
68528
68797
|
if (result.stdout !== "") {
|
|
68529
|
-
|
|
68798
|
+
import_node_fs16.default.appendFileSync(paths.headroomLogPath, result.stdout);
|
|
68530
68799
|
}
|
|
68531
68800
|
if (result.stderr !== "") {
|
|
68532
|
-
|
|
68801
|
+
import_node_fs16.default.appendFileSync(paths.headroomLogPath, result.stderr);
|
|
68533
68802
|
}
|
|
68534
68803
|
return result.status === 0 ? { ok: true } : { ok: false, error: `uv tool install exited ${String(result.status)}` };
|
|
68535
68804
|
},
|
|
@@ -68539,8 +68808,8 @@ function realSupervisorPorts(paths) {
|
|
|
68539
68808
|
},
|
|
68540
68809
|
installedCommit: readInstalledHeadroomCommit,
|
|
68541
68810
|
log: (line) => {
|
|
68542
|
-
|
|
68543
|
-
|
|
68811
|
+
import_node_fs16.default.mkdirSync(paths.logsDir, { recursive: true });
|
|
68812
|
+
import_node_fs16.default.appendFileSync(paths.headroomLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
68544
68813
|
`);
|
|
68545
68814
|
}
|
|
68546
68815
|
};
|
|
@@ -69253,224 +69522,6 @@ function buildProgram(deps) {
|
|
|
69253
69522
|
var import_node_os8 = __toESM(require("node:os"), 1);
|
|
69254
69523
|
var import_node_crypto16 = require("node:crypto");
|
|
69255
69524
|
|
|
69256
|
-
// src/frontdoor/realFrontDoorPort.ts
|
|
69257
|
-
var import_node_fs16 = __toESM(require("node:fs"), 1);
|
|
69258
|
-
|
|
69259
|
-
// src/frontdoor/ensure.ts
|
|
69260
|
-
var import_node_crypto13 = require("node:crypto");
|
|
69261
|
-
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
69262
|
-
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
69263
|
-
var FrontDoorStartError = class extends CliError {
|
|
69264
|
-
constructor(message) {
|
|
69265
|
-
super(message);
|
|
69266
|
-
this.name = "FrontDoorStartError";
|
|
69267
|
-
}
|
|
69268
|
-
};
|
|
69269
|
-
function ensureFrontDoor(params) {
|
|
69270
|
-
const { paths, ports } = params;
|
|
69271
|
-
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
69272
|
-
let spawned = false;
|
|
69273
|
-
let replaced = false;
|
|
69274
|
-
const distrusted = /* @__PURE__ */ new Set();
|
|
69275
|
-
let lastRejection;
|
|
69276
|
-
for (; ; ) {
|
|
69277
|
-
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
69278
|
-
const supervisorPid = state?.supervisorPid;
|
|
69279
|
-
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
69280
|
-
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
69281
|
-
const verdict = ports.verifyListener(state.port);
|
|
69282
|
-
if (verdict.ok && (state.protocol ?? 0) < FRONT_DOOR_PROTOCOL && !replaced) {
|
|
69283
|
-
ports.stopSupervisor(supervisorPid);
|
|
69284
|
-
while (ports.isRunning(supervisorPid)) {
|
|
69285
|
-
if (ports.now() >= deadline) {
|
|
69286
|
-
throw new FrontDoorStartError(`agent-shim: the front door (pid ${String(supervisorPid)}, protocol ${String(state.protocol ?? 0)}) is older than this launcher (protocol ${String(FRONT_DOOR_PROTOCOL)}) and did not exit when asked. Daemon log: ${paths.frontdoorLogPath}`);
|
|
69287
|
-
}
|
|
69288
|
-
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
69289
|
-
}
|
|
69290
|
-
replaced = true;
|
|
69291
|
-
continue;
|
|
69292
|
-
}
|
|
69293
|
-
if (verdict.ok) {
|
|
69294
|
-
const token = (0, import_node_crypto13.randomUUID)();
|
|
69295
|
-
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
69296
|
-
return { port: state.port, connectPort: state.connectPort, token };
|
|
69297
|
-
}
|
|
69298
|
-
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
69299
|
-
if (spawned) {
|
|
69300
|
-
throw new FrontDoorStartError(`agent-shim: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
69301
|
-
}
|
|
69302
|
-
distrusted.add(supervisorPid);
|
|
69303
|
-
continue;
|
|
69304
|
-
}
|
|
69305
|
-
if (state?.lastError !== void 0) {
|
|
69306
|
-
throw new FrontDoorStartError(`agent-shim: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
69307
|
-
}
|
|
69308
|
-
} else if (!spawned) {
|
|
69309
|
-
ports.fs.mkdirp(paths.frontdoorDir);
|
|
69310
|
-
const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
69311
|
-
`);
|
|
69312
|
-
if (created) {
|
|
69313
|
-
ports.spawnSupervisor(paths);
|
|
69314
|
-
spawned = true;
|
|
69315
|
-
} else {
|
|
69316
|
-
const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
|
|
69317
|
-
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
69318
|
-
ports.fs.removeRecursive(paths.frontdoorLockFile);
|
|
69319
|
-
continue;
|
|
69320
|
-
}
|
|
69321
|
-
}
|
|
69322
|
-
}
|
|
69323
|
-
if (ports.now() >= deadline) {
|
|
69324
|
-
const lastError = state?.lastError;
|
|
69325
|
-
throw new FrontDoorStartError(
|
|
69326
|
-
`agent-shim: the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
|
|
69327
|
-
);
|
|
69328
|
-
}
|
|
69329
|
-
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
69330
|
-
}
|
|
69331
|
-
}
|
|
69332
|
-
|
|
69333
|
-
// src/frontdoor/probe.ts
|
|
69334
|
-
var import_node_worker_threads = require("node:worker_threads");
|
|
69335
|
-
var PROBE_TIMEOUT_MS = 2e3;
|
|
69336
|
-
var PROBE_WAIT_MARGIN_MS = 1e3;
|
|
69337
|
-
var REASON_BYTES = 1024;
|
|
69338
|
-
var HEADER_SLOTS = 2;
|
|
69339
|
-
var INT32_BYTES = 4;
|
|
69340
|
-
var VERDICT_PENDING = 0;
|
|
69341
|
-
var VERDICT_OK = 1;
|
|
69342
|
-
var VERDICT_FAILED = 2;
|
|
69343
|
-
var PROBE_SOURCE = `
|
|
69344
|
-
const { workerData } = require("node:worker_threads");
|
|
69345
|
-
const https = require("node:https");
|
|
69346
|
-
const { port, ca, timeoutMs, shared, ok, failed, headerSlots } = workerData;
|
|
69347
|
-
const header = new Int32Array(shared, 0, headerSlots);
|
|
69348
|
-
const reason = new Uint8Array(shared, headerSlots * 4);
|
|
69349
|
-
let done = false;
|
|
69350
|
-
const finish = (verdict, message) => {
|
|
69351
|
-
if (done) return;
|
|
69352
|
-
done = true;
|
|
69353
|
-
const bytes = Buffer.from(String(message), "utf8").subarray(0, reason.length);
|
|
69354
|
-
reason.set(bytes);
|
|
69355
|
-
Atomics.store(header, 1, bytes.length);
|
|
69356
|
-
Atomics.store(header, 0, verdict);
|
|
69357
|
-
Atomics.notify(header, 0);
|
|
69358
|
-
};
|
|
69359
|
-
const request = https.get({ host: "127.0.0.1", port, path: "/healthz", ca: [ca], agent: false }, (response) => {
|
|
69360
|
-
let body = "";
|
|
69361
|
-
response.setEncoding("utf8");
|
|
69362
|
-
response.on("data", (chunk) => { body += chunk; });
|
|
69363
|
-
response.on("end", () => {
|
|
69364
|
-
if (response.statusCode === 200 && body === "ok") finish(ok, "");
|
|
69365
|
-
else finish(failed, "the listener answered its health probe with HTTP " + String(response.statusCode));
|
|
69366
|
-
});
|
|
69367
|
-
response.on("error", (error) => finish(failed, error.message));
|
|
69368
|
-
});
|
|
69369
|
-
// A hard deadline over the whole exchange, handshake included: a listener that accepts the connection and then says nothing must still fail the probe.
|
|
69370
|
-
setTimeout(() => { request.destroy(new Error("no answer within " + String(timeoutMs) + "ms")); }, timeoutMs);
|
|
69371
|
-
request.on("error", (error) => finish(failed, (error.code ? error.code + ": " : "") + error.message));
|
|
69372
|
-
`;
|
|
69373
|
-
function probeFrontDoorSync(port, caPem) {
|
|
69374
|
-
const shared = new SharedArrayBuffer(HEADER_SLOTS * INT32_BYTES + REASON_BYTES);
|
|
69375
|
-
const header = new Int32Array(shared, 0, HEADER_SLOTS);
|
|
69376
|
-
const worker = new import_node_worker_threads.Worker(PROBE_SOURCE, {
|
|
69377
|
-
eval: true,
|
|
69378
|
-
workerData: { port, ca: caPem, timeoutMs: PROBE_TIMEOUT_MS, shared, ok: VERDICT_OK, failed: VERDICT_FAILED, headerSlots: HEADER_SLOTS }
|
|
69379
|
-
});
|
|
69380
|
-
worker.unref();
|
|
69381
|
-
try {
|
|
69382
|
-
const waited = Atomics.wait(header, 0, VERDICT_PENDING, PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS);
|
|
69383
|
-
const verdict = Atomics.load(header, 0);
|
|
69384
|
-
if (waited === "timed-out" || verdict === VERDICT_PENDING) {
|
|
69385
|
-
return { ok: false, reason: `the probe did not finish within ${String(PROBE_TIMEOUT_MS + PROBE_WAIT_MARGIN_MS)}ms` };
|
|
69386
|
-
}
|
|
69387
|
-
if (verdict === VERDICT_OK) {
|
|
69388
|
-
return { ok: true };
|
|
69389
|
-
}
|
|
69390
|
-
const length = Atomics.load(header, 1);
|
|
69391
|
-
return { ok: false, reason: Buffer.from(new Uint8Array(shared, HEADER_SLOTS * INT32_BYTES, length)).toString("utf8") };
|
|
69392
|
-
} finally {
|
|
69393
|
-
void worker.terminate();
|
|
69394
|
-
}
|
|
69395
|
-
}
|
|
69396
|
-
|
|
69397
|
-
// src/frontdoor/trust.ts
|
|
69398
|
-
var import_node_crypto14 = require("node:crypto");
|
|
69399
|
-
var import_node_path36 = __toESM(require("node:path"), 1);
|
|
69400
|
-
var BUNDLE_NAME_HEX_DIGITS = 16;
|
|
69401
|
-
function resolveTrustBundle(params) {
|
|
69402
|
-
const { caCertFile, bundlesDir, inherited, fs: fs19 } = params;
|
|
69403
|
-
if (inherited === void 0 || inherited === "" || import_node_path36.default.resolve(inherited) === import_node_path36.default.resolve(caCertFile)) {
|
|
69404
|
-
return { path: caCertFile };
|
|
69405
|
-
}
|
|
69406
|
-
const caPem = fs19.readFileUtf8(caCertFile);
|
|
69407
|
-
let inheritedPem;
|
|
69408
|
-
try {
|
|
69409
|
-
inheritedPem = fs19.readFileUtf8(inherited);
|
|
69410
|
-
} catch (error62) {
|
|
69411
|
-
return {
|
|
69412
|
-
path: caCertFile,
|
|
69413
|
-
warning: `agent-shim: NODE_EXTRA_CA_CERTS names ${inherited}, which could not be read (${error62 instanceof Error ? error62.message : String(error62)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
|
|
69414
|
-
};
|
|
69415
|
-
}
|
|
69416
|
-
if (inheritedPem.includes(caPem.trim())) {
|
|
69417
|
-
return { path: inherited };
|
|
69418
|
-
}
|
|
69419
|
-
const combined = `${inheritedPem.trimEnd()}
|
|
69420
|
-
${caPem}`;
|
|
69421
|
-
const bundle = import_node_path36.default.join(bundlesDir, `${(0, import_node_crypto14.createHash)("sha256").update(combined).digest("hex").slice(0, BUNDLE_NAME_HEX_DIGITS)}.pem`);
|
|
69422
|
-
if (!fs19.exists(bundle)) {
|
|
69423
|
-
fs19.mkdirp(bundlesDir);
|
|
69424
|
-
fs19.writeFileAtomic(bundle, combined);
|
|
69425
|
-
}
|
|
69426
|
-
return { path: bundle };
|
|
69427
|
-
}
|
|
69428
|
-
|
|
69429
|
-
// src/frontdoor/realFrontDoorPort.ts
|
|
69430
|
-
var realTrustBundleFs = {
|
|
69431
|
-
readFileUtf8: (file2) => import_node_fs16.default.readFileSync(file2, "utf8"),
|
|
69432
|
-
exists: (file2) => import_node_fs16.default.existsSync(file2),
|
|
69433
|
-
mkdirp: (dir) => {
|
|
69434
|
-
import_node_fs16.default.mkdirSync(dir, { recursive: true });
|
|
69435
|
-
},
|
|
69436
|
-
writeFileAtomic: (file2, contents) => {
|
|
69437
|
-
realFarmFs.writeFilePrivate(file2, contents);
|
|
69438
|
-
}
|
|
69439
|
-
};
|
|
69440
|
-
function realFrontDoorPort(paths, spawnDaemon) {
|
|
69441
|
-
return {
|
|
69442
|
-
ensure: (inheritedExtraCaCerts) => {
|
|
69443
|
-
const up = ensureFrontDoor({
|
|
69444
|
-
paths,
|
|
69445
|
-
launcherPid: process.pid,
|
|
69446
|
-
ports: {
|
|
69447
|
-
fs: realFarmFs,
|
|
69448
|
-
isRunning: realIsProcessRunning,
|
|
69449
|
-
now: () => Date.now(),
|
|
69450
|
-
sleep: realSleepSync,
|
|
69451
|
-
spawnSupervisor: (layout) => spawnDaemon(layout, "__frontdoor-supervisor", layout.frontdoorLogPath),
|
|
69452
|
-
stopSupervisor: (pid) => {
|
|
69453
|
-
process.kill(pid, "SIGTERM");
|
|
69454
|
-
},
|
|
69455
|
-
// Read fresh for each probe: a replacement supervisor may have regenerated an unparseable CA, and the probe must trust exactly what the serving door's leaf chains to.
|
|
69456
|
-
verifyListener: (port) => probeFrontDoorSync(port, import_node_fs16.default.readFileSync(paths.frontdoorCaCertFile, "utf8"))
|
|
69457
|
-
}
|
|
69458
|
-
});
|
|
69459
|
-
const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
|
|
69460
|
-
return {
|
|
69461
|
-
port: up.port,
|
|
69462
|
-
connectPort: up.connectPort,
|
|
69463
|
-
trustBundlePath: trust.path,
|
|
69464
|
-
...trust.warning === void 0 ? {} : { trustWarning: trust.warning },
|
|
69465
|
-
sessionToken: up.token
|
|
69466
|
-
};
|
|
69467
|
-
},
|
|
69468
|
-
release: () => {
|
|
69469
|
-
removeFrontDoorSession(realFarmFs, paths.frontdoorSessionsDir, process.pid);
|
|
69470
|
-
}
|
|
69471
|
-
};
|
|
69472
|
-
}
|
|
69473
|
-
|
|
69474
69525
|
// src/launcher.ts
|
|
69475
69526
|
var import_node_crypto15 = require("node:crypto");
|
|
69476
69527
|
var import_node_path38 = __toESM(require("node:path"), 1);
|
package/dist/index.cjs
CHANGED
|
@@ -2959,14 +2959,11 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
2959
2959
|
};
|
|
2960
2960
|
const sessions = config.interceptHosts.map((host) => {
|
|
2961
2961
|
if (config.tapHosts.includes(host)) {
|
|
2962
|
-
const
|
|
2963
|
-
config.
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
TAP_ALPN_PROTOCOLS
|
|
2968
|
-
);
|
|
2969
|
-
return { host, tlsAcceptor: tlsAcceptor2 };
|
|
2962
|
+
const serveSecure2 = (secure) => {
|
|
2963
|
+
pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
|
|
2964
|
+
};
|
|
2965
|
+
const tlsAcceptor2 = effects.createTlsAcceptor(config.leafFor(host), serveSecure2, TAP_ALPN_PROTOCOLS);
|
|
2966
|
+
return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
|
|
2970
2967
|
}
|
|
2971
2968
|
const handler = (request3, response) => {
|
|
2972
2969
|
if (host === config.routedHost && servedByPipeline(request3.url)) {
|
|
@@ -2980,19 +2977,29 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
2980
2977
|
effects.forwardHttp(config.upstreamFor(host), request3, response, config.capture?.observePassthrough(request3));
|
|
2981
2978
|
};
|
|
2982
2979
|
const httpSession = effects.createHttpSession(handler, onUpgrade(host));
|
|
2983
|
-
const
|
|
2984
|
-
tunnelTokens.set(secure,
|
|
2980
|
+
const serveSecure = (secure, capability) => {
|
|
2981
|
+
tunnelTokens.set(secure, capability);
|
|
2985
2982
|
httpSession.serve(secure);
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2983
|
+
};
|
|
2984
|
+
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), serveSecure);
|
|
2985
|
+
return { host, serveSecure, httpSession, tlsAcceptor };
|
|
2988
2986
|
});
|
|
2989
2987
|
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
2990
2988
|
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
2991
2989
|
handleConnect(socket, { config, effects, sessions, ledger });
|
|
2992
2990
|
});
|
|
2993
2991
|
const apiSession = sessions.find((session) => session.host === config.routedHost);
|
|
2992
|
+
const sessionByHost = new Map(sessions.map((session) => [session.host, session]));
|
|
2993
|
+
const sniAcceptor = effects.createSniTlsAcceptor(config.interceptHosts, config.leafFor, (secure, host) => {
|
|
2994
|
+
const session = sessionByHost.get(host);
|
|
2995
|
+
if (session === void 0) {
|
|
2996
|
+
secure.destroy();
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
session.serveSecure(secure, config.transparentCapability ?? "");
|
|
3000
|
+
});
|
|
2994
3001
|
const transparent = config.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback(config.transparentPort, (socket) => {
|
|
2995
|
-
|
|
3002
|
+
sniAcceptor.accept(socket);
|
|
2996
3003
|
});
|
|
2997
3004
|
return {
|
|
2998
3005
|
port: listener.port,
|
|
@@ -3001,6 +3008,7 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
3001
3008
|
ledger.close();
|
|
3002
3009
|
await listener.close();
|
|
3003
3010
|
await transparent?.close();
|
|
3011
|
+
sniAcceptor.close();
|
|
3004
3012
|
for (const { httpSession, tlsAcceptor } of sessions) {
|
|
3005
3013
|
tlsAcceptor.close();
|
|
3006
3014
|
httpSession?.close();
|
|
@@ -3330,6 +3338,38 @@ function realConnectEffects() {
|
|
|
3330
3338
|
}
|
|
3331
3339
|
};
|
|
3332
3340
|
},
|
|
3341
|
+
createSniTlsAcceptor: (hosts, leafFor, onSecure) => {
|
|
3342
|
+
const contexts = new Map(
|
|
3343
|
+
hosts.map((host) => {
|
|
3344
|
+
const leaf = leafFor(host);
|
|
3345
|
+
return [host, tls.createSecureContext({ key: leaf.keyPem, cert: leaf.certPem })];
|
|
3346
|
+
})
|
|
3347
|
+
);
|
|
3348
|
+
const server = tls.createServer(
|
|
3349
|
+
{
|
|
3350
|
+
// No default key or cert exists, so the SNI selection is the only source of credentials: an unserved name fails its handshake here, and a client that sent no SNI at all never reaches the selection and fails for want of any certificate, which is exactly the fail-closed shape this surface wants.
|
|
3351
|
+
SNICallback: (servername, callback) => {
|
|
3352
|
+
const context = contexts.get(servername);
|
|
3353
|
+
callback(context === void 0 ? new Error(`the transparent surface serves no host named ${servername}`) : null, context);
|
|
3354
|
+
}
|
|
3355
|
+
},
|
|
3356
|
+
(secure) => {
|
|
3357
|
+
onSecure(secure, typeof secure.servername === "string" ? secure.servername : "");
|
|
3358
|
+
}
|
|
3359
|
+
);
|
|
3360
|
+
server.on("clientError", (_error, socket) => {
|
|
3361
|
+
socket.destroy();
|
|
3362
|
+
});
|
|
3363
|
+
return {
|
|
3364
|
+
// The same hand-off the single-leaf acceptor uses: emitting the connection event runs the TLS server's own connection listener on a socket this process already owns (the redirect half of the connection) rather than one the server accepted itself.
|
|
3365
|
+
accept: (socket) => {
|
|
3366
|
+
server.emit("connection", socket);
|
|
3367
|
+
},
|
|
3368
|
+
close: () => {
|
|
3369
|
+
server.close();
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
},
|
|
3333
3373
|
createHttpSession: (handler, onUpgrade) => {
|
|
3334
3374
|
const server = http.createServer(handler);
|
|
3335
3375
|
server.on("clientError", (_error, socket) => {
|
|
@@ -8142,7 +8182,7 @@ var import_node_crypto10 = require("node:crypto");
|
|
|
8142
8182
|
var import_node_path28 = __toESM(require("node:path"), 1);
|
|
8143
8183
|
var BUNDLE_NAME_HEX_DIGITS = 16;
|
|
8144
8184
|
function resolveTrustBundle(params) {
|
|
8145
|
-
const { caCertFile, bundlesDir, inherited, fs: fs12 } = params;
|
|
8185
|
+
const { caCertFile, bundlesDir, variable, inherited, fs: fs12 } = params;
|
|
8146
8186
|
if (inherited === void 0 || inherited === "" || import_node_path28.default.resolve(inherited) === import_node_path28.default.resolve(caCertFile)) {
|
|
8147
8187
|
return { path: caCertFile };
|
|
8148
8188
|
}
|
|
@@ -8153,7 +8193,7 @@ function resolveTrustBundle(params) {
|
|
|
8153
8193
|
} catch (error) {
|
|
8154
8194
|
return {
|
|
8155
8195
|
path: caCertFile,
|
|
8156
|
-
warning: `agent-shim:
|
|
8196
|
+
warning: `agent-shim: ${variable} names ${inherited}, which could not be read (${error instanceof Error ? error.message : String(error)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
|
|
8157
8197
|
};
|
|
8158
8198
|
}
|
|
8159
8199
|
if (inheritedPem.includes(caPem.trim())) {
|
|
@@ -8199,7 +8239,7 @@ function realFrontDoorPort(paths, spawnDaemon) {
|
|
|
8199
8239
|
verifyListener: (port) => probeFrontDoorSync(port, import_node_fs10.default.readFileSync(paths.frontdoorCaCertFile, "utf8"))
|
|
8200
8240
|
}
|
|
8201
8241
|
});
|
|
8202
|
-
const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
|
|
8242
|
+
const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, variable: "NODE_EXTRA_CA_CERTS", inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
|
|
8203
8243
|
return {
|
|
8204
8244
|
port: up.port,
|
|
8205
8245
|
connectPort: up.connectPort,
|
package/dist/index.mjs
CHANGED
|
@@ -2793,14 +2793,11 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
2793
2793
|
};
|
|
2794
2794
|
const sessions = config.interceptHosts.map((host) => {
|
|
2795
2795
|
if (config.tapHosts.includes(host)) {
|
|
2796
|
-
const
|
|
2797
|
-
config.
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
TAP_ALPN_PROTOCOLS
|
|
2802
|
-
);
|
|
2803
|
-
return { host, tlsAcceptor: tlsAcceptor2 };
|
|
2796
|
+
const serveSecure2 = (secure) => {
|
|
2797
|
+
pumpTapSession(secure, async (clientAlpn) => await effects.connectTlsUpstream(host, HTTPS_PORT, clientAlpn), config.capture?.tapStream?.(host));
|
|
2798
|
+
};
|
|
2799
|
+
const tlsAcceptor2 = effects.createTlsAcceptor(config.leafFor(host), serveSecure2, TAP_ALPN_PROTOCOLS);
|
|
2800
|
+
return { host, serveSecure: serveSecure2, tlsAcceptor: tlsAcceptor2 };
|
|
2804
2801
|
}
|
|
2805
2802
|
const handler = (request3, response) => {
|
|
2806
2803
|
if (host === config.routedHost && servedByPipeline(request3.url)) {
|
|
@@ -2814,19 +2811,29 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
2814
2811
|
effects.forwardHttp(config.upstreamFor(host), request3, response, config.capture?.observePassthrough(request3));
|
|
2815
2812
|
};
|
|
2816
2813
|
const httpSession = effects.createHttpSession(handler, onUpgrade(host));
|
|
2817
|
-
const
|
|
2818
|
-
tunnelTokens.set(secure,
|
|
2814
|
+
const serveSecure = (secure, capability) => {
|
|
2815
|
+
tunnelTokens.set(secure, capability);
|
|
2819
2816
|
httpSession.serve(secure);
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2817
|
+
};
|
|
2818
|
+
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(host), serveSecure);
|
|
2819
|
+
return { host, serveSecure, httpSession, tlsAcceptor };
|
|
2822
2820
|
});
|
|
2823
2821
|
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
2824
2822
|
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
2825
2823
|
handleConnect(socket, { config, effects, sessions, ledger });
|
|
2826
2824
|
});
|
|
2827
2825
|
const apiSession = sessions.find((session) => session.host === config.routedHost);
|
|
2826
|
+
const sessionByHost = new Map(sessions.map((session) => [session.host, session]));
|
|
2827
|
+
const sniAcceptor = effects.createSniTlsAcceptor(config.interceptHosts, config.leafFor, (secure, host) => {
|
|
2828
|
+
const session = sessionByHost.get(host);
|
|
2829
|
+
if (session === void 0) {
|
|
2830
|
+
secure.destroy();
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
session.serveSecure(secure, config.transparentCapability ?? "");
|
|
2834
|
+
});
|
|
2828
2835
|
const transparent = config.transparentPort === void 0 || apiSession === void 0 ? void 0 : await effects.listenLoopback(config.transparentPort, (socket) => {
|
|
2829
|
-
|
|
2836
|
+
sniAcceptor.accept(socket);
|
|
2830
2837
|
});
|
|
2831
2838
|
return {
|
|
2832
2839
|
port: listener.port,
|
|
@@ -2835,6 +2842,7 @@ ${request3.rawHeaders.reduce((accumulated, value, index) => index % 2 === 0 ? `$
|
|
|
2835
2842
|
ledger.close();
|
|
2836
2843
|
await listener.close();
|
|
2837
2844
|
await transparent?.close();
|
|
2845
|
+
sniAcceptor.close();
|
|
2838
2846
|
for (const { httpSession, tlsAcceptor } of sessions) {
|
|
2839
2847
|
tlsAcceptor.close();
|
|
2840
2848
|
httpSession?.close();
|
|
@@ -3164,6 +3172,38 @@ function realConnectEffects() {
|
|
|
3164
3172
|
}
|
|
3165
3173
|
};
|
|
3166
3174
|
},
|
|
3175
|
+
createSniTlsAcceptor: (hosts, leafFor, onSecure) => {
|
|
3176
|
+
const contexts = new Map(
|
|
3177
|
+
hosts.map((host) => {
|
|
3178
|
+
const leaf = leafFor(host);
|
|
3179
|
+
return [host, tls.createSecureContext({ key: leaf.keyPem, cert: leaf.certPem })];
|
|
3180
|
+
})
|
|
3181
|
+
);
|
|
3182
|
+
const server = tls.createServer(
|
|
3183
|
+
{
|
|
3184
|
+
// No default key or cert exists, so the SNI selection is the only source of credentials: an unserved name fails its handshake here, and a client that sent no SNI at all never reaches the selection and fails for want of any certificate, which is exactly the fail-closed shape this surface wants.
|
|
3185
|
+
SNICallback: (servername, callback) => {
|
|
3186
|
+
const context = contexts.get(servername);
|
|
3187
|
+
callback(context === void 0 ? new Error(`the transparent surface serves no host named ${servername}`) : null, context);
|
|
3188
|
+
}
|
|
3189
|
+
},
|
|
3190
|
+
(secure) => {
|
|
3191
|
+
onSecure(secure, typeof secure.servername === "string" ? secure.servername : "");
|
|
3192
|
+
}
|
|
3193
|
+
);
|
|
3194
|
+
server.on("clientError", (_error, socket) => {
|
|
3195
|
+
socket.destroy();
|
|
3196
|
+
});
|
|
3197
|
+
return {
|
|
3198
|
+
// The same hand-off the single-leaf acceptor uses: emitting the connection event runs the TLS server's own connection listener on a socket this process already owns (the redirect half of the connection) rather than one the server accepted itself.
|
|
3199
|
+
accept: (socket) => {
|
|
3200
|
+
server.emit("connection", socket);
|
|
3201
|
+
},
|
|
3202
|
+
close: () => {
|
|
3203
|
+
server.close();
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
},
|
|
3167
3207
|
createHttpSession: (handler, onUpgrade) => {
|
|
3168
3208
|
const server = http.createServer(handler);
|
|
3169
3209
|
server.on("clientError", (_error, socket) => {
|
|
@@ -7976,7 +8016,7 @@ import { createHash as createHash5 } from "node:crypto";
|
|
|
7976
8016
|
import path28 from "node:path";
|
|
7977
8017
|
var BUNDLE_NAME_HEX_DIGITS = 16;
|
|
7978
8018
|
function resolveTrustBundle(params) {
|
|
7979
|
-
const { caCertFile, bundlesDir, inherited, fs: fs12 } = params;
|
|
8019
|
+
const { caCertFile, bundlesDir, variable, inherited, fs: fs12 } = params;
|
|
7980
8020
|
if (inherited === void 0 || inherited === "" || path28.resolve(inherited) === path28.resolve(caCertFile)) {
|
|
7981
8021
|
return { path: caCertFile };
|
|
7982
8022
|
}
|
|
@@ -7987,7 +8027,7 @@ function resolveTrustBundle(params) {
|
|
|
7987
8027
|
} catch (error) {
|
|
7988
8028
|
return {
|
|
7989
8029
|
path: caCertFile,
|
|
7990
|
-
warning: `agent-shim:
|
|
8030
|
+
warning: `agent-shim: ${variable} names ${inherited}, which could not be read (${error instanceof Error ? error.message : String(error)}); the child trusts agent-shim's front-door CA on top of the system store, and nothing from that file`
|
|
7991
8031
|
};
|
|
7992
8032
|
}
|
|
7993
8033
|
if (inheritedPem.includes(caPem.trim())) {
|
|
@@ -8033,7 +8073,7 @@ function realFrontDoorPort(paths, spawnDaemon) {
|
|
|
8033
8073
|
verifyListener: (port) => probeFrontDoorSync(port, fs11.readFileSync(paths.frontdoorCaCertFile, "utf8"))
|
|
8034
8074
|
}
|
|
8035
8075
|
});
|
|
8036
|
-
const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
|
|
8076
|
+
const trust = resolveTrustBundle({ caCertFile: paths.frontdoorCaCertFile, bundlesDir: paths.frontdoorCaBundlesDir, variable: "NODE_EXTRA_CA_CERTS", inherited: inheritedExtraCaCerts, fs: realTrustBundleFs });
|
|
8037
8077
|
return {
|
|
8038
8078
|
port: up.port,
|
|
8039
8079
|
connectPort: up.connectPort,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -686,7 +686,7 @@ interface LayoutPaths {
|
|
|
686
686
|
readonly frontdoorCaCertFile: string;
|
|
687
687
|
/** Path to the front door's CA private key, mode 0600: whoever can read it can impersonate the door to every routed child. */
|
|
688
688
|
readonly frontdoorCaKeyFile: string;
|
|
689
|
-
/** Directory of combined CA bundles: a parent environment's own NODE_EXTRA_CA_CERTS file plus the front door's CA, one file per distinct combination, for
|
|
689
|
+
/** Directory of combined CA bundles: a parent environment's own NODE_EXTRA_CA_CERTS (for a routed child) or HEADROOM_CA_BUNDLE (for the headroom daemon) file plus the front door's CA, one file per distinct combination, for processes that must keep trusting both. */
|
|
690
690
|
readonly frontdoorCaBundlesDir: string;
|
|
691
691
|
/** Path to the front door's state.json: its supervisor pid, its port, and the sticky port it last served on. */
|
|
692
692
|
readonly frontdoorStateFile: string;
|
|
@@ -1470,7 +1470,7 @@ declare function mintLeaf(ca: CaMaterial, names: readonly string[], now: Readonl
|
|
|
1470
1470
|
/** Mints each host's leaf once from the CA and reuses it, because generating a 2048-bit RSA keypair costs noticeable time and nothing about the certificate changes between sessions. */
|
|
1471
1471
|
declare function createLeafCache(ca: CaMaterial, now: () => Date): (host: string) => LeafCert;
|
|
1472
1472
|
/**
|
|
1473
|
-
* Persistence for the CA: generated once on the machine's first
|
|
1473
|
+
* Persistence for the CA: generated once on the machine's first front-door start and reused forever after, because regenerating it would invalidate every child's `NODE_EXTRA_CA_CERTS` pointing at the old file. The real implementation writes the key with mode 0600; injected fakes keep it in memory.
|
|
1474
1474
|
*/
|
|
1475
1475
|
interface ConnectCertStore {
|
|
1476
1476
|
/** The persisted CA, or undefined when none exists yet (or only half of it does: a cert without its key can sign nothing, so it counts as absent). */
|
|
@@ -1509,6 +1509,13 @@ interface TlsAcceptor {
|
|
|
1509
1509
|
/** Drops every session this acceptor is terminating. */
|
|
1510
1510
|
readonly close: () => void;
|
|
1511
1511
|
}
|
|
1512
|
+
/** Presents every served host's leaf on one handshake, selected by the SNI name the client sent: the transparent surface's terminator, answering each redirected connection as the host it named. */
|
|
1513
|
+
interface SniTlsAcceptor {
|
|
1514
|
+
/** Starts the TLS handshake on a redirected socket; a handshake that completes for a served host hands its secure session to the acceptor's `onSecure` with that host's name. */
|
|
1515
|
+
readonly accept: (socket: net.Socket) => void;
|
|
1516
|
+
/** Drops every session this acceptor is terminating. */
|
|
1517
|
+
readonly close: () => void;
|
|
1518
|
+
}
|
|
1512
1519
|
/** Parses HTTP over terminated sockets and serves each request through the handler, keep-alive included. */
|
|
1513
1520
|
interface HttpParserSession {
|
|
1514
1521
|
readonly serve: (socket: net.Socket) => void;
|
|
@@ -1522,6 +1529,8 @@ interface ConnectEffects {
|
|
|
1522
1529
|
readonly listenLoopback: (preferredPort: number | undefined, onSocket: (socket: net.Socket) => void) => Promise<ConnectListenerHandle>;
|
|
1523
1530
|
/** Builds the TLS terminator for one leaf, handing each successfully handshaked session to `onSecure` together with the context its `accept` call carried. `alpnProtocols`, when given, is advertised during the handshake, which is how a tap host's session can offer HTTP/2 the way its real server does. */
|
|
1524
1531
|
readonly createTlsAcceptor: (leaf: LeafCert, onSecure: (secure: net.Socket, context: string) => void, alpnProtocols?: readonly string[]) => TlsAcceptor;
|
|
1532
|
+
/** Builds the transparent surface's TLS terminator: one handshake whose SNI name selects which served host's leaf answers it, so a connection redirected from any of the hosts sharing the redirected address is terminated as the host it named. A name the surface does not serve is answered with no certificate at all (a client that sends no SNI never reaches the selection and finds no default certificate either), because the only leaf that could answer such a name is another host's, which the client's own hostname check would reject anyway. No ALPN is offered: node's TLS server negotiates ALPN from the listener's own options and ignores protocols named on an SNI-selected secure context, and the API host's terminated session is parsed as HTTP/1.1, which a negotiated `h2` would strand. */
|
|
1533
|
+
readonly createSniTlsAcceptor: (hosts: readonly string[], leafFor: (host: string) => LeafCert, onSecure: (secure: net.Socket, host: string) => void) => SniTlsAcceptor;
|
|
1525
1534
|
/** Opens a TLS connection to a real host, presenting its name as SNI, for a tap session's upstream half. `clientAlpn`, when given, is the protocol the client negotiated with the surface and the only one offered upstream, so the tap never changes the channel's protocol. */
|
|
1526
1535
|
readonly connectTlsUpstream: (host: string, port: number, clientAlpn: string | undefined) => Promise<net.Socket>;
|
|
1527
1536
|
/** Builds the HTTP parser bound to one request handler. `onUpgrade`, when given, owns every HTTP upgrade request arriving on a terminated session, socket and already-read bytes included; a session without one lets Node destroy the upgrade, its own no-listener behaviour. */
|
|
@@ -1550,7 +1559,7 @@ interface ConnectServerConfig {
|
|
|
1550
1559
|
/** Whether a capability presented as a CONNECT request's proxy credential belongs to a live launch: the same constant-time check against the same registry the routed pipeline's admission makes, read fresh on every call because launches come and go. */
|
|
1551
1560
|
readonly isLiveCapability: (token: string) => boolean;
|
|
1552
1561
|
/**
|
|
1553
|
-
* Binds a second listener that
|
|
1562
|
+
* Binds a second listener that terminates redirected connections per host, with no CONNECT handshake to authenticate: what an operating-system redirect (a pf rule sending the real hosts' address to this port) delivers, pre-established. A redirect names addresses, and several real hosts share the API host's, so the client's TLS SNI name is what says which host arrived: each intercept host is served with its own leaf and its own session exactly as a CONNECT-tunnelled connection to that host is (the API host's `/v1/` rides the routed pipeline, a tap host's stream is pumped to its real host), and a name outside the intercept set, or a connection presenting none, is refused with no certificate at all, because the only leaf that could answer it is another host's. There is no capability to check on this surface: the connection carried no proxy credential, so every request rides the pipeline anonymously, admitted the way a tunnel's own header-less requests are (by nothing, which is exactly what they present), and the surface exists solely for clients that ignore proxies and would otherwise be invisible to the door entirely.
|
|
1554
1563
|
*/
|
|
1555
1564
|
readonly transparentPort?: number;
|
|
1556
1565
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-shim",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.2",
|
|
4
4
|
"description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joseph Mearman <joseph@mearman.co.uk>",
|