@rogatio/cli 6.21.0 → 6.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.
Files changed (2) hide show
  1. package/dist/node/index.js +148 -96
  2. package/package.json +1 -1
@@ -2318,18 +2318,19 @@ import { isAbsolute as isAbsolute3 } from "node:path";
2318
2318
  import { Buffer as Buffer2 } from "node:buffer";
2319
2319
  import { existsSync as existsSync2 } from "node:fs";
2320
2320
  import { join as join6 } from "node:path";
2321
+ import { timingSafeEqual } from "node:crypto";
2321
2322
  import { lookup as dnsLookup2 } from "node:dns/promises";
2322
2323
  import {
2323
2324
  createServer as createServer2
2324
2325
  } from "node:http";
2325
2326
  import { connect } from "node:net";
2326
2327
  import { Worker } from "node:worker_threads";
2328
+ import { randomBytes } from "node:crypto";
2327
2329
  import { lookup as dnsLookup } from "node:dns/promises";
2328
2330
  import { request as httpRequest2 } from "node:http";
2329
2331
  import { request as httpsRequest } from "node:https";
2330
- import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
2332
+ import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
2331
2333
  import { spawn as spawn4 } from "node:child_process";
2332
- import { randomBytes as randomBytes2 } from "node:crypto";
2333
2334
  import { createHash as createHash3 } from "node:crypto";
2334
2335
  import { createHash as createHash4 } from "node:crypto";
2335
2336
  var __create = Object.create;
