@rogatio/cli 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/node/index.js +600 -12
  2. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { basename as basename3, dirname as dirname8, resolve as resolve8 } from
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
8
8
  // packages/runtime/dist/node/index.js
9
+ import { isIP } from "node:net";
9
10
  import * as fs from "node:fs/promises";
10
11
  import * as path from "node:path";
11
12
 
@@ -1979,6 +1980,42 @@ function sourceMatches(source, url) {
1979
1980
  return false;
1980
1981
  }
1981
1982
  }
1983
+ var PAC_STEER_MARKER = "steer:";
1984
+ function decodePacSteer(entry) {
1985
+ if (!entry.startsWith(PAC_STEER_MARKER)) return null;
1986
+ const value = entry.slice(PAC_STEER_MARKER.length);
1987
+ let url;
1988
+ try {
1989
+ url = new URL(`${value}/`);
1990
+ } catch {
1991
+ return null;
1992
+ }
1993
+ if (url.username !== "" || url.password !== "" || url.search !== "") {
1994
+ return null;
1995
+ }
1996
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
1997
+ const scheme = url.protocol === "https:" ? "https" : "http";
1998
+ const port = url.port === "" ? void 0 : Number(url.port);
1999
+ if (port !== void 0 && (!Number.isInteger(port) || port < 1 || port > 65535)) {
2000
+ return null;
2001
+ }
2002
+ const rebuilt = port === void 0 ? `${scheme}://${url.hostname}` : `${scheme}://${url.hostname}:${port}`;
2003
+ if (rebuilt !== value || !isLiteralSteerHost(url.hostname)) return null;
2004
+ return port === void 0 ? { scheme, host: url.hostname } : { scheme, host: url.hostname, port };
2005
+ }
2006
+ function isLiteralSteerHost(host) {
2007
+ if (host.length === 0 || host.length > 253) return false;
2008
+ if (host.startsWith(".") || host.endsWith(".") || host.includes("..")) {
2009
+ return false;
2010
+ }
2011
+ const labels = host.split(".");
2012
+ for (const label2 of labels) {
2013
+ if (label2.length === 0 || label2.length > 63) return false;
2014
+ if (!/^[A-Za-z0-9-]+$/.test(label2)) return false;
2015
+ if (label2.startsWith("-") || label2.endsWith("-")) return false;
2016
+ }
2017
+ return true;
2018
+ }
1982
2019
  function sameOrigin(a, b) {
1983
2020
  try {
1984
2021
  return new URL(a).origin === new URL(b).origin;
@@ -1998,6 +2035,9 @@ import {
1998
2035
  } from "node:http";
1999
2036
  import { connect } from "node:net";
2000
2037
  import { Worker } from "node:worker_threads";
2038
+ import { lookup as dnsLookup } from "node:dns/promises";
2039
+ import { request as httpRequest } from "node:http";
2040
+ import { request as httpsRequest } from "node:https";
2001
2041
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2002
2042
  import { randomBytes as randomBytes2 } from "node:crypto";
2003
2043
  import { readFile as readFile2, realpath as realpath2, stat } from "node:fs/promises";
@@ -20701,6 +20741,131 @@ var require_x509_cjs = __commonJS({
20701
20741
  exports.idX448 = idX448;
20702
20742
  }
20703
20743
  });
20744
+ function parseIPv4(value) {
20745
+ const parts = value.split(".");
20746
+ if (parts.length !== 4) return null;
20747
+ const numbers = parts.map((part) => {
20748
+ if (!/^(?:0|[1-9]\d*)$/.test(part)) return -1;
20749
+ const number = Number(part);
20750
+ return Number.isInteger(number) && number >= 0 && number <= 255 ? number : -1;
20751
+ });
20752
+ return numbers.some((number) => number < 0) ? null : [numbers[0], numbers[1], numbers[2], numbers[3]];
20753
+ }
20754
+ function parseIPv6(value) {
20755
+ if (value.includes("%")) return null;
20756
+ const halves = value.split("::");
20757
+ if (halves.length > 2) return null;
20758
+ const left = halves[0] === "" ? [] : halves[0]?.split(":") ?? [];
20759
+ const right = halves.length === 2 && halves[1] !== "" ? halves[1]?.split(":") ?? [] : [];
20760
+ const parts = [...left, ...right];
20761
+ const words = [];
20762
+ for (const part of parts) {
20763
+ if (part.includes(".")) {
20764
+ const ipv4 = parseIPv4(part);
20765
+ if (ipv4 === null) return null;
20766
+ words.push(ipv4[0] << 8 | ipv4[1], ipv4[2] << 8 | ipv4[3]);
20767
+ continue;
20768
+ }
20769
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part)) return null;
20770
+ words.push(Number.parseInt(part, 16));
20771
+ }
20772
+ if (halves.length === 1) return words.length === 8 ? words : null;
20773
+ if (words.length >= 8) return null;
20774
+ return [
20775
+ ...words.slice(0, left.length),
20776
+ ...new Array(8 - words.length).fill(0),
20777
+ ...words.slice(left.length)
20778
+ ];
20779
+ }
20780
+ function ipv4Number(value) {
20781
+ return ((value[0] * 256 + value[1]) * 256 + value[2]) * 256 + value[3] >>> 0;
20782
+ }
20783
+ function inIPv4Range(value, start, end) {
20784
+ return value >= start && value <= end;
20785
+ }
20786
+ function prefixMatches(value, prefix, bits) {
20787
+ let remaining = bits;
20788
+ for (let index = 0; remaining > 0; index += 1) {
20789
+ const compareBits = Math.min(remaining, 16);
20790
+ const mask = compareBits === 16 ? 65535 : 65535 << 16 - compareBits & 65535;
20791
+ if (((value[index] ?? 0) & mask) !== ((prefix[index] ?? 0) & mask))
20792
+ return false;
20793
+ remaining -= compareBits;
20794
+ }
20795
+ return true;
20796
+ }
20797
+ function mappedIPv4(value) {
20798
+ if (value.slice(0, 5).some((word) => word !== 0) || value[5] !== 65535)
20799
+ return null;
20800
+ return [
20801
+ (value[6] ?? 0) >> 8,
20802
+ (value[6] ?? 0) & 255,
20803
+ (value[7] ?? 0) >> 8,
20804
+ (value[7] ?? 0) & 255
20805
+ ];
20806
+ }
20807
+ function classifyIPv4(value) {
20808
+ const number = ipv4Number(value);
20809
+ if (inIPv4Range(number, 0, 16777215)) return "unspecified";
20810
+ if (inIPv4Range(number, 167772160, 184549375) || inIPv4Range(number, 2886729728, 2887778303) || inIPv4Range(number, 3232235520, 3232301055))
20811
+ return "private";
20812
+ if (inIPv4Range(number, 2130706432, 2147483647)) return "loopback";
20813
+ if (inIPv4Range(number, 2851995648, 2852061183)) return "link-local";
20814
+ if (inIPv4Range(number, 1681915904, 1686110207)) return "carrier-grade";
20815
+ if (inIPv4Range(number, 3221225472, 3221225727) || inIPv4Range(number, 3221225984, 3221226239) || inIPv4Range(number, 3227017984, 3227018239))
20816
+ return "reserved";
20817
+ if (inIPv4Range(number, 3323068416, 3323199487)) return "benchmarking";
20818
+ if (inIPv4Range(number, 3325256704, 3325256959) || inIPv4Range(number, 3405803776, 3405804031))
20819
+ return "documentation";
20820
+ if (inIPv4Range(number, 3758096384, 4294967295)) return "multicast";
20821
+ return "public";
20822
+ }
20823
+ function classifyIPv6(value) {
20824
+ const mapped = mappedIPv4(value);
20825
+ if (mapped !== null) {
20826
+ return classifyIPv4(mapped);
20827
+ }
20828
+ if (value.every((word) => word === 0)) return "unspecified";
20829
+ if (value.slice(0, 7).every((word) => word === 0) && value[7] === 1)
20830
+ return "loopback";
20831
+ if (prefixMatches(value, [64512], 7)) return "private";
20832
+ if (prefixMatches(value, [65152], 10)) return "link-local";
20833
+ if (prefixMatches(value, [65280], 8)) return "multicast";
20834
+ if (prefixMatches(value, [8193, 3512], 32)) return "documentation";
20835
+ if (prefixMatches(value, [8193, 0], 32)) return "reserved";
20836
+ if (prefixMatches(value, [8193, 2, 0], 48)) return "benchmarking";
20837
+ if (prefixMatches(value, [8193, 16], 28)) return "reserved";
20838
+ if (prefixMatches(value, [100, 65435, 0, 0, 0, 0], 96))
20839
+ return "reserved";
20840
+ if (prefixMatches(value, [256], 8)) return "reserved";
20841
+ return "public";
20842
+ }
20843
+ function classifyAddress(value) {
20844
+ if (typeof value !== "string") return "invalid";
20845
+ const family = isIP(value);
20846
+ if (family === 4) {
20847
+ const parsed = parseIPv4(value);
20848
+ return parsed === null ? "invalid" : classifyIPv4(parsed);
20849
+ }
20850
+ if (family === 6) {
20851
+ const parsed = parseIPv6(value);
20852
+ return parsed === null ? "invalid" : classifyIPv6(parsed);
20853
+ }
20854
+ return "invalid";
20855
+ }
20856
+ function validateResolvedAddresses(addresses, maxAddresses) {
20857
+ if (addresses.length === 0 || addresses.length > maxAddresses) return null;
20858
+ const checked = [];
20859
+ for (const address of addresses) {
20860
+ if (address.family !== 4 && address.family !== 6 || classifyAddress(address.address) !== "public")
20861
+ return null;
20862
+ checked.push({ address: address.address, family: address.family });
20863
+ }
20864
+ checked.sort(
20865
+ (left, right) => left.family - right.family || (left.address < right.address ? -1 : left.address > right.address ? 1 : 0)
20866
+ );
20867
+ return checked;
20868
+ }
20704
20869
  var MAX_RETRIES = 2;
