@rogatio/cli 6.20.0 → 6.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ rogatio <command> [options]
45
45
  | `rogatio runtime <install\|uninstall\|verify>` | Register the native-messaging host with `install` and trust the device-local CA in one **transactional** call; it needs elevated privileges (Linux `sudo`, macOS keychain authorization, Windows Administrator) and rolls back with exit `1` and `trust unsupported: <reasons>` when a capability or elevation is missing. Release users never pass `--extension-id`. That flag is only for development (a local unpacked build without the release key) and for forks. `uninstall` removes the host manifest, the device-local CA files, and the CA trust installation (idempotent). `verify` reports whether manifest, `runtime-host` wrapper, allowed origins, and CA trust are all valid, and fails when `allowed_origins` omits the release ID. Release users never pass `--extension-id` on verify either. |
46
46
  | `rogatio runtime host [path]` | Run the consolidated native-messaging host for the project (pair/authorize/body transforms and mock responses over stdio). Normally launched by the browser; run manually only for debugging. `--root <dir>` overrides the confined mock file root. When omitted, a saved device-local root for that project file is used, otherwise the project directory. A stdin project that contains a file mock requires `--root`. |
47
47
 
48
- `rogatio edit` includes **Run checks**. It calls `POST /api/doctor` (CSRF-protected) and shows editor checks plus the same report as `rogatio doctor` for the open file. A provider configured after the page opened is picked up on the next `rogatio edit`.
48
+ `rogatio edit` includes **Run checks**. It calls `POST /e/<session>/api/doctor` (session path, loopback Origin, and `X-CSRF-Token`) and shows editor checks plus the same report as `rogatio doctor` for the open file. A provider configured after the page opened is picked up on the next `rogatio edit`.
49
49
 
50
50
  Global options: `--help, -h` and `--version, -v`. Run `rogatio <command> --help`
51
51
  for command-specific usage.
@@ -7,8 +7,6 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
7
7
 
8
8
  // packages/runtime/dist/node/index.js
9
9
  import { isIP } from "node:net";
10
- import * as fs from "node:fs/promises";
11
- import * as path from "node:path";
12
10
 
13
11
  // packages/schema/dist/node/index.js
14
12
  import { Ajv2020 } from "ajv/dist/2020.js";
@@ -2260,6 +2258,8 @@ function sameOrigin(a, b) {
2260
2258
  }
2261
2259
 
2262
2260
  // packages/runtime/dist/node/index.js
2261
+ import * as fs from "node:fs/promises";
2262
+ import * as path from "node:path";
2263
2263
  import { isIP as isIP2 } from "node:net";
2264
2264
  import { constants, existsSync } from "node:fs";
2265
2265
  import { open, readlink, realpath } from "node:fs/promises";
