serverless-event-orchestrator 2.0.1 → 2.3.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.
- package/LICENSE +21 -21
- package/README.md +489 -434
- package/dist/dispatcher.d.ts +6 -1
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +66 -7
- package/dist/dispatcher.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/event-type.enum.d.ts +1 -0
- package/dist/types/event-type.enum.d.ts.map +1 -1
- package/dist/types/event-type.enum.js +1 -0
- package/dist/types/event-type.enum.js.map +1 -1
- package/dist/types/routes.d.ts +6 -0
- package/dist/types/routes.d.ts.map +1 -1
- package/jest.config.js +32 -32
- package/package.json +82 -81
- package/src/dispatcher.ts +586 -519
- package/src/http/body-parser.ts +60 -60
- package/src/http/cors.ts +76 -76
- package/src/http/index.ts +3 -3
- package/src/http/response.ts +209 -209
- package/src/identity/extractor.ts +207 -207
- package/src/identity/index.ts +2 -2
- package/src/identity/jwt-verifier.ts +41 -41
- package/src/index.ts +128 -127
- package/src/middleware/crm-guard.ts +51 -51
- package/src/middleware/index.ts +3 -3
- package/src/middleware/init-tenant-context.ts +59 -59
- package/src/middleware/tenant-guard.ts +54 -54
- package/src/tenant/TenantContext.ts +115 -115
- package/src/tenant/helpers.ts +112 -112
- package/src/tenant/index.ts +21 -21
- package/src/tenant/types.ts +101 -101
- package/src/types/event-type.enum.ts +21 -20
- package/src/types/index.ts +2 -2
- package/src/types/routes.ts +218 -211
- package/src/utils/headers.ts +72 -72
- package/src/utils/index.ts +2 -2
- package/src/utils/path-matcher.ts +84 -84
- package/tests/cors.test.ts +133 -133
- package/tests/dispatcher.test.ts +795 -715
- package/tests/headers.test.ts +99 -99
- package/tests/identity.test.ts +301 -301
- package/tests/middleware/crm-guard.test.ts +69 -69
- package/tests/middleware/init-tenant-context.test.ts +147 -147
- package/tests/middleware/tenant-guard.test.ts +100 -100
- package/tests/path-matcher.test.ts +102 -102
- package/tests/response.test.ts +155 -155
- package/tests/tenant/TenantContext.test.ts +134 -134
- package/tests/tenant/helpers.test.ts +187 -187
- package/tsconfig.json +24 -24
package/src/dispatcher.ts
CHANGED
|
@@ -1,519 +1,586 @@
|
|
|
1
|
-
import { EventType, RouteSegment } from './types/event-type.enum.js';
|
|
2
|
-
import {
|
|
3
|
-
DispatchRoutes,
|
|
4
|
-
HttpMethod,
|
|
5
|
-
HttpRouter,
|
|
6
|
-
SegmentedHttpRouter,
|
|
7
|
-
AdvancedSegmentedRouter,
|
|
8
|
-
SegmentConfig,
|
|
9
|
-
RouteConfig,
|
|
10
|
-
RouteMatch,
|
|
11
|
-
NormalizedEvent,
|
|
12
|
-
OrchestratorConfig,
|
|
13
|
-
MiddlewareFn,
|
|
14
|
-
} from './types/routes.js';
|
|
15
|
-
import { matchPath, normalizePath } from './utils/path-matcher.js';
|
|
16
|
-
import { normalizeHeaders } from './utils/headers.js';
|
|
17
|
-
import { parseJsonBody, parseQueryParams } from './http/body-parser.js';
|
|
18
|
-
import { extractIdentity, validateIssuer } from './identity/extractor.js';
|
|
19
|
-
import type { JwtVerificationPoolConfig } from './types/routes.js';
|
|
20
|
-
import { forbiddenResponse, badRequestResponse, notFoundResponse, unauthorizedResponse } from './http/response.js';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Detects the type of AWS event
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return null;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (!
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
*
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
): Promise<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1
|
+
import { EventType, RouteSegment } from './types/event-type.enum.js';
|
|
2
|
+
import {
|
|
3
|
+
DispatchRoutes,
|
|
4
|
+
HttpMethod,
|
|
5
|
+
HttpRouter,
|
|
6
|
+
SegmentedHttpRouter,
|
|
7
|
+
AdvancedSegmentedRouter,
|
|
8
|
+
SegmentConfig,
|
|
9
|
+
RouteConfig,
|
|
10
|
+
RouteMatch,
|
|
11
|
+
NormalizedEvent,
|
|
12
|
+
OrchestratorConfig,
|
|
13
|
+
MiddlewareFn,
|
|
14
|
+
} from './types/routes.js';
|
|
15
|
+
import { matchPath, normalizePath } from './utils/path-matcher.js';
|
|
16
|
+
import { normalizeHeaders } from './utils/headers.js';
|
|
17
|
+
import { parseJsonBody, parseQueryParams } from './http/body-parser.js';
|
|
18
|
+
import { extractIdentity, validateIssuer } from './identity/extractor.js';
|
|
19
|
+
import type { JwtVerificationPoolConfig } from './types/routes.js';
|
|
20
|
+
import { forbiddenResponse, badRequestResponse, notFoundResponse, unauthorizedResponse } from './http/response.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detects the type of AWS event.
|
|
24
|
+
*
|
|
25
|
+
* EventBridge detection: AWS EventBridge events tienen una estructura
|
|
26
|
+
* estándar con `version: '0'`, `id`, `source`, `detail-type` y `detail`.
|
|
27
|
+
* El `source` lo setea el productor (cualquier string), no es un magic value
|
|
28
|
+
* fijo — por eso se detecta por estructura, no por valor del source.
|
|
29
|
+
*/
|
|
30
|
+
export function detectEventType(event: any): EventType {
|
|
31
|
+
if (event.source === 'aws.events' && event['detail-type'] === 'Scheduled Event') return EventType.Scheduled;
|
|
32
|
+
if (isEventBridgeEvent(event)) return EventType.EventBridge;
|
|
33
|
+
if (event.requestContext && event.httpMethod) return EventType.ApiGateway;
|
|
34
|
+
if (event.Records && Array.isArray(event.Records) && event.Records[0]?.eventSource === 'aws:sqs') return EventType.Sqs;
|
|
35
|
+
if (event.awsRequestId) return EventType.Lambda;
|
|
36
|
+
return EventType.Unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* True if the event has the structural shape of an AWS EventBridge event.
|
|
41
|
+
* EventBridge events siempre tienen estos campos:
|
|
42
|
+
* version: '0', id, source, detail-type, detail, account, region, time
|
|
43
|
+
*/
|
|
44
|
+
function isEventBridgeEvent(event: any): boolean {
|
|
45
|
+
return (
|
|
46
|
+
event &&
|
|
47
|
+
typeof event === 'object' &&
|
|
48
|
+
event.version === '0' &&
|
|
49
|
+
typeof event.id === 'string' &&
|
|
50
|
+
typeof event.source === 'string' &&
|
|
51
|
+
typeof event['detail-type'] === 'string' &&
|
|
52
|
+
event.detail !== undefined
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a router is segmented (has public/private/backoffice/internal keys)
|
|
58
|
+
*/
|
|
59
|
+
function isSegmentedRouter(router: any): router is SegmentedHttpRouter | AdvancedSegmentedRouter {
|
|
60
|
+
if (!router || typeof router !== 'object') return false;
|
|
61
|
+
const segmentKeys = ['public', 'private', 'backoffice', 'internal'];
|
|
62
|
+
const routerKeys = Object.keys(router);
|
|
63
|
+
return routerKeys.some(key => segmentKeys.includes(key));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if a segment config has middleware
|
|
68
|
+
*/
|
|
69
|
+
function isSegmentConfig(config: any): config is SegmentConfig {
|
|
70
|
+
return config && typeof config === 'object' && 'routes' in config;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gets the HttpRouter from a segment (handles both simple and advanced config)
|
|
75
|
+
*/
|
|
76
|
+
function getRouterFromSegment(segment: HttpRouter | SegmentConfig | undefined): HttpRouter | undefined {
|
|
77
|
+
if (!segment) return undefined;
|
|
78
|
+
if (isSegmentConfig(segment)) return segment.routes;
|
|
79
|
+
return segment;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets middleware from a segment config
|
|
84
|
+
*/
|
|
85
|
+
function getMiddlewareFromSegment(segment: HttpRouter | SegmentConfig | undefined): MiddlewareFn[] {
|
|
86
|
+
if (!segment) return [];
|
|
87
|
+
if (isSegmentConfig(segment)) return segment.middleware ?? [];
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Finds a matching route using pattern for lookup and actualPath for params extraction
|
|
94
|
+
*/
|
|
95
|
+
function findRouteInRouterWithActualPath(
|
|
96
|
+
router: HttpRouter | undefined,
|
|
97
|
+
method: HttpMethod,
|
|
98
|
+
routePattern: string,
|
|
99
|
+
actualPath: string
|
|
100
|
+
): { config: RouteConfig; params: Record<string, string> } | null {
|
|
101
|
+
if (!router) return null;
|
|
102
|
+
|
|
103
|
+
const methodRoutes = router[method];
|
|
104
|
+
if (!methodRoutes) return null;
|
|
105
|
+
|
|
106
|
+
const normalizedPattern = normalizePath(routePattern);
|
|
107
|
+
const normalizedActualPath = normalizePath(actualPath);
|
|
108
|
+
|
|
109
|
+
// First, try exact match with the pattern
|
|
110
|
+
if (methodRoutes[normalizedPattern]) {
|
|
111
|
+
// Extract params from the actual path using the pattern
|
|
112
|
+
const params = matchPath(normalizedPattern, normalizedActualPath) ?? {};
|
|
113
|
+
return { config: methodRoutes[normalizedPattern], params };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Then, try pattern matching
|
|
117
|
+
for (const [pattern, config] of Object.entries(methodRoutes)) {
|
|
118
|
+
const params = matchPath(pattern, normalizedActualPath);
|
|
119
|
+
if (params !== null) {
|
|
120
|
+
return { config, params };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Finds a route across all segments with actual path for params
|
|
129
|
+
*/
|
|
130
|
+
function findRouteInSegmentsWithActualPath(
|
|
131
|
+
router: SegmentedHttpRouter | AdvancedSegmentedRouter,
|
|
132
|
+
method: HttpMethod,
|
|
133
|
+
routePattern: string,
|
|
134
|
+
actualPath: string
|
|
135
|
+
): RouteMatch | null {
|
|
136
|
+
const segments: RouteSegment[] = [
|
|
137
|
+
RouteSegment.Public,
|
|
138
|
+
RouteSegment.Private,
|
|
139
|
+
RouteSegment.Backoffice,
|
|
140
|
+
RouteSegment.Internal,
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
for (const segment of segments) {
|
|
144
|
+
const segmentRouter = router[segment];
|
|
145
|
+
const httpRouter = getRouterFromSegment(segmentRouter);
|
|
146
|
+
const result = findRouteInRouterWithActualPath(httpRouter, method, routePattern, actualPath);
|
|
147
|
+
|
|
148
|
+
if (result) {
|
|
149
|
+
return {
|
|
150
|
+
handler: result.config.handler,
|
|
151
|
+
params: result.params,
|
|
152
|
+
segment,
|
|
153
|
+
middleware: getMiddlewareFromSegment(segmentRouter),
|
|
154
|
+
config: result.config,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Normalizes an API Gateway event into a standard format
|
|
165
|
+
*/
|
|
166
|
+
async function normalizeApiGatewayEvent(
|
|
167
|
+
event: any,
|
|
168
|
+
segment: RouteSegment,
|
|
169
|
+
extractedParams: Record<string, string>,
|
|
170
|
+
autoExtract: boolean = false,
|
|
171
|
+
jwtVerificationConfig?: JwtVerificationPoolConfig
|
|
172
|
+
): Promise<NormalizedEvent> {
|
|
173
|
+
const identity = await extractIdentity(event, { autoExtract, jwtVerificationConfig });
|
|
174
|
+
|
|
175
|
+
// Safely extract pathParameters from event (handle null/undefined)
|
|
176
|
+
const eventPathParams: Record<string, string> = event.pathParameters && typeof event.pathParameters === 'object'
|
|
177
|
+
? { ...event.pathParameters }
|
|
178
|
+
: {};
|
|
179
|
+
|
|
180
|
+
// Merge: pathParameters from original event + extracted params (extractedParams takes priority)
|
|
181
|
+
const params = { ...eventPathParams, ...extractedParams };
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
eventRaw: event,
|
|
185
|
+
eventType: EventType.ApiGateway,
|
|
186
|
+
payload: {
|
|
187
|
+
body: parseJsonBody(event.body, event.isBase64Encoded),
|
|
188
|
+
pathParameters: params,
|
|
189
|
+
queryStringParameters: parseQueryParams(event.queryStringParameters),
|
|
190
|
+
headers: normalizeHeaders(event.headers),
|
|
191
|
+
},
|
|
192
|
+
params,
|
|
193
|
+
context: {
|
|
194
|
+
segment,
|
|
195
|
+
identity,
|
|
196
|
+
requestId: event.requestContext?.requestId,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Normalizes an EventBridge event
|
|
203
|
+
*/
|
|
204
|
+
function normalizeEventBridgeEvent(event: any): NormalizedEvent {
|
|
205
|
+
return {
|
|
206
|
+
eventRaw: event,
|
|
207
|
+
eventType: EventType.EventBridge,
|
|
208
|
+
payload: {
|
|
209
|
+
body: event.detail,
|
|
210
|
+
},
|
|
211
|
+
params: {},
|
|
212
|
+
context: {
|
|
213
|
+
segment: RouteSegment.Internal,
|
|
214
|
+
requestId: event.id,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Normalizes an SQS event
|
|
221
|
+
*/
|
|
222
|
+
function normalizeSqsEvent(event: any): NormalizedEvent {
|
|
223
|
+
const record = event.Records[0];
|
|
224
|
+
let body: Record<string, any> = {};
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
body = JSON.parse(record.body) as Record<string, any>;
|
|
228
|
+
} catch {
|
|
229
|
+
body = { rawBody: record.body };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
eventRaw: event,
|
|
234
|
+
eventType: EventType.Sqs,
|
|
235
|
+
payload: {
|
|
236
|
+
body,
|
|
237
|
+
},
|
|
238
|
+
params: {},
|
|
239
|
+
context: {
|
|
240
|
+
segment: RouteSegment.Internal,
|
|
241
|
+
requestId: record.messageId,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Normalizes a Scheduled event (EventBridge Scheduler / CloudWatch Events rule)
|
|
248
|
+
*/
|
|
249
|
+
function normalizeScheduledEvent(event: any): NormalizedEvent {
|
|
250
|
+
// Extract rule name from resources ARN: arn:aws:events:region:account:rule/RuleName
|
|
251
|
+
const ruleArn = event.resources?.[0];
|
|
252
|
+
const ruleName = ruleArn?.split('/')?.pop() ?? 'default';
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
eventRaw: event,
|
|
256
|
+
eventType: EventType.Scheduled,
|
|
257
|
+
payload: {
|
|
258
|
+
body: event.detail ?? {},
|
|
259
|
+
},
|
|
260
|
+
params: { ruleName },
|
|
261
|
+
context: {
|
|
262
|
+
segment: RouteSegment.Internal,
|
|
263
|
+
requestId: event.id,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Normalizes a Lambda invocation event
|
|
270
|
+
*/
|
|
271
|
+
function normalizeLambdaEvent(event: any): NormalizedEvent {
|
|
272
|
+
return {
|
|
273
|
+
eventRaw: event,
|
|
274
|
+
eventType: EventType.Lambda,
|
|
275
|
+
payload: {
|
|
276
|
+
body: event,
|
|
277
|
+
},
|
|
278
|
+
params: {},
|
|
279
|
+
context: {
|
|
280
|
+
segment: RouteSegment.Internal,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Validates User Pool for a segment
|
|
287
|
+
*/
|
|
288
|
+
function validateSegmentUserPool(
|
|
289
|
+
normalized: NormalizedEvent,
|
|
290
|
+
segment: RouteSegment,
|
|
291
|
+
config: OrchestratorConfig
|
|
292
|
+
): boolean {
|
|
293
|
+
// Public routes don't require validation
|
|
294
|
+
if (segment === RouteSegment.Public) return true;
|
|
295
|
+
|
|
296
|
+
// If no user pool config, skip validation
|
|
297
|
+
const expectedUserPoolId = config.userPools?.[segment];
|
|
298
|
+
if (!expectedUserPoolId) return true;
|
|
299
|
+
|
|
300
|
+
// Validate issuer matches expected user pool
|
|
301
|
+
return validateIssuer(normalized.context.identity, expectedUserPoolId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Checks if a thrown value is an HTTP response (used by middleware to halt execution)
|
|
306
|
+
*/
|
|
307
|
+
function isHttpResponse(value: unknown): value is { statusCode: number; body: string } {
|
|
308
|
+
return (
|
|
309
|
+
typeof value === 'object' &&
|
|
310
|
+
value !== null &&
|
|
311
|
+
'statusCode' in value &&
|
|
312
|
+
typeof (value as any).statusCode === 'number'
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Executes middleware chain.
|
|
318
|
+
* If a middleware throws an HttpResponse-like object (has statusCode),
|
|
319
|
+
* it is treated as an early return (e.g., 403 Forbidden from tenantGuard).
|
|
320
|
+
* If it throws a regular Error, it is re-thrown.
|
|
321
|
+
*/
|
|
322
|
+
async function executeMiddleware(
|
|
323
|
+
middleware: MiddlewareFn[],
|
|
324
|
+
event: NormalizedEvent
|
|
325
|
+
): Promise<NormalizedEvent> {
|
|
326
|
+
let currentEvent = event;
|
|
327
|
+
|
|
328
|
+
for (const mw of middleware) {
|
|
329
|
+
const result = await mw(currentEvent);
|
|
330
|
+
if (result) {
|
|
331
|
+
currentEvent = result;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return currentEvent;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Applies CORS headers to any API Gateway response
|
|
340
|
+
* This ensures CORS works regardless of how the handler builds its response
|
|
341
|
+
*/
|
|
342
|
+
function applyCorsToResponse(response: any): any {
|
|
343
|
+
if (!response || typeof response !== 'object') return response;
|
|
344
|
+
|
|
345
|
+
const corsOrigin = process.env.CORS_ALLOWED_ORIGINS || '*';
|
|
346
|
+
const corsHeaders = process.env.CORS_ALLOWED_HEADERS ||
|
|
347
|
+
'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token,appVersion,app-version,platform,geo,x-forwarded-for,x-real-ip';
|
|
348
|
+
const corsMethods = process.env.CORS_ALLOWED_METHODS || 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
349
|
+
|
|
350
|
+
const existingHeaders = response.headers || {};
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
...response,
|
|
354
|
+
headers: {
|
|
355
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
356
|
+
'Access-Control-Allow-Headers': corsHeaders,
|
|
357
|
+
'Access-Control-Allow-Methods': corsMethods,
|
|
358
|
+
...existingHeaders,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Main dispatch function with all improvements
|
|
365
|
+
*/
|
|
366
|
+
export async function dispatchEvent(
|
|
367
|
+
event: any,
|
|
368
|
+
routes: DispatchRoutes,
|
|
369
|
+
config: OrchestratorConfig = {}
|
|
370
|
+
): Promise<any> {
|
|
371
|
+
const debug = config.debug ?? false;
|
|
372
|
+
|
|
373
|
+
if (debug) {
|
|
374
|
+
console.log('[SEO] Event received:', JSON.stringify(event, null, 2));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const type = detectEventType(event);
|
|
378
|
+
|
|
379
|
+
if (debug) {
|
|
380
|
+
console.log('[SEO] Event type:', type);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Handle API Gateway events
|
|
384
|
+
if (type === EventType.ApiGateway) {
|
|
385
|
+
const method = event.httpMethod?.toLowerCase() as HttpMethod;
|
|
386
|
+
const routePattern = event.resource || event.path;
|
|
387
|
+
const actualPath = event.path || event.resource;
|
|
388
|
+
|
|
389
|
+
// Handle CORS preflight requests automatically
|
|
390
|
+
if (method === 'options') {
|
|
391
|
+
if (debug) {
|
|
392
|
+
console.log('[SEO] Handling OPTIONS preflight request');
|
|
393
|
+
}
|
|
394
|
+
const corsOrigin = process.env.CORS_ALLOWED_ORIGINS || '*';
|
|
395
|
+
const corsHeaders = process.env.CORS_ALLOWED_HEADERS ||
|
|
396
|
+
'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token,appVersion,app-version,platform,geo,x-forwarded-for,x-real-ip';
|
|
397
|
+
const corsMethods = process.env.CORS_ALLOWED_METHODS || 'GET,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
398
|
+
const corsMaxAge = process.env.CORS_MAX_AGE || '600';
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
statusCode: 204,
|
|
402
|
+
headers: {
|
|
403
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
404
|
+
'Access-Control-Allow-Headers': corsHeaders,
|
|
405
|
+
'Access-Control-Allow-Methods': corsMethods,
|
|
406
|
+
'Access-Control-Max-Age': corsMaxAge,
|
|
407
|
+
},
|
|
408
|
+
body: '',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (debug) {
|
|
413
|
+
console.log('[SEO] Method:', method, 'Path:', routePattern, 'Actual:', actualPath);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const apiRoutes = routes.apigateway;
|
|
417
|
+
if (!apiRoutes) {
|
|
418
|
+
return applyCorsToResponse(config.responses?.notFound?.() ?? notFoundResponse('No API routes configured'));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let routeMatch: RouteMatch | null = null;
|
|
422
|
+
|
|
423
|
+
// Use routePattern for finding routes, but extract params from actualPath
|
|
424
|
+
if (isSegmentedRouter(apiRoutes)) {
|
|
425
|
+
routeMatch = findRouteInSegmentsWithActualPath(apiRoutes, method, routePattern, actualPath);
|
|
426
|
+
} else {
|
|
427
|
+
// Flat router - treat as public
|
|
428
|
+
const result = findRouteInRouterWithActualPath(apiRoutes as HttpRouter, method, routePattern, actualPath);
|
|
429
|
+
if (result) {
|
|
430
|
+
routeMatch = {
|
|
431
|
+
handler: result.config.handler,
|
|
432
|
+
params: result.params,
|
|
433
|
+
segment: RouteSegment.Public,
|
|
434
|
+
middleware: [],
|
|
435
|
+
config: result.config,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!routeMatch) {
|
|
441
|
+
if (debug) {
|
|
442
|
+
console.log('[SEO] No route found for:', method, routePattern);
|
|
443
|
+
}
|
|
444
|
+
return applyCorsToResponse(config.responses?.notFound?.() ?? notFoundResponse(`Route not found: ${method.toUpperCase()} ${routePattern}`));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (debug) {
|
|
448
|
+
console.log('[SEO] Route matched:', routeMatch.segment, 'Params:', routeMatch.params);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Normalize event (with JWT verification if configured for this segment)
|
|
452
|
+
const jwtVerificationConfig = config.jwtVerification?.[routeMatch.segment];
|
|
453
|
+
let normalized = await normalizeApiGatewayEvent(
|
|
454
|
+
event,
|
|
455
|
+
routeMatch.segment,
|
|
456
|
+
routeMatch.params,
|
|
457
|
+
config.autoExtractIdentity,
|
|
458
|
+
jwtVerificationConfig
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// If JWT verification is configured but identity is missing, return 401
|
|
462
|
+
if (jwtVerificationConfig && !normalized.context.identity) {
|
|
463
|
+
if (debug) {
|
|
464
|
+
console.log('[SEO] JWT verification failed — no valid identity for segment:', routeMatch.segment);
|
|
465
|
+
}
|
|
466
|
+
return applyCorsToResponse(unauthorizedResponse('Invalid or missing authentication token'));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Validate User Pool (legacy issuer-only check)
|
|
470
|
+
if (!validateSegmentUserPool(normalized, routeMatch.segment, config)) {
|
|
471
|
+
if (debug) {
|
|
472
|
+
console.log('[SEO] User Pool validation failed for segment:', routeMatch.segment);
|
|
473
|
+
}
|
|
474
|
+
return applyCorsToResponse(config.responses?.forbidden?.() ?? forbiddenResponse('Access denied: Invalid token issuer'));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Execute middlewares with error handling for HttpResponse throws
|
|
478
|
+
try {
|
|
479
|
+
// Execute global middleware
|
|
480
|
+
if (config.globalMiddleware?.length) {
|
|
481
|
+
normalized = await executeMiddleware(config.globalMiddleware, normalized);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Execute segment middleware
|
|
485
|
+
if (routeMatch.middleware?.length) {
|
|
486
|
+
normalized = await executeMiddleware(routeMatch.middleware, normalized);
|
|
487
|
+
}
|
|
488
|
+
} catch (thrown) {
|
|
489
|
+
// If middleware threw an HttpResponse (e.g., forbiddenResponse from tenantGuard), return it
|
|
490
|
+
if (isHttpResponse(thrown)) {
|
|
491
|
+
return applyCorsToResponse(thrown);
|
|
492
|
+
}
|
|
493
|
+
// Otherwise, re-throw as unhandled error
|
|
494
|
+
throw thrown;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Execute handler and apply CORS headers to response
|
|
498
|
+
const handlerResponse = await routeMatch.handler(normalized);
|
|
499
|
+
return applyCorsToResponse(handlerResponse);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Handle Scheduled events (EventBridge Scheduler / CloudWatch Events rules)
|
|
503
|
+
if (type === EventType.Scheduled) {
|
|
504
|
+
const normalized = normalizeScheduledEvent(event);
|
|
505
|
+
const ruleName = normalized.params.ruleName;
|
|
506
|
+
const handler = routes.scheduled?.[ruleName] ?? routes.scheduled?.default;
|
|
507
|
+
|
|
508
|
+
if (!handler) {
|
|
509
|
+
if (debug) {
|
|
510
|
+
console.log('[SEO] No Scheduled handler for rule:', ruleName);
|
|
511
|
+
}
|
|
512
|
+
return { statusCode: 404, body: 'Scheduled handler not found' };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return handler(normalized);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Handle EventBridge events.
|
|
519
|
+
//
|
|
520
|
+
// Routing prioriza el campo nativo de AWS `detail-type` (estándar EventBridge),
|
|
521
|
+
// con fallback a `detail.operationName` (convención legacy de versiones <2.3
|
|
522
|
+
// de esta librería). Si ninguno matchea, cae a `routes.eventbridge.default`.
|
|
523
|
+
if (type === EventType.EventBridge) {
|
|
524
|
+
const detailType: string | undefined = event['detail-type'];
|
|
525
|
+
const operationName: string | undefined = event.detail?.operationName;
|
|
526
|
+
const handler =
|
|
527
|
+
(detailType ? routes.eventbridge?.[detailType] : undefined)
|
|
528
|
+
?? (operationName ? routes.eventbridge?.[operationName] : undefined)
|
|
529
|
+
?? routes.eventbridge?.default;
|
|
530
|
+
|
|
531
|
+
if (!handler) {
|
|
532
|
+
if (debug) {
|
|
533
|
+
console.log('[SEO] No EventBridge handler for:', { detailType, operationName });
|
|
534
|
+
}
|
|
535
|
+
return { statusCode: 404, body: 'EventBridge handler not found' };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const normalized = normalizeEventBridgeEvent(event);
|
|
539
|
+
return handler(normalized);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Handle SQS events
|
|
543
|
+
if (type === EventType.Sqs) {
|
|
544
|
+
const queueArn = event.Records[0]?.eventSourceARN;
|
|
545
|
+
const queueName = queueArn?.split(':').pop();
|
|
546
|
+
const handler = routes.sqs?.[queueName] ?? routes.sqs?.default;
|
|
547
|
+
|
|
548
|
+
if (!handler) {
|
|
549
|
+
if (debug) {
|
|
550
|
+
console.log('[SEO] No SQS handler for queue:', queueName);
|
|
551
|
+
}
|
|
552
|
+
return { statusCode: 404, body: 'SQS handler not found' };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const normalized = normalizeSqsEvent(event);
|
|
556
|
+
return handler(normalized);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Handle Lambda invocation
|
|
560
|
+
if (type === EventType.Lambda) {
|
|
561
|
+
const handler = routes.lambda?.default;
|
|
562
|
+
|
|
563
|
+
if (!handler) {
|
|
564
|
+
return { statusCode: 404, body: 'Lambda handler not found' };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const normalized = normalizeLambdaEvent(event);
|
|
568
|
+
return handler(normalized);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Unknown event type
|
|
572
|
+
if (debug) {
|
|
573
|
+
console.log('[SEO] Unknown event type');
|
|
574
|
+
}
|
|
575
|
+
return config.responses?.badRequest?.('Unknown event type') ?? badRequestResponse('Unknown event type');
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Creates an orchestrator instance with pre-configured options
|
|
580
|
+
*/
|
|
581
|
+
export function createOrchestrator(config: OrchestratorConfig = {}) {
|
|
582
|
+
return {
|
|
583
|
+
dispatch: (event: any, routes: DispatchRoutes) => dispatchEvent(event, routes, config),
|
|
584
|
+
config,
|
|
585
|
+
};
|
|
586
|
+
}
|