@griffin-app/griffin-executor 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 (153) hide show
  1. package/README.md +152 -0
  2. package/dist/adapters/axios.d.ts +5 -0
  3. package/dist/adapters/axios.d.ts.map +1 -0
  4. package/dist/adapters/axios.js +36 -0
  5. package/dist/adapters/axios.js.map +1 -0
  6. package/dist/adapters/index.d.ts +3 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +3 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/stub.d.ts +22 -0
  11. package/dist/adapters/stub.d.ts.map +1 -0
  12. package/dist/adapters/stub.js +36 -0
  13. package/dist/adapters/stub.js.map +1 -0
  14. package/dist/events/adapters/in-memory.d.ts +52 -0
  15. package/dist/events/adapters/in-memory.d.ts.map +1 -0
  16. package/dist/events/adapters/in-memory.js +70 -0
  17. package/dist/events/adapters/in-memory.js.map +1 -0
  18. package/dist/events/adapters/in-memory.test.d.ts +2 -0
  19. package/dist/events/adapters/in-memory.test.d.ts.map +1 -0
  20. package/dist/events/adapters/in-memory.test.js +109 -0
  21. package/dist/events/adapters/in-memory.test.js.map +1 -0
  22. package/dist/events/adapters/index.d.ts +9 -0
  23. package/dist/events/adapters/index.d.ts.map +1 -0
  24. package/dist/events/adapters/index.js +9 -0
  25. package/dist/events/adapters/index.js.map +1 -0
  26. package/dist/events/adapters/kinesis.d.ts +91 -0
  27. package/dist/events/adapters/kinesis.d.ts.map +1 -0
  28. package/dist/events/adapters/kinesis.js +136 -0
  29. package/dist/events/adapters/kinesis.js.map +1 -0
  30. package/dist/events/adapters/kinesis.test.d.ts +2 -0
  31. package/dist/events/adapters/kinesis.test.d.ts.map +1 -0
  32. package/dist/events/adapters/kinesis.test.js +249 -0
  33. package/dist/events/adapters/kinesis.test.js.map +1 -0
  34. package/dist/events/emitter.d.ts +68 -0
  35. package/dist/events/emitter.d.ts.map +1 -0
  36. package/dist/events/emitter.js +83 -0
  37. package/dist/events/emitter.js.map +1 -0
  38. package/dist/events/emitter.test.d.ts +2 -0
  39. package/dist/events/emitter.test.d.ts.map +1 -0
  40. package/dist/events/emitter.test.js +262 -0
  41. package/dist/events/emitter.test.js.map +1 -0
  42. package/dist/events/index.d.ts +4 -0
  43. package/dist/events/index.d.ts.map +1 -0
  44. package/dist/events/index.js +4 -0
  45. package/dist/events/index.js.map +1 -0
  46. package/dist/events/types.d.ts +112 -0
  47. package/dist/events/types.d.ts.map +1 -0
  48. package/dist/events/types.js +9 -0
  49. package/dist/events/types.js.map +1 -0
  50. package/dist/executor.d.ts +4 -0
  51. package/dist/executor.d.ts.map +1 -0
  52. package/dist/executor.js +799 -0
  53. package/dist/executor.js.map +1 -0
  54. package/dist/executor.test.d.ts +2 -0
  55. package/dist/executor.test.d.ts.map +1 -0
  56. package/dist/executor.test.js +1584 -0
  57. package/dist/executor.test.js.map +1 -0
  58. package/dist/index.d.ts +9 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +15 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/secrets/factory.d.ts +121 -0
  63. package/dist/secrets/factory.d.ts.map +1 -0
  64. package/dist/secrets/factory.js +137 -0
  65. package/dist/secrets/factory.js.map +1 -0
  66. package/dist/secrets/index.d.ts +14 -0
  67. package/dist/secrets/index.d.ts.map +1 -0
  68. package/dist/secrets/index.js +18 -0
  69. package/dist/secrets/index.js.map +1 -0
  70. package/dist/secrets/providers/aws.d.ts +63 -0
  71. package/dist/secrets/providers/aws.d.ts.map +1 -0
  72. package/dist/secrets/providers/aws.js +110 -0
  73. package/dist/secrets/providers/aws.js.map +1 -0
  74. package/dist/secrets/providers/env.d.ts +36 -0
  75. package/dist/secrets/providers/env.d.ts.map +1 -0
  76. package/dist/secrets/providers/env.js +37 -0
  77. package/dist/secrets/providers/env.js.map +1 -0
  78. package/dist/secrets/providers/index.d.ts +7 -0
  79. package/dist/secrets/providers/index.d.ts.map +1 -0
  80. package/dist/secrets/providers/index.js +7 -0
  81. package/dist/secrets/providers/index.js.map +1 -0
  82. package/dist/secrets/providers/vault.d.ts +75 -0
  83. package/dist/secrets/providers/vault.d.ts.map +1 -0
  84. package/dist/secrets/providers/vault.js +143 -0
  85. package/dist/secrets/providers/vault.js.map +1 -0
  86. package/dist/secrets/registry.d.ts +39 -0
  87. package/dist/secrets/registry.d.ts.map +1 -0
  88. package/dist/secrets/registry.js +134 -0
  89. package/dist/secrets/registry.js.map +1 -0
  90. package/dist/secrets/resolver.d.ts +45 -0
  91. package/dist/secrets/resolver.d.ts.map +1 -0
  92. package/dist/secrets/resolver.js +188 -0
  93. package/dist/secrets/resolver.js.map +1 -0
  94. package/dist/secrets/secrets.test.d.ts +2 -0
  95. package/dist/secrets/secrets.test.d.ts.map +1 -0
  96. package/dist/secrets/secrets.test.js +317 -0
  97. package/dist/secrets/secrets.test.js.map +1 -0
  98. package/dist/secrets/types.d.ts +70 -0
  99. package/dist/secrets/types.d.ts.map +1 -0
  100. package/dist/secrets/types.js +42 -0
  101. package/dist/secrets/types.js.map +1 -0
  102. package/dist/shared.d.ts +8 -0
  103. package/dist/shared.d.ts.map +1 -0
  104. package/dist/shared.js +30 -0
  105. package/dist/shared.js.map +1 -0
  106. package/dist/test-monitor-types.d.ts +43 -0
  107. package/dist/test-monitor-types.d.ts.map +1 -0
  108. package/dist/test-monitor-types.js +2 -0
  109. package/dist/test-monitor-types.js.map +1 -0
  110. package/dist/test-plan-types.d.ts +43 -0
  111. package/dist/test-plan-types.d.ts.map +1 -0
  112. package/dist/test-plan-types.js +2 -0
  113. package/dist/test-plan-types.js.map +1 -0
  114. package/dist/types.d.ts +93 -0
  115. package/dist/types.d.ts.map +1 -0
  116. package/dist/types.js +3 -0
  117. package/dist/types.js.map +1 -0
  118. package/dist/utils/dates.d.ts +11 -0
  119. package/dist/utils/dates.d.ts.map +1 -0
  120. package/dist/utils/dates.js +13 -0
  121. package/dist/utils/dates.js.map +1 -0
  122. package/package.json +39 -0
  123. package/src/adapters/axios.ts +39 -0
  124. package/src/adapters/index.ts +2 -0
  125. package/src/adapters/stub.ts +47 -0
  126. package/src/events/adapters/README.md +144 -0
  127. package/src/events/adapters/in-memory.test.ts +146 -0
  128. package/src/events/adapters/in-memory.ts +93 -0
  129. package/src/events/adapters/index.ts +9 -0
  130. package/src/events/adapters/kinesis.test.ts +323 -0
  131. package/src/events/adapters/kinesis.ts +211 -0
  132. package/src/events/emitter.test.ts +327 -0
  133. package/src/events/emitter.ts +133 -0
  134. package/src/events/index.ts +3 -0
  135. package/src/events/types.ts +136 -0
  136. package/src/executor.test.ts +1732 -0
  137. package/src/executor.ts +1075 -0
  138. package/src/index.ts +81 -0
  139. package/src/secrets/factory.ts +248 -0
  140. package/src/secrets/index.ts +48 -0
  141. package/src/secrets/providers/aws.ts +178 -0
  142. package/src/secrets/providers/env.ts +66 -0
  143. package/src/secrets/providers/index.ts +15 -0
  144. package/src/secrets/providers/vault.ts +257 -0
  145. package/src/secrets/resolver.ts +269 -0
  146. package/src/secrets/secrets.test.ts +402 -0
  147. package/src/secrets/types.ts +106 -0
  148. package/src/shared.ts +46 -0
  149. package/src/test-monitor-types.ts +49 -0
  150. package/src/types.ts +114 -0
  151. package/src/utils/dates.ts +13 -0
  152. package/tsconfig.json +20 -0
  153. package/vitest.config.ts +14 -0