@@ -21133,79 +21133,51 @@ function sleep(ms) {
21133
21133
  function calculateRetryDelay(attempt) {
21134
21134
  return RETRY_DELAY_BASE * 2 ** attempt + Math.random() * 100;
21135
21135
  }
21136
- async function fetchWithRetry(url, init, signal) {
21137
- let lastError;
21138
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
21139
- if (signal?.aborted) {
21140
- throw new DOMException("Aborted", "AbortError");
21141
- }
21142
- try {
21143
- const controller = new AbortController();
21144
- const timeoutId = setTimeout(() => controller.abort(), COMPLETE_TIMEOUT);
21145
- if (signal) {
21146
- signal.addEventListener("abort", () => controller.abort());
21147
- }
21148
- const response = await fetch(url, {
21149
- ...init,
21150
- signal: controller.signal
21151
- });
21152
- clearTimeout(timeoutId);
21153
- if (response.ok) {
21154
- return response;
21155
- }
21156
- const shouldRetry = response.status === 429 || response.status >= 500;
21157
- if (shouldRetry && attempt < MAX_RETRIES) {
21158
- const retryAfter = response.headers.get("Retry-After");
21159
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : calculateRetryDelay(attempt);
21160
- await sleep(Math.min(delay, 1e4));
21161
- continue;
21162
- }
21163
- const errorText = await response.text().catch(() => "");
21164
- throw new Error(`HTTP ${response.status}: ${errorText}`);
21165
- } catch (e) {
21166
- lastError = e;
21167
- if (e instanceof DOMException && e.name === "AbortError") {
21168
- throw e;
21169
- }
21170
- if (attempt < MAX_RETRIES && e.name !== "AbortError") {
21171
- await sleep(calculateRetryDelay(attempt));
21172
- continue;
21173
- }
21174
- throw e;
21175
- }
21176
- }
21177
- throw lastError;
21136
+ function providerStatusError(status) {
21137
+ return new Error(`HTTP ${status}`);
21138
+ }
21139
+ async function releaseResponse(response) {
21140
+ await response.body?.cancel().catch(() => void 0);
21178
21141
  }
21179
- async function fetchStreamWithRetry(url, init, signal) {
21142
+ async function fetchProvider(url, init, timeoutMs, signal) {
21180
21143
  let lastError;
21181
21144
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
21182
21145
  if (signal?.aborted) {
21183
21146
  throw new DOMException("Aborted", "AbortError");
21184
21147
  }
21148
+ const controller = new AbortController();
21149
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
21150
+ if (signal) {
21151
+ signal.addEventListener("abort", () => controller.abort());
21152
+ }
21153
+ let terminalStatus;
21185
21154
  try {
21186
- const controller = new AbortController();
21187
- const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT);
21188
- if (signal) {
21189
- signal.addEventListener("abort", () => controller.abort());
21190
- }
21191
21155
  const response = await fetch(url, {
21192
21156
  ...init,
21157
+ // Stay on the configured URL. A 3xx response is not followed.
21158
+ redirect: "manual",
21193
21159
  signal: controller.signal
21194
21160
  });
21195
21161
  clearTimeout(timeoutId);
21196
- if (response.ok) {
21162
+ if (response.status >= 300 && response.status <= 399) {
21163
+ await releaseResponse(response);
21164
+ terminalStatus = response.status;
21165
+ } else if (response.ok) {
21197
21166
  return response;
21167
+ } else {
21168
+ const shouldRetry = response.status === 429 || response.status >= 500;
21169
+ if (shouldRetry && attempt < MAX_RETRIES) {
21170
+ await releaseResponse(response);
21171
+ const retryAfter = response.headers.get("Retry-After");
21172
+ const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : calculateRetryDelay(attempt);
21173
+ await sleep(Math.min(delay, 1e4));
21174
+ continue;
21175
+ }
21176
+ await releaseResponse(response);
21177
+ terminalStatus = response.status;
21198
21178
  }
21199
- const shouldRetry = response.status === 429 || response.status >= 500;
21200
- if (shouldRetry && attempt < MAX_RETRIES) {
21201
- const retryAfter = response.headers.get("Retry-After");
21202
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : calculateRetryDelay(attempt);
21203
- await sleep(Math.min(delay, 1e4));
21204
- continue;
21205
- }
21206
- const errorText = await response.text().catch(() => "");
21207
- throw new Error(`HTTP ${response.status}: ${errorText}`);
21208
21179
  } catch (e) {
21180
+ clearTimeout(timeoutId);
21209
21181
  lastError = e;
21210
21182
  if (e instanceof DOMException && e.name === "AbortError") {
21211
21183
  throw e;
@@ -21216,9 +21188,18 @@ async function fetchStreamWithRetry(url, init, signal) {
21216
21188
  }
21217
21189
  throw e;
21218
21190
  }
21191
+ if (terminalStatus !== void 0) {
21192
+ throw providerStatusError(terminalStatus);
21193
+ }
21219
21194
  }
21220
21195
  throw lastError;
21221
21196
  }
21197
+ function fetchWithRetry(url, init, signal) {
21198
+ return fetchProvider(url, init, COMPLETE_TIMEOUT, signal);
21199
+ }
21200
+ function fetchStreamWithRetry(url, init, signal) {
21201
+ return fetchProvider(url, init, STREAM_TIMEOUT, signal);
21202
+ }
21222
21203
  function sanitizeResponse(text) {
21223
21204
  return text;
21224
21205
  }
@@ -21318,67 +21299,23 @@ function createAIClient(config) {
21318
21299
  }
21319
21300
  };
21320
21301
  }