20705
20870
  var RETRY_DELAY_BASE = 1e3;
20706
20871
  var COMPLETE_TIMEOUT = 6e4;
@@ -22084,11 +22249,183 @@ async function rewriteRequestBody(input, action, captureContext) {
22084
22249
  }
22085
22250
  };
22086
22251
  }
22252
+ var defaultResolver = {
22253
+ async lookup(hostname, signal) {
22254
+ if (signal?.aborted) throw new Error("aborted");
22255
+ const addresses = await dnsLookup(hostname, { all: true });
22256
+ return addresses.map((address) => ({
22257
+ address: address.address,
22258
+ family: address.family
22259
+ }));
22260
+ }
22261
+ };
22262
+ function responseHeaders(response) {
22263
+ const headers = [];
22264
+ for (let index = 0; index < response.rawHeaders.length; index += 2) {
22265
+ const name = response.rawHeaders[index];
22266
+ const value = response.rawHeaders[index + 1];
22267
+ if (name !== void 0 && value !== void 0)
22268
+ headers.push([name.toLowerCase(), value]);
22269
+ }
22270
+ return headers;
22271
+ }
22272
+ var defaultTransport = {
22273
+ request(options) {
22274
+ return new Promise((resolve22, reject) => {
22275
+ const lookup = (_hostname, _options, callback) => {
22276
+ callback(null, options.address, options.address.includes(":") ? 6 : 4);
22277
+ };
22278
+ const requestOptions = {
22279
+ hostname: options.hostname,
22280
+ port: options.port,
22281
+ path: options.path,
22282
+ method: options.method,
22283
+ headers: options.headers,
22284
+ agent: false,
22285
+ lookup,
22286
+ signal: options.signal,
22287
+ setHost: false,
22288
+ autoSelectFamily: false
22289
+ };
22290
+ const request = options.protocol === "https:" ? httpsRequest(
22291
+ { ...requestOptions, servername: options.servername },
22292
+ (response) => {
22293
+ resolve22({
22294
+ status: response.statusCode ?? 0,
22295
+ headers: responseHeaders(response),
22296
+ body: response
22297
+ });
22298
+ }
22299
+ ) : httpRequest(requestOptions, (response) => {
22300
+ resolve22({
22301
+ status: response.statusCode ?? 0,
22302
+ headers: responseHeaders(response),
22303
+ body: response
22304
+ });
22305
+ });
22306
+ request.once("error", reject);
22307
+ request.end();
22308
+ });
22309
+ }
22310
+ };
22311
+ function headerValue(headers, name) {
22312
+ return headers.find(([headerName]) => headerName.toLowerCase() === name)?.[1];
22313
+ }
22314
+ function responseHeaderBytes(headers) {
22315
+ let bytes = 0;
22316
+ for (const [name, value] of headers)
22317
+ bytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
22318
+ return bytes;
22319
+ }
22320
+ function combineSignals(signal, timeoutMs) {
22321
+ const controller = new AbortController();
22322
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
22323
+ const abort = () => controller.abort();
22324
+ signal?.addEventListener("abort", abort, { once: true });
22325
+ return {
22326
+ signal: controller.signal,
22327
+ dispose: () => {
22328
+ clearTimeout(timer);
22329
+ signal?.removeEventListener("abort", abort);
22330
+ }
22331
+ };
22332
+ }
22333
+ async function fetchAuthorized(operation, options = {}) {
22334
+ if (operation.kind !== "outbound-http" || operation.method !== "GET" && operation.method !== "HEAD")
22335
+ return failure("runtime.unsupported-method");
22336
+ const target = canonicalizeOutboundTarget(operation.target);
22337
+ if (target === null || target !== operation.target)
22338
+ return failure("runtime.invalid-target");
22339
+ const url = new URL(target);
22340
+ const protocol = url.protocol === "https:" ? "https:" : "http:";
22341
+ const port = protocol === "https:" ? 443 : 80;
22342
+ const timeoutMs = options.operationTimeoutMs ?? RUNTIME_LIMITS.operationTimeoutMs;
22343
+ const combined = combineSignals(
22344
+ options.signal,
22345
+ Math.max(1, Math.min(timeoutMs, RUNTIME_LIMITS.operationTimeoutMs))
22346
+ );
22347
+ try {
22348
+ const resolver = options.resolver ?? defaultResolver;
22349
+ const resolved = await resolver.lookup(url.hostname, combined.signal);
22350
+ const addresses = validateResolvedAddresses(
22351
+ resolved,
22352
+ RUNTIME_LIMITS.maxDnsAddresses
22353
+ );
22354
+ if (addresses === null || addresses.length === 0)
22355
+ return failure("runtime.address-denied");
22356
+ const selected = addresses[0];
22357
+ const transport = options.transport ?? defaultTransport;
22358
+ let response;
22359
+ try {
22360
+ response = await transport.request({
22361
+ protocol,
22362
+ hostname: url.hostname,
22363
+ address: selected.address,
22364
+ port,
22365
+ path: `${url.pathname}${url.search}`,
22366
+ method: operation.method,
22367
+ headers: { host: url.host, "accept-encoding": "identity" },
22368
+ ...protocol === "https:" ? { servername: url.hostname } : {},
22369
+ signal: combined.signal
22370
+ });
22371
+ } catch {
22372
+ return combined.signal.aborted ? failure("runtime.timeout") : failure("runtime.internal");
22373
+ }
22374
+ if (combined.signal.aborted) return failure("runtime.timeout");
22375
+ if (response.status >= 300 && response.status <= 399)
22376
+ return failure("runtime.redirect-rejected");
22377
+ if (responseHeaderBytes(response.headers) > RUNTIME_LIMITS.maxResponseHeaderBytes)
22378
+ return failure("runtime.size-limit");
22379
+ const encoding = headerValue(response.headers, "content-encoding");
22380
+ if (encoding !== void 0 && encoding.toLowerCase() !== "identity")
22381
+ return failure("runtime.size-limit");
22382
+ const chunks = [];
22383
+ let total = 0;
22384
+ try {
22385
+ for await (const chunk of response.body) {
22386
+ if (combined.signal.aborted) return failure("runtime.timeout");
22387
+ const bytes = chunk instanceof Uint8Array ? Uint8Array.from(chunk) : Uint8Array.from(chunk);
22388
+ total += bytes.byteLength;
22389
+ if (total > RUNTIME_LIMITS.maxResponseBodyBytes)
22390
+ return failure("runtime.size-limit");
22391
+ chunks.push(bytes);
22392
+ }
22393
+ } catch {
22394
+ return combined.signal.aborted ? failure("runtime.timeout") : failure("runtime.internal");
22395
+ }
22396
+ const body = new Uint8Array(total);
22397
+ let offset = 0;
22398
+ for (const chunk of chunks) {
22399
+ body.set(chunk, offset);
22400
+ offset += chunk.byteLength;
22401
+ }
22402
+ return {
22403
+ ok: true,
22404
+ value: { status: response.status, headers: response.headers, body }
22405
+ };
22406
+ } catch {
22407
+ return combined.signal.aborted ? failure("runtime.timeout") : failure("runtime.dns-failed");
22408
+ } finally {
22409
+ combined.dispose();
22410
+ }
22411
+ }
22087
22412
  function supportedContentType2(value) {
22088
22413
  if (value === void 0) return false;
22089
22414
  const mediaType = value.split(";", 1)[0]?.trim().toLowerCase() ?? "";
22090
22415
  return mediaType.startsWith("text/") || mediaType === "application/json" || mediaType === "application/javascript" || mediaType === "text/javascript" || mediaType === "text/css" || mediaType === "application/xml" || mediaType === "text/xml";
22091
22416
  }
