@ondc/automation-mock-runner 1.3.42 → 1.3.44

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.
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for the default helper functions + bundle integrity.
4
+ *
5
+ * Unit tests import the functions as a normal CJS module and call them.
6
+ * The bundle test loads DEFAULT_HELPER_LIB into a fresh vm context — this
7
+ * simulates how the string is actually used in the worker sandbox and
8
+ * catches any stringification / hoisting / free-variable regressions.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ const vm = __importStar(require("vm"));
45
+ const default_helpers_1 = require("./default-helpers");
46
+ const index_1 = require("./index");
47
+ describe("default helpers — unit", () => {
48
+ describe("uuidv4", () => {
49
+ it("matches the RFC 4122 v4 shape", () => {
50
+ expect((0, default_helpers_1.uuidv4)()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
51
+ });
52
+ it("produces distinct values", () => {
53
+ expect((0, default_helpers_1.uuidv4)()).not.toBe((0, default_helpers_1.uuidv4)());
54
+ });
55
+ });
56
+ describe("generate6DigitId", () => {
57
+ it("returns a 6-digit string in [100000, 999999]", () => {
58
+ for (let i = 0; i < 50; i++) {
59
+ const id = (0, default_helpers_1.generate6DigitId)();
60
+ expect(id).toMatch(/^\d{6}$/);
61
+ const n = parseInt(id, 10);
62
+ expect(n).toBeGreaterThanOrEqual(100000);
63
+ expect(n).toBeLessThanOrEqual(999999);
64
+ }
65
+ });
66
+ });
67
+ describe("currentTimestamp", () => {
68
+ it("returns a parseable ISO timestamp close to now", () => {
69
+ const before = Date.now();
70
+ const ts = (0, default_helpers_1.currentTimestamp)();
71
+ const after = Date.now();
72
+ const parsed = Date.parse(ts);
73
+ expect(parsed).toBeGreaterThanOrEqual(before);
74
+ expect(parsed).toBeLessThanOrEqual(after);
75
+ expect(ts.endsWith("Z")).toBe(true);
76
+ });
77
+ });
78
+ describe("isoDurToSec", () => {
79
+ it.each([
80
+ ["PT30S", 30],
81
+ ["PT5M", 300],
82
+ ["PT1H", 3600],
83
+ ["P1D", 86400],
84
+ ["PT1H30M", 5400],
85
+ ["PT0S", 0],
86
+ ])("%s → %d sec", (dur, expected) => {
87
+ expect((0, default_helpers_1.isoDurToSec)(dur)).toBe(expected);
88
+ });
89
+ it("returns 0 on unparseable input", () => {
90
+ expect((0, default_helpers_1.isoDurToSec)("not-a-duration")).toBe(0);
91
+ });
92
+ });
93
+ describe("setCityFromInputs", () => {
94
+ it("writes flat context.city for v1.x", () => {
95
+ const payload = { context: { core_version: "1.2.0", city: "*" } };
96
+ (0, default_helpers_1.setCityFromInputs)(payload, { city_code: "std:080" });
97
+ expect(payload.context.city).toBe("std:080");
98
+ });
99
+ it("writes nested context.location.city.code for v2.x", () => {
100
+ const payload = {
101
+ context: { version: "2.0.0", location: { city: { code: "*" } } },
102
+ };
103
+ (0, default_helpers_1.setCityFromInputs)(payload, { city_code: "std:011" });
104
+ expect(payload.context.location.city.code).toBe("std:011");
105
+ });
106
+ it("falls back to '*' when city_code is missing", () => {
107
+ const payload = {
108
+ context: { version: "2.0.0", location: { city: { code: "foo" } } },
109
+ };
110
+ (0, default_helpers_1.setCityFromInputs)(payload, {});
111
+ expect(payload.context.location.city.code).toBe("*");
112
+ });
113
+ it("no-ops when inputs is falsy", () => {
114
+ const payload = {
115
+ context: { version: "2.0.0", location: { city: { code: "keep" } } },
116
+ };
117
+ (0, default_helpers_1.setCityFromInputs)(payload, null);
118
+ expect(payload.context.location.city.code).toBe("keep");
119
+ });
120
+ });
121
+ describe("createFormURL", () => {
122
+ it("interpolates domain, formId, transactionId, sessionId", () => {
123
+ const url = (0, default_helpers_1.createFormURL)("ONDC:RET10", "FORM_A", {
124
+ mockBaseUrl: "https://mocks.example.com",
125
+ transactionId: ["txn-xyz"],
126
+ sessionId: "sess-1",
127
+ });
128
+ expect(url).toBe("https://mocks.example.com/forms/ONDC:RET10/FORM_A/?transaction_id=txn-xyz&session_id=sess-1");
129
+ });
130
+ });
131
+ describe("getSubscriberUrl", () => {
132
+ const sessionData = { bppUri: "https://bpp.example.com", bapUri: "https://bap.example.com" };
133
+ it("returns bppUri when type is 'bpp'", () => {
134
+ expect((0, default_helpers_1.getSubscriberUrl)(sessionData, "bpp")).toBe("https://bpp.example.com");
135
+ });
136
+ it("returns bapUri otherwise", () => {
137
+ expect((0, default_helpers_1.getSubscriberUrl)(sessionData, "bap")).toBe("https://bap.example.com");
138
+ expect((0, default_helpers_1.getSubscriberUrl)(sessionData, "anything-else")).toBe("https://bap.example.com");
139
+ });
140
+ });
141
+ describe("generateConsentHandler", () => {
142
+ function fakeResponse(init) {
143
+ return {
144
+ ok: init.ok,
145
+ status: init.status ?? (init.ok ? 200 : 500),
146
+ json: init.json ?? (async () => ({})),
147
+ text: init.text ?? (async () => ""),
148
+ };
149
+ }
150
+ let fetchSpy;
151
+ beforeEach(() => {
152
+ fetchSpy = jest.spyOn(global, "fetch");
153
+ });
154
+ afterEach(() => {
155
+ fetchSpy.mockRestore();
156
+ });
157
+ it("throws when custId is missing", async () => {
158
+ await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu" }, {})).rejects.toThrow("custId is required");
159
+ expect(fetchSpy).not.toHaveBeenCalled();
160
+ });
161
+ it("throws when sessionData.finvuUrl is missing", async () => {
162
+ await expect((0, default_helpers_1.generateConsentHandler)({}, { custId: "c1" })).rejects.toThrow("sessionData.finvuUrl is required");
163
+ expect(fetchSpy).not.toHaveBeenCalled();
164
+ });
165
+ it("POSTs JSON to /finvu-aa/consent/generate and returns consentHandler", async () => {
166
+ fetchSpy.mockResolvedValueOnce(fakeResponse({
167
+ ok: true,
168
+ json: async () => ({ consentHandler: "HANDLE-123" }),
169
+ }));
170
+ const result = await (0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local:3002" }, { custId: "cust-42" });
171
+ expect(result).toBe("HANDLE-123");
172
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
173
+ const [url, init] = fetchSpy.mock.calls[0];
174
+ expect(url).toBe("http://finvu.local:3002/finvu-aa/consent/generate");
175
+ expect(init?.method).toBe("POST");
176
+ expect((init?.headers)["Content-Type"]).toBe("application/json");
177
+ const body = JSON.parse(init?.body);
178
+ expect(body).toEqual({
179
+ custId: "cust-42",
180
+ templateName: "FINVUDEMO_TESTING",
181
+ consentDescription: "Gold Loan Account Aggregator Consent",
182
+ redirectUrl: "https://google.co.in",
183
+ });
184
+ expect(init?.signal).toBeDefined();
185
+ });
186
+ it("throws 'Request failed' on non-OK responses", async () => {
187
+ fetchSpy.mockResolvedValueOnce(fakeResponse({
188
+ ok: false,
189
+ status: 503,
190
+ text: async () => "Service Unavailable",
191
+ }));
192
+ await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local" }, { custId: "c1" })).rejects.toThrow("Request failed: 503 Service Unavailable");
193
+ });
194
+ it("throws when consentHandler is missing from response body", async () => {
195
+ fetchSpy.mockResolvedValueOnce(fakeResponse({
196
+ ok: true,
197
+ json: async () => ({ somethingElse: true }),
198
+ }));
199
+ await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local" }, { custId: "c1" })).rejects.toThrow("Invalid response: consentHandler missing");
200
+ });
201
+ it("surfaces AbortError as a timeout message", async () => {
202
+ fetchSpy.mockImplementationOnce(async () => {
203
+ const err = new Error("aborted");
204
+ err.name = "AbortError";
205
+ throw err;
206
+ });
207
+ await expect((0, default_helpers_1.generateConsentHandler)({ finvuUrl: "http://finvu.local" }, { custId: "c1" })).rejects.toThrow("Request timed out after 10 seconds");
208
+ });
209
+ });
210
+ });
211
+ describe("DEFAULT_HELPER_LIB bundle", () => {
212
+ const EXPECTED_NAMES = [
213
+ "getSubscriberUrl",
214
+ "uuidv4",
215
+ "generate6DigitId",
216
+ "currentTimestamp",
217
+ "isoDurToSec",
218
+ "setCityFromInputs",
219
+ "createFormURL",
220
+ "generateConsentHandler",
221
+ ];
222
+ function loadBundle() {
223
+ const sandbox = {};
224
+ vm.createContext(sandbox);
225
+ vm.runInContext(index_1.DEFAULT_HELPER_LIB, sandbox);
226
+ return sandbox;
227
+ }
228
+ it("does not leak `module.exports` or `require` into the bundle source", () => {
229
+ expect(index_1.DEFAULT_HELPER_LIB).not.toMatch(/module\.exports/);
230
+ expect(index_1.DEFAULT_HELPER_LIB).not.toMatch(/\brequire\s*\(/);
231
+ });
232
+ it("declares every expected helper as a function", () => {
233
+ const sandbox = loadBundle();
234
+ for (const name of EXPECTED_NAMES) {
235
+ expect(typeof sandbox[name]).toBe("function");
236
+ }
237
+ });
238
+ it("round-trips key helpers when executed in the vm context", () => {
239
+ const sandbox = loadBundle();
240
+ expect(sandbox.uuidv4()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
241
+ expect(sandbox.generate6DigitId()).toMatch(/^\d{6}$/);
242
+ expect(sandbox.isoDurToSec("PT1H")).toBe(3600);
243
+ const payload = {
244
+ context: { version: "2.0.0", location: { city: { code: "*" } } },
245
+ };
246
+ sandbox.setCityFromInputs(payload, { city_code: "std:022" });
247
+ expect(payload.context.location.city.code).toBe("std:022");
248
+ expect(sandbox.getSubscriberUrl({ bppUri: "http://bpp" }, "bpp")).toBe("http://bpp");
249
+ });
250
+ it("preserves in-body docs so playground users see them", () => {
251
+ // Guard against a future refactor silently dropping the in-body comments
252
+ // the way the pre-refactor .toString() pattern did.
253
+ expect(index_1.DEFAULT_HELPER_LIB).toContain("Generates a UUID v4");
254
+ expect(index_1.DEFAULT_HELPER_LIB).toContain("ISO 8601 duration");
255
+ expect(index_1.DEFAULT_HELPER_LIB).toContain("Finvu AA Service");
256
+ });
257
+ });
@@ -0,0 +1 @@
1
+ export declare const DEFAULT_HELPER_LIB: string;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_HELPER_LIB = void 0;
37
+ const defaultHelperFns = __importStar(require("./default-helpers"));
38
+ const HEADER = `/*
39
+ Custom helper functions available in all mock generate() functions.
40
+ Assembled from src/lib/helpers/default-helpers.js — edit there.
41
+ */`;
42
+ exports.DEFAULT_HELPER_LIB = [
43
+ HEADER,
44
+ ...Object.values(defaultHelperFns)
45
+ .filter((v) => typeof v === "function")
46
+ .map((f) => f.toString()),
47
+ ].join("\n\n");
@@ -1,6 +1,19 @@
1
1
  import { FunctionSchema } from "../constants/function-registry";
2
2
  import { ExecutionResult } from "../types/execution-results";
3
3
  import { BaseCodeRunner } from "./base-runner";
4
+ export interface NodeRunnerOptions {
5
+ maxMemoryMB?: number;
6
+ poolSize?: number;
7
+ maxExecutionsPerWorker?: number;
8
+ maxWorkerAgeMs?: number;
9
+ /**
10
+ * Base URLs that `generate` functions may fetch. Each entry is parsed with
11
+ * `new URL(entry)`; a request is allowed iff its origin matches the entry's
12
+ * origin AND its pathname is a strict segment-prefix of the entry's pathname.
13
+ * Absent / empty → fetch is not injected (blocked).
14
+ */
15
+ allowedFetchBaseUrls?: string[];
16
+ }
4
17
  export declare class NodeRunner implements BaseCodeRunner {
5
18
  private pool;
6
19
  private waitQueue;
@@ -11,12 +24,8 @@ export declare class NodeRunner implements BaseCodeRunner {
11
24
  private readonly poolSize;
12
25
  private readonly maxExecutionsPerWorker;
13
26
  private readonly maxWorkerAgeMs;
14
- constructor(options?: {
15
- maxMemoryMB?: number;
16
- poolSize?: number;
17
- maxExecutionsPerWorker?: number;
18
- maxWorkerAgeMs?: number;
19
- });
27
+ private readonly allowedFetchBaseUrls;
28
+ constructor(options?: NodeRunnerOptions);
20
29
  private createPooledWorker;
21
30
  /**
22
31
  * Terminate old worker (frees its V8 isolate) and replace with a fresh one.
@@ -52,6 +52,7 @@ class NodeRunner {
52
52
  this.maxExecutionsPerWorker =
53
53
  options.maxExecutionsPerWorker || MAX_EXECUTIONS_PER_WORKER;
54
54
  this.maxWorkerAgeMs = options.maxWorkerAgeMs || MAX_WORKER_AGE_MS;
55
+ this.allowedFetchBaseUrls = options.allowedFetchBaseUrls ?? [];
55
56
  // Pre-warm the pool
56
57
  for (let i = 0; i < this.poolSize; i++) {
57
58
  this.pool.push(this.createPooledWorker());
@@ -66,6 +67,9 @@ class NodeRunner {
66
67
  codeRangeSizeMb: 16,
67
68
  },
68
69
  env: {},
70
+ workerData: {
71
+ allowedFetchBaseUrls: this.allowedFetchBaseUrls,
72
+ },
69
73
  });
70
74
  const pw = {
71
75
  worker,
@@ -1,6 +1,7 @@
1
1
  import { Worker } from "worker_threads";
2
2
  import { BrowserRunner } from "./browser-runner";
3
- import { NodeRunner } from "./node-runner";
3
+ import { NodeRunner, NodeRunnerOptions } from "./node-runner";
4
+ export type RunnerOptions = NodeRunnerOptions;
4
5
  /**
5
6
  * Factory for creating workers in both browser and Node.js environments
6
7
  */
@@ -21,5 +22,5 @@ export declare class RunnerFactory {
21
22
  /**
22
23
  * Creates appropriate runner based on environment
23
24
  */
24
- static createRunner(options?: any, logger?: any): BrowserRunner | NodeRunner;
25
+ static createRunner(options?: RunnerOptions, logger?: any): BrowserRunner | NodeRunner;
25
26
  }
@@ -316,7 +316,9 @@ CodeValidator.FORBIDDEN_GLOBALS = [
316
316
  "SharedWorker",
317
317
  "WebSocket",
318
318
  "XMLHttpRequest",
319
- "fetch",
319
+ // `fetch` is allowed past static analysis and gated at runtime in the
320
+ // worker sandbox: injected only for `generate` and only when the
321
+ // configured allowlist matches origin + path prefix.
320
322
  ];
321
323
  CodeValidator.FORBIDDEN_PROPERTIES = [
322
324
  "localStorage",
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for generate-only fetch allowlist + 45s sandbox setTimeout cap.
3
+ */
4
+ export {};
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for generate-only fetch allowlist + 45s sandbox setTimeout cap.
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const http = __importStar(require("http"));
40
+ const MockRunner_1 = require("../lib/MockRunner");
41
+ const function_registry_1 = require("../lib/constants/function-registry");
42
+ function baseConfig() {
43
+ return {
44
+ meta: { domain: "ONDC:TRV14", version: "2.0.0", flowId: "fetch-test" },
45
+ transaction_data: {
46
+ transaction_id: "e9e0b5cb-3f15-48a1-9d86-d4d643f0909d",
47
+ latest_timestamp: "1970-01-01T00:00:00.000Z",
48
+ },
49
+ steps: [],
50
+ transaction_history: [],
51
+ validationLib: "",
52
+ helperLib: "",
53
+ };
54
+ }
55
+ function stepWithGenerate(runner, actionId, genSource) {
56
+ const step = runner.getDefaultStep("search", actionId);
57
+ step.mock.generate = MockRunner_1.MockRunner.encodeBase64(genSource);
58
+ step.mock.inputs = {};
59
+ return step;
60
+ }
61
+ async function resetSharedRunner() {
62
+ const sr = MockRunner_1.MockRunner.sharedRunner;
63
+ if (sr?.terminate) {
64
+ await sr.terminate();
65
+ }
66
+ MockRunner_1.MockRunner.sharedRunner = undefined;
67
+ }
68
+ describe("generate timeout = 45s", () => {
69
+ afterEach(resetSharedRunner);
70
+ it("schema advertises a 45s timeout for generate", () => {
71
+ expect(function_registry_1.FUNCTION_REGISTRY.generate.timeout).toBe(45 * 1000);
72
+ expect((0, function_registry_1.getFunctionSchema)("generate").timeout).toBe(45 * 1000);
73
+ });
74
+ it("other function kinds keep their tighter timeouts", () => {
75
+ expect(function_registry_1.FUNCTION_REGISTRY.validate.timeout).toBe(5000);
76
+ expect(function_registry_1.FUNCTION_REGISTRY.meetsRequirements.timeout).toBe(3000);
77
+ });
78
+ it("sandbox setTimeout accepts delays up to 45000ms (was capped at 35000)", async () => {
79
+ const cfg = baseConfig();
80
+ const r = new MockRunner_1.MockRunner(cfg);
81
+ r.getConfig().steps.push(stepWithGenerate(r, "delayed", `async function generate(defaultPayload, sessionData) {
82
+ // Just exercise the clamp — schedule but don't await.
83
+ setTimeout(() => {}, 40000);
84
+ return { scheduled: true };
85
+ }`));
86
+ const res = await r.runGeneratePayload("delayed", {});
87
+ expect(res.success).toBe(true);
88
+ expect(res.result).toEqual({ scheduled: true });
89
+ });
90
+ it("sandbox setTimeout still rejects delays above 45000ms", async () => {
91
+ const cfg = baseConfig();
92
+ const r = new MockRunner_1.MockRunner(cfg);
93
+ r.getConfig().steps.push(stepWithGenerate(r, "too-long", `async function generate(defaultPayload, sessionData) {
94
+ setTimeout(() => {}, 60000);
95
+ return {};
96
+ }`));
97
+ const res = await r.runGeneratePayload("too-long", {});
98
+ expect(res.success).toBe(false);
99
+ expect(res.error?.message || "").toMatch(/1-45000ms/);
100
+ });
101
+ });
102
+ describe("fetch allowlist (generate-only)", () => {
103
+ let server;
104
+ let baseUrl;
105
+ beforeAll(async () => {
106
+ server = http.createServer((req, res) => {
107
+ switch (req.url) {
108
+ case "/v1/ping":
109
+ res.writeHead(200, { "content-type": "text/plain" });
110
+ res.end("pong");
111
+ return;
112
+ case "/v1/redir":
113
+ res.writeHead(302, { location: "http://127.0.0.1:1/nope" });
114
+ res.end();
115
+ return;
116
+ case "/v10/foo":
117
+ res.writeHead(200);
118
+ res.end("v10");
119
+ return;
120
+ case "/other":
121
+ res.writeHead(200);
122
+ res.end("other");
123
+ return;
124
+ default:
125
+ res.writeHead(404);
126
+ res.end();
127
+ }
128
+ });
129
+ await new Promise((r) => server.listen(0, "127.0.0.1", () => r()));
130
+ const addr = server.address();
131
+ baseUrl = `http://127.0.0.1:${addr.port}`;
132
+ });
133
+ afterAll(async () => {
134
+ await new Promise((r) => server.close(() => r()));
135
+ });
136
+ afterEach(resetSharedRunner);
137
+ it("fetch is undefined when allowlist is empty/unset", async () => {
138
+ // No initSharedRunner → default runner has no allowlist.
139
+ const cfg = baseConfig();
140
+ const r = new MockRunner_1.MockRunner(cfg);
141
+ r.getConfig().steps.push(stepWithGenerate(r, "fetch-undef", `async function generate(defaultPayload, sessionData) {
142
+ return { t: typeof fetch };
143
+ }`));
144
+ const res = await r.runGeneratePayload("fetch-undef", {});
145
+ expect(res.success).toBe(true);
146
+ expect(res.result).toEqual({ t: "undefined" });
147
+ });
148
+ it("allows fetch matching origin + path prefix", async () => {
149
+ MockRunner_1.MockRunner.initSharedRunner({
150
+ allowedFetchBaseUrls: [`${baseUrl}/v1`],
151
+ });
152
+ const cfg = baseConfig();
153
+ const r = new MockRunner_1.MockRunner(cfg);
154
+ r.getConfig().steps.push(stepWithGenerate(r, "ok", `async function generate(defaultPayload, sessionData) {
155
+ const res = await fetch("${baseUrl}/v1/ping");
156
+ const body = await res.text();
157
+ return { body };
158
+ }`));
159
+ const res = await r.runGeneratePayload("ok", {});
160
+ expect(res.success).toBe(true);
161
+ expect(res.result).toEqual({ body: "pong" });
162
+ });
163
+ it("rejects URLs with non-allowlisted path prefix", async () => {
164
+ MockRunner_1.MockRunner.initSharedRunner({
165
+ allowedFetchBaseUrls: [`${baseUrl}/v1`],
166
+ });
167
+ const cfg = baseConfig();
168
+ const r = new MockRunner_1.MockRunner(cfg);
169
+ r.getConfig().steps.push(stepWithGenerate(r, "bad-path", `async function generate(defaultPayload, sessionData) {
170
+ const res = await fetch("${baseUrl}/other");
171
+ return { body: await res.text() };
172
+ }`));
173
+ const res = await r.runGeneratePayload("bad-path", {});
174
+ expect(res.success).toBe(false);
175
+ expect(res.error?.message || "").toMatch(/fetch blocked/);
176
+ });
177
+ it("treats /v1 as a strict segment prefix (no /v10/* match)", async () => {
178
+ MockRunner_1.MockRunner.initSharedRunner({
179
+ allowedFetchBaseUrls: [`${baseUrl}/v1`],
180
+ });
181
+ const cfg = baseConfig();
182
+ const r = new MockRunner_1.MockRunner(cfg);
183
+ r.getConfig().steps.push(stepWithGenerate(r, "prefix-strict", `async function generate(defaultPayload, sessionData) {
184
+ const res = await fetch("${baseUrl}/v10/foo");
185
+ return { body: await res.text() };
186
+ }`));
187
+ const res = await r.runGeneratePayload("prefix-strict", {});
188
+ expect(res.success).toBe(false);
189
+ expect(res.error?.message || "").toMatch(/fetch blocked/);
190
+ });
191
+ it("rejects different origin even with matching path", async () => {
192
+ MockRunner_1.MockRunner.initSharedRunner({
193
+ allowedFetchBaseUrls: [`${baseUrl}/v1`],
194
+ });
195
+ const cfg = baseConfig();
196
+ const r = new MockRunner_1.MockRunner(cfg);
197
+ r.getConfig().steps.push(stepWithGenerate(r, "bad-origin", `async function generate(defaultPayload, sessionData) {
198
+ const res = await fetch("http://example.invalid/v1/ping");
199
+ return { body: await res.text() };
200
+ }`));
201
+ const res = await r.runGeneratePayload("bad-origin", {});
202
+ expect(res.success).toBe(false);
203
+ expect(res.error?.message || "").toMatch(/fetch blocked/);
204
+ });
205
+ it("blocks 3xx redirects (no allowlist bypass via Location header)", async () => {
206
+ MockRunner_1.MockRunner.initSharedRunner({
207
+ allowedFetchBaseUrls: [`${baseUrl}/v1`],
208
+ });
209
+ const cfg = baseConfig();
210
+ const r = new MockRunner_1.MockRunner(cfg);
211
+ r.getConfig().steps.push(stepWithGenerate(r, "redir", `async function generate(defaultPayload, sessionData) {
212
+ await fetch("${baseUrl}/v1/redir");
213
+ return { ok: true };
214
+ }`));
215
+ const res = await r.runGeneratePayload("redir", {});
216
+ expect(res.success).toBe(false);
217
+ // undici surfaces a TypeError when redirect:'error' encounters a 3xx
218
+ expect((res.error?.message || "").toLowerCase()).toMatch(/redirect|fetch failed/);
219
+ });
220
+ it("fetch is not injected into validate even when allowlist is configured", async () => {
221
+ MockRunner_1.MockRunner.initSharedRunner({
222
+ allowedFetchBaseUrls: [`${baseUrl}/v1`],
223
+ });
224
+ const cfg = baseConfig();
225
+ const r = new MockRunner_1.MockRunner(cfg);
226
+ const step = r.getDefaultStep("search", "validate-pure");
227
+ step.mock.inputs = {};
228
+ step.mock.validate = MockRunner_1.MockRunner.encodeBase64(`function validate(targetPayload, sessionData) {
229
+ return {
230
+ valid: typeof fetch === "undefined",
231
+ code: 200,
232
+ description: "fetch-absence probe",
233
+ };
234
+ }`);
235
+ r.getConfig().steps.push(step);
236
+ const res = await r.runValidatePayload("validate-pure", {});
237
+ expect(res.success).toBe(true);
238
+ expect(res.result?.valid).toBe(true);
239
+ });
240
+ });