@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,448 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Readable } from 'node:stream';
3
+ import {
4
+ clone,
5
+ serializeBodyAndInferContentType,
6
+ serializeFetchResponse,
7
+ deepMerge,
8
+ isPlainObject,
9
+ toUrlSafeBase64
10
+ } from './utils.js';
11
+
12
+ describe( 'clone', () => {
13
+ it( 'produces a deep copy without shared references', () => {
14
+ const original = { a: 1, nested: { b: 2 } };
15
+ const copied = clone( original );
16
+
17
+ copied.nested.b = 3;
18
+
19
+ expect( original.nested.b ).toBe( 2 );
20
+ expect( copied.nested.b ).toBe( 3 );
21
+ expect( copied ).not.toBe( original );
22
+ } );
23
+ } );
24
+
25
+ describe( 'serializeFetchResponse', () => {
26
+ it( 'serializes JSON response body and flattens headers', async () => {
27
+ const payload = { a: 1, b: 'two' };
28
+ const response = new Response( JSON.stringify( payload ), {
29
+ status: 200,
30
+ statusText: 'OK',
31
+ headers: { 'content-type': 'application/json' }
32
+ } );
33
+
34
+ const result = await serializeFetchResponse( response );
35
+ expect( result.status ).toBe( 200 );
36
+ expect( result.ok ).toBe( true );
37
+ expect( result.statusText ).toBe( 'OK' );
38
+ expect( result.headers['content-type'] ).toContain( 'application/json' );
39
+ expect( result.body ).toEqual( payload );
40
+ } );
41
+
42
+ it( 'serializes text/* response via text()', async () => {
43
+ const bodyText = 'hello world';
44
+ const response = new Response( bodyText, {
45
+ status: 201,
46
+ statusText: 'Created',
47
+ headers: { 'content-type': 'text/plain; charset=utf-8' }
48
+ } );
49
+
50
+ const result = await serializeFetchResponse( response );
51
+ expect( result.status ).toBe( 201 );
52
+ expect( result.ok ).toBe( true );
53
+ expect( result.statusText ).toBe( 'Created' );
54
+ expect( result.headers['content-type'] ).toContain( 'text/plain' );
55
+ expect( result.body ).toBe( bodyText );
56
+ } );
57
+
58
+ if ( typeof ReadableStream !== 'undefined' ) {
59
+ it( 'serializes ReadableStream body for text/* via text()', async () => {
60
+ const encoder = new TextEncoder();
61
+ const chunk = encoder.encode( 'streamed text' );
62
+ const stream = new ReadableStream( {
63
+ start( controller ) {
64
+ controller.enqueue( chunk );
65
+ controller.close();
66
+ }
67
+ } );
68
+ const response = new Response( stream, {
69
+ status: 200,
70
+ statusText: 'OK',
71
+ headers: { 'content-type': 'text/plain; charset=utf-8' }
72
+ } );
73
+
74
+ const result = await serializeFetchResponse( response );
75
+ expect( result.status ).toBe( 200 );
76
+ expect( result.ok ).toBe( true );
77
+ expect( result.statusText ).toBe( 'OK' );
78
+ expect( result.headers['content-type'] ).toContain( 'text/plain' );
79
+ expect( result.body ).toBe( 'streamed text' );
80
+ } );
81
+ }
82
+
83
+ it( 'serializes non-text/non-json response as base64 from arrayBuffer()', async () => {
84
+ const bytes = Uint8Array.from( [ 0, 1, 2, 3 ] );
85
+ const response = new Response( bytes, {
86
+ status: 200,
87
+ statusText: 'OK',
88
+ headers: { 'content-type': 'application/octet-stream' }
89
+ } );
90
+
91
+ const result = await serializeFetchResponse( response );
92
+ expect( result.status ).toBe( 200 );
93
+ expect( result.ok ).toBe( true );
94
+ expect( result.statusText ).toBe( 'OK' );
95
+ expect( result.headers['content-type'] ).toBe( 'application/octet-stream' );
96
+ expect( result.body ).toBe( Buffer.from( bytes ).toString( 'base64' ) );
97
+ } );
98
+
99
+ it( 'defaults to base64 when content-type header is missing', async () => {
100
+ const bytes = Uint8Array.from( [ 0, 1, 2, 3 ] );
101
+ const response = new Response( bytes, { status: 200 } );
102
+ // No headers set; content-type resolves to ''
103
+
104
+ const result = await serializeFetchResponse( response );
105
+ expect( result.headers['content-type'] ?? '' ).toBe( '' );
106
+ expect( result.body ).toBe( Buffer.from( bytes ).toString( 'base64' ) );
107
+ } );
108
+ } );
109
+
110
+ describe( 'serializeBodyAndInferContentType', () => {
111
+ it( 'returns undefineds for null payload', () => {
112
+ const { body, contentType } = serializeBodyAndInferContentType( null );
113
+ expect( body ).toBeUndefined();
114
+ expect( contentType ).toBeUndefined();
115
+ } );
116
+
117
+ it( 'returns undefineds for undefined payload', () => {
118
+ const { body, contentType } = serializeBodyAndInferContentType( undefined );
119
+ expect( body ).toBeUndefined();
120
+ expect( contentType ).toBeUndefined();
121
+ } );
122
+
123
+ it( 'handles ArrayBuffer with octet-stream', () => {
124
+ const buf = new ArrayBuffer( 4 );
125
+ const { body, contentType } = serializeBodyAndInferContentType( buf );
126
+ expect( body ).toBe( buf );
127
+ expect( contentType ).toBe( 'application/octet-stream' );
128
+ } );
129
+
130
+ it( 'handles TypedArray with octet-stream', () => {
131
+ const view = new Uint8Array( [ 1, 2, 3 ] );
132
+ const { body, contentType } = serializeBodyAndInferContentType( view );
133
+ expect( body ).toBe( view );
134
+ expect( contentType ).toBe( 'application/octet-stream' );
135
+ } );
136
+
137
+ it( 'handles DataView with octet-stream', () => {
138
+ const ab = new ArrayBuffer( 2 );
139
+ const dv = new DataView( ab );
140
+ const { body, contentType } = serializeBodyAndInferContentType( dv );
141
+ expect( body ).toBe( dv );
142
+ expect( contentType ).toBe( 'application/octet-stream' );
143
+ } );
144
+
145
+ // Environment-provided web types
146
+ if ( typeof URLSearchParams !== 'undefined' ) {
147
+ it( 'passes through URLSearchParams without content type', () => {
148
+ const usp = new URLSearchParams( { a: '1', b: 'two' } );
149
+ const { body, contentType } = serializeBodyAndInferContentType( usp );
150
+ expect( body ).toBe( usp );
151
+ expect( contentType ).toBeUndefined();
152
+ } );
153
+ }
154
+
155
+ if ( typeof FormData !== 'undefined' ) {
156
+ it( 'passes through FormData without content type', () => {
157
+ const fd = new FormData();
158
+ fd.append( 'a', '1' );
159
+ const { body, contentType } = serializeBodyAndInferContentType( fd );
160
+ expect( body ).toBe( fd );
161
+ expect( contentType ).toBeUndefined();
162
+ } );
163
+ }
164
+
165
+ if ( typeof Blob !== 'undefined' ) {
166
+ it( 'passes through Blob without content type', () => {
167
+ const blob = new Blob( [ 'abc' ], { type: 'text/plain' } );
168
+ const { body, contentType } = serializeBodyAndInferContentType( blob );
169
+ expect( body ).toBe( blob );
170
+ expect( contentType ).toBeUndefined();
171
+ } );
172
+ }
173
+
174
+ if ( typeof File !== 'undefined' ) {
175
+ it( 'passes through File without content type', () => {
176
+ const file = new File( [ 'abc' ], 'a.txt', { type: 'text/plain' } );
177
+ const { body, contentType } = serializeBodyAndInferContentType( file );
178
+ expect( body ).toBe( file );
179
+ expect( contentType ).toBeUndefined();
180
+ } );
181
+ }
182
+
183
+ it( 'passes through async iterator without content type', () => {
184
+ const asyncIter = ( async function *() {
185
+ yield 'chunk';
186
+ } )();
187
+ const { body, contentType } = serializeBodyAndInferContentType( asyncIter );
188
+ expect( typeof body[Symbol.asyncIterator] ).toBe( 'function' );
189
+ expect( contentType ).toBeUndefined();
190
+ } );
191
+
192
+ it( 'passes through Node Readable without content type', () => {
193
+ const readable = Readable.from( [ 'a', 'b' ] );
194
+ const { body, contentType } = serializeBodyAndInferContentType( readable );
195
+ expect( body ).toBe( readable );
196
+ expect( contentType ).toBeUndefined();
197
+ } );
198
+
199
+ it( 'serializes plain object as JSON with JSON content type', () => {
200
+ const input = { a: 1, b: 'two' };
201
+ const { body, contentType } = serializeBodyAndInferContentType( input );
202
+ expect( body ).toBe( JSON.stringify( input ) );
203
+ expect( contentType ).toBe( 'application/json; charset=UTF-8' );
204
+ } );
205
+
206
+ it( 'serializes string primitive with text/plain content type', () => {
207
+ const { body, contentType } = serializeBodyAndInferContentType( 'hello' );
208
+ expect( body ).toBe( 'hello' );
209
+ expect( contentType ).toBe( 'text/plain; charset=UTF-8' );
210
+ } );
211
+
212
+ it( 'serializes number primitive with text/plain content type', () => {
213
+ const { body, contentType } = serializeBodyAndInferContentType( 42 );
214
+ expect( body ).toBe( '42' );
215
+ expect( contentType ).toBe( 'text/plain; charset=UTF-8' );
216
+ } );
217
+
218
+ it( 'serializes boolean primitive with text/plain content type', () => {
219
+ const { body, contentType } = serializeBodyAndInferContentType( true );
220
+ expect( body ).toBe( 'true' );
221
+ expect( contentType ).toBe( 'text/plain; charset=UTF-8' );
222
+ } );
223
+ } );
224
+
225
+ describe( 'deepMerge', () => {
226
+ it( 'Overwrites properties in object "a"', () => {
227
+ const a = {
228
+ a: 1,
229
+ b: {
230
+ c: 2
231
+ }
232
+ };
233
+ const b = {
234
+ a: false,
235
+ b: {
236
+ c: true
237
+ }
238
+ };
239
+ expect( deepMerge( a, b ) ).toEqual( {
240
+ a: false,
241
+ b: {
242
+ c: true
243
+ }
244
+ } );
245
+ } );
246
+
247
+ it( 'Adds properties existing in "b" but absent in "a"', () => {
248
+ const a = {
249
+ a: 1
250
+ };
251
+ const b = {
252
+ a: false,
253
+ b: true
254
+ };
255
+ expect( deepMerge( a, b ) ).toEqual( {
256
+ a: false,
257
+ b: true
258
+ } );
259
+ } );
260
+
261
+ it( 'Keep extra properties in "a"', () => {
262
+ const a = {
263
+ a: 1
264
+ };
265
+ const b = {
266
+ b: true
267
+ };
268
+ expect( deepMerge( a, b ) ).toEqual( {
269
+ a: 1,
270
+ b: true
271
+ } );
272
+ } );
273
+
274
+ it( 'Merge object is a clone', () => {
275
+ const a = {
276
+ a: 1
277
+ };
278
+ const b = {
279
+ b: 1
280
+ };
281
+ const result = deepMerge( a, b );
282
+ a.a = 2;
283
+ b.b = 2;
284
+ expect( result.a ).toEqual( 1 );
285
+ } );
286
+
287
+ it( 'Returns copy of "a" if "b" is not an object', () => {
288
+ const a = {
289
+ a: 1
290
+ };
291
+ expect( deepMerge( a, null ) ).toEqual( { a: 1 } );
292
+ expect( deepMerge( a, undefined ) ).toEqual( { a: 1 } );
293
+ } );
294
+
295
+ it( 'Copy of object "a" is a clone', () => {
296
+ const a = {
297
+ a: 1
298
+ };
299
+ const result = deepMerge( a, null );
300
+ a.a = 2;
301
+ expect( result.a ).toEqual( 1 );
302
+ } );
303
+
304
+ it( 'Throws when first argument is not a plain object', () => {
305
+ expect( () => deepMerge( Function ) ).toThrow( Error );
306
+ expect( () => deepMerge( () => {} ) ).toThrow( Error );
307
+ expect( () => deepMerge( 'a' ) ).toThrow( Error );
308
+ expect( () => deepMerge( true ) ).toThrow( Error );
309
+ expect( () => deepMerge( /a/ ) ).toThrow( Error );
310
+ expect( () => deepMerge( [] ) ).toThrow( Error );
311
+ expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
312
+ expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
313
+ expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
314
+ } );
315
+ } );
316
+
317
+ describe( 'isPlainObject', () => {
318
+ it( 'Detects plain objects', () => {
319
+ expect( isPlainObject( {} ) ).toBe( true );
320
+ expect( isPlainObject( { a: 1 } ) ).toBe( true );
321
+ expect( isPlainObject( new Object() ) ).toBe( true );
322
+ expect( isPlainObject( new Object( { foo: 'bar' } ) ) ).toBe( true );
323
+ expect( isPlainObject( Object.create( {}.constructor.prototype ) ) ).toBe( true );
324
+ expect( isPlainObject( Object.create( Object.prototype ) ) ).toBe( true );
325
+ } );
326
+
327
+ it( 'Detects plain objects with different prototypes than Object.prototype', () => {
328
+ // Object with null prototype
329
+ expect( isPlainObject( Object.create( null ) ) ).toBe( true );
330
+ } );
331
+
332
+ it( 'Detects non plain objects that had their __proto__ mutated to Object.prototype or null', () => {
333
+ class Foo {}
334
+ const x = new Foo();
335
+ x.__proto__ = Object.prototype;
336
+ expect( isPlainObject( x ) ).toBe( true );
337
+
338
+ const y = new Foo();
339
+ y.__proto__ = null;
340
+ expect( isPlainObject( y ) ).toBe( true );
341
+ } );
342
+
343
+ it( 'Returns false for object which the prototype is not Object.prototype or null', () => {
344
+ // Object which the prototype is a plain {}
345
+ expect( isPlainObject( Object.create( {} ) ) ).toBe( false );
346
+ // Object which prototype is a another object with null prototype
347
+ expect( isPlainObject( Object.create( Object.create( null ) ) ) ).toBe( false );
348
+ } );
349
+
350
+ it( 'Returns false for functions', () => {
351
+ expect( isPlainObject( Function ) ).toBe( false );
352
+ expect( isPlainObject( () => {} ) ).toBe( false );
353
+ expect( isPlainObject( class Foo {} ) ).toBe( false );
354
+ expect( isPlainObject( Number.constructor ) ).toBe( false );
355
+ expect( isPlainObject( Number.constructor.prototype ) ).toBe( false );
356
+ } );
357
+
358
+ it( 'Returns false for arrays', () => {
359
+ expect( isPlainObject( [ 1, 2, 3 ] ) ).toBe( false );
360
+ expect( isPlainObject( [] ) ).toBe( false );
361
+ expect( isPlainObject( Array( 3 ) ) ).toBe( false );
362
+ } );
363
+
364
+ it( 'Returns false for primitives', () => {
365
+ expect( isPlainObject( null ) ).toBe( false );
366
+ expect( isPlainObject( undefined ) ).toBe( false );
367
+ expect( isPlainObject( false ) ).toBe( false );
368
+ expect( isPlainObject( true ) ).toBe( false );
369
+ expect( isPlainObject( 1 ) ).toBe( false );
370
+ expect( isPlainObject( 0 ) ).toBe( false );
371
+ expect( isPlainObject( '' ) ).toBe( false );
372
+ expect( isPlainObject( 'foo' ) ).toBe( false );
373
+ expect( isPlainObject( Symbol( 'foo' ) ) ).toBe( false );
374
+ expect( isPlainObject( Symbol.for( 'foo' ) ) ).toBe( false );
375
+ } );
376
+
377
+ it( 'Returns true for built in objects', () => {
378
+ expect( isPlainObject( Math ) ).toBe( true );
379
+ expect( isPlainObject( JSON ) ).toBe( true );
380
+ } );
381
+
382
+ it( 'Returns false for built in types', () => {
383
+ expect( isPlainObject( String ) ).toBe( false );
384
+ expect( isPlainObject( Number ) ).toBe( false );
385
+ expect( isPlainObject( Date ) ).toBe( false );
386
+ } );
387
+
388
+ it( 'Returns false for other instance where prototype is not object or null', () => {
389
+ expect( isPlainObject( /foo/ ) ).toBe( false );
390
+ expect( isPlainObject( new RegExp( 'foo' ) ) ).toBe( false );
391
+ expect( isPlainObject( new Date() ) ).toBe( false );
392
+ class Foo {}
393
+ expect( isPlainObject( new Foo() ) ).toBe( false );
394
+ expect( isPlainObject( Object.create( ( class Foo {} ).prototype ) ) ).toBe( false );
395
+ } );
396
+
397
+ it( 'Returns false if tries to change the prototype to simulate an object', () => {
398
+ function Bar() {}
399
+ Bar.prototype = Object.create( null );
400
+ expect( isPlainObject( new Bar() ) ).toBe( false );
401
+ } );
402
+
403
+ it( 'Returns false if object proto was mutated to anything else than object or null', () => {
404
+ const zum = {};
405
+ zum.__proto__ = Number.prototype;
406
+ expect( isPlainObject( zum ) ).toBe( false );
407
+ } );
408
+ } );
409
+
410
+ describe( 'toUrlSafeBase64', () => {
411
+ const urlSafeAlphabet = /^[A-Za-z0-9_-]+$/;
412
+
413
+ it( 'returns a string for a valid UUID', () => {
414
+ const uuid = '550e8400-e29b-41d4-a716-446655440000';
415
+ expect( typeof toUrlSafeBase64( uuid ) ).toBe( 'string' );
416
+ expect( toUrlSafeBase64( uuid ).length ).toBeGreaterThan( 0 );
417
+ } );
418
+
419
+ it( 'output length is 21 or 22 for a standard UUID', () => {
420
+ const uuid = '550e8400-e29b-41d4-a716-446655440000';
421
+ const out = toUrlSafeBase64( uuid );
422
+ expect( out.length ).toBeGreaterThanOrEqual( 21 );
423
+ expect( out.length ).toBeLessThanOrEqual( 22 );
424
+ } );
425
+
426
+ it( 'output contains only url-safe alphabet characters', () => {
427
+ const uuid = '550e8400-e29b-41d4-a716-446655440000';
428
+ const out = toUrlSafeBase64( uuid );
429
+ expect( out ).toMatch( urlSafeAlphabet );
430
+ } );
431
+
432
+ it( 'is deterministic for the same UUID', () => {
433
+ const uuid = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
434
+ expect( toUrlSafeBase64( uuid ) ).toBe( toUrlSafeBase64( uuid ) );
435
+ } );
436
+
437
+ it( 'different UUIDs produce different strings', () => {
438
+ const a = toUrlSafeBase64( '550e8400-e29b-41d4-a716-446655440000' );
439
+ const b = toUrlSafeBase64( '6ba7b810-9dad-11d1-80b4-00c04fd430c8' );
440
+ expect( a ).not.toBe( b );
441
+ } );
442
+
443
+ it( 'strips hyphens and encodes hex (same as 32-char hex)', () => {
444
+ const withHyphens = '550e8400-e29b-41d4-a716-446655440000';
445
+ const hexOnly = '550e8400e29b41d4a716446655440000';
446
+ expect( toUrlSafeBase64( withHyphens ) ).toBe( toUrlSafeBase64( hexOnly ) );
447
+ } );
448
+ } );
@@ -0,0 +1,43 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const __dirname = dirname( fileURLToPath( import.meta.url ) );
5
+ const workerDir = __dirname; // sdk/core/src/worker
6
+ const interfaceDir = join( __dirname, '..', 'interface' );
7
+
8
+ export const webpackConfigHook = config => {
9
+ // Prefer the "output-workflow-bundle" export condition when resolving packages.
10
+ // Packages that transitively depend on Node.js built-ins (which can't exist in the
11
+ // Temporal workflow bundle) can provide an alternative entry point under this condition
12
+ // that excludes the offending code paths. Packages without this condition fall through
13
+ // to the standard "import" / "module" / "default" conditions as normal.
14
+ config.resolve = config.resolve ?? {};
15
+ config.resolve.conditionNames = [
16
+ 'output-workflow-bundle',
17
+ ...( config.resolve.conditionNames ?? [ 'import', 'module', 'webpack', 'default' ] )
18
+ ];
19
+
20
+ config.module = config.module ?? { };
21
+ config.module.rules = config.module.rules ?? [];
22
+
23
+ // Validation loader (runs first)
24
+ config.module.rules.push( {
25
+ test: /\.js$/,
26
+ // Exclude node_modules and internal core worker files
27
+ exclude: resource => /node_modules/.test( resource ) || resource.startsWith( workerDir ) || resource.startsWith( interfaceDir ),
28
+ enforce: 'pre',
29
+ use: {
30
+ loader: join( __dirname, './webpack_loaders/workflow_validator/index.mjs' )
31
+ }
32
+ } );
33
+ // Use AST-based loader for rewriting steps/workflows
34
+ config.module.rules.push( {
35
+ test: /\.js$/,
36
+ // Exclude node_modules and internal core worker files
37
+ exclude: resource => /node_modules/.test( resource ) || resource.startsWith( workerDir ) || resource.startsWith( interfaceDir ),
38
+ use: {
39
+ loader: join( __dirname, './webpack_loaders/workflow_rewriter/index.mjs' )
40
+ }
41
+ } );
42
+ return config;
43
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Represents the collection of metadata from workflows and activities that a worker has.
3
+ */
4
+ export class Catalog {
5
+ /**
6
+ * All workflows in the catalog
7
+ * @type {Array<CatalogWorkflow>}
8
+ */
9
+ workflows;
10
+
11
+ constructor() {
12
+ this.workflows = [];
13
+ };
14
+
15
+ /**
16
+ * Add a workflow entry to the catalog.
17
+ *
18
+ * @param {CatalogWorkflow} workflow - Workflow to add.
19
+ * @returns {Catalog} This catalog instance (for chaining).
20
+ */
21
+ addWorkflow( workflow ) {
22
+ this.workflows.push( workflow );
23
+ return this;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Base type for catalog entries (workflows, activities).
29
+ *
30
+ * Encapsulates common descriptive fields and JSON schemas.
31
+ */
32
+ class CatalogEntry {
33
+ /**
34
+ * Name of the entry. Only letters, numbers and _ allowed.
35
+ * @type {string}
36
+ */
37
+ name;
38
+ /**
39
+ * Optional description.
40
+ * @type {string|undefined}
41
+ */
42
+ description;
43
+ /**
44
+ * JSON schema describing the expected input.
45
+ * @type {object}
46
+ */
47
+ inputSchema;
48
+ /**
49
+ * JSON schema describing the produced output.
50
+ * @type {object}
51
+ */
52
+ outputSchema;
53
+ /**
54
+ * Absolute path of the entity in the file system.
55
+ * @type {string}
56
+ */
57
+ path;
58
+
59
+ /**
60
+ * @param {Object} params - Entry parameters.
61
+ * @param {string} params.name - Name of the entry.
62
+ * @param {string} [params.description] - Optional description.
63
+ * @param {object} [params.inputSchema] - JSON schema describing the expected input.
64
+ * @param {object} [params.outputSchema] - JSON schema describing the produced output.
65
+ * @param {string} params.path - Absolute path of the entity in the file system.
66
+ */
67
+ constructor( { name, description, inputSchema, outputSchema, path } ) {
68
+ this.name = name;
69
+ this.description = description;
70
+ this.inputSchema = inputSchema;
71
+ this.outputSchema = outputSchema;
72
+ this.path = path;
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Describes a single activity within a workflow.
78
+ *
79
+ * @class
80
+ * @extends CatalogEntry
81
+ */
82
+ export class CatalogActivity extends CatalogEntry {}
83
+
84
+ /**
85
+ * @param { CatalogWorkflowOptions}
86
+ */
87
+
88
+ /**
89
+ * Describes a single workflow within the catalog.
90
+ *
91
+ * @class
92
+ * @extends CatalogEntry
93
+ */
94
+ export class CatalogWorkflow extends CatalogEntry {
95
+ /**
96
+ * Each activity of this workflow.
97
+ * @type {Array<CatalogActivity>}
98
+ */
99
+ activities;
100
+
101
+ /**
102
+ * @param {Object} params - Entry parameters.
103
+ * @param {string} params.name - Name of the entry.
104
+ * @param {string} [params.description] - Optional description.
105
+ * @param {object} [params.inputSchema] - JSON schema describing the expected input.
106
+ * @param {object} [params.outputSchema] - JSON schema describing the produced output.
107
+ * @param {string} params.path - Absolute path of the entity in the file system.
108
+ * @param {Array<CatalogActivity>} params.activities - Each activity of this workflow
109
+ */
110
+ constructor( { activities, ...args } ) {
111
+ super( args );
112
+ this.activities = activities;
113
+ };
114
+ };
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+ import { dirname } from 'node:path';
3
+ import { METADATA_ACCESS_SYMBOL } from '#consts';
4
+ import { Catalog, CatalogActivity, CatalogWorkflow } from './catalog.js';
5
+ import { createChildLogger } from '#logger';
6
+
7
+ const log = createChildLogger( 'Catalog' );
8
+
9
+ /**
10
+ * Converts a Zod schema to JSON Schema format.
11
+ *
12
+ * @param {any} schema - A zod schema
13
+ * @returns {object|null} JSON Schema object, or null if schema is invalid
14
+ */
15
+ const convertToJsonSchema = schema => {
16
+ if ( !schema ) {
17
+ return null;
18
+ }
19
+
20
+ try {
21
+ return z.toJSONSchema( schema );
22
+ } catch ( error ) {
23
+ log.warn( 'Invalid schema provided (expected Zod schema)', { error: error.message } );
24
+ return null;
25
+ }
26
+ };
27
+
28
+ /**
29
+ * Converts the list of workflows and the activities into the catalog information.
30
+ *
31
+ * This has information of all workflows and their activities from this worker.
32
+ *
33
+ * @param {object[]} workflows - The workflows objects, as they are returned from the loader module
34
+ * @param {object} activities - The activities functions map with metadata, as they are returned from the loader module
35
+ * @returns {Catalog} An catalog instance
36
+ */
37
+ export const createCatalog = ( { workflows, activities } ) =>
38
+ workflows.reduce( ( catalog, workflow ) =>
39
+ catalog.addWorkflow( new CatalogWorkflow( {
40
+ ...workflow,
41
+ inputSchema: convertToJsonSchema( workflow.inputSchema ),
42
+ outputSchema: convertToJsonSchema( workflow.outputSchema ),
43
+ activities: Object.entries( activities )
44
+ .filter( ( [ k ] ) => k.startsWith( `${dirname( workflow.path )}#` ) )
45
+ .map( ( [ _, v ] ) => {
46
+ const metadata = v[METADATA_ACCESS_SYMBOL];
47
+ return new CatalogActivity( {
48
+ ...metadata,
49
+ inputSchema: convertToJsonSchema( metadata.inputSchema ),
50
+ outputSchema: convertToJsonSchema( metadata.outputSchema )
51
+ } );
52
+ } )
53
+ } ) )
54
+ , new Catalog() );