@rogatio/cli 6.16.2 → 6.16.3

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 +231 -45
  2. package/package.json +1 -1
@@ -2301,6 +2301,7 @@ import { isAbsolute as isAbsolute3 } from "node:path";
2301
2301
  import { Buffer as Buffer2 } from "node:buffer";
2302
2302
  import { existsSync as existsSync2 } from "node:fs";
2303
2303
  import { join as join6 } from "node:path";
2304
+ import { lookup as dnsLookup2 } from "node:dns/promises";
2304
2305
  import {
2305
2306
  createServer as createServer2
2306
2307
  } from "node:http";
@@ -25131,6 +25132,133 @@ function forwardRequestHeaders(headers, host, contentLength) {
25131
25132
  }
25132
25133
  return out;
25133
25134
  }
25135
+ var defaultResolver2 = {
25136
+ async lookup(hostname, signal) {
25137
+ if (signal?.aborted) throw new Error("aborted");
25138
+ const addresses = await dnsLookup2(hostname, { all: true });
25139
+ return addresses.map((address) => ({
25140
+ address: address.address,
25141
+ family: address.family === 6 ? 6 : 4
25142
+ }));
25143
+ }
25144
+ };
25145
+ var defaultDial = (port, address) => connect(port, address);
25146
+ function localOriginsFromProject(project) {
25147
+ if (project === null || typeof project !== "object") return [];
25148
+ const policy = project.requestBodyPolicy;
25149
+ if (policy === null || typeof policy !== "object") return [];
25150
+ const origins = policy.localOrigins;
25151
+ if (!Array.isArray(origins)) return [];
25152
+ return origins.filter(
25153
+ (origin) => typeof origin === "string"
25154
+ );
25155
+ }
25156
+ function effectiveLocalOrigins(policy) {
25157
+ if (policy === null) return /* @__PURE__ */ new Set();
25158
+ const raw = policy.localOrigins ?? localOriginsFromProject(policy.project);
25159
+ const origins = /* @__PURE__ */ new Set();
25160
+ for (const origin of raw) {
25161
+ const normalized = normalizeSiteOrigin(origin);
25162
+ if (normalized !== null) origins.add(normalized);
25163
+ }
25164
+ return origins;
25165
+ }
25166
+ function compareAddresses(left, right) {
25167
+ return left.family - right.family || (left.address < right.address ? -1 : left.address > right.address ? 1 : 0);
25168
+ }
25169
+ function selectPinnedAddress(addresses, allowNonPublic) {
25170
+ const publicOnly = validateResolvedAddresses(
25171
+ addresses,
25172
+ RUNTIME_LIMITS.maxDnsAddresses
25173
+ );
25174
+ if (publicOnly !== null) return publicOnly[0] ?? null;
25175
+ if (!allowNonPublic) return null;
25176
+ if (addresses.length === 0 || addresses.length > RUNTIME_LIMITS.maxDnsAddresses) {
25177
+ return null;
25178
+ }
25179
+ const checked = [];
25180
+ for (const address of addresses) {
25181
+ if (address.family !== 4 && address.family !== 6) return null;
25182
+ const classification = classifyAddress(address.address);
25183
+ if (classification === "invalid" || classification === "public") {
25184
+ return null;
25185
+ }
25186
+ checked.push({ address: address.address, family: address.family });
25187
+ }
25188
+ checked.sort(compareAddresses);
25189
+ return checked[0] ?? null;
25190
+ }
25191
+ async function pinHostname(hostname, origin, deps) {
25192
+ if (hostname.length === 0) return null;
25193
+ let resolved;
25194
+ try {
25195
+ resolved = await deps.resolver.lookup(hostname);
25196
+ } catch {
25197
+ return null;
25198
+ }
25199
+ const normalized = normalizeSiteOrigin(origin);
25200
+ const allowNonPublic = normalized !== null && deps.localOrigins.has(normalized);
25201
+ return selectPinnedAddress(resolved, allowNonPublic);
25202
+ }
25203
+ function httpUpstreamPort(target) {
25204
+ if (target.protocol !== "http:") return null;
25205
+ if (target.username.length > 0 || target.password.length > 0) return null;
25206
+ if (target.port.length === 0 || target.port === "80") return 80;
25207
+ return null;
25208
+ }
25209
+ function formatUpstreamRequest(method, path2, headers) {
25210
+ const lines = [];
25211
+ for (const [name, value] of Object.entries(headers)) {
25212
+ if (name.includes("\r") || name.includes("\n") || value.includes("\r") || value.includes("\n")) {
25213
+ return null;
25214
+ }
25215
+ lines.push(`${name}: ${value}`);
25216
+ }
25217
+ return `${method} ${path2} HTTP/1.1\r
25218
+ ${lines.join("\r\n")}\r
25219
+ \r
25220
+ `;
25221
+ }
25222
+ function httpsOrigin(host) {
25223
+ const authority = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
25224
+ try {
25225
+ return new URL(`https://${authority}`).origin;
25226
+ } catch {
25227
+ return null;
25228
+ }
25229
+ }
25230
+ function parseConnectAuthority(authority) {
25231
+ if (authority.length === 0) return null;
25232
+ let host;
25233
+ let portText;
25234
+ if (authority.startsWith("[")) {
25235
+ const end = authority.indexOf("]");
25236
+ if (end <= 1) return null;
25237
+ host = authority.slice(1, end);
25238
+ const rest = authority.slice(end + 1);
25239
+ if (!rest.startsWith(":")) return null;
25240
+ portText = rest.slice(1);
25241
+ } else {
25242
+ const colon = authority.lastIndexOf(":");
25243
+ if (colon <= 0) return null;
25244
+ host = authority.slice(0, colon);
25245
+ portText = authority.slice(colon + 1);
25246
+ }
25247
+ if (host.length === 0 || !/^\d+$/.test(portText)) return null;
25248
+ const port = Number(portText);
25249
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
25250
+ return { host, port };
25251
+ }
25252
+ function endConnect(socket, status, reason) {
25253
+ if (socket.destroyed || socket.writableEnded) return;
25254
+ socket.end(
25255
+ `HTTP/1.1 ${status} ${reason}\r
25256
+ Content-Length: 0\r
25257
+ Connection: close\r
25258
+ \r
25259
+ `
25260
+ );
25261
+ }
25134
25262
  function pipeSockets(a, b) {
25135
25263
  a.pipe(b);
25136
25264
  b.pipe(a);
@@ -25143,11 +25271,16 @@ function pipeSockets(a, b) {
25143
25271
  a.on("close", () => b.destroy());
25144
25272
  b.on("close", () => a.destroy());
25145
25273
  }
25146
- async function upstreamHttp(method, target, headers, body) {
25147
- const port = target.port ? Number(target.port) : 80;
25274
+ async function upstreamHttp(method, target, headers, body, deps) {
25275
+ const port = httpUpstreamPort(target);
25276
+ if (port === null) return { ok: false, status: 403 };
25148
25277
  const path2 = `${target.pathname}${target.search}`;
25278
+ const head = formatUpstreamRequest(method, path2, headers);
25279
+ if (head === null) return { ok: false, status: 403 };
25280
+ const pinned = await pinHostname(target.hostname, target.origin, deps);
25281
+ if (pinned === null) return { ok: false, status: 403 };
25149
25282
  return new Promise((resolve12, reject) => {
25150
- const socket = connect(port, target.hostname);
25283
+ const socket = deps.dial(port, pinned.address);
25151
25284
  let buf = Buffer.alloc(0);
25152
25285
  let settled = false;
25153
25286
  const fail2 = (err) => {
@@ -25160,11 +25293,7 @@ async function upstreamHttp(method, target, headers, body) {
25160
25293
  socket.on("timeout", () => fail2(new Error("upstream timeout")));
25161
25294
  socket.on("error", (err) => fail2(err));
25162
25295
  socket.on("connect", () => {
25163
- const lines = Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join("\r\n");
25164
- socket.write(`${method} ${path2} HTTP/1.1\r
25165
- ${lines}\r
25166
- \r
25167
- `);
25296
+ socket.write(head);
25168
25297
  if (body.byteLength > 0) socket.write(body);
25169
25298
  });
25170
25299
  socket.on("data", (chunk) => {
@@ -25172,9 +25301,9 @@ ${lines}\r
25172
25301
  buf = Buffer.concat([buf, piece]);
25173
25302
  const sep = buf.indexOf("\r\n\r\n");
25174
25303
  if (sep < 0) return;
25175
- const head = buf.subarray(0, sep).toString("latin1");
25304
+ const head2 = buf.subarray(0, sep).toString("latin1");
25176
25305
  const statusMatch = /^HTTP\/1\.[01] (\d+)(?: (.*))?/.exec(
25177
- head.split("\r\n")[0] ?? ""
25306
+ head2.split("\r\n")[0] ?? ""
25178
25307
  );
25179
25308
  if (!statusMatch) {
25180
25309
  fail2(new Error("bad upstream status"));
@@ -25183,7 +25312,7 @@ ${lines}\r
25183
25312
  const status = Number(statusMatch[1]);
25184
25313
  const statusMessage = statusMatch[2]?.trim() ?? "";
25185
25314
  const headerMap = {};
25186
- for (const line of head.split("\r\n").slice(1)) {
25315
+ for (const line of head2.split("\r\n").slice(1)) {
25187
25316
  const i = line.indexOf(":");
25188
25317
  if (i <= 0) continue;
25189
25318
  const name = line.slice(0, i).trim().toLowerCase();
@@ -25203,6 +25332,7 @@ ${lines}\r
25203
25332
  settled = true;
25204
25333
  socket.destroy();
25205
25334
  resolve12({
25335
+ ok: true,
25206
25336
  status,
25207
25337
  statusMessage,
25208
25338
  headers: headerMap,
@@ -25218,16 +25348,16 @@ ${lines}\r
25218
25348
  fail2(new Error("upstream closed early"));
25219
25349
  return;
25220
25350
  }
25221
- const head = buf.subarray(0, sep).toString("latin1");
25351
+ const head2 = buf.subarray(0, sep).toString("latin1");
25222
25352
  const statusMatch = /^HTTP\/1\.[01] (\d+)(?: (.*))?/.exec(
25223
- head.split("\r\n")[0] ?? ""
25353
+ head2.split("\r\n")[0] ?? ""
25224
25354
  );
25225
25355
  if (!statusMatch) {
25226
25356
  fail2(new Error("bad upstream status"));
25227
25357
  return;
25228
25358
  }
25229
25359
  const headerMap = {};
25230
- for (const line of head.split("\r\n").slice(1)) {
25360
+ for (const line of head2.split("\r\n").slice(1)) {
25231
25361
  const i = line.indexOf(":");
25232
25362
  if (i <= 0) continue;
25233
25363
  headerMap[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
@@ -25242,6 +25372,7 @@ ${lines}\r
25242
25372
  }
25243
25373
  settled = true;
25244
25374
  resolve12({
25375
+ ok: true,
25245
25376
  status: Number(statusMatch[1]),
25246
25377
  statusMessage: statusMatch[2]?.trim() ?? "",
25247
25378
  headers: headerMap,
@@ -25317,6 +25448,8 @@ function authorizeOp(policy, op, url, method, headers) {
25317
25448
  async function startInterceptProxy(options = {}) {
25318
25449
  let policy = options.policy ?? null;
25319
25450
  const serveMock = options.serveMock;
25451
+ const resolver = options.resolver ?? defaultResolver2;
25452
+ const dial = options.dial ?? defaultDial;
25320
25453
  let activeMocks = 0;
25321
25454
  const mockAborts = /* @__PURE__ */ new Set();
25322
25455
  let stopped = false;
@@ -25411,8 +25544,24 @@ async function startInterceptProxy(options = {}) {
25411
25544
  method,
25412
25545
  target,
25413
25546
  forwardHeaders,
25414
- requestBody
25547
+ requestBody,
25548
+ {
25549
+ resolver,
25550
+ dial,
25551
+ localOrigins: effectiveLocalOrigins(active)
25552
+ }
25415
25553
  );
25554
+ if (!upstream.ok) {
25555
+ if (!res.headersSent) {
25556
+ res.writeHead(upstream.status, {
25557
+ "cache-control": "no-store",
25558
+ "content-length": "0",
25559
+ connection: "close"
25560
+ });
25561
+ res.end();
25562
+ }
25563
+ return;
25564
+ }
25416
25565
  let responseBody = upstream.body;
25417
25566
  if (responseOp !== null && active !== null) {
25418
25567
  if (responseBody.byteLength > RUNTIME_LIMITS.maxResponseBodyBytes) {
@@ -25608,25 +25757,52 @@ async function startInterceptProxy(options = {}) {
25608
25757
  );
25609
25758
  }
25610
25759
  function handleConnect(req, clientSocket, head) {
25611
- const authority = req.url ?? "";
25612
- const [host, portText] = authority.split(":");
25613
- const port = Number(portText);
25614
- if (!host || !Number.isInteger(port) || port <= 0) {
25615
- clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
25616
- clientSocket.destroy();
25760
+ void tunnelConnect(req, clientSocket, head);
25761
+ }
25762
+ async function tunnelConnect(req, clientSocket, head) {
25763
+ let upstream;
25764
+ clientSocket.on("error", () => upstream?.destroy());
25765
+ const parsed = parseConnectAuthority(req.url ?? "");
25766
+ if (parsed === null) {
25767
+ endConnect(clientSocket, 400, "Bad Request");
25768
+ return;
25769
+ }
25770
+ if (parsed.port !== 443) {
25771
+ endConnect(clientSocket, 403, "Forbidden");
25772
+ return;
25773
+ }
25774
+ const pinned = await pinHostname(
25775
+ parsed.host,
25776
+ httpsOrigin(parsed.host) ?? "",
25777
+ {
25778
+ resolver,
25779
+ dial,
25780
+ localOrigins: effectiveLocalOrigins(policy)
25781
+ }
25782
+ );
25783
+ if (pinned === null || clientSocket.destroyed) {
25784
+ endConnect(clientSocket, 403, "Forbidden");
25785
+ return;
25786
+ }
25787
+ let upstreamSocket;
25788
+ try {
25789
+ upstreamSocket = dial(443, pinned.address);
25790
+ } catch {
25791
+ endConnect(clientSocket, 502, "Bad Gateway");
25617
25792
  return;
25618
25793
  }
25619
- const upstream = connect(port, host);
25620
- upstream.on("connect", () => {
25794
+ upstream = upstreamSocket;
25795
+ upstreamSocket.on("connect", () => {
25621
25796
  clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
25622
- if (head.byteLength > 0) upstream.write(head);
25623
- pipeSockets(clientSocket, upstream);
25797
+ if (head.byteLength > 0) upstreamSocket.write(head);
25798
+ pipeSockets(clientSocket, upstreamSocket);
25624
25799
  });
25625
- upstream.on("error", () => {
25626
- clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
25627
- clientSocket.destroy();
25800
+ upstreamSocket.on("error", () => {
25801
+ if (!clientSocket.destroyed && !clientSocket.writableEnded) {
25802
+ endConnect(clientSocket, 502, "Bad Gateway");
25803
+ }
25804
+ upstreamSocket.destroy();
25628
25805
  });
25629
- clientSocket.on("error", () => upstream.destroy());
25630
25806
  }
25631
25807
  const endpoint = await new Promise((resolve12, reject) => {
25632
25808
  server.once("error", reject);
@@ -25677,7 +25853,10 @@ async function startInterception(activation, policyDigest, extensionId, pacRoute
25677
25853
  const sessionId = `session-${activation.startedAt}-${Math.random().toString(36).slice(2)}`;
25678
25854
  let proxy;
25679
25855
  try {
25680
- const started = await registeredProvider.start(activation);
25856
+ const started = await registeredProvider.start({
25857
+ ...activation,
25858
+ localOrigins: targetPolicy.localOrigins
25859
+ });
25681
25860
  if (started && typeof started === "object" && typeof started.host === "string" && typeof started.port === "number") {
25682
25861
  proxy = started;
25683
25862
  } else if (activation.proxy) {
@@ -27414,6 +27593,25 @@ function createNativeHost(options) {
27414
27593
  ...options.clock ? { clock: options.clock } : {}
27415
27594
  });
27416
27595
  let interceptProxy = null;
27596
+ function policyForProxy(activation) {
27597
+ const policy = controller.getActivePolicy();
27598
+ if (!policy) return null;
27599
+ const fromProject = localOriginsFromProject(policy.project);
27600
+ const fromSession = activation.localOrigins ?? [];
27601
+ const localOrigins = [];
27602
+ const seen = /* @__PURE__ */ new Set();
27603
+ for (const origin of [...fromProject, ...fromSession]) {
27604
+ if (seen.has(origin)) continue;
27605
+ seen.add(origin);
27606
+ localOrigins.push(origin);
27607
+ }
27608
+ return {
27609
+ project: policy.project,
27610
+ operations: policy.operations,
27611
+ presetDigest: policy.presetDigest,
27612
+ localOrigins
27613
+ };
27614
+ }
27417
27615
  let outboundWrite = null;
27418
27616
  let hostRequestCounter = 0;
27419
27617
  const pendingHost = /* @__PURE__ */ new Map();
@@ -27482,18 +27680,13 @@ function createNativeHost(options) {
27482
27680
  } catch {
27483
27681
  }
27484
27682
  },
27485
- async startTlsProxy() {
27683
+ async startTlsProxy(activation) {
27486
27684
  if (interceptProxy) {
27487
27685
  await interceptProxy.stop();
27488
27686
  interceptProxy = null;
27489
27687
  }
27490
- const policy = controller.getActivePolicy();
27491
27688
  interceptProxy = await startInterceptProxy({
27492
- policy: policy ? {
27493
- project: policy.project,
27494
- operations: policy.operations,
27495
- presetDigest: policy.presetDigest
27496
- } : null,
27689
+ policy: policyForProxy(activation),
27497
27690
  serveMock: (request) => controller.serveMock(request.token, {
27498
27691
  method: request.method,
27499
27692
  signal: request.signal
@@ -27516,14 +27709,7 @@ function createNativeHost(options) {
27516
27709
  async start(activation) {
27517
27710
  const routes = pendingRoutes.length > 0 ? pendingRoutes : activation.pacRoutes;
27518
27711
  const endpoint = await platformProvider.start(activation, routes);
27519
- const policy = controller.getActivePolicy();
27520
- interceptProxy?.setPolicy(
27521
- policy ? {
27522
- project: policy.project,
27523
- operations: policy.operations,
27524
- presetDigest: policy.presetDigest
27525
- } : null
27526
- );
27712
+ interceptProxy?.setPolicy(policyForProxy(activation));
27527
27713
  return endpoint;
27528
27714
  },
27529
27715
  async stop() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.16.2",
3
+ "version": "6.16.3",
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.",