@rogatio/cli 1.0.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 +3928 -0
  2. package/package.json +51 -0
@@ -0,0 +1,3928 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/src/index.ts
4
+ import { realpathSync } from "node:fs";
5
+ import { dirname as dirname6, resolve as resolve6 } from "node:path";
6
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
7
+
8
+ // packages/cli/src/commands/edit.ts
9
+ import { dirname as dirname2, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ // packages/cli/src/server/http.ts
13
+ import { randomInt } from "node:crypto";
14
+ import {
15
+ createServer as createHttpServer
16
+ } from "node:http";
17
+ var HttpServerError = class extends Error {
18
+ code;
19
+ constructor(code, message, cause) {
20
+ super(message, { cause });
21
+ this.name = "HttpServerError";
22
+ this.code = code;
23
+ }
24
+ };
25
+ function createServer(handler, options = {}) {
26
+ const server = createHttpServer(handler);
27
+ const preferredPort = options.port;
28
+ let port = null;
29
+ let started = false;
30
+ async function listenOn(candidatePort) {
31
+ await new Promise((resolve7, reject) => {
32
+ server.once("error", reject);
33
+ server.listen(candidatePort, "127.0.0.1", () => {
34
+ server.off("error", reject);
35
+ resolve7();
36
+ });
37
+ });
38
+ }
39
+ return {
40
+ get port() {
41
+ if (port === null)
42
+ throw new HttpServerError("not-started", "Server not started");
43
+ return port;
44
+ },
45
+ async start() {
46
+ if (started) return;
47
+ if (preferredPort !== void 0) {
48
+ try {
49
+ await listenOn(preferredPort);
50
+ port = preferredPort;
51
+ started = true;
52
+ return;
53
+ } catch (e) {
54
+ throw new HttpServerError(
55
+ "listen-failed",
56
+ `Failed to start server on port ${preferredPort}: ${e}`,
57
+ e
58
+ );
59
+ }
60
+ }
61
+ const maxRetries = 3;
62
+ let lastError = null;
63
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
64
+ const candidatePort = randomInt(1024, 65535);
65
+ try {
66
+ await listenOn(candidatePort);
67
+ port = candidatePort;
68
+ started = true;
69
+ return;
70
+ } catch (e) {
71
+ lastError = e;
72
+ if (e.code !== "EADDRINUSE") {
73
+ throw new HttpServerError(
74
+ "listen-failed",
75
+ `Failed to start server: ${e}`,
76
+ e
77
+ );
78
+ }
79
+ }
80
+ }
81
+ throw new HttpServerError(
82
+ "port-exhausted",
83
+ `Failed to bind to port after ${maxRetries} attempts`,
84
+ lastError ?? void 0
85
+ );
86
+ },
87
+ async stop() {
88
+ if (!started) return;
89
+ await new Promise((resolve7) => {
90
+ server.close(() => resolve7());
91
+ });
92
+ started = false;
93
+ }
94
+ };
95
+ }
96
+
97
+ // packages/cli/src/server/routes.ts
98
+ import { randomBytes } from "node:crypto";
99
+ import { readFile } from "node:fs/promises";
100
+ import { compileProject } from "@rogatio/compiler";
101
+
102
+ // packages/dry-run/dist/node/index.js
103
+ import { compileUrlRegex, HTTP_METHODS, RESOURCE_TYPES } from "@rogatio/schema";
104
+ function parseTestUrl(input) {
105
+ if (typeof input !== "string" || input.length === 0) {
106
+ return { ok: false };
107
+ }
108
+ let url;
109
+ try {
110
+ url = new URL(input);
111
+ } catch {
112
+ return { ok: false };
113
+ }
114
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
115
+ return { ok: false };
116
+ }
117
+ if (url.origin === "null") {
118
+ return { ok: false };
119
+ }
120
+ return { ok: true, value: { origin: url.origin, href: url.href } };
121
+ }
122
+ var DEFAULT_MAX_CASES = 256;
123
+ function invalidCase(index, message = "Test case is invalid") {
124
+ return {
125
+ code: "dryrun.invalid-case",
126
+ message,
127
+ ...index === void 0 ? {} : { index }
128
+ };
129
+ }
130
+ function invalidUrl(index, message = "Test case URL is invalid") {
131
+ return { code: "dryrun.invalid-url", message, index };
132
+ }
133
+ function invalidOptions() {
134
+ return invalidCase(void 0, "Dry-run options are invalid");
135
+ }
136
+ function ownPropertyNames(value) {
137
+ try {
138
+ const names = Object.getOwnPropertyNames(value);
139
+ const symbols = Object.getOwnPropertySymbols(value);
140
+ return symbols.length === 0 ? names : null;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ function dataProperty(value, key) {
146
+ try {
147
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
148
+ if (descriptor === void 0) return { present: false, valid: true };
149
+ if (!("value" in descriptor) || descriptor.enumerable === false) {
150
+ return { present: true, valid: false };
151
+ }
152
+ return { present: true, value: descriptor.value, valid: true };
153
+ } catch {
154
+ return { present: true, valid: false };
155
+ }
156
+ }
157
+ function validateCase(raw, index) {
158
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
159
+ return { ok: false, error: invalidCase(index) };
160
+ }
161
+ try {
162
+ const prototype = Object.getPrototypeOf(raw);
163
+ if (prototype !== Object.prototype && prototype !== null) {
164
+ return { ok: false, error: invalidCase(index) };
165
+ }
166
+ } catch {
167
+ return { ok: false, error: invalidCase(index) };
168
+ }
169
+ const names = ownPropertyNames(raw);
170
+ if (names === null) return { ok: false, error: invalidCase(index) };
171
+ const allowed = /* @__PURE__ */ new Set(["url", "method", "resourceType"]);
172
+ if (names.some((name) => !allowed.has(name))) {
173
+ return { ok: false, error: invalidCase(index) };
174
+ }
175
+ const urlProperty = dataProperty(raw, "url");
176
+ if (!urlProperty.valid) return { ok: false, error: invalidCase(index) };
177
+ if (!urlProperty.present) {
178
+ return { ok: false, error: invalidCase(index) };
179
+ }
180
+ if (typeof urlProperty.value !== "string") {
181
+ return { ok: false, error: invalidUrl(index) };
182
+ }
183
+ const url = urlProperty.value;
184
+ if (url.length === 0) return { ok: false, error: invalidUrl(index) };
185
+ const methodProperty = dataProperty(raw, "method");
186
+ const resourceTypeProperty = dataProperty(raw, "resourceType");
187
+ if (!methodProperty.valid || !resourceTypeProperty.valid) {
188
+ return { ok: false, error: invalidCase(index) };
189
+ }
190
+ const method = methodProperty.value;
191
+ if (methodProperty.present && (typeof method !== "string" || !HTTP_METHODS.includes(method))) {
192
+ return { ok: false, error: invalidCase(index) };
193
+ }
194
+ const resourceType = resourceTypeProperty.value;
195
+ if (resourceTypeProperty.present && (typeof resourceType !== "string" || !RESOURCE_TYPES.includes(resourceType))) {
196
+ return { ok: false, error: invalidCase(index) };
197
+ }
198
+ return {
199
+ ok: true,
200
+ value: {
201
+ url,
202
+ method,
203
+ resourceType
204
+ }
205
+ };
206
+ }
207
+ function safePreview(fn, operation, url, testCase) {
208
+ try {
209
+ return fn(operation, url, testCase);
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+ function buildDimension(state, detail) {
215
+ return {
216
+ state,
217
+ matched: state === "not-applicable" ? null : state === "matched",
218
+ detail
219
+ };
220
+ }
221
+ function emptySummary() {
222
+ return {
223
+ caseCount: 0,
224
+ urlCount: 0,
225
+ matchedUrlCount: 0,
226
+ matchedRuleTotal: 0
227
+ };
228
+ }
229
+ function normalizeOptions(raw) {
230
+ if (raw === void 0) {
231
+ return { ok: true, maxCases: DEFAULT_MAX_CASES };
232
+ }
233
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
234
+ return { ok: false };
235
+ }
236
+ try {
237
+ const prototype = Object.getPrototypeOf(raw);
238
+ if (prototype !== Object.prototype && prototype !== null)
239
+ return { ok: false };
240
+ } catch {
241
+ return { ok: false };
242
+ }
243
+ const names = ownPropertyNames(raw);
244
+ if (names === null || names.some((name) => !["maxCases", "previewAction"].includes(name))) {
245
+ return { ok: false };
246
+ }
247
+ const maxCasesProperty = dataProperty(raw, "maxCases");
248
+ const previewProperty = dataProperty(raw, "previewAction");
249
+ if (!maxCasesProperty.valid || !previewProperty.valid) return { ok: false };
250
+ let maxCases = DEFAULT_MAX_CASES;
251
+ if (maxCasesProperty.present) {
252
+ if (typeof maxCasesProperty.value !== "number" || !Number.isSafeInteger(maxCasesProperty.value) || maxCasesProperty.value <= 0) {
253
+ return { ok: false };
254
+ }
255
+ maxCases = maxCasesProperty.value;
256
+ }
257
+ let previewAction;
258
+ if (previewProperty.present) {
259
+ if (previewProperty.value !== void 0 && typeof previewProperty.value !== "function") {
260
+ return { ok: false };
261
+ }
262
+ previewAction = previewProperty.value;
263
+ }
264
+ return { ok: true, maxCases, previewAction };
265
+ }
266
+ function dryRunProject(operations, cases, options) {
267
+ const normalizedOptions = normalizeOptions(options);
268
+ if (!normalizedOptions.ok) {
269
+ return { results: [], errors: [invalidOptions()], summary: emptySummary() };
270
+ }
271
+ const errors = [];
272
+ if (!Array.isArray(cases)) {
273
+ return {
274
+ results: [],
275
+ errors: [invalidCase(void 0, "Test cases must be an array")],
276
+ summary: emptySummary()
277
+ };
278
+ }
279
+ let caseCount;
280
+ try {
281
+ caseCount = cases.length;
282
+ if (caseCount > normalizedOptions.maxCases) {
283
+ return {
284
+ results: [],
285
+ errors: [
286
+ {
287
+ code: "dryrun.batch-limit",
288
+ message: `Test batch exceeds maxCases (${normalizedOptions.maxCases})`
289
+ }
290
+ ],
291
+ summary: emptySummary()
292
+ };
293
+ }
294
+ for (let index = 0; index < caseCount; index += 1) {
295
+ if (!Object.hasOwn(cases, index)) {
296
+ return {
297
+ results: [],
298
+ errors: [invalidCase(index)],
299
+ summary: emptySummary()
300
+ };
301
+ }
302
+ }
303
+ } catch {
304
+ return { results: [], errors: [invalidCase()], summary: emptySummary() };
305
+ }
306
+ const validCases = [];
307
+ for (let index = 0; index < caseCount; index += 1) {
308
+ let raw;
309
+ try {
310
+ raw = cases[index];
311
+ } catch {
312
+ errors.push(invalidCase(index));
313
+ continue;
314
+ }
315
+ const result = validateCase(raw, index);
316
+ if (result.ok) {
317
+ validCases.push({ value: result.value, index });
318
+ } else {
319
+ errors.push(result.error);
320
+ }
321
+ }
322
+ const regexCache = /* @__PURE__ */ new Map();
323
+ const getRegex = (source) => {
324
+ let cached = regexCache.get(source);
325
+ if (cached === void 0) {
326
+ cached = compileUrlRegex(source);
327
+ regexCache.set(source, cached);
328
+ }
329
+ return cached;
330
+ };
331
+ const results = [];
332
+ let matchedUrlCount = 0;
333
+ let matchedRuleTotal = 0;
334
+ for (const validCase of validCases) {
335
+ const testCase = validCase.value;
336
+ const parsed = parseTestUrl(testCase.url);
337
+ if (!parsed.ok) {
338
+ errors.push(invalidUrl(validCase.index));
339
+ continue;
340
+ }
341
+ const rules = [];
342
+ for (const op of operations) {
343
+ const matcher = op.matcher;
344
+ const regex = getRegex(matcher.urlRegex.source);
345
+ const regexState = regex ? regex.test(testCase.url) ? "matched" : "unmatched" : "unmatched";
346
+ const originState = matcher.origins.includes(
347
+ parsed.value.origin
348
+ ) ? "matched" : "unmatched";
349
+ const methodState = testCase.method === void 0 ? "not-applicable" : matcher.method === void 0 || matcher.method === testCase.method ? "matched" : "unmatched";
350
+ const resourceState = testCase.resourceType === void 0 ? "not-applicable" : matcher.resourceTypes.length === 0 || matcher.resourceTypes.includes(testCase.resourceType) ? "matched" : "unmatched";
351
+ const matched = regexState === "matched" && originState === "matched" && methodState !== "unmatched" && resourceState !== "unmatched";
352
+ const urlRegexDim = buildDimension(
353
+ regexState,
354
+ regexState === "matched" ? `matches /${matcher.urlRegex.source}/` : `does not match /${matcher.urlRegex.source}/`
355
+ );
356
+ const originDim = buildDimension(
357
+ originState,
358
+ originState === "matched" ? `origin ${parsed.value.origin} in [${matcher.origins.join(", ")}]` : `origin ${parsed.value.origin} not in [${matcher.origins.join(", ")}]`
359
+ );
360
+ const methodDim = buildDimension(
361
+ methodState,
362
+ methodState === "not-applicable" ? "method not specified" : methodState === "matched" ? matcher.method === void 0 ? "rule has no method constraint" : `method ${testCase.method} matches` : `rule method ${matcher.method} != ${testCase.method}`
363
+ );
364
+ const resourceDim = buildDimension(
365
+ resourceState,
366
+ resourceState === "not-applicable" ? "resource type not specified" : resourceState === "matched" ? matcher.resourceTypes.length === 0 ? "rule has no resource type constraint" : `resource type ${testCase.resourceType} matches` : `rule resource types [${matcher.resourceTypes.join(", ")}] exclude ${testCase.resourceType}`
367
+ );
368
+ const actionPreview = normalizedOptions.previewAction ? safePreview(
369
+ normalizedOptions.previewAction,
370
+ op,
371
+ testCase.url,
372
+ testCase
373
+ ) : null;
374
+ rules.push({
375
+ groupId: op.groupId,
376
+ ruleId: op.ruleId,
377
+ matched,
378
+ urlRegex: urlRegexDim,
379
+ effectiveOrigin: originDim,
380
+ method: methodDim,
381
+ resourceType: resourceDim,
382
+ actionPreview
383
+ });
384
+ }
385
+ const matchedRuleCount = rules.filter((rule) => rule.matched).length;
386
+ results.push({
387
+ url: testCase.url,
388
+ rules,
389
+ matchedRuleCount
390
+ });
391
+ if (matchedRuleCount > 0) {
392
+ matchedUrlCount += 1;
393
+ }
394
+ matchedRuleTotal += matchedRuleCount;
395
+ }
396
+ return {
397
+ results,
398
+ errors,
399
+ summary: {
400
+ caseCount: validCases.length,
401
+ urlCount: results.length,
402
+ matchedUrlCount,
403
+ matchedRuleTotal
404
+ }
405
+ };
406
+ }
407
+
408
+ // packages/cli/src/server/routes.ts
409
+ import { validateProjectDetailed } from "@rogatio/schema";
410
+
411
+ // packages/cli/src/utils/mock-preview.ts
412
+ import { basename } from "node:path";
413
+ function createMockPreviewAction(operations) {
414
+ const mockByRuleId = /* @__PURE__ */ new Map();
415
+ for (const operation of operations) {
416
+ if (operation.kind === "mock") {
417
+ mockByRuleId.set(operation.ruleId, operation);
418
+ }
419
+ }
420
+ return (operation, _url, _testCase) => {
421
+ const mock = mockByRuleId.get(operation.ruleId);
422
+ if (mock === void 0) return null;
423
+ if (mock.mock.body !== void 0) {
424
+ return {
425
+ kind: "mock",
426
+ summary: `Mock ${mock.mock.status} (inline body, ${Buffer.byteLength(
427
+ mock.mock.body,
428
+ "utf8"
429
+ )} bytes)`
430
+ };
431
+ }
432
+ return {
433
+ kind: "mock",
434
+ summary: `Mock ${mock.mock.status} (file snapshot: ${basename(
435
+ mock.mock.file ?? ""
436
+ )})`
437
+ };
438
+ };
439
+ }
440
+
441
+ // packages/cli/src/server/routes.ts
442
+ function generateCsrfToken() {
443
+ return randomBytes(16).toString("hex");
444
+ }
445
+ function validateCsrf(req, expectedToken) {
446
+ const token = req.headers["x-csrf-token"];
447
+ return token === expectedToken;
448
+ }
449
+ var MAX_REQUEST_BODY_BYTES = 8 * 1024 * 1024;
450
+ var RequestBodyError = class extends Error {
451
+ code = "request-body-too-large";
452
+ };
453
+ function getRequestBody(req) {
454
+ return new Promise((resolve7, reject) => {
455
+ let body = "";
456
+ let bytes = 0;
457
+ let settled = false;
458
+ const contentLength = req.headers["content-length"];
459
+ const declaredLength = Array.isArray(contentLength) ? Number(contentLength[0]) : Number(contentLength);
460
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_REQUEST_BODY_BYTES) {
461
+ reject(new RequestBodyError());
462
+ return;
463
+ }
464
+ req.on("data", (chunk) => {
465
+ if (settled) return;
466
+ try {
467
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
468
+ bytes += Buffer.byteLength(text);
469
+ if (bytes > MAX_REQUEST_BODY_BYTES) {
470
+ settled = true;
471
+ reject(new RequestBodyError());
472
+ req.resume();
473
+ return;
474
+ }
475
+ body += text;
476
+ } catch (error) {
477
+ settled = true;
478
+ reject(error);
479
+ }
480
+ });
481
+ req.on("end", () => {
482
+ if (!settled) {
483
+ settled = true;
484
+ resolve7(body);
485
+ }
486
+ });
487
+ req.on("error", (error) => {
488
+ if (!settled) {
489
+ settled = true;
490
+ reject(error);
491
+ }
492
+ });
493
+ });
494
+ }
495
+ function bodyErrorResponse(error) {
496
+ if (error instanceof RequestBodyError) {
497
+ return {
498
+ status: 413,
499
+ body: {
500
+ code: error.code,
501
+ message: "Request body exceeds the maximum size"
502
+ }
503
+ };
504
+ }
505
+ return {
506
+ status: 400,
507
+ body: { code: "invalid-json", message: "Invalid JSON body" }
508
+ };
509
+ }
510
+ function toMatcherOperations(operations) {
511
+ return operations.map(({ groupId, ruleId, matcher }) => ({
512
+ kind: "matcher",
513
+ groupId,
514
+ ruleId,
515
+ matcher
516
+ }));
517
+ }
518
+ function isRecord(value) {
519
+ return typeof value === "object" && value !== null && !Array.isArray(value);
520
+ }
521
+ function parseUrl(req) {
522
+ const url = new URL(
523
+ req.url || "/",
524
+ `http://${req.headers.host || "localhost"}`
525
+ );
526
+ return { pathname: url.pathname, searchParams: url.searchParams };
527
+ }
528
+ function parseDiagnostics(result) {
529
+ if (!result.valid) {
530
+ return result.errors.map((e) => ({
531
+ code: `schema.${e.keyword}`,
532
+ severity: "error",
533
+ path: e.instancePath || "/",
534
+ message: e.message,
535
+ params: e.params
536
+ }));
537
+ }
538
+ return [];
539
+ }
540
+ function parseCompilerDiagnostics(result) {
541
+ if (!result.ok) {
542
+ return result.diagnostics.map((d) => ({
543
+ code: d.code,
544
+ severity: d.severity,
545
+ path: d.path,
546
+ message: d.message,
547
+ params: d.params
548
+ }));
549
+ }
550
+ return [];
551
+ }
552
+ function createRoutes(context) {
553
+ return async function handleRequest(req, res) {
554
+ const { pathname } = parseUrl(req);
555
+ const method = req.method || "GET";
556
+ const corsHeaders = {
557
+ "Access-Control-Allow-Origin": "http://127.0.0.1:*",
558
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
559
+ "Access-Control-Allow-Headers": "Content-Type, X-CSRF-Token"
560
+ };
561
+ if (method === "OPTIONS") {
562
+ res.writeHead(204, corsHeaders);
563
+ res.end();
564
+ return;
565
+ }
566
+ if (pathname === "/editor.html" && method === "GET") {
567
+ res.writeHead(200, {
568
+ "Content-Type": "text/html; charset=utf-8",
569
+ ...corsHeaders
570
+ });
571
+ res.end(context.editorHtml);
572
+ return;
573
+ }
574
+ if (pathname === "/vendor/editor.js" && method === "GET") {
575
+ try {
576
+ const bundle = await readFile(context.editorBundlePath, "utf-8");
577
+ res.writeHead(200, {
578
+ "Content-Type": "text/javascript; charset=utf-8",
579
+ ...corsHeaders
580
+ });
581
+ res.end(bundle);
582
+ } catch (e) {
583
+ res.writeHead(500, { "Content-Type": "application/json" });
584
+ res.end(
585
+ JSON.stringify({
586
+ code: "bundle-load-failed",
587
+ message: e instanceof Error ? e.message : "Failed to load editor bundle"
588
+ })
589
+ );
590
+ }
591
+ return;
592
+ }
593
+ if (pathname === "/api/project" && method === "GET") {
594
+ res.writeHead(200, { "Content-Type": "application/json" });
595
+ res.end(JSON.stringify(context.project));
596
+ return;
597
+ }
598
+ if (pathname === "/api/validate" && method === "POST") {
599
+ if (!validateCsrf(req, context.csrfToken)) {
600
+ res.writeHead(403, { "Content-Type": "application/json" });
601
+ res.end(
602
+ JSON.stringify({
603
+ code: "csrf-invalid",
604
+ message: "Invalid CSRF token"
605
+ })
606
+ );
607
+ return;
608
+ }
609
+ let body;
610
+ try {
611
+ const bodyText = await getRequestBody(req);
612
+ body = JSON.parse(bodyText);
613
+ } catch (error) {
614
+ const failure2 = bodyErrorResponse(error);
615
+ res.writeHead(failure2.status, { "Content-Type": "application/json" });
616
+ res.end(JSON.stringify(failure2.body));
617
+ return;
618
+ }
619
+ const schemaResult = validateProjectDetailed(body);
620
+ const diagnostics = [...parseDiagnostics(schemaResult)];
621
+ if (schemaResult.valid) {
622
+ const compileResult = compileProject(schemaResult.data);
623
+ diagnostics.push(...parseCompilerDiagnostics(compileResult));
624
+ }
625
+ res.writeHead(200, { "Content-Type": "application/json" });
626
+ res.end(JSON.stringify({ diagnostics }));
627
+ return;
628
+ }
629
+ if (pathname === "/api/save" && method === "POST") {
630
+ if (!validateCsrf(req, context.csrfToken)) {
631
+ res.writeHead(403, { "Content-Type": "application/json" });
632
+ res.end(
633
+ JSON.stringify({
634
+ code: "csrf-invalid",
635
+ message: "Invalid CSRF token"
636
+ })
637
+ );
638
+ return;
639
+ }
640
+ let body;
641
+ try {
642
+ const bodyText = await getRequestBody(req);
643
+ body = JSON.parse(bodyText);
644
+ } catch (error) {
645
+ const failure2 = bodyErrorResponse(error);
646
+ res.writeHead(failure2.status, { "Content-Type": "application/json" });
647
+ res.end(JSON.stringify(failure2.body));
648
+ return;
649
+ }
650
+ const schemaResult = validateProjectDetailed(body);
651
+ if (!schemaResult.valid) {
652
+ res.writeHead(400, { "Content-Type": "application/json" });
653
+ res.end(
654
+ JSON.stringify({
655
+ code: "validation-failed",
656
+ message: "Project validation failed",
657
+ diagnostics: parseDiagnostics(schemaResult)
658
+ })
659
+ );
660
+ return;
661
+ }
662
+ const compileResult = compileProject(schemaResult.data);
663
+ if (!compileResult.ok) {
664
+ res.writeHead(400, { "Content-Type": "application/json" });
665
+ res.end(
666
+ JSON.stringify({
667
+ code: "compilation-failed",
668
+ message: "Project compilation failed",
669
+ diagnostics: parseCompilerDiagnostics(compileResult)
670
+ })
671
+ );
672
+ return;
673
+ }
674
+ try {
675
+ await context.writeProject(context.filePath, body);
676
+ context.project = body;
677
+ res.writeHead(200, { "Content-Type": "application/json" });
678
+ res.end(JSON.stringify({ ok: true }));
679
+ return;
680
+ } catch (e) {
681
+ res.writeHead(500, { "Content-Type": "application/json" });
682
+ res.end(
683
+ JSON.stringify({
684
+ code: "write-failed",
685
+ message: e instanceof Error ? e.message : "Failed to write file"
686
+ })
687
+ );
688
+ return;
689
+ }
690
+ }
691
+ if (pathname === "/api/cancel" && method === "POST") {
692
+ if (!validateCsrf(req, context.csrfToken)) {
693
+ res.writeHead(403, { "Content-Type": "application/json" });
694
+ res.end(
695
+ JSON.stringify({
696
+ code: "csrf-invalid",
697
+ message: "Invalid CSRF token"
698
+ })
699
+ );
700
+ return;
701
+ }
702
+ context.shutdown();
703
+ res.writeHead(200, { "Content-Type": "application/json" });
704
+ res.end(JSON.stringify({ ok: true }));
705
+ return;
706
+ }
707
+ if (pathname === "/api/dry-run" && method === "POST") {
708
+ if (!validateCsrf(req, context.csrfToken)) {
709
+ res.writeHead(403, { "Content-Type": "application/json" });
710
+ res.end(
711
+ JSON.stringify({
712
+ code: "csrf-invalid",
713
+ message: "Invalid CSRF token"
714
+ })
715
+ );
716
+ return;
717
+ }
718
+ let body;
719
+ try {
720
+ const bodyText = await getRequestBody(req);
721
+ body = JSON.parse(bodyText);
722
+ } catch (error) {
723
+ const failure2 = bodyErrorResponse(error);
724
+ res.writeHead(failure2.status, { "Content-Type": "application/json" });
725
+ res.end(JSON.stringify(failure2.body));
726
+ return;
727
+ }
728
+ if (!isRecord(body) || !isRecord(body.project)) {
729
+ res.writeHead(400, { "Content-Type": "application/json" });
730
+ res.end(
731
+ JSON.stringify({
732
+ code: "invalid-project",
733
+ message: "Missing or invalid project"
734
+ })
735
+ );
736
+ return;
737
+ }
738
+ if (!Array.isArray(body.cases)) {
739
+ res.writeHead(400, { "Content-Type": "application/json" });
740
+ res.end(
741
+ JSON.stringify({
742
+ code: "invalid-cases",
743
+ message: "Cases must be an array"
744
+ })
745
+ );
746
+ return;
747
+ }
748
+ const schemaResult = validateProjectDetailed(body.project);
749
+ if (!schemaResult.valid) {
750
+ res.writeHead(400, { "Content-Type": "application/json" });
751
+ res.end(
752
+ JSON.stringify({
753
+ code: "validation-failed",
754
+ message: "Project validation failed",
755
+ diagnostics: parseDiagnostics(schemaResult)
756
+ })
757
+ );
758
+ return;
759
+ }
760
+ const compileResult = compileProject(schemaResult.data);
761
+ if (!compileResult.ok) {
762
+ res.writeHead(400, { "Content-Type": "application/json" });
763
+ res.end(
764
+ JSON.stringify({
765
+ code: "compilation-failed",
766
+ message: "Project compilation failed",
767
+ diagnostics: parseCompilerDiagnostics(compileResult)
768
+ })
769
+ );
770
+ return;
771
+ }
772
+ const result = dryRunProject(
773
+ toMatcherOperations(compileResult.operations),
774
+ body.cases,
775
+ {
776
+ ...body.options ?? {},
777
+ previewAction: createMockPreviewAction(compileResult.operations)
778
+ }
779
+ );
780
+ res.writeHead(200, { "Content-Type": "application/json" });
781
+ res.end(JSON.stringify(result));
782
+ return;
783
+ }
784
+ res.writeHead(404, { "Content-Type": "application/json" });
785
+ res.end(JSON.stringify({ code: "not-found", message: "Not found" }));
786
+ };
787
+ }
788
+
789
+ // packages/cli/src/utils/browser.ts
790
+ import { spawn } from "node:child_process";
791
+ var BrowserLaunchError = class extends Error {
792
+ platform;
793
+ constructor(platform, message) {
794
+ super(message);
795
+ this.name = "BrowserLaunchError";
796
+ this.platform = platform;
797
+ }
798
+ };
799
+ async function launchBrowser(url) {
800
+ const platform = process.platform;
801
+ let command;
802
+ let args;
803
+ switch (platform) {
804
+ case "darwin":
805
+ command = "open";
806
+ args = [url];
807
+ break;
808
+ case "win32":
809
+ command = "cmd";
810
+ args = ["/c", "start", "", url];
811
+ break;
812
+ case "linux":
813
+ case "freebsd":
814
+ case "openbsd":
815
+ case "sunos":
816
+ command = "xdg-open";
817
+ args = [url];
818
+ break;
819
+ default:
820
+ throw new BrowserLaunchError(
821
+ platform,
822
+ `Unsupported platform: ${platform}`
823
+ );
824
+ }
825
+ return new Promise((resolve7) => {
826
+ const child = spawn(command, args, {
827
+ detached: true,
828
+ stdio: "ignore"
829
+ });
830
+ child.unref();
831
+ child.on("error", (err) => {
832
+ if (err.code === "ENOENT") {
833
+ resolve7(false);
834
+ } else {
835
+ resolve7(false);
836
+ }
837
+ });
838
+ child.on("close", (code) => {
839
+ resolve7(code === 0);
840
+ });
841
+ });
842
+ }
843
+
844
+ // packages/cli/src/utils/file.ts
845
+ import { randomBytes as randomBytes2 } from "node:crypto";
846
+ import { mkdir, readFile as readFile2, rename, writeFile } from "node:fs/promises";
847
+ import { basename as basename2, dirname, join } from "node:path";
848
+ var ProjectFileError = class extends Error {
849
+ code;
850
+ path;
851
+ constructor(code, path, message, cause) {
852
+ super(message, { cause });
853
+ this.name = "ProjectFileError";
854
+ this.code = code;
855
+ this.path = path;
856
+ }
857
+ };
858
+ async function readProject(path) {
859
+ try {
860
+ const content = await readFile2(path, "utf-8");
861
+ const parsed = JSON.parse(content);
862
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
863
+ throw new ProjectFileError(
864
+ "invalid-format",
865
+ path,
866
+ "Project file must contain a JSON object"
867
+ );
868
+ }
869
+ return parsed;
870
+ } catch (e) {
871
+ if (e instanceof ProjectFileError) throw e;
872
+ if (e.code === "ENOENT") {
873
+ throw new ProjectFileError(
874
+ "not-found",
875
+ path,
876
+ "Project file not found",
877
+ e
878
+ );
879
+ }
880
+ if (e instanceof SyntaxError) {
881
+ throw new ProjectFileError(
882
+ "invalid-json",
883
+ path,
884
+ "Project file contains invalid JSON",
885
+ e
886
+ );
887
+ }
888
+ throw new ProjectFileError(
889
+ "read-failed",
890
+ path,
891
+ "Failed to read project file",
892
+ e
893
+ );
894
+ }
895
+ }
896
+ async function writeProject(path, data) {
897
+ const tempName = `.${basename2(dirname(path))}.${randomBytes2(8).toString("hex")}.tmp`;
898
+ const tempPath = join(dirname(path), tempName);
899
+ try {
900
+ await mkdir(dirname(path), { recursive: true });
901
+ await writeFile(tempPath, JSON.stringify(data, null, 2), "utf-8");
902
+ await rename(tempPath, path);
903
+ } catch (e) {
904
+ try {
905
+ await import("node:fs/promises").then((fs) => fs.unlink(tempPath));
906
+ } catch {
907
+ }
908
+ if (e.code === "EISDIR") {
909
+ throw new ProjectFileError(
910
+ "is-directory",
911
+ path,
912
+ "Target path is a directory",
913
+ e
914
+ );
915
+ }
916
+ throw new ProjectFileError(
917
+ "write-failed",
918
+ path,
919
+ "Failed to write project file",
920
+ e
921
+ );
922
+ }
923
+ }
924
+
925
+ // packages/cli/src/commands/edit.ts
926
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
927
+ async function editCommand(args, options = {}) {
928
+ const customLaunchBrowser = options.launchBrowser;
929
+ const positionalArgs = [];
930
+ let port;
931
+ for (let i = 0; i < args.length; i++) {
932
+ const arg = args[i];
933
+ if (arg === "--port" && i + 1 < args.length) {
934
+ port = parseInt(args[++i], 10);
935
+ if (Number.isNaN(port)) {
936
+ console.error("Error: Invalid port number");
937
+ return { exitCode: Promise.resolve(2), shutdown: () => {
938
+ } };
939
+ }
940
+ } else if (!arg.startsWith("-")) {
941
+ positionalArgs.push(arg);
942
+ } else if (arg === "--help") {
943
+ console.log(`Usage: rogatio edit [options] [path]
944
+ Options:
945
+ --port <n> Fixed port (default: random)
946
+ --help Show help`);
947
+ return { exitCode: Promise.resolve(0), shutdown: () => {
948
+ } };
949
+ } else {
950
+ console.error(`Error: Unknown option: ${arg}`);
951
+ return { exitCode: Promise.resolve(2), shutdown: () => {
952
+ } };
953
+ }
954
+ }
955
+ if (positionalArgs.length > 1) {
956
+ console.error("Error: Too many arguments");
957
+ return { exitCode: Promise.resolve(2), shutdown: () => {
958
+ } };
959
+ }
960
+ let filePath;
961
+ if (positionalArgs[0]) {
962
+ filePath = resolve(positionalArgs[0]);
963
+ } else {
964
+ filePath = resolve(process.cwd(), ".rogatio.json");
965
+ }
966
+ try {
967
+ const stat3 = await import("node:fs/promises").then(
968
+ (fs) => fs.stat(filePath)
969
+ );
970
+ if (stat3.isDirectory()) {
971
+ console.error("Error: Path is a directory");
972
+ return { exitCode: Promise.resolve(2), shutdown: () => {
973
+ } };
974
+ }
975
+ } catch (e) {
976
+ if (e.code !== "ENOENT") {
977
+ console.error(`Error: ${e}`);
978
+ return { exitCode: Promise.resolve(2), shutdown: () => {
979
+ } };
980
+ }
981
+ }
982
+ let projectData;
983
+ let isNewFile = false;
984
+ try {
985
+ projectData = await readProject(filePath);
986
+ } catch (e) {
987
+ if (e instanceof ProjectFileError && e.code === "not-found") {
988
+ projectData = { version: 1, name: "", groups: [] };
989
+ isNewFile = true;
990
+ } else {
991
+ console.error(`Error: ${e}`);
992
+ return { exitCode: Promise.resolve(2), shutdown: () => {
993
+ } };
994
+ }
995
+ }
996
+ if (isNewFile) {
997
+ try {
998
+ await writeProject(filePath, projectData);
999
+ } catch (e) {
1000
+ console.error(`Error writing initial project: ${e}`);
1001
+ return { exitCode: Promise.resolve(2), shutdown: () => {
1002
+ } };
1003
+ }
1004
+ }
1005
+ const csrfToken = generateCsrfToken();
1006
+ const context = {
1007
+ project: projectData,
1008
+ filePath,
1009
+ csrfToken,
1010
+ writeProject,
1011
+ shutdown: () => {
1012
+ shutdown();
1013
+ },
1014
+ editorHtml: "",
1015
+ editorBundlePath: ""
1016
+ };
1017
+ let server;
1018
+ try {
1019
+ server = createServer(
1020
+ createRoutes(context),
1021
+ port !== void 0 ? { port } : {}
1022
+ );
1023
+ await server.start();
1024
+ } catch (e) {
1025
+ console.error(`Error starting server: ${e}`);
1026
+ return { exitCode: Promise.resolve(2), shutdown: () => {
1027
+ } };
1028
+ }
1029
+ const serverUrl = `http://127.0.0.1:${server.port}`;
1030
+ const editorUrl = `${serverUrl}/editor.html`;
1031
+ let editorBundlePath;
1032
+ try {
1033
+ editorBundlePath = fileURLToPath(import.meta.resolve("@rogatio/editor"));
1034
+ } catch (e) {
1035
+ console.error(`Error: cannot resolve @rogatio/editor bundle: ${e}`);
1036
+ await server.stop();
1037
+ return { exitCode: Promise.resolve(2), shutdown: () => {
1038
+ } };
1039
+ }
1040
+ context.editorHtml = generateEditorHtml(serverUrl, csrfToken, filePath);
1041
+ context.editorBundlePath = editorBundlePath;
1042
+ let shutdownCalled = false;
1043
+ function shutdown() {
1044
+ shutdownCalled = true;
1045
+ server.stop();
1046
+ }
1047
+ const browserLaunched = await (customLaunchBrowser ?? launchBrowser)(
1048
+ editorUrl
1049
+ );
1050
+ if (!browserLaunched) {
1051
+ console.log(`Editor available at: ${editorUrl}`);
1052
+ console.log("Open this URL in your browser to edit the project.");
1053
+ }
1054
+ const exitCodePromise = new Promise((resolve7) => {
1055
+ const checkShutdown = setInterval(() => {
1056
+ if (shutdownCalled) {
1057
+ clearInterval(checkShutdown);
1058
+ resolve7(0);
1059
+ }
1060
+ }, 100);
1061
+ const handleSignal = () => {
1062
+ shutdown();
1063
+ };
1064
+ process.on("SIGINT", handleSignal);
1065
+ process.on("SIGTERM", handleSignal);
1066
+ const originalResolve = resolve7;
1067
+ resolve7 = (code) => {
1068
+ clearInterval(checkShutdown);
1069
+ process.off("SIGINT", handleSignal);
1070
+ process.off("SIGTERM", handleSignal);
1071
+ originalResolve(code);
1072
+ };
1073
+ });
1074
+ return {
1075
+ exitCode: exitCodePromise,
1076
+ shutdown
1077
+ };
1078
+ }
1079
+ function generateEditorHtml(apiBase, csrfToken, filePath) {
1080
+ return `<!DOCTYPE html>
1081
+ <html lang="en">
1082
+ <head>
1083
+ <meta charset="UTF-8">
1084
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1085
+ <title>Rogatio Editor</title>
1086
+ <style>
1087
+ body { margin: 0; font-family: system-ui, sans-serif; }
1088
+ #editor-root { width: 100vw; height: 100vh; }
1089
+ </style>
1090
+ </head>
1091
+ <body>
1092
+ <div id="editor-root"></div>
1093
+ <script type="importmap">
1094
+ { "imports": { "@rogatio/editor": "/vendor/editor.js" } }
1095
+ </script>
1096
+ <script type="module">
1097
+ import { createEditor, createMockRuleType, createRedirectRuleType, createResponseBodyRuleType } from '@rogatio/editor';
1098
+
1099
+ const root = document.getElementById('editor-root');
1100
+ const apiBase = '${apiBase}';
1101
+ const csrfToken = '${csrfToken}';
1102
+ const filePath = '${filePath}';
1103
+
1104
+ async function fetchProject() {
1105
+ const res = await fetch(apiBase + '/api/project');
1106
+ return res.json();
1107
+ }
1108
+
1109
+ async function validateProject(project) {
1110
+ const res = await fetch(apiBase + '/api/validate', {
1111
+ method: 'POST',
1112
+ headers: {
1113
+ 'Content-Type': 'application/json',
1114
+ 'X-CSRF-Token': csrfToken,
1115
+ },
1116
+ body: JSON.stringify(project),
1117
+ });
1118
+ return res.json();
1119
+ }
1120
+
1121
+ async function saveProject(project) {
1122
+ const res = await fetch(apiBase + '/api/save', {
1123
+ method: 'POST',
1124
+ headers: {
1125
+ 'Content-Type': 'application/json',
1126
+ 'X-CSRF-Token': csrfToken,
1127
+ },
1128
+ body: JSON.stringify(project),
1129
+ });
1130
+ return res.json();
1131
+ }
1132
+
1133
+ async function cancel() {
1134
+ await fetch(apiBase + '/api/cancel', {
1135
+ method: 'POST',
1136
+ headers: {
1137
+ 'Content-Type': 'application/json',
1138
+ 'X-CSRF-Token': csrfToken,
1139
+ },
1140
+ body: '{}',
1141
+ });
1142
+ }
1143
+
1144
+ const project = await fetchProject();
1145
+
1146
+ const editor = createEditor(root, {
1147
+ ruleTypes: [createRedirectRuleType(), createMockRuleType(), createResponseBodyRuleType()],
1148
+ initialProject: project,
1149
+ validate: async (value) => {
1150
+ const result = await validateProject(value);
1151
+ return result.diagnostics.map((d: any) => ({
1152
+ code: d.code,
1153
+ severity: d.severity,
1154
+ path: d.path,
1155
+ message: d.message,
1156
+ }));
1157
+ },
1158
+ save: async (project) => {
1159
+ const result = await saveProject(project);
1160
+ if (result.ok) {
1161
+ return { ok: true };
1162
+ }
1163
+ return { ok: false, code: result.code, message: result.message };
1164
+ },
1165
+ dryRun: async (currentProject, cases, options) => {
1166
+ const res = await fetch(apiBase + '/api/dry-run', {
1167
+ method: 'POST',
1168
+ headers: {
1169
+ 'Content-Type': 'application/json',
1170
+ 'X-CSRF-Token': csrfToken,
1171
+ },
1172
+ body: JSON.stringify({ project: currentProject, cases, options }),
1173
+ });
1174
+ return res.json();
1175
+ },
1176
+ onCancel: () => {
1177
+ cancel();
1178
+ },
1179
+ });
1180
+ </script>
1181
+ </body>
1182
+ </html>`;
1183
+ }
1184
+
1185
+ // packages/cli/src/commands/runtime.ts
1186
+ import { dirname as dirname4, isAbsolute, join as join3, relative, resolve as resolve3, sep as sep2 } from "node:path";
1187
+ import { compileProject as compileProject2 } from "@rogatio/compiler";
1188
+
1189
+ // packages/runtime/dist/node/index.js
1190
+ import { HTTP_METHODS as HTTP_METHODS2 } from "@rogatio/schema";
1191
+ import { isIP as isIP2 } from "node:net";
1192
+ import { normalizeSiteOrigin } from "@rogatio/schema";
1193
+ import { normalizeSiteOrigin as normalizeSiteOrigin2 } from "@rogatio/schema";
1194
+ import {
1195
+ compileUrlRegex as compileUrlRegex2,
1196
+ normalizeSiteOrigin as normalizeSiteOrigin3
1197
+ } from "@rogatio/schema";
1198
+ import { mkdir as mkdir2, readFile as readFile3, rename as rename2, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
1199
+ import { basename as basename3, dirname as dirname3, isAbsolute as isAbsolute2, join as join2, relative as relative2 } from "node:path";
1200
+ import { generateKeyPairSync } from "node:crypto";
1201
+ import {
1202
+ compileUrlRegex as compileUrlRegex22,
1203
+ HTTP_METHODS as HTTP_METHODS22,
1204
+ LIMITS,
1205
+ normalizeSiteOrigin as normalizeSiteOrigin4,
1206
+ RESOURCE_TYPES as RESOURCE_TYPES2
1207
+ } from "@rogatio/schema";
1208
+ import { createHash as createHash2 } from "node:crypto";
1209
+ import { LIMITS as LIMITS2 } from "@rogatio/schema";
1210
+ import { LIMITS as LIMITS3 } from "@rogatio/schema";
1211
+ import { createServer as createServer2 } from "node:http";
1212
+ import { isAbsolute as isAbsolute4 } from "node:path";
1213
+ import { createHash as createHash3, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
1214
+ import { randomBytes as randomBytes22 } from "node:crypto";
1215
+ import { readFile as readFile22, realpath as realpath2, stat as stat2 } from "node:fs/promises";
1216
+ import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve2, sep } from "node:path";
1217
+ function runtimeError(code) {
1218
+ return { code };
1219
+ }
1220
+ function failure(code) {
1221
+ return { ok: false, error: runtimeError(code) };
1222
+ }
1223
+ function hasControl(value) {
1224
+ for (let index = 0; index < value.length; index += 1) {
1225
+ const code = value.charCodeAt(index);
1226
+ if (code <= 31 || code === 127) return true;
1227
+ }
1228
+ return false;
1229
+ }
1230
+ function normalizeLogicalPath(value) {
1231
+ if (typeof value !== "string" || value.length === 0) return null;
1232
+ if (value.includes("\\") || value.includes("%") || hasControl(value)) {
1233
+ return null;
1234
+ }
1235
+ if (value.startsWith("/") || value.endsWith("/") || value.includes("//")) {
1236
+ return null;
1237
+ }
1238
+ const parts = value.split("/");
1239
+ if (parts.some(
1240
+ (part) => part.length === 0 || part === "." || part === ".." || part.includes(":") || /[*?[\]]/.test(part)
1241
+ )) {
1242
+ return null;
1243
+ }
1244
+ return parts.join("/");
1245
+ }
1246
+ var MAX_ARRAY_LENGTH = 4096;
1247
+ var MAX_OBJECT_PROPERTIES = 256;
1248
+ function snapshotOwnData(value, ancestors = /* @__PURE__ */ new WeakSet()) {
1249
+ if (value === null || typeof value !== "object") {
1250
+ return { valid: true, value };
1251
+ }
1252
+ if (ancestors.has(value)) return { valid: false };
1253
+ ancestors.add(value);
1254
+ try {
1255
+ if (Object.getOwnPropertySymbols(value).length > 0) {
1256
+ return { valid: false };
1257
+ }
1258
+ if (Array.isArray(value)) {
1259
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
1260
+ if (lengthDescriptor === void 0 || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0 || lengthDescriptor.value > MAX_ARRAY_LENGTH) {
1261
+ return { valid: false };
1262
+ }
1263
+ const length = lengthDescriptor.value;
1264
+ for (const propertyName of Object.getOwnPropertyNames(value)) {
1265
+ if (propertyName === "length") continue;
1266
+ const index = Number(propertyName);
1267
+ if (!Number.isInteger(index) || index < 0 || index >= length || String(index) !== propertyName) {
1268
+ return { valid: false };
1269
+ }
1270
+ }
1271
+ const snapshot2 = new Array(length);
1272
+ for (let index = 0; index < length; index += 1) {
1273
+ const descriptor = Object.getOwnPropertyDescriptor(
1274
+ value,
1275
+ String(index)
1276
+ );
1277
+ if (descriptor === void 0 || !("value" in descriptor) || !descriptor.enumerable) {
1278
+ return { valid: false };
1279
+ }
1280
+ const child = snapshotOwnData(descriptor.value, ancestors);
1281
+ if (!child.valid) return child;
1282
+ snapshot2[index] = child.value;
1283
+ }
1284
+ return { valid: true, value: snapshot2 };
1285
+ }
1286
+ const names = Object.getOwnPropertyNames(value);
1287
+ if (names.length > MAX_OBJECT_PROPERTIES) return { valid: false };
1288
+ const snapshot = /* @__PURE__ */ Object.create(null);
1289
+ for (const key of names) {
1290
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1291
+ if (descriptor === void 0 || !("value" in descriptor) || !descriptor.enumerable) {
1292
+ return { valid: false };
1293
+ }
1294
+ const child = snapshotOwnData(descriptor.value, ancestors);
1295
+ if (!child.valid) return child;
1296
+ Object.defineProperty(snapshot, key, {
1297
+ configurable: true,
1298
+ enumerable: true,
1299
+ value: child.value,
1300
+ writable: true
1301
+ });
1302
+ }
1303
+ return { valid: true, value: snapshot };
1304
+ } catch {
1305
+ return { valid: false };
1306
+ } finally {
1307
+ ancestors.delete(value);
1308
+ }
1309
+ }
1310
+ function hasOwn(value, key) {
1311
+ return Object.hasOwn(value, key);
1312
+ }
1313
+ var ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
1314
+ function hasControl2(value) {
1315
+ for (let index = 0; index < value.length; index += 1) {
1316
+ const code = value.charCodeAt(index);
1317
+ if (code <= 32 || code === 127) return true;
1318
+ }
1319
+ return false;
1320
+ }
1321
+ function hasValidPercentEncoding(value) {
1322
+ for (let index = 0; index < value.length; index += 1) {
1323
+ if (value[index] !== "%") continue;
1324
+ if (!/^[0-9a-fA-F]{2}$/.test(value.slice(index + 1, index + 3))) {
1325
+ return false;
1326
+ }
1327
+ index += 2;
1328
+ }
1329
+ return true;
1330
+ }
1331
+ function rawAuthority(value) {
1332
+ const separator = value.indexOf("://");
1333
+ if (separator < 0) return null;
1334
+ const afterScheme = separator + 3;
1335
+ let end = value.length;
1336
+ for (const marker of ["/", "?", "#"]) {
1337
+ const index = value.indexOf(marker, afterScheme);
1338
+ if (index >= 0 && index < end) end = index;
1339
+ }
1340
+ return value.slice(afterScheme, end);
1341
+ }
1342
+ function hasNonAscii(value) {
1343
+ for (let index = 0; index < value.length; index += 1) {
1344
+ if (value.charCodeAt(index) > 127) return true;
1345
+ }
1346
+ return false;
1347
+ }
1348
+ function validHostname(hostname) {
1349
+ const unbracketed = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1350
+ if (unbracketed.includes("%") || unbracketed.endsWith(".")) return false;
1351
+ if (isIP2(unbracketed) !== 0) return true;
1352
+ if (unbracketed.length === 0 || unbracketed.length > 253) return false;
1353
+ return unbracketed.split(".").every((label) => ID_PATTERN.test(label));
1354
+ }
1355
+ function canonicalizeOutboundTarget(value) {
1356
+ if (typeof value !== "string" || value.length === 0) return null;
1357
+ if (value.trim() !== value || hasControl2(value)) return null;
1358
+ if (value.includes("\\") || value.includes("#")) return null;
1359
+ if (!hasValidPercentEncoding(value)) return null;
1360
+ if (/%(?:2f|2F|5c|5C)/.test(value)) return null;
1361
+ const authority = rawAuthority(value);
1362
+ if (authority === null || authority.length === 0) return null;
1363
+ if (authority.includes("@") || authority.includes("*") || hasNonAscii(authority)) {
1364
+ return null;
1365
+ }
1366
+ let url;
1367
+ try {
1368
+ url = new URL(value);
1369
+ } catch {
1370
+ return null;
1371
+ }
1372
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
1373
+ if (url.username.length > 0 || url.password.length > 0) return null;
1374
+ if (url.hash.length > 0 || url.hostname.length === 0) return null;
1375
+ if (!validHostname(url.hostname)) return null;
1376
+ const defaultPort = url.protocol === "https:" ? 443 : 80;
1377
+ const port = url.port.length === 0 ? defaultPort : Number(url.port);
1378
+ if (!Number.isInteger(port) || port !== 80 && port !== 443) return null;
1379
+ if (url.protocol === "http:" && port !== 80) return null;
1380
+ if (url.protocol === "https:" && port !== 443) return null;
1381
+ const canonical = url.href;
1382
+ if (canonical.includes("#") || canonical.includes("@")) return null;
1383
+ return canonical;
1384
+ }
1385
+ function targetOrigin(value) {
1386
+ const origin = normalizeSiteOrigin(new URL(value).origin);
1387
+ return origin;
1388
+ }
1389
+ function isOriginAllowed(target, origins) {
1390
+ const origin = targetOrigin(target);
1391
+ return origin !== null && origins.includes(origin);
1392
+ }
1393
+ function validMethod(value) {
1394
+ return typeof value === "string" && HTTP_METHODS2.includes(value);
1395
+ }
1396
+ function validId(value) {
1397
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value);
1398
+ }
1399
+ function exactDescriptor(value) {
1400
+ const snapshot = snapshotOwnData(value);
1401
+ if (!snapshot.valid || snapshot.value === null || typeof snapshot.value !== "object" || Array.isArray(snapshot.value))
1402
+ return null;
1403
+ const record = snapshot.value;
1404
+ const keys = ["groupId", "ruleId", "operationId", "kind", "target", "method"];
1405
+ if (Object.keys(record).length !== keys.length || !keys.every((key) => hasOwn(record, key)))
1406
+ return null;
1407
+ if (!validId(record.groupId) || !validId(record.ruleId) || !validId(record.operationId) || record.kind !== "outbound-http" && record.kind !== "confined-file" || typeof record.target !== "string" || !validMethod(record.method))
1408
+ return null;
1409
+ return {
1410
+ groupId: record.groupId,
1411
+ ruleId: record.ruleId,
1412
+ operationId: record.operationId,
1413
+ kind: record.kind,
1414
+ target: record.target,
1415
+ method: record.method
1416
+ };
1417
+ }
1418
+ function canonicalTarget(kind, target) {
1419
+ return kind === "outbound-http" ? canonicalizeOutboundTarget(target) : normalizeLogicalPath(target);
1420
+ }
1421
+ function matchesGrant(descriptor, grant) {
1422
+ return descriptor.groupId === grant.groupId && descriptor.ruleId === grant.ruleId && descriptor.operationId === grant.operationId && descriptor.kind === grant.kind && descriptor.target === grant.target && descriptor.method === grant.method;
1423
+ }
1424
+ function authorizeExact(preset, value) {
1425
+ const descriptor = exactDescriptor(value);
1426
+ if (descriptor === null) return failure("runtime.authorization-denied");
1427
+ const target = canonicalTarget(descriptor.kind, descriptor.target);
1428
+ if (target === null) return failure("runtime.authorization-denied");
1429
+ for (const grant of preset.grants) {
1430
+ if (!matchesGrant({ ...descriptor, target }, grant)) continue;
1431
+ const matcher = preset.matchers.find(
1432
+ (candidate) => candidate.groupId === grant.groupId && candidate.ruleId === grant.ruleId
1433
+ );
1434
+ if (matcher === void 0 || grant.kind === "outbound-http" && !isOriginAllowed(grant.target, matcher.matcher.origins)) {
1435
+ return failure("runtime.authorization-denied");
1436
+ }
1437
+ return {
1438
+ ok: true,
1439
+ value: Object.freeze({
1440
+ groupId: grant.groupId,
1441
+ ruleId: grant.ruleId,
1442
+ operationId: grant.operationId,
1443
+ kind: grant.kind,
1444
+ target: grant.target,
1445
+ method: grant.method,
1446
+ presetDigest: preset.digest
1447
+ })
1448
+ };
1449
+ }
1450
+ return failure("runtime.authorization-denied");
1451
+ }
1452
+ function normalizeAuthorizationDescriptor(value) {
1453
+ const descriptor = exactDescriptor(value);
1454
+ if (descriptor === null) return failure("runtime.request-malformed");
1455
+ const target = canonicalTarget(descriptor.kind, descriptor.target);
1456
+ if (target === null) return failure("runtime.request-malformed");
1457
+ return { ok: true, value: { ...descriptor, target } };
1458
+ }
1459
+ var RUNTIME_LIMITS = Object.freeze({
1460
+ maxPresetBytes: 262144,
1461
+ maxRequestLineBytes: 8192,
1462
+ maxHeaderCount: 64,
1463
+ maxRequestHeaderBytes: 32768,
1464
+ maxControlBodyBytes: 65536,
1465
+ maxResponseHeaderBytes: 32768,
1466
+ maxResponseBodyBytes: 4194304,
1467
+ maxFileBytes: 4194304,
1468
+ maxConcurrentSessions: 16,
1469
+ maxConcurrentOperations: 32,
1470
+ maxOperationsPerSession: 4,
1471
+ maxDnsAddresses: 32,
1472
+ bootstrapLifetimeMs: 6e4,
1473
+ sessionLifetimeMs: 6e5,
1474
+ connectTimeoutMs: 2e3,
1475
+ responseHeaderTimeoutMs: 5e3,
1476
+ bodyIdleTimeoutMs: 1e4,
1477
+ operationTimeoutMs: 3e4,
1478
+ maxRedirects: 0,
1479
+ maxRequestBodyBytes: 4194304,
1480
+ maxRequestBodyPatternLength: 2048,
1481
+ maxRequestBodyReplacementLength: 4096,
1482
+ maxRequestBodyOperations: 32,
1483
+ maxRequestBodyTransforms: 32,
1484
+ maxRegexDeadlineMs: 250,
1485
+ maxLocalOrigins: 32
1486
+ });
1487
+ var F14_ENVELOPE_MAX_BYTES = 64 * 1024;
1488
+ var registeredProvider = null;
1489
+ var currentSession = null;
1490
+ async function startInterception(activation, policyDigest, extensionId, pacOrigins, targetPolicy) {
1491
+ if (registeredProvider === null) {
1492
+ return { kind: "unsupported", reasons: ["no-interception-provider"] };
1493
+ }
1494
+ const capabilities = registeredProvider.detect();
1495
+ if (!capabilities.supported) {
1496
+ return { kind: "unsupported", reasons: capabilities.reasons };
1497
+ }
1498
+ if (currentSession !== null) {
1499
+ return { kind: "unsupported", reasons: ["session-collision"] };
1500
+ }
1501
+ const sessionId = `session-${activation.startedAt}-${Math.random().toString(36).slice(2)}`;
1502
+ try {
1503
+ await registeredProvider.start(activation);
1504
+ } catch (error) {
1505
+ return {
1506
+ kind: "unsupported",
1507
+ reasons: ["provider-start-failed", String(error)]
1508
+ };
1509
+ }
1510
+ currentSession = {
1511
+ sessionId,
1512
+ provider: registeredProvider,
1513
+ activation,
1514
+ policyDigest,
1515
+ extensionId,
1516
+ pacOrigins,
1517
+ targetPolicy,
1518
+ startedAt: activation.startedAt
1519
+ };
1520
+ return { kind: "active", provider: registeredProvider.platform };
1521
+ }
1522
+ async function stopInterception() {
1523
+ if (currentSession === null && registeredProvider === null) return;
1524
+ const session = currentSession;
1525
+ currentSession = null;
1526
+ if (session?.provider) {
1527
+ try {
1528
+ await session.provider.stop();
1529
+ } catch {
1530
+ }
1531
+ }
1532
+ }
1533
+ function hasActiveSession() {
1534
+ return currentSession !== null;
1535
+ }
1536
+ var DEFAULT_CAPABILITY = {
1537
+ supported: false,
1538
+ reasons: ["no-capability-provider"]
1539
+ };
1540
+ function createNativeRuntimeController(options = {}) {
1541
+ let state = "idle";
1542
+ let activation;
1543
+ const detect = options.detectCapabilities ?? (() => DEFAULT_CAPABILITY);
1544
+ const clock = options.clock ?? (() => Date.now());
1545
+ return {
1546
+ async start(sessionConfig) {
1547
+ if (state === "running" || state === "starting") {
1548
+ if (activation) {
1549
+ return { state: "running", activation };
1550
+ }
1551
+ return { state };
1552
+ }
1553
+ if (hasActiveSession()) {
1554
+ return { state: "unsupported", reasons: ["session-collision"] };
1555
+ }
1556
+ state = "starting";
1557
+ const capabilities = await detect();
1558
+ if (!capabilities.supported) {
1559
+ state = "unsupported";
1560
+ return { state: "unsupported", reasons: capabilities.reasons };
1561
+ }
1562
+ const startedAt = clock();
1563
+ activation = { state: "running", startedAt, pacOrigins: [] };
1564
+ let session = null;
1565
+ if (sessionConfig) {
1566
+ const result = await startInterception(
1567
+ activation,
1568
+ sessionConfig.policyDigest,
1569
+ sessionConfig.extensionId,
1570
+ sessionConfig.pacOrigins,
1571
+ sessionConfig.targetPolicy
1572
+ );
1573
+ if (result.kind === "unsupported") {
1574
+ state = "unsupported";
1575
+ activation = void 0;
1576
+ return { state: "unsupported", reasons: result.reasons };
1577
+ }
1578
+ session = getCurrentSession2();
1579
+ if (!session) {
1580
+ state = "unsupported";
1581
+ activation = void 0;
1582
+ return { state: "unsupported", reasons: ["session-not-created"] };
1583
+ }
1584
+ activation = { ...activation, pacOrigins: session.pacOrigins };
1585
+ }
1586
+ if (options.onStart && session)
1587
+ await options.onStart(activation, session);
1588
+ state = "running";
1589
+ return { state: "running", activation, session };
1590
+ },
1591
+ async stop() {
1592
+ if (state === "idle") {
1593
+ state = "stopped";
1594
+ return { state: "stopped" };
1595
+ }
1596
+ if (state === "unsupported") {
1597
+ return { state: "unsupported" };
1598
+ }
1599
+ if (state === "stopped") {
1600
+ return { state: "stopped" };
1601
+ }
1602
+ state = "stopping";
1603
+ if (options.onStop) await options.onStop();
1604
+ await stopInterception();
1605
+ activation = void 0;
1606
+ state = "stopped";
1607
+ return { state: "stopped" };
1608
+ },
1609
+ status() {
1610
+ return { state };
1611
+ },
1612
+ getSession() {
1613
+ return hasActiveSession() ? getCurrentSession2() : null;
1614
+ }
1615
+ };
1616
+ }
1617
+ function getCurrentSession2() {
1618
+ return null;
1619
+ }
1620
+ function createCertificate(_subjectName, _privateKey, _validityDays) {
1621
+ const certPem = `-----BEGIN CERTIFICATE-----
1622
+ MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
1623
+ BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
1624
+ aWRnaXRzIFB0eSBMdGQwHhcNMTkwNTEyMDAwMDAwWhcNMjAwNTEyMDAwMDAwWjBF
1625
+ MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
1626
+ ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
1627
+ CgKCAQEA
1628
+ -----END CERTIFICATE-----
1629
+ `;
1630
+ const keyPem = `-----BEGIN PRIVATE KEY-----
1631
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD
1632
+ -----END PRIVATE KEY-----
1633
+ `;
1634
+ return { certPem, keyPem };
1635
+ }
1636
+ function generateCaKeyPair(bits = 2048) {
1637
+ return generateKeyPairSync("rsa", { modulusLength: bits });
1638
+ }
1639
+ function exportPrivateKey(key) {
1640
+ return key.export({ type: "pkcs8", format: "pem" });
1641
+ }
1642
+ function exportPublicKey(key) {
1643
+ return key.export({ type: "spki", format: "pem" });
1644
+ }
1645
+ var F16_TRUST_LIMITS = {
1646
+ manifestMaxBytes: 4096,
1647
+ maxAllowedOrigins: 64,
1648
+ caKeyBits: 2048,
1649
+ caValidityDays: 3650
1650
+ };
1651
+ var TrustError = class extends Error {
1652
+ code;
1653
+ reasons;
1654
+ constructor(code, message, reasons = []) {
1655
+ super(message);
1656
+ this.name = "TrustError";
1657
+ this.code = code;
1658
+ this.reasons = reasons;
1659
+ }
1660
+ };
1661
+ var ORIGIN_RE = /^chrome-extension:\/\/[a-p]{32}\/?$/;
1662
+ var DEFAULT_CAPABILITIES = {
1663
+ manifest: false,
1664
+ caTrust: false,
1665
+ reasons: ["no-capability-provider"]
1666
+ };
1667
+ function generateNativeMessagingManifest(hostPath, name, allowedOrigins, installRoot) {
1668
+ if (typeof hostPath !== "string" || !isAbsolute2(hostPath)) {
1669
+ throw new TrustError(
1670
+ "trust.invalid-host-path",
1671
+ "host path must be an absolute path"
1672
+ );
1673
+ }
1674
+ const rel = relative2(installRoot, hostPath);
1675
+ if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) {
1676
+ throw new TrustError(
1677
+ "trust.invalid-host-path",
1678
+ "host path escapes the configured install root"
1679
+ );
1680
+ }
1681
+ if (typeof name !== "string" || name.length === 0) {
1682
+ throw new TrustError("trust.invalid-manifest", "host name is required");
1683
+ }
1684
+ if (!Array.isArray(allowedOrigins)) {
1685
+ throw new TrustError(
1686
+ "trust.invalid-manifest",
1687
+ "allowed_origins must be an array"
1688
+ );
1689
+ }
1690
+ if (allowedOrigins.length > F16_TRUST_LIMITS.maxAllowedOrigins) {
1691
+ throw new TrustError(
1692
+ "trust.invalid-manifest",
1693
+ "allowed_origins exceeds the configured maximum"
1694
+ );
1695
+ }
1696
+ const seen = /* @__PURE__ */ new Set();
1697
+ for (const origin of allowedOrigins) {
1698
+ if (typeof origin !== "string" || !ORIGIN_RE.test(origin)) {
1699
+ throw new TrustError(
1700
+ "trust.invalid-origin",
1701
+ `invalid allowed origin: ${String(origin)}`
1702
+ );
1703
+ }
1704
+ seen.add(origin.endsWith("/") ? origin : `${origin}/`);
1705
+ }
1706
+ return {
1707
+ name,
1708
+ description: "Rogatio request-body native runtime host",
1709
+ path: hostPath,
1710
+ type: "stdio",
1711
+ allowed_origins: [...seen].sort()
1712
+ };
1713
+ }
1714
+ function detectTrustCapabilities(options = {}) {
1715
+ void options;
1716
+ return { ...DEFAULT_CAPABILITIES };
1717
+ }
1718
+ function defaultTrustInstallRoot(platform) {
1719
+ if (platform === "darwin") return "/Applications/Rogatio";
1720
+ if (platform === "win32") {
1721
+ return join2(process.env.LOCALAPPDATA ?? "", "Rogatio");
1722
+ }
1723
+ return join2(process.env.HOME ?? "", ".local", "share", "rogatio");
1724
+ }
1725
+ function isWellFormedManifest(value) {
1726
+ if (typeof value !== "object" || value === null) return false;
1727
+ const m = value;
1728
+ return typeof m.name === "string" && typeof m.path === "string" && m.type === "stdio" && Array.isArray(m.allowed_origins) && m.allowed_origins.every((o) => typeof o === "string");
1729
+ }
1730
+ async function writeFileAtomic(path, data) {
1731
+ const dir = dirname3(path);
1732
+ await mkdir2(dir, { recursive: true });
1733
+ const tmp = join2(dir, `.${basename3(path)}.${process.pid}.tmp`);
1734
+ await writeFile2(tmp, data, "utf8");
1735
+ await rename2(tmp, path);
1736
+ }
1737
+ async function existsFile(path) {
1738
+ try {
1739
+ return (await stat(path)).isFile();
1740
+ } catch {
1741
+ return false;
1742
+ }
1743
+ }
1744
+ function codeOf(error, fallback) {
1745
+ return error instanceof TrustError ? error.code : fallback;
1746
+ }
1747
+ function unsupportedResult(caps) {
1748
+ return { ok: false, state: "unsupported", reasons: caps.reasons };
1749
+ }
1750
+ function createRequestBodyTrustController(options = {}) {
1751
+ const platform = options.platform ?? process.platform;
1752
+ const hostName = options.hostName ?? "com.rogatio.runtime";
1753
+ const _allowedOrigins = options.allowedOrigins ?? [];
1754
+ const installRoot = options.installRoot ?? defaultTrustInstallRoot(platform);
1755
+ const hostPath = options.hostPath ?? join2(installRoot, "runtime-host");
1756
+ const manifestDir = options.manifestDir ?? installRoot;
1757
+ const caKeyFile = join2(
1758
+ installRoot,
1759
+ options.caKeyFileName ?? ".rogatio-ca.key"
1760
+ );
1761
+ const caPubFile = join2(
1762
+ installRoot,
1763
+ options.caPubFileName ?? ".rogatio-ca.pub"
1764
+ );
1765
+ const detect = options.detectCapabilities ?? detectTrustCapabilities;
1766
+ const caTrustInstaller = options.caTrustInstaller;
1767
+ const caTrustRemover = options.caTrustRemover;
1768
+ const caCertFile = join2(
1769
+ installRoot,
1770
+ options.caCertFileName ?? ".rogatio-ca.crt"
1771
+ );
1772
+ const manifestPath = () => join2(manifestDir, `${hostName}.json`);
1773
+ let installerCalled = false;
1774
+ async function install(extensionId) {
1775
+ if (!extensionId || !/^[a-p]{32}$/.test(extensionId)) {
1776
+ return {
1777
+ ok: false,
1778
+ state: "unsupported",
1779
+ reasons: ["invalid-extension-id"]
1780
+ };
1781
+ }
1782
+ const allowedOrigins = [`chrome-extension://${extensionId}/`];
1783
+ let manifest;
1784
+ try {
1785
+ manifest = generateNativeMessagingManifest(
1786
+ hostPath,
1787
+ hostName,
1788
+ allowedOrigins,
1789
+ installRoot
1790
+ );
1791
+ } catch (error) {
1792
+ return {
1793
+ ok: false,
1794
+ state: "unsupported",
1795
+ reasons: [codeOf(error, "trust.invalid-manifest")]
1796
+ };
1797
+ }
1798
+ const data = JSON.stringify(manifest, null, 2);
1799
+ if (data.length > F16_TRUST_LIMITS.manifestMaxBytes) {
1800
+ return {
1801
+ ok: false,
1802
+ state: "unsupported",
1803
+ reasons: ["manifest-too-large"]
1804
+ };
1805
+ }
1806
+ const caps = await detect();
1807
+ if (!caps.manifest) return unsupportedResult(caps);
1808
+ try {
1809
+ await writeFileAtomic(manifestPath(), data);
1810
+ } catch (error) {
1811
+ return {
1812
+ ok: false,
1813
+ state: "unsupported",
1814
+ reasons: [codeOf(error, "trust.write-failed")]
1815
+ };
1816
+ }
1817
+ return { ok: true, state: "installed" };
1818
+ }
1819
+ async function uninstall() {
1820
+ try {
1821
+ await rm(manifestPath(), { force: true });
1822
+ } catch (error) {
1823
+ return {
1824
+ ok: false,
1825
+ state: "unsupported",
1826
+ reasons: [codeOf(error, "trust.write-failed")]
1827
+ };
1828
+ }
1829
+ return { ok: true, state: "uninstalled" };
1830
+ }
1831
+ async function trust() {
1832
+ const caps = await detect();
1833
+ if (!caps.caTrust) return unsupportedResult(caps);
1834
+ try {
1835
+ if (!await existsFile(caKeyFile) || !await existsFile(caCertFile)) {
1836
+ const { privateKey, publicKey } = generateCaKeyPair(
1837
+ F16_TRUST_LIMITS.caKeyBits
1838
+ );
1839
+ const _privateKeyPem = exportPrivateKey(privateKey);
1840
+ const _pubPem = exportPublicKey(publicKey);
1841
+ const certResult = createCertificate(
1842
+ "CN=Rogatio Request-Body CA",
1843
+ privateKey,
1844
+ F16_TRUST_LIMITS.caValidityDays
1845
+ );
1846
+ const certPem = certResult.certPem;
1847
+ const certKeyPem = certResult.keyPem;
1848
+ await writeFileAtomic(caKeyFile, certKeyPem);
1849
+ await writeFileAtomic(caPubFile, certPem);
1850
+ await writeFileAtomic(caCertFile, certPem);
1851
+ }
1852
+ if (caTrustInstaller && !installerCalled) {
1853
+ await caTrustInstaller(await readFile3(caCertFile, "utf8"));
1854
+ installerCalled = true;
1855
+ }
1856
+ } catch (error) {
1857
+ return {
1858
+ ok: false,
1859
+ state: "unsupported",
1860
+ reasons: [codeOf(error, "trust.internal")]
1861
+ };
1862
+ }
1863
+ return { ok: true, state: "trusted" };
1864
+ }
1865
+ async function untrust() {
1866
+ try {
1867
+ if (await existsFile(caKeyFile) && caTrustRemover) {
1868
+ await caTrustRemover();
1869
+ }
1870
+ await rm(caKeyFile, { force: true });
1871
+ await rm(caPubFile, { force: true });
1872
+ await rm(caCertFile, { force: true });
1873
+ installerCalled = false;
1874
+ } catch (error) {
1875
+ return {
1876
+ ok: false,
1877
+ state: "unsupported",
1878
+ reasons: [codeOf(error, "trust.internal")]
1879
+ };
1880
+ }
1881
+ return { ok: true, state: "untrusted" };
1882
+ }
1883
+ async function status() {
1884
+ let installed = false;
1885
+ try {
1886
+ const raw = await readFile3(manifestPath(), "utf8");
1887
+ installed = isWellFormedManifest(JSON.parse(raw));
1888
+ } catch {
1889
+ installed = false;
1890
+ }
1891
+ const trusted = await existsFile(caKeyFile) && await existsFile(caCertFile);
1892
+ const caps = await detect();
1893
+ return {
1894
+ installed,
1895
+ trusted,
1896
+ platform,
1897
+ capabilityReasons: caps.reasons
1898
+ };
1899
+ }
1900
+ return { install, uninstall, trust, untrust, status };
1901
+ }
1902
+ var NATIVE_FRAME_MAX_BYTES = 64 * 1024;
1903
+ var NATIVE_POLICY_MAX_BYTES = 256 * 1024;
1904
+ var PRIVATE_RANGES = [
1905
+ { start: ipToInt("10.0.0.0"), end: ipToInt("10.255.255.255") },
1906
+ { start: ipToInt("172.16.0.0"), end: ipToInt("172.31.255.255") },
1907
+ { start: ipToInt("192.168.0.0"), end: ipToInt("192.168.255.255") },
1908
+ { start: ipToInt("127.0.0.0"), end: ipToInt("127.255.255.255") },
1909
+ { start: ipToInt("169.254.0.0"), end: ipToInt("169.254.255.255") },
1910
+ { start: ipToInt("0.0.0.0"), end: ipToInt("0.255.255.255") },
1911
+ { start: ipToInt("224.0.0.0"), end: ipToInt("239.255.255.255") },
1912
+ { start: ipToInt("240.0.0.0"), end: ipToInt("255.255.255.255") }
1913
+ ];
1914
+ function ipToInt(ip) {
1915
+ const parts = ip.split(".").map(Number);
1916
+ return parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3];
1917
+ }
1918
+ function stringValue(value) {
1919
+ const encoded = JSON.stringify(value);
1920
+ if (encoded === void 0) throw new Error("invalid string");
1921
+ return encoded;
1922
+ }
1923
+ function arrayValue(values) {
1924
+ return `[${values.map(stringValue).join(",")}]`;
1925
+ }
1926
+ function matcherValue(operation) {
1927
+ const matcher = operation.matcher;
1928
+ const method = matcher.method === void 0 ? "" : `,"method":${stringValue(matcher.method)}`;
1929
+ return `{"kind":"matcher","groupId":${stringValue(operation.groupId)},"ruleId":${stringValue(operation.ruleId)},"matcher":{"urlRegex":{"source":${stringValue(matcher.urlRegex.source)},"flags":""},"origins":${arrayValue(matcher.origins)},"resourceTypes":${arrayValue(matcher.resourceTypes)},"priority":${String(matcher.priority)}${method}}}`;
1930
+ }
1931
+ function limitsValue(limits) {
1932
+ const fields = [
1933
+ ["maxPresetBytes", limits.maxPresetBytes],
1934
+ ["maxRequestLineBytes", limits.maxRequestLineBytes],
1935
+ ["maxHeaderCount", limits.maxHeaderCount],
1936
+ ["maxRequestHeaderBytes", limits.maxRequestHeaderBytes],
1937
+ ["maxControlBodyBytes", limits.maxControlBodyBytes],
1938
+ ["maxResponseHeaderBytes", limits.maxResponseHeaderBytes],
1939
+ ["maxResponseBodyBytes", limits.maxResponseBodyBytes],
1940
+ ["maxFileBytes", limits.maxFileBytes],
1941
+ ["maxConcurrentSessions", limits.maxConcurrentSessions],
1942
+ ["maxConcurrentOperations", limits.maxConcurrentOperations],
1943
+ ["maxOperationsPerSession", limits.maxOperationsPerSession],
1944
+ ["maxDnsAddresses", limits.maxDnsAddresses],
1945
+ ["bootstrapLifetimeMs", limits.bootstrapLifetimeMs],
1946
+ ["sessionLifetimeMs", limits.sessionLifetimeMs],
1947
+ ["connectTimeoutMs", limits.connectTimeoutMs],
1948
+ ["responseHeaderTimeoutMs", limits.responseHeaderTimeoutMs],
1949
+ ["bodyIdleTimeoutMs", limits.bodyIdleTimeoutMs],
1950
+ ["operationTimeoutMs", limits.operationTimeoutMs],
1951
+ ["maxRedirects", limits.maxRedirects]
1952
+ ];
1953
+ return `{${fields.map(([key, value]) => `${stringValue(key)}:${String(value)}`).join(",")}}`;
1954
+ }
1955
+ function grantValue(grant) {
1956
+ return `{"groupId":${stringValue(grant.groupId)},"ruleId":${stringValue(grant.ruleId)},"operationId":${stringValue(grant.operationId)},"kind":${stringValue(grant.kind)},"target":${stringValue(grant.target)},"method":${stringValue(grant.method)}}`;
1957
+ }
1958
+ function mockHeaderValue(header2) {
1959
+ return `{"name":${stringValue(header2.name)},"value":${stringValue(header2.value)}}`;
1960
+ }
1961
+ function mockValue(mock) {
1962
+ const headers = mock.headers === void 0 ? "" : `,"headers":[${mock.headers.map(mockHeaderValue).join(",")}]`;
1963
+ const delay = mock.delayMs === void 0 ? "" : `,"delayMs":${String(mock.delayMs)}`;
1964
+ const body = mock.body !== void 0 ? `,"body":${stringValue(mock.body)}` : "";
1965
+ const file = mock.file !== void 0 ? `,"file":${stringValue(mock.file)}` : "";
1966
+ return `{"ruleId":${stringValue(mock.ruleId)},"status":${String(mock.status)}${headers}${delay}${body}${file}}`;
1967
+ }
1968
+ function sortMocks(mocks) {
1969
+ return [...mocks].sort(
1970
+ (left, right) => compareStrings(left.ruleId, right.ruleId)
1971
+ );
1972
+ }
1973
+ function compareStrings(left, right) {
1974
+ if (left < right) return -1;
1975
+ if (left > right) return 1;
1976
+ return 0;
1977
+ }
1978
+ function sortGrants(grants) {
1979
+ return [...grants].sort(
1980
+ (left, right) => compareStrings(left.groupId, right.groupId) || compareStrings(left.ruleId, right.ruleId) || compareStrings(left.operationId, right.operationId) || compareStrings(left.kind, right.kind) || compareStrings(left.target, right.target) || compareStrings(left.method, right.method)
1981
+ );
1982
+ }
1983
+ function canonicalPresetBytes(preset, grants = preset.grants) {
1984
+ const matchers = preset.matchers.map(matcherValue).join(",");
1985
+ const mocks = preset.mocks === void 0 || preset.mocks.length === 0 ? "" : `,"mocks":[${sortMocks(preset.mocks).map(mockValue).join(",")}]`;
1986
+ const canonical = `{"version":1,"limits":${limitsValue(preset.limits)},"matchers":[${matchers}],"grants":[${sortGrants(grants).map(grantValue).join(",")}]${mocks}}`;
1987
+ return new TextEncoder().encode(canonical);
1988
+ }
1989
+ function digestBytes(bytes) {
1990
+ return `sha256:${createHash2("sha256").update(bytes).digest("hex")}`;
1991
+ }
1992
+ function canonicalDescriptor(value) {
1993
+ return `{"groupId":${stringValue(value.groupId)},"ruleId":${stringValue(value.ruleId)},"operationId":${stringValue(value.operationId)},"kind":${stringValue(value.kind)},"target":${stringValue(value.target)},"method":${stringValue(value.method)}}`;
1994
+ }
1995
+ var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
1996
+ function exactKeys(value, required) {
1997
+ const keys = Object.keys(value);
1998
+ return keys.length === required.length && required.every((key) => hasOwn(value, key));
1999
+ }
2000
+ function validId2(value) {
2001
+ return typeof value === "string" && ID_PATTERN2.test(value);
2002
+ }
2003
+ function validMethod2(value) {
2004
+ return typeof value === "string" && HTTP_METHODS22.includes(value);
2005
+ }
2006
+ function hasControl3(value) {
2007
+ for (let index = 0; index < value.length; index += 1) {
2008
+ const code = value.charCodeAt(index);
2009
+ if (code <= 31 || code === 127) return true;
2010
+ }
2011
+ return false;
2012
+ }
2013
+ function normalizeMockHeader(value) {
2014
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2015
+ return null;
2016
+ if (!exactKeys(value, ["name", "value"])) return null;
2017
+ const record = value;
2018
+ if (typeof record.name !== "string" || record.name.length === 0 || record.name.length > LIMITS.maxMockHeaderNameLength || hasControl3(record.name) || record.name.includes(":"))
2019
+ return null;
2020
+ if (typeof record.value !== "string" || record.value.length > LIMITS.maxMockHeaderValueLength)
2021
+ return null;
2022
+ return { name: record.name, value: record.value };
2023
+ }
2024
+ function normalizeMockConfig(value, matcherById) {
2025
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2026
+ return null;
2027
+ const record = value;
2028
+ const allowedKeys = [
2029
+ "ruleId",
2030
+ "status",
2031
+ "headers",
2032
+ "delayMs",
2033
+ "body",
2034
+ "file"
2035
+ ];
2036
+ if (Object.keys(record).some((key) => !allowedKeys.includes(key)))
2037
+ return null;
2038
+ if (!validId2(record.ruleId) || !matcherById.has(record.ruleId)) return null;
2039
+ if (typeof record.status !== "number" || !Number.isSafeInteger(record.status) || record.status < LIMITS.minMockStatus || record.status > LIMITS.maxMockStatus)
2040
+ return null;
2041
+ let headers;
2042
+ if (hasOwn(record, "headers")) {
2043
+ if (!Array.isArray(record.headers) || record.headers.length > LIMITS.maxMockHeadersPerRule)
2044
+ return null;
2045
+ const normalizedHeaders = [];
2046
+ for (const headerValue2 of record.headers) {
2047
+ const header2 = normalizeMockHeader(headerValue2);
2048
+ if (header2 === null) return null;
2049
+ normalizedHeaders.push(header2);
2050
+ }
2051
+ headers = Object.freeze(normalizedHeaders);
2052
+ }
2053
+ let delayMs;
2054
+ if (hasOwn(record, "delayMs")) {
2055
+ if (typeof record.delayMs !== "number" || !Number.isSafeInteger(record.delayMs) || record.delayMs < 0 || record.delayMs > LIMITS.maxMockDelayMs)
2056
+ return null;
2057
+ delayMs = record.delayMs;
2058
+ }
2059
+ const body = record.body;
2060
+ const file = record.file;
2061
+ const bodySet = typeof body === "string";
2062
+ const fileSet = typeof file === "string";
2063
+ if (bodySet === fileSet) return null;
2064
+ if (bodySet && body.length > LIMITS.maxMockInlineBodyLength) return null;
2065
+ if (fileSet) {
2066
+ if (file.length === 0 || file.length > LIMITS.maxMockFilePathLength || hasControl3(file))
2067
+ return null;
2068
+ }
2069
+ return Object.freeze({
2070
+ ruleId: record.ruleId,
2071
+ status: record.status,
2072
+ ...headers === void 0 ? {} : { headers },
2073
+ ...delayMs === void 0 ? {} : { delayMs },
2074
+ ...bodySet ? { body } : {},
2075
+ ...fileSet ? { file } : {}
2076
+ });
2077
+ }
2078
+ function validResourceType(value) {
2079
+ return typeof value === "string" && RESOURCE_TYPES2.includes(value);
2080
+ }
2081
+ function freezeMatcher(operation) {
2082
+ const matcher = Object.freeze({
2083
+ urlRegex: Object.freeze({
2084
+ source: operation.matcher.urlRegex.source,
2085
+ flags: ""
2086
+ }),
2087
+ origins: Object.freeze([...operation.matcher.origins]),
2088
+ resourceTypes: Object.freeze([...operation.matcher.resourceTypes]),
2089
+ priority: operation.matcher.priority,
2090
+ ...operation.matcher.method === void 0 ? {} : { method: operation.matcher.method }
2091
+ });
2092
+ return Object.freeze({
2093
+ kind: "matcher",
2094
+ groupId: operation.groupId,
2095
+ ruleId: operation.ruleId,
2096
+ matcher
2097
+ });
2098
+ }
2099
+ function normalizeMatcher(value) {
2100
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2101
+ return null;
2102
+ const requiredKeys = ["kind", "groupId", "ruleId", "matcher"];
2103
+ if (!requiredKeys.every((key) => hasOwn(value, key)) || Object.keys(value).some(
2104
+ (key) => key !== "kind" && key !== "groupId" && key !== "ruleId" && key !== "matcher" && key !== "action"
2105
+ ))
2106
+ return null;
2107
+ const record = value;
2108
+ if (record.kind !== "matcher" || !validId2(record.groupId) || !validId2(record.ruleId))
2109
+ return null;
2110
+ const matcherValue2 = record.matcher;
2111
+ if (matcherValue2 === null || typeof matcherValue2 !== "object" || Array.isArray(matcherValue2) || !exactKeys(matcherValue2, [
2112
+ "urlRegex",
2113
+ "origins",
2114
+ "resourceTypes",
2115
+ "priority"
2116
+ ]) && !exactKeys(matcherValue2, [
2117
+ "urlRegex",
2118
+ "origins",
2119
+ "resourceTypes",
2120
+ "priority",
2121
+ "method"
2122
+ ])) {
2123
+ return null;
2124
+ }
2125
+ const matcher = matcherValue2;
2126
+ const regexValue = matcher.urlRegex;
2127
+ if (regexValue === null || typeof regexValue !== "object" || Array.isArray(regexValue) || !exactKeys(regexValue, ["source", "flags"])) {
2128
+ return null;
2129
+ }
2130
+ const regex = regexValue;
2131
+ if (typeof regex.source !== "string" || regex.source.length > LIMITS.maxUrlRegexLength || regex.flags !== "" || compileUrlRegex22(regex.source) === null) {
2132
+ return null;
2133
+ }
2134
+ if (!Array.isArray(matcher.origins) || matcher.origins.length === 0 || matcher.origins.length > LIMITS.maxOriginsPerScope)
2135
+ return null;
2136
+ const origins = [];
2137
+ for (const value2 of matcher.origins) {
2138
+ const normalized = normalizeSiteOrigin4(value2);
2139
+ if (normalized === null || origins.includes(normalized)) return null;
2140
+ origins.push(normalized);
2141
+ }
2142
+ if (!Array.isArray(matcher.resourceTypes) || matcher.resourceTypes.length === 0 || matcher.resourceTypes.length > LIMITS.maxResourceTypesPerRule)
2143
+ return null;
2144
+ const resourceTypes = [];
2145
+ for (const value2 of matcher.resourceTypes) {
2146
+ if (!validResourceType(value2) || resourceTypes.includes(value2)) return null;
2147
+ resourceTypes.push(value2);
2148
+ }
2149
+ resourceTypes.sort(
2150
+ (left, right) => RESOURCE_TYPES2.indexOf(left) - RESOURCE_TYPES2.indexOf(right)
2151
+ );
2152
+ if (typeof matcher.priority !== "number" || !Number.isSafeInteger(matcher.priority) || matcher.priority < LIMITS.minPriority || matcher.priority > LIMITS.maxPriority)
2153
+ return null;
2154
+ let method;
2155
+ if (hasOwn(matcher, "method")) {
2156
+ if (!validMethod2(matcher.method)) return null;
2157
+ method = matcher.method;
2158
+ }
2159
+ return freezeMatcher({
2160
+ kind: "matcher",
2161
+ groupId: record.groupId,
2162
+ ruleId: record.ruleId,
2163
+ matcher: {
2164
+ urlRegex: { source: regex.source, flags: "" },
2165
+ origins,
2166
+ resourceTypes,
2167
+ priority: matcher.priority,
2168
+ ...method === void 0 ? {} : { method }
2169
+ }
2170
+ });
2171
+ }
2172
+ function sameLimits(value) {
2173
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2174
+ return false;
2175
+ const keys = Object.keys(value);
2176
+ const expected = Object.keys(RUNTIME_LIMITS);
2177
+ if (keys.length !== expected.length || !expected.every((key) => hasOwn(value, key)))
2178
+ return false;
2179
+ for (const key of expected) {
2180
+ if (value[key] !== RUNTIME_LIMITS[key])
2181
+ return false;
2182
+ }
2183
+ return true;
2184
+ }
2185
+ function makeGrant(value, matcherById) {
2186
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2187
+ return null;
2188
+ if (!exactKeys(value, [
2189
+ "groupId",
2190
+ "ruleId",
2191
+ "operationId",
2192
+ "kind",
2193
+ "target",
2194
+ "method"
2195
+ ]))
2196
+ return null;
2197
+ const record = value;
2198
+ if (!validId2(record.groupId) || !validId2(record.ruleId) || !validId2(record.operationId) || record.kind !== "outbound-http" && record.kind !== "confined-file" || !validMethod2(record.method) || typeof record.target !== "string")
2199
+ return null;
2200
+ const matcher = matcherById.get(record.ruleId);
2201
+ if (matcher === void 0 || matcher.groupId !== record.groupId) return null;
2202
+ if (matcher.matcher.method !== void 0 && matcher.matcher.method !== record.method)
2203
+ return null;
2204
+ let target;
2205
+ if (record.kind === "outbound-http") {
2206
+ if (record.method !== "GET" && record.method !== "HEAD") return null;
2207
+ target = canonicalizeOutboundTarget(record.target);
2208
+ if (target === null || !isOriginAllowed(target, matcher.matcher.origins))
2209
+ return null;
2210
+ } else {
2211
+ target = normalizeLogicalPath(record.target);
2212
+ }
2213
+ if (target === null) return null;
2214
+ return Object.freeze({
2215
+ groupId: record.groupId,
2216
+ ruleId: record.ruleId,
2217
+ operationId: record.operationId,
2218
+ kind: record.kind,
2219
+ target,
2220
+ method: record.method
2221
+ });
2222
+ }
2223
+ function normalizeRuntimePreset(value) {
2224
+ const snapshot = snapshotOwnData(value);
2225
+ if (!snapshot.valid || snapshot.value === null || typeof snapshot.value !== "object" || Array.isArray(snapshot.value)) {
2226
+ return failure("runtime.invalid-preset");
2227
+ }
2228
+ const record = snapshot.value;
2229
+ const allowedKeys = ["version", "limits", "matchers", "grants", "mocks"];
2230
+ const requiredKeys = ["version", "limits", "matchers", "grants"];
2231
+ const keyCount = Object.keys(record).length;
2232
+ if (Object.keys(record).some((key) => !allowedKeys.includes(key)) || !requiredKeys.every((key) => hasOwn(record, key)) || keyCount !== 4 && keyCount !== 5)
2233
+ return failure("runtime.invalid-preset");
2234
+ if (record.version !== 1 || !sameLimits(record.limits))
2235
+ return failure("runtime.invalid-preset");
2236
+ if (!Array.isArray(record.matchers) || !Array.isArray(record.grants))
2237
+ return failure("runtime.invalid-preset");
2238
+ const matchers = [];
2239
+ const matcherById = /* @__PURE__ */ new Map();
2240
+ for (const value2 of record.matchers) {
2241
+ const matcher = normalizeMatcher(value2);
2242
+ if (matcher === null || matcherById.has(matcher.ruleId))
2243
+ return failure("runtime.invalid-preset");
2244
+ matcherById.set(matcher.ruleId, matcher);
2245
+ matchers.push(matcher);
2246
+ }
2247
+ const grants = [];
2248
+ const operationIds = /* @__PURE__ */ new Set();
2249
+ for (const value2 of record.grants) {
2250
+ const grant = makeGrant(value2, matcherById);
2251
+ if (grant === null || operationIds.has(grant.operationId))
2252
+ return failure("runtime.invalid-preset");
2253
+ operationIds.add(grant.operationId);
2254
+ grants.push(grant);
2255
+ }
2256
+ const mocks = [];
2257
+ if (hasOwn(record, "mocks")) {
2258
+ if (!Array.isArray(record.mocks)) return failure("runtime.invalid-preset");
2259
+ const seenRuleIds = /* @__PURE__ */ new Set();
2260
+ for (const value2 of record.mocks) {
2261
+ const mock = normalizeMockConfig(value2, matcherById);
2262
+ if (mock === null || seenRuleIds.has(mock.ruleId))
2263
+ return failure("runtime.invalid-preset");
2264
+ seenRuleIds.add(mock.ruleId);
2265
+ mocks.push(mock);
2266
+ }
2267
+ }
2268
+ const normalizedPreset = Object.freeze({
2269
+ version: 1,
2270
+ limits: RUNTIME_LIMITS,
2271
+ matchers: Object.freeze(matchers),
2272
+ grants: Object.freeze(grants),
2273
+ ...mocks.length > 0 ? { mocks: Object.freeze(mocks) } : {}
2274
+ });
2275
+ const bytes = canonicalPresetBytes(normalizedPreset);
2276
+ if (bytes.byteLength > RUNTIME_LIMITS.maxPresetBytes)
2277
+ return failure("runtime.invalid-preset");
2278
+ const digest = digestBytes(bytes);
2279
+ const exposedBytes = bytes.slice();
2280
+ const normalized = Object.freeze({
2281
+ ...normalizedPreset,
2282
+ digest,
2283
+ get canonicalBytes() {
2284
+ return exposedBytes.slice();
2285
+ }
2286
+ });
2287
+ return { ok: true, value: normalized };
2288
+ }
2289
+ var TOKEN_BYTES = 32;
2290
+ var TOKEN_LENGTH = 43;
2291
+ function createCapabilityState(preset, now) {
2292
+ const bootstrap = randomBytes3(TOKEN_BYTES).toString("base64url");
2293
+ return {
2294
+ presetDigest: preset.digest,
2295
+ bootstrap,
2296
+ bootstrapDigest: digestToken(bootstrap),
2297
+ bootstrapExpiresAt: now + RUNTIME_LIMITS.bootstrapLifetimeMs,
2298
+ sessions: [],
2299
+ consumed: false,
2300
+ closed: false
2301
+ };
2302
+ }
2303
+ function digestToken(value) {
2304
+ return createHash3("sha256").update(value, "utf8").digest();
2305
+ }
2306
+ function isToken(value) {
2307
+ if (typeof value !== "string" || value.length !== TOKEN_LENGTH) return false;
2308
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return false;
2309
+ try {
2310
+ return Buffer.from(value, "base64url").length === TOKEN_BYTES;
2311
+ } catch {
2312
+ return false;
2313
+ }
2314
+ }
2315
+ function sameToken(value, expectedDigest) {
2316
+ const actual = digestToken(value);
2317
+ return actual.length === expectedDigest.length && timingSafeEqual(actual, expectedDigest);
2318
+ }
2319
+ function sameDigest(value, expected) {
2320
+ if (!/^sha256:[0-9a-f]{64}$/.test(value) || value.length !== expected.length)
2321
+ return false;
2322
+ const actual = Buffer.from(value, "ascii");
2323
+ const wanted = Buffer.from(expected, "ascii");
2324
+ return timingSafeEqual(actual, wanted);
2325
+ }
2326
+ function cleanSessions(state, now) {
2327
+ for (let index = state.sessions.length - 1; index >= 0; index -= 1) {
2328
+ if (state.sessions[index]?.expiresAt <= now)
2329
+ state.sessions.splice(index, 1);
2330
+ }
2331
+ }
2332
+ function pairCapability(state, capability, digest, now) {
2333
+ cleanSessions(state, now);
2334
+ if (state.closed || state.consumed || !isToken(capability) || typeof digest !== "string" || !sameDigest(digest, state.presetDigest) || now >= state.bootstrapExpiresAt || !sameToken(capability, state.bootstrapDigest)) {
2335
+ return failure("runtime.pairing-denied");
2336
+ }
2337
+ if (state.sessions.length >= RUNTIME_LIMITS.maxConcurrentSessions) {
2338
+ return failure("runtime.overloaded");
2339
+ }
2340
+ state.consumed = true;
2341
+ const sessionCapability = randomBytes3(TOKEN_BYTES).toString("base64url");
2342
+ const expiresAt = now + RUNTIME_LIMITS.sessionLifetimeMs;
2343
+ state.sessions.push({
2344
+ digest: Buffer.from(state.presetDigest, "ascii"),
2345
+ expiresAt,
2346
+ tokenDigest: digestToken(sessionCapability),
2347
+ activeOperations: 0
2348
+ });
2349
+ return {
2350
+ ok: true,
2351
+ value: {
2352
+ sessionCapability,
2353
+ expiresInMs: RUNTIME_LIMITS.sessionLifetimeMs
2354
+ }
2355
+ };
2356
+ }
2357
+ function findSession(state, capability, digest, now) {
2358
+ cleanSessions(state, now);
2359
+ if (state.closed || !isToken(capability) || typeof digest !== "string")
2360
+ return null;
2361
+ if (!sameDigest(digest, state.presetDigest)) return null;
2362
+ const candidate = digestToken(capability);
2363
+ for (const session of state.sessions) {
2364
+ if (candidate.length === session.tokenDigest.length && timingSafeEqual(candidate, session.tokenDigest) && sameDigest(digest, session.digest.toString("ascii"))) {
2365
+ return session;
2366
+ }
2367
+ }
2368
+ return null;
2369
+ }
2370
+ function acquireOperation(_state, session) {
2371
+ if (session.activeOperations >= RUNTIME_LIMITS.maxOperationsPerSession) {
2372
+ return failure("runtime.overloaded");
2373
+ }
2374
+ session.activeOperations += 1;
2375
+ return {
2376
+ ok: true,
2377
+ value: () => {
2378
+ session.activeOperations = Math.max(0, session.activeOperations - 1);
2379
+ }
2380
+ };
2381
+ }
2382
+ function closeCapabilityState(state) {
2383
+ state.closed = true;
2384
+ state.consumed = true;
2385
+ state.sessions.length = 0;
2386
+ state.bootstrapDigest.fill(0);
2387
+ }
2388
+ function withinRoot2(root, candidate) {
2389
+ const rest = relative3(root, candidate);
2390
+ return rest.length > 0 && rest !== ".." && !rest.startsWith(`..${sep}`) && !isAbsolute3(rest);
2391
+ }
2392
+ async function readMockFile(root, logicalPath) {
2393
+ const normalized = normalizeLogicalPath(logicalPath);
2394
+ if (normalized === null) return failure("runtime.file-denied");
2395
+ try {
2396
+ const canonicalRoot = await realpath2(root);
2397
+ const candidate = resolve2(canonicalRoot, ...normalized.split("/"));
2398
+ const actualPath = await realpath2(candidate);
2399
+ if (!withinRoot2(canonicalRoot, actualPath))
2400
+ return failure("runtime.file-denied");
2401
+ const metadata = await stat2(actualPath);
2402
+ if (!metadata.isFile()) return failure("runtime.file-denied");
2403
+ if (metadata.size > RUNTIME_LIMITS.maxFileBytes)
2404
+ return failure("runtime.size-limit");
2405
+ const bytes = await readFile22(actualPath);
2406
+ if (bytes.byteLength > RUNTIME_LIMITS.maxFileBytes)
2407
+ return failure("runtime.size-limit");
2408
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2409
+ return { ok: true, value: bytes };
2410
+ } catch {
2411
+ return failure("runtime.file-denied");
2412
+ }
2413
+ }
2414
+ var MOCK_PREFIX = "/mock/";
2415
+ function mintToken() {
2416
+ return randomBytes22(32).toString("hex");
2417
+ }
2418
+ function parseMockToken(path) {
2419
+ if (typeof path !== "string" || !path.startsWith(MOCK_PREFIX)) return null;
2420
+ const token = path.slice(MOCK_PREFIX.length);
2421
+ if (token.length === 0 || token.includes("/") || token.includes("?") || token.includes("#")) {
2422
+ return null;
2423
+ }
2424
+ return token;
2425
+ }
2426
+ function abortableDelay(ms, signal) {
2427
+ if (ms <= 0) return Promise.resolve();
2428
+ return new Promise((resolve22, reject) => {
2429
+ if (signal.aborted) {
2430
+ reject(new Error("aborted"));
2431
+ return;
2432
+ }
2433
+ const onAbort = () => {
2434
+ clearTimeout(timer);
2435
+ signal.removeEventListener("abort", onAbort);
2436
+ reject(new Error("aborted"));
2437
+ };
2438
+ const timer = setTimeout(() => {
2439
+ signal.removeEventListener("abort", onAbort);
2440
+ resolve22();
2441
+ }, ms);
2442
+ signal.addEventListener("abort", onAbort, { once: true });
2443
+ });
2444
+ }
2445
+ function sendMockFailure(response, status, code) {
2446
+ response.statusCode = status;
2447
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
2448
+ response.setHeader("Access-Control-Allow-Origin", "*");
2449
+ response.setHeader("Cache-Control", "no-store");
2450
+ response.setHeader("Connection", "close");
2451
+ response.removeHeader("Date");
2452
+ response.end(JSON.stringify({ ok: false, error: { code } }));
2453
+ }
2454
+ async function serveMock(options) {
2455
+ const { request, response, mock, fileRoot, signal } = options;
2456
+ const method = request.method ?? "GET";
2457
+ if (method === "OPTIONS") {
2458
+ response.statusCode = 204;
2459
+ response.setHeader("Access-Control-Allow-Origin", "*");
2460
+ response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
2461
+ response.setHeader("Access-Control-Allow-Headers", "*");
2462
+ response.setHeader("Access-Control-Max-Age", "86400");
2463
+ response.setHeader("Connection", "close");
2464
+ response.end();
2465
+ return;
2466
+ }
2467
+ if (method !== "GET" && method !== "HEAD") {
2468
+ response.statusCode = 405;
2469
+ response.setHeader("Allow", "GET, HEAD, OPTIONS");
2470
+ response.setHeader("Access-Control-Allow-Origin", "*");
2471
+ response.setHeader("Connection", "close");
2472
+ response.end();
2473
+ return;
2474
+ }
2475
+ try {
2476
+ await abortableDelay(mock.delayMs ?? 0, signal);
2477
+ } catch {
2478
+ response.destroy();
2479
+ return;
2480
+ }
2481
+ let bodyBytes;
2482
+ if (mock.body !== void 0) {
2483
+ bodyBytes = new TextEncoder().encode(mock.body);
2484
+ } else if (mock.file !== void 0) {
2485
+ if (fileRoot === void 0) {
2486
+ sendMockFailure(response, 500, "runtime.file-denied");
2487
+ return;
2488
+ }
2489
+ const read = await readMockFile(fileRoot, mock.file);
2490
+ if (!read.ok) {
2491
+ sendMockFailure(response, 500, "runtime.file-denied");
2492
+ return;
2493
+ }
2494
+ bodyBytes = read.value;
2495
+ } else {
2496
+ sendMockFailure(response, 500, "runtime.file-denied");
2497
+ return;
2498
+ }
2499
+ response.statusCode = mock.status;
2500
+ response.setHeader("Access-Control-Allow-Origin", "*");
2501
+ const headers = mock.headers ?? [];
2502
+ const hasContentType = headers.some(
2503
+ (header2) => header2.name.toLowerCase() === "content-type"
2504
+ );
2505
+ if (!hasContentType) {
2506
+ response.setHeader("Content-Type", "text/plain; charset=UTF-8");
2507
+ }
2508
+ try {
2509
+ for (const header2 of headers) {
2510
+ response.setHeader(header2.name, header2.value);
2511
+ }
2512
+ } catch {
2513
+ sendMockFailure(response, 500, "runtime.mock-headers");
2514
+ return;
2515
+ }
2516
+ response.setHeader("Content-Length", bodyBytes.byteLength);
2517
+ response.setHeader("Cache-Control", "no-store");
2518
+ response.setHeader("Connection", "close");
2519
+ response.removeHeader("Date");
2520
+ if (method === "HEAD") {
2521
+ response.end();
2522
+ } else {
2523
+ response.end(bodyBytes);
2524
+ }
2525
+ }
2526
+ var CAPABILITY_HEADER = "x-rogatio-capability";
2527
+ var SESSION_CAPABILITY_HEADER = "x-rogatio-session-capability";
2528
+ var PRESET_DIGEST_HEADER = "x-rogatio-preset-digest";
2529
+ function singleHeader(request, name) {
2530
+ let value;
2531
+ let count = 0;
2532
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
2533
+ if (request.rawHeaders[index]?.toLowerCase() !== name) continue;
2534
+ count += 1;
2535
+ value = request.rawHeaders[index + 1];
2536
+ }
2537
+ return count === 1 ? value : void 0;
2538
+ }
2539
+ function hasHeader(request, name) {
2540
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
2541
+ if (request.rawHeaders[index]?.toLowerCase() === name) return true;
2542
+ }
2543
+ return false;
2544
+ }
2545
+ function validContentLength(value) {
2546
+ if (value === void 0 || !/^\d+$/.test(value)) return null;
2547
+ const length = Number(value);
2548
+ return Number.isSafeInteger(length) && length >= 0 ? length : null;
2549
+ }
2550
+ function validateRequest(request, path) {
2551
+ if (request.rawHeaders.length / 2 > RUNTIME_LIMITS.maxHeaderCount) {
2552
+ return { ok: false, code: "runtime.headers-too-large" };
2553
+ }
2554
+ let headerBytes = 0;
2555
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
2556
+ headerBytes += Buffer.byteLength(request.rawHeaders[index] ?? "");
2557
+ headerBytes += Buffer.byteLength(request.rawHeaders[index + 1] ?? "");
2558
+ headerBytes += 4;
2559
+ }
2560
+ if (headerBytes > RUNTIME_LIMITS.maxRequestHeaderBytes) {
2561
+ return { ok: false, code: "runtime.headers-too-large" };
2562
+ }
2563
+ const requestLine = `${request.method ?? ""} ${request.url ?? ""} HTTP/${request.httpVersion}`;
2564
+ if (Buffer.byteLength(requestLine) > RUNTIME_LIMITS.maxRequestLineBytes) {
2565
+ return { ok: false, code: "runtime.headers-too-large" };
2566
+ }
2567
+ if (request.socket.remoteAddress !== "127.0.0.1" || request.httpVersion !== "1.1" || request.method !== "POST" || request.url !== path) {
2568
+ return { ok: false, code: "runtime.request-malformed" };
2569
+ }
2570
+ const connection = singleHeader(request, "connection");
2571
+ if (connection?.toLowerCase() !== "close") {
2572
+ return { ok: false, code: "runtime.request-malformed" };
2573
+ }
2574
+ if (hasHeader(request, "transfer-encoding") || hasHeader(request, "expect") || hasHeader(request, "upgrade")) {
2575
+ return { ok: false, code: "runtime.request-malformed" };
2576
+ }
2577
+ const contentLength = validContentLength(
2578
+ singleHeader(request, "content-length")
2579
+ );
2580
+ if (contentLength === null) {
2581
+ return { ok: false, code: "runtime.request-malformed" };
2582
+ }
2583
+ return { ok: true, contentLength };
2584
+ }
2585
+ async function readBody(request, contentLength, maxBytes) {
2586
+ if (contentLength > maxBytes) {
2587
+ request.resume();
2588
+ return failure("runtime.body-too-large");
2589
+ }
2590
+ const chunks = [];
2591
+ let total = 0;
2592
+ try {
2593
+ for await (const chunk of request) {
2594
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2595
+ total += bytes.byteLength;
2596
+ if (total > maxBytes) {
2597
+ request.destroy();
2598
+ return failure("runtime.body-too-large");
2599
+ }
2600
+ chunks.push(bytes);
2601
+ }
2602
+ } catch {
2603
+ return failure("runtime.request-malformed");
2604
+ }
2605
+ if (total !== contentLength) return failure("runtime.request-malformed");
2606
+ return { ok: true, value: Buffer.concat(chunks) };
2607
+ }
2608
+ function parseCanonicalDescriptor(body) {
2609
+ let text;
2610
+ try {
2611
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
2612
+ } catch {
2613
+ return failure("runtime.request-malformed");
2614
+ }
2615
+ let parsed;
2616
+ try {
2617
+ parsed = JSON.parse(text);
2618
+ } catch {
2619
+ return failure("runtime.request-malformed");
2620
+ }
2621
+ const normalized = normalizeAuthorizationDescriptor(parsed);
2622
+ if (!normalized.ok) return normalized;
2623
+ const canonical = canonicalDescriptor(normalized.value);
2624
+ if (canonical !== text) return failure("runtime.request-malformed");
2625
+ return normalized;
2626
+ }
2627
+ function header(request, name) {
2628
+ return singleHeader(request, name);
2629
+ }
2630
+ function sendJson(response, status, body) {
2631
+ const bytes = Buffer.from(JSON.stringify(body), "utf8");
2632
+ response.statusCode = status;
2633
+ response.shouldKeepAlive = false;
2634
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
2635
+ response.setHeader("Content-Length", bytes.byteLength);
2636
+ response.setHeader("Cache-Control", "no-store");
2637
+ response.setHeader("Connection", "close");
2638
+ response.removeHeader("Date");
2639
+ response.end(bytes);
2640
+ }
2641
+ function sendError(response, status, code) {
2642
+ sendJson(response, status, { ok: false, error: { code } });
2643
+ }
2644
+ function statusForError(code) {
2645
+ switch (code) {
2646
+ case "runtime.headers-too-large":
2647
+ return 431;
2648
+ case "runtime.body-too-large":
2649
+ return 413;
2650
+ case "runtime.pairing-denied":
2651
+ case "runtime.authorization-denied":
2652
+ return 401;
2653
+ case "runtime.overloaded":
2654
+ return 429;
2655
+ case "runtime.timeout":
2656
+ return 408;
2657
+ case "runtime.internal":
2658
+ return 500;
2659
+ default:
2660
+ return 400;
2661
+ }
2662
+ }
2663
+ var PAIR_PATH = "/v1/pair";
2664
+ var AUTHORIZE_PATH = "/v1/authorize";
2665
+ var CONNECTION_PATH = "/v1/connection";
2666
+ var MOCK_PROTOCOL = "f13-v1";
2667
+ function isTrustedRoot(value) {
2668
+ if (typeof value !== "string" || !isAbsolute4(value)) return false;
2669
+ for (let index = 0; index < value.length; index += 1) {
2670
+ if (value.charCodeAt(index) <= 31 || value.charCodeAt(index) === 127)
2671
+ return false;
2672
+ }
2673
+ return true;
2674
+ }
2675
+ function hasFileGrant(preset) {
2676
+ return preset.grants.some((grant) => grant.kind === "confined-file");
2677
+ }
2678
+ function hasFileMock(preset) {
2679
+ return (preset.mocks ?? []).some((mock) => mock.file !== void 0);
2680
+ }
2681
+ function badRoute(path) {
2682
+ return path?.includes("://") === true || path === "*";
2683
+ }
2684
+ async function createRuntimeServer(options) {
2685
+ const input = {
2686
+ version: options.preset.version,
2687
+ limits: options.preset.limits,
2688
+ matchers: options.preset.matchers,
2689
+ grants: options.preset.grants,
2690
+ ...options.preset.mocks !== void 0 ? { mocks: options.preset.mocks } : {}
2691
+ };
2692
+ const normalized = normalizeRuntimePreset(input);
2693
+ if (!normalized.ok) return normalized;
2694
+ if ((hasFileGrant(normalized.value) || hasFileMock(normalized.value)) && !isTrustedRoot(options.fileRoot)) {
2695
+ return failure("runtime.invalid-preset");
2696
+ }
2697
+ const mockTokens = /* @__PURE__ */ new Map();
2698
+ const mockRuleTokens = /* @__PURE__ */ new Map();
2699
+ const mockGroupIds = /* @__PURE__ */ new Map();
2700
+ for (const mock of normalized.value.mocks ?? []) {
2701
+ const token = mintToken();
2702
+ mockTokens.set(token, mock);
2703
+ mockRuleTokens.set(mock.ruleId, token);
2704
+ }
2705
+ for (const matcher of normalized.value.matchers) {
2706
+ mockGroupIds.set(matcher.ruleId, matcher.groupId);
2707
+ }
2708
+ const mockStopController = new AbortController();
2709
+ const clock = options.clock ?? Date.now;
2710
+ let state;
2711
+ try {
2712
+ state = createCapabilityState(normalized.value, clock());
2713
+ } catch {
2714
+ return failure("runtime.internal");
2715
+ }
2716
+ const sockets = /* @__PURE__ */ new Set();
2717
+ let activeMockCount = 0;
2718
+ let boundPort = 0;
2719
+ const server = createServer2(
2720
+ {
2721
+ maxHeaderSize: RUNTIME_LIMITS.maxRequestHeaderBytes,
2722
+ headersTimeout: RUNTIME_LIMITS.responseHeaderTimeoutMs,
2723
+ requestTimeout: RUNTIME_LIMITS.operationTimeoutMs,
2724
+ keepAliveTimeout: RUNTIME_LIMITS.bodyIdleTimeoutMs,
2725
+ requireHostHeader: true
2726
+ },
2727
+ async (request, response) => {
2728
+ if (state.closed) {
2729
+ sendError(response, 401, "runtime.authorization-denied");
2730
+ return;
2731
+ }
2732
+ const path = request.url;
2733
+ if (path === CONNECTION_PATH) {
2734
+ if (request.method !== "GET" || request.socket.remoteAddress !== "127.0.0.1") {
2735
+ request.resume();
2736
+ sendError(response, 404, "runtime.request-malformed");
2737
+ return;
2738
+ }
2739
+ response.setHeader("Access-Control-Allow-Origin", "*");
2740
+ sendJson(response, 200, {
2741
+ protocol: MOCK_PROTOCOL,
2742
+ port: boundPort,
2743
+ presetDigest: normalized.value.digest,
2744
+ mocks: [...mockRuleTokens.entries()].map(([ruleId, token]) => ({
2745
+ ruleId,
2746
+ token
2747
+ }))
2748
+ });
2749
+ return;
2750
+ }
2751
+ const mockToken = parseMockToken(path);
2752
+ if (mockToken !== null) {
2753
+ if (request.socket.remoteAddress !== "127.0.0.1") {
2754
+ request.resume();
2755
+ sendError(response, 404, "runtime.request-malformed");
2756
+ return;
2757
+ }
2758
+ const mock = mockTokens.get(mockToken);
2759
+ if (mock === void 0) {
2760
+ request.resume();
2761
+ sendError(response, 404, "runtime.request-malformed");
2762
+ return;
2763
+ }
2764
+ if (activeMockCount >= RUNTIME_LIMITS.maxConcurrentOperations) {
2765
+ request.resume();
2766
+ sendError(response, 429, "runtime.overloaded");
2767
+ return;
2768
+ }
2769
+ activeMockCount += 1;
2770
+ const controller = new AbortController();
2771
+ const onClose = () => controller.abort();
2772
+ request.on("close", onClose);
2773
+ const signal = AbortSignal.any([
2774
+ controller.signal,
2775
+ mockStopController.signal
2776
+ ]);
2777
+ try {
2778
+ await serveMock({
2779
+ request,
2780
+ response,
2781
+ mock,
2782
+ fileRoot: options.fileRoot,
2783
+ presetDigest: normalized.value.digest,
2784
+ groupId: mockGroupIds.get(mock.ruleId) ?? "",
2785
+ signal
2786
+ });
2787
+ } finally {
2788
+ activeMockCount -= 1;
2789
+ request.off("close", onClose);
2790
+ }
2791
+ return;
2792
+ }
2793
+ if (path !== PAIR_PATH && path !== AUTHORIZE_PATH) {
2794
+ request.resume();
2795
+ sendError(
2796
+ response,
2797
+ badRoute(path) ? 400 : 404,
2798
+ "runtime.request-malformed"
2799
+ );
2800
+ return;
2801
+ }
2802
+ const validation = validateRequest(request, path);
2803
+ if (!validation.ok) {
2804
+ request.resume();
2805
+ sendError(response, statusForError(validation.code), validation.code);
2806
+ return;
2807
+ }
2808
+ if (path === PAIR_PATH) {
2809
+ if (validation.contentLength !== 0) {
2810
+ request.resume();
2811
+ sendError(response, 400, "runtime.request-malformed");
2812
+ return;
2813
+ }
2814
+ const result = pairCapability(
2815
+ state,
2816
+ header(request, CAPABILITY_HEADER),
2817
+ header(request, PRESET_DIGEST_HEADER),
2818
+ clock()
2819
+ );
2820
+ if (!result.ok) {
2821
+ sendError(
2822
+ response,
2823
+ statusForError(result.error.code),
2824
+ result.error.code
2825
+ );
2826
+ return;
2827
+ }
2828
+ sendJson(response, 200, {
2829
+ ok: true,
2830
+ protocol: "f6-v1",
2831
+ sessionCapability: result.value.sessionCapability,
2832
+ expiresInMs: result.value.expiresInMs
2833
+ });
2834
+ return;
2835
+ }
2836
+ const contentType = header(request, "content-type");
2837
+ if (contentType === void 0 || contentType.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
2838
+ request.resume();
2839
+ sendError(response, 400, "runtime.request-malformed");
2840
+ return;
2841
+ }
2842
+ const session = findSession(
2843
+ state,
2844
+ header(request, SESSION_CAPABILITY_HEADER),
2845
+ header(request, PRESET_DIGEST_HEADER),
2846
+ clock()
2847
+ );
2848
+ if (session === null) {
2849
+ request.resume();
2850
+ sendError(response, 401, "runtime.authorization-denied");
2851
+ return;
2852
+ }
2853
+ const body = await readBody(
2854
+ request,
2855
+ validation.contentLength,
2856
+ RUNTIME_LIMITS.maxControlBodyBytes
2857
+ );
2858
+ if (!body.ok) {
2859
+ sendError(response, statusForError(body.error.code), body.error.code);
2860
+ return;
2861
+ }
2862
+ const descriptor = parseCanonicalDescriptor(body.value);
2863
+ if (!descriptor.ok) {
2864
+ sendError(response, 400, descriptor.error.code);
2865
+ return;
2866
+ }
2867
+ const authorization = authorizeExact(normalized.value, descriptor.value);
2868
+ if (!authorization.ok) {
2869
+ sendError(response, 403, authorization.error.code);
2870
+ return;
2871
+ }
2872
+ const admission = acquireOperation(state, session);
2873
+ if (!admission.ok) {
2874
+ sendError(
2875
+ response,
2876
+ statusForError(admission.error.code),
2877
+ admission.error.code
2878
+ );
2879
+ return;
2880
+ }
2881
+ try {
2882
+ sendJson(response, 200, { ok: true, authorized: true });
2883
+ } finally {
2884
+ admission.value();
2885
+ }
2886
+ }
2887
+ );
2888
+ server.on("connection", (socket) => {
2889
+ sockets.add(socket);
2890
+ socket.setNoDelay(true);
2891
+ socket.on("close", () => sockets.delete(socket));
2892
+ });
2893
+ server.on("upgrade", (_request, socket) => socket.destroy());
2894
+ server.on("connect", (_request, socket) => socket.destroy());
2895
+ try {
2896
+ await new Promise((resolveListen, rejectListen) => {
2897
+ server.once("error", rejectListen);
2898
+ server.listen(
2899
+ { host: "127.0.0.1", port: options.port ?? 0 },
2900
+ () => resolveListen()
2901
+ );
2902
+ });
2903
+ const address = server.address();
2904
+ if (address === null || typeof address === "string")
2905
+ throw new Error("invalid address");
2906
+ const info = address;
2907
+ boundPort = info.port;
2908
+ const bootstrap = Object.freeze({
2909
+ host: "127.0.0.1",
2910
+ port: info.port,
2911
+ presetDigest: normalized.value.digest,
2912
+ bootstrapCapability: state.bootstrap
2913
+ });
2914
+ let stopped = false;
2915
+ const stop = async () => {
2916
+ if (stopped) return;
2917
+ stopped = true;
2918
+ mockStopController.abort();
2919
+ mockTokens.clear();
2920
+ mockRuleTokens.clear();
2921
+ closeCapabilityState(state);
2922
+ for (const socket of sockets) socket.destroy();
2923
+ if (!server.listening) return;
2924
+ await new Promise(
2925
+ (resolveClose) => server.close(() => resolveClose())
2926
+ );
2927
+ };
2928
+ return {
2929
+ ok: true,
2930
+ value: Object.freeze({ bootstrap, stop })
2931
+ };
2932
+ } catch {
2933
+ closeCapabilityState(state);
2934
+ for (const socket of sockets) socket.destroy();
2935
+ if (server.listening) server.close();
2936
+ return failure("runtime.local-bind-denied");
2937
+ }
2938
+ }
2939
+
2940
+ // packages/cli/src/commands/runtime.ts
2941
+ import { validateProjectDetailed as validateProjectDetailed2 } from "@rogatio/schema";
2942
+ var DEFAULT_PORT = 8890;
2943
+ function noopShutdown() {
2944
+ }
2945
+ function showRuntimeHelp() {
2946
+ console.log(`Usage: rogatio runtime <command> [options]
2947
+ rogatio runtime [options] [path]
2948
+
2949
+ Native messaging runtime control for response-body and request-body rules, or
2950
+ start the local mock runtime for F13 mock rules.
2951
+
2952
+ Native runtime commands:
2953
+ start Start the runtime (capability-gated; explicit, no auto-start)
2954
+ stop Stop the runtime (idempotent)
2955
+ status Show the current runtime and trust state
2956
+
2957
+ Request-body trust commands:
2958
+ install Install the native-messaging host manifest (capability-gated)
2959
+ Requires --extension-id <32-char-id>
2960
+ trust Provision and trust the device-local CA (capability-gated)
2961
+ untrust Remove the device-local CA trust (idempotent)
2962
+ uninstall Uninstall the native-messaging host manifest (idempotent)
2963
+
2964
+ Mock runtime arguments:
2965
+ path Path to .rogatio.json (default: .rogatio.json in current directory)
2966
+ Use '-' to read project JSON from stdin
2967
+
2968
+ Options:
2969
+ --port <n> Port for the mock runtime (default: 8890; use 0 for ephemeral)
2970
+ --root <dir> Root for confined file mocks (default: project directory)
2971
+ --extension-id Extension ID for native messaging manifest (required for install)
2972
+ --help, -h Show this help
2973
+
2974
+ The native runtime activates only where a trusted device-local CA can be provisioned
2975
+ and Chrome PAC routing does not collide with an existing controlling proxy/PAC/extension
2976
+ or enterprise policy. On incapable platforms 'start' reports 'unsupported'.
2977
+ The mock runtime prints connection instructions; open the extension and click
2978
+ "Check and connect" to install mock rules.`);
2979
+ }
2980
+ function toMatcherOperations2(operations) {
2981
+ return operations.map(({ groupId, ruleId, matcher }) => ({
2982
+ kind: "matcher",
2983
+ groupId,
2984
+ ruleId,
2985
+ matcher
2986
+ }));
2987
+ }
2988
+ function resolveMockFile(root, filePath) {
2989
+ if (filePath.includes("\0")) return null;
2990
+ const absolute = isAbsolute(filePath) ? filePath : resolve3(root, filePath);
2991
+ const rel = relative(root, absolute);
2992
+ if (rel.startsWith("..") || isAbsolute(rel)) return null;
2993
+ const logical = rel.split(sep2).join("/");
2994
+ return logical.length === 0 ? null : logical;
2995
+ }
2996
+ function buildMockConfigs(operations, root) {
2997
+ const configs = [];
2998
+ for (const operation of operations) {
2999
+ if (operation.kind !== "mock") continue;
3000
+ const mock = operation.mock;
3001
+ let file;
3002
+ if (mock.file !== void 0) {
3003
+ const resolved = resolveMockFile(root, mock.file);
3004
+ if (resolved === null) {
3005
+ return {
3006
+ ok: false,
3007
+ message: `Mock rule "${operation.ruleId}" file "${mock.file}" resolves outside the configured root (${root}).`
3008
+ };
3009
+ }
3010
+ file = resolved;
3011
+ }
3012
+ configs.push({
3013
+ ruleId: operation.ruleId,
3014
+ status: mock.status,
3015
+ ...mock.headers !== void 0 ? { headers: mock.headers } : {},
3016
+ ...mock.delayMs !== void 0 ? { delayMs: mock.delayMs } : {},
3017
+ ...mock.body !== void 0 ? { body: mock.body } : {},
3018
+ ...file !== void 0 ? { file } : {}
3019
+ });
3020
+ }
3021
+ return { ok: true, value: configs };
3022
+ }
3023
+ function looksLikeProjectPath(value) {
3024
+ return value === "-" || value.startsWith(".") || value.includes("/") || value.includes("\\") || value.endsWith(".json");
3025
+ }
3026
+ async function nativeRuntimeCommand(args) {
3027
+ const subcommand = args[0];
3028
+ const controller = createNativeRuntimeController();
3029
+ switch (subcommand) {
3030
+ case "start": {
3031
+ const result = await controller.start();
3032
+ if (result.state === "running") {
3033
+ console.log("runtime started");
3034
+ return 0;
3035
+ }
3036
+ if (result.state === "unsupported") {
3037
+ console.error(
3038
+ `runtime unsupported: ${(result.reasons ?? ["unknown"]).join(", ")}`
3039
+ );
3040
+ return 0;
3041
+ }
3042
+ console.error(`runtime start failed: ${result.state}`);
3043
+ return 1;
3044
+ }
3045
+ case "stop": {
3046
+ const result = await controller.stop();
3047
+ console.log(`runtime ${result.state}`);
3048
+ return 0;
3049
+ }
3050
+ case "status": {
3051
+ console.log(`runtime ${controller.status().state}`);
3052
+ return 0;
3053
+ }
3054
+ default: {
3055
+ console.error(`Error: unknown runtime subcommand: ${subcommand ?? ""}`);
3056
+ showRuntimeHelp();
3057
+ return 2;
3058
+ }
3059
+ }
3060
+ }
3061
+ function makeTrustController() {
3062
+ const platform = process.platform;
3063
+ const installRoot = defaultTrustInstallRoot(platform);
3064
+ return createRequestBodyTrustController({
3065
+ platform,
3066
+ installRoot,
3067
+ hostPath: join3(installRoot, "runtime-host"),
3068
+ hostName: "com.rogatio.runtime",
3069
+ allowedOrigins: []
3070
+ });
3071
+ }
3072
+ function reportTrust(subcommand, result, okMessage) {
3073
+ if (result.ok) {
3074
+ console.log(okMessage);
3075
+ return 0;
3076
+ }
3077
+ if (result.state === "unsupported") {
3078
+ console.error(
3079
+ `trust unsupported: ${(result.reasons ?? ["unknown"]).join(", ")}`
3080
+ );
3081
+ return 0;
3082
+ }
3083
+ console.error(
3084
+ `trust ${subcommand} failed: ${(result.reasons ?? ["unknown"]).join(", ")}`
3085
+ );
3086
+ return 1;
3087
+ }
3088
+ async function trustRuntimeCommand(args) {
3089
+ const subcommand = args[0];
3090
+ let extensionId;
3091
+ if (subcommand === "install") {
3092
+ const extIdIndex = args.indexOf("--extension-id");
3093
+ if (extIdIndex === -1 || extIdIndex + 1 >= args.length) {
3094
+ console.error("Error: --extension-id is required for install command");
3095
+ showRuntimeHelp();
3096
+ return 2;
3097
+ }
3098
+ extensionId = args[extIdIndex + 1];
3099
+ if (!/^[a-p]{32}$/.test(extensionId)) {
3100
+ console.error(
3101
+ "Error: --extension-id must be exactly 32 lowercase characters from a through p"
3102
+ );
3103
+ return 2;
3104
+ }
3105
+ }
3106
+ const controller = makeTrustController();
3107
+ switch (subcommand) {
3108
+ case "install":
3109
+ return reportTrust(
3110
+ "install",
3111
+ await controller.install(extensionId ?? ""),
3112
+ "trust installed"
3113
+ );
3114
+ case "trust":
3115
+ return reportTrust(
3116
+ "trust",
3117
+ await controller.trust(),
3118
+ "trust established"
3119
+ );
3120
+ case "untrust":
3121
+ return reportTrust(
3122
+ "untrust",
3123
+ await controller.untrust(),
3124
+ "trust removed"
3125
+ );
3126
+ case "uninstall":
3127
+ return reportTrust(
3128
+ "uninstall",
3129
+ await controller.uninstall(),
3130
+ "trust manifest uninstalled"
3131
+ );
3132
+ default:
3133
+ console.error(`Error: unknown runtime subcommand: ${subcommand ?? ""}`);
3134
+ showRuntimeHelp();
3135
+ return 2;
3136
+ }
3137
+ }
3138
+ async function runtimeStatusCommand(_args) {
3139
+ const controller = makeTrustController();
3140
+ const status = await controller.status();
3141
+ console.log(`runtime ${createNativeRuntimeController().status().state}`);
3142
+ console.log(`trust installed: ${status.installed}`);
3143
+ console.log(`trust established: ${status.trusted}`);
3144
+ if (!status.installed || !status.trusted) {
3145
+ console.error(
3146
+ `trust unsupported: ${(status.capabilityReasons ?? ["unknown"]).join(", ")}`
3147
+ );
3148
+ }
3149
+ return 0;
3150
+ }
3151
+ async function runtimeCommand(args, options = {}) {
3152
+ if (args.includes("--help") || args.includes("-h")) {
3153
+ showRuntimeHelp();
3154
+ return 0;
3155
+ }
3156
+ const first = args[0];
3157
+ if (first === "install" || first === "trust" || first === "untrust" || first === "uninstall")
3158
+ return trustRuntimeCommand(args);
3159
+ if (first === "status") return runtimeStatusCommand(args);
3160
+ if (first === "start" || first === "stop") return nativeRuntimeCommand(args);
3161
+ if (first !== void 0 && !first.startsWith("-") && !looksLikeProjectPath(first))
3162
+ return nativeRuntimeCommand(args);
3163
+ let port = DEFAULT_PORT;
3164
+ let root;
3165
+ const positional = [];
3166
+ let argumentError;
3167
+ for (let index = 0; index < args.length; index += 1) {
3168
+ const arg = args[index];
3169
+ if (arg === "--port" && index + 1 < args.length) {
3170
+ port = Number(args[++index]);
3171
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
3172
+ argumentError = "--port must be an integer between 0 and 65535";
3173
+ }
3174
+ } else if (arg === "--root" && index + 1 < args.length) {
3175
+ root = resolve3(args[++index]);
3176
+ } else if (arg === "--port" || arg === "--root") {
3177
+ argumentError = `${arg} requires a value`;
3178
+ } else if (arg === "-" || !arg.startsWith("-")) {
3179
+ positional.push(arg);
3180
+ } else {
3181
+ argumentError = `Unknown option: ${arg}`;
3182
+ }
3183
+ }
3184
+ if (argumentError) {
3185
+ console.error(`Error: ${argumentError}`);
3186
+ return { exitCode: Promise.resolve(2), shutdown: noopShutdown };
3187
+ }
3188
+ if (positional.length > 1) {
3189
+ console.error("Error: Too many arguments");
3190
+ return { exitCode: Promise.resolve(2), shutdown: noopShutdown };
3191
+ }
3192
+ const inputPath = positional[0];
3193
+ let filePath;
3194
+ let projectData;
3195
+ try {
3196
+ if (inputPath === "-") {
3197
+ if (!options.stdinInput) throw new Error("No stdin input provided");
3198
+ filePath = "<stdin>";
3199
+ projectData = JSON.parse(options.stdinInput);
3200
+ } else {
3201
+ filePath = inputPath ? resolve3(inputPath) : resolve3(process.cwd(), ".rogatio.json");
3202
+ projectData = await readProject(filePath);
3203
+ }
3204
+ } catch (error) {
3205
+ const message = error instanceof Error ? error.message : "Unknown error";
3206
+ console.error(`Error: ${message}`);
3207
+ return { exitCode: Promise.resolve(2), shutdown: noopShutdown };
3208
+ }
3209
+ const schemaResult = validateProjectDetailed2(projectData);
3210
+ if (!schemaResult.valid) {
3211
+ for (const issue of schemaResult.errors) {
3212
+ console.error(
3213
+ `${issue.instancePath || "/"}: ${issue.message} (schema.${issue.keyword})`
3214
+ );
3215
+ }
3216
+ return { exitCode: Promise.resolve(1), shutdown: noopShutdown };
3217
+ }
3218
+ const compileResult = compileProject2(schemaResult.data);
3219
+ if (!compileResult.ok) {
3220
+ for (const diagnostic of compileResult.diagnostics) {
3221
+ console.error(
3222
+ `${diagnostic.path}: ${diagnostic.message} (${diagnostic.code})`
3223
+ );
3224
+ }
3225
+ return { exitCode: Promise.resolve(1), shutdown: noopShutdown };
3226
+ }
3227
+ const rootDir = root ?? (inputPath === "-" ? process.cwd() : dirname4(filePath));
3228
+ const mocksResult = buildMockConfigs(compileResult.operations, rootDir);
3229
+ if (!mocksResult.ok) {
3230
+ console.error(`Error: ${mocksResult.message}`);
3231
+ return { exitCode: Promise.resolve(1), shutdown: noopShutdown };
3232
+ }
3233
+ const normalized = normalizeRuntimePreset({
3234
+ version: 1,
3235
+ limits: RUNTIME_LIMITS,
3236
+ matchers: toMatcherOperations2(compileResult.operations),
3237
+ grants: [],
3238
+ ...mocksResult.value.length > 0 ? { mocks: mocksResult.value } : {}
3239
+ });
3240
+ if (!normalized.ok) {
3241
+ console.error("Error: Failed to build runtime preset");
3242
+ return { exitCode: Promise.resolve(2), shutdown: noopShutdown };
3243
+ }
3244
+ let server;
3245
+ try {
3246
+ const result = await createRuntimeServer({
3247
+ preset: normalized.value,
3248
+ fileRoot: rootDir,
3249
+ port
3250
+ });
3251
+ if (!result.ok) throw new Error(result.error.code);
3252
+ server = result.value;
3253
+ } catch (error) {
3254
+ const detail = error instanceof Error ? error.message : "startup failed";
3255
+ console.error(
3256
+ `Error: Failed to start mock runtime on port ${port} (${detail})`
3257
+ );
3258
+ return { exitCode: Promise.resolve(2), shutdown: noopShutdown };
3259
+ }
3260
+ console.log(
3261
+ `Rogatio mock runtime listening on http://127.0.0.1:${server.bootstrap.port}`
3262
+ );
3263
+ console.log(`Project digest: ${server.bootstrap.presetDigest}`);
3264
+ console.log(`Mock rules: ${mocksResult.value.length}`);
3265
+ console.log(
3266
+ 'Open the extension and click "Check and connect" to install mock rules.'
3267
+ );
3268
+ console.log("Press Ctrl+C to stop.");
3269
+ let shutdownCalled = false;
3270
+ let resolveExit;
3271
+ const exitCode = new Promise((resolvePromise) => {
3272
+ resolveExit = resolvePromise;
3273
+ });
3274
+ const handleSignal = () => shutdown();
3275
+ process.on("SIGINT", handleSignal);
3276
+ process.on("SIGTERM", handleSignal);
3277
+ function shutdown() {
3278
+ if (shutdownCalled) return;
3279
+ shutdownCalled = true;
3280
+ process.off("SIGINT", handleSignal);
3281
+ process.off("SIGTERM", handleSignal);
3282
+ server.stop().then(
3283
+ () => resolveExit(0),
3284
+ () => resolveExit(0)
3285
+ );
3286
+ }
3287
+ return { exitCode, shutdown };
3288
+ }
3289
+
3290
+ // packages/cli/src/commands/test.ts
3291
+ import { readFile as readFile4 } from "node:fs/promises";
3292
+ import { resolve as resolve4 } from "node:path";
3293
+ import { compileProject as compileProject3 } from "@rogatio/compiler";
3294
+ import { validateProjectDetailed as validateProjectDetailed3 } from "@rogatio/schema";
3295
+ function usageError(message) {
3296
+ return `Error: ${message}
3297
+ `;
3298
+ }
3299
+ function toMatcherOperations3(operations) {
3300
+ return operations.map(({ groupId, ruleId, matcher }) => ({
3301
+ kind: "matcher",
3302
+ groupId,
3303
+ ruleId,
3304
+ matcher
3305
+ }));
3306
+ }
3307
+ function isUrl(value) {
3308
+ return parseTestUrl(value).ok;
3309
+ }
3310
+ function ownDataProperty(value, key) {
3311
+ try {
3312
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
3313
+ if (descriptor === void 0) return { present: false, valid: true };
3314
+ if (!("value" in descriptor) || descriptor.enumerable === false) {
3315
+ return { present: true, valid: false };
3316
+ }
3317
+ return { present: true, value: descriptor.value, valid: true };
3318
+ } catch {
3319
+ return { present: true, valid: false };
3320
+ }
3321
+ }
3322
+ function applyDefaults(raw, defaultMethod, defaultResourceType) {
3323
+ if (defaultMethod === void 0 && defaultResourceType === void 0) {
3324
+ return raw;
3325
+ }
3326
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
3327
+ return raw;
3328
+ }
3329
+ try {
3330
+ const prototype = Object.getPrototypeOf(raw);
3331
+ if (prototype !== Object.prototype && prototype !== null) return raw;
3332
+ const names = Object.getOwnPropertyNames(raw);
3333
+ if (Object.getOwnPropertySymbols(raw).length > 0) return raw;
3334
+ if (names.some((name) => !["url", "method", "resourceType"].includes(name))) {
3335
+ return raw;
3336
+ }
3337
+ const url = ownDataProperty(raw, "url");
3338
+ const method = ownDataProperty(raw, "method");
3339
+ const resourceType = ownDataProperty(raw, "resourceType");
3340
+ if (!url.valid || !method.valid || !resourceType.valid) return raw;
3341
+ if (typeof url.value !== "string") return raw;
3342
+ if (method.present && typeof method.value !== "string") return raw;
3343
+ if (resourceType.present && typeof resourceType.value !== "string")
3344
+ return raw;
3345
+ const result = { url: url.value };
3346
+ result.method = method.present ? method.value : defaultMethod;
3347
+ result.resourceType = resourceType.present ? resourceType.value : defaultResourceType;
3348
+ if (result.method === void 0) delete result.method;
3349
+ if (result.resourceType === void 0) delete result.resourceType;
3350
+ return result;
3351
+ } catch {
3352
+ return raw;
3353
+ }
3354
+ }
3355
+ function addDefaults(cases, defaultMethod, defaultResourceType) {
3356
+ return cases.map(
3357
+ (raw) => applyDefaults(raw, defaultMethod, defaultResourceType)
3358
+ );
3359
+ }
3360
+ function resultOptions(maxCases, operations) {
3361
+ const options = {};
3362
+ if (maxCases !== void 0) options.maxCases = maxCases;
3363
+ options.previewAction = createMockPreviewAction(operations);
3364
+ return options;
3365
+ }
3366
+ function diagnosticsFromSchema(projectData) {
3367
+ const result = validateProjectDetailed3(projectData);
3368
+ if (result.valid) return [];
3369
+ return result.errors.map((error) => ({
3370
+ code: `schema.${error.keyword}`,
3371
+ severity: "error",
3372
+ path: error.instancePath || "/",
3373
+ message: error.message,
3374
+ params: error.params
3375
+ }));
3376
+ }
3377
+ function diagnosticsFromCompiler(result) {
3378
+ if (result.ok) return [];
3379
+ return result.diagnostics.map((diagnostic) => ({
3380
+ code: diagnostic.code,
3381
+ severity: diagnostic.severity,
3382
+ path: diagnostic.path,
3383
+ message: diagnostic.message,
3384
+ params: diagnostic.params
3385
+ }));
3386
+ }
3387
+ function jsonOutput(value) {
3388
+ return `${JSON.stringify(value, null, 2)}
3389
+ `;
3390
+ }
3391
+ function testCommandNeedsStdin(args) {
3392
+ let hasUrlSource = false;
3393
+ const positional = [];
3394
+ for (let index = 0; index < args.length; index += 1) {
3395
+ const arg = args[index];
3396
+ if (arg === "--urls") {
3397
+ hasUrlSource = true;
3398
+ index += 1;
3399
+ } else if (arg === "--urls-file") {
3400
+ hasUrlSource = true;
3401
+ if (args[index + 1] === "-") return true;
3402
+ index += 1;
3403
+ } else if (arg === "--method" || arg === "--resource-type" || arg === "--max-cases") {
3404
+ index += 1;
3405
+ } else if (arg !== "--json" && !arg.startsWith("-")) {
3406
+ positional.push(arg);
3407
+ } else if (arg === "-") {
3408
+ positional.push(arg);
3409
+ }
3410
+ }
3411
+ if (positional[0] === "-") return true;
3412
+ const positionalUrls = isUrl(positional[0] ?? "") ? positional : positional.slice(1);
3413
+ return !hasUrlSource && positionalUrls.length === 0;
3414
+ }
3415
+ async function testCommandImpl(args, stdinInput, captureOutput) {
3416
+ let filePath = resolve4(process.cwd(), ".rogatio.json");
3417
+ let jsonMode = false;
3418
+ let maxCases;
3419
+ const urlCases = [];
3420
+ let urlsFile;
3421
+ let defaultMethod;
3422
+ let defaultResourceType;
3423
+ let argumentError;
3424
+ const positionalArgs = [];
3425
+ for (let i = 0; i < args.length; i++) {
3426
+ const arg = args[i];
3427
+ if (arg === "--json") {
3428
+ jsonMode = true;
3429
+ } else if (arg === "--max-cases" && i + 1 < args.length) {
3430
+ maxCases = Number(args[++i]);
3431
+ if (!Number.isInteger(maxCases) || maxCases <= 0) {
3432
+ argumentError = "--max-cases must be a positive integer";
3433
+ }
3434
+ } else if (arg === "--urls" && i + 1 < args.length) {
3435
+ const urlList = args[++i].split(",").map((u) => u.trim()).filter((u) => u.length > 0);
3436
+ for (const url of urlList) urlCases.push({ url });
3437
+ } else if (arg === "--urls-file" && i + 1 < args.length) {
3438
+ urlsFile = args[++i];
3439
+ } else if (arg === "--method" && i + 1 < args.length) {
3440
+ defaultMethod = args[++i];
3441
+ } else if (arg === "--resource-type" && i + 1 < args.length) {
3442
+ defaultResourceType = args[++i];
3443
+ } else if (arg === "--max-cases" || arg === "--urls" || arg === "--urls-file" || arg === "--method" || arg === "--resource-type") {
3444
+ argumentError = `${arg} requires a value`;
3445
+ } else if (arg === "-" || !arg.startsWith("-")) {
3446
+ positionalArgs.push(arg);
3447
+ } else {
3448
+ argumentError = `Unknown option: ${arg}`;
3449
+ }
3450
+ }
3451
+ if (argumentError) {
3452
+ const output = usageError(argumentError);
3453
+ if (captureOutput) return output;
3454
+ console.error(output.trim());
3455
+ return 2;
3456
+ }
3457
+ const firstIsUrl = isUrl(positionalArgs[0] ?? "");
3458
+ const inputPath = firstIsUrl ? void 0 : positionalArgs[0];
3459
+ const positionalUrls = firstIsUrl ? positionalArgs : positionalArgs.slice(1);
3460
+ if (inputPath === "-" && urlsFile === "-") {
3461
+ const output = usageError("project and URL cases cannot both read stdin");
3462
+ if (captureOutput) return output;
3463
+ console.error(output.trim());
3464
+ return 2;
3465
+ }
3466
+ if (inputPath === "-") {
3467
+ if (!stdinInput) {
3468
+ const output = usageError("No stdin input provided");
3469
+ if (captureOutput) return output;
3470
+ console.error(output.trim());
3471
+ return 2;
3472
+ }
3473
+ filePath = "<stdin>";
3474
+ } else if (inputPath) {
3475
+ filePath = resolve4(inputPath);
3476
+ }
3477
+ for (const url of positionalUrls) urlCases.push({ url });
3478
+ if (urlsFile) {
3479
+ try {
3480
+ const content = urlsFile === "-" ? stdinInput ?? "" : await readFile4(urlsFile, "utf8");
3481
+ const parsed = JSON.parse(content);
3482
+ if (!Array.isArray(parsed)) {
3483
+ const output = usageError(`${urlsFile} must contain a JSON array`);
3484
+ if (captureOutput) return output;
3485
+ console.error(output.trim());
3486
+ return 2;
3487
+ }
3488
+ urlCases.push(...parsed);
3489
+ } catch (e) {
3490
+ const message = e instanceof Error ? e.message : "Unable to read URL cases";
3491
+ const output = usageError(`Error reading ${urlsFile}: ${message}`);
3492
+ if (captureOutput) return output;
3493
+ console.error(output.trim());
3494
+ return 2;
3495
+ }
3496
+ }
3497
+ if (urlCases.length === 0 && inputPath !== "-" && urlsFile === void 0 && stdinInput !== void 0) {
3498
+ for (const line of stdinInput.split(/\r?\n/u)) {
3499
+ const url = line.trim();
3500
+ if (url.length > 0) urlCases.push({ url });
3501
+ }
3502
+ }
3503
+ if (urlCases.length === 0) {
3504
+ const output = usageError(
3505
+ "No test cases provided (use positional URLs, --urls, --urls-file, or stdin)"
3506
+ );
3507
+ if (captureOutput) return output;
3508
+ console.error(output.trim());
3509
+ return 2;
3510
+ }
3511
+ let projectData;
3512
+ try {
3513
+ if (inputPath === "-") {
3514
+ if (!stdinInput) throw new Error("No stdin input provided");
3515
+ projectData = JSON.parse(stdinInput);
3516
+ } else {
3517
+ projectData = await readProject(filePath);
3518
+ }
3519
+ } catch (e) {
3520
+ const message = e instanceof Error ? e.message : "Unknown error";
3521
+ const output = `Error: ${message}
3522
+ `;
3523
+ if (captureOutput) return output;
3524
+ console.error(output.trim());
3525
+ return 2;
3526
+ }
3527
+ const schemaResult = validateProjectDetailed3(projectData);
3528
+ let diagnostics = diagnosticsFromSchema(projectData);
3529
+ if (!schemaResult.valid) {
3530
+ const output = jsonMode ? jsonOutput({ diagnostics }) : diagnostics.map(
3531
+ (diagnostic) => `${diagnostic.path}: ${diagnostic.message} (${diagnostic.code})
3532
+ `
3533
+ ).join("");
3534
+ if (captureOutput) return output;
3535
+ if (jsonMode) console.log(output.trim());
3536
+ else console.error(output.trim());
3537
+ return 1;
3538
+ }
3539
+ const compileResult = compileProject3(schemaResult.data);
3540
+ diagnostics = diagnosticsFromCompiler(compileResult);
3541
+ if (!compileResult.ok) {
3542
+ const output = jsonMode ? jsonOutput({ diagnostics }) : diagnostics.map(
3543
+ (diagnostic) => `${diagnostic.path}: ${diagnostic.message} (${diagnostic.code})
3544
+ `
3545
+ ).join("");
3546
+ if (captureOutput) return output;
3547
+ if (jsonMode) console.log(output.trim());
3548
+ else console.error(output.trim());
3549
+ return 1;
3550
+ }
3551
+ const testCases = addDefaults(
3552
+ urlCases,
3553
+ defaultMethod,
3554
+ defaultResourceType
3555
+ );
3556
+ const dryRunResult = dryRunProject(
3557
+ toMatcherOperations3(compileResult.operations),
3558
+ testCases,
3559
+ resultOptions(maxCases, compileResult.operations)
3560
+ );
3561
+ if (jsonMode) {
3562
+ const output = jsonOutput(dryRunResult);
3563
+ if (captureOutput) return output;
3564
+ console.log(output.trim());
3565
+ } else {
3566
+ let output = "";
3567
+ for (const urlResult of dryRunResult.results) {
3568
+ output += `
3569
+ URL: ${urlResult.url}
3570
+ `;
3571
+ output += ` Matched rules: ${urlResult.matchedRuleCount}
3572
+ `;
3573
+ for (const rule of urlResult.rules) {
3574
+ output += ` ${rule.groupId}/${rule.ruleId}: ${rule.matched ? "MATCHED" : "NOT MATCHED"}
3575
+ `;
3576
+ output += ` urlRegex: ${rule.urlRegex.state} - ${rule.urlRegex.detail}
3577
+ `;
3578
+ output += ` effectiveOrigin: ${rule.effectiveOrigin.state} - ${rule.effectiveOrigin.detail}
3579
+ `;
3580
+ output += ` method: ${rule.method.state} - ${rule.method.detail}
3581
+ `;
3582
+ output += ` resourceType: ${rule.resourceType.state} - ${rule.resourceType.detail}
3583
+ `;
3584
+ if (rule.actionPreview) {
3585
+ output += ` actionPreview: ${rule.actionPreview.kind} - ${rule.actionPreview.summary}
3586
+ `;
3587
+ }
3588
+ }
3589
+ }
3590
+ if (dryRunResult.results.length === 0) output += "No valid URLs to test\n";
3591
+ output += `
3592
+ Summary:
3593
+ `;
3594
+ output += ` Total cases: ${dryRunResult.summary.caseCount}
3595
+ `;
3596
+ output += ` Valid URLs: ${dryRunResult.summary.urlCount}
3597
+ `;
3598
+ output += ` Matched URLs: ${dryRunResult.summary.matchedUrlCount}
3599
+ `;
3600
+ output += ` Total rule matches: ${dryRunResult.summary.matchedRuleTotal}
3601
+ `;
3602
+ for (const error of dryRunResult.errors) {
3603
+ output += `
3604
+ ${error.code}: ${error.message}${error.index === void 0 ? "" : ` (case ${error.index})`}
3605
+ `;
3606
+ }
3607
+ if (captureOutput) return output;
3608
+ console.log(output.trim());
3609
+ }
3610
+ return dryRunResult.errors.length === 0 ? 0 : 1;
3611
+ }
3612
+ async function testCommand(args, stdinInput, captureOutput = false) {
3613
+ return testCommandImpl(args, stdinInput, captureOutput);
3614
+ }
3615
+
3616
+ // packages/cli/src/commands/verify.ts
3617
+ import { dirname as dirname5, resolve as resolve5 } from "node:path";
3618
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3619
+ import { compileProject as compileProject4 } from "@rogatio/compiler";
3620
+ import { validateProjectDetailed as validateProjectDetailed4 } from "@rogatio/schema";
3621
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
3622
+ async function verifyCommandImpl(args, stdinInput, captureOutput) {
3623
+ let filePath;
3624
+ let jsonOutput2 = false;
3625
+ const positionalArgs = [];
3626
+ for (const arg of args) {
3627
+ if (arg === "--json") {
3628
+ jsonOutput2 = true;
3629
+ } else if (arg === "-" || !arg.startsWith("-")) {
3630
+ positionalArgs.push(arg);
3631
+ }
3632
+ }
3633
+ if (positionalArgs.length > 1) {
3634
+ if (captureOutput) return "Error: Too many arguments\n";
3635
+ console.error("Error: Too many arguments");
3636
+ return 2;
3637
+ }
3638
+ const inputPath = positionalArgs[0];
3639
+ if (inputPath === "-") {
3640
+ if (!stdinInput) {
3641
+ if (captureOutput) return "Error: No stdin input provided\n";
3642
+ console.error("Error: No stdin input provided");
3643
+ return 2;
3644
+ }
3645
+ filePath = "<stdin>";
3646
+ } else if (inputPath) {
3647
+ filePath = resolve5(inputPath);
3648
+ } else {
3649
+ filePath = resolve5(process.cwd(), ".rogatio.json");
3650
+ }
3651
+ let projectData;
3652
+ try {
3653
+ if (inputPath === "-") {
3654
+ if (!stdinInput) throw new Error("No stdin input provided");
3655
+ projectData = JSON.parse(stdinInput);
3656
+ } else {
3657
+ projectData = await readProject(filePath);
3658
+ }
3659
+ } catch (e) {
3660
+ const message = e instanceof Error ? e.message : "Unknown error";
3661
+ const output2 = `Error: ${message}
3662
+ `;
3663
+ if (captureOutput) return output2;
3664
+ console.error(output2.trim());
3665
+ return 2;
3666
+ }
3667
+ const schemaResult = validateProjectDetailed4(projectData);
3668
+ const diagnostics = [];
3669
+ if (!schemaResult.valid) {
3670
+ for (const error of schemaResult.errors) {
3671
+ diagnostics.push({
3672
+ code: `schema.${error.keyword}`,
3673
+ severity: "error",
3674
+ path: error.instancePath || "/",
3675
+ message: error.message,
3676
+ params: error.params
3677
+ });
3678
+ }
3679
+ } else {
3680
+ const compileResult = compileProject4(schemaResult.data);
3681
+ if (!compileResult.ok) {
3682
+ for (const diag of compileResult.diagnostics) {
3683
+ diagnostics.push({
3684
+ code: diag.code,
3685
+ severity: diag.severity,
3686
+ path: diag.path,
3687
+ message: diag.message,
3688
+ params: diag.params
3689
+ });
3690
+ }
3691
+ }
3692
+ }
3693
+ let output = "";
3694
+ if (jsonOutput2) {
3695
+ output = `${JSON.stringify(diagnostics, null, 2)}
3696
+ `;
3697
+ } else {
3698
+ if (diagnostics.length === 0) {
3699
+ output = "Valid\n";
3700
+ } else {
3701
+ for (const diag of diagnostics) {
3702
+ output += `${diag.path}: ${diag.message} (${diag.code})
3703
+ `;
3704
+ }
3705
+ }
3706
+ }
3707
+ if (captureOutput) return output;
3708
+ if (output) console.log(output.trim());
3709
+ if (diagnostics.length === 0) return 0;
3710
+ return 1;
3711
+ }
3712
+ async function verifyCommand(args, stdinInput, captureOutput = false) {
3713
+ return verifyCommandImpl(args, stdinInput, captureOutput);
3714
+ }
3715
+
3716
+ // packages/cli/src/index.ts
3717
+ var __dirname3 = dirname6(fileURLToPath3(import.meta.url));
3718
+ var isDist = __dirname3.includes("/dist/") || __dirname3.includes("\\dist\\");
3719
+ var packageJsonPath = resolve6(
3720
+ __dirname3,
3721
+ isDist ? "../../package.json" : "../package.json"
3722
+ );
3723
+ var packageJson = JSON.parse(
3724
+ await import("node:fs/promises").then(
3725
+ (fs) => fs.readFile(packageJsonPath, "utf-8")
3726
+ )
3727
+ );
3728
+ var VERSION = packageJson.version;
3729
+ async function cli(args = process.argv.slice(2)) {
3730
+ if (args.length === 0) {
3731
+ return showHelp();
3732
+ }
3733
+ const command = args[0];
3734
+ const commandArgs = args.slice(1);
3735
+ switch (command) {
3736
+ case "edit":
3737
+ return handleEdit(commandArgs);
3738
+ case "test":
3739
+ return handleTest(commandArgs);
3740
+ case "verify":
3741
+ return handleVerify(commandArgs);
3742
+ case "runtime":
3743
+ return handleRuntime(commandArgs);
3744
+ case "--help":
3745
+ case "-h":
3746
+ return showHelp();
3747
+ case "--version":
3748
+ case "-v":
3749
+ console.log(VERSION);
3750
+ return 0;
3751
+ default:
3752
+ console.error(`Error: Unknown command: ${command}`);
3753
+ console.error("Run 'rogatio --help' for usage.");
3754
+ return 2;
3755
+ }
3756
+ }
3757
+ async function handleEdit(args) {
3758
+ if (args.includes("--help") || args.includes("-h")) {
3759
+ showEditHelp();
3760
+ return 0;
3761
+ }
3762
+ const result = await editCommand(args);
3763
+ return result.exitCode;
3764
+ }
3765
+ async function handleVerify(args) {
3766
+ if (args.includes("--help") || args.includes("-h")) {
3767
+ showVerifyHelp();
3768
+ return 0;
3769
+ }
3770
+ const result = await verifyCommand(args);
3771
+ return typeof result === "number" ? result : 1;
3772
+ }
3773
+ async function handleRuntime(args) {
3774
+ if (args.includes("--help") || args.includes("-h")) {
3775
+ showRuntimeHelp2();
3776
+ return 0;
3777
+ }
3778
+ const result = await runtimeCommand(args);
3779
+ return typeof result === "number" ? result : await result.exitCode;
3780
+ }
3781
+ async function handleTest(args) {
3782
+ if (args.includes("--help") || args.includes("-h")) {
3783
+ showTestHelp();
3784
+ return 0;
3785
+ }
3786
+ let stdinInput;
3787
+ if (testCommandNeedsStdin(args)) {
3788
+ try {
3789
+ const chunks = [];
3790
+ for await (const chunk of process.stdin) {
3791
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
3792
+ }
3793
+ stdinInput = chunks.join("");
3794
+ } catch (error) {
3795
+ console.error(
3796
+ `Error: Unable to read stdin (${error instanceof Error ? error.message : "read failed"})`
3797
+ );
3798
+ return 2;
3799
+ }
3800
+ }
3801
+ const result = await testCommand(args, stdinInput);
3802
+ return typeof result === "number" ? result : 1;
3803
+ }
3804
+ function showHelp() {
3805
+ console.log(`Rogatio CLI - Local-first browser request/response rules
3806
+
3807
+ Usage: rogatio <command> [options]
3808
+
3809
+ Commands:
3810
+ edit [path] Launch browser editor for .rogatio.json
3811
+ test [path] [url...] Run offline dry-run tests against .rogatio.json
3812
+ verify [path] Validate .rogatio.json file
3813
+ runtime <start|stop|status|install|trust|untrust|uninstall> Native messaging runtime and request-body trust control
3814
+ runtime [path] Start the mock runtime server (F13)
3815
+
3816
+ Global Options:
3817
+ --help, -h Show help
3818
+ --version, -v Show version
3819
+
3820
+ Run 'rogatio <command> --help' for command-specific help.`);
3821
+ return 0;
3822
+ }
3823
+ function showEditHelp() {
3824
+ console.log(`Usage: rogatio edit [options] [path]
3825
+
3826
+ Launch browser-based editor for .rogatio.json project file.
3827
+
3828
+ Arguments:
3829
+ path Path to .rogatio.json (default: .rogatio.json in current directory)
3830
+
3831
+ Options:
3832
+ --port <n> Fixed port for editor server (default: random)
3833
+ --help, -h Show this help
3834
+
3835
+ The editor runs in your default browser and communicates with a local server
3836
+ bound to 127.0.0.1. Changes are saved atomically to the project file.`);
3837
+ }
3838
+ function showVerifyHelp() {
3839
+ console.log(`Usage: rogatio verify [options] [path]
3840
+
3841
+ Validate a .rogatio.json project file using schema and compiler.
3842
+
3843
+ Arguments:
3844
+ path Path to .rogatio.json (default: .rogatio.json in current directory)
3845
+ Use '-' to read from stdin
3846
+
3847
+ Options:
3848
+ --json Output diagnostics as JSON
3849
+ --help, -h Show this help
3850
+
3851
+ Exit codes:
3852
+ 0 Valid (no diagnostics)
3853
+ 1 Invalid (diagnostics present)
3854
+ 2 Error (IO, parse, or unexpected failure)`);
3855
+ }
3856
+ function showRuntimeHelp2() {
3857
+ console.log(`Usage: rogatio runtime <command> [options]
3858
+ rogatio runtime [options] [path]
3859
+
3860
+ Native messaging runtime control for response-body and request-body rules, or
3861
+ start the local mock runtime for F13 mock rules.
3862
+
3863
+ Native runtime commands:
3864
+ start Start the runtime (capability-gated; explicit, no auto-start)
3865
+ stop Stop the runtime (idempotent)
3866
+ status Show the current runtime state
3867
+
3868
+ Mock runtime arguments:
3869
+ path Path to .rogatio.json (default: .rogatio.json in current directory)
3870
+ Use '-' to read project JSON from stdin
3871
+
3872
+ Options:
3873
+ --port <n> Port for the mock runtime (default: 8890; use 0 for ephemeral)
3874
+ --root <dir> Root for confined file mocks (default: project directory)
3875
+ --help, -h Show this help
3876
+
3877
+ The native runtime activates only where a trusted device-local CA can be provisioned
3878
+ and Chrome PAC routing does not collide with an existing controlling proxy/PAC/extension
3879
+ or enterprise policy. On incapable platforms 'start' reports 'unsupported'.
3880
+ The mock runtime prints connection instructions; open the extension and click
3881
+ "Check and connect" to install mock rules.
3882
+
3883
+ Exit codes:
3884
+ 0 Stopped cleanly
3885
+ 1 Invalid project (diagnostics present) or file outside the root
3886
+ 2 Error (IO, startup, port conflict)`);
3887
+ }
3888
+ function showTestHelp() {
3889
+ console.log(`Usage: rogatio test [options] [path] [url...]
3890
+
3891
+ Run offline dry-run tests against a .rogatio.json project file.
3892
+
3893
+ Arguments:
3894
+ path Path to .rogatio.json (default: .rogatio.json in current directory)
3895
+ Use '-' to read project JSON from stdin
3896
+ url... URLs to test; when path is omitted, first URL is detected automatically
3897
+
3898
+ Options:
3899
+ --urls <list> Comma-separated list of URLs to test
3900
+ --urls-file <path> Path to JSON file containing array of test cases
3901
+ Each case: { "url": "...", "method"?: "...", "resourceType"?: "..." }
3902
+ Use '-' to read from stdin
3903
+ --method <m> Default HTTP method for all test cases (GET, POST, etc.)
3904
+ --resource-type <t> Default resource type for all test cases
3905
+ --max-cases <n> Maximum number of test cases (default: 256)
3906
+ --json Output results as JSON
3907
+ --help, -h Show this help
3908
+
3909
+ Test case format (JSON):
3910
+ [
3911
+ { "url": "https://example.com/", "method": "GET", "resourceType": "main_frame" },
3912
+ { "url": "https://example.com/script.js" }
3913
+ ]
3914
+
3915
+ Exit codes:
3916
+ 0 Success (all valid, results may include non-matches)
3917
+ 1 Validation/compile/test errors
3918
+ 2 Usage error (invalid arguments, missing input)`);
3919
+ }
3920
+ if (process.argv[1] !== void 0 && realpathSync.native(fileURLToPath3(import.meta.url)) === realpathSync.native(resolve6(process.argv[1]))) {
3921
+ cli().catch((err) => {
3922
+ console.error(err);
3923
+ process.exit(1);
3924
+ });
3925
+ }
3926
+ export {
3927
+ cli
3928
+ };