@hostwebhook/node-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/code-runner.d.ts +20 -0
  2. package/dist/code-runner.js +138 -0
  3. package/dist/contratos.d.ts +121 -0
  4. package/dist/contratos.js +24 -0
  5. package/dist/dto/output-node.dto.d.ts +19 -0
  6. package/dist/dto/output-node.dto.js +96 -0
  7. package/dist/ensure-meta.d.ts +22 -0
  8. package/dist/ensure-meta.js +35 -0
  9. package/dist/execute-with-iteration.d.ts +18 -0
  10. package/dist/execute-with-iteration.js +66 -0
  11. package/dist/filter-utils.d.ts +22 -0
  12. package/dist/filter-utils.js +178 -0
  13. package/dist/handler-helpers.d.ts +21 -0
  14. package/dist/handler-helpers.js +53 -0
  15. package/dist/index.d.ts +51 -0
  16. package/dist/index.js +73 -0
  17. package/dist/log-metadata.d.ts +191 -0
  18. package/dist/log-metadata.js +375 -0
  19. package/dist/node-dispatch.registry.d.ts +32 -0
  20. package/dist/node-dispatch.registry.js +45 -0
  21. package/dist/node-executors.d.ts +299 -0
  22. package/dist/node-executors.js +555 -0
  23. package/dist/node-lifecycle.d.ts +399 -0
  24. package/dist/node-lifecycle.js +782 -0
  25. package/dist/normalize-nodes.d.ts +18 -0
  26. package/dist/normalize-nodes.js +22 -0
  27. package/dist/output-node-ref.schema.d.ts +82 -0
  28. package/dist/output-node-ref.schema.js +90 -0
  29. package/dist/output-webhook-scope.d.ts +36 -0
  30. package/dist/output-webhook-scope.js +42 -0
  31. package/dist/payload-preview.d.ts +10 -0
  32. package/dist/payload-preview.js +39 -0
  33. package/dist/pipeline.constants.d.ts +29 -0
  34. package/dist/pipeline.constants.js +51 -0
  35. package/dist/pre-request-pool.d.ts +58 -0
  36. package/dist/pre-request-pool.js +308 -0
  37. package/dist/pre-request-runner-source.d.ts +28 -0
  38. package/dist/pre-request-runner-source.js +411 -0
  39. package/dist/regex-de-inquilino.d.ts +15 -0
  40. package/dist/regex-de-inquilino.js +98 -0
  41. package/dist/request-context.d.ts +18 -0
  42. package/dist/request-context.js +34 -0
  43. package/dist/retry-transient.d.ts +54 -0
  44. package/dist/retry-transient.js +67 -0
  45. package/dist/retry-utils.d.ts +17 -0
  46. package/dist/retry-utils.js +23 -0
  47. package/dist/schema-validator-utils.d.ts +9 -0
  48. package/dist/schema-validator-utils.js +140 -0
  49. package/dist/ssrf-guard.d.ts +202 -0
  50. package/dist/ssrf-guard.js +917 -0
  51. package/dist/swallow.d.ts +52 -0
  52. package/dist/swallow.js +55 -0
  53. package/dist/template-render.d.ts +33 -0
  54. package/dist/template-render.js +43 -0
  55. package/dist/try-parse.d.ts +41 -0
  56. package/dist/try-parse.js +69 -0
  57. package/dist/workspace-payloads.d.ts +66 -0
  58. package/dist/workspace-payloads.js +496 -0
  59. package/package.json +35 -0
