@rogatio/cli 6.20.1 → 6.20.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.
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";
@@ -2283,21 +2283,23 @@ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join2
2283
2283
  import { X509Certificate } from "node:crypto";
2284
2284
  import { generateKeyPairSync, webcrypto } from "node:crypto";
2285
2285
  import { spawn, spawnSync } from "node:child_process";
2286
- import { accessSync, constants as constants2, unlinkSync, writeFileSync } from "node:fs";
2287
- import { mkdtemp, open as open3, rm as rm2 } from "node:fs/promises";
2288
- import { tmpdir } from "node:os";
2289
- import { dirname as dirname3, join as join3 } from "node:path";
2286
+ import { accessSync, constants as constants3 } from "node:fs";
2287
+ import { mkdtemp as mkdtemp2, rm as rm3 } from "node:fs/promises";
2288
+ import { tmpdir as tmpdir2 } from "node:os";
2289
+ import { dirname as dirname3, join as join4 } from "node:path";
2290
2290
  import { join as posixJoin } from "node:path/posix";
2291
+ import { constants as constants2 } from "node:fs";
2292
+ import { lstat as lstat2, mkdtemp, open as open3, rm as rm2 } from "node:fs/promises";
2293
+ import { tmpdir } from "node:os";
2294
+ import { join as join3 } from "node:path";
2291
2295
  import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
2292
- import { accessSync as accessSync2, constants as constants3, mkdirSync, unlinkSync as unlinkSync2 } from "node:fs";
2296
+ import { accessSync as accessSync2, constants as constants4, mkdirSync, unlinkSync } from "node:fs";
2293
2297
  import { dirname as dirname4 } from "node:path";
2294
2298
  import { join as posixJoin2 } from "node:path/posix";
2295
2299
  import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
2296
- import { accessSync as accessSync3, constants as constants4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
2297
- import { tmpdir as tmpdir2 } from "node:os";
2298
- import { join as join4 } from "node:path";
2300
+ import { accessSync as accessSync3, constants as constants5 } from "node:fs";
2299
2301
  import { join as posixJoin3 } from "node:path/posix";
2300
- import { constants as constants5 } from "node:fs";
2302
+ import { constants as constants6 } from "node:fs";
2301
2303
  import { open as open4, realpath as realpath2 } from "node:fs/promises";
2302
2304
  import { isAbsolute as isAbsolute3 } from "node:path";
2303
2305
  import { Buffer as Buffer2 } from "node:buffer";
@@ -21299,67 +21301,23 @@ function createAIClient(config) {
21299
21301
  }
21300
21302
  };
21301
21303
  }
21302
- var RESOURCE_TYPES2 = [
21303
- "main_frame",
21304
- "sub_frame",
21305
- "stylesheet",
21306
- "script",
21307
- "image",
21308
- "font",
21309
- "object",
21310
- "xmlhttprequest",
21311
- "fetch",
21312
- "media",
21313
- "websocket",
21314
- "manifest",
21315
- "other"
21316
- ];
21317
- var HTTP_METHODS2 = [
21318
- "GET",
21319
- "POST",
21320
- "PUT",
21321
- "PATCH",
21322
- "DELETE",
21323
- "HEAD",
21324
- "OPTIONS",
21325
- "CONNECT",
21326
- "TRACE"
21327
- ];
21328
- var FORBIDDEN_REQUEST_HEADERS2 = [
21329
- "accept-charset",
21330
- "accept-encoding",
21331
- "access-control-request-headers",
21332
- "access-control-request-method",
21333
- "connection",
21334
- "content-length",
21335
- "cookie",
21336
- "cookie2",
21337
- "date",
21338
- "dnt",
21339
- "expect",
21340
- "host",
21341
- "keep-alive",
21342
- "origin",
21343
- "referer",
21344
- "te",
21345
- "trailer",
21346
- "transfer-encoding",
21347
- "upgrade",
21348
- "via"
21349
- ];
21350
- var FORBIDDEN_RESPONSE_HEADERS2 = ["set-cookie", "set-cookie2"];
21304
+ var MAX_PROJECTS_PER_PROFILE = 64;
21351
21305
  function formatResourceTypes() {
21352
- return RESOURCE_TYPES2.join(", ");
21306
+ return [...RESOURCE_TYPES].join(", ");
21353
21307
  }
