@outputai/core 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 (114) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/bin/healthcheck.mjs +36 -0
  4. package/bin/healthcheck.spec.js +90 -0
  5. package/bin/worker.sh +26 -0
  6. package/package.json +67 -0
  7. package/src/activity_integration/context.d.ts +27 -0
  8. package/src/activity_integration/context.js +17 -0
  9. package/src/activity_integration/context.spec.js +42 -0
  10. package/src/activity_integration/events.d.ts +7 -0
  11. package/src/activity_integration/events.js +10 -0
  12. package/src/activity_integration/index.d.ts +9 -0
  13. package/src/activity_integration/index.js +3 -0
  14. package/src/activity_integration/tracing.d.ts +32 -0
  15. package/src/activity_integration/tracing.js +37 -0
  16. package/src/async_storage.js +19 -0
  17. package/src/bus.js +3 -0
  18. package/src/consts.js +32 -0
  19. package/src/errors.d.ts +15 -0
  20. package/src/errors.js +14 -0
  21. package/src/hooks/index.d.ts +28 -0
  22. package/src/hooks/index.js +32 -0
  23. package/src/index.d.ts +49 -0
  24. package/src/index.js +4 -0
  25. package/src/interface/evaluation_result.d.ts +173 -0
  26. package/src/interface/evaluation_result.js +215 -0
  27. package/src/interface/evaluator.d.ts +70 -0
  28. package/src/interface/evaluator.js +34 -0
  29. package/src/interface/evaluator.spec.js +565 -0
  30. package/src/interface/index.d.ts +9 -0
  31. package/src/interface/index.js +26 -0
  32. package/src/interface/step.d.ts +138 -0
  33. package/src/interface/step.js +22 -0
  34. package/src/interface/types.d.ts +27 -0
  35. package/src/interface/validations/runtime.js +20 -0
  36. package/src/interface/validations/runtime.spec.js +29 -0
  37. package/src/interface/validations/schema_utils.js +8 -0
  38. package/src/interface/validations/schema_utils.spec.js +67 -0
  39. package/src/interface/validations/static.js +136 -0
  40. package/src/interface/validations/static.spec.js +366 -0
  41. package/src/interface/webhook.d.ts +84 -0
  42. package/src/interface/webhook.js +64 -0
  43. package/src/interface/webhook.spec.js +122 -0
  44. package/src/interface/workflow.d.ts +273 -0
  45. package/src/interface/workflow.js +128 -0
  46. package/src/interface/workflow.spec.js +467 -0
  47. package/src/interface/workflow_context.js +31 -0
  48. package/src/interface/workflow_utils.d.ts +76 -0
  49. package/src/interface/workflow_utils.js +50 -0
  50. package/src/interface/workflow_utils.spec.js +190 -0
  51. package/src/interface/zod_integration.spec.js +646 -0
  52. package/src/internal_activities/index.js +66 -0
  53. package/src/internal_activities/index.spec.js +102 -0
  54. package/src/logger.js +73 -0
  55. package/src/tracing/internal_interface.js +71 -0
  56. package/src/tracing/processors/local/index.js +111 -0
  57. package/src/tracing/processors/local/index.spec.js +149 -0
  58. package/src/tracing/processors/s3/configs.js +31 -0
  59. package/src/tracing/processors/s3/configs.spec.js +64 -0
  60. package/src/tracing/processors/s3/index.js +114 -0
  61. package/src/tracing/processors/s3/index.spec.js +153 -0
  62. package/src/tracing/processors/s3/redis_client.js +62 -0
  63. package/src/tracing/processors/s3/redis_client.spec.js +185 -0
  64. package/src/tracing/processors/s3/s3_client.js +27 -0
  65. package/src/tracing/processors/s3/s3_client.spec.js +62 -0
  66. package/src/tracing/tools/build_trace_tree.js +83 -0
  67. package/src/tracing/tools/build_trace_tree.spec.js +135 -0
  68. package/src/tracing/tools/utils.js +21 -0
  69. package/src/tracing/tools/utils.spec.js +14 -0
  70. package/src/tracing/trace_engine.js +97 -0
  71. package/src/tracing/trace_engine.spec.js +199 -0
  72. package/src/utils/index.d.ts +134 -0
  73. package/src/utils/index.js +2 -0
  74. package/src/utils/resolve_invocation_dir.js +34 -0
  75. package/src/utils/resolve_invocation_dir.spec.js +102 -0
  76. package/src/utils/utils.js +211 -0
  77. package/src/utils/utils.spec.js +448 -0
  78. package/src/worker/bundler_options.js +43 -0
  79. package/src/worker/catalog_workflow/catalog.js +114 -0
  80. package/src/worker/catalog_workflow/index.js +54 -0
  81. package/src/worker/catalog_workflow/index.spec.js +196 -0
  82. package/src/worker/catalog_workflow/workflow.js +24 -0
  83. package/src/worker/configs.js +49 -0
  84. package/src/worker/configs.spec.js +130 -0
  85. package/src/worker/index.js +89 -0
  86. package/src/worker/index.spec.js +177 -0
  87. package/src/worker/interceptors/activity.js +62 -0
  88. package/src/worker/interceptors/activity.spec.js +212 -0
  89. package/src/worker/interceptors/workflow.js +70 -0
  90. package/src/worker/interceptors/workflow.spec.js +167 -0
  91. package/src/worker/interceptors.js +10 -0
  92. package/src/worker/loader.js +151 -0
  93. package/src/worker/loader.spec.js +236 -0
  94. package/src/worker/loader_tools.js +132 -0
  95. package/src/worker/loader_tools.spec.js +156 -0
  96. package/src/worker/log_hooks.js +95 -0
  97. package/src/worker/log_hooks.spec.js +217 -0
  98. package/src/worker/sandboxed_utils.js +18 -0
  99. package/src/worker/shutdown.js +26 -0
  100. package/src/worker/shutdown.spec.js +82 -0
  101. package/src/worker/sinks.js +74 -0
  102. package/src/worker/start_catalog.js +36 -0
  103. package/src/worker/start_catalog.spec.js +118 -0
  104. package/src/worker/webpack_loaders/consts.js +9 -0
  105. package/src/worker/webpack_loaders/tools.js +548 -0
  106. package/src/worker/webpack_loaders/tools.spec.js +330 -0
  107. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
  108. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
  109. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
  110. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
  111. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
  112. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
  113. package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
  114. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +613 -0
