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.
Files changed (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +489 -434
  3. package/dist/dispatcher.d.ts +6 -1
  4. package/dist/dispatcher.d.ts.map +1 -1
  5. package/dist/dispatcher.js +66 -7
  6. package/dist/dispatcher.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/types/event-type.enum.d.ts +1 -0
  11. package/dist/types/event-type.enum.d.ts.map +1 -1
  12. package/dist/types/event-type.enum.js +1 -0
  13. package/dist/types/event-type.enum.js.map +1 -1
  14. package/dist/types/routes.d.ts +6 -0
  15. package/dist/types/routes.d.ts.map +1 -1
  16. package/jest.config.js +32 -32
  17. package/package.json +82 -81
  18. package/src/dispatcher.ts +586 -519
  19. package/src/http/body-parser.ts +60 -60
  20. package/src/http/cors.ts +76 -76
  21. package/src/http/index.ts +3 -3
  22. package/src/http/response.ts +209 -209
  23. package/src/identity/extractor.ts +207 -207
  24. package/src/identity/index.ts +2 -2
  25. package/src/identity/jwt-verifier.ts +41 -41
  26. package/src/index.ts +128 -127
  27. package/src/middleware/crm-guard.ts +51 -51
  28. package/src/middleware/index.ts +3 -3
  29. package/src/middleware/init-tenant-context.ts +59 -59
  30. package/src/middleware/tenant-guard.ts +54 -54
  31. package/src/tenant/TenantContext.ts +115 -115
  32. package/src/tenant/helpers.ts +112 -112
  33. package/src/tenant/index.ts +21 -21
  34. package/src/tenant/types.ts +101 -101
  35. package/src/types/event-type.enum.ts +21 -20
  36. package/src/types/index.ts +2 -2
  37. package/src/types/routes.ts +218 -211
  38. package/src/utils/headers.ts +72 -72
  39. package/src/utils/index.ts +2 -2
  40. package/src/utils/path-matcher.ts +84 -84
  41. package/tests/cors.test.ts +133 -133
  42. package/tests/dispatcher.test.ts +795 -715
  43. package/tests/headers.test.ts +99 -99
  44. package/tests/identity.test.ts +301 -301
  45. package/tests/middleware/crm-guard.test.ts +69 -69
  46. package/tests/middleware/init-tenant-context.test.ts +147 -147
  47. package/tests/middleware/tenant-guard.test.ts +100 -100
  48. package/tests/path-matcher.test.ts +102 -102
  49. package/tests/response.test.ts +155 -155
  50. package/tests/tenant/TenantContext.test.ts +134 -134
  51. package/tests/tenant/helpers.test.ts +187 -187
  52. package/tsconfig.json +24 -24
package/README.md CHANGED
@@ -1,434 +1,489 @@
1
- # serverless-event-orchestrator
2
-
3
- [![npm version](https://badge.fury.io/js/serverless-event-orchestrator.svg)](https://www.npmjs.com/package/serverless-event-orchestrator)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
-
6
- A lightweight, type-safe event dispatcher and middleware orchestrator for AWS Lambda. Designed for hexagonal architectures with support for segmented routing (public, private, backoffice), Cognito User Pool validation, and built-in infrastructure middlewares.
7
-
8
- ## Features
9
-
10
- - **Multi-Trigger Support**: Handle HTTP (API Gateway), SQS, EventBridge, and Lambda invocations with a single handler
11
- - **Segmented Routing**: Organize routes by security context (`public`, `private`, `backoffice`, `internal`)
12
- - **Path Parameters**: Built-in support for dynamic routes like `/users/{id}`
13
- - **Identity Aware**: Cryptographic JWT signature verification via `aws-jwt-verify`, Cognito User Pool validation per segment
14
- - **Middleware Support**: Global and per-segment middleware chains
15
- - **Zero Config CORS**: Built-in CORS handling with sensible defaults
16
- - **Response Utilities**: Standardized response helpers (success, error, etc.)
17
- - **TypeScript First**: Full type safety with exported interfaces
18
-
19
- ## Installation
20
-
21
- ```bash
22
- npm install serverless-event-orchestrator
23
- ```
24
-
25
- ## Quick Start
26
-
27
- ### Basic Usage (Flat Routes)
28
-
29
- ```typescript
30
- import { dispatchEvent, HttpRouter, successResponse } from 'serverless-event-orchestrator';
31
-
32
- const routes: HttpRouter = {
33
- get: {
34
- '/users': {
35
- handler: async (event) => {
36
- return successResponse({ users: [] });
37
- }
38
- },
39
- '/users/{id}': {
40
- handler: async (event) => {
41
- const userId = event.params.id;
42
- return successResponse({ id: userId, name: 'John' });
43
- }
44
- }
45
- },
46
- post: {
47
- '/users': {
48
- handler: async (event) => {
49
- const body = event.payload.body;
50
- return successResponse({ created: true, data: body });
51
- }
52
- }
53
- }
54
- };
55
-
56
- export const handler = async (event: any) => {
57
- return dispatchEvent(event, { apigateway: routes });
58
- };
59
- ```
60
-
61
- ### Segmented Routes (Recommended)
62
-
63
- Organize routes by security context for cleaner code and automatic validation:
64
-
65
- ```typescript
66
- import {
67
- dispatchEvent,
68
- SegmentedHttpRouter,
69
- successResponse,
70
- forbiddenResponse
71
- } from 'serverless-event-orchestrator';
72
-
73
- const routes: SegmentedHttpRouter = {
74
- // No authentication required
75
- public: {
76
- post: {
77
- '/auth/login': { handler: loginHandler },
78
- '/auth/register': { handler: registerHandler }
79
- }
80
- },
81
-
82
- // Requires authenticated user (Client User Pool)
83
- private: {
84
- get: {
85
- '/me': { handler: getProfileHandler },
86
- '/orders': { handler: getOrdersHandler }
87
- },
88
- put: {
89
- '/me': { handler: updateProfileHandler }
90
- }
91
- },
92
-
93
- // Requires admin user (Backoffice User Pool)
94
- backoffice: {
95
- get: {
96
- '/admin/users': { handler: listAllUsersHandler }
97
- },
98
- delete: {
99
- '/admin/users/{id}': { handler: deleteUserHandler }
100
- }
101
- },
102
-
103
- // Internal Lambda-to-Lambda calls
104
- internal: {
105
- post: {
106
- '/internal/sync': { handler: syncDataHandler }
107
- }
108
- }
109
- };
110
-
111
- export const handler = async (event: any) => {
112
- return dispatchEvent(event, { apigateway: routes }, {
113
- debug: process.env.DEBUG === 'true',
114
- autoExtractIdentity: true,
115
- jwtVerification: {
116
- private: {
117
- userPoolId: process.env.USER_POOL_ID!,
118
- clientId: null,
119
- },
120
- backoffice: {
121
- userPoolId: process.env.ADMIN_POOL_ID!,
122
- clientId: null,
123
- },
124
- },
125
- });
126
- };
127
- ```
128
-
129
- ### With Middleware
130
-
131
- ```typescript
132
- import {
133
- AdvancedSegmentedRouter,
134
- NormalizedEvent,
135
- forbiddenResponse
136
- } from 'serverless-event-orchestrator';
137
-
138
- // Custom middleware
139
- const validateAdminRole = async (event: NormalizedEvent) => {
140
- const groups = event.context.identity?.groups ?? [];
141
- if (!groups.includes('Admins')) {
142
- throw forbiddenResponse('Admin role required');
143
- }
144
- return event;
145
- };
146
-
147
- const routes: AdvancedSegmentedRouter = {
148
- public: {
149
- routes: {
150
- get: { '/health': { handler: healthCheck } }
151
- }
152
- },
153
- backoffice: {
154
- middleware: [validateAdminRole],
155
- routes: {
156
- get: { '/admin/dashboard': { handler: dashboardHandler } }
157
- }
158
- }
159
- };
160
- ```
161
-
162
- ## Handling Multiple Event Types
163
-
164
- ```typescript
165
- import { dispatchEvent, DispatchRoutes, NormalizedEvent } from 'serverless-event-orchestrator';
166
-
167
- const routes: DispatchRoutes = {
168
- // HTTP routes
169
- apigateway: {
170
- public: {
171
- get: { '/status': { handler: statusHandler } }
172
- }
173
- },
174
-
175
- // EventBridge events
176
- eventbridge: {
177
- 'user.created': async (event: NormalizedEvent) => {
178
- console.log('User created:', event.payload.body);
179
- },
180
- 'order.completed': async (event: NormalizedEvent) => {
181
- console.log('Order completed:', event.payload.body);
182
- },
183
- default: async (event: NormalizedEvent) => {
184
- console.log('Unknown event:', event.payload.body);
185
- }
186
- },
187
-
188
- // SQS queues
189
- sqs: {
190
- 'notification-queue': async (event: NormalizedEvent) => {
191
- console.log('Notification:', event.payload.body);
192
- },
193
- default: async (event: NormalizedEvent) => {
194
- console.log('Unknown queue message:', event.payload.body);
195
- }
196
- }
197
- };
198
-
199
- export const handler = async (event: any) => {
200
- return dispatchEvent(event, routes);
201
- };
202
- ```
203
-
204
- ## Response Utilities
205
-
206
- Built-in response helpers for consistent API responses:
207
-
208
- ```typescript
209
- import {
210
- successResponse,
211
- createdResponse,
212
- badRequestResponse,
213
- unauthorizedResponse,
214
- forbiddenResponse,
215
- notFoundResponse,
216
- conflictResponse,
217
- validationErrorResponse,
218
- internalErrorResponse,
219
- customErrorResponse
220
- } from 'serverless-event-orchestrator';
221
-
222
- // Success responses
223
- successResponse({ user: { id: 1 } });
224
- // { statusCode: 200, body: '{"status":200,"code":"SUCCESS","data":{"user":{"id":1}}}' }
225
-
226
- createdResponse({ id: 123 });
227
- // { statusCode: 201, body: '{"status":201,"code":"CREATED","data":{"id":123}}' }
228
-
229
- // Error responses
230
- badRequestResponse('Invalid email format');
231
- notFoundResponse('User not found');
232
-
233
- // Custom error codes (your domain-specific codes)
234
- enum MyErrorCodes {
235
- USER_SUSPENDED = 'USER_SUSPENDED',
236
- QUOTA_EXCEEDED = 'QUOTA_EXCEEDED'
237
- }
238
-
239
- const codeToStatus = {
240
- [MyErrorCodes.USER_SUSPENDED]: 403,
241
- [MyErrorCodes.QUOTA_EXCEEDED]: 429
242
- };
243
-
244
- customErrorResponse(MyErrorCodes.QUOTA_EXCEEDED, 'API quota exceeded', codeToStatus);
245
- ```
246
-
247
- ## Identity & Security
248
-
249
- ### JWT Signature Verification (v2.0+)
250
-
251
- JWTs from the `Authorization` header are cryptographically verified against Cognito's JWKS endpoint using [`aws-jwt-verify`](https://github.com/awslabs/aws-jwt-verify). This prevents accepting fabricated tokens with arbitrary claims.
252
-
253
- ```typescript
254
- export const handler = async (event: any) => {
255
- return dispatchEvent(event, { apigateway: routes }, {
256
- autoExtractIdentity: true,
257
- jwtVerification: {
258
- // Verify tokens for private routes against the Portal User Pool
259
- private: {
260
- userPoolId: process.env.COGNITO_PORTAL_USER_POOL_ID!,
261
- clientId: null, // null = skip client ID check
262
- },
263
- // Verify tokens for backoffice routes against the Backoffice User Pool
264
- backoffice: {
265
- userPoolId: process.env.COGNITO_BACKOFFICE_USER_POOL_ID!,
266
- clientId: null,
267
- },
268
- },
269
- });
270
- };
271
- ```
272
-
273
- **How it works:**
274
-
275
- - If `event.requestContext.authorizer.claims` exists (API Gateway authorizer), those claims are used directly (already verified by API Gateway)
276
- - If no authorizer claims exist and `autoExtractIdentity` + `jwtVerification` are configured, the `Authorization` header JWT is verified cryptographically
277
- - If verification fails, the dispatcher returns **401 Unauthorized**
278
- - Public and internal segments don't require `jwtVerification`
279
- - JWKS keys are cached at the module level (persists across Lambda warm starts)
280
-
281
- **`JwtVerificationPoolConfig` options:**
282
-
283
- | Property | Type | Description |
284
- |----------|------|-------------|
285
- | `userPoolId` | `string` | Cognito User Pool ID (e.g., `us-east-1_ABC123`) |
286
- | `clientId` | `string \| string[] \| null` | App Client ID(s). `null` to skip client ID verification |
287
- | `tokenUse` | `'id' \| 'access' \| null` | Expected token type. `null` to accept either |
288
-
289
- ### Working with Identity in Handlers
290
-
291
- ```typescript
292
- import {
293
- hasAnyGroup,
294
- hasAllGroups
295
- } from 'serverless-event-orchestrator';
296
-
297
- const myHandler = async (event: NormalizedEvent) => {
298
- const identity = event.context.identity;
299
-
300
- // Access user info
301
- console.log(identity?.userId); // Cognito sub
302
- console.log(identity?.email); // User email
303
- console.log(identity?.groups); // Cognito groups
304
- console.log(identity?.claims); // All JWT claims
305
-
306
- // Check groups
307
- if (hasAnyGroup(identity, ['Admins', 'Moderators'])) {
308
- // User has admin or moderator role
309
- }
310
-
311
- if (hasAllGroups(identity, ['Premium', 'Verified'])) {
312
- // User has both premium and verified status
313
- }
314
- };
315
- ```
316
-
317
- ## CORS Handling
318
-
319
- ```typescript
320
- import { withCors, applyCorsHeaders } from 'serverless-event-orchestrator';
321
-
322
- // Option 1: Wrap handler
323
- const handler = withCors(async (event) => {
324
- return successResponse({ data: 'Hello' });
325
- }, {
326
- origins: ['https://myapp.com', 'https://admin.myapp.com'],
327
- credentials: true,
328
- maxAge: 86400
329
- });
330
-
331
- // Option 2: Apply to response
332
- const response = successResponse({ data: 'Hello' });
333
- return applyCorsHeaders(response, { origins: '*' });
334
- ```
335
-
336
- ## Configuration
337
-
338
- ```typescript
339
- import { dispatchEvent, OrchestratorConfig } from 'serverless-event-orchestrator';
340
-
341
- const config: OrchestratorConfig = {
342
- // Enable debug logging
343
- debug: process.env.NODE_ENV !== 'production',
344
-
345
- // Extract identity from Authorization header JWT
346
- autoExtractIdentity: true,
347
-
348
- // JWT signature verification per segment (v2.0+)
349
- jwtVerification: {
350
- private: {
351
- userPoolId: 'us-east-1_ABC123',
352
- clientId: null,
353
- },
354
- backoffice: {
355
- userPoolId: 'us-east-1_XYZ789',
356
- clientId: null,
357
- },
358
- },
359
-
360
- // Global middleware (runs for all routes)
361
- globalMiddleware: [
362
- async (event) => {
363
- console.log('Request:', event.context.requestId);
364
- return event;
365
- }
366
- ],
367
-
368
- // Custom response handlers
369
- responses: {
370
- notFound: () => ({ statusCode: 404, body: JSON.stringify({ error: 'Not found' }) }),
371
- forbidden: () => ({ statusCode: 403, body: JSON.stringify({ error: 'Access denied' }) })
372
- }
373
- };
374
-
375
- export const handler = async (event: any) => {
376
- return dispatchEvent(event, routes, config);
377
- };
378
- ```
379
-
380
- ## API Reference
381
-
382
- ### Core Functions
383
-
384
- | Function | Description |
385
- |----------|-------------|
386
- | `dispatchEvent(event, routes, config?)` | Main dispatcher function |
387
- | `createOrchestrator(config)` | Creates a pre-configured dispatcher |
388
- | `detectEventType(event)` | Detects AWS event type |
389
-
390
- ### Response Helpers
391
-
392
- | Function | Status Code |
393
- |----------|-------------|
394
- | `successResponse(data?, code?)` | 200 |
395
- | `createdResponse(data?, code?)` | 201 |
396
- | `badRequestResponse(message?, code?)` | 400 |
397
- | `unauthorizedResponse(message?, code?)` | 401 |
398
- | `forbiddenResponse(message?, code?)` | 403 |
399
- | `notFoundResponse(message?, code?)` | 404 |
400
- | `conflictResponse(message?, code?)` | 409 |
401
- | `validationErrorResponse(message?, code?)` | 422 |
402
- | `internalErrorResponse(message?, code?)` | 500 |
403
-
404
- ### Identity Functions
405
-
406
- | Function | Description |
407
- |----------|-------------|
408
- | `extractIdentity(event, options?)` | Extracts and verifies Cognito claims (async) |
409
- | `verifyJwt(token, poolConfig)` | Verifies a JWT against Cognito JWKS |
410
- | `validateIssuer(identity, userPoolId)` | Validates token issuer |
411
- | `hasAnyGroup(identity, groups)` | Checks if user has any of the groups |
412
- | `hasAllGroups(identity, groups)` | Checks if user has all groups |
413
-
414
- ## TypeScript Support
415
-
416
- All types are exported for full TypeScript support:
417
-
418
- ```typescript
419
- import type {
420
- HttpRouter,
421
- SegmentedHttpRouter,
422
- AdvancedSegmentedRouter,
423
- NormalizedEvent,
424
- IdentityContext,
425
- RouteConfig,
426
- OrchestratorConfig,
427
- JwtVerificationPoolConfig,
428
- MiddlewareFn
429
- } from 'serverless-event-orchestrator';
430
- ```
431
-
432
- ## License
433
-
434
- MIT 2024
1
+ # serverless-event-orchestrator
2
+
3
+ [![npm version](https://badge.fury.io/js/serverless-event-orchestrator.svg)](https://www.npmjs.com/package/serverless-event-orchestrator)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A lightweight, type-safe event dispatcher and middleware orchestrator for AWS Lambda. Designed for hexagonal architectures with support for segmented routing (public, private, backoffice), Cognito User Pool validation, and built-in infrastructure middlewares.
7
+
8
+ ## Features
9
+
10
+ - **Multi-Trigger Support**: Handle HTTP (API Gateway), SQS, EventBridge, Scheduled Events (cron/rate), and Lambda invocations with a single handler
11
+ - **Segmented Routing**: Organize routes by security context (`public`, `private`, `backoffice`, `internal`)
12
+ - **Path Parameters**: Built-in support for dynamic routes like `/users/{id}`
13
+ - **Identity Aware**: Cryptographic JWT signature verification via `aws-jwt-verify`, Cognito User Pool validation per segment
14
+ - **Middleware Support**: Global and per-segment middleware chains
15
+ - **Zero Config CORS**: Built-in CORS handling with sensible defaults
16
+ - **Response Utilities**: Standardized response helpers (success, error, etc.)
17
+ - **TypeScript First**: Full type safety with exported interfaces
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install serverless-event-orchestrator
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### Basic Usage (Flat Routes)
28
+
29
+ ```typescript
30
+ import { dispatchEvent, HttpRouter, successResponse } from 'serverless-event-orchestrator';
31
+
32
+ const routes: HttpRouter = {
33
+ get: {
34
+ '/users': {
35
+ handler: async (event) => {
36
+ return successResponse({ users: [] });
37
+ }
38
+ },
39
+ '/users/{id}': {
40
+ handler: async (event) => {
41
+ const userId = event.params.id;
42
+ return successResponse({ id: userId, name: 'John' });
43
+ }
44
+ }
45
+ },
46
+ post: {
47
+ '/users': {
48
+ handler: async (event) => {
49
+ const body = event.payload.body;
50
+ return successResponse({ created: true, data: body });
51
+ }
52
+ }
53
+ }
54
+ };
55
+
56
+ export const handler = async (event: any) => {
57
+ return dispatchEvent(event, { apigateway: routes });
58
+ };
59
+ ```
60
+
61
+ ### Segmented Routes (Recommended)
62
+
63
+ Organize routes by security context for cleaner code and automatic validation:
64
+
65
+ ```typescript
66
+ import {
67
+ dispatchEvent,
68
+ SegmentedHttpRouter,
69
+ successResponse,
70
+ forbiddenResponse
71
+ } from 'serverless-event-orchestrator';
72
+
73
+ const routes: SegmentedHttpRouter = {
74
+ // No authentication required
75
+ public: {
76
+ post: {
77
+ '/auth/login': { handler: loginHandler },
78
+ '/auth/register': { handler: registerHandler }
79
+ }
80
+ },
81
+
82
+ // Requires authenticated user (Client User Pool)
83
+ private: {
84
+ get: {
85
+ '/me': { handler: getProfileHandler },
86
+ '/orders': { handler: getOrdersHandler }
87
+ },
88
+ put: {
89
+ '/me': { handler: updateProfileHandler }
90
+ }
91
+ },
92
+
93
+ // Requires admin user (Backoffice User Pool)
94
+ backoffice: {
95
+ get: {
96
+ '/admin/users': { handler: listAllUsersHandler }
97
+ },
98
+ delete: {
99
+ '/admin/users/{id}': { handler: deleteUserHandler }
100
+ }
101
+ },
102
+
103
+ // Internal Lambda-to-Lambda calls
104
+ internal: {
105
+ post: {
106
+ '/internal/sync': { handler: syncDataHandler }
107
+ }
108
+ }
109
+ };
110
+
111
+ export const handler = async (event: any) => {
112
+ return dispatchEvent(event, { apigateway: routes }, {
113
+ debug: process.env.DEBUG === 'true',
114
+ autoExtractIdentity: true,
115
+ jwtVerification: {
116
+ private: {
117
+ userPoolId: process.env.USER_POOL_ID!,
118
+ clientId: null,
119
+ },
120
+ backoffice: {
121
+ userPoolId: process.env.ADMIN_POOL_ID!,
122
+ clientId: null,
123
+ },
124
+ },
125
+ });
126
+ };
127
+ ```
128
+
129
+ ### With Middleware
130
+
131
+ ```typescript
132
+ import {
133
+ AdvancedSegmentedRouter,
134
+ NormalizedEvent,
135
+ forbiddenResponse
136
+ } from 'serverless-event-orchestrator';
137
+
138
+ // Custom middleware
139
+ const validateAdminRole = async (event: NormalizedEvent) => {
140
+ const groups = event.context.identity?.groups ?? [];
141
+ if (!groups.includes('Admins')) {
142
+ throw forbiddenResponse('Admin role required');
143
+ }
144
+ return event;
145
+ };
146
+
147
+ const routes: AdvancedSegmentedRouter = {
148
+ public: {
149
+ routes: {
150
+ get: { '/health': { handler: healthCheck } }
151
+ }
152
+ },
153
+ backoffice: {
154
+ middleware: [validateAdminRole],
155
+ routes: {
156
+ get: { '/admin/dashboard': { handler: dashboardHandler } }
157
+ }
158
+ }
159
+ };
160
+ ```
161
+
162
+ ## Handling Multiple Event Types
163
+
164
+ ```typescript
165
+ import { dispatchEvent, DispatchRoutes, NormalizedEvent } from 'serverless-event-orchestrator';
166
+
167
+ const routes: DispatchRoutes = {
168
+ // HTTP routes
169
+ apigateway: {
170
+ public: {
171
+ get: { '/status': { handler: statusHandler } }
172
+ }
173
+ },
174
+
175
+ // EventBridge events
176
+ eventbridge: {
177
+ 'user.created': async (event: NormalizedEvent) => {
178
+ console.log('User created:', event.payload.body);
179
+ },
180
+ 'order.completed': async (event: NormalizedEvent) => {
181
+ console.log('Order completed:', event.payload.body);
182
+ },
183
+ default: async (event: NormalizedEvent) => {
184
+ console.log('Unknown event:', event.payload.body);
185
+ }
186
+ },
187
+
188
+ // SQS queues
189
+ sqs: {
190
+ 'notification-queue': async (event: NormalizedEvent) => {
191
+ console.log('Notification:', event.payload.body);
192
+ },
193
+ default: async (event: NormalizedEvent) => {
194
+ console.log('Unknown queue message:', event.payload.body);
195
+ }
196
+ },
197
+
198
+ // Scheduled events (EventBridge Scheduler / CloudWatch Events rules)
199
+ scheduled: {
200
+ // Route by rule name (extracted from the event's resources ARN)
201
+ 'MyDailyCronRule': async (event: NormalizedEvent) => {
202
+ console.log('Daily cron triggered, rule:', event.params.ruleName);
203
+ },
204
+ // Fallback for any unmatched scheduled event
205
+ default: async (event: NormalizedEvent) => {
206
+ console.log('Scheduled event:', event.params.ruleName);
207
+ }
208
+ }
209
+ };
210
+
211
+ export const handler = async (event: any) => {
212
+ return dispatchEvent(event, routes);
213
+ };
214
+ ```
215
+
216
+ ## Scheduled Events
217
+
218
+ Supports EventBridge Scheduler and CloudWatch Events rules (cron/rate expressions). Events with `source: "aws.events"` and `detail-type: "Scheduled Event"` are automatically detected and routed.
219
+
220
+ ```typescript
221
+ import { dispatchEvent, DispatchRoutes, ScheduledRoutes } from 'serverless-event-orchestrator';
222
+
223
+ const scheduledRouter: ScheduledRoutes = {
224
+ // Route by rule name (extracted from resources ARN)
225
+ 'PropertyExpirationSchedule': async (event) => {
226
+ // Run expiration logic
227
+ return { statusCode: 200, body: 'OK' };
228
+ },
229
+ // Fallback handler
230
+ default: async (event) => {
231
+ console.log('Unhandled scheduled event:', event.params.ruleName);
232
+ }
233
+ };
234
+
235
+ const routes: DispatchRoutes = {
236
+ apigateway: httpRouter,
237
+ scheduled: scheduledRouter,
238
+ };
239
+ ```
240
+
241
+ **How routing works:**
242
+ - The rule name is extracted from `event.resources[0]` (the last segment after `/` in the ARN)
243
+ - First tries to match by exact rule name, then falls back to `default`
244
+ - The rule name is available in `event.params.ruleName`
245
+ - Scheduled events are assigned `segment: "internal"` (no authentication required)
246
+
247
+ **SAM template example:**
248
+ ```yaml
249
+ Events:
250
+ MyCronRule:
251
+ Type: Schedule
252
+ Properties:
253
+ Schedule: "cron(0 7 * * ? *)"
254
+ Description: "Run daily at 07:00 UTC"
255
+ Enabled: true
256
+ ```
257
+
258
+ ## Response Utilities
259
+
260
+ Built-in response helpers for consistent API responses:
261
+
262
+ ```typescript
263
+ import {
264
+ successResponse,
265
+ createdResponse,
266
+ badRequestResponse,
267
+ unauthorizedResponse,
268
+ forbiddenResponse,
269
+ notFoundResponse,
270
+ conflictResponse,
271
+ validationErrorResponse,
272
+ internalErrorResponse,
273
+ customErrorResponse
274
+ } from 'serverless-event-orchestrator';
275
+
276
+ // Success responses
277
+ successResponse({ user: { id: 1 } });
278
+ // { statusCode: 200, body: '{"status":200,"code":"SUCCESS","data":{"user":{"id":1}}}' }
279
+
280
+ createdResponse({ id: 123 });
281
+ // { statusCode: 201, body: '{"status":201,"code":"CREATED","data":{"id":123}}' }
282
+
283
+ // Error responses
284
+ badRequestResponse('Invalid email format');
285
+ notFoundResponse('User not found');
286
+
287
+ // Custom error codes (your domain-specific codes)
288
+ enum MyErrorCodes {
289
+ USER_SUSPENDED = 'USER_SUSPENDED',
290
+ QUOTA_EXCEEDED = 'QUOTA_EXCEEDED'
291
+ }
292
+
293
+ const codeToStatus = {
294
+ [MyErrorCodes.USER_SUSPENDED]: 403,
295
+ [MyErrorCodes.QUOTA_EXCEEDED]: 429
296
+ };
297
+
298
+ customErrorResponse(MyErrorCodes.QUOTA_EXCEEDED, 'API quota exceeded', codeToStatus);
299
+ ```
300
+
301
+ ## Identity & Security
302
+
303
+ ### JWT Signature Verification (v2.0+)
304
+
305
+ JWTs from the `Authorization` header are cryptographically verified against Cognito's JWKS endpoint using [`aws-jwt-verify`](https://github.com/awslabs/aws-jwt-verify). This prevents accepting fabricated tokens with arbitrary claims.
306
+
307
+ ```typescript
308
+ export const handler = async (event: any) => {
309
+ return dispatchEvent(event, { apigateway: routes }, {
310
+ autoExtractIdentity: true,
311
+ jwtVerification: {
312
+ // Verify tokens for private routes against the Portal User Pool
313
+ private: {
314
+ userPoolId: process.env.COGNITO_PORTAL_USER_POOL_ID!,
315
+ clientId: null, // null = skip client ID check
316
+ },
317
+ // Verify tokens for backoffice routes against the Backoffice User Pool
318
+ backoffice: {
319
+ userPoolId: process.env.COGNITO_BACKOFFICE_USER_POOL_ID!,
320
+ clientId: null,
321
+ },
322
+ },
323
+ });
324
+ };
325
+ ```
326
+
327
+ **How it works:**
328
+
329
+ - If `event.requestContext.authorizer.claims` exists (API Gateway authorizer), those claims are used directly (already verified by API Gateway)
330
+ - If no authorizer claims exist and `autoExtractIdentity` + `jwtVerification` are configured, the `Authorization` header JWT is verified cryptographically
331
+ - If verification fails, the dispatcher returns **401 Unauthorized**
332
+ - Public and internal segments don't require `jwtVerification`
333
+ - JWKS keys are cached at the module level (persists across Lambda warm starts)
334
+
335
+ **`JwtVerificationPoolConfig` options:**
336
+
337
+ | Property | Type | Description |
338
+ |----------|------|-------------|
339
+ | `userPoolId` | `string` | Cognito User Pool ID (e.g., `us-east-1_ABC123`) |
340
+ | `clientId` | `string \| string[] \| null` | App Client ID(s). `null` to skip client ID verification |
341
+ | `tokenUse` | `'id' \| 'access' \| null` | Expected token type. `null` to accept either |
342
+
343
+ ### Working with Identity in Handlers
344
+
345
+ ```typescript
346
+ import {
347
+ hasAnyGroup,
348
+ hasAllGroups
349
+ } from 'serverless-event-orchestrator';
350
+
351
+ const myHandler = async (event: NormalizedEvent) => {
352
+ const identity = event.context.identity;
353
+
354
+ // Access user info
355
+ console.log(identity?.userId); // Cognito sub
356
+ console.log(identity?.email); // User email
357
+ console.log(identity?.groups); // Cognito groups
358
+ console.log(identity?.claims); // All JWT claims
359
+
360
+ // Check groups
361
+ if (hasAnyGroup(identity, ['Admins', 'Moderators'])) {
362
+ // User has admin or moderator role
363
+ }
364
+
365
+ if (hasAllGroups(identity, ['Premium', 'Verified'])) {
366
+ // User has both premium and verified status
367
+ }
368
+ };
369
+ ```
370
+
371
+ ## CORS Handling
372
+
373
+ ```typescript
374
+ import { withCors, applyCorsHeaders } from 'serverless-event-orchestrator';
375
+
376
+ // Option 1: Wrap handler
377
+ const handler = withCors(async (event) => {
378
+ return successResponse({ data: 'Hello' });
379
+ }, {
380
+ origins: ['https://myapp.com', 'https://admin.myapp.com'],
381
+ credentials: true,
382
+ maxAge: 86400
383
+ });
384
+
385
+ // Option 2: Apply to response
386
+ const response = successResponse({ data: 'Hello' });
387
+ return applyCorsHeaders(response, { origins: '*' });
388
+ ```
389
+
390
+ ## Configuration
391
+
392
+ ```typescript
393
+ import { dispatchEvent, OrchestratorConfig } from 'serverless-event-orchestrator';
394
+
395
+ const config: OrchestratorConfig = {
396
+ // Enable debug logging
397
+ debug: process.env.NODE_ENV !== 'production',
398
+
399
+ // Extract identity from Authorization header JWT
400
+ autoExtractIdentity: true,
401
+
402
+ // JWT signature verification per segment (v2.0+)
403
+ jwtVerification: {
404
+ private: {
405
+ userPoolId: 'us-east-1_ABC123',
406
+ clientId: null,
407
+ },
408
+ backoffice: {
409
+ userPoolId: 'us-east-1_XYZ789',
410
+ clientId: null,
411
+ },
412
+ },
413
+
414
+ // Global middleware (runs for all routes)
415
+ globalMiddleware: [
416
+ async (event) => {
417
+ console.log('Request:', event.context.requestId);
418
+ return event;
419
+ }
420
+ ],
421
+
422
+ // Custom response handlers
423
+ responses: {
424
+ notFound: () => ({ statusCode: 404, body: JSON.stringify({ error: 'Not found' }) }),
425
+ forbidden: () => ({ statusCode: 403, body: JSON.stringify({ error: 'Access denied' }) })
426
+ }
427
+ };
428
+
429
+ export const handler = async (event: any) => {
430
+ return dispatchEvent(event, routes, config);
431
+ };
432
+ ```
433
+
434
+ ## API Reference
435
+
436
+ ### Core Functions
437
+
438
+ | Function | Description |
439
+ |----------|-------------|
440
+ | `dispatchEvent(event, routes, config?)` | Main dispatcher function |
441
+ | `createOrchestrator(config)` | Creates a pre-configured dispatcher |
442
+ | `detectEventType(event)` | Detects AWS event type |
443
+
444
+ ### Response Helpers
445
+
446
+ | Function | Status Code |
447
+ |----------|-------------|
448
+ | `successResponse(data?, code?)` | 200 |
449
+ | `createdResponse(data?, code?)` | 201 |
450
+ | `badRequestResponse(message?, code?)` | 400 |
451
+ | `unauthorizedResponse(message?, code?)` | 401 |
452
+ | `forbiddenResponse(message?, code?)` | 403 |
453
+ | `notFoundResponse(message?, code?)` | 404 |
454
+ | `conflictResponse(message?, code?)` | 409 |
455
+ | `validationErrorResponse(message?, code?)` | 422 |
456
+ | `internalErrorResponse(message?, code?)` | 500 |
457
+
458
+ ### Identity Functions
459
+
460
+ | Function | Description |
461
+ |----------|-------------|
462
+ | `extractIdentity(event, options?)` | Extracts and verifies Cognito claims (async) |
463
+ | `verifyJwt(token, poolConfig)` | Verifies a JWT against Cognito JWKS |
464
+ | `validateIssuer(identity, userPoolId)` | Validates token issuer |
465
+ | `hasAnyGroup(identity, groups)` | Checks if user has any of the groups |
466
+ | `hasAllGroups(identity, groups)` | Checks if user has all groups |
467
+
468
+ ## TypeScript Support
469
+
470
+ All types are exported for full TypeScript support:
471
+
472
+ ```typescript
473
+ import type {
474
+ HttpRouter,
475
+ SegmentedHttpRouter,
476
+ AdvancedSegmentedRouter,
477
+ NormalizedEvent,
478
+ IdentityContext,
479
+ RouteConfig,
480
+ OrchestratorConfig,
481
+ JwtVerificationPoolConfig,
482
+ MiddlewareFn,
483
+ ScheduledRoutes
484
+ } from 'serverless-event-orchestrator';
485
+ ```
486
+
487
+ ## License
488
+
489
+ MIT 2024