@@ -0,0 +1,555 @@
1
+ "use strict";
2
+ /**
3
+ * Shared node execution functions.
4
+ *
5
+ * Both pipeline-run.service.ts (Run Pipeline / test) and event-pipeline.service.ts
6
+ * (real events) call these functions so that "how to execute node type X" lives in
7
+ * exactly ONE place. Callers still own Delivery records, child events, telemetry,
8
+ * and output-routing — only core execution logic is here.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.HANDLERLESS_NODE_TYPES = void 0;
12
+ exports.executeFilter = executeFilter;
13
+ exports.executeTransform = executeTransform;
14
+ exports.executeHttpAction = executeHttpAction;
15
+ exports.executeEmailAction = executeEmailAction;
16
+ exports.executeSheetsAction = executeSheetsAction;
17
+ exports.executeDriveAction = executeDriveAction;
18
+ exports.executeVectorStoreNode = executeVectorStoreNode;
19
+ exports.executeFileTransformNode = executeFileTransformNode;
20
+ exports.executePostgresAction = executePostgresAction;
21
+ exports.executeCalendarAction = executeCalendarAction;
22
+ exports.executeDocsAction = executeDocsAction;
23
+ exports.executeMarkdownNode = executeMarkdownNode;
24
+ exports.executeAiNode = executeAiNode;
25
+ exports.executeMongoAction = executeMongoAction;
26
+ exports.executeAggregator = executeAggregator;
27
+ exports.executeCache = executeCache;
28
+ exports.executeRateLimiter = executeRateLimiter;
29
+ exports.executeConditional = executeConditional;
30
+ exports.executeSchemaValidator = executeSchemaValidator;
31
+ exports.executeNotificationAction = executeNotificationAction;
32
+ exports.executeSplit = executeSplit;
33
+ exports.executeCode = executeCode;
34
+ exports.executeLoopFanOut = executeLoopFanOut;
35
+ exports.isHandlerless = isHandlerless;
36
+ const common_1 = require("@nestjs/common");
37
+ const template_engine_1 = require("@hostwebhook/template-engine");
38
+ const schemaLogger = new common_1.Logger('SchemaValidator');
39
+ const filterLogger = new common_1.Logger('Filter');
40
+ const conditionalLogger = new common_1.Logger('Conditional');
41
+ const filter_utils_1 = require("./filter-utils");
42
+ const schema_validator_utils_1 = require("./schema-validator-utils");
43
+ const try_parse_1 = require("./try-parse");
44
+ const code_runner_1 = require("./code-runner");
45
+ function executeFilter(entity, payload, context) {
46
+ const filterCount = (entity.filters ?? []).filter((f) => f.enabled !== false).length;
47
+ const meta = payload._meta;
48
+ filterLogger.log(`"${entity.name}" evaluating — ${filterCount} active filters, mode=${entity.filterMode}, iterable=${!!meta?.iterable}${meta?.count ? `, ${meta.count} items` : ''}`);
49
+ const filterOpts = context?.workspacePayloads
50
+ ? { workspacePayloads: context.workspacePayloads }
51
+ : undefined;
52
+ const start = Date.now();
53
+ let result;
54
+ try {
55
+ result = (0, filter_utils_1.evaluateFiltersIterable)(entity.filters, payload, entity.filterMode, entity.outputAsSingle, filterOpts);
56
+ }
57
+ catch (err) {
58
+ const ms = Date.now() - start;
59
+ common_1.Logger.error(`[Filter] "${entity.name}" CRASHED after ${ms}ms: ${err instanceof Error ? err.message : err}`, 'NodeExecutors');
60
+ return {
61
+ statusCode: 500,
62
+ responseBody: JSON.stringify({
63
+ error: err instanceof Error ? err.message : 'Filter evaluation failed',
64
+ }),
65
+ latencyMs: ms,
66
+ outputPayload: payload,
67
+ passed: false,
68
+ totalItems: 0,
69
+ passedItems: 0,
70
+ };
71
+ }
72
+ const { passed, resultPayload, totalItems, passedItems } = result;
73
+ const ms = Date.now() - start;
74
+ filterLogger.log(`"${entity.name}" ${passed ? 'PASSED' : 'BLOCKED'} — ${passedItems}/${totalItems} items in ${ms}ms`);
75
+ return {
76
+ statusCode: passed ? 200 : 403,
77
+ responseBody: JSON.stringify({
78
+ passed,
79
+ totalItems,
80
+ passedItems,
81
+ payload: resultPayload,
82
+ }),
83
+ latencyMs: ms,
84
+ outputPayload: resultPayload,
85
+ passed,
86
+ totalItems,
87
+ passedItems,
88
+ };
89
+ }
90
+ async function executeTransform(svc, entity, payload, context) {
91
+ return svc.executeSingle(entity, payload, {
92
+ headers: context?.headers,
93
+ meta: context?.eventMeta,
94
+ workspacePayloads: context?.workspacePayloads,
95
+ });
96
+ }
97
+ async function executeHttpAction(svc, entity, payload, context) {
98
+ return svc.executeSingle(entity, payload, {
99
+ eventMeta: context?.eventMeta,
100
+ workspacePayloads: context?.workspacePayloads,
101
+ });
102
+ }
103
+ async function executeEmailAction(svc, entity, payload) {
104
+ return svc.executeSingle(entity, payload);
105
+ }
106
+ async function executeSheetsAction(svc, entity, payload, context) {
107
+ const start = Date.now();
108
+ const result = await svc.executeForEvent(entity, payload, {
109
+ workspacePayloads: context?.workspacePayloads,
110
+ });
111
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
112
+ const responseBody = typeof result?.responseBody === 'string'
113
+ ? result.responseBody
114
+ : JSON.stringify(result ?? {});
115
+ return {
116
+ statusCode,
117
+ responseBody,
118
+ latencyMs: result?.latencyMs ?? Date.now() - start,
119
+ };
120
+ }
121
+ async function executeDriveAction(svc, entity, payload, context) {
122
+ const start = Date.now();
123
+ const result = await svc.executeForEvent(entity, payload, {
124
+ workspacePayloads: context?.workspacePayloads,
125
+ eventMeta: context?.eventMeta,
126
+ });
127
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
128
+ const responseBody = typeof result?.responseBody === 'string'
129
+ ? result.responseBody
130
+ : JSON.stringify(result ?? {});
131
+ return {
132
+ statusCode,
133
+ responseBody,
134
+ latencyMs: result?.latencyMs ?? Date.now() - start,
135
+ };
136
+ }
137
+ async function executeVectorStoreNode(svc, entity, payload, context) {
138
+ const start = Date.now();
139
+ // Wrapped-payload shape: the service builds ctx via { ...payload, meta }
140
+ // and renders templates against ctx.payload, so the iterated item must
141
+ // sit under `payload` — passing raw payload here would break {{payload.X}}.
142
+ const result = await svc.executeForEvent(entity, {
143
+ payload,
144
+ workspacePayloads: context?.workspacePayloads,
145
+ eventMeta: context?.eventMeta,
146
+ });
147
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
148
+ const responseBody = typeof result?.responseBody === 'string'
149
+ ? result.responseBody
150
+ : JSON.stringify(result ?? {});
151
+ return {
152
+ statusCode,
153
+ responseBody,
154
+ latencyMs: result?.latencyMs ?? Date.now() - start,
155
+ };
156
+ }
157
+ async function executeFileTransformNode(svc, entity, payload, context) {
158
+ const start = Date.now();
159
+ const result = await svc.executeForEvent(entity, payload, {
160
+ workspacePayloads: context?.workspacePayloads,
161
+ });
162
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
163
+ const responseBody = typeof result?.responseBody === 'string'
164
+ ? result.responseBody
165
+ : JSON.stringify(result ?? {});
166
+ return {
167
+ statusCode,
168
+ responseBody,
169
+ latencyMs: result?.latencyMs ?? Date.now() - start,
170
+ };
171
+ }
172
+ async function executePostgresAction(svc, entity, payload, context) {
173
+ const start = Date.now();
174
+ // Wrapped-payload shape — see executeVectorStoreNode for rationale.
175
+ const result = await svc.executeForEvent(entity, {
176
+ payload,
177
+ workspacePayloads: context?.workspacePayloads,
178
+ eventMeta: context?.eventMeta,
179
+ });
180
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
181
+ const responseBody = typeof result?.responseBody === 'string'
182
+ ? result.responseBody
183
+ : JSON.stringify(result ?? {});
184
+ return {
185
+ statusCode,
186
+ responseBody,
187
+ latencyMs: result?.latencyMs ?? Date.now() - start,
188
+ };
189
+ }
190
+ async function executeCalendarAction(svc, entity, payload, context) {
191
+ const start = Date.now();
192
+ const result = await svc.executeForEvent(entity, payload, {
193
+ workspacePayloads: context?.workspacePayloads,
194
+ });
195
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
196
+ const responseBody = typeof result?.responseBody === 'string'
197
+ ? result.responseBody
198
+ : JSON.stringify(result ?? {});
199
+ return {
200
+ statusCode,
201
+ responseBody,
202
+ latencyMs: result?.latencyMs ?? Date.now() - start,
203
+ };
204
+ }
205
+ async function executeDocsAction(svc, entity, payload, context) {
206
+ const start = Date.now();
207
+ const result = await svc.executeForEvent(entity, payload, {
208
+ workspacePayloads: context?.workspacePayloads,
209
+ });
210
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
211
+ const responseBody = typeof result?.responseBody === 'string'
212
+ ? result.responseBody
213
+ : JSON.stringify(result ?? {});
214
+ return {
215
+ statusCode,
216
+ responseBody,
217
+ latencyMs: result?.latencyMs ?? Date.now() - start,
218
+ };
219
+ }
220
+ async function executeMarkdownNode(svc, entity, payload, context) {
221
+ const start = Date.now();
222
+ const result = await svc.executeForEvent(entity, payload, {
223
+ workspacePayloads: context?.workspacePayloads,
224
+ });
225
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
226
+ const responseBody = typeof result?.responseBody === 'string'
227
+ ? result.responseBody
228
+ : JSON.stringify(result ?? {});
229
+ return {
230
+ statusCode,
231
+ responseBody,
232
+ latencyMs: result?.latencyMs ?? Date.now() - start,
233
+ };
234
+ }
235
+ async function executeAiNode(svc, entity, payload, context) {
236
+ const start = Date.now();
237
+ const result = await svc.executeForEvent(entity, payload, {
238
+ workspacePayloads: context?.workspacePayloads,
239
+ eventMeta: context?.eventMeta,
240
+ });
241
+ const statusCode = result?.statusCode ?? (result?.success ? 200 : 500);
242
+ const responseBody = typeof result?.responseBody === 'string'
243
+ ? result.responseBody
244
+ : JSON.stringify(result ?? {});
245
+ return {
246
+ statusCode,
247
+ responseBody,
248
+ latencyMs: result?.latencyMs ?? Date.now() - start,
249
+ };
250
+ }
251
+ async function executeMongoAction(svc, entity, payload, context) {
252
+ const res = await svc.executeForEvent(entity, {
253
+ payload,
254
+ workspacePayloads: context?.workspacePayloads,
255
+ });
256
+ return {
257
+ statusCode: 200,
258
+ responseBody: JSON.stringify(res.result ?? {}),
259
+ latencyMs: res.latencyMs,
260
+ outputPayload: res.result ?? {},
261
+ };
262
+ }
263
+ async function executeAggregator(svc, entity, payload) {
264
+ return svc.processBatchPayload(entity, payload);
265
+ }
266
+ function executeCache(svc, entity, payload) {
267
+ const start = Date.now();
268
+ const { action, outputPayload } = svc.processEvent(entity, payload);
269
+ return {
270
+ statusCode: action === 'blocked' ? 304 : 200,
271
+ responseBody: JSON.stringify({ action, payload: outputPayload }),
272
+ latencyMs: Date.now() - start,
273
+ outputPayload: outputPayload ?? undefined,
274
+ };
275
+ }
276
+ function executeRateLimiter(svc, entity) {
277
+ const start = Date.now();
278
+ const { allowed, delayMs } = svc.checkRate(entity);
279
+ return {
280
+ statusCode: allowed ? 200 : 429,
281
+ responseBody: JSON.stringify({ allowed, delayMs }),
282
+ latencyMs: Date.now() - start,
283
+ };
284
+ }
285
+ function executeConditional(entity, payload, context) {
286
+ const start = Date.now();
287
+ const meta = payload?._meta;
288
+ conditionalLogger.log(`"${entity.name}" _meta=${JSON.stringify(meta)} iterateField=${meta?.iterateField} items=${Array.isArray(payload?.[meta?.iterateField]) ? payload[meta.iterateField].length : 'N/A'}`);
289
+ for (const branch of entity.branches ?? []) {
290
+ conditionalLogger.debug(`"${entity.name}" branch="${branch.name}" filters=${JSON.stringify(branch.filters)}`);
291
+ }
292
+ const filterOpts = context?.workspacePayloads
293
+ ? { workspacePayloads: context.workspacePayloads }
294
+ : undefined;
295
+ let matched = null;
296
+ for (const branch of entity.branches ?? []) {
297
+ if ((branch.filters ?? []).length === 0)
298
+ continue;
299
+ const { passed } = (0, filter_utils_1.evaluateFiltersIterable)(branch.filters, payload, branch.filterMode ?? 'and', false, filterOpts);
300
+ conditionalLogger.log(`"${entity.name}" branch="${branch.name}" passed=${passed}`);
301
+ if (passed) {
302
+ matched = branch;
303
+ break;
304
+ }
305
+ }
306
+ return {
307
+ statusCode: 200,
308
+ responseBody: JSON.stringify({ matched: matched?.name ?? null }),
309
+ latencyMs: Date.now() - start,
310
+ outputPayload: payload,
311
+ matchedBranch: matched,
312
+ };
313
+ }
314
+ function executeSchemaValidator(entity, payload) {
315
+ const start = Date.now();
316
+ const result = (0, schema_validator_utils_1.validateSchema)(entity.fields, payload, entity.strictMode);
317
+ const latencyMs = Date.now() - start;
318
+ if (result.valid) {
319
+ schemaLogger.log(`Schema "${entity.name}" VALID — ${entity.fields?.length ?? 0} fields${entity.strictMode ? ' (strict)' : ''} (${latencyMs}ms)`);
320
+ }
321
+ else {
322
+ schemaLogger.warn(`Schema "${entity.name}" INVALID — ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.message}`).join(', ')} (${latencyMs}ms)`);
323
+ }
324
+ return {
325
+ statusCode: 200, // Always 200 — validity is in responseBody.valid, routing via getOutputNodes
326
+ responseBody: JSON.stringify(result),
327
+ latencyMs,
328
+ outputPayload: payload,
329
+ };
330
+ }
331
+ async function executeNotificationAction(svc, entity, payload, notifCtx) {
332
+ const start = Date.now();
333
+ const result = await svc.sendForEvent(entity, notifCtx ?? {}, payload);
334
+ return {
335
+ statusCode: result.success ? 200 : (result.statusCode ?? 500),
336
+ responseBody: JSON.stringify(result),
337
+ latencyMs: Date.now() - start,
338
+ };
339
+ }
340
+ /** Resolve a dot-notation path (e.g. "data.orders") from an object */
341
+ function getNestedValue(obj, path) {
342
+ let current = obj;
343
+ for (const key of path.split('.')) {
344
+ if (current == null || typeof current !== 'object')
345
+ return undefined;
346
+ current = current[key];
347
+ }
348
+ return current;
349
+ }
350
+ // resolveFieldPath replaced by resolveFieldPath from @hostwebhook/template-engine
351
+ function executeSplit(entity, payload) {
352
+ const start = Date.now();
353
+ // Unwrap iterable payload — split operates on the inner data, not the wrapper
354
+ let effective = payload;
355
+ const meta = payload._meta;
356
+ if (meta?.iterable && meta.iterateField) {
357
+ const arr = payload[meta.iterateField];
358
+ if (Array.isArray(arr) && arr.length > 0) {
359
+ effective =
360
+ typeof arr[0] === 'object' && arr[0] !== null
361
+ ? { ...arr[0] }
362
+ : { value: arr[0] };
363
+ }
364
+ }
365
+ const outputs = [];
366
+ const assignedFields = new Set();
367
+ for (const output of entity.outputs ?? []) {
368
+ const field = (0, template_engine_1.resolveFieldPath)(output.field);
369
+ const value = getNestedValue(effective, field);
370
+ assignedFields.add(field.split('.')[0]);
371
+ outputs.push({ name: output.name, field: output.field, payload: value });
372
+ }
373
+ // Build remainder: top-level fields not assigned to any output
374
+ const remainderEntries = Object.entries(effective).filter(([key]) => key !== '_meta' && !assignedFields.has(key));
375
+ const remainder = remainderEntries.length > 0 ? Object.fromEntries(remainderEntries) : null;
376
+ return {
377
+ statusCode: 200,
378
+ responseBody: JSON.stringify({ splitOutputs: outputs, remainder }),
379
+ latencyMs: Date.now() - start,
380
+ outputPayload: effective,
381
+ splitOutputs: outputs,
382
+ remainder,
383
+ };
384
+ }
385
+ /**
386
+ * Ejecutar el nodo de código.
387
+ *
388
+ * ## Por qué esto ya no corre aquí dentro
389
+ *
390
+ * Antes iba a `renderTemplate(..., { mode: 'code' })`, o sea a `node:vm` con
391
+ * una lista negra sobre el texto fuente. `vm` no es una frontera y una lista
392
+ * de expresiones regulares sobre el fuente no es un control: la concatenación
393
+ * de cadenas la cruza sin despeinarse, y el contexto se sembraba con los
394
+ * intrínsecos del propio realm, así que `Math.constructor.constructor` era el
395
+ * `Function` de la casa.
396
+ *
397
+ * Con eso, un `return Math["constr"+"uctor"]…("return this")()["proc"+"ess"].env`
398
+ * devolvía `process.env` — es decir `MONGO_URI` y `API_SECRET`, que es la
399
+ * entrada del PBKDF2 con el que se descifran las credenciales guardadas de
400
+ * TODOS los inquilinos. Y cambiando la pieza final, ejecución de comandos.
401
+ *
402
+ * Ahora corre en el mismo hijo desechable que los scripts pre-request: entorno
403
+ * vacío, sin ficheros ni spawn donde el runtime lo soporte, muerto tras cada
404
+ * ejecución. Salirse del vm sigue siendo fácil; lo que cambia es que ya no
405
+ * lleva a ningún sitio.
406
+ *
407
+ * El contrato hacia fuera no cambia — mismos códigos, mismo `outputPayload`,
408
+ * mismos logs— salvo que ahora es asíncrono, que es lo que cuesta un proceso.
409
+ */
410
+ async function executeCode(entity, payload, workspacePayloads) {
411
+ const start = Date.now();
412
+ const timeout = entity.timeout || 5000;
413
+ let output;
414
+ let logs;
415
+ try {
416
+ const r = await (0, code_runner_1.runTenantCode)(entity.code, { payload, headers: {}, meta: {}, workspacePayloads }, timeout);
417
+ output = r.output;
418
+ logs = r.logs;
419
+ }
420
+ catch (err) {
421
+ // Los logs que alcanzó a emitir antes de reventar son justo los que el
422
+ // usuario necesita para entender por qué. `CodeNodeError` los trae del
423
+ // worker; el hijo los cuelga del Error igual, así que este camino no
424
+ // necesita saber cuál de los dos corrió.
425
+ const conLogs = err;
426
+ return {
427
+ statusCode: 500,
428
+ responseBody: JSON.stringify({
429
+ error: conLogs?.message ?? String(err),
430
+ logs: conLogs?.logs ?? [],
431
+ }),
432
+ latencyMs: Date.now() - start,
433
+ logs: conLogs?.logs ?? [],
434
+ };
435
+ }
436
+ if (output == null) {
437
+ return {
438
+ statusCode: 204,
439
+ responseBody: JSON.stringify({ blocked: true, logs }),
440
+ latencyMs: Date.now() - start,
441
+ outputPayload: undefined,
442
+ logs,
443
+ };
444
+ }
445
+ const outputPayload = typeof output === 'object' && !Array.isArray(output)
446
+ ? output
447
+ : { result: output };
448
+ return {
449
+ statusCode: 200,
450
+ responseBody: JSON.stringify(outputPayload),
451
+ latencyMs: Date.now() - start,
452
+ outputPayload,
453
+ logs,
454
+ };
455
+ }
456
+ /** If the executor didn't set outputPayload, try to parse it from responseBody. */
457
+ function ensureOutputPayload(result) {
458
+ if (result.outputPayload !== undefined)
459
+ return result;
460
+ // tryParseJson rather than tryParseJsonObject: an array body is a legitimate
461
+ // payload here, and the object-only helper would quietly drop it.
462
+ const parsed = (0, try_parse_1.tryParseJson)(result.responseBody);
463
+ if (typeof parsed === 'object' && parsed !== null) {
464
+ return { ...result, outputPayload: parsed };
465
+ }
466
+ return result;
467
+ }
468
+ /**
469
+ * Loop fan-out: turn the iterable field into one payload per item, each
470
+ * stamped with its position. The loop node owns its own iteration and
471
+ * LoopState, which is why it has no registered handler — the caller drives
472
+ * the iterations from this list.
473
+ */
474
+ function executeLoopFanOut(entity, payload) {
475
+ // Extract iterable array — use _meta.iterateField or entity.iterableField
476
+ const loopMeta = payload?._meta;
477
+ const loopField = loopMeta?.iterable && loopMeta.iterateField
478
+ ? loopMeta.iterateField
479
+ : (entity.iterableField ?? '').trim() || '';
480
+ const loopItems = loopField ? payload?.[loopField] : payload;
481
+ common_1.Logger.debug(`[Loop] loopField="${loopField}", iterableField="${entity.iterableField}", payloadKeys=${Object.keys(payload)}, isArray=${Array.isArray(loopItems)}`, 'NodeExecutors');
482
+ if (!Array.isArray(loopItems)) {
483
+ return {
484
+ statusCode: 400,
485
+ responseBody: JSON.stringify({
486
+ error: `Field "${loopField || 'payload'}" is not an array`,
487
+ }),
488
+ latencyMs: 0,
489
+ };
490
+ }
491
+ // Build per-iteration payloads
492
+ const perIteration = loopItems.map((item, i) => {
493
+ const base = typeof item === 'object' && item !== null && !Array.isArray(item)
494
+ ? { ...item }
495
+ : { value: item };
496
+ return {
497
+ _meta: {
498
+ loopIndex: i,
499
+ loopTotal: loopItems.length,
500
+ isFirst: i === 0,
501
+ isLast: i === loopItems.length - 1,
502
+ },
503
+ ...base,
504
+ };
505
+ });
506
+ const outputPayload = {
507
+ _meta: { iterable: true, iterateField: 'items', count: loopItems.length },
508
+ items: perIteration,
509
+ };
510
+ return {
511
+ statusCode: 200,
512
+ responseBody: JSON.stringify(outputPayload),
513
+ latencyMs: 0,
514
+ outputPayload,
515
+ };
516
+ }
517
+ /**
518
+ * The seven node types that do NOT call `registerNodeHandler`, and what
519
+ * Run Pipeline does for each. Everything else executes through its
520
+ * registered handler — the same object prod runs — so the two cycles
521
+ * cannot drift.
522
+ *
523
+ * This list is deliberately explicit rather than a `default:` arm. A
524
+ * `default` silently absorbs any node type nobody wired up, which is
525
+ * exactly how the Limit node came to return 15 of 15 items in Run
526
+ * Pipeline while prod limited correctly. A missing entry here is a
527
+ * loud `undefined`, and `architecture.spec.ts` asserts the set matches
528
+ * the types that actually lack a handler.
529
+ */
530
+ exports.HANDLERLESS_NODE_TYPES = {
531
+ /** Annotation — never executes. */
532
+ stickyNote: { run: 'passthrough', telemetrySource: 'sticky_note' },
533
+ // Sources. They execute by being *called* (an HTTP request arrives, a cron
534
+ // fires, a call connects), not by being dispatched to, so the payload they
535
+ // hand downstream is the payload they received.
536
+ webhook: { run: 'passthrough', telemetrySource: 'entry_webhook' },
537
+ scheduledWorkflow: {
538
+ run: 'passthrough',
539
+ telemetrySource: 'scheduled_webhook',
540
+ },
541
+ voiceAgent: { run: 'passthrough', telemetrySource: 'voice_agent' },
542
+ /** Queue-based: prod enqueues, Run Pipeline has nothing to wait on. */
543
+ delay: { run: 'passthrough', telemetrySource: 'delay_node' },
544
+ /** Waits on a human. Run Pipeline cannot block on a click. */
545
+ approval: { run: 'passthrough', telemetrySource: 'approval_node' },
546
+ /** pipeline-run.service collects the inputs itself and merges them. */
547
+ merge: { run: 'passthrough', telemetrySource: 'merge_node' },
548
+ /** Owns its own iteration + LoopState; pipeline-run drives it directly. */
549
+ loop: { run: 'custom', telemetrySource: 'loop_node' },
550
+ /** Counter lives in Redis; prod dispatches it from event-pipeline. */
551
+ rateLimiter: { run: 'custom', telemetrySource: 'rate_limiter_node' },
552
+ };
553
+ function isHandlerless(nodeType) {
554
+ return nodeType in exports.HANDLERLESS_NODE_TYPES;
555
+ }