@ondc/automation-mock-runner 1.3.43 → 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.
@@ -1,4 +1,5 @@
1
1
  import { BaseCodeRunner } from "./runners/base-runner";
2
+ import { RunnerOptions } from "./runners/runner-factory";
2
3
  import { MockPlaygroundConfigType } from "./types/mock-config";
3
4
  import { Logger } from "./utils/logger";
4
5
  import { ExecutionResult } from "./types/execution-results";
@@ -7,6 +8,14 @@ export declare class MockRunner {
7
8
  private static sharedRunner;
8
9
  logger: Logger;
9
10
  private static getSharedRunner;
11
+ /**
12
+ * Initialize (or replace) the process-wide shared runner with explicit
13
+ * options. Call this once at service boot — before constructing any
14
+ * MockRunner — to configure the fetch allowlist and worker pool settings.
15
+ *
16
+ * If a shared runner already exists it is terminated and replaced.
17
+ */
18
+ static initSharedRunner(options?: RunnerOptions): BaseCodeRunner;
10
19
  constructor(config: MockPlaygroundConfigType, skipValidation?: boolean);
11
20
  getRunnerInstance(): BaseCodeRunner;
12
21
  getConfig(): {
@@ -69,9 +78,9 @@ export declare class MockRunner {
69
78
  success: boolean;
70
79
  errors?: import("zod/v4/core").$ZodIssue[];
71
80
  };
72
- runGeneratePayload(actionId: string, inputs?: any): Promise<ExecutionResult>;
81
+ runGeneratePayload(actionId: string, inputs?: any, extraSessionData?: Record<string, any>): Promise<ExecutionResult>;
73
82
  runGeneratePayloadWithSession(actionId: string, sessionData: any): Promise<ExecutionResult>;
74
- runValidatePayload(actionId: string, targetPayload: any): Promise<ExecutionResult>;
83
+ runValidatePayload(actionId: string, targetPayload: any, extraSessionData?: Record<string, any>): Promise<ExecutionResult>;
75
84
  runValidatePayloadWithSession(actionId: string, targetPayload: any, sessionData: any): Promise<ExecutionResult>;
76
85
  runMeetRequirements(actionId: string): Promise<ExecutionResult>;
77
86
  runMeetRequirementsWithSession(actionId: string, sessionData: any): Promise<ExecutionResult>;
@@ -18,6 +18,27 @@ class MockRunner {
18
18
  }
19
19
  return MockRunner.sharedRunner;
20
20
  }
21
+ /**
22
+ * Initialize (or replace) the process-wide shared runner with explicit
23
+ * options. Call this once at service boot — before constructing any
24
+ * MockRunner — to configure the fetch allowlist and worker pool settings.
25
+ *
26
+ * If a shared runner already exists it is terminated and replaced.
27
+ */
28
+ static initSharedRunner(options = {}) {
29
+ const logger = logger_1.Logger.getInstance();
30
+ if (MockRunner.sharedRunner) {
31
+ logger.warn("Replacing existing shared runner");
32
+ try {
33
+ MockRunner.sharedRunner.terminate();
34
+ }
35
+ catch (e) {
36
+ logger.error("Failed to terminate previous shared runner", {}, e);
37
+ }
38
+ }
39
+ MockRunner.sharedRunner = runner_factory_1.RunnerFactory.createRunner(options, logger);
40
+ return MockRunner.sharedRunner;
41
+ }
21
42
  constructor(config, skipValidation = false) {
22
43
  this.logger = logger_1.Logger.getInstance();
23
44
  if (!skipValidation) {
@@ -55,7 +76,7 @@ class MockRunner {
55
76
  const res = (0, validateConfig_1.validateConfigWithErrors)(this.config);
56
77
  return res;
57
78
  }
58
- async runGeneratePayload(actionId, inputs = {}) {
79
+ async runGeneratePayload(actionId, inputs = {}, extraSessionData) {
59
80
  const executionId = this.logger.createExecutionContext(actionId);
60
81
  const startTime = Date.now();
61
82
  try {
@@ -73,6 +94,9 @@ class MockRunner {
73
94
  // Deep clone to avoid mutations
74
95
  const defaultPayload = JSON.parse(JSON.stringify(step.mock.defaultPayload));
75
96
  const sessionData = await this.getSessionDataUpToStep(index);
97
+ if (extraSessionData) {
98
+ Object.assign(sessionData, extraSessionData);
99
+ }
76
100
  // Validate inputs against schema if provided
77
101
  if (step.mock.inputs?.jsonSchema && Object.keys(inputs).length > 0) {
78
102
  // TODO: Add JSON schema validation for inputs
@@ -191,7 +215,7 @@ class MockRunner {
191
215
  };
192
216
  }
193
217
  }
194
- async runValidatePayload(actionId, targetPayload) {
218
+ async runValidatePayload(actionId, targetPayload, extraSessionData) {
195
219
  try {
196
220
  const baseActionId = MockRunner.resolveBaseActionId(actionId);
197
221
  const step = this.config.steps.find((s) => s.action_id === baseActionId);
@@ -201,6 +225,9 @@ class MockRunner {
201
225
  const index = this.config.steps.findIndex((s) => s.action_id === baseActionId);
202
226
  const schema = (0, function_registry_1.getFunctionSchema)("validate");
203
227
  const sessionData = await this.getSessionDataUpToStep(index);
228
+ if (extraSessionData) {
229
+ Object.assign(sessionData, extraSessionData);
230
+ }
204
231
  const result = await this.getRunnerInstance().execute(MockRunner.decodeBase64(step.mock.validate), schema, [targetPayload, sessionData]);
205
232
  return result;
206
233
  }
@@ -11,6 +11,7 @@ const MockRunner_1 = require("./MockRunner");
11
11
  const uuid_1 = require("uuid");
12
12
  const terser_1 = require("terser");
13
13
  const validateConfig_1 = require("./utils/validateConfig");
14
+ const helpers_1 = require("./helpers");
14
15
  function createInitialMockConfig(domain, version, flowId) {
15
16
  return {
16
17
  meta: {
@@ -32,65 +33,9 @@ function createInitialMockConfig(domain, version, flowId) {
32
33
  steps: [],
33
34
  transaction_history: [],
34
35
  validationLib: "",
35
- helperLib: MockRunner_1.MockRunner.encodeBase64(defaultHelpers),
36
+ helperLib: MockRunner_1.MockRunner.encodeBase64(helpers_1.DEFAULT_HELPER_LIB),
36
37
  };
37
38
  }
38
- const defaultHelpers = `/*
39
- Custom helper functions available in all mock generation functions.
40
- these are appended below the generate function for each step.
41
- */
42
-
43
- const createFormURL = (domain,formId, sessionData) => {
44
- const baseURL = sessionData.mockBaseUrl;
45
- const transactionId = sessionData.transactionId[0];
46
- const sessionId = sessionData.sessionId;
47
- return \`\${baseURL}/forms/\${domain}/\${formId}/?transaction_id=\${transactionId}&session_id=\${sessionId}\`;
48
- }
49
-
50
- // Generates a UUID v4
51
- function uuidv4() {
52
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
53
- const r = Math.random() * 16 | 0;
54
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
55
- return v.toString(16);
56
- });
57
- }
58
-
59
- // Generate a 6 digit string ID
60
- function generate6DigitId() {
61
- return Math.floor(100000 + Math.random() * 900000).toString();
62
- }
63
-
64
- // Returns the current ISO timestamp
65
- function currentTimestamp() {
66
- return new Date().toISOString();
67
- }
68
-
69
- // Converts ISO 8601 duration string to total seconds
70
- const isoDurToSec = (duration) => {
71
- const durRE = /P((\d+)Y)?((\d+)M)?((\d+)W)?((\d+)D)?T?((\d+)H)?((\d+)M)?((\d+)S)?/;
72
- const s = durRE.exec(duration);
73
- if (!s) return 0;
74
-
75
- return (Number(s?.[2]) || 0) * 31536000 +
76
- (Number(s?.[4]) || 0) * 2628288 +
77
- (Number(s?.[6]) || 0) * 604800 +
78
- (Number(s?.[8]) || 0) * 86400 +
79
- (Number(s?.[10]) || 0) * 3600 +
80
- (Number(s?.[12]) || 0) * 60 +
81
- (Number(s?.[14]) || 0);
82
- };
83
-
84
- const setCityFromInputs = (payload, inputs) => {
85
- if (!inputs) return "*";
86
- let version = payload.context.version || payload.context.core_version || "2.0.0";
87
- if (version.startsWith("1")) {
88
- payload.context.city = inputs.city_code ?? "*";
89
- } else {
90
- payload.context.location.city.code = inputs.city_code ?? "*";
91
- }
92
- }
93
- `;
94
39
  function convertToFlowConfig(config) {
95
40
  const flowConfig = {};
96
41
  flowConfig.id = config.meta.flowId;
@@ -172,13 +117,25 @@ function convertToFlowConfig(config) {
172
117
  if (step.mock.inputs !== undefined &&
173
118
  step.mock.inputs !== null &&
174
119
  Object.keys(step.mock.inputs).length > 0) {
175
- flowStep.input = [
176
- {
177
- name: step.mock.inputs.id,
178
- type: step.mock.inputs.id,
179
- schema: step.mock.inputs.jsonSchema,
180
- },
181
- ];
120
+ if (step.mock.inputs.id == "finvu_verification") {
121
+ flowStep.input = [
122
+ {
123
+ name: "finvu_verification",
124
+ label: "Complete Account Aggregator Verification",
125
+ type: "FINVU_REDIRECT",
126
+ payloadField: "$.context.aa_consent_verified",
127
+ },
128
+ ];
129
+ }
130
+ else {
131
+ flowStep.input = [
132
+ {
133
+ name: step.mock.inputs.id,
134
+ type: step.mock.inputs.id,
135
+ schema: step.mock.inputs.jsonSchema,
136
+ },
137
+ ];
138
+ }
182
139
  }
183
140
  if (step.mock.inputs?.oldInputs) {
184
141
  flowStep.input = step.mock.inputs.oldInputs;
@@ -26,7 +26,7 @@ exports.FUNCTION_REGISTRY = {
26
26
  description: "The generated payload object to be sent in the API request",
27
27
  },
28
28
  description: "Generates the mock payload for an API call",
29
- timeout: 35 * 1000,
29
+ timeout: 45 * 1000,
30
30
  defaultBody: ` return defaultPayload;`,
31
31
  template: (body) => `/**
32
32
  * Generates the mock payload for an API call in the transaction flow.
@@ -0,0 +1,13 @@
1
+ export function getSubscriberUrl(sessionData: any, type: any): any;
2
+ export function uuidv4(): string;
3
+ export function generate6DigitId(): string;
4
+ export function currentTimestamp(): string;
5
+ export function isoDurToSec(duration: any): number;
6
+ export function setCityFromInputs(payload: any, inputs: any): "*" | undefined;
7
+ export function createFormURL(domain: any, formId: any, sessionData: any): string;
8
+ export function generateConsentHandler(sessionData: any, { custId, templateName, consentDescription, redirectUrl, }: {
9
+ custId: any;
10
+ templateName?: string | undefined;
11
+ consentDescription?: string | undefined;
12
+ redirectUrl?: string | undefined;
13
+ }): Promise<any>;
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /*
3
+ * Default helpers available to every `generate()` in a mock step.
4
+ *
5
+ * Authoring rules (enforced by how this file is consumed, not by tooling):
6
+ * 1. Use `function` declarations only. Arrow `const x = () => {}` stringifies
7
+ * as an expression — it won't hoist or concat cleanly when each function
8
+ * is `.toString()`-ed into the helper bundle.
9
+ * 2. No `require` / `import` inside function bodies. The VM sandbox has no
10
+ * module system. Only sandbox-whitelisted globals (Math, Date, JSON, …)
11
+ * and sibling helpers are in scope.
12
+ * 3. Cross-helper calls are fine — every function declaration lands in the
13
+ * same flat script after assembly, and declarations hoist.
14
+ * 4. Put docs INSIDE the function body (first statement). Leading comments
15
+ * above a declaration are dropped by `fn.toString()`, so they never
16
+ * reach the assembled bundle.
17
+ * 5. If the helper needs request-scope data, take `sessionData` as an
18
+ * explicit parameter. Free-variable references to `sessionData` do NOT
19
+ * resolve at runtime — helpers are declared at script scope, `sessionData`
20
+ * is only a parameter of `generate()`.
21
+ *
22
+ * The `module.exports = {...}` line at the bottom is only used by Node
23
+ * (index.ts and the Jest tests). `fn.toString()` never includes it, so it
24
+ * doesn't leak into the sandbox string.
25
+ */
26
+ function getSubscriberUrl(sessionData, type) {
27
+ // Resolve the BPP or BAP subscriber URL from session data.
28
+ // Usage: getSubscriberUrl(sessionData, "bpp")
29
+ if (type === "bpp") {
30
+ return sessionData.bppUri;
31
+ }
32
+ else {
33
+ return sessionData.bapUri;
34
+ }
35
+ }
36
+ function uuidv4() {
37
+ // Generates a UUID v4 (RFC 4122, random-based).
38
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
39
+ const r = (Math.random() * 16) | 0;
40
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
41
+ return v.toString(16);
42
+ });
43
+ }
44
+ function generate6DigitId() {
45
+ // Generate a 6-digit numeric string ID in [100000, 999999].
46
+ return Math.floor(100000 + Math.random() * 900000).toString();
47
+ }
48
+ function currentTimestamp() {
49
+ // Returns the current ISO-8601 UTC timestamp (e.g. "2026-04-23T12:34:56.789Z").
50
+ return new Date().toISOString();
51
+ }
52
+ function isoDurToSec(duration) {
53
+ /*
54
+ * Convert an ISO 8601 duration string (e.g. "PT1H30M", "P2DT3H") to total seconds.
55
+ * Returns 0 for unparseable input.
56
+ * Approximations used: 1 week = 7 days, 1 month ≈ 30.42 days (2628288 sec),
57
+ * 1 year = 365 days. Not calendar-exact.
58
+ */
59
+ const durRE = /P((\d+)Y)?((\d+)M)?((\d+)W)?((\d+)D)?T?((\d+)H)?((\d+)M)?((\d+)S)?/;
60
+ const s = durRE.exec(duration);
61
+ if (!s)
62
+ return 0;
63
+ return ((Number(s?.[2]) || 0) * 31536000 +
64
+ (Number(s?.[4]) || 0) * 2628288 +
65
+ (Number(s?.[6]) || 0) * 604800 +
66
+ (Number(s?.[8]) || 0) * 86400 +
67
+ (Number(s?.[10]) || 0) * 3600 +
68
+ (Number(s?.[12]) || 0) * 60 +
69
+ (Number(s?.[14]) || 0));
70
+ }
71
+ function setCityFromInputs(payload, inputs) {
72
+ /*
73
+ * Mutates `payload.context` in place to set the city code from `inputs.city_code`.
74
+ * Version-aware: ONDC v1.x uses flat `context.city`, v2.x uses nested
75
+ * `context.location.city.code`. Falls back to "*" when city_code is missing.
76
+ * No-op when `inputs` is falsy.
77
+ */
78
+ if (!inputs)
79
+ return "*";
80
+ const version = payload.context.version || payload.context.core_version || "2.0.0";
81
+ if (version.startsWith("1")) {
82
+ payload.context.city = inputs.city_code ?? "*";
83
+ }
84
+ else {
85
+ payload.context.location.city.code = inputs.city_code ?? "*";
86
+ }
87
+ }
88
+ function createFormURL(domain, formId, sessionData) {
89
+ /*
90
+ * Build a form submission URL from session data.
91
+ * Reads sessionData.mockBaseUrl, sessionData.transactionId[0], sessionData.sessionId.
92
+ * Returns: `${baseURL}/forms/${domain}/${formId}/?transaction_id=...&session_id=...`
93
+ */
94
+ const baseURL = sessionData.mockBaseUrl;
95
+ const transactionId = sessionData.transactionId[0];
96
+ const sessionId = sessionData.sessionId;
97
+ return `${baseURL}/forms/${domain}/${formId}/?transaction_id=${transactionId}&session_id=${sessionId}`;
98
+ }
99
+ async function generateConsentHandler(sessionData, { custId, templateName = "FINVUDEMO_TESTING", consentDescription = "Gold Loan Account Aggregator Consent", redirectUrl = "https://google.co.in", }) {
100
+ /*
101
+ * Generate a consent handler from the Finvu AA Service.
102
+ * Reads the service base URL from `sessionData.finvuUrl` — the installing
103
+ * service MUST include that origin in
104
+ * MockRunner.initSharedRunner({ allowedFetchBaseUrls: [...] })
105
+ * otherwise the sandboxed fetch will be blocked.
106
+ *
107
+ * Times out after 10s via AbortController.
108
+ *
109
+ * @param {Object} sessionData session data; sessionData.finvuUrl is required
110
+ * @param {Object} params
111
+ * @param {string} params.custId customer ID (required)
112
+ * @param {string} [params.templateName]
113
+ * @param {string} [params.consentDescription]
114
+ * @param {string} [params.redirectUrl]
115
+ * @returns {Promise<string>} consentHandler
116
+ */
117
+ if (!custId) {
118
+ throw new Error("custId is required");
119
+ }
120
+ const baseUrl = sessionData && sessionData.finvuUrl;
121
+ if (!baseUrl) {
122
+ throw new Error("sessionData.finvuUrl is required");
123
+ }
124
+ const url = `${baseUrl}/finvu-aa/consent/generate`;
125
+ const payload = {
126
+ custId,
127
+ templateName,
128
+ consentDescription,
129
+ redirectUrl,
130
+ };
131
+ console.log("Calling Finvu AA Service:", url);
132
+ console.log("Consent request payload:", payload);
133
+ const controller = new AbortController();
134
+ const timeout = setTimeout(() => controller.abort(), 10000);
135
+ try {
136
+ const res = await fetch(url, {
137
+ method: "POST",
138
+ headers: {
139
+ "Content-Type": "application/json",
140
+ },
141
+ body: JSON.stringify(payload),
142
+ signal: controller.signal,
143
+ });
144
+ if (!res.ok) {
145
+ const text = await res.text();
146
+ throw new Error(`Request failed: ${res.status} ${text}`);
147
+ }
148
+ const data = await res.json();
149
+ if (!data || !data.consentHandler) {
150
+ throw new Error("Invalid response: consentHandler missing");
151
+ }
152
+ return data.consentHandler;
153
+ }
154
+ catch (err) {
155
+ if (err && err.name === "AbortError") {
156
+ throw new Error("Request timed out after 10 seconds");
157
+ }
158
+ throw err;
159
+ }
160
+ finally {
161
+ clearTimeout(timeout);
162
+ }
163
+ }
164
+ module.exports = {
165
+ getSubscriberUrl,
166
+ uuidv4,
167
+ generate6DigitId,
168
+ currentTimestamp,
169
+ isoDurToSec,
170
+ setCityFromInputs,
171
+ createFormURL,
172
+ generateConsentHandler,
173
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Unit tests for the default helper functions + bundle integrity.
3
+ *
4
+ * Unit tests import the functions as a normal CJS module and call them.
5
+ * The bundle test loads DEFAULT_HELPER_LIB into a fresh vm context — this
6
+ * simulates how the string is actually used in the worker sandbox and
7
+ * catches any stringification / hoisting / free-variable regressions.
8
+ */
9
+ export {};
@@ -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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondc/automation-mock-runner",
3
- "version": "1.3.43",
3
+ "version": "1.3.44",
4
4
  "description": "A TypeScript library for ONDC automation mock runner",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -45,7 +45,7 @@
45
45
  "author": "ONDC Development Team <dev@ondc.org>",
46
46
  "license": "ISC",
47
47
  "engines": {
48
- "node": ">=16.0.0"
48
+ "node": ">=18.0.0"
49
49
  },
50
50
  "repository": {
51
51
  "type": "git",
@@ -1,8 +1,73 @@
1
- const { parentPort } = require("worker_threads");
1
+ const { parentPort, workerData } = require("worker_threads");
2
2
  const vm = require("vm");
3
3
 
4
+ const ALLOWED_FETCH_BASE_URLS = Array.isArray(workerData?.allowedFetchBaseUrls)
5
+ ? workerData.allowedFetchBaseUrls
6
+ : [];
7
+
8
+ // Parse + normalize allowlist entries once per worker.
9
+ // Each entry contributes { origin, pathname } where pathname has no trailing
10
+ // slash; matching requires request.origin === entry.origin AND the request
11
+ // pathname is a strict segment-prefix of entry.pathname (so `/v1` matches
12
+ // `/v1` and `/v1/foo` but NOT `/v10/foo`).
13
+ const PARSED_ALLOWLIST = ALLOWED_FETCH_BASE_URLS.map((raw) => {
14
+ try {
15
+ const u = new URL(raw);
16
+ let pathname = u.pathname;
17
+ if (pathname.endsWith("/") && pathname !== "/") {
18
+ pathname = pathname.slice(0, -1);
19
+ }
20
+ return { origin: u.origin, pathname };
21
+ } catch {
22
+ return null;
23
+ }
24
+ }).filter(Boolean);
25
+
26
+ function isFetchAllowed(requestUrl) {
27
+ let parsed;
28
+ try {
29
+ parsed = new URL(requestUrl);
30
+ } catch {
31
+ return false;
32
+ }
33
+ let reqPath = parsed.pathname;
34
+ if (reqPath.endsWith("/") && reqPath !== "/") {
35
+ reqPath = reqPath.slice(0, -1);
36
+ }
37
+ for (const entry of PARSED_ALLOWLIST) {
38
+ if (parsed.origin !== entry.origin) continue;
39
+ if (entry.pathname === "" || entry.pathname === "/") return true;
40
+ if (reqPath === entry.pathname) return true;
41
+ if (reqPath.startsWith(entry.pathname + "/")) return true;
42
+ }
43
+ return false;
44
+ }
45
+
46
+ function makeScopedFetch() {
47
+ if (typeof globalThis.fetch !== "function") {
48
+ return undefined;
49
+ }
50
+ if (PARSED_ALLOWLIST.length === 0) {
51
+ return undefined;
52
+ }
53
+ return async function scopedFetch(input, init) {
54
+ const requestUrl =
55
+ typeof input === "string"
56
+ ? input
57
+ : input && typeof input.url === "string"
58
+ ? input.url
59
+ : String(input);
60
+ if (!isFetchAllowed(requestUrl)) {
61
+ throw new Error(
62
+ `fetch blocked: ${requestUrl} is not in the configured allowlist`,
63
+ );
64
+ }
65
+ return globalThis.fetch(input, { ...(init || {}), redirect: "error" });
66
+ };
67
+ }
68
+
4
69
  // Create a secure sandbox context
5
- function createSandbox() {
70
+ function createSandbox(functionName) {
6
71
  const logs = [];
7
72
 
8
73
  // Safe console implementation that captures logs
@@ -108,12 +173,17 @@ function createSandbox() {
108
173
  decodeURIComponent,
109
174
  // Utility functions for ONDC operations
110
175
  setTimeout: (fn, delay) => {
111
- if (delay < 1 || delay > 35 * 1000) {
112
- throw new Error("Timeout must be between 1-35000ms");
176
+ if (delay < 1 || delay > 45 * 1000) {
177
+ throw new Error("Timeout must be between 1-45000ms");
113
178
  }
114
179
  return setTimeout(fn, delay);
115
180
  },
116
181
  clearTimeout,
182
+ // AbortController is a pure control-flow primitive with no I/O of its
183
+ // own — safe to expose unconditionally. Needed by helpers that pair
184
+ // `fetch` with a timeout (see generateConsentHandler).
185
+ AbortController,
186
+ AbortSignal,
117
187
  // Blocked globals
118
188
  require: undefined,
119
189
  process: undefined,
@@ -128,6 +198,28 @@ function createSandbox() {
128
198
  Function: undefined,
129
199
  };
130
200
 
201
+ // Only `generate` gets outbound HTTP — validate/meetsRequirements/getSave
202
+ // stay pure. Fetch itself is still gated by the allowlist inside the wrapper.
203
+ if (functionName === "generate") {
204
+ const scopedFetch = makeScopedFetch();
205
+ if (scopedFetch) {
206
+ sandbox.fetch = scopedFetch;
207
+ if (typeof globalThis.URL === "function") sandbox.URL = globalThis.URL;
208
+ if (typeof globalThis.URLSearchParams === "function") {
209
+ sandbox.URLSearchParams = globalThis.URLSearchParams;
210
+ }
211
+ if (typeof globalThis.Headers === "function") {
212
+ sandbox.Headers = globalThis.Headers;
213
+ }
214
+ if (typeof globalThis.Request === "function") {
215
+ sandbox.Request = globalThis.Request;
216
+ }
217
+ if (typeof globalThis.Response === "function") {
218
+ sandbox.Response = globalThis.Response;
219
+ }
220
+ }
221
+ }
222
+
131
223
  return { sandbox, logs };
132
224
  }
133
225
 
@@ -138,7 +230,7 @@ parentPort?.on("message", async (message) => {
138
230
 
139
231
  try {
140
232
  // Create fresh sandbox for each execution
141
- const { sandbox, logs } = createSandbox();
233
+ const { sandbox, logs } = createSandbox(functionName);
142
234
 
143
235
  // Create VM context with timeout
144
236
  const context = vm.createContext(sandbox);
@@ -151,7 +243,7 @@ parentPort?.on("message", async (message) => {
151
243
 
152
244
  // Execute the script
153
245
  script.runInContext(context, {
154
- timeout: timeout || 35000,
246
+ timeout: timeout || 45000,
155
247
  breakOnSigint: true,
156
248
  });
157
249