21354
21308
  function formatHttpMethods() {
21355
- return HTTP_METHODS2.join(", ");
21309
+ return [...HTTP_METHODS].join(", ");
21356
21310
  }
21357
21311
  function formatForbiddenHeaders() {
21358
21312
  return [
21359
- ...FORBIDDEN_REQUEST_HEADERS2.map((h) => `request: ${h}`),
21360
- ...FORBIDDEN_RESPONSE_HEADERS2.map((h) => `response: ${h}`)
21313
+ ...FORBIDDEN_REQUEST_HEADERS.map((header) => `request: ${header}`),
21314
+ ...FORBIDDEN_REQUEST_PREFIXES.map((prefix) => `request prefix: ${prefix}`),
21315
+ ...FORBIDDEN_RESPONSE_HEADERS.map((header) => `response: ${header}`)
21361
21316
  ].join(", ");
21362
21317
  }
21318
+ function formatDiagnostics() {
21319
+ return Object.entries(MESSAGES).map(([code, message]) => `- ${code} - ${message}`).join("\n");
21320
+ }
21363
21321
  function formatProjectContext(project) {
21364
21322
  try {
21365
21323
  return JSON.stringify(project, null, 2);
@@ -21392,9 +21350,10 @@ ${formatResourceTypes()}
21392
21350
  ${formatHttpMethods()}
21393
21351
 
21394
21352
  ### Bounds
21395
- - Max 64 projects per profile
21396
- - Max 100 groups per project
21397
- - Max 500 rules per project
21353
+ - Max ${MAX_PROJECTS_PER_PROFILE} projects per profile
21354
+ - Max ${LIMITS.maxGroups} groups per project
21355
+ - Max ${LIMITS.maxRulesPerGroup} rules per group
21356
+ - Max ${LIMITS.maxRulesPerProject} rules per project
21398
21357
 
21399
21358
  ## Rule Types
21400
21359
 
@@ -21416,22 +21375,7 @@ Replace-mode body may use URL captures. Regex-mode replacement strings use captu
21416
21375
  Replace-mode body may use URL captures. Regex-mode replacement strings use captures from the body pattern, not the source pattern.
21417
21376
 
21418
21377
  ## Compiler Diagnostic Codes (stable)
21419
- - schema.required - Required project data is missing
21420
- - schema.unknown-property - The project contains an unknown property
21421
- - schema.invalid-type - A value has an invalid type
21422
- - schema.invalid-format - A value has an invalid format
21423
- - schema.invalid-value - A value is invalid
21424
- - schema.out-of-range - A value is outside its allowed bounds
21425
- - schema.invalid-structure - A value has invalid structure
21426
- - schema.duplicate-id - Duplicate group or rule ID
21427
- - schema.duplicate-name - Duplicate group or rule name; every group and rule name must be unique in the project
21428
- - schema.rule-limit - The project contains too many rules
21429
- - compiler.invariant - The compiler could not normalize the project
21430
- - compiler.forbidden-header - The header is forbidden for this direction
21431
- - compiler.header-value-required - A header value is required for set and append
21432
- - compiler.header-value-unexpected - A header value is not allowed for remove
21433
- - compiler.invalid-header-direction - Invalid header direction
21434
- - compiler.invalid-header-operation - Invalid header operation
21378
+ ${formatDiagnostics()}
21435
21379
 
21436
21380
  ## Current Project
21437
21381
  ${projectJson}
@@ -23211,6 +23155,84 @@ function createRequestBodyTrustController(options = {}) {
23211
23155
  }
23212
23156
  return { install, uninstall, status, verify: verify2 };
23213
23157
  }
23158
+ var INSTALL_CERT_NAME = "ca.crt";
23159
+ function optionalFlag(flag) {
23160
+ return typeof flag === "number" ? flag : 0;
23161
+ }
23162
+ function exclusiveWriteFlags() {
23163
+ return constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_TRUNC | optionalFlag(constants2.O_NOFOLLOW);
23164
+ }
23165
+ function readFlags() {
23166
+ return constants2.O_RDONLY | optionalFlag(constants2.O_NOFOLLOW) | optionalFlag(constants2.O_NONBLOCK);
23167
+ }
23168
+ function refuseCertificateFile(message) {
23169
+ throw new TrustError("trust.internal", message, [
23170
+ "certificate-file-rejected"
23171
+ ]);
23172
+ }
23173
+ async function writeExclusiveCertificate(certPath, pem) {
23174
+ const handle = await open3(certPath, exclusiveWriteFlags(), 384);
23175
+ try {
23176
+ await handle.writeFile(Buffer.from(pem, "utf8"));
23177
+ await handle.chmod(384);
23178
+ } finally {
23179
+ await handle.close();
23180
+ }
23181
+ }
23182
+ async function assertCertificateReady(certPath, pem) {
23183
+ let info;
23184
+ try {
23185
+ info = await lstat2(certPath);
23186
+ } catch {
23187
+ refuseCertificateFile("certificate file is not a regular file");
23188
+ }
23189
+ if (!info.isFile() || info.isSymbolicLink() || info.isFIFO()) {
23190
+ refuseCertificateFile("certificate file is not a regular file");
23191
+ }
23192
+ let handle;
23193
+ try {
23194
+ handle = await open3(certPath, readFlags());
23195
+ const opened = await handle.stat();
23196
+ if (!opened.isFile() || opened.isFIFO() || opened.isSymbolicLink()) {
23197
+ refuseCertificateFile("certificate file is not a regular file");
23198
+ }
23199
+ const bytes = await handle.readFile();
23200
+ if (!bytes.equals(Buffer.from(pem, "utf8"))) {
23201
+ refuseCertificateFile(
23202
+ "certificate file does not match the certificate to install"
23203
+ );
23204
+ }
23205
+ if (!isInstallableCaCertificate(bytes.toString("utf8"))) {
23206
+ throw new TrustError("trust.internal", "invalid-ca-certificate", [
23207
+ "invalid-ca-certificate"
23208
+ ]);
23209
+ }
23210
+ } catch (error) {
23211
+ if (error instanceof TrustError) throw error;
23212
+ refuseCertificateFile("certificate file is not a regular file");
23213
+ } finally {
23214
+ await handle?.close().catch(() => void 0);
23215
+ }
23216
+ }
23217
+ async function withInstallCertificateFile(pem, use) {
23218
+ if (!isInstallableCaCertificate(pem)) {
23219
+ throw new TrustError("trust.internal", "invalid-ca-certificate", [
23220
+ "invalid-ca-certificate"
23221
+ ]);
23222
+ }
23223
+ const directory = await mkdtemp(join3(tmpdir(), "rogatio-ca-"));
23224
+ try {
23225
+ const certPath = join3(directory, INSTALL_CERT_NAME);
23226
+ await writeExclusiveCertificate(certPath, pem);
23227
+ await assertCertificateReady(certPath, pem);
23228
+ await use(certPath);
23229
+ } finally {
23230
+ try {
23231
+ await rm2(directory, { recursive: true, force: true });
23232
+ } catch {
23233
+ }
23234
+ }
23235
+ }
23214
23236
  var PREVIOUS_CA_PEM_MAX_BYTES = 16 * 1024;
23215
23237
  function spawnSecurity(args) {
23216
23238
  return new Promise((resolve12) => {
@@ -23249,12 +23271,12 @@ var darwinAdapter = {
23249
23271
  reasons.push("tooling-missing");
23250
23272
  }
23251
23273
  try {
23252
- accessSync(manifestDir, constants2.W_OK);
23274
+ accessSync(manifestDir, constants3.W_OK);
23253
23275
  } catch {
23254
23276
  reasons.push("manifest-dir-unwritable");
23255
23277
  }
23256
23278
  try {
23257
- accessSync(caDir, constants2.W_OK);
23279
+ accessSync(caDir, constants3.W_OK);
23258
23280
  } catch {
23259
23281
  reasons.push("keychain-unwritable");
23260
23282
  }
@@ -23268,44 +23290,31 @@ var darwinAdapter = {
23268
23290
  },
23269
23291
  async caTrustInstaller(certPem) {
23270
23292
  const keychain = this.defaultCaInstallPath();
23271
- const tmpPath = join3(tmpdir(), `rogatio-ca-${Date.now()}.crt`);
23272
23293
  try {
23273
- writeFileSync(tmpPath, certPem, "utf8");
23274
- } catch {
23275
- throw new TrustError("trust.internal", "keychain-unwritable", [
23276
- "keychain-unwritable"
23277
- ]);
23278
- }
23279
- const result = await new Promise(
23280
- (resolve12) => {
23281
- const child = spawn("security", [
23294
+ await withInstallCertificateFile(certPem, async (certPath) => {
23295
+ const result = await spawnSecurity([
23282
23296
  "add-trusted-cert",
23283
23297
  "-d",
23284
23298
  "-r",
23285
23299
  "trustRoot",
23286
23300
  "-k",
23287
23301
  keychain,
23288
- tmpPath
23302
+ certPath
23289
23303
  ]);
23290
- let stderr = "";
23291
- child.stderr?.on("data", (data) => {
23292
- stderr += data.toString();
23293
- });
23294
- child.on("close", (code) => resolve12({ code: code ?? 1, stderr }));
23295
- child.on("error", (err) => resolve12({ code: 1, stderr: err.message }));
23296
- }
23297
- );
23298
- try {
23299
- unlinkSync(tmpPath);
23300
- } catch {
23301
- }
23302
- if (result.code !== 0) {
23303
- const stderr = result.stderr.toLowerCase();
23304
- let reason = "keychain-unwritable";
23305
- if (stderr.includes("permission") || stderr.includes("auth")) {
23306
- reason = "keychain-unwritable";
23307
- }
23308
- throw new TrustError("trust.internal", reason, [reason]);
23304
+ if (result.code !== 0) {
23305
+ const stderr = result.stderr.toLowerCase();
23306
+ let reason = "keychain-unwritable";
23307
+ if (stderr.includes("permission") || stderr.includes("auth")) {
23308
+ reason = "keychain-unwritable";
23309
+ }
23310
+ throw new TrustError("trust.internal", reason, [reason]);
23311
+ }
23312
+ });
23313
+ } catch (error) {
23314
+ if (error instanceof TrustError) throw error;
23315
+ throw new TrustError("trust.internal", "keychain-unwritable", [
23316
+ "keychain-unwritable"
23317
+ ]);
23309
23318
  }
23310
23319
  },
23311
23320
  async caTrustRemover() {
@@ -23347,17 +23356,11 @@ var darwinAdapter = {
23347
23356
  ]);
23348
23357
  }
23349
23358
  const keychain = this.defaultCaInstallPath();
23350
- const dir = await mkdtemp(join3(tmpdir(), "rogatio-ca-"));
23351
- const tmpPath = join3(dir, "previous.crt");
23359
+ const dir = await mkdtemp2(join4(tmpdir2(), "rogatio-ca-"));
23360
+ const tmpPath = join4(dir, "previous.crt");
23352
23361
  let failed = false;
23353
23362
  try {
23354
- const handle = await open3(tmpPath, "wx", 384);
23355
- try {
23356
- await handle.writeFile(certPem, "utf8");
23357
- await handle.chmod(384);
23358
- } finally {
23359
- await handle.close();
23360
- }
23363
+ await writeExclusiveCertificate(tmpPath, certPem);
23361
23364
  const trust = await spawnSecurity(["remove-trusted-cert", "-d", tmpPath]);
23362
23365
  if (trust.code !== 0 && !securityMiss(trust.stderr)) failed = true;
23363
23366
  const item = await spawnSecurity([
@@ -23372,7 +23375,7 @@ var darwinAdapter = {
23372
23375
  failed = true;
23373
23376
  } finally {
23374
23377
  try {
23375
- await rm2(dir, { recursive: true, force: true });
23378
+ await rm3(dir, { recursive: true, force: true });
23376
23379
  } catch {
23377
23380
  }
23378
23381
  }
@@ -23410,7 +23413,7 @@ function exists(path2) {
23410
23413
  }
23411
23414
  function writable(path2) {
23412
23415
  try {
23413
- accessSync2(path2, constants3.W_OK);
23416
+ accessSync2(path2, constants4.W_OK);
23414
23417
  return true;
23415
23418
  } catch {
23416
23419
  return false;
@@ -23447,7 +23450,7 @@ function p11kitAnchorsDir() {
23447
23450
  function needsElevation(dir) {
23448
23451
  if (writable(dir)) return false;
23449
23452
  try {
23450
- accessSync2(dir, constants3.W_OK);
23453
+ accessSync2(dir, constants4.W_OK);
23451
23454
  } catch (err) {
23452
23455
  if (nodeCode(err) === "ENOENT" && writable(dirname4(dir))) return false;
23453
23456
  }
@@ -23464,13 +23467,13 @@ function manifestDirFor(browser) {
23464
23467
  }
23465
23468
  function canHostManifest(dir) {
23466
23469
  try {
23467
- accessSync2(dir, constants3.W_OK);
23470
+ accessSync2(dir, constants4.W_OK);
23468
23471
  return true;
23469
23472
  } catch (err) {
23470
23473
  if (nodeCode(err) !== "ENOENT") return false;
23471
23474
  }
23472
23475
  try {
23473
- accessSync2(dirname4(dir), constants3.W_OK);
23476
+ accessSync2(dirname4(dir), constants4.W_OK);
23474
23477
  return true;
23475
23478
  } catch {
23476
23479
  return false;
@@ -23600,7 +23603,7 @@ var linuxAdapter = {
23600
23603
  },
23601
23604
  async caTrustRemover() {
23602
23605
  try {
23603
- unlinkSync2(DEBIAN_CERT);
23606
+ unlinkSync(DEBIAN_CERT);
23604
23607
  } catch {
23605
23608
  }
23606
23609
  if (!toolOnPath("update-ca-trust")) return;
@@ -23661,7 +23664,7 @@ var win32Adapter = {
23661
23664
  reasons.push("tooling-missing");
23662
23665
  }
23663
23666
  try {
23664
- accessSync3(manifestDir, constants4.W_OK);
23667
+ accessSync3(manifestDir, constants5.W_OK);
23665
23668
  } catch {
23666
23669
  reasons.push("manifest-dir-unwritable");
23667
23670
  }
@@ -23674,37 +23677,42 @@ var win32Adapter = {
23674
23677
  };
23675
23678
  },
23676
23679
  async caTrustInstaller(certPem) {
23677
- const tmpPath = join4(tmpdir2(), `rogatio-ca-${Date.now()}.crt`);
23678
23680
  try {
23679
- writeFileSync2(tmpPath, certPem, "utf8");
23680
- } catch {
23681
+ await withInstallCertificateFile(certPem, async (certPath) => {
23682
+ const result = await new Promise(
23683
+ (resolve12) => {
23684
+ const child = spawn3("certutil", [
23685
+ "-addstore",
23686
+ "-f",
23687
+ "Root",
23688
+ certPath
23689
+ ]);
23690
+ let stderr = "";
23691
+ child.stderr?.on("data", (data) => {
23692
+ stderr += data.toString();
23693
+ });
23694
+ child.on("close", (code) => resolve12({ code: code ?? 1, stderr }));
23695
+ child.on(
23696
+ "error",
23697
+ (err) => resolve12({ code: 1, stderr: err.message })
23698
+ );
23699
+ }
23700
+ );
23701
+ if (result.code !== 0) {
23702
+ const stderr = result.stderr.toLowerCase();
23703
+ let reason = "elevation-required";
23704
+ if (stderr.includes("access denied") || stderr.includes("permission")) {
23705
+ reason = "elevation-required";
23706
+ }
23707
+ throw new TrustError("trust.internal", reason, [reason]);
23708
+ }
23709
+ });
23710
+ } catch (error) {
23711
+ if (error instanceof TrustError) throw error;
23681
23712
  throw new TrustError("trust.internal", "elevation-required", [
23682
23713
  "elevation-required"
23683
23714
  ]);
23684
23715
  }
23685
- const result = await new Promise(
23686
- (resolve12) => {
23687
- const child = spawn3("certutil", ["-addstore", "-f", "Root", tmpPath]);
23688
- let stderr = "";
23689
- child.stderr?.on("data", (data) => {
23690
- stderr += data.toString();
23691
- });
23692
- child.on("close", (code) => resolve12({ code: code ?? 1, stderr }));
23693
- child.on("error", (err) => resolve12({ code: 1, stderr: err.message }));
23694
- }
23695
- );
23696
- try {
23697
- unlinkSync3(tmpPath);
23698
- } catch {
23699
- }
23700
- if (result.code !== 0) {
23701
- const stderr = result.stderr.toLowerCase();
23702
- let reason = "elevation-required";
23703
- if (stderr.includes("access denied") || stderr.includes("permission")) {
23704
- reason = "elevation-required";
23705
- }
23706
- throw new TrustError("trust.internal", reason, [reason]);
23707
- }
23708
23716
  },
23709
23717
  async caTrustRemover() {
23710
23718
  const result = await new Promise(
@@ -24644,7 +24652,7 @@ async function resolveConfinedRoot(value) {
24644
24652
  }
24645
24653
  let directory;
24646
24654
  try {
24647
- directory = await open4(value, constants5.O_RDONLY | constants5.O_DIRECTORY);
24655
+ directory = await open4(value, constants6.O_RDONLY | constants6.O_DIRECTORY);
24648
24656
  const info = await directory.stat();
24649
24657
  if (!info.isDirectory()) return failure("runtime.root-invalid");
24650
24658
  return { ok: true, value: await realpath2(value) };
@@ -29678,7 +29686,7 @@ function createServer3(handler, options = {}) {
29678
29686
  }
29679
29687
 
29680
29688
  // packages/cli/src/server/routes.ts
29681
- import { randomBytes as randomBytes4 } from "node:crypto";
29689
+ import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
29682
29690
  import { readFile as readFile5 } from "node:fs/promises";
29683
29691
  import { dirname as dirname9, isAbsolute as isAbsolute5, resolve as resolve5 } from "node:path";
29684
29692
 
@@ -30134,17 +30142,44 @@ function isAllowedEditorHost(host, boundPort) {
30134
30142
  return host === `127.0.0.1:${expected}` || host === `localhost:${expected}`;
30135
30143
  }
30136
30144
  function isAllowedEditorOrigin(origin, boundPort) {
30137
- if (origin === void 0) return true;
30138
- if (!Number.isInteger(boundPort) || boundPort <= 0) return false;
30145
+ if (origin === void 0 || !Number.isInteger(boundPort) || boundPort <= 0) {
30146
+ return false;
30147
+ }
30139
30148
  const expected = String(boundPort);
30140
30149
  return origin === `http://127.0.0.1:${expected}` || origin === `http://localhost:${expected}`;
30141
30150
  }
30142
30151
  function generateCsrfToken() {
30143
30152
  return randomBytes4(16).toString("hex");
30144
30153
  }
30154
+ function tokensEqual(provided, expected) {
30155
+ const providedBytes = Buffer.from(provided);
30156
+ const expectedBytes = Buffer.from(expected);
30157
+ if (providedBytes.length !== expectedBytes.length) return false;
30158
+ return timingSafeEqual2(providedBytes, expectedBytes);
30159
+ }
30160
+ function scopedPathname(pathname, sessionToken) {
30161
+ const marker = "/e/";
30162
+ if (!pathname.startsWith(marker)) return void 0;
30163
+ const after = pathname.slice(marker.length);
30164
+ const slash = after.indexOf("/");
30165
+ const candidate = slash === -1 ? after : after.slice(0, slash);
30166
+ if (candidate.length === 0 || !tokensEqual(candidate, sessionToken)) {
30167
+ return void 0;
30168
+ }
30169
+ return slash === -1 ? "/" : after.slice(slash);
30170
+ }
30171
+ function editorSessionPath(token, suffix) {
30172
+ const path2 = suffix.startsWith("/") ? suffix : `/${suffix}`;
30173
+ return `/e/${token}${path2}`;
30174
+ }
30145
30175
  function validateCsrf(req, expectedToken) {
30146
- const token = req.headers["x-csrf-token"];
30147
- return token === expectedToken;
30176
+ const token = headerValue3(req.headers["x-csrf-token"]);
30177
+ if (token === void 0) return false;
30178
+ return tokensEqual(token, expectedToken);
30179
+ }
30180
+ function emptyNotFound(res) {
30181
+ res.writeHead(404);
30182
+ res.end();
30148
30183
  }
30149
30184
  var MAX_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
30150
30185
  var RequestBodyError = class extends Error {
@@ -30249,8 +30284,13 @@ function createRoutes(context) {
30249
30284
  );
30250
30285
  return;
30251
30286
  }
30252
- const { pathname } = parseUrl(req);
30287
+ const { pathname: requestPath } = parseUrl(req);
30253
30288
  const method = req.method || "GET";
30289
+ const pathname = scopedPathname(requestPath, context.csrfToken);
30290
+ if (pathname === void 0) {
30291
+ emptyNotFound(res);
30292
+ return;
30293
+ }
30254
30294
  if (method === "POST") {
30255
30295
  const origin = headerValue3(req.headers.origin);
30256
30296
  if (!isAllowedEditorOrigin(origin, context.boundPort)) {
@@ -30271,7 +30311,8 @@ function createRoutes(context) {
30271
30311
  }
30272
30312
  if (pathname === "/editor.html" && method === "GET") {
30273
30313
  res.writeHead(200, {
30274
- "Content-Type": "text/html; charset=utf-8"
30314
+ "Content-Type": "text/html; charset=utf-8",
30315
+ "Referrer-Policy": "no-referrer"
30275
30316
  });
30276
30317
  res.end(context.editorHtml);
30277
30318
  return;
@@ -31016,18 +31057,13 @@ async function editCommand(args, options = {}) {
31016
31057
  }
31017
31058
  context.boundPort = server.port;
31018
31059
  const serverUrl = `http://127.0.0.1:${server.port}`;
31019
- const editorUrl = `${serverUrl}/editor.html`;
31060
+ const editorUrl = `${serverUrl}${editorSessionPath(csrfToken, "/editor.html")}`;
31020
31061
  const {
31021
31062
  bundle: editorBundlePath,
31022
31063
  css: editorCssPath,
31023
31064
  fonts: editorFontsPath
31024
31065
  } = editorAssetPaths();
31025
- context.editorHtml = generateEditorHtml(
31026
- serverUrl,
31027
- csrfToken,
31028
- filePath,
31029
- !!aiClient
31030
- );
31066
+ context.editorHtml = generateEditorHtml(filePath, !!aiClient);
31031
31067
  context.editorBundlePath = editorBundlePath;
31032
31068
  context.editorCssPath = editorCssPath;
31033
31069
  context.editorFontsPath = editorFontsPath;
@@ -31092,7 +31128,7 @@ async function ensureCivilizationProjectName(storage, filePath, data) {
31092
31128
  await storage.update(filePath, named);
31093
31129
  return named;
31094
31130
  }
31095
- function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
31131
+ function generateEditorHtml(filePath, aiConfigured) {
31096
31132
  const aiAssistHandler = aiConfigured ? `
31097
31133
  aiAssist: async function(request) {
31098
31134
  const res = await fetch(apiBase + '/api/ai/assist', {
@@ -31121,7 +31157,7 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
31121
31157
  <meta charset="UTF-8">
31122
31158
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
31123
31159
  <title>Rogatio Editor</title>
31124
- <link rel="stylesheet" href="/vendor/editor.css" />
31160
+ <link rel="stylesheet" href="vendor/editor.css" />
31125
31161
  <style>
31126
31162
  html, body { margin: 0; min-height: 100%; }
31127
31163
  body {
@@ -31149,14 +31185,16 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
31149
31185
  <div id="editor-doctor" hidden></div>
31150
31186
  <div id="editor-root"></div>
31151
31187
  <script type="importmap">
31152
- { "imports": { "@rogatio/editor": "/vendor/editor.js" } }
31188
+ { "imports": { "@rogatio/editor": "./vendor/editor.js" } }
31153
31189
  </script>
31154
31190
  <script type="module">
31155
31191
  import { createEditor } from '@rogatio/editor';
31156
31192
 
31157
31193
  const root = document.getElementById('editor-root');
31158
- const apiBase = ${JSON.stringify(apiBase)};
31159
- const csrfToken = ${JSON.stringify(csrfToken)};
31194
+ const apiBase = location.pathname.endsWith('/editor.html')
31195
+ ? location.pathname.slice(0, -'/editor.html'.length)
31196
+ : location.pathname;
31197
+ const csrfToken = apiBase.slice(apiBase.lastIndexOf('/') + 1);
31160
31198
  const filePath = ${embedJson(filePath)};
31161
31199
  const doctorTitles = ${embedJson(editorPageTitles())};
31162
31200
  const serverStoppedJson = ${embedJson(serializeEditorDiagnostics(editorServerStoppedReport(filePath)))};
@@ -33467,7 +33505,7 @@ function errorMessage(error) {
33467
33505
  }
33468
33506
 
33469
33507
  // packages/cli/src/commands/runtime.ts
33470
- import { chmod, mkdir as mkdir5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
33508
+ import { chmod, mkdir as mkdir5, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
33471
33509
  import { dirname as dirname10, join as join9, resolve as resolve8 } from "node:path";
33472
33510
  function generateHostWrapper(rogatioCommand, logPath) {
33473
33511
  const parts = rogatioCommand.split(" ");
@@ -33578,11 +33616,11 @@ Note: reload the extension at chrome://extensions for the native host to be dete
33578
33616
  case "uninstall": {
33579
33617
  const uninstallResult = await controller.uninstall();
33580
33618
  try {
33581
- await rm3(hostPath, { force: true });
33619
+ await rm4(hostPath, { force: true });
33582
33620
  } catch {
33583
33621
  }
33584
33622
  try {
33585
- await rm3(join9(installRoot, "runtime-host.log"), { force: true });
33623
+ await rm4(join9(installRoot, "runtime-host.log"), { force: true });
33586
33624
  } catch {
33587
33625
  }
33588
33626
  return reportTrust(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.20.1",
3
+ "version": "6.20.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.",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "ajv": "8.20.0",
44
- "koffi": "3.3.2"
44
+ "koffi": "3.3.3"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@rogatio/compiler": "workspace:*",
@@ -50,8 +50,8 @@
50
50
  "@rogatio/requestly-import": "workspace:*",
51
51
  "@rogatio/runtime": "workspace:*",
52
52
  "@rogatio/schema": "workspace:*",
53
- "@types/node": "26.6.4",
53
+ "@types/node": "26.6.5",
54
54
  "typescript": "7.0.2",
55
- "vitest": "5.0.3"
55
+ "vitest": "5.0.4"
56
56
  }
57
57
  }