21321
- var RESOURCE_TYPES2 = [
21322
- "main_frame",
21323
- "sub_frame",
21324
- "stylesheet",
21325
- "script",
21326
- "image",
21327
- "font",
21328
- "object",
21329
- "xmlhttprequest",
21330
- "fetch",
21331
- "media",
21332
- "websocket",
21333
- "manifest",
21334
- "other"
21335
- ];
21336
- var HTTP_METHODS2 = [
21337
- "GET",
21338
- "POST",
21339
- "PUT",
21340
- "PATCH",
21341
- "DELETE",
21342
- "HEAD",
21343
- "OPTIONS",
21344
- "CONNECT",
21345
- "TRACE"
21346
- ];
21347
- var FORBIDDEN_REQUEST_HEADERS2 = [
21348
- "accept-charset",
21349
- "accept-encoding",
21350
- "access-control-request-headers",
21351
- "access-control-request-method",
21352
- "connection",
21353
- "content-length",
21354
- "cookie",
21355
- "cookie2",
21356
- "date",
21357
- "dnt",
21358
- "expect",
21359
- "host",
21360
- "keep-alive",
21361
- "origin",
21362
- "referer",
21363
- "te",
21364
- "trailer",
21365
- "transfer-encoding",
21366
- "upgrade",
21367
- "via"
21368
- ];
21369
- var FORBIDDEN_RESPONSE_HEADERS2 = ["set-cookie", "set-cookie2"];
21302
+ var MAX_PROJECTS_PER_PROFILE = 64;
21370
21303
  function formatResourceTypes() {
21371
- return RESOURCE_TYPES2.join(", ");
21304
+ return [...RESOURCE_TYPES].join(", ");
21372
21305
  }
21373
21306
  function formatHttpMethods() {
21374
- return HTTP_METHODS2.join(", ");
21307
+ return [...HTTP_METHODS].join(", ");
21375
21308
  }
21376
21309
  function formatForbiddenHeaders() {
21377
21310
  return [
21378
- ...FORBIDDEN_REQUEST_HEADERS2.map((h) => `request: ${h}`),
21379
- ...FORBIDDEN_RESPONSE_HEADERS2.map((h) => `response: ${h}`)
21311
+ ...FORBIDDEN_REQUEST_HEADERS.map((header) => `request: ${header}`),
21312
+ ...FORBIDDEN_REQUEST_PREFIXES.map((prefix) => `request prefix: ${prefix}`),
21313
+ ...FORBIDDEN_RESPONSE_HEADERS.map((header) => `response: ${header}`)
21380
21314
  ].join(", ");
21381
21315
  }
