agent-shim 8.20.2 → 8.21.1
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 -0
- package/dist/cli.cjs +1126 -421
- package/dist/index.cjs +386 -27
- package/dist/index.mjs +378 -27
- package/dist/types/index.d.ts +92 -10
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2719,22 +2719,22 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
2719
2719
|
};
|
|
2720
2720
|
writeFrontDoorState(fs12, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
2721
2721
|
const previousSticky = lastPort;
|
|
2722
|
-
let
|
|
2722
|
+
let http3;
|
|
2723
2723
|
try {
|
|
2724
|
-
|
|
2724
|
+
http3 = await ports.startProviderListener(lastPort);
|
|
2725
2725
|
} catch (error) {
|
|
2726
2726
|
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
2727
2727
|
}
|
|
2728
|
-
if (previousSticky !== void 0 &&
|
|
2729
|
-
ports.log(`agent-shim frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(
|
|
2728
|
+
if (previousSticky !== void 0 && http3.port !== previousSticky) {
|
|
2729
|
+
ports.log(`agent-shim frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http3.port)} (sessions launched against the old port are stale until they relaunch)`);
|
|
2730
2730
|
}
|
|
2731
|
-
lastPort =
|
|
2731
|
+
lastPort = http3.port;
|
|
2732
2732
|
const previousStickyConnect = lastConnectPort;
|
|
2733
2733
|
let connect3;
|
|
2734
2734
|
try {
|
|
2735
2735
|
connect3 = await ports.startConnectListener(lastConnectPort);
|
|
2736
2736
|
} catch (error) {
|
|
2737
|
-
await
|
|
2737
|
+
await http3.close();
|
|
2738
2738
|
return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
2739
2739
|
}
|
|
2740
2740
|
if (previousStickyConnect !== void 0 && connect3.port !== previousStickyConnect) {
|
|
@@ -2747,16 +2747,16 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
2747
2747
|
direct = await ports.startDirectListener(lastDirectPort);
|
|
2748
2748
|
} catch (error) {
|
|
2749
2749
|
await connect3.close();
|
|
2750
|
-
await
|
|
2750
|
+
await http3.close();
|
|
2751
2751
|
return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
2752
2752
|
}
|
|
2753
2753
|
if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
|
|
2754
2754
|
ports.log(`agent-shim frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
2755
2755
|
}
|
|
2756
2756
|
lastDirectPort = direct.port;
|
|
2757
|
-
writeFrontDoorState(fs12, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port:
|
|
2757
|
+
writeFrontDoorState(fs12, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http3.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
|
|
2758
2758
|
fs12.removeRecursive(paths.frontdoorLockFile);
|
|
2759
|
-
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(
|
|
2759
|
+
ports.log(`agent-shim frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http3.port)}, CONNECT surface on 127.0.0.1:${String(connect3.port)}`);
|
|
2760
2760
|
let idleSince;
|
|
2761
2761
|
let ticks = 0;
|
|
2762
2762
|
for (; ; ) {
|
|
@@ -2774,7 +2774,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
|
|
|
2774
2774
|
ports.log(`agent-shim frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
|
|
2775
2775
|
await direct.close();
|
|
2776
2776
|
await connect3.close();
|
|
2777
|
-
await
|
|
2777
|
+
await http3.close();
|
|
2778
2778
|
ports.closeRcStreamHub();
|
|
2779
2779
|
writeFrontDoorState(fs12, paths.frontdoorStateFile, sticky());
|
|
2780
2780
|
return 0;
|
|
@@ -9988,6 +9988,252 @@ function createRcStreamHub(deps) {
|
|
|
9988
9988
|
};
|
|
9989
9989
|
}
|
|
9990
9990
|
|
|
9991
|
+
// src/frontdoor/rcWebFetch.ts
|
|
9992
|
+
import * as dns2 from "node:dns";
|
|
9993
|
+
import * as http2 from "node:http";
|
|
9994
|
+
import * as https3 from "node:https";
|
|
9995
|
+
import * as net3 from "node:net";
|
|
9996
|
+
var RC_WEB_FETCH_MAX_BYTES = 12582912;
|
|
9997
|
+
var RC_WEB_FETCH_DEADLINE_MS = 4e4;
|
|
9998
|
+
var RC_WEB_FETCH_MAX_REDIRECTS = 20;
|
|
9999
|
+
var RC_WEB_FETCH_ALLOW_PRIVATE_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST_WEB_FETCH_ALLOW_PRIVATE";
|
|
10000
|
+
function rcWebFetchAllowsPrivateFromEnv(env) {
|
|
10001
|
+
return env[RC_WEB_FETCH_ALLOW_PRIVATE_ENV] === "1";
|
|
10002
|
+
}
|
|
10003
|
+
var WEB_FETCH_USER_AGENT = "agent-shim-frontdoor";
|
|
10004
|
+
var ERROR_PREFIX = "web_fetch_";
|
|
10005
|
+
var V4_THIS_NETWORK_OCTET = 0;
|
|
10006
|
+
var V4_PRIVATE_A_OCTET = 10;
|
|
10007
|
+
var V4_LOOPBACK_OCTET = 127;
|
|
10008
|
+
var V4_CGNAT_OCTET = 100;
|
|
10009
|
+
var V4_CGNAT_SECOND_FROM = 64;
|
|
10010
|
+
var V4_CGNAT_SECOND_TO = 127;
|
|
10011
|
+
var V4_LINK_LOCAL_OCTET = 169;
|
|
10012
|
+
var V4_LINK_LOCAL_SECOND = 254;
|
|
10013
|
+
var V4_PRIVATE_B_OCTET = 172;
|
|
10014
|
+
var V4_PRIVATE_B_SECOND_FROM = 16;
|
|
10015
|
+
var V4_PRIVATE_B_SECOND_TO = 31;
|
|
10016
|
+
var V4_PRIVATE_C_OCTET = 192;
|
|
10017
|
+
var V4_PRIVATE_C_SECOND = 168;
|
|
10018
|
+
var V4_MULTICAST_FROM_OCTET = 224;
|
|
10019
|
+
var V6_UNIQUE_LOCAL_FROM = 64512;
|
|
10020
|
+
var V6_UNIQUE_LOCAL_TO = 65023;
|
|
10021
|
+
var V6_LINK_LOCAL_FROM = 65152;
|
|
10022
|
+
var V6_LINK_LOCAL_TO = 65215;
|
|
10023
|
+
var V6_MULTICAST_FROM = 65280;
|
|
10024
|
+
var IP_VERSION_4 = 4;
|
|
10025
|
+
var IP_VERSION_6 = 6;
|
|
10026
|
+
var HTTP_MOVED_PERMANENTLY = 301;
|
|
10027
|
+
var HTTP_FOUND = 302;
|
|
10028
|
+
var HTTP_SEE_OTHER = 303;
|
|
10029
|
+
var HTTP_TEMPORARY_REDIRECT = 307;
|
|
10030
|
+
var HTTP_PERMANENT_REDIRECT = 308;
|
|
10031
|
+
var SUCCESS_STATUS_MAX_EXCLUSIVE4 = 300;
|
|
10032
|
+
function refused(errorType, errorMessage2) {
|
|
10033
|
+
return { kind: "refused", errorType: `${ERROR_PREFIX}${errorType}`, errorMessage: errorMessage2 };
|
|
10034
|
+
}
|
|
10035
|
+
function isPrivateAddress(address) {
|
|
10036
|
+
const version = net3.isIP(address);
|
|
10037
|
+
if (version === IP_VERSION_4) {
|
|
10038
|
+
const octets = address.split(".").map((part) => Number(part));
|
|
10039
|
+
const first = octets[0] ?? -1;
|
|
10040
|
+
const second = octets[1] ?? -1;
|
|
10041
|
+
if (first === V4_THIS_NETWORK_OCTET || first === V4_PRIVATE_A_OCTET || first === V4_LOOPBACK_OCTET) {
|
|
10042
|
+
return true;
|
|
10043
|
+
}
|
|
10044
|
+
if (first === V4_CGNAT_OCTET && second >= V4_CGNAT_SECOND_FROM && second <= V4_CGNAT_SECOND_TO) {
|
|
10045
|
+
return true;
|
|
10046
|
+
}
|
|
10047
|
+
if (first === V4_LINK_LOCAL_OCTET && second === V4_LINK_LOCAL_SECOND) {
|
|
10048
|
+
return true;
|
|
10049
|
+
}
|
|
10050
|
+
if (first === V4_PRIVATE_B_OCTET && second >= V4_PRIVATE_B_SECOND_FROM && second <= V4_PRIVATE_B_SECOND_TO) {
|
|
10051
|
+
return true;
|
|
10052
|
+
}
|
|
10053
|
+
if (first === V4_PRIVATE_C_OCTET && second === V4_PRIVATE_C_SECOND) {
|
|
10054
|
+
return true;
|
|
10055
|
+
}
|
|
10056
|
+
return first >= V4_MULTICAST_FROM_OCTET;
|
|
10057
|
+
}
|
|
10058
|
+
if (version === IP_VERSION_6) {
|
|
10059
|
+
const lower = address.toLowerCase();
|
|
10060
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
|
|
10061
|
+
if (mapped !== null) {
|
|
10062
|
+
return isPrivateAddress(mapped[1] ?? "");
|
|
10063
|
+
}
|
|
10064
|
+
if (lower === "::" || lower === "::1") {
|
|
10065
|
+
return true;
|
|
10066
|
+
}
|
|
10067
|
+
const firstGroup = Number.parseInt(/^([0-9a-f]+):/.exec(lower)?.[1] ?? "0", 16);
|
|
10068
|
+
if (Number.isNaN(firstGroup)) {
|
|
10069
|
+
return true;
|
|
10070
|
+
}
|
|
10071
|
+
if (firstGroup >= V6_UNIQUE_LOCAL_FROM && firstGroup <= V6_UNIQUE_LOCAL_TO) {
|
|
10072
|
+
return true;
|
|
10073
|
+
}
|
|
10074
|
+
if (firstGroup >= V6_LINK_LOCAL_FROM && firstGroup <= V6_LINK_LOCAL_TO) {
|
|
10075
|
+
return true;
|
|
10076
|
+
}
|
|
10077
|
+
return firstGroup >= V6_MULTICAST_FROM;
|
|
10078
|
+
}
|
|
10079
|
+
return true;
|
|
10080
|
+
}
|
|
10081
|
+
function isSuccessful4(status) {
|
|
10082
|
+
return status >= HTTP_STATUS.ok && status < SUCCESS_STATUS_MAX_EXCLUSIVE4;
|
|
10083
|
+
}
|
|
10084
|
+
function isRedirect(status) {
|
|
10085
|
+
return status === HTTP_MOVED_PERMANENTLY || status === HTTP_FOUND || status === HTTP_SEE_OTHER || status === HTTP_TEMPORARY_REDIRECT || status === HTTP_PERMANENT_REDIRECT;
|
|
10086
|
+
}
|
|
10087
|
+
function readableContentType(contentType) {
|
|
10088
|
+
if (contentType === void 0 || contentType.trim() === "") {
|
|
10089
|
+
return true;
|
|
10090
|
+
}
|
|
10091
|
+
const normalised = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
10092
|
+
const slash = normalised.indexOf("/");
|
|
10093
|
+
const main = slash === -1 ? normalised : normalised.slice(0, slash);
|
|
10094
|
+
const sub = slash === -1 ? "" : normalised.slice(slash + 1);
|
|
10095
|
+
if (main === "text") {
|
|
10096
|
+
return true;
|
|
10097
|
+
}
|
|
10098
|
+
return /json|xml|javascript|ecmascript|yaml|csv/.test(sub);
|
|
10099
|
+
}
|
|
10100
|
+
async function readCapped(chunks) {
|
|
10101
|
+
const collected = [];
|
|
10102
|
+
let total = 0;
|
|
10103
|
+
for await (const chunk of chunks) {
|
|
10104
|
+
total += chunk.byteLength;
|
|
10105
|
+
if (total > RC_WEB_FETCH_MAX_BYTES) {
|
|
10106
|
+
return { tooLarge: true, text: "" };
|
|
10107
|
+
}
|
|
10108
|
+
collected.push(Buffer.from(chunk));
|
|
10109
|
+
}
|
|
10110
|
+
return { tooLarge: false, text: Buffer.concat(collected).toString("utf8") };
|
|
10111
|
+
}
|
|
10112
|
+
async function checkTarget(url, deps) {
|
|
10113
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
10114
|
+
return refused("scheme", `the web-fetch proxy dials http and https only, not ${url.protocol}`);
|
|
10115
|
+
}
|
|
10116
|
+
if (deps.allowPrivate) {
|
|
10117
|
+
return void 0;
|
|
10118
|
+
}
|
|
10119
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
10120
|
+
let addresses;
|
|
10121
|
+
try {
|
|
10122
|
+
addresses = net3.isIP(hostname) === 0 ? await deps.lookup(hostname) : [hostname];
|
|
10123
|
+
} catch {
|
|
10124
|
+
return refused("resolve", `${hostname} did not resolve`);
|
|
10125
|
+
}
|
|
10126
|
+
for (const address of addresses) {
|
|
10127
|
+
if (isPrivateAddress(address)) {
|
|
10128
|
+
return refused("private_address", `${hostname} resolves to ${address}, a private address this web-fetch proxy refuses; set ${RC_WEB_FETCH_ALLOW_PRIVATE_ENV}=1 on the door if serving internal targets is intended`);
|
|
10129
|
+
}
|
|
10130
|
+
}
|
|
10131
|
+
return void 0;
|
|
10132
|
+
}
|
|
10133
|
+
async function fetchThroughWebProxy(url, deps) {
|
|
10134
|
+
let current;
|
|
10135
|
+
try {
|
|
10136
|
+
current = new URL(url);
|
|
10137
|
+
} catch {
|
|
10138
|
+
return refused("malformed_url", `${url} is not a URL this proxy can dial`);
|
|
10139
|
+
}
|
|
10140
|
+
const controller = new AbortController();
|
|
10141
|
+
const timer = setTimeout(() => {
|
|
10142
|
+
controller.abort();
|
|
10143
|
+
}, deps.deadlineMs);
|
|
10144
|
+
timer.unref();
|
|
10145
|
+
try {
|
|
10146
|
+
let redirects = 0;
|
|
10147
|
+
for (; ; ) {
|
|
10148
|
+
const refusal = await checkTarget(current, deps);
|
|
10149
|
+
if (refusal !== void 0) {
|
|
10150
|
+
return refusal;
|
|
10151
|
+
}
|
|
10152
|
+
let hop;
|
|
10153
|
+
try {
|
|
10154
|
+
hop = await deps.dial(current, controller.signal);
|
|
10155
|
+
} catch (error) {
|
|
10156
|
+
if (controller.signal.aborted) {
|
|
10157
|
+
return refused("deadline", `the fetch of ${url} did not settle within its deadline`);
|
|
10158
|
+
}
|
|
10159
|
+
return refused("transport", `the dial of ${current.toString()} failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
10160
|
+
}
|
|
10161
|
+
if (isRedirect(hop.status) && hop.location !== void 0) {
|
|
10162
|
+
if (redirects >= RC_WEB_FETCH_MAX_REDIRECTS) {
|
|
10163
|
+
controller.abort();
|
|
10164
|
+
return refused("too_many_redirects", `${url} followed more than ${String(RC_WEB_FETCH_MAX_REDIRECTS)} redirects without settling`);
|
|
10165
|
+
}
|
|
10166
|
+
let next;
|
|
10167
|
+
try {
|
|
10168
|
+
next = new URL(hop.location, current);
|
|
10169
|
+
} catch {
|
|
10170
|
+
controller.abort();
|
|
10171
|
+
return refused("malformed_url", `a redirect of ${current.toString()} named ${hop.location}, which is not a URL this proxy can dial`);
|
|
10172
|
+
}
|
|
10173
|
+
const drained = await readCapped(hop.chunks);
|
|
10174
|
+
if (drained.tooLarge) {
|
|
10175
|
+
controller.abort();
|
|
10176
|
+
return refused("too_large", `the body of a redirect from ${url} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads`);
|
|
10177
|
+
}
|
|
10178
|
+
redirects += 1;
|
|
10179
|
+
current = next;
|
|
10180
|
+
continue;
|
|
10181
|
+
}
|
|
10182
|
+
if (!isSuccessful4(hop.status)) {
|
|
10183
|
+
controller.abort();
|
|
10184
|
+
return refused("status", `the target answered HTTP ${String(hop.status)} for ${current.toString()}`);
|
|
10185
|
+
}
|
|
10186
|
+
const contentType = hop.contentType ?? "";
|
|
10187
|
+
if (!readableContentType(contentType)) {
|
|
10188
|
+
controller.abort();
|
|
10189
|
+
return refused("content_type", `the target at ${current.toString()} served a ${contentType} answer, which the proxy's text field cannot carry`);
|
|
10190
|
+
}
|
|
10191
|
+
const read = await readCapped(hop.chunks);
|
|
10192
|
+
if (read.tooLarge) {
|
|
10193
|
+
controller.abort();
|
|
10194
|
+
return refused("too_large", `the body of ${current.toString()} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads`);
|
|
10195
|
+
}
|
|
10196
|
+
return { kind: "fetched", url, destinationUrl: current.toString(), contentType: hop.contentType, text: read.text };
|
|
10197
|
+
}
|
|
10198
|
+
} finally {
|
|
10199
|
+
clearTimeout(timer);
|
|
10200
|
+
}
|
|
10201
|
+
}
|
|
10202
|
+
function realRcWebFetch(options) {
|
|
10203
|
+
const plainAgent = new ExemptHttpAgent();
|
|
10204
|
+
const tlsAgent = new ExemptTlsAgent();
|
|
10205
|
+
const deps = {
|
|
10206
|
+
dial: async (url, signal) => await new Promise((resolve, reject) => {
|
|
10207
|
+
const request4 = (url.protocol === "https:" ? https3 : http2).request(
|
|
10208
|
+
{
|
|
10209
|
+
hostname: url.hostname,
|
|
10210
|
+
port: url.port === "" ? void 0 : Number(url.port),
|
|
10211
|
+
method: "GET",
|
|
10212
|
+
path: `${url.pathname}${url.search}`,
|
|
10213
|
+
headers: { accept: "*/*", "accept-encoding": "identity", "user-agent": WEB_FETCH_USER_AGENT },
|
|
10214
|
+
agent: url.protocol === "https:" ? tlsAgent : plainAgent,
|
|
10215
|
+
signal
|
|
10216
|
+
},
|
|
10217
|
+
(response) => {
|
|
10218
|
+
const location = response.headers.location;
|
|
10219
|
+
resolve({
|
|
10220
|
+
status: response.statusCode ?? 0,
|
|
10221
|
+
location: typeof location === "string" ? location : void 0,
|
|
10222
|
+
contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : void 0,
|
|
10223
|
+
chunks: response
|
|
10224
|
+
});
|
|
10225
|
+
}
|
|
10226
|
+
);
|
|
10227
|
+
request4.once("error", reject);
|
|
10228
|
+
request4.end();
|
|
10229
|
+
}),
|
|
10230
|
+
lookup: async (hostname) => (await dns2.promises.lookup(hostname, { all: true })).map((entry) => entry.address),
|
|
10231
|
+
allowPrivate: options.allowPrivate,
|
|
10232
|
+
deadlineMs: RC_WEB_FETCH_DEADLINE_MS
|
|
10233
|
+
};
|
|
10234
|
+
return async (url) => await fetchThroughWebProxy(url, deps);
|
|
10235
|
+
}
|
|
10236
|
+
|
|
9991
10237
|
// src/frontdoor/rcSelfHost.ts
|
|
9992
10238
|
var RC_SELF_HOST_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST";
|
|
9993
10239
|
function rcSelfHostFromEnv(env) {
|
|
@@ -10017,6 +10263,9 @@ var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
|
|
|
10017
10263
|
function isRecord6(value) {
|
|
10018
10264
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10019
10265
|
}
|
|
10266
|
+
function isWebSearchRefusal(value) {
|
|
10267
|
+
return !Array.isArray(value);
|
|
10268
|
+
}
|
|
10020
10269
|
function nonEmptyString2(record, field) {
|
|
10021
10270
|
const value = record === void 0 ? void 0 : record[field];
|
|
10022
10271
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
@@ -10031,6 +10280,20 @@ function numberOf(record, field) {
|
|
|
10031
10280
|
}
|
|
10032
10281
|
return void 0;
|
|
10033
10282
|
}
|
|
10283
|
+
function stringArray(record, field) {
|
|
10284
|
+
const value = record === void 0 ? void 0 : record[field];
|
|
10285
|
+
if (!Array.isArray(value)) {
|
|
10286
|
+
return void 0;
|
|
10287
|
+
}
|
|
10288
|
+
const members = [];
|
|
10289
|
+
for (const member of value) {
|
|
10290
|
+
if (typeof member !== "string") {
|
|
10291
|
+
return void 0;
|
|
10292
|
+
}
|
|
10293
|
+
members.push(member);
|
|
10294
|
+
}
|
|
10295
|
+
return members;
|
|
10296
|
+
}
|
|
10034
10297
|
function singleHeader2(headers, name) {
|
|
10035
10298
|
const value = headers[name];
|
|
10036
10299
|
return typeof value === "string" ? value : void 0;
|
|
@@ -10042,6 +10305,17 @@ function bearerOf(headers) {
|
|
|
10042
10305
|
}
|
|
10043
10306
|
return presented.slice(0, "bearer ".length).toLowerCase() === "bearer " ? presented.slice("bearer ".length) : void 0;
|
|
10044
10307
|
}
|
|
10308
|
+
function presentationRefusal(headers) {
|
|
10309
|
+
const presented = singleHeader2(headers, "authorization");
|
|
10310
|
+
if (presented === void 0) {
|
|
10311
|
+
return "absent";
|
|
10312
|
+
}
|
|
10313
|
+
if (presented.slice(0, "bearer ".length).toLowerCase() !== "bearer ") {
|
|
10314
|
+
return "not-a-bearer";
|
|
10315
|
+
}
|
|
10316
|
+
const bearer = presented.slice("bearer ".length);
|
|
10317
|
+
return bearer.slice(0, "bearer ".length).toLowerCase() === "bearer " ? "doubled-scheme-word" : "unrecognised";
|
|
10318
|
+
}
|
|
10045
10319
|
async function readBody2(request4, capBytes) {
|
|
10046
10320
|
return await new Promise((resolve, reject) => {
|
|
10047
10321
|
const chunks = [];
|
|
@@ -10130,6 +10404,9 @@ function createRcSelfHostSurface(deps) {
|
|
|
10130
10404
|
}
|
|
10131
10405
|
return false;
|
|
10132
10406
|
};
|
|
10407
|
+
const credentialRefusal = (headers) => presentsCredential(headers) ? void 0 : presentationRefusal(headers);
|
|
10408
|
+
const workerRefusal = (headers, session) => presentsWorkerJwt(headers, session) ? void 0 : presentationRefusal(headers);
|
|
10409
|
+
const workerOrCredentialRefusal = (headers, session) => presentsWorkerJwt(headers, session) || presentsCredential(headers) ? void 0 : presentationRefusal(headers);
|
|
10133
10410
|
const frameOf = (event) => {
|
|
10134
10411
|
const envelope = {
|
|
10135
10412
|
event_id: event.eventId,
|
|
@@ -10218,8 +10495,14 @@ data: ${JSON.stringify(envelope)}
|
|
|
10218
10495
|
const epochStale = async (response) => {
|
|
10219
10496
|
await answerJson2(response, HTTP_CONFLICT, { type: "error", error: { type: "api_error", message: "worker_epoch does not match the session's live epoch" } }, { [CONFLICT_REASON_HEADER2]: CONFLICT_REASON_EPOCH_STALE });
|
|
10220
10497
|
};
|
|
10221
|
-
const
|
|
10222
|
-
|
|
10498
|
+
const refusalMessages = {
|
|
10499
|
+
absent: "the self-hosted Remote Control surface authenticates by the Authorization header, and this call carried none",
|
|
10500
|
+
"not-a-bearer": "the self-hosted Remote Control surface reads a Bearer authorization, and this call's Authorization header did not carry the Bearer scheme word",
|
|
10501
|
+
"doubled-scheme-word": "the self-hosted Remote Control surface reads one Bearer scheme word, and this call's Authorization header carried it twice: a persisted session record holds the whole header value with the scheme word included, so prefixing Bearer onto it presents a different credential",
|
|
10502
|
+
unrecognised: "the self-hosted Remote Control surface did not recognise the presented credential"
|
|
10503
|
+
};
|
|
10504
|
+
const unauthorized = async (response, refusal) => {
|
|
10505
|
+
await answerJson2(response, HTTP_STATUS.unauthorized, { type: "error", error: { type: "authentication_error", message: refusalMessages[refusal] } });
|
|
10223
10506
|
};
|
|
10224
10507
|
const serveSessionPath = async (session, tail, request4, response) => {
|
|
10225
10508
|
const method = request4.method.toUpperCase();
|
|
@@ -10237,8 +10520,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10237
10520
|
return;
|
|
10238
10521
|
}
|
|
10239
10522
|
if (tail === "bridge" && method === "POST") {
|
|
10240
|
-
|
|
10241
|
-
|
|
10523
|
+
const refusal = credentialRefusal(request4.headers);
|
|
10524
|
+
if (refusal !== void 0) {
|
|
10525
|
+
await unauthorized(response, refusal);
|
|
10242
10526
|
return;
|
|
10243
10527
|
}
|
|
10244
10528
|
const workerJwt = mintWorkerJwt(deps.randomToken, deps.now());
|
|
@@ -10248,8 +10532,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10248
10532
|
return;
|
|
10249
10533
|
}
|
|
10250
10534
|
if (tail === "archive" && method === "POST") {
|
|
10251
|
-
|
|
10252
|
-
|
|
10535
|
+
const refusal = credentialRefusal(request4.headers);
|
|
10536
|
+
if (refusal !== void 0) {
|
|
10537
|
+
await unauthorized(response, refusal);
|
|
10253
10538
|
return;
|
|
10254
10539
|
}
|
|
10255
10540
|
for (const sink of [...session.workerStreams, ...session.clientStreams]) {
|
|
@@ -10261,11 +10546,13 @@ data: ${JSON.stringify(envelope)}
|
|
|
10261
10546
|
return;
|
|
10262
10547
|
}
|
|
10263
10548
|
if (tail === "worker" || tail.startsWith("worker/")) {
|
|
10264
|
-
|
|
10265
|
-
|
|
10549
|
+
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
10550
|
+
const webProxy = workerTail === "web-fetch" || workerTail === "web-search";
|
|
10551
|
+
const refusal = webProxy ? workerOrCredentialRefusal(request4.headers, session) : workerRefusal(request4.headers, session);
|
|
10552
|
+
if (refusal !== void 0) {
|
|
10553
|
+
await unauthorized(response, refusal);
|
|
10266
10554
|
return;
|
|
10267
10555
|
}
|
|
10268
|
-
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
10269
10556
|
if (workerTail === "" && method === "GET") {
|
|
10270
10557
|
await answerJson2(response, HTTP_STATUS.ok, { worker: { external_metadata: session.externalMetadata ?? null } });
|
|
10271
10558
|
return;
|
|
@@ -10364,11 +10651,64 @@ data: ${JSON.stringify(envelope)}
|
|
|
10364
10651
|
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
10365
10652
|
return;
|
|
10366
10653
|
}
|
|
10654
|
+
if (workerTail === "web-fetch" && method === "POST") {
|
|
10655
|
+
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
10656
|
+
const target = nonEmptyString2(body, "url");
|
|
10657
|
+
if (target === void 0) {
|
|
10658
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
|
|
10659
|
+
return;
|
|
10660
|
+
}
|
|
10661
|
+
const outcome = await deps.webFetch(target);
|
|
10662
|
+
if (outcome.kind === "fetched") {
|
|
10663
|
+
const answer = { url: target, destination_url: outcome.destinationUrl, text: outcome.text, ...outcome.contentType === void 0 ? {} : { content_type: outcome.contentType } };
|
|
10664
|
+
if (Buffer.byteLength(JSON.stringify(answer), "utf8") > RC_WEB_FETCH_MAX_BYTES) {
|
|
10665
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} escaped past the reader cap when framed`);
|
|
10666
|
+
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: "web_fetch_too_large", error_message: `the answer for ${target} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads` } });
|
|
10667
|
+
return;
|
|
10668
|
+
}
|
|
10669
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch served ${target} as ${outcome.destinationUrl}`);
|
|
10670
|
+
await answerJson2(response, HTTP_STATUS.ok, answer);
|
|
10671
|
+
return;
|
|
10672
|
+
}
|
|
10673
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} refused: ${outcome.errorType}`);
|
|
10674
|
+
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: outcome.errorType, error_message: outcome.errorMessage } });
|
|
10675
|
+
return;
|
|
10676
|
+
}
|
|
10677
|
+
if (workerTail === "web-search" && method === "POST") {
|
|
10678
|
+
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
10679
|
+
const query = nonEmptyString2(body, "query");
|
|
10680
|
+
if (query === void 0) {
|
|
10681
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
|
|
10682
|
+
return;
|
|
10683
|
+
}
|
|
10684
|
+
const allowedDomains = stringArray(body, "allowed_domains");
|
|
10685
|
+
const blockedDomains = stringArray(body, "blocked_domains");
|
|
10686
|
+
if (body?.allowed_domains !== void 0 && allowedDomains === void 0 || body?.blocked_domains !== void 0 && blockedDomains === void 0) {
|
|
10687
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "the domain filters of a worker web-search are string arrays" } });
|
|
10688
|
+
return;
|
|
10689
|
+
}
|
|
10690
|
+
const searchProfile = nonEmptyString2(body, "search_profile");
|
|
10691
|
+
const backend = deps.webSearch;
|
|
10692
|
+
if (backend === void 0) {
|
|
10693
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
|
|
10694
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: "web_search_unavailable", error_message: "this front door serves the Remote Control web-search proxy without a search backend, so no query can be served here" } });
|
|
10695
|
+
return;
|
|
10696
|
+
}
|
|
10697
|
+
const answered = await backend({ query, ...allowedDomains === void 0 ? {} : { allowedDomains }, ...blockedDomains === void 0 ? {} : { blockedDomains }, ...searchProfile === void 0 ? {} : { searchProfile } });
|
|
10698
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" answered by its backend`);
|
|
10699
|
+
if (isWebSearchRefusal(answered)) {
|
|
10700
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: answered.errorType, error_message: answered.errorMessage } });
|
|
10701
|
+
return;
|
|
10702
|
+
}
|
|
10703
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: answered.map((result) => ({ title: result.title ?? "", url: result.url, snippet: result.snippet ?? "" })) });
|
|
10704
|
+
return;
|
|
10705
|
+
}
|
|
10367
10706
|
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no worker path "${workerTail}"` } });
|
|
10368
10707
|
return;
|
|
10369
10708
|
}
|
|
10370
|
-
|
|
10371
|
-
|
|
10709
|
+
const clientRefusal = credentialRefusal(request4.headers);
|
|
10710
|
+
if (clientRefusal !== void 0) {
|
|
10711
|
+
await unauthorized(response, clientRefusal);
|
|
10372
10712
|
return;
|
|
10373
10713
|
}
|
|
10374
10714
|
if (tail === "events" && method === "POST") {
|
|
@@ -10433,8 +10773,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10433
10773
|
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the compatibility session list is a GET" } });
|
|
10434
10774
|
return;
|
|
10435
10775
|
}
|
|
10436
|
-
|
|
10437
|
-
|
|
10776
|
+
const listRefusal = credentialRefusal(request4.headers);
|
|
10777
|
+
if (listRefusal !== void 0) {
|
|
10778
|
+
await unauthorized(response, listRefusal);
|
|
10438
10779
|
return;
|
|
10439
10780
|
}
|
|
10440
10781
|
await answerJson2(response, HTTP_STATUS.ok, { data: [] });
|
|
@@ -10446,8 +10787,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10446
10787
|
}
|
|
10447
10788
|
if (pathname === RC_SESSIONS_PATH_PREFIX2) {
|
|
10448
10789
|
if (method === "POST") {
|
|
10449
|
-
|
|
10450
|
-
|
|
10790
|
+
const createRefusal = credentialRefusal(request4.headers);
|
|
10791
|
+
if (createRefusal !== void 0) {
|
|
10792
|
+
await unauthorized(response, createRefusal);
|
|
10451
10793
|
return;
|
|
10452
10794
|
}
|
|
10453
10795
|
const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
|
|
@@ -10472,8 +10814,9 @@ data: ${JSON.stringify(envelope)}
|
|
|
10472
10814
|
return;
|
|
10473
10815
|
}
|
|
10474
10816
|
if (method === "GET") {
|
|
10475
|
-
|
|
10476
|
-
|
|
10817
|
+
const readRefusal = credentialRefusal(request4.headers);
|
|
10818
|
+
if (readRefusal !== void 0) {
|
|
10819
|
+
await unauthorized(response, readRefusal);
|
|
10477
10820
|
return;
|
|
10478
10821
|
}
|
|
10479
10822
|
await answerJson2(response, HTTP_STATUS.ok, { data: [...sessions.values()].map((session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), updated_at: new Date(session.lastTrafficAt).toISOString(), session_status: "active" })) });
|
|
@@ -11909,6 +12252,10 @@ export {
|
|
|
11909
12252
|
RC_SELF_HOST_WORKER_JWT_TTL_SECONDS,
|
|
11910
12253
|
RC_SESSIONS_PATH_PREFIX,
|
|
11911
12254
|
RC_STREAM_BACKOFF_MS,
|
|
12255
|
+
RC_WEB_FETCH_ALLOW_PRIVATE_ENV,
|
|
12256
|
+
RC_WEB_FETCH_DEADLINE_MS,
|
|
12257
|
+
RC_WEB_FETCH_MAX_BYTES,
|
|
12258
|
+
RC_WEB_FETCH_MAX_REDIRECTS,
|
|
11912
12259
|
ROUTED_PATH_PREFIX,
|
|
11913
12260
|
RcPermissionModeSchema,
|
|
11914
12261
|
RcStreamEnvelopeSchema,
|
|
@@ -11967,6 +12314,7 @@ export {
|
|
|
11967
12314
|
ensureHeadroom,
|
|
11968
12315
|
evaluateAmbientCredentialGuard,
|
|
11969
12316
|
evaluateWhen,
|
|
12317
|
+
fetchThroughWebProxy,
|
|
11970
12318
|
formatAmbientCredentialGuardMessage,
|
|
11971
12319
|
formatCheckReport,
|
|
11972
12320
|
formatDoctorReport,
|
|
@@ -11983,6 +12331,7 @@ export {
|
|
|
11983
12331
|
interruptRcSession,
|
|
11984
12332
|
isIdentityDirectoryName,
|
|
11985
12333
|
isInterceptedHost,
|
|
12334
|
+
isPrivateAddress,
|
|
11986
12335
|
isRcPermissionMode,
|
|
11987
12336
|
lateRcEventDial,
|
|
11988
12337
|
lateRcStreamDial,
|
|
@@ -12007,6 +12356,7 @@ export {
|
|
|
12007
12356
|
rcFanoutOnDoorHub,
|
|
12008
12357
|
rcObserverAsPassthrough,
|
|
12009
12358
|
rcSelfHostFromEnv,
|
|
12359
|
+
rcWebFetchAllowsPrivateFromEnv,
|
|
12010
12360
|
readActiveIdentity,
|
|
12011
12361
|
readDirectoryRules,
|
|
12012
12362
|
readFarmManifest,
|
|
@@ -12022,6 +12372,7 @@ export {
|
|
|
12022
12372
|
realRcControlTransport,
|
|
12023
12373
|
realRcEventDial,
|
|
12024
12374
|
realRcStreamDial,
|
|
12375
|
+
realRcWebFetch,
|
|
12025
12376
|
recoverFarm,
|
|
12026
12377
|
recoveryDiagnostics,
|
|
12027
12378
|
removeDirectoryRule,
|