@ondc/automation-mock-runner 1.3.45 → 1.3.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,48 +1,23 @@
1
1
  # @ondc/automation-mock-runner
2
2
 
3
- A robust TypeScript library designed for testing and validating ONDC (Open Network for Digital Commerce) transaction flows. This tool helps developers build reliable ONDC integrations by providing a comprehensive framework for generating, validating, and testing API payloads across different transaction scenarios.
3
+ A TypeScript library for driving ONDC (Open Network for Digital Commerce) transaction flows end-to-end. It turns a **base64-encoded, sandboxed function config** into an executable multi-step flow, handling payload generation, response validation, session state, and (optionally) outbound HTTP in either Node (worker_threads) or the browser (Web Workers).
4
4
 
5
- ## What is this?
5
+ ## What it does
6
6
 
7
- When building applications that integrate with the ONDC network, you need to handle complex multi-step transaction flows where each API call depends on data from previous steps. This library provides a structured way to:
8
-
9
- - **Generate realistic test payloads** for ONDC APIs (search, select, init, confirm, etc.)
10
- - **Validate incoming requests** against your business logic with custom validation functions
11
- - **Check prerequisites** before proceeding with each transaction step
12
- - **Maintain session state** across the entire transaction flow
13
- - **Execute code securely** using sandboxed Worker Threads with base64-encoded functions
14
-
15
- The core concept is simple: define your transaction flow with base64-encoded functions, then let the runner handle payload generation, validation, and state management automatically in a secure environment.
7
+ - **Generate payloads** for every step of a flow, with `context` (domain, version, ids, timestamps, bap/bpp) produced automatically.
8
+ - **Validate responses** against custom per-step logic.
9
+ - **Check prerequisites** before a step runs.
10
+ - **Carry session state** across steps via JSONPath extraction of prior payloads.
11
+ - **Sandbox every function** so user-authored JS runs with a whitelisted set of globals and per-function timeouts, and cannot touch the host filesystem, network, or module system — except where the installing service has explicitly allowlisted outbound URLs for `generate`.
16
12
 
17
13
  ## Key Features
18
14
 
19
- ### 🔄 Transaction Flow Management
20
-
21
- - Define multi-step ONDC transaction flows with dependencies between steps
22
- - Automatic context generation with proper ONDC headers and metadata
23
- - Session data persistence across transaction steps
24
-
25
- ### 🔒 Secure Code Execution
26
-
27
- - **Base64-encoded functions** for secure storage and transmission
28
- - **Sandboxed Worker Threads** with isolated VM contexts
29
- - **Complete function declarations** required (not just function bodies)
30
- - Built-in timeout protection and error isolation
31
- - Memory limits and resource monitoring
32
-
33
- ### ✅ Schema Validation
34
-
35
- - Zod-based configuration validation with base64 string validation
36
- - Runtime type checking for all inputs and outputs
37
- - Detailed error reporting with line-by-line feedback
38
- - JSON Schema validation for user inputs
39
-
40
- ### 🎯 ONDC-Specific Features
41
-
42
- - Built-in support for BAP (Buyer App) and BPP (Seller App) roles
43
- - Automatic message ID correlation for request-response pairs
44
- - Version-aware context generation (supports ONDC v1.x and v2.x)
45
- - Domain-specific helper utilities
15
+ - 🔄 Multi-step flow management with automatic `context` building and request/response correlation via `responseFor`.
16
+ - 🔒 Worker-thread (Node) / Web Worker (browser) sandbox. Whitelisted globals, per-function timeouts, workers recycled after 100 executions or 10 min.
17
+ - 🌐 Opt-in outbound `fetch` from `generate` with a per-installer origin+path allowlist. Redirects blocked (`redirect: "error"`).
18
+ - 📚 Built-in **default helper library** (`uuidv4`, `currentTimestamp`, `isoDurToSec`, `setCityFromInputs`, `createFormURL`, `generate6DigitId`, `getSubscriberUrl`, `generateConsentHandler`) prepended to every `generate`.
19
+ - Zod-based config validation, JSON Schema for user inputs.
20
+ - 🎯 ONDC-aware: version-aware context (v1.x flat `city`, v2.x nested `location.city.code`), BAP/BPP roles, form steps (`dynamic_form`, `html_form`, `HTML_FORM_MULTI`), dynamic action IDs (`GENERATED#n#action_id`).
46
21
 
47
22
  ## Installation
48
23
 
@@ -50,125 +25,89 @@ The core concept is simple: define your transaction flow with base64-encoded fun
50
25
  npm install @ondc/automation-mock-runner
51
26
  ```
52
27
 
53
- ## Quick Start
28
+ **Requires Node ≥ 18** (the sandbox uses native `fetch` and `AbortController`).
54
29
 
55
- Here's how to set up a basic ONDC search flow with base64-encoded functions:
30
+ ## Quick Start
56
31
 
57
32
  ```typescript
58
- import { MockRunner } from "@ondc/automation-mock-runner";
59
- import { MockPlaygroundConfigType } from "@ondc/automation-mock-runner";
60
-
61
- // Helper function to encode functions as base64
62
- function encodeFunction(functionCode: string): string {
63
- return MockRunner.encodeBase64(functionCode);
64
- }
33
+ import {
34
+ MockRunner,
35
+ createInitialMockConfig,
36
+ } from "@ondc/automation-mock-runner";
37
+
38
+ // 1. Boot the shared runner once at service startup.
39
+ // Only needed if any `generate` function calls fetch().
40
+ MockRunner.initSharedRunner({
41
+ allowedFetchBaseUrls: ["https://dev-automation.ondc.org/finvu"],
42
+ });
65
43
 