21316
+ function formatDiagnostics() {
21317
+ return Object.entries(MESSAGES).map(([code, message]) => `- ${code} - ${message}`).join("\n");
21318
+ }
21382
21319
  function formatProjectContext(project) {
21383
21320
  try {
21384
21321
  return JSON.stringify(project, null, 2);
@@ -21411,9 +21348,10 @@ ${formatResourceTypes()}
21411
21348
  ${formatHttpMethods()}
21412
21349
 
21413
21350
  ### Bounds
21414
- - Max 64 projects per profile
21415
- - Max 100 groups per project
21416
- - Max 500 rules per project
21351
+ - Max ${MAX_PROJECTS_PER_PROFILE} projects per profile
21352
+ - Max ${LIMITS.maxGroups} groups per project
21353
+ - Max ${LIMITS.maxRulesPerGroup} rules per group
21354
+ - Max ${LIMITS.maxRulesPerProject} rules per project
21417
21355
 
21418
21356
  ## Rule Types
21419
21357
 
@@ -21435,22 +21373,7 @@ Replace-mode body may use URL captures. Regex-mode replacement strings use captu
21435
21373
  Replace-mode body may use URL captures. Regex-mode replacement strings use captures from the body pattern, not the source pattern.
21436
21374
 
21437
21375
  ## Compiler Diagnostic Codes (stable)
21438
- - schema.required - Required project data is missing
21439
- - schema.unknown-property - The project contains an unknown property
21440
- - schema.invalid-type - A value has an invalid type
21441
- - schema.invalid-format - A value has an invalid format
21442
- - schema.invalid-value - A value is invalid
21443
- - schema.out-of-range - A value is outside its allowed bounds
21444
- - schema.invalid-structure - A value has invalid structure
21445
- - schema.duplicate-id - Duplicate group or rule ID
21446
- - schema.duplicate-name - Duplicate group or rule name; every group and rule name must be unique in the project
21447
- - schema.rule-limit - The project contains too many rules
21448
- - compiler.invariant - The compiler could not normalize the project
21449
- - compiler.forbidden-header - The header is forbidden for this direction
21450
- - compiler.header-value-required - A header value is required for set and append
21451
- - compiler.header-value-unexpected - A header value is not allowed for remove
21452
- - compiler.invalid-header-direction - Invalid header direction
21453
- - compiler.invalid-header-operation - Invalid header operation
21376
+ ${formatDiagnostics()}
21454
21377
 
21455
21378
  ## Current Project
21456
21379
  ${projectJson}
@@ -29697,7 +29620,7 @@ function createServer3(handler, options = {}) {
29697
29620
  }
29698
29621
 
29699
29622
  // packages/cli/src/server/routes.ts
29700
- import { randomBytes as randomBytes4 } from "node:crypto";
29623
+ import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
29701
29624
  import { readFile as readFile5 } from "node:fs/promises";
29702
29625
  import { dirname as dirname9, isAbsolute as isAbsolute5, resolve as resolve5 } from "node:path";
29703
29626
 
@@ -30153,17 +30076,44 @@ function isAllowedEditorHost(host, boundPort) {
30153
30076
  return host === `127.0.0.1:${expected}` || host === `localhost:${expected}`;
30154
30077
  }
30155
30078
  function isAllowedEditorOrigin(origin, boundPort) {
30156
- if (origin === void 0) return true;
30157
- if (!Number.isInteger(boundPort) || boundPort <= 0) return false;
30079
+ if (origin === void 0 || !Number.isInteger(boundPort) || boundPort <= 0) {
30080
+ return false;
30081
+ }
30158
30082
  const expected = String(boundPort);
30159
30083
  return origin === `http://127.0.0.1:${expected}` || origin === `http://localhost:${expected}`;
30160
30084
  }
30161
30085
  function generateCsrfToken() {
30162
30086
  return randomBytes4(16).toString("hex");
30163
30087
  }
30088
+ function tokensEqual(provided, expected) {
30089
+ const providedBytes = Buffer.from(provided);
30090
+ const expectedBytes = Buffer.from(expected);
30091
+ if (providedBytes.length !== expectedBytes.length) return false;
30092
+ return timingSafeEqual2(providedBytes, expectedBytes);
30093
+ }
30094
+ function scopedPathname(pathname, sessionToken) {
30095
+ const marker = "/e/";
30096
+ if (!pathname.startsWith(marker)) return void 0;
30097
+ const after = pathname.slice(marker.length);
30098
+ const slash = after.indexOf("/");
30099
+ const candidate = slash === -1 ? after : after.slice(0, slash);
30100
+ if (candidate.length === 0 || !tokensEqual(candidate, sessionToken)) {
30101
+ return void 0;
30102
+ }
30103
+ return slash === -1 ? "/" : after.slice(slash);
30104
+ }
30105
+ function editorSessionPath(token, suffix) {
30106
+ const path2 = suffix.startsWith("/") ? suffix : `/${suffix}`;
30107
+ return `/e/${token}${path2}`;
30108
+ }
30164
30109
  function validateCsrf(req, expectedToken) {
30165
- const token = req.headers["x-csrf-token"];
30166
- return token === expectedToken;
30110
+ const token = headerValue3(req.headers["x-csrf-token"]);
30111
+ if (token === void 0) return false;
30112
+ return tokensEqual(token, expectedToken);
30113
+ }
30114
+ function emptyNotFound(res) {
30115
+ res.writeHead(404);
30116
+ res.end();
30167
30117
  }
30168
30118
  var MAX_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
30169
30119
  var RequestBodyError = class extends Error {
@@ -30268,8 +30218,13 @@ function createRoutes(context) {
30268
30218
  );
30269
30219
  return;
30270
30220
  }
30271
- const { pathname } = parseUrl(req);
30221
+ const { pathname: requestPath } = parseUrl(req);
30272
30222
  const method = req.method || "GET";
30223
+ const pathname = scopedPathname(requestPath, context.csrfToken);
30224
+ if (pathname === void 0) {
30225
+ emptyNotFound(res);
30226
+ return;
30227
+ }
30273
30228
  if (method === "POST") {
30274
30229
  const origin = headerValue3(req.headers.origin);
30275
30230
  if (!isAllowedEditorOrigin(origin, context.boundPort)) {
@@ -30290,7 +30245,8 @@ function createRoutes(context) {
30290
30245
  }
30291
30246
  if (pathname === "/editor.html" && method === "GET") {
30292
30247
  res.writeHead(200, {
30293
- "Content-Type": "text/html; charset=utf-8"
30248
+ "Content-Type": "text/html; charset=utf-8",
30249
+ "Referrer-Policy": "no-referrer"
30294
30250
  });
30295
30251
  res.end(context.editorHtml);
30296
30252
  return;
@@ -31035,18 +30991,13 @@ async function editCommand(args, options = {}) {
31035
30991
  }
31036
30992
  context.boundPort = server.port;
31037
30993
  const serverUrl = `http://127.0.0.1:${server.port}`;
31038
- const editorUrl = `${serverUrl}/editor.html`;
30994
+ const editorUrl = `${serverUrl}${editorSessionPath(csrfToken, "/editor.html")}`;
31039
30995
  const {
31040
30996
  bundle: editorBundlePath,
31041
30997
  css: editorCssPath,
31042
30998
  fonts: editorFontsPath
31043
30999
  } = editorAssetPaths();
31044
- context.editorHtml = generateEditorHtml(
31045
- serverUrl,
31046
- csrfToken,
31047
- filePath,
31048
- !!aiClient
31049
- );
31000
+ context.editorHtml = generateEditorHtml(filePath, !!aiClient);
31050
31001
  context.editorBundlePath = editorBundlePath;
31051
31002
  context.editorCssPath = editorCssPath;
31052
31003
  context.editorFontsPath = editorFontsPath;
@@ -31111,7 +31062,7 @@ async function ensureCivilizationProjectName(storage, filePath, data) {
31111
31062
  await storage.update(filePath, named);
31112
31063
  return named;
31113
31064
  }
31114
- function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
31065
+ function generateEditorHtml(filePath, aiConfigured) {
31115
31066
  const aiAssistHandler = aiConfigured ? `
31116
31067
  aiAssist: async function(request) {
31117
31068
  const res = await fetch(apiBase + '/api/ai/assist', {
@@ -31140,7 +31091,7 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
31140
31091
  <meta charset="UTF-8">
31141
31092
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
31142
31093
  <title>Rogatio Editor</title>
31143
- <link rel="stylesheet" href="/vendor/editor.css" />
31094
+ <link rel="stylesheet" href="vendor/editor.css" />
31144
31095
  <style>
31145
31096
  html, body { margin: 0; min-height: 100%; }
31146
31097
  body {
@@ -31168,14 +31119,16 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
31168
31119
  <div id="editor-doctor" hidden></div>
31169
31120
  <div id="editor-root"></div>
31170
31121
  <script type="importmap">
31171
- { "imports": { "@rogatio/editor": "/vendor/editor.js" } }
31122
+ { "imports": { "@rogatio/editor": "./vendor/editor.js" } }
31172
31123
  </script>
31173
31124
  <script type="module">
31174
31125
  import { createEditor } from '@rogatio/editor';
31175
31126
 
31176
31127
  const root = document.getElementById('editor-root');
31177
- const apiBase = ${JSON.stringify(apiBase)};
31178
- const csrfToken = ${JSON.stringify(csrfToken)};
31128
+ const apiBase = location.pathname.endsWith('/editor.html')
31129
+ ? location.pathname.slice(0, -'/editor.html'.length)
31130
+ : location.pathname;
31131
+ const csrfToken = apiBase.slice(apiBase.lastIndexOf('/') + 1);
31179
31132
  const filePath = ${embedJson(filePath)};
31180
31133
  const doctorTitles = ${embedJson(editorPageTitles())};
31181
31134
  const serverStoppedJson = ${embedJson(serializeEditorDiagnostics(editorServerStoppedReport(filePath)))};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.20.0",
3
+ "version": "6.20.2",
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.",