@rogatio/cli 6.4.1 → 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 +567 -11
  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,6 +23152,19 @@ 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
+ }
22649
23168
  function pacCheck(route) {
22650
23169
  if (typeof route.url === "string" && isExactHttpUrl(route.url)) {
22651
23170
  return {
@@ -22653,6 +23172,12 @@ function pacCheck(route) {
22653
23172
  line: `url === ${JSON.stringify(route.url)}`
22654
23173
  };
22655
23174
  }
23175
+ if (route.steer !== void 0) {
23176
+ return {
23177
+ key: steerKey(route.steer),
23178
+ line: steerPredicate(route.steer)
23179
+ };
23180
+ }
22656
23181
  if (typeof route.hostname === "string" && route.hostname.length > 0 && !route.hostname.includes("://")) {
22657
23182
  return {
22658
23183
  key: `host:${route.hostname}`,
@@ -22661,6 +23186,17 @@ function pacCheck(route) {
22661
23186
  }
22662
23187
  return null;
22663
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
+ }
22664
23200
  function isExactHttpUrl(value) {
22665
23201
  try {
22666
23202
  const url = new URL(value);
@@ -22782,6 +23318,7 @@ function unsupportedReasons(capabilities) {
22782
23318
  function createPlatformInterceptionProvider(adapter) {
22783
23319
  let state = "stopped";
22784
23320
  let active = false;
23321
+ let pacInstalled = false;
22785
23322
  return {
22786
23323
  platform: adapter.platform,
22787
23324
  detect() {
@@ -22793,6 +23330,17 @@ function createPlatformInterceptionProvider(adapter) {
22793
23330
  };
22794
23331
  },
22795
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
+ }
22796
23344
  const capabilities = adapter.detect();
22797
23345
  const reasons = unsupportedReasons(capabilities);
22798
23346
  if (!capabilities.supported || reasons.length > 0) {
@@ -22804,20 +23352,17 @@ function createPlatformInterceptionProvider(adapter) {
22804
23352
  throw new Error("runtime.platform-unsupported");
22805
23353
  }
22806
23354
  const endpoint = await adapter.startTlsProxy(activation);
22807
- const pac = generatePacScript(
22808
- pacRoutes.map(
22809
- (entry) => entry.includes("://") ? { url: entry } : { hostname: entry }
22810
- ),
22811
- endpoint
22812
- );
23355
+ const pac = generatePacScript(decoded, endpoint);
22813
23356
  try {
22814
23357
  await adapter.installPac(pac);
22815
23358
  active = true;
23359
+ pacInstalled = true;
22816
23360
  state = "running";
22817
23361
  return endpoint;
22818
23362
  } catch (error) {
22819
23363
  await adapter.stopTlsProxy();
22820
23364
  active = false;
23365
+ pacInstalled = false;
22821
23366
  state = "stopped";
22822
23367
  throw error;
22823
23368
  }
@@ -22827,9 +23372,10 @@ function createPlatformInterceptionProvider(adapter) {
22827
23372
  state = state === "unsupported" ? "unsupported" : "stopped";
22828
23373
  return;
22829
23374
  }
22830
- await adapter.removePac();
23375
+ if (pacInstalled) await adapter.removePac();
22831
23376
  await adapter.stopTlsProxy();
22832
23377
  active = false;
23378
+ pacInstalled = false;
22833
23379
  state = "stopped";
22834
23380
  },
22835
23381
  status() {
@@ -23429,7 +23975,7 @@ function createNativeRuntimeController(options) {
23429
23975
  return aiConfigInForce;
23430
23976
  }
23431
23977
  async function bindInterception(current, sessionConfig) {
23432
- if (sessionConfig.pacRoutes.length === 0) {
23978
+ if (sessionConfig.pacRoutes.length === 0 && sessionConfig.contentListener !== true) {
23433
23979
  return {
23434
23980
  activation: { ...current, pacRoutes: [] },
23435
23981
  session: null,
@@ -23638,7 +24184,8 @@ function createNativeRuntimeController(options) {
23638
24184
  preset = normalized.value;
23639
24185
  activePolicy = {
23640
24186
  project: schemaResult.data,
23641
- operations: compileResult.operations
24187
+ operations: compileResult.operations,
24188
+ presetDigest: preset.digest
23642
24189
  };
23643
24190
  const startResult = await this.start();
23644
24191
  if (startResult.state !== "running") {
@@ -23686,6 +24233,7 @@ function createNativeRuntimeController(options) {
23686
24233
  policyDigest: typeof meta.policyDigest === "string" ? meta.policyDigest : "",
23687
24234
  extensionId: typeof meta.extensionId === "string" ? meta.extensionId : "",
23688
24235
  pacRoutes: Array.isArray(meta.pacRoutes) ? meta.pacRoutes : [],
24236
+ contentListener: meta.contentListener === true,
23689
24237
  targetPolicy: {
23690
24238
  public: meta.targetPolicy?.public === true || meta.targetPolicy?.publicAllowed === true,
23691
24239
  localOrigins: Array.isArray(meta.targetPolicy?.localOrigins) ? meta.targetPolicy.localOrigins : []
@@ -24539,7 +25087,11 @@ function createNativeHost(options) {
24539
25087
  }
24540
25088
  const policy = controller.getActivePolicy();
24541
25089
  interceptProxy = await startInterceptProxy({
24542
- 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
24543
25095
  });
24544
25096
  return interceptProxy.endpoint;
24545
25097
  },
@@ -24560,7 +25112,11 @@ function createNativeHost(options) {
24560
25112
  const endpoint = await platformProvider.start(activation, routes);
24561
25113
  const policy = controller.getActivePolicy();
24562
25114
  interceptProxy?.setPolicy(
24563
- policy ? { project: policy.project, operations: policy.operations } : null
25115
+ policy ? {
25116
+ project: policy.project,
25117
+ operations: policy.operations,
25118
+ presetDigest: policy.presetDigest
25119
+ } : null
24564
25120
  );
24565
25121
  return endpoint;
24566
25122
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.4.1",
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.",