@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.
@@ -655,4 +655,68 @@ describe("MockRunner", () => {
655
655
  expect(result.result.message.discountPercent).toBe(20);
656
656
  });
657
657
  });
658
+ describe("baseActionId extraction (GENERATED# prefix support)", () => {
659
+ let prefixedRunner;
660
+ beforeEach(async () => {
661
+ const baseConfig = {
662
+ meta: {
663
+ domain: "ONDC:TRV14",
664
+ version: "2.0.0",
665
+ flowId: "prefix-test",
666
+ },
667
+ transaction_data: {
668
+ transaction_id: "prefix-test-txn-id",
669
+ latest_timestamp: "1970-01-01T00:00:00.000Z",
670
+ },
671
+ steps: [],
672
+ transaction_history: [],
673
+ validationLib: "",
674
+ helperLib: "",
675
+ };
676
+ const base = new MockRunner_1.MockRunner(baseConfig, true);
677
+ base.getConfig().steps.push(base.getDefaultStep("search", "search_0"));
678
+ prefixedRunner = new MockRunner_1.MockRunner(await (0, configHelper_1.createOptimizedMockConfig)(base.getConfig()), true);
679
+ });
680
+ it("runGeneratePayload: GENERATED#1#search_0 resolves to search_0", async () => {
681
+ const result = await prefixedRunner.runGeneratePayload("GENERATED#1#search_0", {});
682
+ expect(result.success).toBe(true);
683
+ });
684
+ it("runGeneratePayload: plain search_0 still works", async () => {
685
+ const result = await prefixedRunner.runGeneratePayload("search_0", {});
686
+ expect(result.success).toBe(true);
687
+ });
688
+ it("runGeneratePayloadWithSession: GENERATED#1#search_0 resolves to search_0", async () => {
689
+ const result = await prefixedRunner.runGeneratePayloadWithSession("GENERATED#1#search_0", { transaction_id: "some-txn" });
690
+ expect(result.success).toBe(true);
691
+ });
692
+ it("runValidatePayload: GENERATED#1#search_0 resolves to search_0", async () => {
693
+ const payload = {
694
+ context: { domain: "ONDC:TRV14", action: "search" },
695
+ message: {},
696
+ };
697
+ const result = await prefixedRunner.runValidatePayload("GENERATED#1#search_0", payload);
698
+ expect(result.success).toBe(true);
699
+ });
700
+ it("runValidatePayloadWithSession: GENERATED#1#search_0 resolves to search_0", async () => {
701
+ const result = await prefixedRunner.runValidatePayloadWithSession("GENERATED#1#search_0", { context: {}, message: {} }, {});
702
+ expect(result.success).toBe(true);
703
+ });
704
+ it("runMeetRequirements: GENERATED#1#search_0 resolves to search_0", async () => {
705
+ const result = await prefixedRunner.runMeetRequirements("GENERATED#1#search_0");
706
+ expect(result.success).toBe(true);
707
+ });
708
+ it("runMeetRequirementsWithSession: GENERATED#1#search_0 resolves to search_0", async () => {
709
+ const result = await prefixedRunner.runMeetRequirementsWithSession("GENERATED#1#search_0", {});
710
+ expect(result.success).toBe(true);
711
+ });
712
+ it("unknown prefixed action ID returns failure", async () => {
713
+ const result = await prefixedRunner.runGeneratePayload("GENERATED#1#unknown_action", {});
714
+ expect(result.success).toBe(false);
715
+ expect(result.error?.message).toContain("unknown_action");
716
+ });
717
+ it("deeply prefixed ID (multiple #) uses only the last segment", async () => {
718
+ const result = await prefixedRunner.runGeneratePayload("PREFIX#ANOTHER#search_0", {});
719
+ expect(result.success).toBe(true);
720
+ });
721
+ });
658
722
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondc/automation-mock-runner",
3
- "version": "1.3.42",
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