22417
+ function responseHeader(headers, name) {
22418
+ return headers.find(([key]) => key.toLowerCase() === name)?.[1];
22419
+ }
22420
+ function responseBodyReplacements(action) {
22421
+ if ("replacements" in action && Array.isArray(action.replacements)) {
22422
+ return action.replacements;
22423
+ }
22424
+ return [];
22425
+ }
22426
+ function isReplaceAction(action) {
22427
+ return "mode" in action && action.mode === "replace";
22428
+ }
22092
22429
  async function rewriteResponseBody(input, replacements, replaceAction, captureContext) {
22093
22430
  if (!supportedContentType2(input.contentType) || input.contentType === void 0)
22094
22431
  return failure("runtime.size-limit");
@@ -22134,6 +22471,78 @@ async function rewriteResponseBody(input, replacements, replaceAction, captureCo
22134
22471
  }
22135
22472
  };
22136
22473
  }
22474
+ function replaceResponseBody(body, contentType, captureContext) {
22475
+ const captures = captureContext === void 0 ? [] : matchUrlCaptures(captureContext.urlRegex, captureContext.url);
22476
+ if (captureContext !== void 0 && captures === null) {
22477
+ return failure("runtime.url-capture-mismatch");
22478
+ }
22479
+ const expanded = captureContext === void 0 ? body : substituteUrlCaptures(body, captures ?? []);
22480
+ const encoded = new TextEncoder().encode(expanded);
22481
+ if (encoded.byteLength > RUNTIME_LIMITS.maxResponseBodyBytes)
22482
+ return failure("runtime.size-limit");
22483
+ return {
22484
+ ok: true,
22485
+ value: {
22486
+ body: encoded,
22487
+ contentType: contentType?.split(";", 1)[0]?.trim() ?? "text/plain"
22488
+ }
22489
+ };
22490
+ }
22491
+ async function fetchAndRewriteAuthorizedResponse(operation, action, options = {}) {
22492
+ if (operation.kind !== "outbound-http" || operation.method !== "GET")
22493
+ return failure("runtime.unsupported-method");
22494
+ const response = await fetchAuthorized(operation, options);
22495
+ if (!response.ok) return response;
22496
+ const contentType = responseHeader(response.value.headers, "content-type");
22497
+ const transformed = isReplaceAction(action) ? replaceResponseBody(
22498
+ action.body,
22499
+ contentType,
22500
+ operation.sourceValue === void 0 ? void 0 : { url: operation.target, urlRegex: operation.sourceValue }
22501
+ ) : await rewriteResponseBody(
22502
+ {
22503
+ contentType,
22504
+ contentEncoding: responseHeader(
22505
+ response.value.headers,
22506
+ "content-encoding"
22507
+ ),
22508
+ body: response.value.body
22509
+ },
22510
+ responseBodyReplacements(action)
22511
+ );
22512
+ if (!transformed.ok) return transformed;
22513
+ return {
22514
+ ok: true,
22515
+ value: {
22516
+ ...transformed.value,
22517
+ status: response.value.status,
22518
+ headers: response.value.headers
22519
+ }
22520
+ };
22521
+ }
22522
+ var RESPONSE_BODY_LISTENER_PREFIX = "/.rogatio/body/";
22523
+ function parseResponseBodyRedirect(rawUrl) {
22524
+ const queryAt = rawUrl.indexOf("?");
22525
+ const path2 = queryAt === -1 ? rawUrl : rawUrl.slice(0, queryAt);
22526
+ const search = queryAt === -1 ? "" : rawUrl.slice(queryAt);
22527
+ if (!path2.startsWith(RESPONSE_BODY_LISTENER_PREFIX)) return null;
22528
+ const rest = path2.slice(RESPONSE_BODY_LISTENER_PREFIX.length);
22529
+ const slash1 = rest.indexOf("/");
22530
+ if (slash1 <= 0) return null;
22531
+ const slash2 = rest.indexOf("/", slash1 + 1);
22532
+ if (slash2 <= slash1 + 1) return null;
22533
+ let ruleId;
22534
+ let digest;
22535
+ try {
22536
+ ruleId = decodeURIComponent(rest.slice(0, slash1));
22537
+ digest = decodeURIComponent(rest.slice(slash1 + 1, slash2));
22538
+ } catch {
22539
+ return null;
22540
+ }
22541
+ if (ruleId.length === 0 || digest.length === 0) return null;
22542
+ const original = rest.slice(slash2 + 1) + search;
22543
+ if (!/^https?:\/\//i.test(original)) return null;
22544
+ return { ruleId, digest, originalUrl: original };
22545
+ }
22137
22546
  function originOf(value) {
22138
22547
  const direct = normalizeSiteOrigin(value);
22139
22548
  if (direct !== null) return direct;
@@ -22445,6 +22854,9 @@ function writeResponse(res, status, statusMessage, headers, body) {
22445
22854
  res.writeHead(status, statusMessage, out);
22446
22855
  res.end(body);
22447
22856
  }
22857
+ function asPresetDigest(value) {
22858
+ return value.startsWith("sha256:") ? value : null;
22859
+ }
22448
22860
  function initiatorFromHeaders(headers, targetUrl) {
22449
22861
  const origin = headerValue2(headers, "origin");
22450
22862
  if (typeof origin === "string" && /^https?:\/\//i.test(origin)) {
@@ -22487,6 +22899,10 @@ async function startInterceptProxy(options = {}) {
22487
22899
  async function handleHttp(req, res) {
22488
22900
  try {
22489
22901
  const targetUrl = req.url ?? "";
22902
+ if (targetUrl.startsWith("/.rogatio/body/")) {
22903
+ await handleResponseBodyRedirect(req, res, targetUrl);
22904
+ return;
22905
+ }
22490
22906
  if (!/^https?:\/\//i.test(targetUrl)) {
22491
22907
  res.writeHead(400);
22492
22908
  res.end();
@@ -22602,6 +23018,96 @@ async function startInterceptProxy(options = {}) {
22602
23018
  }
22603
23019
  }
22604
23020
  }
23021
+ async function handleResponseBodyRedirect(req, res, rawUrl) {
23022
+ const method = (req.method ?? "GET").toUpperCase();
23023
+ if (method !== "GET") {
23024
+ res.writeHead(405);
23025
+ res.end();
23026
+ return;
23027
+ }
23028
+ const parsed = parseResponseBodyRedirect(rawUrl);
23029
+ const active = policy;
23030
+ if (parsed === null || active === null || active.presetDigest === void 0) {
23031
+ res.writeHead(parsed === null ? 400 : 403);
23032
+ res.end();
23033
+ return;
23034
+ }
23035
+ const presetDigest = asPresetDigest(active.presetDigest);
23036
+ if (presetDigest === null || parsed.digest !== presetDigest) {
23037
+ res.writeHead(403);
23038
+ res.end();
23039
+ return;
23040
+ }
23041
+ const operation = active.operations.find(
23042
+ (op) => op.kind === "response-body" && op.ruleId === parsed.ruleId
23043
+ );
23044
+ if (operation === void 0 || !sourceMatches(operation.matcher.source, parsed.originalUrl)) {
23045
+ res.writeHead(404);
23046
+ res.end();
23047
+ return;
23048
+ }
23049
+ if (operation.matcher.method !== void 0 && operation.matcher.method !== "GET") {
23050
+ res.writeHead(405);
23051
+ res.end();
23052
+ return;
23053
+ }
23054
+ const target = canonicalizeOutboundTarget(parsed.originalUrl);
23055
+ if (target === null) {
23056
+ res.writeHead(400);
23057
+ res.end();
23058
+ return;
23059
+ }
23060
+ const allowed = revalidateAuthority(active.project, active.operations, {
23061
+ groupId: operation.groupId,
23062
+ ruleId: operation.ruleId,
23063
+ url: parsed.originalUrl,
23064
+ target,
23065
+ method: "GET",
23066
+ initiator: initiatorFromHeaders(req.headers, parsed.originalUrl) ?? new URL(target).origin,
23067
+ resourceType: operation.matcher.resourceTypes[0]
23068
+ });
23069
+ if (!allowed.allowed) {
23070
+ res.writeHead(403);
23071
+ res.end();
23072
+ return;
23073
+ }
23074
+ const fetched = await fetchAndRewriteAuthorizedResponse(
23075
+ {
23076
+ groupId: operation.groupId,
23077
+ ruleId: operation.ruleId,
23078
+ operationId: operation.ruleId,
23079
+ kind: "outbound-http",
23080
+ target,
23081
+ method: "GET",
23082
+ sourceValue: operation.matcher.source.value,
23083
+ presetDigest
23084
+ },
23085
+ operation.responseBody
23086
+ );
23087
+ if (!fetched.ok) {
23088
+ res.writeHead(502);
23089
+ res.end();
23090
+ return;
23091
+ }
23092
+ const headers = {};
23093
+ for (const [name, value] of fetched.value.headers) {
23094
+ const lower = name.toLowerCase();
23095
+ if (lower === "set-cookie" || lower === "cookie" || lower === "authorization" || lower === "proxy-authorization") {
23096
+ continue;
23097
+ }
23098
+ const existing = headers[lower];
23099
+ if (existing === void 0) headers[lower] = value;
23100
+ else if (Array.isArray(existing)) existing.push(value);
23101
+ else headers[lower] = [existing, value];
23102
+ }
23103
+ writeResponse(
23104
+ res,
23105
+ fetched.value.status,
23106
+ fetched.value.status === 200 ? "OK" : "Upstream",
23107
+ headers,
23108
+ Buffer.from(fetched.value.body)
23109
+ );
23110
+ }
22605
23111
  function handleConnect(req, clientSocket, head) {
22606
23112
  const authority = req.url ?? "";
22607
23113
  const [host, portText] = authority.split(":");
@@ -22646,12 +23152,72 @@ async function startInterceptProxy(options = {}) {
22646
23152
  }
22647
23153
  };
22648
23154
  }
23155
+ function steerKey(origin) {
23156
+ const port = origin.port !== void 0 ? `:${origin.port}` : "";
23157
+ return `steer:${origin.scheme}://${origin.host}${port}`;
23158
+ }
23159
+ function pacRouteFromEntry(entry) {
23160
+ const steer = decodePacSteer(entry);
23161
+ if (steer !== null) return { steer };
23162
+ if (entry.includes("://")) {
23163
+ return isExactHttpUrl(entry) ? { url: entry } : null;
23164
+ }
23165
+ if (entry.length === 0) return null;
23166
+ return { hostname: entry };
23167
+ }
23168
+ function pacCheck(route) {
23169
+ if (typeof route.url === "string" && isExactHttpUrl(route.url)) {
23170
+ return {
23171
+ key: `url:${route.url}`,
23172
+ line: `url === ${JSON.stringify(route.url)}`
23173
+ };
23174
+ }
23175
+ if (route.steer !== void 0) {
23176
+ return {
23177
+ key: steerKey(route.steer),
23178
+ line: steerPredicate(route.steer)
23179
+ };
23180
+ }
23181
+ if (typeof route.hostname === "string" && route.hostname.length > 0 && !route.hostname.includes("://")) {
23182
+ return {
23183
+ key: `host:${route.hostname}`,
23184
+ line: `host === ${JSON.stringify(route.hostname)}`
23185
+ };
23186
+ }
23187
+ return null;
23188
+ }
23189
+ function steerPredicate(origin) {
23190
+ const hostCheck = `host === ${JSON.stringify(origin.host)}`;
23191
+ const base = origin.port !== void 0 ? `${origin.scheme}://${origin.host}:${origin.port}` : `${origin.scheme}://${origin.host}`;
23192
+ if (origin.port !== void 0) {
23193
+ const withSlash = JSON.stringify(`${base}/`);
23194
+ const exact = JSON.stringify(base);
23195
+ const withQuery = JSON.stringify(`${base}?`);
23196
+ return `${hostCheck} && (url.indexOf(${withSlash}) === 0 || url === ${exact} || url.indexOf(${withQuery}) === 0)`;
23197
+ }
23198
+ return `${hostCheck} && url.indexOf(${JSON.stringify(base)}) === 0`;
23199
+ }
23200
+ function isExactHttpUrl(value) {
23201
+ try {
23202
+ const url = new URL(value);
23203
+ return (url.protocol === "http:" || url.protocol === "https:") && url.username === "" && url.password === "" && url.href === value;
23204
+ } catch {
23205
+ return false;
23206
+ }
23207
+ }
22649
23208
  function generatePacScript(routes, endpoint, options) {
22650
23209
  if (!Array.isArray(routes)) {
22651
23210
  throw new Error("runtime.pac-route-limit");
22652
23211
  }
22653
- const unique = routes.map((route) => route.hostname).filter((hostname) => typeof hostname === "string" && hostname.length > 0);
22654
- const sorted = [...new Set(unique)].sort();
23212
+ const checksByKey = /* @__PURE__ */ new Map();
23213
+ for (const route of routes) {
23214
+ const check = pacCheck(route);
23215
+ if (check === null) continue;
23216
+ checksByKey.set(check.key, check.line);
23217
+ }
23218
+ const sorted = [...checksByKey.entries()].sort(
23219
+ ([left], [right]) => left.localeCompare(right)
23220
+ );
22655
23221
  if (sorted.length > MAX_PAC_ROUTES) {
22656
23222
  throw new Error("runtime.pac-route-limit");
22657
23223
  }
@@ -22659,7 +23225,7 @@ function generatePacScript(routes, endpoint, options) {
22659
23225
  const proxy = `${proxyType} ${endpoint.host}:${endpoint.port}`;
22660
23226
  const proxyLiteral = JSON.stringify(proxy);
22661
23227
  const checks = sorted.map(
22662
- (hostname) => ` if (host === ${JSON.stringify(hostname)}) {
23228
+ ([, predicate]) => ` if (${predicate}) {
22663
23229
  return ${proxyLiteral};
22664
23230
  }`
22665
23231
  );
@@ -22752,6 +23318,7 @@ function unsupportedReasons(capabilities) {
22752
23318
  function createPlatformInterceptionProvider(adapter) {
22753
23319
  let state = "stopped";
22754
23320
  let active = false;
23321
+ let pacInstalled = false;
22755
23322
  return {
22756
23323
  platform: adapter.platform,
22757
23324
  detect() {
@@ -22763,6 +23330,17 @@ function createPlatformInterceptionProvider(adapter) {
22763
23330
  };
22764
23331
  },
22765
23332
  async start(activation, pacRoutes) {
23333
+ const decoded = pacRoutes.flatMap((entry) => {
23334
+ const route = pacRouteFromEntry(entry);
23335
+ return route === null ? [] : [route];
23336
+ });
23337
+ if (pacRoutes.length === 0) {
23338
+ const endpoint2 = await adapter.startTlsProxy(activation);
23339
+ active = true;
23340
+ pacInstalled = false;
23341
+ state = "running";
23342
+ return endpoint2;
23343
+ }
22766
23344
  const capabilities = adapter.detect();
22767
23345
  const reasons = unsupportedReasons(capabilities);
22768
23346
  if (!capabilities.supported || reasons.length > 0) {
@@ -22774,18 +23352,17 @@ function createPlatformInterceptionProvider(adapter) {
22774
23352
  throw new Error("runtime.platform-unsupported");
22775
23353
  }
22776
23354
  const endpoint = await adapter.startTlsProxy(activation);
22777
- const pac = generatePacScript(
22778
- pacRoutes.map((hostname) => ({ hostname })),
22779
- endpoint
22780
- );
23355
+ const pac = generatePacScript(decoded, endpoint);
22781
23356
  try {
22782
23357
  await adapter.installPac(pac);
22783
23358
  active = true;
23359
+ pacInstalled = true;
22784
23360
  state = "running";
22785
23361
  return endpoint;
22786
23362
  } catch (error) {
22787
23363
  await adapter.stopTlsProxy();
22788
23364
  active = false;
23365
+ pacInstalled = false;
22789
23366
  state = "stopped";
22790
23367
  throw error;
22791
23368
  }
@@ -22795,9 +23372,10 @@ function createPlatformInterceptionProvider(adapter) {
22795
23372
  state = state === "unsupported" ? "unsupported" : "stopped";
22796
23373
  return;
22797
23374
  }
22798
- await adapter.removePac();
23375
+ if (pacInstalled) await adapter.removePac();
22799
23376
  await adapter.stopTlsProxy();
22800
23377
  active = false;
23378
+ pacInstalled = false;
22801
23379
  state = "stopped";
22802
23380
  },
22803
23381
  status() {
@@ -23397,7 +23975,7 @@ function createNativeRuntimeController(options) {
23397
23975
  return aiConfigInForce;
23398
23976
  }
23399
23977
  async function bindInterception(current, sessionConfig) {
23400
- if (sessionConfig.pacRoutes.length === 0) {
23978
+ if (sessionConfig.pacRoutes.length === 0 && sessionConfig.contentListener !== true) {
23401
23979
  return {
23402
23980
  activation: { ...current, pacRoutes: [] },
23403
23981
  session: null,
@@ -23606,7 +24184,8 @@ function createNativeRuntimeController(options) {
23606
24184
  preset = normalized.value;
23607
24185
  activePolicy = {
23608
24186
  project: schemaResult.data,
23609
- operations: compileResult.operations
24187
+ operations: compileResult.operations,
24188
+ presetDigest: preset.digest
23610
24189
  };
23611
24190
  const startResult = await this.start();
23612
24191
  if (startResult.state !== "running") {
@@ -23654,6 +24233,7 @@ function createNativeRuntimeController(options) {
23654
24233
  policyDigest: typeof meta.policyDigest === "string" ? meta.policyDigest : "",
23655
24234
  extensionId: typeof meta.extensionId === "string" ? meta.extensionId : "",
23656
24235
  pacRoutes: Array.isArray(meta.pacRoutes) ? meta.pacRoutes : [],
24236
+ contentListener: meta.contentListener === true,
23657
24237
  targetPolicy: {
23658
24238
  public: meta.targetPolicy?.public === true || meta.targetPolicy?.publicAllowed === true,
23659
24239
  localOrigins: Array.isArray(meta.targetPolicy?.localOrigins) ? meta.targetPolicy.localOrigins : []
@@ -24507,7 +25087,11 @@ function createNativeHost(options) {
24507
25087
  }
24508
25088
  const policy = controller.getActivePolicy();
24509
25089
  interceptProxy = await startInterceptProxy({
24510
- policy: policy ? { project: policy.project, operations: policy.operations } : null
25090
+ policy: policy ? {
25091
+ project: policy.project,
25092
+ operations: policy.operations,
25093
+ presetDigest: policy.presetDigest
25094
+ } : null
24511
25095
  });
24512
25096
  return interceptProxy.endpoint;
24513
25097
  },
@@ -24528,7 +25112,11 @@ function createNativeHost(options) {
24528
25112
  const endpoint = await platformProvider.start(activation, routes);
24529
25113
  const policy = controller.getActivePolicy();
24530
25114
  interceptProxy?.setPolicy(
24531
- policy ? { project: policy.project, operations: policy.operations } : null
25115
+ policy ? {
25116
+ project: policy.project,
25117
+ operations: policy.operations,
25118
+ presetDigest: policy.presetDigest
25119
+ } : null
24532
25120
  );
24533
25121
  return endpoint;
24534
25122
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.4.0",
3
+ "version": "6.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first browser request and response rules — editor host, file verification, test runner, and runtime dispatch.",