@@ -25024,6 +25025,74 @@ async function sourceMatchesWithinDeadline(source, url) {
25024
25025
  function matchUrlCapturesWithinDeadline(urlRegex, url) {
25025
25026
  return runRegexExec(url, urlRegex, RUNTIME_LIMITS.maxRegexDeadlineMs);
25026
25027
  }
25028
+ async function readMockFile(root, logicalPath, signal) {
25029
+ return readConfinedPathBytes(root, logicalPath, signal);
25030
+ }
25031
+ function mintToken() {
25032
+ return randomBytes(32).toString("hex");
25033
+ }
25034
+ function headerName(name) {
25035
+ return name.toLowerCase();
25036
+ }
25037
+ function waitForDelay(delayMs, signal) {
25038
+ if (signal?.aborted) return Promise.resolve(failure("runtime.timeout"));
25039
+ if (delayMs === void 0 || delayMs <= 0) {
25040
+ return Promise.resolve({ ok: true, value: void 0 });
25041
+ }
25042
+ return new Promise((resolve12) => {
25043
+ const onAbort = () => {
25044
+ clearTimeout(timer);
25045
+ resolve12(failure("runtime.timeout"));
25046
+ };
25047
+ const timer = setTimeout(() => {
25048
+ signal?.removeEventListener("abort", onAbort);
25049
+ resolve12({ ok: true, value: void 0 });
25050
+ }, delayMs);
25051
+ signal?.addEventListener("abort", onAbort, { once: true });
25052
+ });
25053
+ }
25054
+ async function renderMockResponse(options) {
25055
+ const { mock, fileRoot, method, signal } = options;
25056
+ const waited = await waitForDelay(mock.delayMs, signal);
25057
+ if (!waited.ok) return waited;
25058
+ let fullBytes;
25059
+ if (mock.body !== void 0) {
25060
+ fullBytes = new TextEncoder().encode(mock.body);
25061
+ } else if (mock.file !== void 0) {
25062
+ if (fileRoot === void 0) return failure("runtime.file-denied");
25063
+ if (!isConfinedFileSupported()) {
25064
+ return failure("runtime.platform-unsupported");
25065
+ }
25066
+ const read2 = await readMockFile(fileRoot, mock.file, signal);
25067
+ if (!read2.ok) return read2;
25068
+ fullBytes = read2.value;
25069
+ } else {
25070
+ return failure("runtime.file-denied");
25071
+ }
25072
+ const headers = [];
25073
+ let hasContentType = false;
25074
+ let hasCacheControl = false;
25075
+ for (const header of mock.headers ?? []) {
25076
+ const name = headerName(header.name);
25077
+ if (name === "content-length") continue;
25078
+ if (name === "content-type") hasContentType = true;
25079
+ if (name === "cache-control") hasCacheControl = true;
25080
+ headers.push([header.name, header.value]);
25081
+ }
25082
+ if (!hasContentType) {
25083
+ headers.push(["Content-Type", "application/octet-stream"]);
25084
+ }
25085
+ headers.push(["Content-Length", String(fullBytes.byteLength)]);
25086
+ if (!hasCacheControl) headers.push(["Cache-Control", "no-store"]);
25087
+ return {
25088
+ ok: true,
25089
+ value: {
25090
+ status: mock.status,
25091
+ headers,
25092
+ bodyBytes: method === "HEAD" ? new Uint8Array() : fullBytes
25093
+ }
25094
+ };
25095
+ }
25027
25096
  var MOCK_LISTENER_PREFIX = "/.rogatio/mock/";
25028
25097
  function isSingleSegment(value) {
25029
25098
  return value.length > 0 && value.length <= 2048 && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !value.includes("?") && !hasControl(value);
@@ -25402,6 +25471,9 @@ async function fetchAndRewriteAuthorizedResponse(operation, action, options = {}
25402
25471
  };
25403
25472
  }
25404
25473
  var RESPONSE_BODY_LISTENER_PREFIX = "/.rogatio/body/";
25474
+ function isSingleSegment2(value) {
25475
+ return value.length > 0 && value.length <= 2048 && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !value.includes("?") && !hasControl(value);
25476
+ }
25405
25477
  function parseResponseBodyRedirect(rawUrl) {
25406
25478
  const queryAt = rawUrl.indexOf("?");
25407
25479
  const path2 = queryAt === -1 ? rawUrl : rawUrl.slice(0, queryAt);
@@ -25412,18 +25484,24 @@ function parseResponseBodyRedirect(rawUrl) {
25412
25484
  if (slash1 <= 0) return null;
25413
25485
  const slash2 = rest.indexOf("/", slash1 + 1);
25414
25486
  if (slash2 <= slash1 + 1) return null;
25487
+ const slash3 = rest.indexOf("/", slash2 + 1);
25488
+ if (slash3 <= slash2 + 1) return null;
25489
+ let token;
25415
25490
  let ruleId;
25416
25491
  let digest;
25417
25492
  try {
25418
- ruleId = decodeURIComponent(rest.slice(0, slash1));
25419
- digest = decodeURIComponent(rest.slice(slash1 + 1, slash2));
25493
+ token = decodeURIComponent(rest.slice(0, slash1));
25494
+ ruleId = decodeURIComponent(rest.slice(slash1 + 1, slash2));
25495
+ digest = decodeURIComponent(rest.slice(slash2 + 1, slash3));
25420
25496
  } catch {
25421
25497
  return null;
25422
25498
  }
25423
- if (ruleId.length === 0 || digest.length === 0) return null;
25424
- const original = rest.slice(slash2 + 1) + search;
25499
+ if (!isSingleSegment2(token) || !isSingleSegment2(ruleId) || !isSingleSegment2(digest)) {
25500
+ return null;
25501
+ }
25502
+ const original = rest.slice(slash3 + 1) + search;
25425
25503
  if (!/^https?:\/\//i.test(original)) return null;
25426
- return { ruleId, digest, originalUrl: original };
25504
+ return { token, ruleId, digest, originalUrl: original };
25427
25505
  }
25428
25506
  function originOf(value) {
25429
25507
  const direct = normalizeSiteOrigin(value);
@@ -25492,8 +25570,12 @@ async function revalidateAuthority(project, operations, request) {
25492
25570
  }
25493
25571
  }
25494
25572
  if (matcher.resourceTypes.length > 0) {
25495
- const resourceType = typeof request.resourceType === "string" ? request.resourceType : void 0;
25496
- if (resourceType === void 0 || !matcher.resourceTypes.includes(resourceType)) {
25573
+ const supplied = request.resourceType;
25574
+ if (supplied === void 0) {
25575
+ if (operation.kind !== "response-body") {
25576
+ return { allowed: false, reason: "resource-type-unauthorized" };
25577
+ }
25578
+ } else if (typeof supplied !== "string" || !matcher.resourceTypes.includes(supplied)) {
25497
25579
  return { allowed: false, reason: "resource-type-unauthorized" };
25498
25580
  }
25499
25581
  }
@@ -25504,6 +25586,24 @@ async function revalidateAuthority(project, operations, request) {
25504
25586
  operation
25505
25587
  };
25506
25588
  }
25589
+ var PROXY_DOCUMENT_CSP = "sandbox; default-src 'none'; script-src 'none'; connect-src 'none'; form-action 'none'; frame-src 'none'";
25590
+ var DROPPED_DOCUMENT_HEADERS = /* @__PURE__ */ new Set([
25591
+ "content-security-policy",
25592
+ "content-security-policy-report-only",
25593
+ "x-content-type-options"
25594
+ ]);
25595
+ function sameToken(supplied, expected) {
25596
+ const left = Buffer.from(supplied);
25597
+ const right = Buffer.from(expected);
25598
+ if (left.byteLength !== right.byteLength) return false;
25599
+ return timingSafeEqual(left, right);
25600
+ }
25601
+ function proxyDocumentHeaders() {
25602
+ return [
25603
+ ["content-security-policy", PROXY_DOCUMENT_CSP],
25604
+ ["x-content-type-options", "nosniff"]
25605
+ ];
25606
+ }
25507
25607
  function isBodyOp(op) {
25508
25608
  return op.kind === "request-body" || op.kind === "response-body";
25509
25609
  }
@@ -25906,6 +26006,8 @@ async function authorizeOp(policy, op, url, method, headers) {
25906
26006
  return decision.allowed;
25907
26007
  }
25908
26008
  async function startInterceptProxy(options = {}) {
26009
+ const responseBodyToken = mintToken();
26010
+ const outbound = options.outbound;
25909
26011
  let policy = options.policy ?? null;
25910
26012
  const serveMock = options.serveMock;
25911
26013
  const resolver = options.resolver ?? defaultResolver2;
@@ -26126,10 +26228,11 @@ async function startInterceptProxy(options = {}) {
26126
26228
  endEmpty(res, status);
26127
26229
  return;
26128
26230
  }
26129
- res.writeHead(
26130
- rendered.value.status,
26131
- rendered.value.headers.map(([name, value]) => [name, value])
26231
+ const mockHeaders = rendered.value.headers.flatMap(
26232
+ ([name, value]) => DROPPED_DOCUMENT_HEADERS.has(name.toLowerCase()) ? [] : [[name, value]]
26132
26233
  );
26234
+ mockHeaders.push(...proxyDocumentHeaders());
26235
+ res.writeHead(rendered.value.status, mockHeaders);
26133
26236
  res.end(Buffer.from(rendered.value.bodyBytes));
26134
26237
  } catch {
26135
26238
  endEmpty(res, 502);
@@ -26146,9 +26249,19 @@ async function startInterceptProxy(options = {}) {
26146
26249
  return;
26147
26250
  }
26148
26251
  const parsed = parseResponseBodyRedirect(rawUrl);
26252
+ if (parsed === null) {
26253
+ res.writeHead(400);
26254
+ res.end();
26255
+ return;
26256
+ }
26257
+ if (!sameToken(parsed.token, responseBodyToken)) {
26258
+ res.writeHead(403);
26259
+ res.end();
26260
+ return;
26261
+ }
26149
26262
  const active = policy;
26150
- if (parsed === null || active === null || active.presetDigest === void 0) {
26151
- res.writeHead(parsed === null ? 400 : 403);
26263
+ if (active === null || active.presetDigest === void 0) {
26264
+ res.writeHead(403);
26152
26265
  res.end();
26153
26266
  return;
26154
26267
  }
@@ -26189,8 +26302,7 @@ async function startInterceptProxy(options = {}) {
26189
26302
  url: parsed.originalUrl,
26190
26303
  target,
26191
26304
  method: "GET",
26192
- initiator: initiatorFromHeaders(req.headers, parsed.originalUrl) ?? new URL(target).origin,
26193
- resourceType: operation.matcher.resourceTypes[0]
26305
+ initiator: initiatorFromHeaders(req.headers, parsed.originalUrl) ?? new URL(target).origin
26194
26306
  }
26195
26307
  );
26196
26308
  if (!allowed.allowed) {
@@ -26209,7 +26321,8 @@ async function startInterceptProxy(options = {}) {
26209
26321
  sourceValue: operation.matcher.source.value,
26210
26322
  presetDigest
26211
26323
  },
26212
- operation.responseBody
26324
+ operation.responseBody,
26325
+ outbound ?? {}
26213
26326
  );
26214
26327
  if (!fetched.ok) {
26215
26328
  res.writeHead(502);
@@ -26219,7 +26332,7 @@ async function startInterceptProxy(options = {}) {
26219
26332
  const headers = {};
26220
26333
  for (const [name, value] of fetched.value.headers) {
26221
26334
  const lower = name.toLowerCase();
26222
- if (lower === "set-cookie" || lower === "cookie" || lower === "authorization" || lower === "proxy-authorization") {
26335
+ if (lower === "set-cookie" || lower === "cookie" || lower === "authorization" || lower === "proxy-authorization" || DROPPED_DOCUMENT_HEADERS.has(lower)) {
26223
26336
  continue;
26224
26337
  }
26225
26338
  const existing = headers[lower];
@@ -26227,6 +26340,9 @@ async function startInterceptProxy(options = {}) {
26227
26340
  else if (Array.isArray(existing)) existing.push(value);
26228
26341
  else headers[lower] = [existing, value];
26229
26342
  }
26343
+ for (const [name, value] of proxyDocumentHeaders()) {
26344
+ headers[name] = value;
26345
+ }
26230
26346
  writeResponse(
26231
26347
  res,
26232
26348
  fetched.value.status,
@@ -26296,6 +26412,7 @@ async function startInterceptProxy(options = {}) {
26296
26412
  });
26297
26413
  return {
26298
26414
  endpoint,
26415
+ responseBodyToken,
26299
26416
  setPolicy(next) {
26300
26417
  policy = next;
26301
26418
  },
@@ -26462,7 +26579,7 @@ function createPlatformInterceptionProvider(adapter) {
26462
26579
  var TOKEN_BYTES = 32;
26463
26580
  var TOKEN_LENGTH = 43;
26464
26581
  function createCapabilityState(preset, now) {
26465
- const bootstrap = randomBytes(TOKEN_BYTES).toString("base64url");
26582
+ const bootstrap = randomBytes2(TOKEN_BYTES).toString("base64url");
26466
26583
  return {
26467
26584
  presetDigest: preset.digest,
26468
26585
  bootstrap,
@@ -26485,15 +26602,15 @@ function isToken(value) {
26485
26602
  return false;
26486
26603
  }
26487
26604
  }
26488
- function sameToken(value, expectedDigest) {
26605
+ function sameToken2(value, expectedDigest) {
26489
26606
  const actual = digestToken(value);
26490
- return actual.length === expectedDigest.length && timingSafeEqual(actual, expectedDigest);
26607
+ return actual.length === expectedDigest.length && timingSafeEqual2(actual, expectedDigest);
26491
26608
  }
26492
26609
  function sameDigest(value, expected) {
26493
26610
  if (!isSha256Digest(value) || value.length !== expected.length) return false;
26494
26611
  const actual = Buffer.from(value, "ascii");
26495
26612
  const wanted = Buffer.from(expected, "ascii");
26496
- return timingSafeEqual(actual, wanted);
26613
+ return timingSafeEqual2(actual, wanted);
26497
26614
  }
26498
26615
  function cleanSessions(state, now) {
26499
26616
  for (let index = state.sessions.length - 1; index >= 0; index -= 1) {
@@ -26503,14 +26620,14 @@ function cleanSessions(state, now) {
26503
26620
  }
26504
26621
  function pairCapability(state, capability, digest, now) {
26505
26622
  cleanSessions(state, now);
26506
- if (state.closed || state.consumed || !isToken(capability) || typeof digest !== "string" || !sameDigest(digest, state.presetDigest) || now >= state.bootstrapExpiresAt || !sameToken(capability, state.bootstrapDigest)) {
26623
+ if (state.closed || state.consumed || !isToken(capability) || typeof digest !== "string" || !sameDigest(digest, state.presetDigest) || now >= state.bootstrapExpiresAt || !sameToken2(capability, state.bootstrapDigest)) {
26507
26624
  return failure("runtime.pairing-denied");
26508
26625
  }
26509
26626
  if (state.sessions.length >= RUNTIME_LIMITS.maxConcurrentSessions) {
26510
26627
  return failure("runtime.overloaded");
26511
26628
  }
26512
26629
  state.consumed = true;
26513
- const sessionCapability = randomBytes(TOKEN_BYTES).toString("base64url");
26630
+ const sessionCapability = randomBytes2(TOKEN_BYTES).toString("base64url");
26514
26631
  const expiresAt = now + RUNTIME_LIMITS.sessionLifetimeMs;
26515
26632
  state.sessions.push({
26516
26633
  digest: Buffer.from(state.presetDigest, "ascii"),
@@ -26533,7 +26650,7 @@ function findSession(state, capability, digest, now) {
26533
26650
  if (!sameDigest(digest, state.presetDigest)) return null;
26534
26651
  const candidate = digestToken(capability);
26535
26652
  for (const session of state.sessions) {
26536
- if (candidate.length === session.tokenDigest.length && timingSafeEqual(candidate, session.tokenDigest) && sameDigest(digest, session.digest.toString("ascii"))) {
26653
+ if (candidate.length === session.tokenDigest.length && timingSafeEqual2(candidate, session.tokenDigest) && sameDigest(digest, session.digest.toString("ascii"))) {
26537
26654
  return session;
26538
26655
  }
26539
26656
  }
@@ -26649,74 +26766,6 @@ async function pickDirectory(run = runDialog, platform = process.platform) {
26649
26766
  }
26650
26767
  return { ok: false, code: "runtime.picker-unavailable" };
26651
26768
  }
26652
- async function readMockFile(root, logicalPath, signal) {
26653
- return readConfinedPathBytes(root, logicalPath, signal);
26654
- }
26655
- function mintToken() {
26656
- return randomBytes2(32).toString("hex");
26657
- }
26658
- function headerName(name) {
26659
- return name.toLowerCase();
26660
- }
26661
- function waitForDelay(delayMs, signal) {
26662
- if (signal?.aborted) return Promise.resolve(failure("runtime.timeout"));
26663
- if (delayMs === void 0 || delayMs <= 0) {
26664
- return Promise.resolve({ ok: true, value: void 0 });
26665
- }
26666
- return new Promise((resolve12) => {
26667
- const onAbort = () => {
26668
- clearTimeout(timer);
26669
- resolve12(failure("runtime.timeout"));
26670
- };
26671
- const timer = setTimeout(() => {
26672
- signal?.removeEventListener("abort", onAbort);
26673
- resolve12({ ok: true, value: void 0 });
26674
- }, delayMs);
26675
- signal?.addEventListener("abort", onAbort, { once: true });
26676
- });
26677
- }
26678
- async function renderMockResponse(options) {
26679
- const { mock, fileRoot, method, signal } = options;
26680
- const waited = await waitForDelay(mock.delayMs, signal);
26681
- if (!waited.ok) return waited;
26682
- let fullBytes;
26683
- if (mock.body !== void 0) {
26684
- fullBytes = new TextEncoder().encode(mock.body);
26685
- } else if (mock.file !== void 0) {
26686
- if (fileRoot === void 0) return failure("runtime.file-denied");
26687
- if (!isConfinedFileSupported()) {
26688
- return failure("runtime.platform-unsupported");
26689
- }
26690
- const read2 = await readMockFile(fileRoot, mock.file, signal);
26691
- if (!read2.ok) return read2;
26692
- fullBytes = read2.value;
26693
- } else {
26694
- return failure("runtime.file-denied");
26695
- }
26696
- const headers = [];
26697
- let hasContentType = false;
26698
- let hasCacheControl = false;
26699
- for (const header of mock.headers ?? []) {
26700
- const name = headerName(header.name);
26701
- if (name === "content-length") continue;
26702
- if (name === "content-type") hasContentType = true;
26703
- if (name === "cache-control") hasCacheControl = true;
26704
- headers.push([header.name, header.value]);
26705
- }
26706
- if (!hasContentType) {
26707
- headers.push(["Content-Type", "application/octet-stream"]);
26708
- }
26709
- headers.push(["Content-Length", String(fullBytes.byteLength)]);
26710
- if (!hasCacheControl) headers.push(["Cache-Control", "no-store"]);
26711
- return {
26712
- ok: true,
26713
- value: {
26714
- status: mock.status,
26715
- headers,
26716
- bodyBytes: method === "HEAD" ? new Uint8Array() : fullBytes
26717
- }
26718
- };
26719
- }
26720
26769
  var MAX_MOCK_FILE_ERRORS = 64;
26721
26770
  var FILE_ERROR_CODES = /* @__PURE__ */ new Set([
26722
26771
  "runtime.file-denied",
@@ -28171,7 +28220,10 @@ function createNativeHost(options) {
28171
28220
  signal: request.signal
28172
28221
  })
28173
28222
  });
28174
- return interceptProxy.endpoint;
28223
+ return {
28224
+ ...interceptProxy.endpoint,
28225
+ responseBodyToken: interceptProxy.responseBodyToken
28226
+ };
28175
28227
  },
28176
28228
  async stopTlsProxy() {
28177
28229
  if (interceptProxy) {
@@ -29792,7 +29844,7 @@ function createServer3(handler, options = {}) {
29792
29844
  }
29793
29845
 
29794
29846
  // packages/cli/src/server/routes.ts
29795
- import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
29847
+ import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
29796
29848
  import { readFile as readFile4 } from "node:fs/promises";
29797
29849
  import { dirname as dirname9, isAbsolute as isAbsolute5, resolve as resolve5 } from "node:path";
29798
29850
 
@@ -30261,7 +30313,7 @@ function tokensEqual(provided, expected) {
30261
30313
  const providedBytes = Buffer.from(provided);
30262
30314
  const expectedBytes = Buffer.from(expected);
30263
30315
  if (providedBytes.length !== expectedBytes.length) return false;
30264
- return timingSafeEqual2(providedBytes, expectedBytes);
30316
+ return timingSafeEqual3(providedBytes, expectedBytes);
30265
30317
  }
30266
30318
  function scopedPathname(pathname, sessionToken) {
30267
30319
  const marker = "/e/";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.21.0",
3
+ "version": "6.21.1",
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.",