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/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 http2;
2722
+ let http3;
2723
2723
  try {
2724
- http2 = await ports.startProviderListener(lastPort);
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 && http2.port !== previousSticky) {
2729
- ports.log(`agent-shim frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http2.port)} (sessions launched against the old port are stale until they relaunch)`);
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 = http2.port;
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 http2.close();
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 http2.close();
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: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
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(http2.port)}, CONNECT surface on 127.0.0.1:${String(connect3.port)}`);
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 http2.close();
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 unauthorized = async (response) => {
10222
- await answerJson2(response, HTTP_STATUS.unauthorized, { type: "error", error: { type: "authentication_error", message: "the self-hosted Remote Control surface did not recognise the presented credential" } });
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
- if (!presentsCredential(request4.headers)) {
10241
- await unauthorized(response);
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
- if (!presentsCredential(request4.headers)) {
10252
- await unauthorized(response);
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
- if (!presentsWorkerJwt(request4.headers, session)) {
10265
- await unauthorized(response);
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
- if (!presentsCredential(request4.headers)) {
10371
- await unauthorized(response);
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
- if (!presentsCredential(request4.headers)) {
10437
- await unauthorized(response);
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
- if (!presentsCredential(request4.headers)) {
10450
- await unauthorized(response);
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
- if (!presentsCredential(request4.headers)) {
10476
- await unauthorized(response);
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,