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.mjs
CHANGED
|
@@ -1733,15 +1733,213 @@ function recoveryDiagnostics(recovery, identity) {
|
|
|
1733
1733
|
];
|
|
1734
1734
|
}
|
|
1735
1735
|
|
|
1736
|
-
// src/
|
|
1736
|
+
// src/frontdoor/connect.ts
|
|
1737
1737
|
import * as fs from "node:fs";
|
|
1738
1738
|
import * as http from "node:http";
|
|
1739
1739
|
import * as https from "node:https";
|
|
1740
1740
|
import * as net from "node:net";
|
|
1741
1741
|
import * as tls from "node:tls";
|
|
1742
1742
|
import forge from "node-forge";
|
|
1743
|
-
|
|
1744
|
-
|
|
1743
|
+
|
|
1744
|
+
// src/frontdoor/capability.ts
|
|
1745
|
+
var CONNECT_PROXY_REALM = "claude-use front door";
|
|
1746
|
+
var BASIC_SCHEME = "basic";
|
|
1747
|
+
function capabilityFromProxyAuthorization(value) {
|
|
1748
|
+
const trimmed = value.trim();
|
|
1749
|
+
const space = trimmed.indexOf(" ");
|
|
1750
|
+
if (space === -1 || trimmed.slice(0, space).toLowerCase() !== BASIC_SCHEME) {
|
|
1751
|
+
return void 0;
|
|
1752
|
+
}
|
|
1753
|
+
const userPass = Buffer.from(trimmed.slice(space + 1).trim(), "base64").toString("utf8");
|
|
1754
|
+
const colon = userPass.indexOf(":");
|
|
1755
|
+
if (colon === -1) {
|
|
1756
|
+
return void 0;
|
|
1757
|
+
}
|
|
1758
|
+
const password = userPass.slice(colon + 1);
|
|
1759
|
+
return password === "" ? void 0 : password;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/frontdoor/state.ts
|
|
1763
|
+
import path5 from "node:path";
|
|
1764
|
+
import { z as z3 } from "zod";
|
|
1765
|
+
var FrontDoorSessionSchema = z3.strictObject({ pid: z3.number().int().positive(), startedAt: z3.number(), token: z3.string().min(1) });
|
|
1766
|
+
var FrontDoorStateSchema = z3.strictObject({
|
|
1767
|
+
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
1768
|
+
supervisorPid: z3.number().int().positive().optional(),
|
|
1769
|
+
/** 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. */
|
|
1770
|
+
port: z3.number().int().positive().optional(),
|
|
1771
|
+
/**
|
|
1772
|
+
* 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.
|
|
1773
|
+
*/
|
|
1774
|
+
lastPort: z3.number().int().positive().optional(),
|
|
1775
|
+
/**
|
|
1776
|
+
* 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`.
|
|
1777
|
+
*/
|
|
1778
|
+
connectPort: z3.number().int().positive().optional(),
|
|
1779
|
+
/**
|
|
1780
|
+
* 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.
|
|
1781
|
+
*/
|
|
1782
|
+
lastConnectPort: z3.number().int().positive().optional(),
|
|
1783
|
+
/**
|
|
1784
|
+
* 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.
|
|
1785
|
+
*/
|
|
1786
|
+
directPort: z3.number().int().positive().optional(),
|
|
1787
|
+
/** The sticky port preference for the direct listener, for the reason `directPort` gives. */
|
|
1788
|
+
lastDirectPort: z3.number().int().positive().optional(),
|
|
1789
|
+
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
1790
|
+
lastError: z3.string().optional()
|
|
1791
|
+
});
|
|
1792
|
+
function readFrontDoorState(fs2, stateFile) {
|
|
1793
|
+
let raw;
|
|
1794
|
+
try {
|
|
1795
|
+
raw = fs2.readFileUtf8(stateFile);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return void 0;
|
|
1798
|
+
}
|
|
1799
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1800
|
+
return void 0;
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
1804
|
+
return parsed.success ? parsed.data : void 0;
|
|
1805
|
+
} catch {
|
|
1806
|
+
return void 0;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function writeFrontDoorState(fs2, stateFile, state) {
|
|
1810
|
+
fs2.mkdirp(path5.dirname(stateFile));
|
|
1811
|
+
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
1812
|
+
`);
|
|
1813
|
+
}
|
|
1814
|
+
function frontDoorOrigin(state) {
|
|
1815
|
+
const port = state?.directPort ?? state?.lastDirectPort;
|
|
1816
|
+
return port === void 0 ? void 0 : `http://127.0.0.1:${String(port)}`;
|
|
1817
|
+
}
|
|
1818
|
+
function writeFrontDoorSession(fs2, sessionsDir, session) {
|
|
1819
|
+
fs2.mkdirPrivate(sessionsDir);
|
|
1820
|
+
fs2.writeFilePrivate(path5.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
1821
|
+
`);
|
|
1822
|
+
}
|
|
1823
|
+
function listFrontDoorSessions(fs2, sessionsDir) {
|
|
1824
|
+
const sessions = [];
|
|
1825
|
+
for (const name of fs2.readdir(sessionsDir)) {
|
|
1826
|
+
if (!name.endsWith(".json")) {
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
try {
|
|
1830
|
+
const parsed = FrontDoorSessionSchema.safeParse(JSON.parse(fs2.readFileUtf8(path5.join(sessionsDir, name)) ?? ""));
|
|
1831
|
+
if (parsed.success) {
|
|
1832
|
+
sessions.push({ pid: parsed.data.pid, startedAt: parsed.data.startedAt });
|
|
1833
|
+
}
|
|
1834
|
+
} catch {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return sessions.sort((a, b) => a.pid - b.pid);
|
|
1839
|
+
}
|
|
1840
|
+
function removeFrontDoorSession(fs2, sessionsDir, pid) {
|
|
1841
|
+
fs2.removeRecursive(path5.join(sessionsDir, `${String(pid)}.json`));
|
|
1842
|
+
}
|
|
1843
|
+
function pruneDeadFrontDoorSessions(fs2, sessionsDir, isRunning) {
|
|
1844
|
+
const removed = [];
|
|
1845
|
+
for (const session of listFrontDoorSessions(fs2, sessionsDir)) {
|
|
1846
|
+
if (!isRunning(session.pid)) {
|
|
1847
|
+
removeFrontDoorSession(fs2, sessionsDir, session.pid);
|
|
1848
|
+
removed.push(session.pid);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return removed;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/frontdoor/supervisor.ts
|
|
1855
|
+
var FRONTDOOR_SUPERVISOR_STILL_RUNNING = -1;
|
|
1856
|
+
var FRONTDOOR_POLL_MS = 1e3;
|
|
1857
|
+
var MS_PER_MINUTE = 6e4;
|
|
1858
|
+
async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {}) {
|
|
1859
|
+
const { fs: fs2, paths } = ports;
|
|
1860
|
+
const previous = readFrontDoorState(fs2, paths.frontdoorStateFile);
|
|
1861
|
+
let lastPort = previous?.lastPort;
|
|
1862
|
+
let lastConnectPort = previous?.lastConnectPort;
|
|
1863
|
+
let lastDirectPort = previous?.lastDirectPort;
|
|
1864
|
+
const sticky = () => ({
|
|
1865
|
+
...lastPort === void 0 ? {} : { lastPort },
|
|
1866
|
+
...lastConnectPort === void 0 ? {} : { lastConnectPort },
|
|
1867
|
+
...lastDirectPort === void 0 ? {} : { lastDirectPort }
|
|
1868
|
+
});
|
|
1869
|
+
const fail = (message) => {
|
|
1870
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { lastError: message, ...sticky() });
|
|
1871
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1872
|
+
ports.log(`claude-use frontdoor supervisor: ${message}`);
|
|
1873
|
+
return 1;
|
|
1874
|
+
};
|
|
1875
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
1876
|
+
const previousSticky = lastPort;
|
|
1877
|
+
let http2;
|
|
1878
|
+
try {
|
|
1879
|
+
http2 = await ports.startProviderListener(lastPort);
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1882
|
+
}
|
|
1883
|
+
if (previousSticky !== void 0 && http2.port !== previousSticky) {
|
|
1884
|
+
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)`);
|
|
1885
|
+
}
|
|
1886
|
+
lastPort = http2.port;
|
|
1887
|
+
const previousStickyConnect = lastConnectPort;
|
|
1888
|
+
let connect2;
|
|
1889
|
+
try {
|
|
1890
|
+
connect2 = await ports.startConnectListener(lastConnectPort);
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
await http2.close();
|
|
1893
|
+
return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
1894
|
+
}
|
|
1895
|
+
if (previousStickyConnect !== void 0 && connect2.port !== previousStickyConnect) {
|
|
1896
|
+
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)`);
|
|
1897
|
+
}
|
|
1898
|
+
lastConnectPort = connect2.port;
|
|
1899
|
+
const previousStickyDirect = lastDirectPort;
|
|
1900
|
+
let direct;
|
|
1901
|
+
try {
|
|
1902
|
+
direct = await ports.startDirectListener(lastDirectPort);
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
await connect2.close();
|
|
1905
|
+
await http2.close();
|
|
1906
|
+
return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1907
|
+
}
|
|
1908
|
+
if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
|
|
1909
|
+
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)`);
|
|
1910
|
+
}
|
|
1911
|
+
lastDirectPort = direct.port;
|
|
1912
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect2.port, directPort: direct.port, ...sticky() });
|
|
1913
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1914
|
+
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)}`);
|
|
1915
|
+
let idleSince;
|
|
1916
|
+
let ticks = 0;
|
|
1917
|
+
for (; ; ) {
|
|
1918
|
+
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
1919
|
+
return FRONTDOOR_SUPERVISOR_STILL_RUNNING;
|
|
1920
|
+
}
|
|
1921
|
+
ticks += 1;
|
|
1922
|
+
pruneDeadFrontDoorSessions(fs2, paths.frontdoorSessionsDir, ports.isRunning);
|
|
1923
|
+
if (listFrontDoorSessions(fs2, paths.frontdoorSessionsDir).length === 0) {
|
|
1924
|
+
idleSince ??= ports.now();
|
|
1925
|
+
if (ports.now() - idleSince >= idleShutdownMinutes * MS_PER_MINUTE) {
|
|
1926
|
+
ports.log(`claude-use frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
|
|
1927
|
+
await direct.close();
|
|
1928
|
+
await connect2.close();
|
|
1929
|
+
await http2.close();
|
|
1930
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, sticky());
|
|
1931
|
+
return 0;
|
|
1932
|
+
}
|
|
1933
|
+
} else {
|
|
1934
|
+
idleSince = void 0;
|
|
1935
|
+
}
|
|
1936
|
+
await ports.sleep(FRONTDOOR_POLL_MS);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/frontdoor/connect.ts
|
|
1941
|
+
var CONNECT_INTERCEPT_HOST = "api.anthropic.com";
|
|
1942
|
+
var ROUTED_PATH_PREFIX = "/v1/";
|
|
1745
1943
|
var HTTPS_PORT = 443;
|
|
1746
1944
|
var MAX_PORT = 65535;
|
|
1747
1945
|
var SERIAL_NUMBER_BYTES = 16;
|
|
@@ -1751,9 +1949,22 @@ var CONNECT_LINE_TOKENS = 3;
|
|
|
1751
1949
|
var HEAD_TERMINATOR = "\r\n\r\n";
|
|
1752
1950
|
var MAX_CONNECT_HEAD_BYTES = 8192;
|
|
1753
1951
|
var CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
1952
|
+
var REFUSAL = {
|
|
1953
|
+
badRequest: "400 Bad Request",
|
|
1954
|
+
proxyAuthenticationRequired: "407 Proxy Authentication Required",
|
|
1955
|
+
requestTimeout: "408 Request Timeout",
|
|
1956
|
+
headTooLarge: "431 Request Header Fields Too Large",
|
|
1957
|
+
serviceUnavailable: "503 Service Unavailable"
|
|
1958
|
+
};
|
|
1959
|
+
var CONNECT_LIMITS = {
|
|
1960
|
+
headDeadlineMs: 1e4,
|
|
1961
|
+
maxPendingHeads: 64,
|
|
1962
|
+
maxTunnels: 1024,
|
|
1963
|
+
revalidateMs: FRONTDOOR_POLL_MS
|
|
1964
|
+
};
|
|
1754
1965
|
var CA_VALIDITY_DAYS = 3650;
|
|
1755
1966
|
var LEAF_VALIDITY_DAYS = 397;
|
|
1756
|
-
var CA_SUBJECT_COMMON_NAME = "claude-use
|
|
1967
|
+
var CA_SUBJECT_COMMON_NAME = "claude-use front door CA";
|
|
1757
1968
|
var MS_PER_DAY = 864e5;
|
|
1758
1969
|
function parseConnectTarget(authority) {
|
|
1759
1970
|
const colon = authority.lastIndexOf(":");
|
|
@@ -1771,8 +1982,8 @@ function parseConnectTarget(authority) {
|
|
|
1771
1982
|
function isInterceptedHost(host, interceptHost) {
|
|
1772
1983
|
return host === interceptHost;
|
|
1773
1984
|
}
|
|
1774
|
-
function
|
|
1775
|
-
return
|
|
1985
|
+
function servedByPipeline(path7) {
|
|
1986
|
+
return path7?.startsWith(ROUTED_PATH_PREFIX) === true;
|
|
1776
1987
|
}
|
|
1777
1988
|
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1778
1989
|
"connection",
|
|
@@ -1820,7 +2031,13 @@ function generateCa(now) {
|
|
|
1820
2031
|
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
1821
2032
|
return { certPem: forge.pki.certificateToPem(cert), keyPem: forge.pki.privateKeyToPem(keys.privateKey) };
|
|
1822
2033
|
}
|
|
1823
|
-
|
|
2034
|
+
var SAN_TYPE_DNS = 2;
|
|
2035
|
+
var SAN_TYPE_IP = 7;
|
|
2036
|
+
function mintLeaf(ca, names, now) {
|
|
2037
|
+
const [commonName] = names;
|
|
2038
|
+
if (commonName === void 0) {
|
|
2039
|
+
throw new Error("a leaf certificate needs at least one name");
|
|
2040
|
+
}
|
|
1824
2041
|
const caCert = forge.pki.certificateFromPem(ca.certPem);
|
|
1825
2042
|
const caKey = forge.pki.privateKeyFromPem(ca.keyPem);
|
|
1826
2043
|
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
@@ -1829,13 +2046,13 @@ function mintLeaf(ca, host, now) {
|
|
|
1829
2046
|
cert.serialNumber = forge.util.bytesToHex(forge.random.getBytesSync(SERIAL_NUMBER_BYTES));
|
|
1830
2047
|
cert.validity.notBefore = new Date(now.getTime() - MS_PER_DAY);
|
|
1831
2048
|
cert.validity.notAfter = new Date(now.getTime() + LEAF_VALIDITY_DAYS * MS_PER_DAY);
|
|
1832
|
-
cert.setSubject([{ name: "commonName", value:
|
|
2049
|
+
cert.setSubject([{ name: "commonName", value: commonName }]);
|
|
1833
2050
|
cert.setIssuer(caCert.subject.attributes);
|
|
1834
2051
|
cert.setExtensions([
|
|
1835
2052
|
{ name: "basicConstraints", cA: false, critical: true },
|
|
1836
2053
|
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
|
|
1837
2054
|
{ name: "extKeyUsage", serverAuth: true },
|
|
1838
|
-
{ name: "subjectAltName", altNames:
|
|
2055
|
+
{ name: "subjectAltName", altNames: names.map((name) => net.isIP(name) === 0 ? { type: SAN_TYPE_DNS, value: name } : { type: SAN_TYPE_IP, ip: name }) },
|
|
1839
2056
|
{ name: "subjectKeyIdentifier" },
|
|
1840
2057
|
{ name: "authorityKeyIdentifier", keyLocatorFields: [] }
|
|
1841
2058
|
]);
|
|
@@ -1849,7 +2066,7 @@ function createLeafCache(ca, now) {
|
|
|
1849
2066
|
if (existing !== void 0) {
|
|
1850
2067
|
return existing;
|
|
1851
2068
|
}
|
|
1852
|
-
const minted = mintLeaf(ca, host, now());
|
|
2069
|
+
const minted = mintLeaf(ca, [host], now());
|
|
1853
2070
|
cache.set(host, minted);
|
|
1854
2071
|
return minted;
|
|
1855
2072
|
};
|
|
@@ -1872,17 +2089,10 @@ function caParses(ca) {
|
|
|
1872
2089
|
return false;
|
|
1873
2090
|
}
|
|
1874
2091
|
}
|
|
1875
|
-
async function
|
|
2092
|
+
async function startConnectServer(config, effects, preferredPort) {
|
|
1876
2093
|
const handler = (request3, response) => {
|
|
1877
|
-
if (
|
|
1878
|
-
|
|
1879
|
-
if (daemonPort === void 0) {
|
|
1880
|
-
const body = "claude-use headroom: the daemon is restarting";
|
|
1881
|
-
response.writeHead(HTTP_BAD_GATEWAY, { "content-type": "text/plain", "content-length": String(body.length) });
|
|
1882
|
-
response.end(body);
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
effects.forwardHttp({ host: "127.0.0.1", port: daemonPort, tls: false }, request3, response);
|
|
2094
|
+
if (servedByPipeline(request3.url)) {
|
|
2095
|
+
config.serveRouted(request3, response);
|
|
1886
2096
|
return;
|
|
1887
2097
|
}
|
|
1888
2098
|
effects.forwardHttp(config.upstream, request3, response);
|
|
@@ -1891,50 +2101,153 @@ async function startMitmServer(config, effects, preferredPort) {
|
|
|
1891
2101
|
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(config.interceptHost), (secure) => {
|
|
1892
2102
|
httpSession.serve(secure);
|
|
1893
2103
|
});
|
|
2104
|
+
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
1894
2105
|
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
1895
|
-
handleConnect(socket, config, effects, tlsAcceptor);
|
|
2106
|
+
handleConnect(socket, { config, effects, tlsAcceptor, ledger });
|
|
1896
2107
|
});
|
|
1897
2108
|
return {
|
|
1898
2109
|
port: listener.port,
|
|
1899
2110
|
close: async () => {
|
|
2111
|
+
ledger.close();
|
|
1900
2112
|
await listener.close();
|
|
1901
2113
|
tlsAcceptor.close();
|
|
1902
2114
|
httpSession.close();
|
|
1903
2115
|
}
|
|
1904
2116
|
};
|
|
1905
2117
|
}
|
|
1906
|
-
function
|
|
2118
|
+
function createConnectionLedger(limits, isLiveCapability) {
|
|
2119
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2120
|
+
const authenticated = /* @__PURE__ */ new Map();
|
|
2121
|
+
let tunnels = 0;
|
|
2122
|
+
const leavePending = (socket) => {
|
|
2123
|
+
const deadline = pending.get(socket);
|
|
2124
|
+
if (deadline !== void 0) {
|
|
2125
|
+
clearTimeout(deadline);
|
|
2126
|
+
pending.delete(socket);
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
const revalidation = setInterval(() => {
|
|
2130
|
+
for (const [capability, sockets] of authenticated) {
|
|
2131
|
+
if (!isLiveCapability(capability)) {
|
|
2132
|
+
for (const socket of sockets) {
|
|
2133
|
+
socket.destroy();
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}, limits.revalidateMs);
|
|
2138
|
+
revalidation.unref();
|
|
2139
|
+
return {
|
|
2140
|
+
accept: (socket) => {
|
|
2141
|
+
const [oldest] = pending.keys();
|
|
2142
|
+
if (pending.size >= limits.maxPendingHeads && oldest !== void 0) {
|
|
2143
|
+
leavePending(oldest);
|
|
2144
|
+
refuse(oldest, REFUSAL.requestTimeout);
|
|
2145
|
+
}
|
|
2146
|
+
pending.set(
|
|
2147
|
+
socket,
|
|
2148
|
+
setTimeout(() => {
|
|
2149
|
+
leavePending(socket);
|
|
2150
|
+
refuse(socket, REFUSAL.requestTimeout);
|
|
2151
|
+
}, limits.headDeadlineMs)
|
|
2152
|
+
);
|
|
2153
|
+
socket.once("close", () => {
|
|
2154
|
+
leavePending(socket);
|
|
2155
|
+
});
|
|
2156
|
+
},
|
|
2157
|
+
headReceived: leavePending,
|
|
2158
|
+
establish: (socket, capability) => {
|
|
2159
|
+
if (tunnels >= limits.maxTunnels) {
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
tunnels += 1;
|
|
2163
|
+
const sockets = authenticated.get(capability) ?? /* @__PURE__ */ new Set();
|
|
2164
|
+
sockets.add(socket);
|
|
2165
|
+
authenticated.set(capability, sockets);
|
|
2166
|
+
socket.once("close", () => {
|
|
2167
|
+
tunnels -= 1;
|
|
2168
|
+
sockets.delete(socket);
|
|
2169
|
+
if (sockets.size === 0 && authenticated.get(capability) === sockets) {
|
|
2170
|
+
authenticated.delete(capability);
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
return true;
|
|
2174
|
+
},
|
|
2175
|
+
close: () => {
|
|
2176
|
+
clearInterval(revalidation);
|
|
2177
|
+
for (const deadline of pending.values()) {
|
|
2178
|
+
clearTimeout(deadline);
|
|
2179
|
+
}
|
|
2180
|
+
pending.clear();
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
function refuse(socket, status, headers = []) {
|
|
2185
|
+
socket.removeAllListeners("data");
|
|
2186
|
+
socket.pause();
|
|
2187
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
socket.end(`HTTP/1.1 ${status}\r
|
|
2191
|
+
${headers.map((header) => `${header}\r
|
|
2192
|
+
`).join("")}content-length: 0\r
|
|
2193
|
+
connection: close\r
|
|
2194
|
+
\r
|
|
2195
|
+
`, () => {
|
|
2196
|
+
socket.destroy();
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
function handleConnect(socket, context) {
|
|
2200
|
+
socket.on("error", () => {
|
|
2201
|
+
socket.destroy();
|
|
2202
|
+
});
|
|
2203
|
+
context.ledger.accept(socket);
|
|
1907
2204
|
let buffer = Buffer.alloc(0);
|
|
1908
2205
|
const onData = (chunk) => {
|
|
1909
2206
|
buffer = Buffer.concat([buffer, chunk]);
|
|
1910
2207
|
const headEnd = buffer.indexOf(HEAD_TERMINATOR);
|
|
2208
|
+
if (headEnd === -1 ? buffer.length > MAX_CONNECT_HEAD_BYTES : headEnd > MAX_CONNECT_HEAD_BYTES) {
|
|
2209
|
+
context.ledger.headReceived(socket);
|
|
2210
|
+
refuse(socket, REFUSAL.headTooLarge);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
1911
2213
|
if (headEnd === -1) {
|
|
1912
|
-
if (buffer.length > MAX_CONNECT_HEAD_BYTES) {
|
|
1913
|
-
socket.removeListener("data", onData);
|
|
1914
|
-
socket.pause();
|
|
1915
|
-
socket.destroy();
|
|
1916
|
-
}
|
|
1917
2214
|
return;
|
|
1918
2215
|
}
|
|
1919
2216
|
socket.removeListener("data", onData);
|
|
1920
2217
|
socket.pause();
|
|
2218
|
+
context.ledger.headReceived(socket);
|
|
1921
2219
|
const head = buffer.subarray(0, headEnd).toString("utf8");
|
|
1922
2220
|
socket.unshift(buffer.subarray(headEnd + HEAD_TERMINATOR.length));
|
|
1923
|
-
routeConnect(socket, head,
|
|
2221
|
+
routeConnect(socket, head, context);
|
|
1924
2222
|
};
|
|
1925
2223
|
socket.on("data", onData);
|
|
1926
|
-
socket.on("error", () => {
|
|
1927
|
-
socket.destroy();
|
|
1928
|
-
});
|
|
1929
2224
|
}
|
|
1930
|
-
function
|
|
2225
|
+
function proxyAuthorizationOf(head) {
|
|
2226
|
+
const values = head.split("\r\n").slice(1).flatMap((line) => {
|
|
2227
|
+
const colon = line.indexOf(":");
|
|
2228
|
+
return colon > 0 && line.slice(0, colon).trim().toLowerCase() === "proxy-authorization" ? [line.slice(colon + 1)] : [];
|
|
2229
|
+
});
|
|
2230
|
+
const [value] = values;
|
|
2231
|
+
return values.length === 1 ? value : void 0;
|
|
2232
|
+
}
|
|
2233
|
+
function routeConnect(socket, head, context) {
|
|
2234
|
+
const { config, effects, tlsAcceptor, ledger } = context;
|
|
2235
|
+
const credential = proxyAuthorizationOf(head);
|
|
2236
|
+
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
2237
|
+
if (capability === void 0 || !config.isLiveCapability(capability)) {
|
|
2238
|
+
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
1931
2241
|
const requestLine = head.split("\r\n", 1)[0] ?? "";
|
|
1932
2242
|
const parts = requestLine.split(" ");
|
|
1933
2243
|
const authority = parts.length === CONNECT_LINE_TOKENS && parts[0]?.toUpperCase() === "CONNECT" ? parts[1] ?? "" : void 0;
|
|
1934
2244
|
const target = authority === void 0 ? void 0 : parseConnectTarget(authority);
|
|
1935
2245
|
if (target === void 0) {
|
|
1936
|
-
socket.
|
|
1937
|
-
|
|
2246
|
+
refuse(socket, REFUSAL.badRequest);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (!ledger.establish(socket, capability)) {
|
|
2250
|
+
refuse(socket, REFUSAL.serviceUnavailable);
|
|
1938
2251
|
return;
|
|
1939
2252
|
}
|
|
1940
2253
|
socket.write(CONNECT_ESTABLISHED);
|
|
@@ -1952,6 +2265,10 @@ async function blindTunnel(socket, target, effects) {
|
|
|
1952
2265
|
socket.destroy();
|
|
1953
2266
|
return;
|
|
1954
2267
|
}
|
|
2268
|
+
if (socket.destroyed) {
|
|
2269
|
+
upstream.destroy();
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
1955
2272
|
socket.pipe(upstream);
|
|
1956
2273
|
upstream.pipe(socket);
|
|
1957
2274
|
const drop = () => {
|
|
@@ -1967,7 +2284,7 @@ async function blindTunnel(socket, target, effects) {
|
|
|
1967
2284
|
socket.destroy();
|
|
1968
2285
|
});
|
|
1969
2286
|
}
|
|
1970
|
-
function
|
|
2287
|
+
function realConnectEffects() {
|
|
1971
2288
|
const plainAgent = new http.Agent({ keepAlive: true });
|
|
1972
2289
|
const tlsAgent = new https.Agent({ keepAlive: true });
|
|
1973
2290
|
const listenOnce = async (port, onSocket) => await new Promise((resolve, reject) => {
|
|
@@ -2096,7 +2413,7 @@ function realMitmEffects() {
|
|
|
2096
2413
|
function isEnoent(error) {
|
|
2097
2414
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2098
2415
|
}
|
|
2099
|
-
function
|
|
2416
|
+
function realConnectCertStore(paths) {
|
|
2100
2417
|
const read = (file) => {
|
|
2101
2418
|
try {
|
|
2102
2419
|
return fs.readFileSync(file, "utf8");
|
|
@@ -2109,54 +2426,49 @@ function realMitmCertStore(paths) {
|
|
|
2109
2426
|
};
|
|
2110
2427
|
return {
|
|
2111
2428
|
loadCa: () => {
|
|
2112
|
-
const certPem = read(paths.
|
|
2113
|
-
const keyPem = read(paths.
|
|
2429
|
+
const certPem = read(paths.frontdoorCaCertFile);
|
|
2430
|
+
const keyPem = read(paths.frontdoorCaKeyFile);
|
|
2114
2431
|
return certPem === void 0 || keyPem === void 0 ? void 0 : { certPem, keyPem };
|
|
2115
2432
|
},
|
|
2116
2433
|
writeCa: (ca) => {
|
|
2117
|
-
fs.mkdirSync(paths.
|
|
2118
|
-
fs.writeFileSync(paths.
|
|
2119
|
-
fs.writeFileSync(paths.
|
|
2120
|
-
fs.chmodSync(paths.
|
|
2434
|
+
fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
|
|
2435
|
+
fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
|
|
2436
|
+
fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
|
|
2437
|
+
fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
|
|
2121
2438
|
}
|
|
2122
2439
|
};
|
|
2123
2440
|
}
|
|
2124
2441
|
|
|
2442
|
+
// src/frontdoor/ensure.ts
|
|
2443
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2444
|
+
|
|
2125
2445
|
// src/headroom/state.ts
|
|
2126
2446
|
import { createHash } from "node:crypto";
|
|
2127
|
-
import
|
|
2128
|
-
import { z as
|
|
2447
|
+
import path6 from "node:path";
|
|
2448
|
+
import { z as z4 } from "zod";
|
|
2129
2449
|
var HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
|
|
2130
|
-
var HeadroomStateSchema =
|
|
2450
|
+
var HeadroomStateSchema = z4.strictObject({
|
|
2131
2451
|
/** The supervisor process owning the daemon. Alive means someone is keeping headroom running. */
|
|
2132
|
-
supervisorPid:
|
|
2452
|
+
supervisorPid: z4.number().int().positive().optional(),
|
|
2133
2453
|
/** The headroom proxy process itself. Absent while the supervisor is between restarts. */
|
|
2134
|
-
headroomPid:
|
|
2454
|
+
headroomPid: z4.number().int().positive().optional(),
|
|
2135
2455
|
/** 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. */
|
|
2136
|
-
port:
|
|
2137
|
-
/**
|
|
2138
|
-
* 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`.
|
|
2139
|
-
*/
|
|
2140
|
-
mitmPort: z3.number().int().positive().optional(),
|
|
2141
|
-
/**
|
|
2142
|
-
* 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.
|
|
2143
|
-
*/
|
|
2144
|
-
lastMitmPort: z3.number().int().positive().optional(),
|
|
2456
|
+
port: z4.number().int().positive().optional(),
|
|
2145
2457
|
/**
|
|
2146
|
-
* 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
|
|
2458
|
+
* 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.
|
|
2147
2459
|
*/
|
|
2148
|
-
lastPort:
|
|
2460
|
+
lastPort: z4.number().int().positive().optional(),
|
|
2149
2461
|
/** The `headroom --version` output of the running install. */
|
|
2150
|
-
version:
|
|
2462
|
+
version: z4.string().optional(),
|
|
2151
2463
|
/** Hash of the allowlist the running proxy was started with, so a provider-file change is detected as drift. */
|
|
2152
|
-
allowlistHash:
|
|
2464
|
+
allowlistHash: z4.string().optional(),
|
|
2153
2465
|
/** The install spec the running install was last installed from, so a `source` config change is detected as drift. */
|
|
2154
|
-
installedSource:
|
|
2466
|
+
installedSource: z4.string().optional(),
|
|
2155
2467
|
/** The last fatal error, kept across shutdowns so `headroom status` can explain a daemon that is not running. */
|
|
2156
|
-
lastError:
|
|
2468
|
+
lastError: z4.string().optional()
|
|
2157
2469
|
});
|
|
2158
|
-
var HeadroomSessionSchema =
|
|
2159
|
-
var HeadroomLockSchema =
|
|
2470
|
+
var HeadroomSessionSchema = z4.strictObject({ pid: z4.number().int().positive(), startedAt: z4.number() });
|
|
2471
|
+
var HeadroomLockSchema = z4.strictObject({ pid: z4.number().int().positive(), at: z4.number() });
|
|
2160
2472
|
function parseJson(raw, schema) {
|
|
2161
2473
|
if (raw === void 0 || raw.trim() === "") {
|
|
2162
2474
|
return void 0;
|
|
@@ -2172,7 +2484,7 @@ function readHeadroomState(fs2, stateFile) {
|
|
|
2172
2484
|
}
|
|
2173
2485
|
}
|
|
2174
2486
|
function writeHeadroomState(fs2, stateFile, state) {
|
|
2175
|
-
fs2.mkdirp(
|
|
2487
|
+
fs2.mkdirp(path6.dirname(stateFile));
|
|
2176
2488
|
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
2177
2489
|
`);
|
|
2178
2490
|
}
|
|
@@ -2184,16 +2496,8 @@ function headroomAllowlist(providers) {
|
|
|
2184
2496
|
return [...urls].sort();
|
|
2185
2497
|
}
|
|
2186
2498
|
function headroomUpstreams(providers, frontDoorOrigin2) {
|
|
2187
|
-
const upstreams =
|
|
2188
|
-
|
|
2189
|
-
for (const provider of providers) {
|
|
2190
|
-
if (isCodexProvider(provider)) {
|
|
2191
|
-
hasCodex = true;
|
|
2192
|
-
} else {
|
|
2193
|
-
upstreams.push({ baseUrl: provider.baseUrl });
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
if (hasCodex && frontDoorOrigin2 !== void 0) {
|
|
2499
|
+
const upstreams = providers.filter((provider) => !isCodexProvider(provider)).map((provider) => ({ baseUrl: provider.baseUrl }));
|
|
2500
|
+
if (providers.length > 0 && frontDoorOrigin2 !== void 0) {
|
|
2197
2501
|
upstreams.push({ baseUrl: frontDoorOrigin2 });
|
|
2198
2502
|
}
|
|
2199
2503
|
return upstreams;
|
|
@@ -2208,7 +2512,7 @@ function readAllProviders(fs2, providersDir) {
|
|
|
2208
2512
|
continue;
|
|
2209
2513
|
}
|
|
2210
2514
|
try {
|
|
2211
|
-
const parsed = parseJson(fs2.readFileUtf8(
|
|
2515
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(providersDir, name)), ProviderSchema);
|
|
2212
2516
|
if (parsed !== void 0) {
|
|
2213
2517
|
result.push({ name: name.slice(0, -".json".length), provider: parsed });
|
|
2214
2518
|
}
|
|
@@ -2220,11 +2524,11 @@ function readAllProviders(fs2, providersDir) {
|
|
|
2220
2524
|
}
|
|
2221
2525
|
function writeSession(fs2, sessionsDir, session) {
|
|
2222
2526
|
fs2.mkdirp(sessionsDir);
|
|
2223
|
-
fs2.writeFileUtf8(
|
|
2527
|
+
fs2.writeFileUtf8(path6.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
2224
2528
|
`);
|
|
2225
2529
|
}
|
|
2226
2530
|
function removeSession(fs2, sessionsDir, pid) {
|
|
2227
|
-
fs2.removeRecursive(
|
|
2531
|
+
fs2.removeRecursive(path6.join(sessionsDir, `${String(pid)}.json`));
|
|
2228
2532
|
}
|
|
2229
2533
|
function listSessions(fs2, sessionsDir) {
|
|
2230
2534
|
const sessions = [];
|
|
@@ -2233,7 +2537,7 @@ function listSessions(fs2, sessionsDir) {
|
|
|
2233
2537
|
continue;
|
|
2234
2538
|
}
|
|
2235
2539
|
try {
|
|
2236
|
-
const parsed = parseJson(fs2.readFileUtf8(
|
|
2540
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(sessionsDir, name)), HeadroomSessionSchema);
|
|
2237
2541
|
if (parsed !== void 0) {
|
|
2238
2542
|
sessions.push(parsed);
|
|
2239
2543
|
}
|
|
@@ -2261,6 +2565,67 @@ function readStartLock(fs2, lockFile) {
|
|
|
2261
2565
|
}
|
|
2262
2566
|
}
|
|
2263
2567
|
|
|
2568
|
+
// src/frontdoor/ensure.ts
|
|
2569
|
+
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
2570
|
+
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
2571
|
+
var FrontDoorStartError = class extends CliError {
|
|
2572
|
+
constructor(message) {
|
|
2573
|
+
super(message);
|
|
2574
|
+
this.name = "FrontDoorStartError";
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
function ensureFrontDoor(params) {
|
|
2578
|
+
const { paths, ports } = params;
|
|
2579
|
+
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
2580
|
+
let spawned = false;
|
|
2581
|
+
const distrusted = /* @__PURE__ */ new Set();
|
|
2582
|
+
let lastRejection;
|
|
2583
|
+
for (; ; ) {
|
|
2584
|
+
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
2585
|
+
const supervisorPid = state?.supervisorPid;
|
|
2586
|
+
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
2587
|
+
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
2588
|
+
const verdict = ports.verifyListener(state.port);
|
|
2589
|
+
if (verdict.ok) {
|
|
2590
|
+
const token = randomUUID2();
|
|
2591
|
+
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
2592
|
+
return { port: state.port, connectPort: state.connectPort, token };
|
|
2593
|
+
}
|
|
2594
|
+
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
2595
|
+
if (spawned) {
|
|
2596
|
+
throw new FrontDoorStartError(`claude-use: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
2597
|
+
}
|
|
2598
|
+
distrusted.add(supervisorPid);
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (state?.lastError !== void 0) {
|
|
2602
|
+
throw new FrontDoorStartError(`claude-use: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
2603
|
+
}
|
|
2604
|
+
} else if (!spawned) {
|
|
2605
|
+
ports.fs.mkdirp(paths.frontdoorDir);
|
|
2606
|
+
const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
2607
|
+
`);
|
|
2608
|
+
if (created) {
|
|
2609
|
+
ports.spawnSupervisor(paths);
|
|
2610
|
+
spawned = true;
|
|
2611
|
+
} else {
|
|
2612
|
+
const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
|
|
2613
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
2614
|
+
ports.fs.removeRecursive(paths.frontdoorLockFile);
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (ports.now() >= deadline) {
|
|
2620
|
+
const lastError = state?.lastError;
|
|
2621
|
+
throw new FrontDoorStartError(
|
|
2622
|
+
`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}`
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2264
2629
|
// src/headroom/ensure.ts
|
|
2265
2630
|
var HEADROOM_START_TIMEOUT_MS = 12e4;
|
|
2266
2631
|
var HEADROOM_LAUNCHER_POLL_MS = 250;
|
|
@@ -2277,10 +2642,10 @@ function ensureHeadroom(params) {
|
|
|
2277
2642
|
for (; ; ) {
|
|
2278
2643
|
const state = readHeadroomState(ports.fs, paths.headroomStateFile);
|
|
2279
2644
|
if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
|
|
2280
|
-
const daemonUp = state.port !== void 0 && state.
|
|
2281
|
-
if (daemonUp && state.port !== void 0
|
|
2645
|
+
const daemonUp = state.port !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
|
|
2646
|
+
if (daemonUp && state.port !== void 0) {
|
|
2282
2647
|
writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now() });
|
|
2283
|
-
return { port: state.port
|
|
2648
|
+
return { port: state.port };
|
|
2284
2649
|
}
|
|
2285
2650
|
if (state.lastError !== void 0) {
|
|
2286
2651
|
throw new HeadroomStartError(
|
|
@@ -2315,42 +2680,6 @@ function ensureHeadroom(params) {
|
|
|
2315
2680
|
}
|
|
2316
2681
|
}
|
|
2317
2682
|
|
|
2318
|
-
// src/frontdoor/state.ts
|
|
2319
|
-
import { z as z4 } from "zod";
|
|
2320
|
-
var FrontDoorStateSchema = z4.strictObject({
|
|
2321
|
-
/** The supervisor process serving the front door. Alive means the listener is up in that process. */
|
|
2322
|
-
supervisorPid: z4.number().int().positive().optional(),
|
|
2323
|
-
/** 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. */
|
|
2324
|
-
port: z4.number().int().positive().optional(),
|
|
2325
|
-
/**
|
|
2326
|
-
* 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.
|
|
2327
|
-
*/
|
|
2328
|
-
lastPort: z4.number().int().positive().optional(),
|
|
2329
|
-
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
2330
|
-
lastError: z4.string().optional()
|
|
2331
|
-
});
|
|
2332
|
-
function readFrontDoorState(fs2, stateFile) {
|
|
2333
|
-
let raw;
|
|
2334
|
-
try {
|
|
2335
|
-
raw = fs2.readFileUtf8(stateFile);
|
|
2336
|
-
} catch {
|
|
2337
|
-
return void 0;
|
|
2338
|
-
}
|
|
2339
|
-
if (raw === void 0 || raw.trim() === "") {
|
|
2340
|
-
return void 0;
|
|
2341
|
-
}
|
|
2342
|
-
try {
|
|
2343
|
-
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
2344
|
-
return parsed.success ? parsed.data : void 0;
|
|
2345
|
-
} catch {
|
|
2346
|
-
return void 0;
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
function frontDoorOrigin(state) {
|
|
2350
|
-
const port = state?.port ?? state?.lastPort;
|
|
2351
|
-
return port === void 0 ? void 0 : `http://127.0.0.1:${String(port)}`;
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
2683
|
// src/headroom/supervisor.ts
|
|
2355
2684
|
function resolveSupervisorConfig(configured) {
|
|
2356
2685
|
return {
|
|
@@ -2363,7 +2692,7 @@ var HEADROOM_BACKOFF_CAP_MS = 3e4;
|
|
|
2363
2692
|
var HEADROOM_START_RETRY_BUDGET = 5;
|
|
2364
2693
|
var HEADROOM_READY_TIMEOUT_MS = 15e3;
|
|
2365
2694
|
var HEADROOM_POLL_MS = 1e3;
|
|
2366
|
-
var
|
|
2695
|
+
var MS_PER_MINUTE2 = 6e4;
|
|
2367
2696
|
function backoffForAttempt(attempt) {
|
|
2368
2697
|
return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
|
|
2369
2698
|
}
|
|
@@ -2423,13 +2752,7 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2423
2752
|
const previousState = readHeadroomState(fs2, paths.headroomStateFile);
|
|
2424
2753
|
let installedSource = previousState?.installedSource;
|
|
2425
2754
|
let lastPort = previousState?.lastPort;
|
|
2426
|
-
|
|
2427
|
-
let daemonPort;
|
|
2428
|
-
const sticky = () => ({
|
|
2429
|
-
...lastPort === void 0 ? {} : { lastPort },
|
|
2430
|
-
...lastMitmPort === void 0 ? {} : { lastMitmPort }
|
|
2431
|
-
});
|
|
2432
|
-
let mitm;
|
|
2755
|
+
const sticky = () => lastPort === void 0 ? {} : { lastPort };
|
|
2433
2756
|
const fail = (message) => {
|
|
2434
2757
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2435
2758
|
lastError: message,
|
|
@@ -2458,18 +2781,7 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2458
2781
|
return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
|
|
2459
2782
|
}
|
|
2460
2783
|
const version = ports.headroomVersion() ?? "unknown";
|
|
2461
|
-
|
|
2462
|
-
mitm = await ports.startMitm(lastMitmPort, () => daemonPort);
|
|
2463
|
-
} catch (error) {
|
|
2464
|
-
return fail(`could not start the MITM proxy: ${error instanceof Error ? error.message : String(error)}`);
|
|
2465
|
-
}
|
|
2466
|
-
lastMitmPort = mitm.port;
|
|
2467
|
-
if (previousState?.lastMitmPort !== void 0 && mitm.port !== previousState.lastMitmPort) {
|
|
2468
|
-
ports.log(
|
|
2469
|
-
`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)`
|
|
2470
|
-
);
|
|
2471
|
-
}
|
|
2472
|
-
writeHeadroomState(fs2, paths.headroomStateFile, { supervisorPid: ports.ownPid, version, installedSource, mitmPort: mitm.port, ...sticky() });
|
|
2784
|
+
writeHeadroomState(fs2, paths.headroomStateFile, { supervisorPid: ports.ownPid, version, installedSource, ...sticky() });
|
|
2473
2785
|
ports.log(`claude-use headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
|
|
2474
2786
|
const orphanPid = previousState?.headroomPid;
|
|
2475
2787
|
if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
|
|
@@ -2494,13 +2806,11 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2494
2806
|
if (crashed) {
|
|
2495
2807
|
ports.log(`claude-use headroom supervisor: headroom pid ${String(headroomPid)} died`);
|
|
2496
2808
|
headroomPid = void 0;
|
|
2497
|
-
daemonPort = void 0;
|
|
2498
2809
|
runningHash = void 0;
|
|
2499
2810
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2500
2811
|
supervisorPid: ports.ownPid,
|
|
2501
2812
|
version,
|
|
2502
2813
|
installedSource,
|
|
2503
|
-
mitmPort: mitm.port,
|
|
2504
2814
|
...sticky()
|
|
2505
2815
|
});
|
|
2506
2816
|
}
|
|
@@ -2517,7 +2827,6 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2517
2827
|
if (ready) {
|
|
2518
2828
|
consecutiveFailures = 0;
|
|
2519
2829
|
headroomPid = pid;
|
|
2520
|
-
daemonPort = port;
|
|
2521
2830
|
runningHash = allowlistHash;
|
|
2522
2831
|
lastPort = port;
|
|
2523
2832
|
if (previousSticky !== void 0 && port !== previousSticky) {
|
|
@@ -2529,7 +2838,6 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2529
2838
|
supervisorPid: ports.ownPid,
|
|
2530
2839
|
headroomPid: pid,
|
|
2531
2840
|
port,
|
|
2532
|
-
mitmPort: mitm.port,
|
|
2533
2841
|
version,
|
|
2534
2842
|
allowlistHash,
|
|
2535
2843
|
installedSource: config.source,
|
|
@@ -2553,7 +2861,6 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2553
2861
|
ports.log("claude-use headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
|
|
2554
2862
|
ports.stopProcess(headroomPid);
|
|
2555
2863
|
headroomPid = void 0;
|
|
2556
|
-
daemonPort = void 0;
|
|
2557
2864
|
runningHash = void 0;
|
|
2558
2865
|
continue;
|
|
2559
2866
|
}
|
|
@@ -2562,13 +2869,11 @@ async function runSupervisor(config, ports, options = {}) {
|
|
|
2562
2869
|
if (sessions.length === 0) {
|
|
2563
2870
|
idleSince ??= ports.now();
|
|
2564
2871
|
const idleMs = ports.now() - idleSince;
|
|
2565
|
-
if (idleMs >= config.idleShutdownMinutes *
|
|
2872
|
+
if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
|
|
2566
2873
|
ports.log(
|
|
2567
|
-
`claude-use headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and
|
|
2874
|
+
`claude-use headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
|
|
2568
2875
|
);
|
|
2569
2876
|
ports.stopProcess(headroomPid);
|
|
2570
|
-
daemonPort = void 0;
|
|
2571
|
-
await mitm.close();
|
|
2572
2877
|
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2573
2878
|
version,
|
|
2574
2879
|
...sticky()
|
|
@@ -2650,15 +2955,18 @@ function evaluateAmbientCredentialGuard(params) {
|
|
|
2650
2955
|
}
|
|
2651
2956
|
export {
|
|
2652
2957
|
AMBIENT_CREDENTIAL_VARS,
|
|
2958
|
+
CONNECT_INTERCEPT_HOST,
|
|
2959
|
+
CONNECT_LIMITS,
|
|
2653
2960
|
FARM_MANIFEST_FILENAME,
|
|
2654
|
-
|
|
2961
|
+
FrontDoorStartError,
|
|
2655
2962
|
HeadroomStartError,
|
|
2656
|
-
|
|
2963
|
+
ROUTED_PATH_PREFIX,
|
|
2657
2964
|
buildEntryFacts,
|
|
2658
2965
|
carryOver,
|
|
2659
2966
|
createLeafCache,
|
|
2660
2967
|
detectAmbientCredential,
|
|
2661
2968
|
ensureCa,
|
|
2969
|
+
ensureFrontDoor,
|
|
2662
2970
|
ensureHeadroom,
|
|
2663
2971
|
evaluateAmbientCredentialGuard,
|
|
2664
2972
|
formatAmbientCredentialGuardMessage,
|
|
@@ -2668,15 +2976,16 @@ export {
|
|
|
2668
2976
|
mintLeaf,
|
|
2669
2977
|
parseConnectTarget,
|
|
2670
2978
|
readFarmManifest,
|
|
2671
|
-
|
|
2672
|
-
|
|
2979
|
+
realConnectCertStore,
|
|
2980
|
+
realConnectEffects,
|
|
2673
2981
|
recoverFarm,
|
|
2674
2982
|
recoveryDiagnostics,
|
|
2675
2983
|
resolveDecisions,
|
|
2676
2984
|
resolveSupervisorConfig,
|
|
2677
2985
|
resyncFarm,
|
|
2986
|
+
runFrontDoorSupervisor,
|
|
2678
2987
|
runSupervisor,
|
|
2679
|
-
|
|
2680
|
-
|
|
2988
|
+
servedByPipeline,
|
|
2989
|
+
startConnectServer,
|
|
2681
2990
|
topLevelNames
|
|
2682
2991
|
};
|