66
- // Define your transaction configuration
67
- const config: MockPlaygroundConfigType = {
68
- meta: {
69
- domain: "ONDC:RET11",
70
- version: "1.2.0",
71
- flowId: "search-select-init-confirm",
72
- },
73
- transaction_data: {
74
- transaction_id: "550e8400-e29b-41d4-a716-446655440000",
75
- latest_timestamp: new Date().toISOString(),
76
- bap_id: "buyer-app.example.com",
77
- bap_uri: "https://buyer-app.example.com",
78
- bpp_id: "seller-app.example.com",
79
- bpp_uri: "https://seller-app.example.com",
80
- },
81
- steps: [
82
- {
83
- api: "search",
84
- action_id: "search_001",
85
- owner: "BAP",
86
- responseFor: null,
87
- unsolicited: false,
88
- description: "Search for products in electronics category",
89
- mock: {
90
- generate: encodeFunction(`
91
- async function generate(defaultPayload, sessionData) {
92
- // Add search intent to the payload
93
- defaultPayload.message = {
94
- intent: {
95
- category: { descriptor: { name: "Electronics" } },
96
- location: { country: { code: "IND" }, city: { code: "std:080" } }
97
- }
98
- };
99
- return defaultPayload;
100
- }
101
- `),
102
- validate: encodeFunction(`
103
- function validate(targetPayload, sessionData) {
104
- if (!targetPayload.message?.catalog?.providers?.length) {
105
- return { valid: false, code: 400, description: "No providers found" };
106
- }
107
- return { valid: true, code: 200, description: "Valid catalog response" };
108
- }
109
- `),
110
- requirements: encodeFunction(`
111
- function meetsRequirements(sessionData) {
112
- return { valid: true, code: 200, description: "Ready to search" };
113
- }
114
- `),
115
- defaultPayload: { context: {}, message: {} },
116
- saveData: {
117
- providers: "$.message.catalog.providers",
118
- },
119
- inputs: {
120
- id: "search_inputs",
121
- jsonSchema: {
122
- type: "object",
123
- properties: {
124
- category: { type: "string", default: "Electronics" },
125
- },
44
+ // 2. Scaffold a config (auto-fills `helperLib` with DEFAULT_HELPER_LIB).
45
+ const config = createInitialMockConfig("ONDC:RET11", "2.0.0", "search-flow");
46
+
47
+ // 3. Add a step. Every helper (uuidv4, currentTimestamp, setCityFromInputs, …)
48
+ // is already in scope inside `generate`.
49
+ config.steps.push({
50
+ api: "search",
51
+ action_id: "search_0",
52
+ owner: "BAP",
53
+ responseFor: null,
54
+ unsolicited: false,
55
+ description: "Search for electronics",
56
+ mock: {
57
+ generate: MockRunner.encodeBase64(`
58
+ async function generate(defaultPayload, sessionData) {
59
+ setCityFromInputs(defaultPayload, sessionData.user_inputs);
60
+ defaultPayload.message = {
61
+ intent: {
62
+ category: { descriptor: { name: "Electronics" } },
126
63
  },
127
- },
64
+ };
65
+ return defaultPayload;
66
+ }
67
+ `),
68
+ validate: MockRunner.encodeBase64(`
69
+ function validate(targetPayload, sessionData) {
70
+ if (!targetPayload.message?.catalog?.providers?.length) {
71
+ return { valid: false, code: 400, description: "No providers" };
72
+ }
73
+ return { valid: true, code: 200, description: "ok" };
74
+ }
75
+ `),
76
+ requirements: MockRunner.encodeBase64(`
77
+ function meetsRequirements(sessionData) {
78
+ return { valid: true, code: 200, description: "ready" };
79
+ }
80
+ `),
81
+ defaultPayload: { context: {}, message: {} },
82
+ saveData: { providers: "$.message.catalog.providers" },
83
+ inputs: {
84
+ id: "search_inputs",
85
+ jsonSchema: {
86
+ type: "object",
87
+ properties: { city_code: { type: "string" } },
88
+ required: ["city_code"],
128
89
  },
129
90
  },
130
- ],
131
- transaction_history: [],
132
- validationLib: encodeFunction(`
133
- // Shared validation utilities
134
- function validateONDCContext(context) {
135
- return context && context.domain && context.action && context.message_id;
136
- }
137
- `),
138
- helperLib: encodeFunction(`
139
- // Shared helper functions
140
- function generateMessageId() {
141
- return crypto.randomUUID();
142
- }
143
- `),
144
- };
145
-
146
- // Initialize the runner
147
- const runner = new MockRunner(config);
148
-
149
- // Generate a search payload
150
- const searchResult = await runner.runGeneratePayload("search_001", {
151
- category: "Electronics",
91
+ },
152
92
  });
153
- console.log("Generated search payload:", searchResult.result);
154
93
 
155
- // Validate a response
156
- const validationResult = await runner.runValidatePayload(
157
- "search_001",
158
- responsePayload,
159
- );
160
- console.log("Validation passed:", validationResult.success);
94
+ // 4. Run.
95
+ const runner = new MockRunner(config);
96
+ const out = await runner.runGeneratePayload("search_0", {
97
+ city_code: "std:080",
98
+ });
99
+ console.log(out.result);
161
100
  ```
162
101
 
163
102
  ## Configuration Structure
164
103
 
165
- ### Transaction Metadata
104
+ ### Meta
166
105
 
167
106
  ```typescript
168
107
  meta: {
169
- domain: string, // ONDC domain (retail, mobility, etc.)
170
- version: string, // ONDC version (1.2.0, 2.0.0, etc.)
171
- flowId: string // Unique identifier for this flow
108
+ domain: string; // e.g. "ONDC:RET11"
109
+ version: string; // e.g. "1.2.0" or "2.0.0" — drives context shape
110
+ flowId: string;
172
111
  }
173
112
  ```
174
113
 
@@ -176,360 +115,296 @@ meta: {
176
115
 
177
116
  ```typescript
178
117
  transaction_data: {
179
- transaction_id: string, // UUID for this transaction
180
- latest_timestamp: string, // ISO timestamp
181
- bap_id?: string, // Buyer app ID
182
- bap_uri?: string, // Buyer app URI
183
- bpp_id?: string, // Seller app ID
184
- bpp_uri?: string // Seller app URI
118
+ transaction_id: string;
119
+ latest_timestamp: string;
120
+ bap_id?: string;
121
+ bap_uri?: string;
122
+ bpp_id?: string;
123
+ bpp_uri?: string;
185
124
  }
186
125
  ```
187
126
 
188
- ### Action Steps
189
-
190
- Each step represents one API call in your transaction flow:
127
+ ### Action Step
191
128
 
192
129
  ```typescript
193
130
  {
194
- api: "search" | "select" | "init" | "confirm" | "on_search" | "on_select" | ...,
195
- action_id: string, // Unique ID for this step
196
- owner: "BAP" | "BPP", // Who initiates this call
197
- responseFor: string | null, // If this responds to another action
198
- unsolicited: boolean, // Whether this is an unsolicited call
199
- description: string, // Human-readable description
200
- mock: {
201
- generate: string, // Base64-encoded complete function for payload generation
202
- validate: string, // Base64-encoded complete function for response validation
203
- requirements: string, // Base64-encoded complete function for prerequisite checks
204
- defaultPayload: object, // Base payload structure
205
- saveData: object, // JSONPath expressions to save data
206
- inputs: object // Input schema for user data
207
- }
131
+ api: "search" | "select" | "init" | "confirm"
132
+ | "on_search" | "on_select" | /* … */
133
+ | "dynamic_form" | "html_form" | "HTML_FORM_MULTI",
134
+ action_id: string, // unique within flow
135
+ owner: "BAP" | "BPP",
136
+ responseFor: string | null, // pair this step with a request action_id
137
+ unsolicited: boolean,
138
+ description: string,
139
+ repeatCount?: number,
140
+ force_proceed?: boolean, // skip the "waiting for input" gate; see Form Steps
141
+ mock: {
142
+ generate: string, // base64 function
143
+ validate: string, // base64 function
144
+ requirements: string, // base64 function
145
+ defaultPayload: object,
146
+ saveData: Record<string, string>, // JSONPath map; supports APPEND# / EVAL# prefixes
147
+ inputs: object | {},
148
+ formHtml?: string, // base64 HTML for form steps
149
+ },
208
150
  }
209
151
  ```
210
152
 
211
- ## 🔑 Base64 Function Requirements
212
-
213
- **IMPORTANT**: All mock functions must be:
153
+ ## Base64 Function Requirements
214
154
 
215
- 1. **Complete function declarations** with proper function names:
216
- - `generate` functions: `async function generate(defaultPayload, sessionData) { ... }`
217
- - `validate` functions: `function validate(targetPayload, sessionData) { ... }`
218
- - `requirements` functions: `function meetsRequirements(sessionData) { ... }`
155
+ All three user functions must be **complete declarations**:
219
156
 
220
- 2. **Base64 encoded** using `MockRunner.encodeBase64()` utility
157
+ ```js
158
+ async function generate(defaultPayload, sessionData) {
159
+ /* … */ return defaultPayload;
160
+ }
161
+ function validate(targetPayload, sessionData) {
162
+ /* … */ return { valid, code, description };
163
+ }
164
+ function meetsRequirements(sessionData) {
165
+ /* … */ return { valid, code, description };
166
+ }
167
+ ```
221
168
 
222
- 3. **Properly formatted** with return statements and error handling
169
+ Encode with `MockRunner.encodeBase64(src)`. The runner decodes, prepends `DEFAULT_HELPER_LIB` (for `generate`), and executes inside the sandbox.
223
170
 
224
- ### Function Signatures
171
+ ## API Reference
225
172
 
226
- Each function type has a specific signature that must be followed:
173
+ ### `MockRunner.initSharedRunner(options?)`
227
174
 
228
- #### Generate Functions
175
+ Static. Configure the process-wide shared runner at boot. Replaces any existing runner (terminates the old one). Call **once before** constructing any `MockRunner`.
229
176
 
230
177
  ```typescript
231
- async function generate(defaultPayload: any, sessionData: any): Promise<any> {
232
- // Parameters:
233
- // - defaultPayload: Base payload with context already populated
234
- // - sessionData: Contains user_inputs and data from previous steps
235
-
236
- // Must return the complete payload to be sent
237
- return defaultPayload;
238
- }
178
+ MockRunner.initSharedRunner({
179
+ allowedFetchBaseUrls: [
180
+ "https://aa.example.com/finvu-aa",
181
+ "https://api.example.com/v1",
182
+ ],
183
+ });
239
184
  ```
240
185
 
241
- #### Validate Functions
186
+ Empty / omitted `allowedFetchBaseUrls` means `fetch` is not injected into the sandbox at all.
187
+
188
+ ### `new MockRunner(config, skipValidation?)`
189
+
190
+ Validates the config (Zod) on construction unless `skipValidation: true`.
191
+
192
+ ### `runGeneratePayload(actionId, inputs?, extraSessionData?)`
242
193
 
243
194
  ```typescript
244
- function validate(targetPayload: any, sessionData: any): ValidationResult {
245
- // Parameters:
246
- // - targetPayload: The incoming payload to validate
247
- // - sessionData: Data from previous steps
248
-
249
- // Must return validation result object
250
- return {
251
- valid: true,
252
- code: 200,
253
- description: "Validation passed",
254
- };
255
- }
195
+ await runner.runGeneratePayload(
196
+ "search_0",
197
+ { city_code: "std:080" }, // sessionData.user_inputs
198
+ { finvuUrl: "https://aa.example.com" }, // shallow-merged into sessionData
199
+ );
256
200
  ```
257
201
 
258
- #### Requirements Functions
202
+ ### `runValidatePayload(actionId, targetPayload, extraSessionData?)`
259
203
 
260
204
  ```typescript
261
- function meetsRequirements(sessionData: any): RequirementResult {
262
- // Parameters:
263
- // - sessionData: Data from previous steps
264
-
265
- // Must return requirement check result
266
- return {
267
- valid: true,
268
- code: 200,
269
- description: "Requirements met",
270
- };
271
- }
205
+ await runner.runValidatePayload("on_search_0", incomingPayload, {
206
+ finvuUrl: "https://aa.example.com",
207
+ });
272
208
  ```
273
209
 
274
- ### Example Function Creation:
210
+ ### `runMeetRequirements(actionId)`
275
211
 
276
212
  ```typescript
277
- // Create a complete function
278
- const generateFunction = `
279
- async function generate(defaultPayload, sessionData) {
280
- // Your logic here
281
- defaultPayload.message = {
282
- intent: { category: { descriptor: { name: "Electronics" } } }
283
- };
284
- return defaultPayload;
285
- }
286
- `;
213
+ await runner.runMeetRequirements("select_0");
214
+ ```
287
215
 
288
- // Encode it as base64
289
- const encodedFunction = MockRunner.encodeBase64(generateFunction);
216
+ ### With-session variants
290
217
 
291
- // Use in configuration
292
- const step = {
293
- // ...other properties...
294
- mock: {
295
- generate: encodedFunction,
296
- // ...other mock properties...
297
- },
298
- };
299
- ```
218
+ Skip the history-based session build and use a caller-supplied object:
300
219
 
301
- ## Advanced Usage
220
+ - `runGeneratePayloadWithSession(actionId, sessionData)`
221
+ - `runValidatePayloadWithSession(actionId, targetPayload, sessionData)`
222
+ - `runMeetRequirementsWithSession(actionId, sessionData)`
302
223
 
303
- ### Chaining Transaction Steps
224
+ ### `getDefaultStep(api, actionId, formType?)`
304
225
 
305
- ```typescript
306
- const steps = [
307
- {
308
- api: "search",
309
- action_id: "search_001",
310
- // ... search configuration
311
- mock: {
312
- saveData: {
313
- selectedProvider: "$.message.catalog.providers[0]",
314
- },
315
- // ...
316
- },
317
- },
318
- {
319
- api: "select",
320
- action_id: "select_001",
321
- // ... select configuration
322
- mock: {
323
- generate: `
324
- // Use data from previous search step
325
- const provider = sessionData.selectedProvider;
326
- defaultPayload.message = {
327
- order: {
328
- provider: { id: provider.id },
329
- items: [{ id: provider.items[0].id, quantity: { count: 1 } }]
330
- }
331
- };
332
- return defaultPayload;
333
- `,
334
- // ...
335
- },
336
- },
337
- ];
338
- ```
226
+ Returns a scaffolded step with template functions already base64-encoded. Pass `formType: "dynamic_form" | "html_form"` for form scaffolds.
339
227
 
340
- ### Custom Validation Logic
228
+ ### `validateConfig()`
341
229
 
342
- ```typescript
343
- // Create complete validation function
344
- const validateFunction = `
345
- function validate(targetPayload, sessionData) {
346
- // Check if order total matches expected amount
347
- const expectedTotal = sessionData.calculatedTotal;
348
- const actualTotal = targetPayload.message.order.quote.total;
349
-
350
- if (Math.abs(expectedTotal - actualTotal) > 0.01) {
351
- return {
352
- valid: false,
353
- code: 400,
354
- description: \`Total mismatch: expected \${expectedTotal}, got \${actualTotal}\`
355
- };
356
- }
357
-
358
- return { valid: true, code: 200, description: "Order total validated" };
359
- }
360
- `;
230
+ Re-validates the stored config; returns `{ success, errors? }`.
361
231
 
362
- // Encode for use in configuration
363
- const encodedValidateFunction = MockRunner.encodeBase64(validateFunction);
364
- ```
232
+ ### Static utilities
233
+
234
+ - `MockRunner.encodeBase64(src)` / `decodeBase64(b64)` — work in both Node and browser (use `TextEncoder`/`TextDecoder`, not `Buffer`).
235
+
236
+ ## Default Helpers
237
+
238
+ Every `generate` call is prefixed with `DEFAULT_HELPER_LIB` — these are always in scope:
239
+
240
+ | Helper | Purpose |
241
+ | ------------------------------------------------------ | ------------------------------------------------------------------------------- |
242
+ | `uuidv4()` | RFC 4122 v4 UUID. |
243
+ | `generate6DigitId()` | 6-digit numeric string in `[100000, 999999]`. |
244
+ | `currentTimestamp()` | ISO-8601 UTC timestamp. |
245
+ | `isoDurToSec(duration)` | ISO 8601 duration → seconds (0 on unparseable input). |
246
+ | `setCityFromInputs(payload, inputs)` | Writes `inputs.city_code` into `payload.context` (v1 flat / v2 nested). |
247
+ | `createFormURL(domain, formId, sessionData)` | Build a `/forms/<domain>/<formId>/?...` submission URL from session data. |
248
+ | `getSubscriberUrl(sessionData, type)` | `"bpp"` → `sessionData.bppUri`; anything else → `bapUri`. |
249
+ | `generateConsentHandler(sessionData, { custId, ... })` | POSTs to Finvu AA; 10s `AbortController` timeout. Needs `finvuUrl` + allowlist. |
250
+
251
+ Source: `src/lib/helpers/default-helpers.js`. Edit that file and run `npm run helpers:gen` to refresh the shipped bundle (also regenerated automatically by `npm run build` and `npm test`).
252
+
253
+ Helpers that need request-scope data (`getSubscriberUrl`, `createFormURL`, `generateConsentHandler`) take `sessionData` as an **explicit first parameter**. Free-variable references do not resolve inside the sandbox — helpers run at script scope, `sessionData` is only a parameter of `generate()`.
365
254
 
366
- ### User Input Handling
255
+ ## 3rd-party HTTP from `generate`
256
+
257
+ Outbound HTTP is **opt-in and scoped**:
258
+
259
+ 1. Only `generate` gets `fetch` (validate / meetsRequirements / getSave stay pure).
260
+ 2. The installing service provides the allowlist at boot:
261
+ ```typescript
262
+ MockRunner.initSharedRunner({
263
+ allowedFetchBaseUrls: ["https://finvu.example.com/aa"],
264
+ });
265
+ ```
266
+ 3. Matching rule: request `origin` must equal an entry's origin **and** the request path must be a strict segment-prefix of the entry's path. `/v1` matches `/v1` and `/v1/foo` but **not** `/v10/foo`.
267
+ 4. Redirects are blocked (`redirect: "error"`) — call final URLs, don't rely on 3xx hops.
268
+ 5. `AbortController` + `AbortSignal` are in the sandbox; use them for per-request timeouts.
269
+
270
+ ### Worked example — Finvu consent via `generateConsentHandler`
367
271
 
368
272
  ```typescript
369
- // Create function that uses user inputs
370
- const generateWithInputs = `
371
- async function generate(defaultPayload, sessionData) {
372
- // Access user inputs
373
- const { email, deliveryAddress } = sessionData.user_inputs;
374
-
375
- defaultPayload.message.order.billing = {
376
- email: email,
377
- address: deliveryAddress
378
- };
379
-
380
- return defaultPayload;
381
- }
273
+ // boot
274
+ MockRunner.initSharedRunner({
275
+ allowedFetchBaseUrls: ["https://dev-automation.ondc.org/finvu"],
276
+ });
277
+
278
+ // step's generate (base64-encoded)
279
+ const src = `
280
+ async function generate(defaultPayload, sessionData) {
281
+ const handle = await generateConsentHandler(sessionData, { custId: "1234" });
282
+ defaultPayload.message.consentHandle = handle;
283
+ return defaultPayload;
284
+ }
382
285
  `;
383
286
 
384
- const stepWithInputs = {
385
- // ... other config
386
- mock: {
387
- generate: MockRunner.encodeBase64(generateWithInputs),
388
- inputs: {
389
- id: "user_details",
390
- jsonSchema: {
391
- type: "object",
392
- properties: {
393
- email: { type: "string", format: "email" },
394
- deliveryAddress: { type: "string", minLength: 10 },
395
- },
396
- required: ["email", "deliveryAddress"],
397
- },
398
- },
399
- },
400
- };
287
+ // caller passes finvuUrl through extraSessionData
288
+ await runner.runGeneratePayload("consent_0", inputs, {
289
+ finvuUrl: "https://dev-automation.ondc.org/finvu",
290
+ });
401
291
  ```
402
292
 
403
- ## API Reference
293
+ ## Sandbox globals & limits
404
294
 
405
- ### MockRunner
295
+ **Always available:** `Array, Boolean, Date, Error, JSON, Math, Number, Object, Promise, RegExp, String, Symbol, Map, Set, WeakMap, WeakSet, parseInt, parseFloat, isNaN, isFinite, encodeURI(Component), decodeURI(Component), setTimeout, clearTimeout, AbortController, AbortSignal, console.{log,error,warn,info}`.
406
296
 
407
- #### Constructor
297
+ **Added for `generate` only** (and only when an allowlist is configured): `fetch, URL, URLSearchParams, Headers, Request, Response`.
408
298
 
409
- ```typescript
410
- new MockRunner(config: MockPlaygroundConfigType)
411
- ```
299
+ **Explicitly denied:** `require, process, global, globalThis, Buffer, __dirname, __filename, module, exports, eval, Function`.
300
+
301
+ **Timeouts** (from `src/lib/constants/function-registry.ts`):
302
+
303
+ | Function kind | Timeout |
304
+ | ------------------- | ------- |
305
+ | `generate` | 45 s |
306
+ | `validate` | 5 s |
307
+ | `meetsRequirements` | 3 s |
308
+ | `getSave` | 3 s |
309
+
310
+ `setTimeout` inside the sandbox is clamped to 1–45000 ms.
311
+
312
+ ## Dynamic action IDs
412
313
 
413
- #### Methods
314
+ Any `actionId` containing `#` is resolved by taking the last `#`-separated segment. So `"GENERATED#1#search_0"` and `"GENERATED#42#search_0"` both resolve to the step with `action_id: "search_0"`. Applies to all `run*` methods — useful when the same step repeats inside a flow.
414
315
 
415
- **`validateConfig()`**
416
- Validates the entire configuration against the schema.
316
+ ## Session data extraction
317
+
318
+ Each step declares a `saveData` map of JSONPath expressions applied to the prior response payload. The compiled values land on `sessionData` for subsequent steps.
417
319
 
418
320
  ```typescript
419
- const validation = runner.validateConfig();
420
- if (!validation.success) {
421
- console.log("Config errors:", validation.errors);
321
+ saveData: {
322
+ providerId: "$.message.catalog.providers[0].id",
323
+ "APPEND#providerIds": "$.message.catalog.providers[*].id", // concat into array
324
+ customValue: "EVAL#<base64 of extractor>", // custom extractor
422
325
  }
423
326
  ```
424
327
 
425
- **`runGeneratePayload(actionId: string, inputs: any)`**
426
- Generates a payload for the specified action step.
328
+ - `APPEND#key` concatenates the JSONPath result into an existing array under `key` instead of overwriting.
329
+ - `EVAL#<base64>` — runs a sandboxed `getSave(payload)` function and stores its return value.
330
+ - Form steps (`dynamic_form`, `html_form`) auto-save under `sessionData.formData[action_id]` and also set `sessionData[action_id]` to the submission ID.
427
331
 
428
- ```typescript
429
- const result = await runner.runGeneratePayload("search_001", {
430
- category: "books",
431
- });
432
- ```
332
+ ## Form steps
433
333
 
434
- **`runValidatePayload(actionId: string, targetPayload: any)`**
435
- Validates an incoming payload against the specified action step.
334
+ Supported `api` values for forms: `dynamic_form`, `html_form`, `HTML_FORM_MULTI`, `FORM`.
436
335
 
437
- ```typescript
438
- const result = await runner.runValidatePayload(
439
- "on_search_001",
440
- responsePayload,
441
- );
442
- ```
336
+ `force_proceed: true` on a step means "don't wait for user input". `convertToFlowConfig` sets this automatically when the previous step is a form step and the current step has no inputs.
443
337
 
444
- **`runMeetRequirements(actionId: string, targetPayload: any)`**
445
- Checks if prerequisites are met before proceeding with an action.
338
+ ## Config builders
446
339
 
447
- ```typescript
448
- const result = await runner.runMeetRequirements("select_001", {});
449
- ```
340
+ From `@ondc/automation-mock-runner` (via `configHelper.ts`):
450
341
 
451
- **`getDefaultStep(api: string, actionId: string)`**
452
- Creates a new step configuration with sensible defaults and base64-encoded template functions.
342
+ - `createInitialMockConfig(domain, version, flowId)` — scaffold with `DEFAULT_HELPER_LIB` pre-installed as `helperLib`.
343
+ - `generatePlaygroundConfigFromFlowConfig(payloads, flowConfig)` reverse: seed a playground config from real ONDC traffic.
344
+ - `generatePlaygroundConfigFromFlowConfigWithMeta(payloads, flowConfig, domain, version)` — same, with explicit meta (useful when payloads are empty).
345
+ - `convertToFlowConfig(config)` — export a playground config to a Flow sequence.
346
+ - `createOptimizedMockConfig(config)` — Terser-minify each step's `generate` / `validate` / `requirements`.
347
+ - `validateConfigForDeployment(config)` — stricter pre-publish check (throws on problems).
453
348
 
454
- ```typescript
455
- const newStep = runner.getDefaultStep("search", "search_002");
456
- // Returns a step with properly encoded template functions
457
- ```
349
+ ## Error handling
458
350
 
459
- **`MockRunner.encodeBase64(functionString: string)`**
460
- Static utility to encode functions as base64.
351
+ Every `run*` method returns an `ExecutionResult`:
461
352
 
462
353
  ```typescript
463
- const encodedFunction = MockRunner.encodeBase64(`
464
- async function generate(defaultPayload, sessionData) {
465
- // Your function logic here
466
- return defaultPayload;
467
- }
468
- `);
354
+ const res = await runner.runGeneratePayload("search_0");
355
+ if (!res.success) {
356
+ console.log(res.error.name, res.error.message);
357
+ console.log(res.logs); // captured console output
358
+ console.log(res.executionTime, "ms");
359
+ }
469
360
  ```
470
361
 
471
- **`MockRunner.decodeBase64(encodedString: string)`**
472
- Static utility to decode base64-encoded functions (used internally).
362
+ Common error names: `ActionNotFoundError`, `SessionDataError`, `ConfigurationError`, `PayloadGenerationError`, `PayloadValidationError`, `MeetRequirementsError`.
473
363
 
474
- ```typescript
475
- const decodedFunction = MockRunner.decodeBase64(encodedFunction);
476
- ```
364
+ ## FAQ / common gotchas
477
365
 
478
- ## Error Handling
366
+ **`DataCloneError: #<Promise> could not be cloned`** — your `generate` returned a payload containing an un-awaited Promise. Make `generate` `async` and `await` any async helper (including `generateConsentHandler`) before returning. Nested Promises inside the payload are not auto-flattened.
479
367
 
480
- The library provides detailed error information for debugging:
368
+ **`fetch blocked: <url> is not in the configured allowlist`** — add the origin+path to `MockRunner.initSharedRunner({ allowedFetchBaseUrls: [...] })`.
481
369
 
482
- ```typescript
483
- const result = await runner.runGeneratePayload("invalid_step", {});
370
+ **`fetch is not defined`** — you're calling it from `validate`, `meetsRequirements`, or `getSave`. Only `generate` gets `fetch`.
484
371
 
485
- if (!result.success) {
486
- console.log("Error:", result.error.message);
487
- console.log("Logs:", result.logs);
488
- console.log("Execution time:", result.executionTime);
489
- }
490
- ```
372
+ **Helper references `sessionData` but throws `ReferenceError`** — take `sessionData` as an explicit first parameter. Helpers don't share scope with `generate`.
491
373
 
492
- Common error types:
374
+ **Edited `default-helpers.js` but the bundle didn't change** — run `npm run helpers:gen` (or `npm test` / `npm run build` — both regen automatically).
493
375
 
494
- - **ValidationError**: Configuration or input validation failed
495
- - **PayloadGenerationError**: Error in payload generation code
496
- - **PayloadValidationError**: Error in validation code
497
- - **MeetRequirementsError**: Error in requirements check code
498
- - **TimeoutError**: Code execution exceeded timeout limit
376
+ **`validationLib` is not injected** the field exists in the schema but is not currently prepended to any function at execution time. Treat as reserved.
499
377
 
500
- ## Testing
378
+ **Execution timed out** — see the timeout table above. `generate` has the largest window (45 s) specifically for delayed-response mocking.
501
379
 
502
- The library includes comprehensive tests. Run them with:
380
+ ## Testing
503
381
 
504
382
  ```bash
505
- npm test # Run all tests
506
- npm run test:watch # Run tests in watch mode
507
- npm run test:coverage # Generate coverage report
383
+ npm test # full suite (regens helpers via pretest)
384
+ npm run test:watch
385
+ npm run test:coverage
386
+ npm run test:browser-mock # BrowserRunner / CrossEnvironment tests only
508
387
  ```
509
388
 
510
- ## Security Considerations
389
+ ## Security notes
511
390
 
512
- - **Base64 encoding** prevents code injection through configuration
513
- - **Complete function declarations** required - no arbitrary code execution
514
- - **Sandboxed Worker Threads** with isolated VM contexts
515
- - **Restricted global access** - dangerous functions like `eval`, `require`, and file system access are blocked
516
- - **Execution timeouts** prevent infinite loops and hanging processes
517
- - **Memory limits** prevent resource exhaustion
518
- - **Input validation** using Zod schemas for all configuration data
391
+ - Base64 encoding prevents casual injection via config files.
392
+ - The sandbox blocks `eval`, `Function`, `require`, `process`, `Buffer`, filesystem, and (by default) network.
393
+ - Network access is per-installer opt-in and path-scoped.
394
+ - Redirects are refused to prevent allowlist bypass.
395
+ - Workers are recycled after 100 executions / 10 min to limit memory creep in the V8 isolate.
519
396
 
520
397
  ## Contributing
521
398
 
522
- This library is built for the ONDC ecosystem. When contributing:
523
-
524
- 1. Ensure all tests pass: `npm test`
525
- 2. Follow the existing code style: `npm run lint`
526
- 3. Add tests for new features
527
- 4. Update documentation for API changes
399
+ 1. `npm test` all tests green.
400
+ 2. `npm run lint` / `npm run format` — code style.
401
+ 3. `npm run type-check` no TypeScript errors.
402
+ 4. Add tests for new features; update README when public API changes.
528
403
 
529
404
  ## License
530
405
 
531
- ISC License - see LICENSE file for details.
406
+ ISC see LICENSE.
532
407
 
533
408
  ## Support
534
409
 
535
- For ONDC-specific questions, refer to the [ONDC documentation](https://ondc.org/). For issues with this library, please file an issue on the repository.
410
+ ONDC-specific questions: [ondc.org](https://ondc.org/). Library issues: file on the repository.
@@ -35,6 +35,7 @@ export declare class MockRunner {
35
35
  bap_uri?: string | undefined;
36
36
  bpp_id?: string | undefined;
37
37
  bpp_uri?: string | undefined;
38
+ external_session_data?: Record<string, any> | undefined;
38
39
  };
39
40
  steps: {
40
41
  api: string;
@@ -29,6 +29,10 @@ function createInitialMockConfig(domain, version, flowId) {
29
29
  bap_uri: "https://bap.example.com",
30
30
  bpp_id: "bpp.example.com",
31
31
  bpp_uri: "https://bpp.example.com",
32
+ external_session_data: {
33
+ finvuUrl: "https://dev-automation.ondc.org/finvu",
34
+ mockBaseUrl: "https://dev-automation.ondc.org/mock",
35
+ },
32
36
  },
33
37
  steps: [],
34
38
  transaction_history: [],
@@ -19,6 +19,7 @@ export declare const TransactionDataSchema: z.ZodObject<{
19
19
  bap_uri: z.ZodOptional<z.ZodString>;
20
20
  bpp_id: z.ZodOptional<z.ZodString>;
21
21
  bpp_uri: z.ZodOptional<z.ZodString>;
22
+ external_session_data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
22
23
  }, z.core.$strip>;
23
24
  export declare const MockConfigSchema: z.ZodObject<{
24
25
  generate: z.ZodBase64;
@@ -92,6 +93,7 @@ export declare const MockPlaygroundConfigSchema: z.ZodObject<{
92
93
  bap_uri: z.ZodOptional<z.ZodString>;
93
94
  bpp_id: z.ZodOptional<z.ZodString>;
94
95
  bpp_uri: z.ZodOptional<z.ZodString>;
96
+ external_session_data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
95
97
  }, z.core.$strip>;
96
98
  steps: z.ZodArray<z.ZodObject<{
97
99
  api: z.ZodString;
@@ -20,6 +20,7 @@ exports.TransactionDataSchema = zod_1.z.object({
20
20
  bap_uri: zod_1.z.string().min(1, "BAP URI is required").optional(),
21
21
  bpp_id: zod_1.z.string().min(1, "BPP ID is required").optional(),
22
22
  bpp_uri: zod_1.z.string().min(1, "BPP URI is required").optional(),
23
+ external_session_data: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional(),
23
24
  });
24
25
  exports.MockConfigSchema = zod_1.z.object({
25
26
  generate: zod_1.z.base64(),
@@ -14,6 +14,12 @@ export declare class CodeValidator {
14
14
  /**
15
15
  * Validate that return statements match expected structure
16
16
  */
17
+ /**
18
+ * Collect ReturnStatement arguments belonging only to the outer (top-level)
19
+ * function. Nested function/arrow bodies are skipped — their returns are
20
+ * not the function's contract and would otherwise produce false positives.
21
+ */
22
+ private static collectTopLevelReturns;
17
23
  private static validateReturnStructure;
18
24
  /**
19
25
  * Security analysis - checks for forbidden functions and properties
@@ -171,16 +171,35 @@ class CodeValidator {
171
171
  /**
172
172
  * Validate that return statements match expected structure
173
173
  */
174
- static validateReturnStructure(ast, expectedProperties) {
175
- const warnings = [];
176
- const foundReturns = [];
177
- walk.simple(ast, {
174
+ /**
175
+ * Collect ReturnStatement arguments belonging only to the outer (top-level)
176
+ * function. Nested function/arrow bodies are skipped — their returns are
177
+ * not the function's contract and would otherwise produce false positives.
178
+ */
179
+ static collectTopLevelReturns(ast) {
180
+ const returns = [];
181
+ let depth = 0;
182
+ const enterFn = (node, _st, c) => {
183
+ if (depth === 0) {
184
+ depth++;
185
+ c(node.body, null);
186
+ depth--;
187
+ }
188
+ };
189
+ walk.recursive(ast, null, {
190
+ FunctionDeclaration: enterFn,
191
+ FunctionExpression: enterFn,
192
+ ArrowFunctionExpression: enterFn,
178
193
  ReturnStatement(node) {
179
- if (node.argument) {
180
- foundReturns.push(node.argument);
181
- }
194
+ if (node.argument)
195
+ returns.push(node.argument);
182
196
  },
183
197
  });
198
+ return returns;
199
+ }
200
+ static validateReturnStructure(ast, expectedProperties) {
201
+ const warnings = [];
202
+ const foundReturns = this.collectTopLevelReturns(ast);
184
203
  // Check if we have return statements
185
204
  if (foundReturns.length === 0) {
186
205
  warnings.push(`Function should return an object with properties: ${Object.keys(expectedProperties).join(", ")}`);
@@ -295,12 +314,7 @@ class CodeValidator {
295
314
  */
296
315
  static checkBestPractices(ast, schema) {
297
316
  const warnings = [];
298
- let hasReturn = false;
299
- walk.simple(ast, {
300
- ReturnStatement() {
301
- hasReturn = true;
302
- },
303
- });
317
+ const hasReturn = this.collectTopLevelReturns(ast).length > 0;
304
318
  if (!hasReturn) {
305
319
  warnings.push(`Function should return a value (expected: ${schema.returnType.description})`);
306
320
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const code_validator_1 = require("../lib/validators/code-validator");
4
+ const function_registry_1 = require("../lib/constants/function-registry");
5
+ const validateSchema = (0, function_registry_1.getFunctionSchema)("validate");
6
+ describe("CodeValidator.validate — return structure", () => {
7
+ it("accepts an outer return with the full expected shape", () => {
8
+ const code = `
9
+ function validate(targetPayload, sessionData) {
10
+ return { valid: false, code: 200, description: "Valid request" };
11
+ }
12
+ `;
13
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
14
+ expect(result.isValid).toBe(true);
15
+ expect(result.errors).toEqual([]);
16
+ });
17
+ it("ignores nested arrow helper returns (regression: false positive on nested non-object returns)", () => {
18
+ const code = `
19
+ function validate(targetPayload, sessionData) {
20
+ const ok = (x) => { return x.length > 0; };
21
+ const items = (targetPayload.items || []).filter(i => { return i.id; });
22
+ return { valid: ok("hi"), code: 200, description: "Valid request" };
23
+ }
24
+ `;
25
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
26
+ expect(result.isValid).toBe(true);
27
+ expect(result.errors).toEqual([]);
28
+ });
29
+ it("ignores nested function declaration returns", () => {
30
+ const code = `
31
+ function validate(targetPayload, sessionData) {
32
+ function getMsg(x) { return "msg: " + x; }
33
+ return { valid: false, code: 200, description: getMsg("ok") };
34
+ }
35
+ `;
36
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
37
+ expect(result.isValid).toBe(true);
38
+ expect(result.errors).toEqual([]);
39
+ });
40
+ it("flags missing properties on the outer return", () => {
41
+ const code = `
42
+ function validate(targetPayload, sessionData) {
43
+ return { valid: true, code: 200 };
44
+ }
45
+ `;
46
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
47
+ expect(result.isValid).toBe(false);
48
+ expect(result.errors.some((e) => e.includes("description"))).toBe(true);
49
+ });
50
+ it("flags an outer return that is not an object literal", () => {
51
+ const code = `
52
+ function validate(targetPayload, sessionData) {
53
+ return true;
54
+ }
55
+ `;
56
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
57
+ expect(result.isValid).toBe(false);
58
+ expect(result.errors.some((e) => e.includes("Function should return an object literal"))).toBe(true);
59
+ });
60
+ it("warns when only a nested helper returns and the outer function has no return", () => {
61
+ const code = `
62
+ function validate(targetPayload, sessionData) {
63
+ function helper() { return 42; }
64
+ helper();
65
+ }
66
+ `;
67
+ const result = code_validator_1.CodeValidator.validate(code, validateSchema);
68
+ expect(result.warnings.some((w) => w.includes("should return a value"))).toBe(true);
69
+ });
70
+ });
@@ -456,6 +456,10 @@ describe("configHelper", () => {
456
456
  bap_uri: "https://bap.example.com",
457
457
  bpp_id: "bpp.example.com",
458
458
  bpp_uri: "https://bpp.example.com",
459
+ external_session_data: {
460
+ finvuUrl: "https://dev-automation.ondc.org/finvu",
461
+ mockBaseUrl: "https://dev-automation.ondc.org/mock",
462
+ },
459
463
  });
460
464
  });
461
465
  it("should initialize empty arrays and strings", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondc/automation-mock-runner",
3
- "version": "1.3.45",
3
+ "version": "1.3.48",
4
4
  "description": "A TypeScript library for ONDC automation mock runner",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",