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.cjs CHANGED
@@ -107,6 +107,10 @@ __export(index_exports, {
107
107
  RC_SELF_HOST_WORKER_JWT_TTL_SECONDS: () => RC_SELF_HOST_WORKER_JWT_TTL_SECONDS,
108
108
  RC_SESSIONS_PATH_PREFIX: () => RC_SESSIONS_PATH_PREFIX,
109
109
  RC_STREAM_BACKOFF_MS: () => RC_STREAM_BACKOFF_MS,
110
+ RC_WEB_FETCH_ALLOW_PRIVATE_ENV: () => RC_WEB_FETCH_ALLOW_PRIVATE_ENV,
111
+ RC_WEB_FETCH_DEADLINE_MS: () => RC_WEB_FETCH_DEADLINE_MS,
112
+ RC_WEB_FETCH_MAX_BYTES: () => RC_WEB_FETCH_MAX_BYTES,
113
+ RC_WEB_FETCH_MAX_REDIRECTS: () => RC_WEB_FETCH_MAX_REDIRECTS,
110
114
  ROUTED_PATH_PREFIX: () => ROUTED_PATH_PREFIX,
111
115
  RcPermissionModeSchema: () => RcPermissionModeSchema,
112
116
  RcStreamEnvelopeSchema: () => RcStreamEnvelopeSchema,
@@ -165,6 +169,7 @@ __export(index_exports, {
165
169
  ensureHeadroom: () => ensureHeadroom,
166
170
  evaluateAmbientCredentialGuard: () => evaluateAmbientCredentialGuard,
167
171
  evaluateWhen: () => evaluateWhen,
172
+ fetchThroughWebProxy: () => fetchThroughWebProxy,
168
173
  formatAmbientCredentialGuardMessage: () => formatAmbientCredentialGuardMessage,
169
174
  formatCheckReport: () => formatCheckReport,
170
175
  formatDoctorReport: () => formatDoctorReport,
@@ -181,6 +186,7 @@ __export(index_exports, {
181
186
  interruptRcSession: () => interruptRcSession,
182
187
  isIdentityDirectoryName: () => isIdentityDirectoryName,
183
188
  isInterceptedHost: () => isInterceptedHost,
189
+ isPrivateAddress: () => isPrivateAddress,
184
190
  isRcPermissionMode: () => isRcPermissionMode,
185
191
  lateRcEventDial: () => lateRcEventDial,
186
192
  lateRcStreamDial: () => lateRcStreamDial,
@@ -205,6 +211,7 @@ __export(index_exports, {
205
211
  rcFanoutOnDoorHub: () => rcFanoutOnDoorHub,
206
212
  rcObserverAsPassthrough: () => rcObserverAsPassthrough,
207
213
  rcSelfHostFromEnv: () => rcSelfHostFromEnv,
214
+ rcWebFetchAllowsPrivateFromEnv: () => rcWebFetchAllowsPrivateFromEnv,
208
215
  readActiveIdentity: () => readActiveIdentity,
209
216
  readDirectoryRules: () => readDirectoryRules,
210
217
  readFarmManifest: () => readFarmManifest,
@@ -220,6 +227,7 @@ __export(index_exports, {
220
227
  realRcControlTransport: () => realRcControlTransport,
221
228
  realRcEventDial: () => realRcEventDial,
222
229
  realRcStreamDial: () => realRcStreamDial,
230
+ realRcWebFetch: () => realRcWebFetch,
223
231
  recoverFarm: () => recoverFarm,
224
232
  recoveryDiagnostics: () => recoveryDiagnostics,
225
233
  removeDirectoryRule: () => removeDirectoryRule,
@@ -2983,22 +2991,22 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
2983
2991
  };
2984
2992
  writeFrontDoorState(fs12, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
2985
2993
  const previousSticky = lastPort;
2986
- let http2;
2994
+ let http3;
2987
2995
  try {
2988
- http2 = await ports.startProviderListener(lastPort);
2996
+ http3 = await ports.startProviderListener(lastPort);
2989
2997
  } catch (error) {
2990
2998
  return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
2991
2999
  }
2992
- if (previousSticky !== void 0 && http2.port !== previousSticky) {
2993
- 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)`);
3000
+ if (previousSticky !== void 0 && http3.port !== previousSticky) {
3001
+ 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)`);
2994
3002
  }
2995
- lastPort = http2.port;
3003
+ lastPort = http3.port;
2996
3004
  const previousStickyConnect = lastConnectPort;
2997
3005
  let connect3;
2998
3006
  try {
2999
3007
  connect3 = await ports.startConnectListener(lastConnectPort);
3000
3008
  } catch (error) {
3001
- await http2.close();
3009
+ await http3.close();
3002
3010
  return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
3003
3011
  }
3004
3012
  if (previousStickyConnect !== void 0 && connect3.port !== previousStickyConnect) {
@@ -3011,16 +3019,16 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
3011
3019
  direct = await ports.startDirectListener(lastDirectPort);
3012
3020
  } catch (error) {
3013
3021
  await connect3.close();
3014
- await http2.close();
3022
+ await http3.close();
3015
3023
  return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
3016
3024
  }
3017
3025
  if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
3018
3026
  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)`);
3019
3027
  }
3020
3028
  lastDirectPort = direct.port;
3021
- writeFrontDoorState(fs12, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http2.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
3029
+ writeFrontDoorState(fs12, paths.frontdoorStateFile, { protocol: FRONT_DOOR_PROTOCOL, supervisorPid: ports.ownPid, port: http3.port, connectPort: connect3.port, directPort: direct.port, ...sticky() });
3022
3030
  fs12.removeRecursive(paths.frontdoorLockFile);
3023
- 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)}`);
3031
+ 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)}`);
3024
3032
  let idleSince;
3025
3033
  let ticks = 0;
3026
3034
  for (; ; ) {
@@ -3038,7 +3046,7 @@ async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {})
3038
3046
  ports.log(`agent-shim frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
3039
3047
  await direct.close();
3040
3048
  await connect3.close();
3041
- await http2.close();
3049
+ await http3.close();
3042
3050
  ports.closeRcStreamHub();
3043
3051
  writeFrontDoorState(fs12, paths.frontdoorStateFile, sticky());
3044
3052
  return 0;
@@ -10252,6 +10260,252 @@ function createRcStreamHub(deps) {
10252
10260
  };
10253
10261
  }
10254
10262
 
10263
+ // src/frontdoor/rcWebFetch.ts
10264
+ var dns2 = __toESM(require("node:dns"), 1);
10265
+ var http2 = __toESM(require("node:http"), 1);
10266
+ var https3 = __toESM(require("node:https"), 1);
10267
+ var net3 = __toESM(require("node:net"), 1);
10268
+ var RC_WEB_FETCH_MAX_BYTES = 12582912;
10269
+ var RC_WEB_FETCH_DEADLINE_MS = 4e4;
10270
+ var RC_WEB_FETCH_MAX_REDIRECTS = 20;
10271
+ var RC_WEB_FETCH_ALLOW_PRIVATE_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST_WEB_FETCH_ALLOW_PRIVATE";
10272
+ function rcWebFetchAllowsPrivateFromEnv(env) {
10273
+ return env[RC_WEB_FETCH_ALLOW_PRIVATE_ENV] === "1";
10274
+ }
10275
+ var WEB_FETCH_USER_AGENT = "agent-shim-frontdoor";
10276
+ var ERROR_PREFIX = "web_fetch_";
10277
+ var V4_THIS_NETWORK_OCTET = 0;
10278
+ var V4_PRIVATE_A_OCTET = 10;
10279
+ var V4_LOOPBACK_OCTET = 127;
10280
+ var V4_CGNAT_OCTET = 100;
10281
+ var V4_CGNAT_SECOND_FROM = 64;
10282
+ var V4_CGNAT_SECOND_TO = 127;
10283
+ var V4_LINK_LOCAL_OCTET = 169;
10284
+ var V4_LINK_LOCAL_SECOND = 254;
10285
+ var V4_PRIVATE_B_OCTET = 172;
10286
+ var V4_PRIVATE_B_SECOND_FROM = 16;
10287
+ var V4_PRIVATE_B_SECOND_TO = 31;
10288
+ var V4_PRIVATE_C_OCTET = 192;
10289
+ var V4_PRIVATE_C_SECOND = 168;
10290
+ var V4_MULTICAST_FROM_OCTET = 224;
10291
+ var V6_UNIQUE_LOCAL_FROM = 64512;
10292
+ var V6_UNIQUE_LOCAL_TO = 65023;
10293
+ var V6_LINK_LOCAL_FROM = 65152;
10294
+ var V6_LINK_LOCAL_TO = 65215;
10295
+ var V6_MULTICAST_FROM = 65280;
10296
+ var IP_VERSION_4 = 4;
10297
+ var IP_VERSION_6 = 6;
10298
+ var HTTP_MOVED_PERMANENTLY = 301;
10299
+ var HTTP_FOUND = 302;
10300
+ var HTTP_SEE_OTHER = 303;
10301
+ var HTTP_TEMPORARY_REDIRECT = 307;
10302
+ var HTTP_PERMANENT_REDIRECT = 308;
10303
+ var SUCCESS_STATUS_MAX_EXCLUSIVE4 = 300;
10304
+ function refused(errorType, errorMessage2) {
10305
+ return { kind: "refused", errorType: `${ERROR_PREFIX}${errorType}`, errorMessage: errorMessage2 };
10306
+ }
10307
+ function isPrivateAddress(address) {
10308
+ const version = net3.isIP(address);
10309
+ if (version === IP_VERSION_4) {
10310
+ const octets = address.split(".").map((part) => Number(part));
10311
+ const first = octets[0] ?? -1;
10312
+ const second = octets[1] ?? -1;
10313
+ if (first === V4_THIS_NETWORK_OCTET || first === V4_PRIVATE_A_OCTET || first === V4_LOOPBACK_OCTET) {
10314
+ return true;
10315
+ }
10316
+ if (first === V4_CGNAT_OCTET && second >= V4_CGNAT_SECOND_FROM && second <= V4_CGNAT_SECOND_TO) {
10317
+ return true;
10318
+ }
10319
+ if (first === V4_LINK_LOCAL_OCTET && second === V4_LINK_LOCAL_SECOND) {
10320
+ return true;
10321
+ }
10322
+ if (first === V4_PRIVATE_B_OCTET && second >= V4_PRIVATE_B_SECOND_FROM && second <= V4_PRIVATE_B_SECOND_TO) {
10323
+ return true;
10324
+ }
10325
+ if (first === V4_PRIVATE_C_OCTET && second === V4_PRIVATE_C_SECOND) {
10326
+ return true;
10327
+ }
10328
+ return first >= V4_MULTICAST_FROM_OCTET;
10329
+ }
10330
+ if (version === IP_VERSION_6) {
10331
+ const lower = address.toLowerCase();
10332
+ const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
10333
+ if (mapped !== null) {
10334
+ return isPrivateAddress(mapped[1] ?? "");
10335
+ }
10336
+ if (lower === "::" || lower === "::1") {
10337
+ return true;
10338
+ }
10339
+ const firstGroup = Number.parseInt(/^([0-9a-f]+):/.exec(lower)?.[1] ?? "0", 16);
10340
+ if (Number.isNaN(firstGroup)) {
10341
+ return true;
10342
+ }
10343
+ if (firstGroup >= V6_UNIQUE_LOCAL_FROM && firstGroup <= V6_UNIQUE_LOCAL_TO) {
10344
+ return true;
10345
+ }
10346
+ if (firstGroup >= V6_LINK_LOCAL_FROM && firstGroup <= V6_LINK_LOCAL_TO) {
10347
+ return true;
10348
+ }
10349
+ return firstGroup >= V6_MULTICAST_FROM;
10350
+ }
10351
+ return true;
10352
+ }
10353
+ function isSuccessful4(status) {
10354
+ return status >= HTTP_STATUS.ok && status < SUCCESS_STATUS_MAX_EXCLUSIVE4;
10355
+ }
10356
+ function isRedirect(status) {
10357
+ return status === HTTP_MOVED_PERMANENTLY || status === HTTP_FOUND || status === HTTP_SEE_OTHER || status === HTTP_TEMPORARY_REDIRECT || status === HTTP_PERMANENT_REDIRECT;
10358
+ }
10359
+ function readableContentType(contentType) {
10360
+ if (contentType === void 0 || contentType.trim() === "") {
10361
+ return true;
10362
+ }
10363
+ const normalised = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
10364
+ const slash = normalised.indexOf("/");
10365
+ const main = slash === -1 ? normalised : normalised.slice(0, slash);
10366
+ const sub = slash === -1 ? "" : normalised.slice(slash + 1);
10367
+ if (main === "text") {
10368
+ return true;
10369
+ }
10370
+ return /json|xml|javascript|ecmascript|yaml|csv/.test(sub);
10371
+ }
10372
+ async function readCapped(chunks) {
10373
+ const collected = [];
10374
+ let total = 0;
10375
+ for await (const chunk of chunks) {
10376
+ total += chunk.byteLength;
10377
+ if (total > RC_WEB_FETCH_MAX_BYTES) {
10378
+ return { tooLarge: true, text: "" };
10379
+ }
10380
+ collected.push(Buffer.from(chunk));
10381
+ }
10382
+ return { tooLarge: false, text: Buffer.concat(collected).toString("utf8") };
10383
+ }
10384
+ async function checkTarget(url, deps) {
10385
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
10386
+ return refused("scheme", `the web-fetch proxy dials http and https only, not ${url.protocol}`);
10387
+ }
10388
+ if (deps.allowPrivate) {
10389
+ return void 0;
10390
+ }
10391
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
10392
+ let addresses;
10393
+ try {
10394
+ addresses = net3.isIP(hostname) === 0 ? await deps.lookup(hostname) : [hostname];
10395
+ } catch {
10396
+ return refused("resolve", `${hostname} did not resolve`);
10397
+ }
10398
+ for (const address of addresses) {
10399
+ if (isPrivateAddress(address)) {
10400
+ 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`);
10401
+ }
10402
+ }
10403
+ return void 0;
10404
+ }
10405
+ async function fetchThroughWebProxy(url, deps) {
10406
+ let current;
10407
+ try {
10408
+ current = new URL(url);
10409
+ } catch {
10410
+ return refused("malformed_url", `${url} is not a URL this proxy can dial`);
10411
+ }
10412
+ const controller = new AbortController();
10413
+ const timer = setTimeout(() => {
10414
+ controller.abort();
10415
+ }, deps.deadlineMs);
10416
+ timer.unref();
10417
+ try {
10418
+ let redirects = 0;
10419
+ for (; ; ) {
10420
+ const refusal = await checkTarget(current, deps);
10421
+ if (refusal !== void 0) {
10422
+ return refusal;
10423
+ }
10424
+ let hop;
10425
+ try {
10426
+ hop = await deps.dial(current, controller.signal);
10427
+ } catch (error) {
10428
+ if (controller.signal.aborted) {
10429
+ return refused("deadline", `the fetch of ${url} did not settle within its deadline`);
10430
+ }
10431
+ return refused("transport", `the dial of ${current.toString()} failed: ${error instanceof Error ? error.message : "unknown error"}`);
10432
+ }
10433
+ if (isRedirect(hop.status) && hop.location !== void 0) {
10434
+ if (redirects >= RC_WEB_FETCH_MAX_REDIRECTS) {
10435
+ controller.abort();
10436
+ return refused("too_many_redirects", `${url} followed more than ${String(RC_WEB_FETCH_MAX_REDIRECTS)} redirects without settling`);
10437
+ }
10438
+ let next;
10439
+ try {
10440
+ next = new URL(hop.location, current);
10441
+ } catch {
10442
+ controller.abort();
10443
+ return refused("malformed_url", `a redirect of ${current.toString()} named ${hop.location}, which is not a URL this proxy can dial`);
10444
+ }
10445
+ const drained = await readCapped(hop.chunks);
10446
+ if (drained.tooLarge) {
10447
+ controller.abort();
10448
+ 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`);
10449
+ }
10450
+ redirects += 1;
10451
+ current = next;
10452
+ continue;
10453
+ }
10454
+ if (!isSuccessful4(hop.status)) {
10455
+ controller.abort();
10456
+ return refused("status", `the target answered HTTP ${String(hop.status)} for ${current.toString()}`);
10457
+ }
10458
+ const contentType = hop.contentType ?? "";
10459
+ if (!readableContentType(contentType)) {
10460
+ controller.abort();
10461
+ return refused("content_type", `the target at ${current.toString()} served a ${contentType} answer, which the proxy's text field cannot carry`);
10462
+ }
10463
+ const read = await readCapped(hop.chunks);
10464
+ if (read.tooLarge) {
10465
+ controller.abort();
10466
+ 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`);
10467
+ }
10468
+ return { kind: "fetched", url, destinationUrl: current.toString(), contentType: hop.contentType, text: read.text };
10469
+ }
10470
+ } finally {
10471
+ clearTimeout(timer);
10472
+ }
10473
+ }
10474
+ function realRcWebFetch(options) {
10475
+ const plainAgent = new ExemptHttpAgent();
10476
+ const tlsAgent = new ExemptTlsAgent();
10477
+ const deps = {
10478
+ dial: async (url, signal) => await new Promise((resolve, reject) => {
10479
+ const request4 = (url.protocol === "https:" ? https3 : http2).request(
10480
+ {
10481
+ hostname: url.hostname,
10482
+ port: url.port === "" ? void 0 : Number(url.port),
10483
+ method: "GET",
10484
+ path: `${url.pathname}${url.search}`,
10485
+ headers: { accept: "*/*", "accept-encoding": "identity", "user-agent": WEB_FETCH_USER_AGENT },
10486
+ agent: url.protocol === "https:" ? tlsAgent : plainAgent,
10487
+ signal
10488
+ },
10489
+ (response) => {
10490
+ const location = response.headers.location;
10491
+ resolve({
10492
+ status: response.statusCode ?? 0,
10493
+ location: typeof location === "string" ? location : void 0,
10494
+ contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : void 0,
10495
+ chunks: response
10496
+ });
10497
+ }
10498
+ );
10499
+ request4.once("error", reject);
10500
+ request4.end();
10501
+ }),
10502
+ lookup: async (hostname) => (await dns2.promises.lookup(hostname, { all: true })).map((entry) => entry.address),
10503
+ allowPrivate: options.allowPrivate,
10504
+ deadlineMs: RC_WEB_FETCH_DEADLINE_MS
10505
+ };
10506
+ return async (url) => await fetchThroughWebProxy(url, deps);
10507
+ }
10508
+
10255
10509
  // src/frontdoor/rcSelfHost.ts
10256
10510
  var RC_SELF_HOST_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST";
10257
10511
  function rcSelfHostFromEnv(env) {
@@ -10281,6 +10535,9 @@ var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
10281
10535
  function isRecord6(value) {
10282
10536
  return typeof value === "object" && value !== null && !Array.isArray(value);
10283
10537
  }
10538
+ function isWebSearchRefusal(value) {
10539
+ return !Array.isArray(value);
10540
+ }
10284
10541
  function nonEmptyString2(record, field) {
10285
10542
  const value = record === void 0 ? void 0 : record[field];
10286
10543
  return typeof value === "string" && value !== "" ? value : void 0;
@@ -10295,6 +10552,20 @@ function numberOf(record, field) {
10295
10552
  }
10296
10553
  return void 0;
10297
10554
  }
10555
+ function stringArray(record, field) {
10556
+ const value = record === void 0 ? void 0 : record[field];
10557
+ if (!Array.isArray(value)) {
10558
+ return void 0;
10559
+ }
10560
+ const members = [];
10561
+ for (const member of value) {
10562
+ if (typeof member !== "string") {
10563
+ return void 0;
10564
+ }
10565
+ members.push(member);
10566
+ }
10567
+ return members;
10568
+ }
10298
10569
  function singleHeader2(headers, name) {
10299
10570
  const value = headers[name];
10300
10571
  return typeof value === "string" ? value : void 0;
@@ -10306,6 +10577,17 @@ function bearerOf(headers) {
10306
10577
  }
10307
10578
  return presented.slice(0, "bearer ".length).toLowerCase() === "bearer " ? presented.slice("bearer ".length) : void 0;
10308
10579
  }
10580
+ function presentationRefusal(headers) {
10581
+ const presented = singleHeader2(headers, "authorization");
10582
+ if (presented === void 0) {
10583
+ return "absent";
10584
+ }
10585
+ if (presented.slice(0, "bearer ".length).toLowerCase() !== "bearer ") {
10586
+ return "not-a-bearer";
10587
+ }
10588
+ const bearer = presented.slice("bearer ".length);
10589
+ return bearer.slice(0, "bearer ".length).toLowerCase() === "bearer " ? "doubled-scheme-word" : "unrecognised";
10590
+ }
10309
10591
  async function readBody2(request4, capBytes) {
10310
10592
  return await new Promise((resolve, reject) => {
10311
10593
  const chunks = [];
@@ -10394,6 +10676,9 @@ function createRcSelfHostSurface(deps) {
10394
10676
  }
10395
10677
  return false;
10396
10678
  };
10679
+ const credentialRefusal = (headers) => presentsCredential(headers) ? void 0 : presentationRefusal(headers);
10680
+ const workerRefusal = (headers, session) => presentsWorkerJwt(headers, session) ? void 0 : presentationRefusal(headers);
10681
+ const workerOrCredentialRefusal = (headers, session) => presentsWorkerJwt(headers, session) || presentsCredential(headers) ? void 0 : presentationRefusal(headers);
10397
10682
  const frameOf = (event) => {
10398
10683
  const envelope = {
10399
10684
  event_id: event.eventId,
@@ -10482,8 +10767,14 @@ data: ${JSON.stringify(envelope)}
10482
10767
  const epochStale = async (response) => {
10483
10768
  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 });
10484
10769
  };
10485
- const unauthorized = async (response) => {
10486
- 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" } });
10770
+ const refusalMessages = {
10771
+ absent: "the self-hosted Remote Control surface authenticates by the Authorization header, and this call carried none",
10772
+ "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",
10773
+ "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",
10774
+ unrecognised: "the self-hosted Remote Control surface did not recognise the presented credential"
10775
+ };
10776
+ const unauthorized = async (response, refusal) => {
10777
+ await answerJson2(response, HTTP_STATUS.unauthorized, { type: "error", error: { type: "authentication_error", message: refusalMessages[refusal] } });
10487
10778
  };
10488
10779
  const serveSessionPath = async (session, tail, request4, response) => {
10489
10780
  const method = request4.method.toUpperCase();
@@ -10501,8 +10792,9 @@ data: ${JSON.stringify(envelope)}
10501
10792
  return;
10502
10793
  }
10503
10794
  if (tail === "bridge" && method === "POST") {
10504
- if (!presentsCredential(request4.headers)) {
10505
- await unauthorized(response);
10795
+ const refusal = credentialRefusal(request4.headers);
10796
+ if (refusal !== void 0) {
10797
+ await unauthorized(response, refusal);
10506
10798
  return;
10507
10799
  }
10508
10800
  const workerJwt = mintWorkerJwt(deps.randomToken, deps.now());
@@ -10512,8 +10804,9 @@ data: ${JSON.stringify(envelope)}
10512
10804
  return;
10513
10805
  }
10514
10806
  if (tail === "archive" && method === "POST") {
10515
- if (!presentsCredential(request4.headers)) {
10516
- await unauthorized(response);
10807
+ const refusal = credentialRefusal(request4.headers);
10808
+ if (refusal !== void 0) {
10809
+ await unauthorized(response, refusal);
10517
10810
  return;
10518
10811
  }
10519
10812
  for (const sink of [...session.workerStreams, ...session.clientStreams]) {
@@ -10525,11 +10818,13 @@ data: ${JSON.stringify(envelope)}
10525
10818
  return;
10526
10819
  }
10527
10820
  if (tail === "worker" || tail.startsWith("worker/")) {
10528
- if (!presentsWorkerJwt(request4.headers, session)) {
10529
- await unauthorized(response);
10821
+ const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
10822
+ const webProxy = workerTail === "web-fetch" || workerTail === "web-search";
10823
+ const refusal = webProxy ? workerOrCredentialRefusal(request4.headers, session) : workerRefusal(request4.headers, session);
10824
+ if (refusal !== void 0) {
10825
+ await unauthorized(response, refusal);
10530
10826
  return;
10531
10827
  }
10532
- const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
10533
10828
  if (workerTail === "" && method === "GET") {
10534
10829
  await answerJson2(response, HTTP_STATUS.ok, { worker: { external_metadata: session.externalMetadata ?? null } });
10535
10830
  return;
@@ -10628,11 +10923,64 @@ data: ${JSON.stringify(envelope)}
10628
10923
  await answerJson2(response, HTTP_STATUS.ok, {});
10629
10924
  return;
10630
10925
  }
10926
+ if (workerTail === "web-fetch" && method === "POST") {
10927
+ const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
10928
+ const target = nonEmptyString2(body, "url");
10929
+ if (target === void 0) {
10930
+ await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
10931
+ return;
10932
+ }
10933
+ const outcome = await deps.webFetch(target);
10934
+ if (outcome.kind === "fetched") {
10935
+ const answer = { url: target, destination_url: outcome.destinationUrl, text: outcome.text, ...outcome.contentType === void 0 ? {} : { content_type: outcome.contentType } };
10936
+ if (Buffer.byteLength(JSON.stringify(answer), "utf8") > RC_WEB_FETCH_MAX_BYTES) {
10937
+ deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} escaped past the reader cap when framed`);
10938
+ 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` } });
10939
+ return;
10940
+ }
10941
+ deps.log?.(`rc selfhost ${session.id}: worker web-fetch served ${target} as ${outcome.destinationUrl}`);
10942
+ await answerJson2(response, HTTP_STATUS.ok, answer);
10943
+ return;
10944
+ }
10945
+ deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} refused: ${outcome.errorType}`);
10946
+ await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: outcome.errorType, error_message: outcome.errorMessage } });
10947
+ return;
10948
+ }
10949
+ if (workerTail === "web-search" && method === "POST") {
10950
+ const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
10951
+ const query = nonEmptyString2(body, "query");
10952
+ if (query === void 0) {
10953
+ await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
10954
+ return;
10955
+ }
10956
+ const allowedDomains = stringArray(body, "allowed_domains");
10957
+ const blockedDomains = stringArray(body, "blocked_domains");
10958
+ if (body?.allowed_domains !== void 0 && allowedDomains === void 0 || body?.blocked_domains !== void 0 && blockedDomains === void 0) {
10959
+ 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" } });
10960
+ return;
10961
+ }
10962
+ const searchProfile = nonEmptyString2(body, "search_profile");
10963
+ const backend = deps.webSearch;
10964
+ if (backend === void 0) {
10965
+ deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
10966
+ 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" } });
10967
+ return;
10968
+ }
10969
+ const answered = await backend({ query, ...allowedDomains === void 0 ? {} : { allowedDomains }, ...blockedDomains === void 0 ? {} : { blockedDomains }, ...searchProfile === void 0 ? {} : { searchProfile } });
10970
+ deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" answered by its backend`);
10971
+ if (isWebSearchRefusal(answered)) {
10972
+ await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: answered.errorType, error_message: answered.errorMessage } });
10973
+ return;
10974
+ }
10975
+ await answerJson2(response, HTTP_STATUS.ok, { results: answered.map((result) => ({ title: result.title ?? "", url: result.url, snippet: result.snippet ?? "" })) });
10976
+ return;
10977
+ }
10631
10978
  await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no worker path "${workerTail}"` } });
10632
10979
  return;
10633
10980
  }
10634
- if (!presentsCredential(request4.headers)) {
10635
- await unauthorized(response);
10981
+ const clientRefusal = credentialRefusal(request4.headers);
10982
+ if (clientRefusal !== void 0) {
10983
+ await unauthorized(response, clientRefusal);
10636
10984
  return;
10637
10985
  }
10638
10986
  if (tail === "events" && method === "POST") {
@@ -10697,8 +11045,9 @@ data: ${JSON.stringify(envelope)}
10697
11045
  await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the compatibility session list is a GET" } });
10698
11046
  return;
10699
11047
  }
10700
- if (!presentsCredential(request4.headers)) {
10701
- await unauthorized(response);
11048
+ const listRefusal = credentialRefusal(request4.headers);
11049
+ if (listRefusal !== void 0) {
11050
+ await unauthorized(response, listRefusal);
10702
11051
  return;
10703
11052
  }
10704
11053
  await answerJson2(response, HTTP_STATUS.ok, { data: [] });
@@ -10710,8 +11059,9 @@ data: ${JSON.stringify(envelope)}
10710
11059
  }
10711
11060
  if (pathname === RC_SESSIONS_PATH_PREFIX2) {
10712
11061
  if (method === "POST") {
10713
- if (!presentsCredential(request4.headers)) {
10714
- await unauthorized(response);
11062
+ const createRefusal = credentialRefusal(request4.headers);
11063
+ if (createRefusal !== void 0) {
11064
+ await unauthorized(response, createRefusal);
10715
11065
  return;
10716
11066
  }
10717
11067
  const body = parseJsonObject2(await readBody2(request4.body, CONTROL_BODY_CAP_BYTES));
@@ -10736,8 +11086,9 @@ data: ${JSON.stringify(envelope)}
10736
11086
  return;
10737
11087
  }
10738
11088
  if (method === "GET") {
10739
- if (!presentsCredential(request4.headers)) {
10740
- await unauthorized(response);
11089
+ const readRefusal = credentialRefusal(request4.headers);
11090
+ if (readRefusal !== void 0) {
11091
+ await unauthorized(response, readRefusal);
10741
11092
  return;
10742
11093
  }
10743
11094
  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" })) });
@@ -12174,6 +12525,10 @@ function prepareClaudeLaunch(options) {
12174
12525
  RC_SELF_HOST_WORKER_JWT_TTL_SECONDS,
12175
12526
  RC_SESSIONS_PATH_PREFIX,
12176
12527
  RC_STREAM_BACKOFF_MS,
12528
+ RC_WEB_FETCH_ALLOW_PRIVATE_ENV,
12529
+ RC_WEB_FETCH_DEADLINE_MS,
12530
+ RC_WEB_FETCH_MAX_BYTES,
12531
+ RC_WEB_FETCH_MAX_REDIRECTS,
12177
12532
  ROUTED_PATH_PREFIX,
12178
12533
  RcPermissionModeSchema,
12179
12534
  RcStreamEnvelopeSchema,
@@ -12232,6 +12587,7 @@ function prepareClaudeLaunch(options) {
12232
12587
  ensureHeadroom,
12233
12588
  evaluateAmbientCredentialGuard,
12234
12589
  evaluateWhen,
12590
+ fetchThroughWebProxy,
12235
12591
  formatAmbientCredentialGuardMessage,
12236
12592
  formatCheckReport,
12237
12593
  formatDoctorReport,
@@ -12248,6 +12604,7 @@ function prepareClaudeLaunch(options) {
12248
12604
  interruptRcSession,
12249
12605
  isIdentityDirectoryName,
12250
12606
  isInterceptedHost,
12607
+ isPrivateAddress,
12251
12608
  isRcPermissionMode,
12252
12609
  lateRcEventDial,
12253
12610
  lateRcStreamDial,
@@ -12272,6 +12629,7 @@ function prepareClaudeLaunch(options) {
12272
12629
  rcFanoutOnDoorHub,
12273
12630
  rcObserverAsPassthrough,
12274
12631
  rcSelfHostFromEnv,
12632
+ rcWebFetchAllowsPrivateFromEnv,
12275
12633
  readActiveIdentity,
12276
12634
  readDirectoryRules,
12277
12635
  readFarmManifest,
@@ -12287,6 +12645,7 @@ function prepareClaudeLaunch(options) {
12287
12645
  realRcControlTransport,
12288
12646
  realRcEventDial,
12289
12647
  realRcStreamDial,
12648
+ realRcWebFetch,
12290
12649
  recoverFarm,
12291
12650
  recoveryDiagnostics,
12292
12651
  removeDirectoryRule,