@@ -0,0 +1,366 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { z } from 'zod';
3
+ import {
4
+ validateStep,
5
+ validateWorkflow,
6
+ validateRequestPayload,
7
+ validateEvaluator,
8
+ validateExecuteInParallel,
9
+ StaticValidationError
10
+ } from './static.js';
11
+
12
+ const validArgs = Object.freeze( {
13
+ name: 'valid_name',
14
+ description: 'desc',
15
+ inputSchema: z.object( {} ),
16
+ outputSchema: z.object( {} ),
17
+ fn: () => {}
18
+ } );
19
+
20
+ describe( 'interface/validator', () => {
21
+ describe( 'validateStep', () => {
22
+ it( 'passes for valid args', () => {
23
+ expect( () => validateStep( { ...validArgs } ) ).not.toThrow();
24
+ } );
25
+
26
+ it( 'rejects missing name', () => {
27
+ const error = new StaticValidationError( '✖ Invalid input: expected string, received undefined\n → at name' );
28
+ expect( () => validateStep( { ...validArgs, name: undefined } ) ).toThrow( error );
29
+ } );
30
+
31
+ it( 'rejects non-string name', () => {
32
+ const error = new StaticValidationError( '✖ Invalid input: expected string, received number\n → at name' );
33
+ expect( () => validateStep( { ...validArgs, name: 123 } ) ).toThrow( error );
34
+ } );
35
+
36
+ it( 'rejects invalid name pattern', () => {
37
+ const error = new StaticValidationError( '✖ Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/i\n → at name' );
38
+ expect( () => validateStep( { ...validArgs, name: '-bad' } ) ).toThrow( error );
39
+ } );
40
+
41
+ it( 'rejects non-string description', () => {
42
+ const error = new StaticValidationError( '✖ Invalid input: expected string, received number\n → at description' );
43
+ expect( () => validateStep( { ...validArgs, description: 10 } ) ).toThrow( error );
44
+ } );
45
+
46
+ it( 'rejects non-Zod inputSchema', () => {
47
+ const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at inputSchema' );
48
+ expect( () => validateStep( { ...validArgs, inputSchema: 'not-a-zod-schema' } ) ).toThrow( error );
49
+ } );
50
+
51
+ it( 'rejects JSON Schema inputSchema', () => {
52
+ const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at inputSchema' );
53
+ expect( () => validateStep( { ...validArgs, inputSchema: { type: 'object' } } ) ).toThrow( error );
54
+ } );
55
+
56
+ it( 'rejects non-Zod outputSchema', () => {
57
+ const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at outputSchema' );
58
+ expect( () => validateStep( { ...validArgs, outputSchema: 10 } ) ).toThrow( error );
59
+ } );
60
+
61
+ it( 'rejects JSON Schema outputSchema', () => {
62
+ const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at outputSchema' );
63
+ expect( () => validateStep( { ...validArgs, outputSchema: { type: 'string' } } ) ).toThrow( error );
64
+ } );
65
+
66
+ it( 'rejects missing fn', () => {
67
+ const error = new StaticValidationError( '✖ Invalid input: expected function, received undefined\n → at fn' );
68
+ expect( () => validateStep( { ...validArgs, fn: undefined } ) ).toThrow( error );
69
+ } );
70
+
71
+ it( 'rejects non-function fn', () => {
72
+ const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at fn' );
73
+ expect( () => validateStep( { ...validArgs, fn: 'not-fn' } ) ).toThrow( error );
74
+ } );
75
+
76
+ it( 'passes with options.activityOptions.retry (second-level options)', () => {
77
+ const args = {
78
+ ...validArgs,
79
+ options: {
80
+ activityOptions: {
81
+ retry: {
82
+ initialInterval: '1s',
83
+ backoffCoefficient: 2,
84
+ maximumInterval: '10s',
85
+ maximumAttempts: 3,
86
+ nonRetryableErrorTypes: [ 'SomeError' ]
87
+ }
88
+ }
89
+ }
90
+ };
91
+ expect( () => validateStep( args ) ).not.toThrow();
92
+ } );
93
+
94
+ it( 'passes with options.activityOptions.activityId as string', () => {
95
+ expect( () => validateStep( { ...validArgs, options: { activityOptions: { activityId: 'act-123' } } } ) ).not.toThrow();
96
+ } );
97
+
98
+ it( 'rejects non-string options.activityOptions.activityId', () => {
99
+ expect( () => validateStep( { ...validArgs, options: { activityOptions: { activityId: 123 } } } ) ).toThrow( StaticValidationError );
100
+ } );
101
+
102
+ it( 'passes with valid options.activityOptions.cancellationType values', () => {
103
+ for ( const v of [ 'TRY_CANCEL', 'WAIT_CANCELLATION_COMPLETED', 'ABANDON' ] ) {
104
+ expect( () => validateStep( { ...validArgs, options: { activityOptions: { cancellationType: v } } } ) ).not.toThrow();
105
+ }
106
+ } );
107
+
108
+ it( 'rejects invalid options.activityOptions.cancellationType', () => {
109
+ const args = { ...validArgs, options: { activityOptions: { cancellationType: 'INVALID' } } };
110
+ expect( () => validateStep( args ) ).toThrow( StaticValidationError );
111
+ } );
112
+
113
+ it( 'accepts duration fields in options.activityOptions', () => {
114
+ const options = {
115
+ activityOptions: {
116
+ heartbeatTimeout: '1s',
117
+ scheduleToCloseTimeout: '2m',
118
+ scheduleToStartTimeout: '3m',
119
+ startToCloseTimeout: '4m'
120
+ }
121
+ };
122
+ expect( () => validateStep( { ...validArgs, options } ) ).not.toThrow();
123
+ } );
124
+
125
+ it( 'rejects invalid duration string in options.activityOptions.heartbeatTimeout', () => {
126
+ expect( () => validateStep( { ...validArgs, options: { activityOptions: { heartbeatTimeout: '5x' } } } ) ).toThrow( StaticValidationError );
127
+ } );
128
+
129
+ it( 'passes with options.activityOptions.summary string', () => {
130
+ expect( () => validateStep( { ...validArgs, options: { activityOptions: { summary: 'brief' } } } ) ).not.toThrow();
131
+ } );
132
+
133
+ it( 'rejects non-string options.activityOptions.summary', () => {
134
+ expect( () => validateStep( { ...validArgs, options: { activityOptions: { summary: 42 } } } ) ).toThrow( StaticValidationError );
135
+ } );
136
+
137
+ it( 'passes with options.activityOptions.priority valid payload', () => {
138
+ const options = {
139
+ activityOptions: {
140
+ priority: {
141
+ fairnessKey: 'user-1',
142
+ fairnessWeight: 1.5,
143
+ priorityKey: 10
144
+ }
145
+ }
146
+ };
147
+ expect( () => validateStep( { ...validArgs, options } ) ).not.toThrow();
148
+ } );
149
+
150
+ it( 'rejects invalid options.activityOptions.priority values', () => {
151
+ const options = { activityOptions: { priority: { fairnessWeight: 0, priorityKey: 0 } } };
152
+ expect( () => validateStep( { ...validArgs, options } ) ).toThrow( StaticValidationError );
153
+ } );
154
+
155
+ it( 'rejects invalid options.activityOptions.retry values', () => {
156
+ const options = { activityOptions: { retry: { backoffCoefficient: 0.5, maximumAttempts: 0, nonRetryableErrorTypes: [ 1 ] } } };
157
+ expect( () => validateStep( { ...validArgs, options } ) ).toThrow( StaticValidationError );
158
+ } );
159
+
160
+ it( 'rejects unknown keys inside options.activityOptions due to strictObject', () => {
161
+ expect( () => validateStep( { ...validArgs, options: { activityOptions: { unknownKey: true } } } ) ).toThrow( StaticValidationError );
162
+ } );
163
+
164
+ it( 'rejects unknown top-level keys due to strictObject', () => {
165
+ expect( () => validateStep( { ...validArgs, extra: 123 } ) ).toThrow( StaticValidationError );
166
+ } );
167
+ } );
168
+
169
+ describe( 'validateWorkflow', () => {
170
+ it( 'passes for valid args', () => {
171
+ expect( () => validateWorkflow( { ...validArgs } ) ).not.toThrow();
172
+ } );
173
+
174
+ it( 'passes with options.disableTrace true', () => {
175
+ expect( () => validateWorkflow( { ...validArgs, options: { disableTrace: true } } ) ).not.toThrow();
176
+ } );
177
+
178
+ it( 'passes with options.disableTrace false', () => {
179
+ expect( () => validateWorkflow( { ...validArgs, options: { disableTrace: false } } ) ).not.toThrow();
180
+ } );
181
+
182
+ it( 'passes with options.activityOptions and options.disableTrace', () => {
183
+ expect( () => validateWorkflow( {
184
+ ...validArgs,
185
+ options: { activityOptions: { activityId: 'wf-1' }, disableTrace: true }
186
+ } ) ).not.toThrow();
187
+ } );
188
+
189
+ it( 'rejects non-boolean options.disableTrace', () => {
190
+ expect( () => validateWorkflow( { ...validArgs, options: { disableTrace: 'yes' } } ) ).toThrow( StaticValidationError );
191
+ } );
192
+ } );
193
+
194
+ describe( 'validateEvaluator', () => {
195
+ const base = Object.freeze( {
196
+ name: 'valid_name',
197
+ description: 'desc',
198
+ inputSchema: z.object( {} ),
199
+ fn: () => {}
200
+ } );
201
+
202
+ it( 'passes for valid args (no outputSchema)', () => {
203
+ expect( () => validateEvaluator( { ...base } ) ).not.toThrow();
204
+ } );
205
+
206
+ it( 'rejects invalid name pattern', () => {
207
+ const error = new StaticValidationError( '✖ Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/i\n → at name' );
208
+ expect( () => validateEvaluator( { ...base, name: '-bad' } ) ).toThrow( error );
209
+ } );
210
+
211
+ it( 'rejects non-Zod inputSchema', () => {
212
+ const error = new StaticValidationError( '✖ Schema must be a Zod schema\n → at inputSchema' );
213
+ expect( () => validateEvaluator( { ...base, inputSchema: 'not-a-zod-schema' } ) ).toThrow( error );
214
+ } );
215
+
216
+ it( 'rejects missing fn', () => {
217
+ const error = new StaticValidationError( '✖ Invalid input: expected function, received undefined\n → at fn' );
218
+ expect( () => validateEvaluator( { ...base, fn: undefined } ) ).toThrow( error );
219
+ } );
220
+ } );
221
+
222
+ describe( 'validate request', () => {
223
+ it( 'passes with valid http url', () => {
224
+ expect( () => validateRequestPayload( { url: 'http://example.com', method: 'GET' } ) ).not.toThrow();
225
+ } );
226
+
227
+ it( 'passes with valid https url', () => {
228
+ expect( () => validateRequestPayload( { url: 'https://example.com/path?q=1', method: 'GET' } ) ).not.toThrow();
229
+ } );
230
+
231
+ it( 'rejects missing url', () => {
232
+ const error = new StaticValidationError( '✖ Invalid input: expected string, received undefined\n → at url' );
233
+ expect( () => validateRequestPayload( { method: 'GET' } ) ).toThrow( error );
234
+ } );
235
+
236
+ it( 'rejects invalid scheme', () => {
237
+ const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
238
+ expect( () => validateRequestPayload( { url: 'ftp://example.com', method: 'GET' } ) ).toThrow( error );
239
+ } );
240
+
241
+ it( 'rejects malformed url', () => {
242
+ const error = new StaticValidationError( '✖ Invalid URL\n → at url' );
243
+ expect( () => validateRequestPayload( { url: 'http:////', method: 'GET' } ) ).toThrow( error );
244
+ } );
245
+
246
+ it( 'rejects missing method', () => {
247
+ expect( () => validateRequestPayload( { url: 'https://example.com' } ) ).toThrow( StaticValidationError );
248
+ } );
249
+
250
+ it( 'passes with headers as string map', () => {
251
+ const request = {
252
+ url: 'https://example.com',
253
+ method: 'GET',
254
+ headers: { 'x-api-key': 'abc', accept: 'application/json' }
255
+ };
256
+ expect( () => validateRequestPayload( request ) ).not.toThrow();
257
+ } );
258
+
259
+ it( 'rejects non-object headers', () => {
260
+ const request = {
261
+ url: 'https://example.com',
262
+ method: 'GET',
263
+ headers: 5
264
+ };
265
+ expect( () => validateRequestPayload( request ) ).toThrow( StaticValidationError );
266
+ } );
267
+
268
+ it( 'rejects headers with non-string values', () => {
269
+ const request = {
270
+ url: 'https://example.com',
271
+ method: 'GET',
272
+ headers: { 'x-num': 123 }
273
+ };
274
+ expect( () => validateRequestPayload( request ) ).toThrow( StaticValidationError );
275
+ } );
276
+
277
+ it( 'passes with payload object', () => {
278
+ const request = {
279
+ url: 'https://example.com/api',
280
+ method: 'POST',
281
+ payload: { a: 1, b: 'two' }
282
+ };
283
+ expect( () => validateRequestPayload( request ) ).not.toThrow();
284
+ } );
285
+
286
+ it( 'passes with payload string', () => {
287
+ const request = {
288
+ url: 'https://example.com/upload',
289
+ method: 'POST',
290
+ payload: 'raw-body'
291
+ };
292
+ expect( () => validateRequestPayload( request ) ).not.toThrow();
293
+ } );
294
+ } );
295
+
296
+ describe( 'validateExecuteInParallel', () => {
297
+ const validArgs = Object.freeze( {
298
+ jobs: [ () => {}, () => {} ],
299
+ concurrency: 5
300
+ } );
301
+
302
+ it( 'passes for valid args', () => {
303
+ expect( () => validateExecuteInParallel( { ...validArgs } ) ).not.toThrow();
304
+ } );
305
+
306
+ it( 'rejects missing concurrency', () => {
307
+ const error = new StaticValidationError( '✖ Invalid input\n → at concurrency' );
308
+ expect( () => validateExecuteInParallel( { jobs: validArgs.jobs } ) ).toThrow( error );
309
+ } );
310
+
311
+ it( 'passes with onJobCompleted callback', () => {
312
+ const args = {
313
+ ...validArgs,
314
+ onJobCompleted: () => {}
315
+ };
316
+ expect( () => validateExecuteInParallel( args ) ).not.toThrow();
317
+ } );
318
+
319
+ it( 'passes with concurrency 1', () => {
320
+ expect( () => validateExecuteInParallel( { ...validArgs, concurrency: 1 } ) ).not.toThrow();
321
+ } );
322
+
323
+ it( 'passes with concurrency Infinity', () => {
324
+ expect( () => validateExecuteInParallel( { ...validArgs, concurrency: Infinity } ) ).not.toThrow();
325
+ } );
326
+
327
+ it( 'rejects missing jobs', () => {
328
+ const error = new StaticValidationError( '✖ Invalid input: expected array, received undefined\n → at jobs' );
329
+ expect( () => validateExecuteInParallel( { concurrency: 5 } ) ).toThrow( error );
330
+ } );
331
+
332
+ it( 'rejects non-array jobs', () => {
333
+ const error = new StaticValidationError( '✖ Invalid input: expected array, received string\n → at jobs' );
334
+ expect( () => validateExecuteInParallel( { jobs: 'not-array', concurrency: 5 } ) ).toThrow( error );
335
+ } );
336
+
337
+ it( 'passes with empty jobs array', () => {
338
+ expect( () => validateExecuteInParallel( { jobs: [], concurrency: 5 } ) ).not.toThrow();
339
+ } );
340
+
341
+ it( 'rejects jobs array with non-function', () => {
342
+ const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at jobs[1]' );
343
+ expect( () => validateExecuteInParallel( { jobs: [ () => {}, 'not-function' ], concurrency: 5 } ) ).toThrow( error );
344
+ } );
345
+
346
+ it( 'rejects non-number concurrency', () => {
347
+ const error = new StaticValidationError( '✖ Invalid input\n → at concurrency' );
348
+ expect( () => validateExecuteInParallel( { jobs: validArgs.jobs, concurrency: '5' } ) ).toThrow( error );
349
+ } );
350
+
351
+ it( 'rejects zero concurrency', () => {
352
+ const error = new StaticValidationError( '✖ Too small: expected number to be >=1\n → at concurrency' );
353
+ expect( () => validateExecuteInParallel( { jobs: validArgs.jobs, concurrency: 0 } ) ).toThrow( error );
354
+ } );
355
+
356
+ it( 'rejects negative concurrency', () => {
357
+ const error = new StaticValidationError( '✖ Too small: expected number to be >=1\n → at concurrency' );
358
+ expect( () => validateExecuteInParallel( { ...validArgs, concurrency: -1 } ) ).toThrow( error );
359
+ } );
360
+
361
+ it( 'rejects non-function onJobCompleted', () => {
362
+ const error = new StaticValidationError( '✖ Invalid input: expected function, received string\n → at onJobCompleted' );
363
+ expect( () => validateExecuteInParallel( { ...validArgs, onJobCompleted: 'not-function' } ) ).toThrow( error );
364
+ } );
365
+ } );
366
+ } );
@@ -0,0 +1,84 @@
1
+ import type { SerializedFetchResponse } from '../utils/index.d.ts';
2
+
3
+ /**
4
+ * Allowed HTTP methods for request helpers.
5
+ */
6
+ export type HttpMethod = 'HEAD' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
7
+
8
+ /**
9
+ * Send an POST HTTP request to a URL, optionally with a payload, then wait for a webhook response.
10
+ *
11
+ * The "Content-Type" is inferred from the payload type and can be overridden via the `headers` argument.
12
+ *
13
+ * If the body is not a type natively accepted by the Fetch API, it is serialized to a string: `JSON.stringify()` for objects, or `String()` for primitives.
14
+ *
15
+ * When a body is sent, the payload is wrapped together with the `workflowId` and sent as:
16
+ * @example
17
+ * ```js
18
+ * const finalPayload = {
19
+ * workflowId,
20
+ * payload
21
+ * }
22
+ * ```
23
+ *
24
+ * After dispatching the request, the workflow pauses and waits for a POST to `/workflow/:id/feedback` (where `:id` is the `workflowId`). When the API receives that request, its body is delivered back to the workflow and execution resumes.
25
+ *
26
+ * @example
27
+ * ```js
28
+ * const response = await sendPostRequestAndAwaitWebhook( {
29
+ * url: 'https://example.com/integration',
30
+ * payload: {
31
+ * }
32
+ * } );
33
+ *
34
+ * assert( response, 'the value sent back via the api' );
35
+ * ```
36
+ *
37
+ * @remarks
38
+ * - Only callable from within a workflow function; do not use in steps or evaluators.
39
+ * - Steps and evaluators are activity-based and are not designed to be paused.
40
+ * - If used within steps or evaluators, a compilation error will be raised.
41
+ * - Uses a Temporal Activity to dispatch the HTTP request, working around the runtime limitation for workflows.
42
+ * - Uses a Temporal Trigger to pause the workflow.
43
+ * - Uses a Temporal Signal to resume the workflow when the API responds.
44
+ *
45
+ * @param params - Parameters object
46
+ * @param params.url - Request URL
47
+ * @param params.payload - Request payload
48
+ * @param params.headers - Headers for the request
49
+ * @returns Resolves with the payload received by the webhook
50
+ */
51
+ export declare function sendPostRequestAndAwaitWebhook( params: {
52
+ url: string;
53
+ payload?: object;
54
+ headers?: Record<string, string>;
55
+ } ): Promise<unknown>;
56
+
57
+ /**
58
+ * Send an HTTP request to a URL.
59
+ *
60
+ * For POST or PUT requests, an optional payload can be sent as the body.
61
+ *
62
+ * The "Content-Type" is inferred from the payload type and can be overridden via the `headers` argument.
63
+ *
64
+ * If the body is not a type natively accepted by the Fetch API, it is serialized to a string: `JSON.stringify()` for objects, or `String()` for primitives.
65
+ *
66
+ * @remarks
67
+ * - Intended for use within workflow functions; do not use in steps or evaluators.
68
+ * - Steps and evaluators are activity-based and can perform HTTP requests directly.
69
+ * - If used within steps or evaluators, a compilation error will be raised.
70
+ * - Uses a Temporal Activity to dispatch the HTTP request, working around the runtime limitation for workflows.
71
+ *
72
+ * @param params - Parameters object
73
+ * @param params.url - Request URL
74
+ * @param params.method - The HTTP method (default: 'GET')
75
+ * @param params.payload - Request payload (only for POST/PUT)
76
+ * @param params.headers - Headers for the request
77
+ * @returns Resolves with an HTTP response serialized to a plain object
78
+ */
79
+ export declare function sendHttpRequest( params: {
80
+ url: string;
81
+ method?: HttpMethod;
82
+ payload?: object;
83
+ headers?: Record<string, string>;
84
+ } ): Promise<SerializedFetchResponse>;
@@ -0,0 +1,64 @@
1
+ // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
+ import { defineSignal, setHandler, proxyActivities, workflowInfo, proxySinks, uuid4, Trigger } from '@temporalio/workflow';
3
+ import { ACTIVITY_SEND_HTTP_REQUEST } from '#consts';
4
+ import { FatalError } from '#errors';
5
+ import { validateRequestPayload } from './validations/static.js';
6
+
7
+ /**
8
+ * Call the internal activity to make a HTTP request and returns its response.
9
+ *
10
+ * @param {Object} parameters
11
+ * @param {string} url
12
+ * @param {string} method
13
+ * @param {unknown} [payload]
14
+ * @param {object} [headers]
15
+ * @returns {Promise<object>} The serialized HTTP response
16
+ */
17
+ export async function sendHttpRequest( { url, method = 'GET', payload = undefined, headers = undefined } ) {
18
+ validateRequestPayload( { method, url, payload, headers } );
19
+ const res = await proxyActivities( {
20
+ startToCloseTimeout: '3m',
21
+ retry: {
22
+ initialInterval: '15s',
23
+ maximumAttempts: 3,
24
+ nonRetryableErrorTypes: [ FatalError.name ]
25
+ }
26
+ } )[ACTIVITY_SEND_HTTP_REQUEST]( { method, url, payload, headers } );
27
+ return res;
28
+ };
29
+
30
+ /**
31
+ * Call the internal activity to make a POST request sending a payload to a given url.
32
+ *
33
+ * After the request succeeds, pause the code using Trigger and wait for a Signal to un-pause it.
34
+ *
35
+ * The signal will be sent by the API when a response is sent to its webhook url.
36
+ *
37
+ * @param {Object} parameters
38
+ * @param {string} url
39
+ * @param {unknown} [payload]
40
+ * @param {object} [headers]
41
+ * @returns {Promise<unknown>} The response received by the webhook
42
+ */
43
+ export async function sendPostRequestAndAwaitWebhook( { url, payload = undefined, headers = undefined } ) {
44
+ const { workflowId } = workflowInfo();
45
+ const wrappedPayload = { workflowId, payload };
46
+
47
+ await sendHttpRequest( { method: 'POST', url, payload: wrappedPayload, headers } );
48
+
49
+ const sinks = await proxySinks();
50
+ const resumeTrigger = new Trigger();
51
+ const resumeSignal = defineSignal( 'resume' );
52
+
53
+ const traceId = `${workflowId}-${url}-${uuid4()}`;
54
+ sinks.trace.start( { id: traceId, name: 'resume', kind: 'webhook' } );
55
+
56
+ setHandler( resumeSignal, webhookPayload => {
57
+ if ( !resumeTrigger.resolved ) {
58
+ sinks.trace.end( { id: traceId, details: webhookPayload } );
59
+ resumeTrigger.resolve( webhookPayload );
60
+ }
61
+ } );
62
+
63
+ return resumeTrigger;
64
+ };
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Mocks for module aliases used by webhook.js
4
+ vi.mock( '#consts', () => ( {
5
+ ACTIVITY_SEND_HTTP_REQUEST: '__internal#sendHttpRequest'
6
+ } ) );
7
+
8
+ const validateRequestPayloadMock = vi.fn();
9
+ vi.mock( './validations/static.js', () => ( {
10
+ validateRequestPayload: validateRequestPayloadMock
11
+ } ) );
12
+
13
+ // Minimal, legible mock of @temporalio/workflow APIs used by webhook.js
14
+ const activityFnMock = vi.fn();
15
+ const proxyActivitiesMock = vi.fn( () => ( { ['__internal#sendHttpRequest']: activityFnMock } ) );
16
+
17
+ const storedHandlers = new Map();
18
+ const defineSignalMock = name => name;
19
+ const setHandlerMock = ( signal, fn ) => {
20
+ storedHandlers.set( signal, fn );
21
+ };
22
+
23
+ const workflowInfoMock = vi.fn( () => ( { workflowId: 'wf-123' } ) );
24
+ const sinks = { trace: { start: vi.fn(), end: vi.fn() } };
25
+ const proxySinksMock = vi.fn( async () => sinks );
26
+
27
+ class TestTrigger {
28
+ constructor() {
29
+ this.resolved = false;
30
+ this._resolve = () => {};
31
+ this.promise = new Promise( res => {
32
+ this._resolve = res;
33
+ } );
34
+ }
35
+ resolve( value ) {
36
+ if ( !this.resolved ) {
37
+ this.resolved = true;
38
+ this._resolve( value );
39
+ }
40
+ }
41
+ then( onFulfilled, onRejected ) {
42
+ return this.promise.then( onFulfilled, onRejected );
43
+ }
44
+ }
45
+
46
+ vi.mock( '@temporalio/workflow', () => ( {
47
+ defineSignal: defineSignalMock,
48
+ setHandler: setHandlerMock,
49
+ proxyActivities: proxyActivitiesMock,
50
+ workflowInfo: workflowInfoMock,
51
+ proxySinks: proxySinksMock,
52
+ uuid4: () => 'uuid-mock',
53
+ Trigger: TestTrigger
54
+ } ) );
55
+
56
+ describe( 'interface/webhook', () => {
57
+ beforeEach( () => {
58
+ vi.clearAllMocks();
59
+ storedHandlers.clear();
60
+ } );
61
+
62
+ it( 'sendHttpRequest validates input and calls activity with correct options and args', async () => {
63
+ const { sendHttpRequest } = await import( './webhook.js' );
64
+
65
+ const fakeSerializedResponse = {
66
+ url: 'https://example.com',
67
+ status: 200,
68
+ statusText: 'OK',
69
+ ok: true,
70
+ headers: { 'content-type': 'application/json' },
71
+ body: { ok: true }
72
+ };
73
+ activityFnMock.mockResolvedValueOnce( fakeSerializedResponse );
74
+
75
+ const args = { url: 'https://example.com/api', method: 'GET' };
76
+ const res = await sendHttpRequest( args );
77
+
78
+ // validated
79
+ expect( validateRequestPayloadMock ).toHaveBeenCalledWith( { ...args, payload: undefined, headers: undefined } );
80
+
81
+ // activity proxied with specified options
82
+ expect( proxyActivitiesMock ).toHaveBeenCalledTimes( 1 );
83
+ const optionsArg = proxyActivitiesMock.mock.calls[0][0];
84
+ expect( optionsArg.startToCloseTimeout ).toBe( '3m' );
85
+ expect( optionsArg.retry ).toEqual( expect.objectContaining( {
86
+ initialInterval: '15s',
87
+ maximumAttempts: 3,
88
+ nonRetryableErrorTypes: expect.arrayContaining( [ 'FatalError' ] )
89
+ } ) );
90
+
91
+ // activity invoked with the same args
92
+ expect( activityFnMock ).toHaveBeenCalledWith( { ...args, payload: undefined, headers: undefined } );
93
+ expect( res ).toEqual( fakeSerializedResponse );
94
+ } );
95
+
96
+ it( 'sendPostRequestAndAwaitWebhook posts wrapped payload and resolves on resume signal', async () => {
97
+ const { sendPostRequestAndAwaitWebhook } = await import( './webhook.js' );
98
+
99
+ // Make the inner activity resolve (through sendHttpRequest)
100
+ activityFnMock.mockResolvedValueOnce( {
101
+ url: 'https://webhook.site',
102
+ status: 200,
103
+ statusText: 'OK',
104
+ ok: true,
105
+ headers: {},
106
+ body: null
107
+ } );
108
+
109
+ const url = 'https://webhook.site/ingest';
110
+ const promise = sendPostRequestAndAwaitWebhook( { url, payload: { x: 1 }, headers: { a: 'b' } } );
111
+
112
+ // The activity was called via sendHttpRequest with POST and wrapped payload
113
+ const callArgs = activityFnMock.mock.calls[0][0];
114
+ expect( callArgs.method ).toBe( 'POST' );
115
+ expect( callArgs.url ).toBe( url );
116
+ expect( callArgs.payload ).toEqual( { workflowId: 'wf-123', payload: { x: 1 } } );
117
+ expect( callArgs.headers ).toEqual( { a: 'b' } );
118
+
119
+ // Returns a promise (async function) for the eventual webhook result
120
+ expect( typeof promise.then ).toBe( 'function' );
121
+ } );
122
+ } );