claude-use 4.5.0 → 5.0.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/README.md +1 -1
- package/dist/cli.cjs +3365 -2594
- package/dist/index.cjs +472 -159
- package/dist/index.mjs +462 -153
- package/dist/types/frontdoor/capability.d.ts +19 -0
- package/dist/types/frontdoor/connect.d.ts +179 -0
- package/dist/types/frontdoor/ensure.d.ts +34 -0
- package/dist/types/frontdoor/probe.d.ts +13 -0
- package/dist/types/frontdoor/state.d.ts +32 -1
- package/dist/types/frontdoor/supervisor.d.ts +48 -0
- package/dist/types/headroom/ensure.d.ts +1 -2
- package/dist/types/headroom/state.d.ts +2 -4
- package/dist/types/headroom/supervisor.d.ts +1 -6
- package/dist/types/index.d.ts +4 -2
- package/dist/types/launcher/ports.d.ts +22 -10
- package/dist/types/paths.d.ts +8 -6
- package/package.json +1 -1
- package/dist/types/headroom/mitm.d.ts +0 -138
package/dist/index.cjs
CHANGED
|
@@ -31,15 +31,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AMBIENT_CREDENTIAL_VARS: () => AMBIENT_CREDENTIAL_VARS,
|
|
34
|
+
CONNECT_INTERCEPT_HOST: () => CONNECT_INTERCEPT_HOST,
|
|
35
|
+
CONNECT_LIMITS: () => CONNECT_LIMITS,
|
|
34
36
|
FARM_MANIFEST_FILENAME: () => FARM_MANIFEST_FILENAME,
|
|
35
|
-
|
|
37
|
+
FrontDoorStartError: () => FrontDoorStartError,
|
|
36
38
|
HeadroomStartError: () => HeadroomStartError,
|
|
37
|
-
|
|
39
|
+
ROUTED_PATH_PREFIX: () => ROUTED_PATH_PREFIX,
|
|
38
40
|
buildEntryFacts: () => buildEntryFacts,
|
|
39
41
|
carryOver: () => carryOver,
|
|
40
42
|
createLeafCache: () => createLeafCache,
|
|
41
43
|
detectAmbientCredential: () => detectAmbientCredential,
|
|
42
44
|
ensureCa: () => ensureCa,
|
|
45
|
+
ensureFrontDoor: () => ensureFrontDoor,
|
|
43
46
|
ensureHeadroom: () => ensureHeadroom,
|
|
44
47
|
evaluateAmbientCredentialGuard: () => evaluateAmbientCredentialGuard,
|
|
45
48
|
formatAmbientCredentialGuardMessage: () => formatAmbientCredentialGuardMessage,
|
|
@@ -49,16 +52,17 @@ __export(index_exports, {
|
|
|
49
52
|
mintLeaf: () => mintLeaf,
|
|
50
53
|
parseConnectTarget: () => parseConnectTarget,
|
|
51
54
|
readFarmManifest: () => readFarmManifest,
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
realConnectCertStore: () => realConnectCertStore,
|
|
56
|
+
realConnectEffects: () => realConnectEffects,
|
|
54
57
|
recoverFarm: () => recoverFarm,
|
|
55
58
|
recoveryDiagnostics: () => recoveryDiagnostics,
|
|
56
59
|
resolveDecisions: () => resolveDecisions,
|
|
57
60
|
resolveSupervisorConfig: () => resolveSupervisorConfig,
|
|
58
61
|
resyncFarm: () => resyncFarm,
|
|
62
|
+
runFrontDoorSupervisor: () => runFrontDoorSupervisor,
|
|
59
63
|
runSupervisor: () => runSupervisor,
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
servedByPipeline: () => servedByPipeline,
|
|
65
|
+
startConnectServer: () => startConnectServer,
|
|
62
66
|
topLevelNames: () => topLevelNames
|
|
63
67
|
});
|
|
64
68
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -1798,15 +1802,213 @@ function recoveryDiagnostics(recovery, identity) {
|
|
|
1798
1802
|
];
|
|
1799
1803
|
}
|
|
1800
1804
|
|
|
1801
|
-
// src/
|
|
1805
|
+
// src/frontdoor/connect.ts
|
|
1802
1806
|
var fs = __toESM(require("node:fs"), 1);
|
|
1803
1807
|
var http = __toESM(require("node:http"), 1);
|
|
1804
1808
|
var https = __toESM(require("node:https"), 1);
|
|
1805
1809
|
var net = __toESM(require("node:net"), 1);
|
|
1806
1810
|
var tls = __toESM(require("node:tls"), 1);
|
|
1807
1811
|
var import_node_forge = __toESM(require("node-forge"), 1);
|
|
1808
|
-
|
|
1809
|
-
|
|
1812
|
+
|
|
1813
|
+
// src/frontdoor/capability.ts
|
|
1814
|
+
var CONNECT_PROXY_REALM = "claude-use front door";
|
|
1815
|
+
var BASIC_SCHEME = "basic";
|
|
1816
|
+
function capabilityFromProxyAuthorization(value) {
|
|
1817
|
+
const trimmed = value.trim();
|
|
1818
|
+
const space = trimmed.indexOf(" ");
|
|
1819
|
+
if (space === -1 || trimmed.slice(0, space).toLowerCase() !== BASIC_SCHEME) {
|
|
1820
|
+
return void 0;
|
|
1821
|
+
}
|
|
1822
|
+
const userPass = Buffer.from(trimmed.slice(space + 1).trim(), "base64").toString("utf8");
|
|
1823
|
+
const colon = userPass.indexOf(":");
|
|
1824
|
+
if (colon === -1) {
|
|
1825
|
+
return void 0;
|
|
1826
|
+
}
|
|
1827
|
+
const password = userPass.slice(colon + 1);
|
|
1828
|
+
return password === "" ? void 0 : password;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// src/frontdoor/state.ts
|
|
1832
|
+
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
1833
|
+
var import_zod3 = require("zod");
|
|
1834
|
+
var FrontDoorSessionSchema = import_zod3.z.strictObject({ pid: import_zod3.z.number().int().positive(), startedAt: import_zod3.z.number(), token: import_zod3.z.string().min(1) });
|
|
1835
|
+
var FrontDoorStateSchema = import_zod3.z.strictObject({
|
|
1836
|
+
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
1837
|
+
supervisorPid: import_zod3.z.number().int().positive().optional(),
|
|
1838
|
+
/** The loopback port the HTTPS provider listener serves on. Absent until the listener has bound and answered its health probe, so "port is set" is itself the ready signal a launcher polls for. */
|
|
1839
|
+
port: import_zod3.z.number().int().positive().optional(),
|
|
1840
|
+
/**
|
|
1841
|
+
* The sticky port preference for the provider listener: kept across crashes, restarts and idle shutdowns so every provider session, whose base URL was frozen at launch, keeps finding the front door at the same address.
|
|
1842
|
+
*/
|
|
1843
|
+
lastPort: import_zod3.z.number().int().positive().optional(),
|
|
1844
|
+
/**
|
|
1845
|
+
* The loopback port the CONNECT surface listens on. Absent whenever no supervisor is serving it, so "connectPort is set" is the ready signal an OAuth launch polls for alongside `port`.
|
|
1846
|
+
*/
|
|
1847
|
+
connectPort: import_zod3.z.number().int().positive().optional(),
|
|
1848
|
+
/**
|
|
1849
|
+
* The sticky port preference for the CONNECT surface, the exact analogue of `lastPort` for the second listener: it survives every shutdown, because every OAuth session's environment was frozen at launch with HTTPS_PROXY pointing at this address and a restart that moves strands them.
|
|
1850
|
+
*/
|
|
1851
|
+
lastConnectPort: import_zod3.z.number().int().positive().optional(),
|
|
1852
|
+
/**
|
|
1853
|
+
* The loopback port the direct listener serves on: the listener a headroom hop tells the daemon to forward routed traffic back to. Sticky like the others, because headroom's allowlist admits its exact origin and an ephemeral port would invalidate the allowlist on every restart.
|
|
1854
|
+
*/
|
|
1855
|
+
directPort: import_zod3.z.number().int().positive().optional(),
|
|
1856
|
+
/** The sticky port preference for the direct listener, for the reason `directPort` gives. */
|
|
1857
|
+
lastDirectPort: import_zod3.z.number().int().positive().optional(),
|
|
1858
|
+
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
1859
|
+
lastError: import_zod3.z.string().optional()
|
|
1860
|
+
});
|
|
1861
|
+
function readFrontDoorState(fs2, stateFile) {
|
|
1862
|
+
let raw;
|
|
1863
|
+
try {
|
|
1864
|
+
raw = fs2.readFileUtf8(stateFile);
|
|
1865
|
+
} catch {
|
|
1866
|
+
return void 0;
|
|
1867
|
+
}
|
|
1868
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1869
|
+
return void 0;
|
|
1870
|
+
}
|
|
1871
|
+
try {
|
|
1872
|
+
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
1873
|
+
return parsed.success ? parsed.data : void 0;
|
|
1874
|
+
} catch {
|
|
1875
|
+
return void 0;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function writeFrontDoorState(fs2, stateFile, state) {
|
|
1879
|
+
fs2.mkdirp(import_node_path5.default.dirname(stateFile));
|
|
1880
|
+
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
1881
|
+
`);
|
|
1882
|
+
}
|
|
1883
|
+
function frontDoorOrigin(state) {
|
|
1884
|
+
const port = state?.directPort ?? state?.lastDirectPort;
|
|
1885
|
+
return port === void 0 ? void 0 : `http://127.0.0.1:${String(port)}`;
|
|
1886
|
+
}
|
|
1887
|
+
function writeFrontDoorSession(fs2, sessionsDir, session) {
|
|
1888
|
+
fs2.mkdirPrivate(sessionsDir);
|
|
1889
|
+
fs2.writeFilePrivate(import_node_path5.default.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
1890
|
+
`);
|
|
1891
|
+
}
|
|
1892
|
+
function listFrontDoorSessions(fs2, sessionsDir) {
|
|
1893
|
+
const sessions = [];
|
|
1894
|
+
for (const name of fs2.readdir(sessionsDir)) {
|
|
1895
|
+
if (!name.endsWith(".json")) {
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
try {
|
|
1899
|
+
const parsed = FrontDoorSessionSchema.safeParse(JSON.parse(fs2.readFileUtf8(import_node_path5.default.join(sessionsDir, name)) ?? ""));
|
|
1900
|
+
if (parsed.success) {
|
|
1901
|
+
sessions.push({ pid: parsed.data.pid, startedAt: parsed.data.startedAt });
|
|
1902
|
+
}
|
|
1903
|
+
} catch {
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return sessions.sort((a, b) => a.pid - b.pid);
|
|
1908
|
+
}
|
|
1909
|
+
function removeFrontDoorSession(fs2, sessionsDir, pid) {
|
|
1910
|
+
fs2.removeRecursive(import_node_path5.default.join(sessionsDir, `${String(pid)}.json`));
|
|
1911
|
+
}
|
|
1912
|
+
function pruneDeadFrontDoorSessions(fs2, sessionsDir, isRunning) {
|
|
1913
|
+
const removed = [];
|
|
1914
|
+
for (const session of listFrontDoorSessions(fs2, sessionsDir)) {
|
|
1915
|
+
if (!isRunning(session.pid)) {
|
|
1916
|
+
removeFrontDoorSession(fs2, sessionsDir, session.pid);
|
|
1917
|
+
removed.push(session.pid);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return removed;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// src/frontdoor/supervisor.ts
|
|
1924
|
+
var FRONTDOOR_SUPERVISOR_STILL_RUNNING = -1;
|
|
1925
|
+
var FRONTDOOR_POLL_MS = 1e3;
|
|
1926
|
+
var MS_PER_MINUTE = 6e4;
|
|
1927
|
+
async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {}) {
|
|
1928
|
+
const { fs: fs2, paths } = ports;
|
|
1929
|
+
const previous = readFrontDoorState(fs2, paths.frontdoorStateFile);
|
|
1930
|
+
let lastPort = previous?.lastPort;
|
|
1931
|
+
let lastConnectPort = previous?.lastConnectPort;
|
|
1932
|
+
let lastDirectPort = previous?.lastDirectPort;
|
|
1933
|
+
const sticky = () => ({
|
|
1934
|
+
...lastPort === void 0 ? {} : { lastPort },
|
|
1935
|
+
...lastConnectPort === void 0 ? {} : { lastConnectPort },
|
|
1936
|
+
...lastDirectPort === void 0 ? {} : { lastDirectPort }
|
|
1937
|
+
});
|
|
1938
|
+
const fail = (message) => {
|
|
1939
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { lastError: message, ...sticky() });
|
|
1940
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1941
|
+
ports.log(`claude-use frontdoor supervisor: ${message}`);
|
|
1942
|
+
return 1;
|
|
1943
|
+
};
|
|
1944
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
1945
|
+
const previousSticky = lastPort;
|
|
1946
|
+
let http2;
|
|
1947
|
+
try {
|
|
1948
|
+
http2 = await ports.startProviderListener(lastPort);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1951
|
+
}
|
|
1952
|
+
if (previousSticky !== void 0 && http2.port !== previousSticky) {
|
|
1953
|
+
ports.log(`claude-use frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http2.port)} (sessions launched against the old port are stale until they relaunch)`);
|
|
1954
|
+
}
|
|
1955
|
+
lastPort = http2.port;
|
|
1956
|
+
const previousStickyConnect = lastConnectPort;
|
|
1957
|
+
let connect2;
|
|
1958
|
+
try {
|
|
1959
|
+
connect2 = await ports.startConnectListener(lastConnectPort);
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
await http2.close();
|
|
1962
|
+
return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
1963
|
+
}
|
|
1964
|
+
if (previousStickyConnect !== void 0 && connect2.port !== previousStickyConnect) {
|
|
1965
|
+
ports.log(`claude-use frontdoor supervisor: sticky CONNECT port ${String(previousStickyConnect)} was occupied; moving to ${String(connect2.port)} (OAuth sessions launched against the old port are stale until they relaunch)`);
|
|
1966
|
+
}
|
|
1967
|
+
lastConnectPort = connect2.port;
|
|
1968
|
+
const previousStickyDirect = lastDirectPort;
|
|
1969
|
+
let direct;
|
|
1970
|
+
try {
|
|
1971
|
+
direct = await ports.startDirectListener(lastDirectPort);
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
await connect2.close();
|
|
1974
|
+
await http2.close();
|
|
1975
|
+
return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1976
|
+
}
|
|
1977
|
+
if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
|
|
1978
|
+
ports.log(`claude-use frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
1979
|
+
}
|
|
1980
|
+
lastDirectPort = direct.port;
|
|
1981
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect2.port, directPort: direct.port, ...sticky() });
|
|
1982
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1983
|
+
ports.log(`claude-use frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http2.port)}, CONNECT surface on 127.0.0.1:${String(connect2.port)}`);
|
|
1984
|
+
let idleSince;
|
|
1985
|
+
let ticks = 0;
|
|
1986
|
+
for (; ; ) {
|
|
1987
|
+
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
1988
|
+
return FRONTDOOR_SUPERVISOR_STILL_RUNNING;
|
|
1989
|
+
}
|
|
1990
|
+
ticks += 1;
|
|
1991
|
+
pruneDeadFrontDoorSessions(fs2, paths.frontdoorSessionsDir, ports.isRunning);
|
|
1992
|
+
if (listFrontDoorSessions(fs2, paths.frontdoorSessionsDir).length === 0) {
|
|
1993
|
+
idleSince ??= ports.now();
|
|
1994
|
+
if (ports.now() - idleSince >= idleShutdownMinutes * MS_PER_MINUTE) {
|
|
1995
|
+
ports.log(`claude-use frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
|
|
1996
|
+
await direct.close();
|
|
1997
|
+
await connect2.close();
|
|
1998
|
+
await http2.close();
|
|
1999
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, sticky());
|
|
2000
|
+
return 0;
|
|
2001
|
+
}
|
|
2002
|
+
} else {
|
|
2003
|
+
idleSince = void 0;
|
|
2004
|
+
}
|
|
2005
|
+
await ports.sleep(FRONTDOOR_POLL_MS);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/frontdoor/connect.ts
|
|
2010
|
+
var CONNECT_INTERCEPT_HOST = "api.anthropic.com";
|
|
2011
|
+
var ROUTED_PATH_PREFIX = "/v1/";
|
|
1810
2012
|
var HTTPS_PORT = 443;
|
|
1811
2013
|
var MAX_PORT = 65535;
|
|
1812
2014
|
var SERIAL_NUMBER_BYTES = 16;
|
|
@@ -1816,9 +2018,22 @@ var CONNECT_LINE_TOKENS = 3;
|
|
|
1816
2018
|
var HEAD_TERMINATOR = "\r\n\r\n";
|
|
1817
2019
|
var MAX_CONNECT_HEAD_BYTES = 8192;
|
|
1818
2020
|
var CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
2021
|
+
var REFUSAL = {
|
|
2022
|
+
badRequest: "400 Bad Request",
|
|
2023
|
+
proxyAuthenticationRequired: "407 Proxy Authentication Required",
|
|
2024
|
+
requestTimeout: "408 Request Timeout",
|
|
2025
|
+
headTooLarge: "431 Request Header Fields Too Large",
|
|
2026
|
+
serviceUnavailable: "503 Service Unavailable"
|
|
2027
|
+
};
|
|
2028
|
+
var CONNECT_LIMITS = {
|
|
2029
|
+
headDeadlineMs: 1e4,
|
|
2030
|
+
maxPendingHeads: 64,
|
|
2031
|
+
maxTunnels: 1024,
|
|
2032
|
+
revalidateMs: FRONTDOOR_POLL_MS
|
|
2033
|
+
};
|
|
1819
2034
|
var CA_VALIDITY_DAYS = 3650;
|
|
1820
2035
|
var LEAF_VALIDITY_DAYS = 397;
|
|
1821
|
-
var CA_SUBJECT_COMMON_NAME = "claude-use
|
|
2036
|
+
var CA_SUBJECT_COMMON_NAME = "claude-use front door CA";
|
|
1822
2037
|
var MS_PER_DAY = 864e5;
|
|
1823
2038
|
function parseConnectTarget(authority) {
|
|
1824
2039
|
const colon = authority.lastIndexOf(":");
|
|
@@ -1836,8 +2051,8 @@ function parseConnectTarget(authority) {
|
|
|
1836
2051
|
function isInterceptedHost(host, interceptHost) {
|
|
1837
2052
|
return host === interceptHost;
|
|
1838
2053
|
}
|
|
1839
|
-
function
|
|
1840
|
-
return
|
|
2054
|
+
function servedByPipeline(path7) {
|
|
2055
|
+
return path7?.startsWith(ROUTED_PATH_PREFIX) === true;
|
|
1841
2056
|
}
|
|
1842
2057
|
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1843
2058
|
"connection",
|
|
@@ -1885,7 +2100,13 @@ function generateCa(now) {
|
|
|
1885
2100
|
cert.sign(keys.privateKey, import_node_forge.default.md.sha256.create());
|
|
1886
2101
|
return { certPem: import_node_forge.default.pki.certificateToPem(cert), keyPem: import_node_forge.default.pki.privateKeyToPem(keys.privateKey) };
|
|
1887
2102
|
}
|
|
1888
|
-
|
|
2103
|
+
var SAN_TYPE_DNS = 2;
|
|
2104
|
+
var SAN_TYPE_IP = 7;
|
|
2105
|
+
function mintLeaf(ca, names, now) {
|
|
2106
|
+
const [commonName] = names;
|
|
2107
|
+
if (commonName === void 0) {
|
|
2108
|
+
throw new Error("a leaf certificate needs at least one name");
|
|
2109
|
+
}
|
|
1889
2110
|
const caCert = import_node_forge.default.pki.certificateFromPem(ca.certPem);
|
|
1890
2111
|
const caKey = import_node_forge.default.pki.privateKeyFromPem(ca.keyPem);
|
|
1891
2112
|
const keys = import_node_forge.default.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
@@ -1894,13 +2115,13 @@ function mintLeaf(ca, host, now) {
|
|
|
1894
2115
|
cert.serialNumber = import_node_forge.default.util.bytesToHex(import_node_forge.default.random.getBytesSync(SERIAL_NUMBER_BYTES));
|
|
1895
2116
|
cert.validity.notBefore = new Date(now.getTime() - MS_PER_DAY);
|
|
1896
2117
|
cert.validity.notAfter = new Date(now.getTime() + LEAF_VALIDITY_DAYS * MS_PER_DAY);
|
|
1897
|
-
cert.setSubject([{ name: "commonName", value:
|
|
2118
|
+
cert.setSubject([{ name: "commonName", value: commonName }]);
|
|
1898
2119
|
cert.setIssuer(caCert.subject.attributes);
|
|
1899
2120
|
cert.setExtensions([
|
|
1900
2121
|
{ name: "basicConstraints", cA: false, critical: true },
|
|
1901
2122
|
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
|
|
1902
2123
|
{ name: "extKeyUsage", serverAuth: true },
|
|
1903
|
-
{ name: "subjectAltName", altNames:
|
|
2124
|
+
{ name: "subjectAltName", altNames: names.map((name) => net.isIP(name) === 0 ? { type: SAN_TYPE_DNS, value: name } : { type: SAN_TYPE_IP, ip: name }) },
|
|
1904
2125
|
{ name: "subjectKeyIdentifier" },
|
|
1905
2126
|
{ name: "authorityKeyIdentifier", keyLocatorFields: [] }
|
|
1906
2127
|
]);
|
|
@@ -1914,7 +2135,7 @@ function createLeafCache(ca, now) {
|
|
|
1914
2135
|
if (existing !== void 0) {
|
|
1915
2136
|
return existing;
|
|
1916
2137
|
}
|
|
1917
|
-
const minted = mintLeaf(ca, host, now());
|
|
2138
|
+
const minted = mintLeaf(ca, [host], now());
|
|
1918
2139
|
cache.set(host, minted);
|
|
1919
2140
|
return minted;
|
|
1920
2141
|
};
|
|
@@ -1937,17 +2158,10 @@ function caParses(ca) {
|
|
|
1937
2158
|
return false;
|
|
1938
2159
|
}
|
|
1939
2160
|
}
|
|
1940
|
-
async function
|
|
2161
|
+
async function startConnectServer(config, effects, preferredPort) {
|
|
1941
2162
|
const handler = (request3, response) => {
|
|
1942
|
-
if (
|
|
1943
|
-
|
|
1944
|
-
if (daemonPort === void 0) {
|
|
1945
|
-
const body = "claude-use headroom: the daemon is restarting";
|
|
1946
|
-
response.writeHead(HTTP_BAD_GATEWAY, { "content-type": "text/plain", "content-length": String(body.length) });
|
|
1947
|
-
response.end(body);
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
effects.forwardHttp({ host: "127.0.0.1", port: daemonPort, tls: false }, request3, response);
|
|
2163
|
+
if (servedByPipeline(request3.url)) {
|
|
2164
|
+
config.serveRouted(request3, response);
|
|
1951
2165
|
return;
|
|
1952
2166
|
}
|
|
1953
2167
|
effects.forwardHttp(config.upstream, request3, response);
|
|
@@ -1956,50 +2170,153 @@ async function startMitmServer(config, effects, preferredPort) {
|
|
|
1956
2170
|
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(config.interceptHost), (secure) => {
|
|
1957
2171
|
httpSession.serve(secure);
|
|
1958
2172
|
});
|
|
2173
|
+
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
1959
2174
|
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
1960
|
-
handleConnect(socket, config, effects, tlsAcceptor);
|
|
2175
|
+
handleConnect(socket, { config, effects, tlsAcceptor, ledger });
|
|
1961
2176
|
});
|
|
1962
2177
|
return {
|
|
1963
2178
|
port: listener.port,
|
|
1964
2179
|
close: async () => {
|
|
2180
|
+
ledger.close();
|
|
1965
2181
|
await listener.close();
|
|
1966
2182
|
tlsAcceptor.close();
|
|
1967
2183
|
httpSession.close();
|
|
1968
2184
|
}
|
|
1969
2185
|
};
|
|
1970
2186
|
}
|
|
1971
|
-
function
|
|
2187
|
+
function createConnectionLedger(limits, isLiveCapability) {
|
|
2188
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2189
|
+
const authenticated = /* @__PURE__ */ new Map();
|
|
2190
|
+
let tunnels = 0;
|
|
2191
|
+
const leavePending = (socket) => {
|
|
2192
|
+
const deadline = pending.get(socket);
|
|
2193
|
+
if (deadline !== void 0) {
|
|
2194
|
+
clearTimeout(deadline);
|
|
2195
|
+
pending.delete(socket);
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
const revalidation = setInterval(() => {
|
|
2199
|
+
for (const [capability, sockets] of authenticated) {
|
|
2200
|
+
if (!isLiveCapability(capability)) {
|
|
2201
|
+
for (const socket of sockets) {
|
|
2202
|
+
socket.destroy();
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}, limits.revalidateMs);
|
|
2207
|
+
revalidation.unref();
|
|
2208
|
+
return {
|
|
2209
|
+
accept: (socket) => {
|
|
2210
|
+
const [oldest] = pending.keys();
|
|
2211
|
+
if (pending.size >= limits.maxPendingHeads && oldest !== void 0) {
|
|
2212
|
+
leavePending(oldest);
|
|
2213
|
+
refuse(oldest, REFUSAL.requestTimeout);
|
|
2214
|
+
}
|
|
2215
|
+
pending.set(
|
|
2216
|
+
socket,
|
|
2217
|
+
setTimeout(() => {
|
|
2218
|
+
leavePending(socket);
|
|
2219
|
+
refuse(socket, REFUSAL.requestTimeout);
|
|
2220
|
+
}, limits.headDeadlineMs)
|
|
2221
|
+
);
|
|
2222
|
+
socket.once("close", () => {
|
|
2223
|
+
leavePending(socket);
|
|
2224
|
+
});
|
|
2225
|
+
},
|
|
2226
|
+
headReceived: leavePending,
|
|
2227
|
+
establish: (socket, capability) => {
|
|
2228
|
+
if (tunnels >= limits.maxTunnels) {
|
|
2229
|
+
return false;
|
|
2230
|
+
}
|
|
2231
|
+
tunnels += 1;
|
|
2232
|
+
const sockets = authenticated.get(capability) ?? /* @__PURE__ */ new Set();
|
|
2233
|
+
sockets.add(socket);
|
|
2234
|
+
authenticated.set(capability, sockets);
|
|
2235
|
+
socket.once("close", () => {
|
|
2236
|
+
tunnels -= 1;
|
|
2237
|
+
sockets.delete(socket);
|
|
2238
|
+
if (sockets.size === 0 && authenticated.get(capability) === sockets) {
|
|
2239
|
+
authenticated.delete(capability);
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
return true;
|
|
2243
|
+
},
|
|
2244
|
+
close: () => {
|
|
2245
|
+
clearInterval(revalidation);
|
|
2246
|
+
for (const deadline of pending.values()) {
|
|
2247
|
+
clearTimeout(deadline);
|
|
2248
|
+
}
|
|
2249
|
+
pending.clear();
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
function refuse(socket, status, headers = []) {
|
|
2254
|
+
socket.removeAllListeners("data");
|
|
2255
|
+
socket.pause();
|
|
2256
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
socket.end(`HTTP/1.1 ${status}\r
|
|
2260
|
+
${headers.map((header) => `${header}\r
|
|
2261
|
+
`).join("")}content-length: 0\r
|
|
2262
|
+
connection: close\r
|
|
2263
|
+
\r
|
|
2264
|
+
`, () => {
|
|
2265
|
+
socket.destroy();
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
function handleConnect(socket, context) {
|
|
2269
|
+
socket.on("error", () => {
|
|
2270
|
+
socket.destroy();
|
|
2271
|
+
});
|
|
2272
|
+
context.ledger.accept(socket);
|
|
1972
2273
|
let buffer = Buffer.alloc(0);
|
|
1973
2274
|
const onData = (chunk) => {
|
|
1974
2275
|
buffer = Buffer.concat([buffer, chunk]);
|
|
1975
2276
|
const headEnd = buffer.indexOf(HEAD_TERMINATOR);
|
|
2277
|
+
if (headEnd === -1 ? buffer.length > MAX_CONNECT_HEAD_BYTES : headEnd > MAX_CONNECT_HEAD_BYTES) {
|
|
2278
|
+
context.ledger.headReceived(socket);
|
|
2279
|
+
refuse(socket, REFUSAL.headTooLarge);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
1976
2282
|
if (headEnd === -1) {
|
|
1977
|
-
if (buffer.length > MAX_CONNECT_HEAD_BYTES) {
|
|
1978
|
-
socket.removeListener("data", onData);
|
|
1979
|
-
socket.pause();
|
|
1980
|
-
socket.destroy();
|
|
1981
|
-
}
|
|
1982
2283
|
return;
|
|
1983
2284
|
}
|
|
1984
2285
|
socket.removeListener("data", onData);
|
|
1985
2286
|
socket.pause();
|
|
2287
|
+
context.ledger.headReceived(socket);
|
|
1986
2288
|
const head = buffer.subarray(0, headEnd).toString("utf8");
|
|
1987
2289
|
socket.unshift(buffer.subarray(headEnd + HEAD_TERMINATOR.length));
|
|
1988
|
-
routeConnect(socket, head,
|
|
2290
|
+
routeConnect(socket, head, context);
|
|
1989
2291
|
};
|
|
1990
2292
|
socket.on("data", onData);
|
|
1991
|
-
socket.on("error", () => {
|
|
1992
|
-
socket.destroy();
|
|
1993
|
-
});
|
|
1994
2293
|
}
|
|
1995
|
-
function
|
|
2294
|
+
function proxyAuthorizationOf(head) {
|
|
2295
|
+
const values = head.split("\r\n").slice(1).flatMap((line) => {
|
|
2296
|
+
const colon = line.indexOf(":");
|
|
2297
|
+
return colon > 0 && line.slice(0, colon).trim().toLowerCase() === "proxy-authorization" ? [line.slice(colon + 1)] : [];
|
|
2298
|
+
});
|
|
2299
|
+
const [value] = values;
|
|
2300
|
+
return values.length === 1 ? value : void 0;
|
|
2301
|
+
}
|
|
2302
|
+
function routeConnect(socket, head, context) {
|
|
2303
|
+
const { config, effects, tlsAcceptor, ledger } = context;
|
|
2304
|
+
const credential = proxyAuthorizationOf(head);
|
|
2305
|
+
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
2306
|
+
if (capability === void 0 || !config.isLiveCapability(capability)) {
|
|
2307
|
+
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
1996
2310
|
const requestLine = head.split("\r\n", 1)[0] ?? "";
|
|
1997
2311
|
const parts = requestLine.split(" ");
|
|
1998
2312
|
const authority = parts.length === CONNECT_LINE_TOKENS && parts[0]?.toUpperCase() === "CONNECT" ? parts[1] ?? "" : void 0;
|
|
1999
2313
|
const target = authority === void 0 ? void 0 : parseConnectTarget(authority);
|
|
2000
2314
|
if (target === void 0) {
|
|
2001
|
-
socket.
|
|
2002
|
-
|
|
2315
|
+
refuse(socket, REFUSAL.badRequest);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
if (!ledger.establish(socket, capability)) {
|
|
2319
|
+
refuse(socket, REFUSAL.serviceUnavailable);
|
|
2003
2320
|
return;
|
|
2004
2321
|
}
|
|
2005
2322
|
socket.write(CONNECT_ESTABLISHED);
|
|
@@ -2017,6 +2334,10 @@ async function blindTunnel(socket, target, effects) {
|
|
|
2017
2334
|
socket.destroy();
|
|
2018
2335
|
return;
|
|
2019
2336
|
}
|
|
2337
|
+
if (socket.destroyed) {
|
|
2338
|
+
upstream.destroy();
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2020
2341
|
socket.pipe(upstream);
|
|
2021
2342
|
upstream.pipe(socket);
|
|
2022
2343
|
const drop = () => {
|
|
@@ -2032,7 +2353,7 @@ async function blindTunnel(socket, target, effects) {
|
|
|
2032
2353
|
socket.destroy();
|
|
2033
2354
|
});
|
|
2034
2355
|
}
|
|
2035
|
-
function
|
|
2356
|
+
function realConnectEffects() {
|
|
2036
2357
|
const plainAgent = new http.Agent({ keepAlive: true });
|
|
2037
2358
|
const tlsAgent = new https.Agent({ keepAlive: true });
|
|
2038
2359
|
const listenOnce = async (port, onSocket) => await new Promise((resolve, reject) => {
|
|
@@ -2161,7 +2482,7 @@ function realMitmEffects() {
|
|
|
2161
2482
|
function isEnoent(error) {
|
|
2162
2483
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2163
2484
|
}
|
|
2164
|
-
function
|
|
2485
|
+
function realConnectCertStore(paths) {
|
|
2165
2486
|
const read = (file) => {
|
|
2166
2487
|
try {
|
|
2167
2488
|
return fs.readFileSync(file, "utf8");
|
|
@@ -2174,54 +2495,49 @@ function realMitmCertStore(paths) {
|
|
|
2174
2495
|
};
|
|
2175
2496
|
return {
|
|
2176
2497
|
loadCa: () => {
|
|
2177
|
-
const certPem = read(paths.
|
|
2178
|
-
const keyPem = read(paths.
|
|
2498
|
+
const certPem = read(paths.frontdoorCaCertFile);
|
|
2499
|
+
const keyPem = read(paths.frontdoorCaKeyFile);
|
|
2179
2500
|
return certPem === void 0 || keyPem === void 0 ? void 0 : { certPem, keyPem };
|
|
2180
2501
|
},
|
|
2181
2502
|
writeCa: (ca) => {
|
|
2182
|
-
fs.mkdirSync(paths.
|
|
2183
|
-
fs.writeFileSync(paths.
|
|
2184
|
-
fs.writeFileSync(paths.
|
|
2185
|
-
fs.chmodSync(paths.
|
|
2503
|
+
fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
|
|
2504
|
+
fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
|
|
2505
|
+
fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
|
|
2506
|
+
fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
|
|
2186
2507
|
}
|
|
2187
2508
|
};
|
|
2188
2509
|
}
|
|
2189
2510
|
|
|
2511
|
+
// src/frontdoor/ensure.ts
|
|
2512
|
+
var import_node_crypto3 = require("node:crypto");
|
|
2513
|
+
|
|
2190
2514
|
// src/headroom/state.ts
|
|
2191
2515
|
var import_node_crypto2 = require("node:crypto");
|
|
2192
|
-
var
|
|
2193
|
-
var
|
|
2516
|
+
var import_node_path6 = __toESM(require("node:path"), 1);
|
|
2517
|
+
var import_zod4 = require("zod");
|
|
2194
2518
|
var HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
|
|
2195
|
-
var HeadroomStateSchema =
|
|
2519
|
+
var HeadroomStateSchema = import_zod4.z.strictObject({
|
|
2196
2520
|
/** The supervisor process owning the daemon. Alive means someone is keeping headroom running. */
|
|
2197
|
-
supervisorPid:
|
|
2521
|
+
supervisorPid: import_zod4.z.number().int().positive().optional(),
|
|
2198
2522
|
/** The headroom proxy process itself. Absent while the supervisor is between restarts. */
|
|
2199
|
-
headroomPid:
|
|
2523
|
+
headroomPid: import_zod4.z.number().int().positive().optional(),
|
|
2200
2524
|
/** The loopback port the proxy listens on. Absent until the proxy has passed its readiness check, so "port is set" is itself the ready signal a launcher polls for. */
|
|
2201
|
-
port:
|
|
2202
|
-
/**
|
|
2203
|
-
* The loopback port the supervisor's in-process MITM CONNECT proxy listens on. Absent whenever no supervisor is serving it, so "mitmPort is set" is the ready signal an OAuth launch polls for alongside `port`.
|
|
2204
|
-
*/
|
|
2205
|
-
mitmPort: import_zod3.z.number().int().positive().optional(),
|
|
2206
|
-
/**
|
|
2207
|
-
* The sticky MITM port preference, the exact analogue of `lastPort` for the second server: it survives every shutdown, because every OAuth session's environment was frozen at launch with HTTPS_PROXY pointing at this address and a restart that moves strands them.
|
|
2208
|
-
*/
|
|
2209
|
-
lastMitmPort: import_zod3.z.number().int().positive().optional(),
|
|
2525
|
+
port: import_zod4.z.number().int().positive().optional(),
|
|
2210
2526
|
/**
|
|
2211
|
-
* The sticky port preference: the address the daemon last served on, kept across crashes, restarts, and idle shutdowns so the next start reuses it. Distinct from `port` on purpose: `port` is the ready signal (absent whenever nothing is serving), while `lastPort` survives every shutdown, because
|
|
2527
|
+
* The sticky port preference: the address the daemon last served on, kept across crashes, restarts, and idle shutdowns so the next start reuses it. Distinct from `port` on purpose: `port` is the ready signal (absent whenever nothing is serving), while `lastPort` survives every shutdown, because the front door (which routes sessions through this daemon) reads it live and follows a restart to whichever address it lands on.
|
|
2212
2528
|
*/
|
|
2213
|
-
lastPort:
|
|
2529
|
+
lastPort: import_zod4.z.number().int().positive().optional(),
|
|
2214
2530
|
/** The `headroom --version` output of the running install. */
|
|
2215
|
-
version:
|
|
2531
|
+
version: import_zod4.z.string().optional(),
|
|
2216
2532
|
/** Hash of the allowlist the running proxy was started with, so a provider-file change is detected as drift. */
|
|
2217
|
-
allowlistHash:
|
|
2533
|
+
allowlistHash: import_zod4.z.string().optional(),
|
|
2218
2534
|
/** The install spec the running install was last installed from, so a `source` config change is detected as drift. */
|
|
2219
|
-
installedSource:
|
|
2535
|
+
installedSource: import_zod4.z.string().optional(),
|
|
2220
2536
|
/** The last fatal error, kept across shutdowns so `headroom status` can explain a daemon that is not running. */
|
|
2221
|
-
lastError:
|
|
2537
|
+
lastError: import_zod4.z.string().optional()
|
|
2222
2538
|
});
|
|
2223
|
-
var HeadroomSessionSchema =
|
|
2224
|
-
var HeadroomLockSchema =
|
|
2539
|
+
var HeadroomSessionSchema = import_zod4.z.strictObject({ pid: import_zod4.z.number().int().positive(), startedAt: import_zod4.z.number() });
|
|
2540
|
+
var HeadroomLockSchema = import_zod4.z.strictObject({ pid: import_zod4.z.number().int().positive(), at: import_zod4.z.number() });
|
|
2225
2541
|
function parseJson(raw, schema) {
|
|
2226
2542
|
if (raw === void 0 || raw.trim() === "") {
|
|
2227
2543
|
return void 0;
|
|
@@ -2237,7 +2553,7 @@ function readHeadroomState(fs2, stateFile) {
|
|
|
2237
2553
|
}
|
|
2238
2554
|
}
|
|
2239
2555
|
function writeHeadroomState(fs2, stateFile, state) {
|
|
2240
|
-
fs2.mkdirp(
|
|
2556
|
+
fs2.mkdirp(import_node_path6.default.dirname(stateFile));
|
|
2241
2557
|
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
2242
2558
|
`);
|
|
2243
2559
|
}
|
|
@@ -2249,16 +2565,8 @@ function headroomAllowlist(providers) {
|
|
|
2249
2565
|
return [...urls].sort();
|
|
2250
2566
|
}
|
|
2251
2567
|
function headroomUpstreams(providers, frontDoorOrigin2) {
|
|
2252
|
-
const upstreams =
|
|
2253
|
-
|
|
2254
|
-
for (const provider of providers) {
|
|
2255
|
-
if (isCodexProvider(provider)) {
|
|
2256
|
-
hasCodex = true;
|
|
2257
|
-
} else {
|
|
2258
|
-
upstreams.push({ baseUrl: provider.baseUrl });
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
if (hasCodex && frontDoorOrigin2 !== void 0) {
|
|
2568
|
+
const upstreams = providers.filter((provider) => !isCodexProvider(provider)).map((provider) => ({ baseUrl: provider.baseUrl }));
|
|
2569
|
+
if (providers.length > 0 && frontDoorOrigin2 !== void 0) {
|
|
2262
2570
|
upstreams.push({ baseUrl: frontDoorOrigin2 });
|
|
2263
2571
|
}
|
|
2264
2572
|
return upstreams;
|
|
@@ -2273,7 +2581,7 @@ function readAllProviders(fs2, providersDir) {
|
|
|
2273
2581
|
continue;
|
|
2274
2582
|
}
|
|
2275
2583
|
try {
|
|
2276
|
-
const parsed = parseJson(fs2.readFileUtf8(
|
|
2584
|
+
const parsed = parseJson(fs2.readFileUtf8(import_node_path6.default.join(providersDir, name)), ProviderSchema);
|
|
2277
2585
|
if (parsed !== void 0) {
|
|
2278
2586
|
result.push({ name: name.slice(0, -".json".length), provider: parsed });
|
|
2279
2587
|
}
|
|
@@ -2285,11 +2593,11 @@ function readAllProviders(fs2, providersDir) {
|
|
|
2285
2593
|
}
|
|
2286
2594
|
function writeSession(fs2, sessionsDir, session) {
|
|
2287
2595
|
fs2.mkdirp(sessionsDir);
|
|
2288
|
-
fs2.writeFileUtf8(
|
|
2596
|
+
fs2.writeFileUtf8(import_node_path6.default.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
2289
2597
|
`);
|
|
2290
2598
|
}
|
|
2291
2599
|
function removeSession(fs2, sessionsDir, pid) {
|
|
2292
|
-
fs2.removeRecursive(
|
|
2600
|
+
fs2.removeRecursive(import_node_path6.default.join(sessionsDir, `${String(pid)}.json`));
|
|
2293
2601
|
}
|
|
2294
2602
|
function listSessions(fs2, sessionsDir) {
|
|
2295
2603
|
const sessions = [];
|
|
@@ -2298,7 +2606,7 @@ function listSessions(fs2, sessionsDir) {
|
|
|
2298
2606
|
continue;
|
|
2299
2607
|
}
|
|
2300
2608
|
try {
|
|
2301
|
-
const parsed = parseJson(fs2.readFileUtf8(
|
|
2609
|
+
const parsed = parseJson(fs2.readFileUtf8(import_node_path6.default.join(sessionsDir, name)), HeadroomSessionSchema);
|
|
2302
2610
|
if (parsed !== void 0) {
|
|
2303
2611
|
sessions.push(parsed);
|
|
2304
2612
|
}
|
|
@@ -2326,6 +2634,67 @@ function readStartLock(fs2, lockFile) {
|
|
|
2326
2634
|
}
|
|
2327
2635
|
}
|
|
2328
2636
|
|
|
2637
|
+
// src/frontdoor/ensure.ts
|
|
2638
|
+
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
2639
|
+
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
2640
|
+
var FrontDoorStartError = class extends CliError {
|
|
2641
|
+
constructor(message) {
|
|
2642
|
+
super(message);
|
|
2643
|
+
this.name = "FrontDoorStartError";
|
|
2644
|
+
}
|
|
2645
|
+
};
|
|
2646
|
+
function ensureFrontDoor(params) {
|
|
2647
|
+
const { paths, ports } = params;
|
|
2648
|
+
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
2649
|
+
let spawned = false;
|
|
2650
|
+
const distrusted = /* @__PURE__ */ new Set();
|
|
2651
|
+
let lastRejection;
|
|
2652
|
+
for (; ; ) {
|
|
2653
|
+
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
2654
|
+
const supervisorPid = state?.supervisorPid;
|
|
2655
|
+
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
2656
|
+
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
2657
|
+
const verdict = ports.verifyListener(state.port);
|
|
2658
|
+
if (verdict.ok) {
|
|
2659
|
+
const token = (0, import_node_crypto3.randomUUID)();
|
|
2660
|
+
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
2661
|
+
return { port: state.port, connectPort: state.connectPort, token };
|
|
2662
|
+
}
|
|
2663
|
+
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
2664
|
+
if (spawned) {
|
|
2665
|
+
throw new FrontDoorStartError(`claude-use: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
2666
|
+
}
|
|
2667
|
+
distrusted.add(supervisorPid);
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
if (state?.lastError !== void 0) {
|
|
2671
|
+
throw new FrontDoorStartError(`claude-use: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
2672
|
+
}
|
|
2673
|
+
} else if (!spawned) {
|
|
2674
|
+
ports.fs.mkdirp(paths.frontdoorDir);
|
|
2675
|
+
const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
2676
|
+
`);
|
|
2677
|
+
if (created) {
|
|
2678
|
+
ports.spawnSupervisor(paths);
|
|
2679
|
+
spawned = true;
|
|
2680
|
+
} else {
|
|
2681
|
+
const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
|
|
2682
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
2683
|
+
ports.fs.removeRecursive(paths.frontdoorLockFile);
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
if (ports.now() >= deadline) {
|
|
2689
|
+
const lastError = state?.lastError;
|
|
2690
|
+
throw new FrontDoorStartError(
|
|
2691
|
+
`claude-use: 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}`
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2694
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2329
2698
|
// src/headroom/ensure.ts
|
|
2330
2699
|
var HEADROOM_START_TIMEOUT_MS = 12e4;
|
|
2331
2700
|
var HEADROOM_LAUNCHER_POLL_MS = 250;
|
|
@@ -2342,10 +2711,10 @@ function ensureHeadroom(params) {
|
|
|
2342
2711
|
for (; ; ) {
|
|
2343
2712
|
const state = readHeadroomState(ports.fs, paths.headroomStateFile);
|
|
2344
2713
|
if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
|
|
2345
|
-
const daemonUp = state.port !== void 0 && state.
|
|
2346
|
-
if (daemonUp && state.port !== void 0
|
|
2714
|
+
const daemonUp = state.port !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
|
|
2715
|
+
if (daemonUp && state.port !== void 0) {
|
|
2347
2716
|
writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now() });
|
|
2348
|
-
return { port: state.port
|
|
2717
|
+
return { port: state.port };
|
|
2349
2718
|
}
|
|
2350
2719
|
if (state.lastError !== void 0) {
|
|
2351
2720
|
throw new HeadroomStartError(
|
|
@@ -2380,42 +2749,6 @@ function ensureHeadroom(params) {
|
|
|
2380
2749
|
}
|
|
2381
2750
|
}
|
|
2382
2751
|
|
|
2383
|
-
// src/frontdoor/state.ts
|
|
2384
|
-
var import_zod4 = require("zod");
|
|
2385
|
-
var FrontDoorStateSchema = import_zod4.z.strictObject({
|
|
2386
|
-
/** The supervisor process serving the front door. Alive means the listener is up in that process. */
|
|
2387
|
-
supervisorPid: import_zod4.z.number().int().positive().optional(),
|
|
2388
|
-
/** The loopback port the front-door listener serves on. Absent until the listener has bound and answered its health probe, so "port is set" is itself the ready signal a launcher polls for. */
|
|
2389
|
-
port: import_zod4.z.number().int().positive().optional(),
|
|
2390
|
-
/**
|
|
2391
|
-
* The sticky port preference: kept across crashes, restarts and idle shutdowns so every session, whose base URL was frozen at launch, keeps finding the front door at the same address.
|
|
2392
|
-
*/
|
|
2393
|
-
lastPort: import_zod4.z.number().int().positive().optional(),
|
|
2394
|
-
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
2395
|
-
lastError: import_zod4.z.string().optional()
|
|
2396
|
-
});
|
|
2397
|
-
function readFrontDoorState(fs2, stateFile) {
|
|
2398
|
-
let raw;
|
|
2399
|
-
try {
|
|
2400
|
-
raw = fs2.readFileUtf8(stateFile);
|
|
2401
|
-
} catch {
|
|
2402
|
-
return void 0;
|
|
2403
|
-
}
|
|
2404
|
-
if (raw === void 0 || raw.trim() === "") {
|
|
2405
|
-
return void 0;
|
|
2406
|
-
}
|
|
2407
|
-
try {
|
|
2408
|
-
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
2409
|
-
return parsed.success ? parsed.data : void 0;
|
|
2410
|
-
} catch {
|
|
2411
|
-
return void 0;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
function frontDoorOrigin(state) {
|
|
2415
|
-
const port = state?.port ?? state?.lastPort;
|
|
2416
|
-
return port === void 0 ? void 0 : `http://127.0.0.1:${String(port)}`;
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
2752
|
// src/headroom/supervisor.ts
|
|
2420
2753
|
function resolveSupervisorConfig(configured) {
|
|
2421
2754
|
return {
|
|
@@ -2428,7 +2761,7 @@ var HEADROOM_BACKOFF_CAP_MS = 3e4;
|
|
|
2428
2761
|
var HEADROOM_START_RETRY_BUDGET = 5;
|
|
2429
2762
|
var HEADROOM_READY_TIMEOUT_MS = 15e3;
|
|
2430
2763
|
var HEADROOM_POLL_MS = 1e3;
|
|
2431
|
-
var
|
|
2764
|
+
var MS_PER_MINUTE2 = 6e4;
|
|
2432
2765
|
function backoffForAttempt(attempt) {
|
|
2433
2766
|
return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
|
|
2434
2767
|
}
|
|
@@ -2488,13 +2821,7 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2488
2821
|
const previousState = readHeadroomState(fs2, paths.headroomStateFile);
|
|
2489
2822
|
let installedSource = previousState?.installedSource;
|
|
2490
2823
|
let lastPort = previousState?.lastPort;
|
|
2491
|
-
|
|
2492
|
-
let daemonPort;
|
|
2493
|
-
const sticky = () => ({
|
|
2494
|
-
...lastPort === void 0 ? {} : { lastPort },
|
|
2495
|
-
...lastMitmPort === void 0 ? {} : { lastMitmPort }
|
|
2496
|
-
});
|
|
2497
|
-
let mitm;
|
|
2824
|
+
const sticky = () => lastPort === void 0 ? {} : { lastPort };
|
|
2498
2825
|
const fail = (message) => {
|
|
2499
2826
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2500
2827
|
lastError: message,
|
|
@@ -2523,18 +2850,7 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2523
2850
|
return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
|
|
2524
2851
|
}
|
|
2525
2852
|
const version = ports.headroomVersion() ?? "unknown";
|
|
2526
|
-
|
|
2527
|
-
mitm = await ports.startMitm(lastMitmPort, () => daemonPort);
|
|
2528
|
-
} catch (error) {
|
|
2529
|
-
return fail(`could not start the MITM proxy: ${error instanceof Error ? error.message : String(error)}`);
|
|
2530
|
-
}
|
|
2531
|
-
lastMitmPort = mitm.port;
|
|
2532
|
-
if (previousState?.lastMitmPort !== void 0 && mitm.port !== previousState.lastMitmPort) {
|
|
2533
|
-
ports.log(
|
|
2534
|
-
`claude-use headroom supervisor: sticky MITM port ${String(previousState.lastMitmPort)} was occupied; moving to ${String(mitm.port)} (OAuth sessions launched against the old port are stale until they relaunch)`
|
|
2535
|
-
);
|
|
2536
|
-
}
|
|
2537
|
-
writeHeadroomState(fs2, paths.headroomStateFile, { supervisorPid: ports.ownPid, version, installedSource, mitmPort: mitm.port, ...sticky() });
|
|
2853
|
+
writeHeadroomState(fs2, paths.headroomStateFile, { supervisorPid: ports.ownPid, version, installedSource, ...sticky() });
|
|
2538
2854
|
ports.log(`claude-use headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
|
|
2539
2855
|
const orphanPid = previousState?.headroomPid;
|
|
2540
2856
|
if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
|
|
@@ -2559,13 +2875,11 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2559
2875
|
if (crashed) {
|
|
2560
2876
|
ports.log(`claude-use headroom supervisor: headroom pid ${String(headroomPid)} died`);
|
|
2561
2877
|
headroomPid = void 0;
|
|
2562
|
-
daemonPort = void 0;
|
|
2563
2878
|
runningHash = void 0;
|
|
2564
2879
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2565
2880
|
supervisorPid: ports.ownPid,
|
|
2566
2881
|
version,
|
|
2567
2882
|
installedSource,
|
|
2568
|
-
mitmPort: mitm.port,
|
|
2569
2883
|
...sticky()
|
|
2570
2884
|
});
|
|
2571
2885
|
}
|
|
@@ -2582,7 +2896,6 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2582
2896
|
if (ready) {
|
|
2583
2897
|
consecutiveFailures = 0;
|
|
2584
2898
|
headroomPid = pid;
|
|
2585
|
-
daemonPort = port;
|
|
2586
2899
|
runningHash = allowlistHash;
|
|
2587
2900
|
lastPort = port;
|
|
2588
2901
|
if (previousSticky !== void 0 && port !== previousSticky) {
|
|
@@ -2594,7 +2907,6 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2594
2907
|
supervisorPid: ports.ownPid,
|
|
2595
2908
|
headroomPid: pid,
|
|
2596
2909
|
port,
|
|
2597
|
-
mitmPort: mitm.port,
|
|
2598
2910
|
version,
|
|
2599
2911
|
allowlistHash,
|
|
2600
2912
|
installedSource: config.source,
|
|
@@ -2618,7 +2930,6 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2618
2930
|
ports.log("claude-use headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
|
|
2619
2931
|
ports.stopProcess(headroomPid);
|
|
2620
2932
|
headroomPid = void 0;
|
|
2621
|
-
daemonPort = void 0;
|
|
2622
2933
|
runningHash = void 0;
|
|
2623
2934
|
continue;
|
|
2624
2935
|
}
|
|
@@ -2627,13 +2938,11 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2627
2938
|
if (sessions.length === 0) {
|
|
2628
2939
|
idleSince ??= ports.now();
|
|
2629
2940
|
const idleMs = ports.now() - idleSince;
|
|
2630
|
-
if (idleMs >= config.idleShutdownMinutes *
|
|
2941
|
+
if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
|
|
2631
2942
|
ports.log(
|
|
2632
|
-
`claude-use headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and
|
|
2943
|
+
`claude-use headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
|
|
2633
2944
|
);
|
|
2634
2945
|
ports.stopProcess(headroomPid);
|
|
2635
|
-
daemonPort = void 0;
|
|
2636
|
-
await mitm.close();
|
|
2637
2946
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2638
2947
|
version,
|
|
2639
2948
|
...sticky()
|
|
@@ -2716,15 +3025,18 @@ function evaluateAmbientCredentialGuard(params) {
|
|
|
2716
3025
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2717
3026
|
0 && (module.exports = {
|
|
2718
3027
|
AMBIENT_CREDENTIAL_VARS,
|
|
3028
|
+
CONNECT_INTERCEPT_HOST,
|
|
3029
|
+
CONNECT_LIMITS,
|
|
2719
3030
|
FARM_MANIFEST_FILENAME,
|
|
2720
|
-
|
|
3031
|
+
FrontDoorStartError,
|
|
2721
3032
|
HeadroomStartError,
|
|
2722
|
-
|
|
3033
|
+
ROUTED_PATH_PREFIX,
|
|
2723
3034
|
buildEntryFacts,
|
|
2724
3035
|
carryOver,
|
|
2725
3036
|
createLeafCache,
|
|
2726
3037
|
detectAmbientCredential,
|
|
2727
3038
|
ensureCa,
|
|
3039
|
+
ensureFrontDoor,
|
|
2728
3040
|
ensureHeadroom,
|
|
2729
3041
|
evaluateAmbientCredentialGuard,
|
|
2730
3042
|
formatAmbientCredentialGuardMessage,
|
|
@@ -2734,15 +3046,16 @@ function evaluateAmbientCredentialGuard(params) {
|
|
|
2734
3046
|
mintLeaf,
|
|
2735
3047
|
parseConnectTarget,
|
|
2736
3048
|
readFarmManifest,
|
|
2737
|
-
|
|
2738
|
-
|
|
3049
|
+
realConnectCertStore,
|
|
3050
|
+
realConnectEffects,
|
|
2739
3051
|
recoverFarm,
|
|
2740
3052
|
recoveryDiagnostics,
|
|
2741
3053
|
resolveDecisions,
|
|
2742
3054
|
resolveSupervisorConfig,
|
|
2743
3055
|
resyncFarm,
|
|
3056
|
+
runFrontDoorSupervisor,
|
|
2744
3057
|
runSupervisor,
|
|
2745
|
-
|
|
2746
|
-
|
|
3058
|
+
servedByPipeline,
|
|
3059
|
+
startConnectServer,
|
|
2747
3060
|
topLevelNames
|
|
2748
3061
|
});
|