@@ -0,0 +1,1075 @@
1
+ import {
2
+ Assertion,
3
+ Assertions,
4
+ Node,
5
+ MonitorV1,
6
+ Wait,
7
+ HttpRequest,
8
+ } from "@griffin-app/griffin-hub-sdk";
9
+
10
+ import type { BinaryPredicate, UnaryPredicate } from "@griffin-app/griffin-core/types";
11
+ import type {
12
+ ExecutionOptions,
13
+ ExecutionResult,
14
+ NodeResult,
15
+ HttpRequestResult,
16
+ WaitResult,
17
+ JSONValue,
18
+ NodeResponseData,
19
+ } from "./types.js";
20
+ import { START, END } from "./types.js";
21
+ import { createStateGraph, graphStore, StateGraphRegistry } from "ts-edge";
22
+ import type { ExecutionEvent, BaseEvent } from "./events/index.js";
23
+ import { randomUUID } from "crypto";
24
+ import {
25
+ resolveSecretsInMonitor,
26
+ planHasSecrets,
27
+ SecretResolutionError,
28
+ } from "./secrets/index.js";
29
+ import { utcNow } from "./utils/dates.js";
30
+
31
+ // Define context type that matches ts-edge's GraphNodeExecuteContext (not exported from library)
32
+ interface NodeExecuteContext {
33
+ stream: (chunk: string) => void;
34
+ metadata: Record<string, unknown>;
35
+ }
36
+
37
+ /**
38
+ * Execution context that tracks event emission state throughout a monitor execution.
39
+ * Maintains sequence counter and provides event creation helpers.
40
+ */
41
+ class ExecutionContext {
42
+ private seq = 0;
43
+
44
+ constructor(
45
+ public readonly executionId: string,
46
+ public readonly monitor: MonitorV1,
47
+ public readonly organizationId: string,
48
+ private readonly emitter?: ExecutionOptions["eventEmitter"],
49
+ ) {}
50
+
51
+ /**
52
+ * Create base event properties with auto-incrementing sequence number.
53
+ */
54
+ private createBaseEvent(): BaseEvent {
55
+ return {
56
+ event_id: randomUUID(),
57
+ seq: this.seq++,
58
+ timestamp: Date.now(),
59
+ monitor_id: this.monitor.id,
60
+ execution_id: this.executionId,
61
+ organization_id: this.organizationId,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Emit an event (best-effort, non-blocking).
67
+ * Accepts an event object without base properties (those are added automatically).
68
+ */
69
+ emit(event: Partial<ExecutionEvent>): void {
70
+ if (!this.emitter) return;
71
+
72
+ const fullEvent = {
73
+ ...this.createBaseEvent(),
74
+ ...event,
75
+ } as ExecutionEvent;
76
+
77
+ try {
78
+ this.emitter.emit(fullEvent);
79
+ } catch (error) {
80
+ // Don't let event emission errors break execution
81
+ console.error("Error emitting event:", error);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Emit an error event with proper error formatting.
87
+ */
88
+ emitError(error: unknown, context?: string): void {
89
+ const errorName = error instanceof Error ? error.constructor.name : "Error";
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ const stack = error instanceof Error ? error.stack : undefined;
92
+
93
+ this.emit({
94
+ type: "ERROR",
95
+ error_name: errorName,
96
+ message,
97
+ context,
98
+ // Only include stack in local mode (best-effort detection)
99
+ ...(stack && { stack }),
100
+ });
101
+ }
102
+ }
103
+ // Dynamic state graph type for runtime-constructed graphs
104
+ // - ExecutionState: the shared state type
105
+ // - string: node names are arbitrary strings (not known at compile time)
106
+ // - never: no nodes are pre-marked as "connected" (having outgoing edges)
107
+ type DynamicStateGraph = StateGraphRegistry<ExecutionState, string, never>;
108
+
109
+ // State shared across all nodes during execution
110
+ interface ExecutionState {
111
+ responses: Record<string, NodeResponseData>;
112
+ results: NodeResult[];
113
+ errors: string[];
114
+ executionContext: ExecutionContext;
115
+ }
116
+
117
+ function buildNode(
118
+ monitor: MonitorV1,
119
+ node: Node,
120
+ options: ExecutionOptions,
121
+ ): {
122
+ name: string;
123
+ execute: (
124
+ state: ExecutionState,
125
+ context: NodeExecuteContext,
126
+ ) => Promise<ExecutionState>;
127
+ } {
128
+ switch (node.type) {
129
+ case "HTTP_REQUEST": {
130
+ return {
131
+ name: node.id,
132
+ execute: async (
133
+ state: ExecutionState,
134
+ tsEdgeContext: NodeExecuteContext,
135
+ ): Promise<ExecutionState> => {
136
+ const { responses, results, errors, executionContext } = state;
137
+ const nodeStartTime = Date.now();
138
+
139
+ // Emit NODE_START event
140
+ executionContext.emit({
141
+ type: "NODE_START",
142
+ node_id: node.id,
143
+ node_type: node.type,
144
+ });
145
+
146
+ // Handle NODE_STREAM events from ts-edge
147
+ const originalStream = tsEdgeContext.stream;
148
+ tsEdgeContext.stream = (chunk: string) => {
149
+ executionContext.emit({
150
+ type: "NODE_STREAM",
151
+ node_id: node.id,
152
+ chunk,
153
+ });
154
+ originalStream(chunk);
155
+ };
156
+
157
+ const result = await executeHttpRequest(
158
+ node.id,
159
+ node,
160
+ options,
161
+ executionContext,
162
+ );
163
+
164
+ // Store complete response data for downstream assertions
165
+ if (result.success && result.response !== undefined) {
166
+ responses[node.id] = {
167
+ body: result.response,
168
+ headers: result.headers || {},
169
+ status: result.status || 0,
170
+ duration_ms: result.duration_ms || 0,
171
+ };
172
+ }
173
+
174
+ // Record result in state
175
+ results.push({
176
+ nodeId: node.id,
177
+ success: result.success,
178
+ response: result.response,
179
+ error: result.error,
180
+ duration_ms: result.duration_ms,
181
+ });
182
+
183
+ // Track errors
184
+ if (!result.success && result.error) {
185
+ errors.push(`${node.id}: ${result.error}`);
186
+ }
187
+
188
+ // Emit NODE_END event
189
+ executionContext.emit({
190
+ type: "NODE_END",
191
+ node_id: node.id,
192
+ node_type: node.type,
193
+ success: result.success,
194
+ duration_ms: Date.now() - nodeStartTime,
195
+ error: result.error,
196
+ });
197
+
198
+ return { responses, results, errors, executionContext };
199
+ },
200
+ };
201
+ }
202
+ case "WAIT": {
203
+ return {
204
+ name: node.id,
205
+ execute: async (
206
+ state: ExecutionState,
207
+ tsEdgeContext: NodeExecuteContext,
208
+ ): Promise<ExecutionState> => {
209
+ const { responses, results, errors, executionContext } = state;
210
+ const nodeStartTime = Date.now();
211
+
212
+ // Emit NODE_START event
213
+ executionContext.emit({
214
+ type: "NODE_START",
215
+ node_id: node.id,
216
+ node_type: node.type,
217
+ });
218
+
219
+ const result = await executeWait(node.id, node, executionContext);
220
+
221
+ // Record result in state
222
+ results.push({
223
+ nodeId: node.id,
224
+ success: result.success,
225
+ duration_ms: result.duration_ms,
226
+ });
227
+
228
+ // Emit NODE_END event
229
+ executionContext.emit({
230
+ type: "NODE_END",
231
+ node_id: node.id,
232
+ node_type: node.type,
233
+ success: result.success,
234
+ duration_ms: Date.now() - nodeStartTime,
235
+ });
236
+
237
+ return { responses, results, errors, executionContext };
238
+ },
239
+ };
240
+ }
241
+ case "ASSERTION": {
242
+ return {
243
+ name: node.id,
244
+ execute: async (
245
+ state: ExecutionState,
246
+ tsEdgeContext: NodeExecuteContext,
247
+ ): Promise<ExecutionState> => {
248
+ const { responses, results, errors, executionContext } = state;
249
+ const nodeStartTime = Date.now();
250
+
251
+ // Emit NODE_START event
252
+ executionContext.emit({
253
+ type: "NODE_START",
254
+ node_id: node.id,
255
+ node_type: node.type,
256
+ });
257
+
258
+ const result = await executeAssertions(
259
+ node.id,
260
+ node,
261
+ responses,
262
+ executionContext,
263
+ );
264
+
265
+ // Record result in state
266
+ results.push(result);
267
+
268
+ // Track errors
269
+ if (!result.success && result.error) {
270
+ errors.push(`${node.id}: ${result.error}`);
271
+ }
272
+
273
+ // Emit NODE_END event
274
+ executionContext.emit({
275
+ type: "NODE_END",
276
+ node_id: node.id,
277
+ node_type: node.type,
278
+ success: result.success,
279
+ duration_ms: Date.now() - nodeStartTime,
280
+ error: result.error,
281
+ });
282
+
283
+ return { responses, results, errors, executionContext };
284
+ },
285
+ };
286
+ }
287
+ }
288
+ }
289
+
290
+ function buildGraph(
291
+ monitor: MonitorV1,
292
+ options: ExecutionOptions,
293
+ executionContext: ExecutionContext,
294
+ ): DynamicStateGraph {
295
+ // Create a state store for execution
296
+ const store = graphStore<ExecutionState>(() => ({
297
+ responses: {},
298
+ results: [],
299
+ errors: [],
300
+ executionContext,
301
+ }));
302
+
303
+ const graph: DynamicStateGraph = createStateGraph(store)
304
+ .addNode({
305
+ name: START,
306
+ execute: () => ({}),
307
+ })
308
+ .addNode({
309
+ name: END,
310
+ execute: () => ({}),
311
+ }) as DynamicStateGraph;
312
+
313
+ // Add all nodes - cast back to DynamicStateGraph to maintain our dynamic type
314
+ const graphWithNodes = monitor.nodes.reduce<DynamicStateGraph>(
315
+ (g, node) =>
316
+ g.addNode(buildNode(monitor, node, options)) as DynamicStateGraph,
317
+ graph,
318
+ );
319
+
320
+ // Add all edges
321
+ // Cast the edge method to accept string arguments since ts-edge expects literal types
322
+ // but we have runtime strings from the monitor
323
+ const graphWithEdges = monitor.edges.reduce<DynamicStateGraph>((g, edge) => {
324
+ const addEdge = g.edge as (from: string, to: string) => DynamicStateGraph;
325
+ return addEdge(edge.from, edge.to);
326
+ }, graphWithNodes);
327
+
328
+ return graphWithEdges;
329
+ }
330
+ export async function executeMonitorV1(
331
+ monitor: MonitorV1,
332
+ organizationId: string,
333
+ options: ExecutionOptions,
334
+ ): Promise<ExecutionResult> {
335
+ const startTime = Date.now();
336
+
337
+ // Generate or use provided executionId
338
+ const executionId = options.executionId || randomUUID();
339
+
340
+ // Create execution context for event emission
341
+ const executionContext = new ExecutionContext(
342
+ executionId,
343
+ monitor,
344
+ organizationId,
345
+ options.eventEmitter,
346
+ );
347
+
348
+ try {
349
+ // Resolve secrets if the monitor contains any
350
+ let resolvedMonitor = monitor;
351
+ if (planHasSecrets(monitor)) {
352
+ if (!options.secretProvider) {
353
+ throw new SecretResolutionError(
354
+ "Monitor contains secret references but no secret provider was provided",
355
+ { ref: "unknown" },
356
+ );
357
+ }
358
+
359
+ executionContext.emit({
360
+ type: "NODE_START",
361
+ node_id: "__SECRETS__",
362
+ node_type: "HTTP_REQUEST",
363
+ });
364
+
365
+ try {
366
+ resolvedMonitor = await resolveSecretsInMonitor(
367
+ monitor,
368
+ options.secretProvider,
369
+ );
370
+
371
+ executionContext.emit({
372
+ type: "NODE_END",
373
+ node_id: "__SECRETS__",
374
+ node_type: "HTTP_REQUEST",
375
+ success: true,
376
+ duration_ms: Date.now() - startTime,
377
+ });
378
+ } catch (error) {
379
+ executionContext.emit({
380
+ type: "NODE_END",
381
+ node_id: "__SECRETS__",
382
+ node_type: "HTTP_REQUEST",
383
+ success: false,
384
+ duration_ms: Date.now() - startTime,
385
+ error: error instanceof Error ? error.message : String(error),
386
+ });
387
+ throw error;
388
+ }
389
+ }
390
+
391
+ // Emit MONITOR_START event
392
+ executionContext.emit({
393
+ type: "MONITOR_START",
394
+ monitor_name: resolvedMonitor.name,
395
+ monitor_version: resolvedMonitor.version,
396
+ node_count: resolvedMonitor.nodes.length,
397
+ edge_count: resolvedMonitor.edges.length,
398
+ });
399
+
400
+ // Call onStart callback if provided
401
+ if (options.statusCallbacks?.onStart) {
402
+ try {
403
+ await options.statusCallbacks.onStart();
404
+ } catch (error) {
405
+ console.error("Error in onStart callback:", error);
406
+ // Don't fail execution due to callback errors
407
+ }
408
+ }
409
+
410
+ // Build execution graph (state-based)
411
+ const graph = buildGraph(resolvedMonitor, options, executionContext);
412
+
413
+ // Compile and run the state graph
414
+ const app = graph.compile(START, END);
415
+ const graphResult = await app.run();
416
+
417
+ // Extract final state - the output is the ExecutionState
418
+ if (!graphResult.isOk) {
419
+ const errorMessage = graphResult.error.message;
420
+ executionContext.emitError(graphResult.error, "graph_execution");
421
+
422
+ const finalResults = graphResult.output?.results || [];
423
+ const finalErrors = graphResult.output?.errors || [errorMessage];
424
+
425
+ // Emit MONITOR_END event
426
+ executionContext.emit({
427
+ type: "MONITOR_END",
428
+ success: false,
429
+ total_duration_ms: Date.now() - startTime,
430
+ node_result_count: finalResults.length,
431
+ error_count: finalErrors.length,
432
+ errors: finalErrors,
433
+ });
434
+
435
+ // Call onComplete callback if provided
436
+ if (options.statusCallbacks?.onComplete) {
437
+ try {
438
+ await options.statusCallbacks.onComplete({
439
+ status: "failed",
440
+ completedAt: utcNow(),
441
+ duration_ms: Date.now() - startTime,
442
+ success: false,
443
+ errors: finalErrors,
444
+ });
445
+ } catch (error) {
446
+ console.error("Error in onComplete callback:", error);
447
+ // Don't fail execution due to callback errors
448
+ }
449
+ }
450
+
451
+ // Flush events before returning
452
+ await options.eventEmitter?.flush?.();
453
+
454
+ return {
455
+ success: false,
456
+ results: finalResults,
457
+ errors: finalErrors,
458
+ totalDuration_ms: Date.now() - startTime,
459
+ };
460
+ }
461
+
462
+ const finalState = graphResult.output;
463
+ const success = finalState.errors.length === 0;
464
+ const duration = Date.now() - startTime;
465
+
466
+ // Emit MONITOR_END event
467
+ executionContext.emit({
468
+ type: "MONITOR_END",
469
+ success,
470
+ total_duration_ms: duration,
471
+ node_result_count: finalState.results.length,
472
+ error_count: finalState.errors.length,
473
+ errors: finalState.errors,
474
+ });
475
+
476
+ // Call onComplete callback if provided
477
+ if (options.statusCallbacks?.onComplete) {
478
+ try {
479
+ await options.statusCallbacks.onComplete({
480
+ status: success ? "completed" : "failed",
481
+ completedAt: utcNow(),
482
+ duration_ms: duration,
483
+ success,
484
+ ...(finalState.errors.length > 0 && { errors: finalState.errors }),
485
+ });
486
+ } catch (error) {
487
+ console.error("Error in onComplete callback:", error);
488
+ // Don't fail execution due to callback errors
489
+ }
490
+ }
491
+
492
+ // Flush events before returning
493
+ await options.eventEmitter?.flush?.();
494
+
495
+ return {
496
+ success,
497
+ results: finalState.results,
498
+ errors: finalState.errors,
499
+ totalDuration_ms: duration,
500
+ };
501
+ } catch (error: unknown) {
502
+ // Catch any unexpected errors
503
+ executionContext.emitError(error, "unexpected_error");
504
+
505
+ const errorMessage = error instanceof Error ? error.message : String(error);
506
+ const duration = Date.now() - startTime;
507
+
508
+ // Emit MONITOR_END event
509
+ executionContext.emit({
510
+ type: "MONITOR_END",
511
+ success: false,
512
+ total_duration_ms: duration,
513
+ node_result_count: 0,
514
+ error_count: 1,
515
+ errors: [errorMessage],
516
+ });
517
+
518
+ // Call onComplete callback if provided
519
+ if (options.statusCallbacks?.onComplete) {
520
+ try {
521
+ await options.statusCallbacks.onComplete({
522
+ status: "failed",
523
+ completedAt: utcNow(),
524
+ duration_ms: duration,
525
+ success: false,
526
+ errors: [errorMessage],
527
+ });
528
+ } catch (callbackError) {
529
+ console.error("Error in onComplete callback:", callbackError);
530
+ // Don't fail execution due to callback errors
531
+ }
532
+ }
533
+
534
+ // Flush events before throwing
535
+ await options.eventEmitter?.flush?.();
536
+
537
+ throw error;
538
+ }
539
+ }
540
+
541
+ async function executeHttpRequest(
542
+ nodeId: string,
543
+ endpoint: HttpRequest,
544
+ options: ExecutionOptions,
545
+ context: ExecutionContext,
546
+ ): Promise<HttpRequestResult> {
547
+ const startTime = Date.now();
548
+
549
+ // Only JSON response format is currently supported
550
+ if (endpoint.response_format !== "JSON") {
551
+ throw new Error(
552
+ `Unsupported response format: ${endpoint.response_format}. Only JSON is currently supported.`,
553
+ );
554
+ }
555
+
556
+ // endpoint.base and endpoint.path are already resolved strings
557
+ const baseUrl = endpoint.base;
558
+ const path = endpoint.path;
559
+ const url = `${baseUrl}${path}`;
560
+
561
+ // TODO: Add retry configuration from monitor (node-level or monitor-level)
562
+ // For now, we always attempt once (attempt: 1)
563
+ const attempt = 1;
564
+
565
+ // After secret resolution, headers are guaranteed to be plain strings
566
+ // Cast is safe because resolveSecretsInMonitor substitutes all SecretRefs
567
+ const resolvedHeaders = endpoint.headers as
568
+ | Record<string, string>
569
+ | undefined;
570
+
571
+ // Emit HTTP_REQUEST event before making the call
572
+ context.emit({
573
+ type: "HTTP_REQUEST",
574
+ node_id: nodeId,
575
+ attempt,
576
+ method: endpoint.method,
577
+ url: url,
578
+ headers: resolvedHeaders,
579
+ has_body: endpoint.body !== undefined,
580
+ });
581
+
582
+ try {
583
+ const response = await options.httpClient.request({
584
+ method: endpoint.method,
585
+ url,
586
+ headers: resolvedHeaders,
587
+ body: endpoint.body,
588
+ timeout: options.timeout || 30000,
589
+ });
590
+
591
+ const duration_ms = Date.now() - startTime;
592
+
593
+ // Emit HTTP_RESPONSE event after receiving response
594
+ context.emit({
595
+ type: "HTTP_RESPONSE",
596
+ node_id: nodeId,
597
+ attempt,
598
+ status: response.status,
599
+ status_text: response.statusText,
600
+ duration_ms,
601
+ has_body: response.data !== undefined,
602
+ });
603
+
604
+ // Parse JSON response if it's a string, otherwise use as-is
605
+ const parsedResponse: JSONValue =
606
+ typeof response.data === "string"
607
+ ? JSON.parse(response.data)
608
+ : response.data;
609
+
610
+ return {
611
+ success: true,
612
+ response: parsedResponse,
613
+ headers: response.headers || {},
614
+ status: response.status,
615
+ duration_ms,
616
+ };
617
+ } catch (error: unknown) {
618
+ const duration_ms = Date.now() - startTime;
619
+ const errorMessage = error instanceof Error ? error.message : String(error);
620
+ // Emit failed HTTP_RESPONSE event
621
+ context.emit({
622
+ type: "HTTP_RESPONSE",
623
+ node_id: nodeId,
624
+ attempt,
625
+ status: 0,
626
+ status_text: "Error",
627
+ duration_ms,
628
+ has_body: false,
629
+ });
630
+
631
+ return {
632
+ success: false,
633
+ error: errorMessage,
634
+ duration_ms,
635
+ };
636
+ }
637
+ }
638
+
639
+ async function executeWait(
640
+ nodeId: string,
641
+ waitNode: Wait,
642
+ context: ExecutionContext,
643
+ ): Promise<WaitResult> {
644
+ const startTime = Date.now();
645
+
646
+ context.emit({
647
+ type: "WAIT_START",
648
+ node_id: nodeId,
649
+ duration_ms: waitNode.duration_ms,
650
+ });
651
+
652
+ await new Promise((resolve) => setTimeout(resolve, waitNode.duration_ms));
653
+ return {
654
+ success: true,
655
+ duration_ms: Date.now() - startTime,
656
+ };
657
+ }
658
+
659
+ /**
660
+ * Extract value from response data using JSONPath
661
+ */
662
+ function extractJsonValue(json: JSONValue, path: string[]): unknown {
663
+ // TODO: Implement JSONPath extraction
664
+ // If no path, return the top-level object
665
+ if (!path || path.length === 0) {
666
+ return json;
667
+ }
668
+
669
+ let current: unknown = json;
670
+
671
+ for (const segment of path) {
672
+ if (typeof current === "object" && current !== null) {
673
+ if (Array.isArray(current)) {
674
+ // Try to interpret the segment as an array index
675
+ const idx = Number(segment);
676
+ if (!isNaN(idx) && idx >= 0 && idx < current.length) {
677
+ current = current[idx];
678
+ } else {
679
+ // If not a valid index, return undefined
680
+ return undefined;
681
+ }
682
+ } else {
683
+ // Plain object: access property by key
684
+ if (Object.prototype.hasOwnProperty.call(current, segment)) {
685
+ current = (current as Record<string, unknown>)[segment];
686
+ } else {
687
+ return undefined;
688
+ }
689
+ }
690
+ } else {
691
+ // Cannot traverse further - path goes too deep
692
+ return undefined;
693
+ }
694
+ }
695
+ return current;
696
+ }
697
+
698
+ function evaluateStatusAssertion(
699
+ assertion: { subject: "status"; predicate: BinaryPredicate; nodeId: string },
700
+ responses: Record<string, NodeResponseData>,
701
+ ) {
702
+ const { nodeId, predicate } = assertion;
703
+ const value = responses[nodeId].status;
704
+ return evaluateBinaryPredicate(value, predicate, `${nodeId}.status`);
705
+ }
706
+
707
+ function evaluateHeadersAssertion(
708
+ assertion: {
709
+ subject: "headers";
710
+ predicate: UnaryPredicate | BinaryPredicate;
711
+ nodeId: string;
712
+ headerName: string;
713
+ },
714
+ responses: Record<string, NodeResponseData>,
715
+ ) {
716
+ const { nodeId, headerName, predicate } = assertion;
717
+ const value = responses[nodeId].headers[headerName];
718
+ switch (predicate.type) {
719
+ case "unary":
720
+ return evaluateUnaryPredicate(
721
+ value,
722
+ predicate.operator,
723
+ `${nodeId}.headers.${headerName}`,
724
+ );
725
+ case "binary":
726
+ return evaluateBinaryPredicate(
727
+ value,
728
+ predicate,
729
+ `${nodeId}.headers.${headerName}`,
730
+ );
731
+ }
732
+ }
733
+ function evaluateLatencyAssertion(
734
+ assertion: { subject: "latency"; predicate: BinaryPredicate; nodeId: string },
735
+ responses: Record<string, NodeResponseData>,
736
+ ) {
737
+ const { nodeId, predicate } = assertion;
738
+ const value = responses[nodeId].duration_ms;
739
+ return evaluateBinaryPredicate(value, predicate, `${nodeId}.duration_ms`);
740
+ }
741
+ function evaluateBodyAssertion(
742
+ assertion: {
743
+ subject: "body";
744
+ predicate: UnaryPredicate | BinaryPredicate;
745
+ nodeId: string;
746
+ responseType: "JSON" | "XML";
747
+ path: string[];
748
+ },
749
+ responses: Record<string, NodeResponseData>,
750
+ ) {
751
+ const { nodeId, responseType, path, predicate } = assertion;
752
+ let value: unknown;
753
+ switch (responseType) {
754
+ case "JSON":
755
+ value = extractJsonValue(responses[nodeId].body, path);
756
+ break;
757
+ case "XML":
758
+ throw new Error(`XML assertions are not supported yet`);
759
+ }
760
+ switch (predicate.type) {
761
+ case "unary":
762
+ return evaluateUnaryPredicate(
763
+ value,
764
+ predicate.operator,
765
+ `${nodeId}.body.${path.join(".")}`,
766
+ );
767
+ case "binary":
768
+ return evaluateBinaryPredicate(
769
+ value,
770
+ predicate,
771
+ `${nodeId}.body.${path.join(".")}`,
772
+ );
773
+ }
774
+ }
775
+
776
+ function evaluateAssertion(
777
+ assertion: Assertion,
778
+ responses: Record<string, NodeResponseData>,
779
+ ): { passed: boolean; message: string } {
780
+ switch (assertion.subject) {
781
+ case "status":
782
+ return evaluateStatusAssertion(assertion, responses);
783
+ case "headers":
784
+ return evaluateHeadersAssertion(assertion, responses);
785
+ case "latency":
786
+ return evaluateLatencyAssertion(assertion, responses);
787
+ case "body":
788
+ return evaluateBodyAssertion(assertion, responses);
789
+ }
790
+ }
791
+
792
+
793
+ /**
794
+ * Evaluate unary predicates
795
+ */
796
+ function evaluateUnaryPredicate(
797
+ value: unknown,
798
+ predicate:
799
+ | "IS_NULL"
800
+ | "IS_NOT_NULL"
801
+ | "IS_TRUE"
802
+ | "IS_FALSE"
803
+ | "IS_EMPTY"
804
+ | "IS_NOT_EMPTY",
805
+ prefix: string,
806
+ //nodeId: string,
807
+ //accessor: string,
808
+ //pathStr: string,
809
+ ): { passed: boolean; message: string } {
810
+ switch (predicate) {
811
+ case "IS_NULL":
812
+ return {
813
+ passed: value === null,
814
+ message:
815
+ value === null
816
+ ? `${prefix} is null`
817
+ : `Expected ${prefix} to be null, got ${JSON.stringify(value)}`,
818
+ };
819
+
820
+ case "IS_NOT_NULL":
821
+ return {
822
+ passed: value !== null && value !== undefined,
823
+ message:
824
+ value !== null && value !== undefined
825
+ ? `${prefix} is not null`
826
+ : `Expected ${prefix} to not be null`,
827
+ };
828
+
829
+ case "IS_TRUE":
830
+ return {
831
+ passed: value === true,
832
+ message:
833
+ value === true
834
+ ? `${prefix} is true`
835
+ : `Expected ${prefix} to be true, got ${JSON.stringify(value)}`,
836
+ };
837
+
838
+ case "IS_FALSE":
839
+ return {
840
+ passed: value === false,
841
+ message:
842
+ value === false
843
+ ? `${prefix} is false`
844
+ : `Expected ${prefix} to be false, got ${JSON.stringify(value)}`,
845
+ };
846
+
847
+ case "IS_EMPTY": {
848
+ const isEmpty =
849
+ value === "" ||
850
+ (Array.isArray(value) && value.length === 0) ||
851
+ (typeof value === "object" &&
852
+ value !== null &&
853
+ Object.keys(value).length === 0);
854
+ return {
855
+ passed: isEmpty,
856
+ message: isEmpty
857
+ ? `${prefix} is empty`
858
+ : `Expected ${prefix} to be empty, got ${JSON.stringify(value)}`,
859
+ };
860
+ }
861
+
862
+ case "IS_NOT_EMPTY": {
863
+ const isNotEmpty =
864
+ value !== "" &&
865
+ !(Array.isArray(value) && value.length === 0) &&
866
+ !(
867
+ typeof value === "object" &&
868
+ value !== null &&
869
+ Object.keys(value).length === 0
870
+ );
871
+ return {
872
+ passed: isNotEmpty,
873
+ message: isNotEmpty
874
+ ? `${prefix} is not empty`
875
+ : `Expected ${prefix} to not be empty`,
876
+ };
877
+ }
878
+
879
+ default:
880
+ throw new Error(`Unknown unary predicate: ${predicate}`);
881
+ }
882
+ }
883
+
884
+ /**
885
+ * Evaluate binary predicates
886
+ */
887
+ function evaluateBinaryPredicate(
888
+ value: unknown,
889
+ predicate: BinaryPredicate,
890
+ //nodeId: string,
891
+ //accessor: string,
892
+ //pathStr: string,
893
+ prefix: string,
894
+ ): { passed: boolean; message: string } {
895
+ const { operator, expected } = predicate;
896
+
897
+ switch (operator) {
898
+ case "EQUAL": {
899
+ const isEqual = JSON.stringify(value) === JSON.stringify(expected);
900
+ return {
901
+ passed: isEqual,
902
+ message: isEqual
903
+ ? `${prefix} equals ${JSON.stringify(expected)}`
904
+ : `Expected ${prefix} to equal ${JSON.stringify(expected)}, got ${JSON.stringify(value)}`,
905
+ };
906
+ }
907
+
908
+ case "NOT_EQUAL": {
909
+ const isNotEqual = JSON.stringify(value) !== JSON.stringify(expected);
910
+ return {
911
+ passed: isNotEqual,
912
+ message: isNotEqual
913
+ ? `${prefix} does not equal ${JSON.stringify(expected)}`
914
+ : `Expected ${prefix} to not equal ${JSON.stringify(expected)}`,
915
+ };
916
+ }
917
+
918
+ case "GREATER_THAN": {
919
+ const isGT = typeof value === "number" && value > (expected as number);
920
+ return {
921
+ passed: isGT,
922
+ message: isGT
923
+ ? `${prefix} (${value}) > ${expected}`
924
+ : `Expected ${prefix} to be greater than ${expected}, got ${JSON.stringify(value)}`,
925
+ };
926
+ }
927
+
928
+ case "LESS_THAN": {
929
+ const isLT = typeof value === "number" && value < (expected as number);
930
+ return {
931
+ passed: isLT,
932
+ message: isLT
933
+ ? `${prefix} (${value}) < ${expected}`
934
+ : `Expected ${prefix} to be less than ${expected}, got ${JSON.stringify(value)}`,
935
+ };
936
+ }
937
+
938
+ case "GREATER_THAN_OR_EQUAL": {
939
+ const isGTE = typeof value === "number" && value >= (expected as number);
940
+ return {
941
+ passed: isGTE,
942
+ message: isGTE
943
+ ? `${prefix} (${value}) >= ${expected}`
944
+ : `Expected ${prefix} to be >= ${expected}, got ${JSON.stringify(value)}`,
945
+ };
946
+ }
947
+
948
+ case "LESS_THAN_OR_EQUAL": {
949
+ const isLTE = typeof value === "number" && value <= (expected as number);
950
+ return {
951
+ passed: isLTE,
952
+ message: isLTE
953
+ ? `${prefix} (${value}) <= ${expected}`
954
+ : `Expected ${prefix} to be <= ${expected}, got ${JSON.stringify(value)}`,
955
+ };
956
+ }
957
+
958
+ case "CONTAINS": {
959
+ const contains =
960
+ typeof value === "string" && value.includes(expected as string);
961
+ return {
962
+ passed: contains,
963
+ message: contains
964
+ ? `${prefix} contains "${expected}"`
965
+ : `Expected ${prefix} to contain "${expected}", got "${value}"`,
966
+ };
967
+ }
968
+
969
+ case "NOT_CONTAINS": {
970
+ const notContains =
971
+ typeof value === "string" && !value.includes(expected as string);
972
+ return {
973
+ passed: notContains,
974
+ message: notContains
975
+ ? `${prefix} does not contain "${expected}"`
976
+ : `Expected ${prefix} to not contain "${expected}", got "${value}"`,
977
+ };
978
+ }
979
+
980
+ case "STARTS_WITH": {
981
+ const startsWith =
982
+ typeof value === "string" && value.startsWith(expected as string);
983
+ return {
984
+ passed: startsWith,
985
+ message: startsWith
986
+ ? `${prefix} starts with "${expected}"`
987
+ : `Expected ${prefix} to start with "${expected}", got "${value}"`,
988
+ };
989
+ }
990
+
991
+ case "NOT_STARTS_WITH": {
992
+ const notStartsWith =
993
+ typeof value === "string" && !value.startsWith(expected as string);
994
+ return {
995
+ passed: notStartsWith,
996
+ message: notStartsWith
997
+ ? `${prefix} does not start with "${expected}"`
998
+ : `Expected ${prefix} to not start with "${expected}", got "${value}"`,
999
+ };
1000
+ }
1001
+
1002
+ case "ENDS_WITH": {
1003
+ const endsWith =
1004
+ typeof value === "string" && value.endsWith(expected as string);
1005
+ return {
1006
+ passed: endsWith,
1007
+ message: endsWith
1008
+ ? `${prefix} ends with "${expected}"`
1009
+ : `Expected ${prefix} to end with "${expected}", got "${value}"`,
1010
+ };
1011
+ }
1012
+
1013
+ case "NOT_ENDS_WITH": {
1014
+ const notEndsWith =
1015
+ typeof value === "string" && !value.endsWith(expected as string);
1016
+ return {
1017
+ passed: notEndsWith,
1018
+ message: notEndsWith
1019
+ ? `${prefix} does not end with "${expected}"`
1020
+ : `Expected ${prefix} to not end with "${expected}", got "${value}"`,
1021
+ };
1022
+ }
1023
+
1024
+ default:
1025
+ throw new Error(`Unknown binary predicate operator: ${operator}`);
1026
+ }
1027
+ }
1028
+
1029
+ async function executeAssertions(
1030
+ nodeId: string,
1031
+ assertionNode: Assertions,
1032
+ responses: Record<string, NodeResponseData>,
1033
+ context: ExecutionContext,
1034
+ ): Promise<NodeResult> {
1035
+ const startTime = Date.now();
1036
+ const errors: string[] = [];
1037
+
1038
+ for (let i = 0; i < assertionNode.assertions.length; i++) {
1039
+ const assertion = assertionNode.assertions[i];
1040
+
1041
+ try {
1042
+ const result = evaluateAssertion(assertion, responses);
1043
+
1044
+ context.emit({
1045
+ type: "ASSERTION_RESULT",
1046
+ node_id: nodeId,
1047
+ assertion_index: i,
1048
+ passed: result.passed,
1049
+ message: result.message,
1050
+ });
1051
+
1052
+ if (!result.passed) {
1053
+ errors.push(result.message);
1054
+ }
1055
+ } catch (error: unknown) {
1056
+ const message = error instanceof Error ? error.message : String(error);
1057
+ errors.push(`Assertion ${i} failed: ${message}`);
1058
+
1059
+ context.emit({
1060
+ type: "ASSERTION_RESULT",
1061
+ node_id: nodeId,
1062
+ assertion_index: i,
1063
+ passed: false,
1064
+ message,
1065
+ });
1066
+ }
1067
+ }
1068
+
1069
+ return {
1070
+ nodeId,
1071
+ success: errors.length === 0,
1072
+ error: errors.length > 0 ? errors.join("; ") : undefined,
1073
+ duration_ms: Date.now() - startTime,
1074
+ };
1075
+ }