serverless-event-orchestrator 2.2.0 → 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 (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +489 -489
  3. package/dist/dispatcher.d.ts +6 -1
  4. package/dist/dispatcher.d.ts.map +1 -1
  5. package/dist/dispatcher.js +31 -7
  6. package/dist/dispatcher.js.map +1 -1
  7. package/jest.config.js +32 -32
  8. package/package.json +82 -81
  9. package/src/dispatcher.ts +586 -558
  10. package/src/http/body-parser.ts +60 -60
  11. package/src/http/cors.ts +76 -76
  12. package/src/http/index.ts +3 -3
  13. package/src/http/response.ts +209 -209
  14. package/src/identity/extractor.ts +207 -207
  15. package/src/identity/index.ts +2 -2
  16. package/src/identity/jwt-verifier.ts +41 -41
  17. package/src/index.ts +128 -128
  18. package/src/middleware/crm-guard.ts +51 -51
  19. package/src/middleware/index.ts +3 -3
  20. package/src/middleware/init-tenant-context.ts +59 -59
  21. package/src/middleware/tenant-guard.ts +54 -54
  22. package/src/tenant/TenantContext.ts +115 -115
  23. package/src/tenant/helpers.ts +112 -112
  24. package/src/tenant/index.ts +21 -21
  25. package/src/tenant/types.ts +101 -101
  26. package/src/types/event-type.enum.ts +21 -21
  27. package/src/types/index.ts +2 -2
  28. package/src/types/routes.ts +218 -218
  29. package/src/utils/headers.ts +72 -72
  30. package/src/utils/index.ts +2 -2
  31. package/src/utils/path-matcher.ts +84 -84
  32. package/tests/cors.test.ts +133 -133
  33. package/tests/dispatcher.test.ts +795 -715
  34. package/tests/headers.test.ts +99 -99
  35. package/tests/identity.test.ts +301 -301
  36. package/tests/middleware/crm-guard.test.ts +69 -69
  37. package/tests/middleware/init-tenant-context.test.ts +147 -147
  38. package/tests/middleware/tenant-guard.test.ts +100 -100
  39. package/tests/path-matcher.test.ts +102 -102
  40. package/tests/response.test.ts +155 -155
  41. package/tests/tenant/TenantContext.test.ts +134 -134
  42. package/tests/tenant/helpers.test.ts +187 -187
  43. package/tsconfig.json +24 -24
@@ -1,301 +1,301 @@
1
- import {
2
- extractIdentity,
3
- extractUserPoolId,
4
- validateIssuer,
5
- hasAnyGroup,
6
- hasAllGroups
7
- } from '../src/identity/extractor';
8
- import { IdentityContext } from '../src/types/routes';
9
-
10
- describe('extractIdentity', () => {
11
- it('should return undefined when no claims present', async () => {
12
- const event = { requestContext: {} };
13
- expect(await extractIdentity(event)).toBeUndefined();
14
- });
15
-
16
- it('should return undefined when event has no requestContext', async () => {
17
- const event = {};
18
- expect(await extractIdentity(event)).toBeUndefined();
19
- });
20
-
21
- it('should extract identity from Cognito claims', async () => {
22
- const event = {
23
- requestContext: {
24
- authorizer: {
25
- claims: {
26
- sub: 'user-123',
27
- email: 'user@example.com',
28
- 'cognito:groups': 'Admin,User',
29
- iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
30
- }
31
- }
32
- }
33
- };
34
-
35
- const identity = await extractIdentity(event);
36
-
37
- expect(identity).toEqual({
38
- userId: 'user-123',
39
- email: 'user@example.com',
40
- groups: ['Admin', 'User'],
41
- issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123',
42
- claims: event.requestContext.authorizer.claims
43
- });
44
- });
45
-
46
- it('should handle cognito:username as userId fallback', async () => {
47
- const event = {
48
- requestContext: {
49
- authorizer: {
50
- claims: {
51
- 'cognito:username': 'john_doe'
52
- }
53
- }
54
- }
55
- };
56
-
57
- const identity = await extractIdentity(event);
58
- expect(identity?.userId).toBe('john_doe');
59
- });
60
-
61
- it('should handle groups as array', async () => {
62
- const event = {
63
- requestContext: {
64
- authorizer: {
65
- claims: {
66
- sub: 'user-123',
67
- 'cognito:groups': ['Admin', 'User']
68
- }
69
- }
70
- }
71
- };
72
-
73
- const identity = await extractIdentity(event);
74
- expect(identity?.groups).toEqual(['Admin', 'User']);
75
- });
76
-
77
- // HTTP API with JWT Authorizer tests
78
- it('should extract identity from HTTP API JWT Authorizer format', async () => {
79
- const event = {
80
- requestContext: {
81
- authorizer: {
82
- jwt: {
83
- claims: {
84
- sub: 'user-456',
85
- email: 'jwt@example.com',
86
- iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_JWT123'
87
- }
88
- }
89
- }
90
- }
91
- };
92
-
93
- const identity = await extractIdentity(event);
94
-
95
- expect(identity).toEqual({
96
- userId: 'user-456',
97
- email: 'jwt@example.com',
98
- groups: [],
99
- issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_JWT123',
100
- claims: event.requestContext.authorizer.jwt.claims
101
- });
102
- });
103
-
104
- // HTTP API with Lambda Authorizer tests
105
- it('should extract identity from HTTP API Lambda Authorizer format', async () => {
106
- const event = {
107
- requestContext: {
108
- authorizer: {
109
- lambda: {
110
- userId: 'lambda-user-789',
111
- email: 'lambda@example.com',
112
- groups: ['Premium', 'Verified']
113
- }
114
- }
115
- }
116
- };
117
-
118
- const identity = await extractIdentity(event);
119
-
120
- expect(identity?.userId).toBe('lambda-user-789');
121
- expect(identity?.email).toBe('lambda@example.com');
122
- expect(identity?.groups).toEqual(['Premium', 'Verified']);
123
- });
124
-
125
- // REST API with Custom Lambda Authorizer (flat structure)
126
- it('should extract identity from custom Lambda Authorizer with flat structure', async () => {
127
- const event = {
128
- requestContext: {
129
- authorizer: {
130
- sub: 'custom-user-101',
131
- email: 'custom@example.com',
132
- iss: 'https://custom-issuer.com'
133
- }
134
- }
135
- };
136
-
137
- const identity = await extractIdentity(event);
138
-
139
- expect(identity?.userId).toBe('custom-user-101');
140
- expect(identity?.email).toBe('custom@example.com');
141
- expect(identity?.issuer).toBe('https://custom-issuer.com');
142
- });
143
-
144
- it('should handle userId fallback from user_id in custom authorizer', async () => {
145
- const event = {
146
- requestContext: {
147
- authorizer: {
148
- user_id: 'underscore-user',
149
- email: 'test@example.com'
150
- }
151
- }
152
- };
153
-
154
- const identity = await extractIdentity(event);
155
- expect(identity?.userId).toBe('underscore-user');
156
- });
157
-
158
- it('should return undefined when authorizer has no identity properties', async () => {
159
- const event = {
160
- requestContext: {
161
- authorizer: {
162
- principalId: 'some-principal',
163
- integrationLatency: 123
164
- }
165
- }
166
- };
167
-
168
- expect(await extractIdentity(event)).toBeUndefined();
169
- });
170
-
171
- it('should extract identity from Authorization header with autoExtract using decode-only fallback', async () => {
172
- const payload = {
173
- sub: 'user-header',
174
- email: 'header@example.com',
175
- 'cognito:groups': 'Admin',
176
- iss: 'https://cognito-idp.com'
177
- };
178
- const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64').replace(/=/g, '');
179
- const mockToken = `header.${encodedPayload}.signature`;
180
-
181
- const event = {
182
- headers: {
183
- Authorization: `Bearer ${mockToken}`
184
- }
185
- };
186
-
187
- // Without jwtVerificationConfig, autoExtract decodes the token without signature verification
188
- const identity = await extractIdentity(event, true);
189
- expect(identity).toEqual({
190
- userId: 'user-header',
191
- email: 'header@example.com',
192
- groups: ['Admin'],
193
- issuer: 'https://cognito-idp.com',
194
- claims: payload
195
- });
196
- });
197
-
198
- it('should not extract from Authorization header when autoExtract is false', async () => {
199
- const event = {
200
- headers: {
201
- Authorization: 'Bearer some.token.here'
202
- }
203
- };
204
- expect(await extractIdentity(event, false)).toBeUndefined();
205
- });
206
- });
207
-
208
- describe('extractUserPoolId', () => {
209
- it('should extract User Pool ID from issuer URL', () => {
210
- const issuer = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123';
211
- expect(extractUserPoolId(issuer)).toBe('us-east-1_ABC123');
212
- });
213
-
214
- it('should return undefined for undefined issuer', () => {
215
- expect(extractUserPoolId(undefined)).toBeUndefined();
216
- });
217
-
218
- it('should handle issuer with trailing slash', () => {
219
- const issuer = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XYZ789';
220
- expect(extractUserPoolId(issuer)).toBe('us-east-1_XYZ789');
221
- });
222
- });
223
-
224
- describe('validateIssuer', () => {
225
- const validIdentity: IdentityContext = {
226
- userId: 'user-123',
227
- issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
228
- };
229
-
230
- it('should return true when User Pool matches', () => {
231
- expect(validateIssuer(validIdentity, 'us-east-1_ABC123')).toBe(true);
232
- });
233
-
234
- it('should return false when User Pool does not match', () => {
235
- expect(validateIssuer(validIdentity, 'us-east-1_WRONG')).toBe(false);
236
- });
237
-
238
- it('should return false when identity is undefined', () => {
239
- expect(validateIssuer(undefined, 'us-east-1_ABC123')).toBe(false);
240
- });
241
-
242
- it('should return false when issuer is missing', () => {
243
- const identity: IdentityContext = { userId: 'user-123' };
244
- expect(validateIssuer(identity, 'us-east-1_ABC123')).toBe(false);
245
- });
246
- });
247
-
248
- describe('hasAnyGroup', () => {
249
- const identity: IdentityContext = {
250
- userId: 'user-123',
251
- groups: ['User', 'Premium']
252
- };
253
-
254
- it('should return true if user has any of the allowed groups', () => {
255
- expect(hasAnyGroup(identity, ['Admin', 'Premium'])).toBe(true);
256
- expect(hasAnyGroup(identity, ['User'])).toBe(true);
257
- });
258
-
259
- it('should return false if user has none of the allowed groups', () => {
260
- expect(hasAnyGroup(identity, ['Admin', 'SuperAdmin'])).toBe(false);
261
- });
262
-
263
- it('should return false for undefined identity', () => {
264
- expect(hasAnyGroup(undefined, ['Admin'])).toBe(false);
265
- });
266
-
267
- it('should return false for identity with no groups', () => {
268
- const noGroupsIdentity: IdentityContext = { userId: 'user-123' };
269
- expect(hasAnyGroup(noGroupsIdentity, ['Admin'])).toBe(false);
270
- });
271
-
272
- it('should return false for empty groups array', () => {
273
- const emptyGroupsIdentity: IdentityContext = { userId: 'user-123', groups: [] };
274
- expect(hasAnyGroup(emptyGroupsIdentity, ['Admin'])).toBe(false);
275
- });
276
- });
277
-
278
- describe('hasAllGroups', () => {
279
- const identity: IdentityContext = {
280
- userId: 'user-123',
281
- groups: ['User', 'Premium', 'Verified']
282
- };
283
-
284
- it('should return true if user has all required groups', () => {
285
- expect(hasAllGroups(identity, ['User', 'Premium'])).toBe(true);
286
- expect(hasAllGroups(identity, ['Verified'])).toBe(true);
287
- });
288
-
289
- it('should return false if user is missing any required group', () => {
290
- expect(hasAllGroups(identity, ['User', 'Admin'])).toBe(false);
291
- });
292
-
293
- it('should return false for undefined identity', () => {
294
- expect(hasAllGroups(undefined, ['Admin'])).toBe(false);
295
- });
296
-
297
- it('should return false for identity with no groups', () => {
298
- const noGroupsIdentity: IdentityContext = { userId: 'user-123' };
299
- expect(hasAllGroups(noGroupsIdentity, ['Admin'])).toBe(false);
300
- });
301
- });
1
+ import {
2
+ extractIdentity,
3
+ extractUserPoolId,
4
+ validateIssuer,
5
+ hasAnyGroup,
6
+ hasAllGroups
7
+ } from '../src/identity/extractor';
8
+ import { IdentityContext } from '../src/types/routes';
9
+
10
+ describe('extractIdentity', () => {
11
+ it('should return undefined when no claims present', async () => {
12
+ const event = { requestContext: {} };
13
+ expect(await extractIdentity(event)).toBeUndefined();
14
+ });
15
+
16
+ it('should return undefined when event has no requestContext', async () => {
17
+ const event = {};
18
+ expect(await extractIdentity(event)).toBeUndefined();
19
+ });
20
+
21
+ it('should extract identity from Cognito claims', async () => {
22
+ const event = {
23
+ requestContext: {
24
+ authorizer: {
25
+ claims: {
26
+ sub: 'user-123',
27
+ email: 'user@example.com',
28
+ 'cognito:groups': 'Admin,User',
29
+ iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
30
+ }
31
+ }
32
+ }
33
+ };
34
+
35
+ const identity = await extractIdentity(event);
36
+
37
+ expect(identity).toEqual({
38
+ userId: 'user-123',
39
+ email: 'user@example.com',
40
+ groups: ['Admin', 'User'],
41
+ issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123',
42
+ claims: event.requestContext.authorizer.claims
43
+ });
44
+ });
45
+
46
+ it('should handle cognito:username as userId fallback', async () => {
47
+ const event = {
48
+ requestContext: {
49
+ authorizer: {
50
+ claims: {
51
+ 'cognito:username': 'john_doe'
52
+ }
53
+ }
54
+ }
55
+ };
56
+
57
+ const identity = await extractIdentity(event);
58
+ expect(identity?.userId).toBe('john_doe');
59
+ });
60
+
61
+ it('should handle groups as array', async () => {
62
+ const event = {
63
+ requestContext: {
64
+ authorizer: {
65
+ claims: {
66
+ sub: 'user-123',
67
+ 'cognito:groups': ['Admin', 'User']
68
+ }
69
+ }
70
+ }
71
+ };
72
+
73
+ const identity = await extractIdentity(event);
74
+ expect(identity?.groups).toEqual(['Admin', 'User']);
75
+ });
76
+
77
+ // HTTP API with JWT Authorizer tests
78
+ it('should extract identity from HTTP API JWT Authorizer format', async () => {
79
+ const event = {
80
+ requestContext: {
81
+ authorizer: {
82
+ jwt: {
83
+ claims: {
84
+ sub: 'user-456',
85
+ email: 'jwt@example.com',
86
+ iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_JWT123'
87
+ }
88
+ }
89
+ }
90
+ }
91
+ };
92
+
93
+ const identity = await extractIdentity(event);
94
+
95
+ expect(identity).toEqual({
96
+ userId: 'user-456',
97
+ email: 'jwt@example.com',
98
+ groups: [],
99
+ issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_JWT123',
100
+ claims: event.requestContext.authorizer.jwt.claims
101
+ });
102
+ });
103
+
104
+ // HTTP API with Lambda Authorizer tests
105
+ it('should extract identity from HTTP API Lambda Authorizer format', async () => {
106
+ const event = {
107
+ requestContext: {
108
+ authorizer: {
109
+ lambda: {
110
+ userId: 'lambda-user-789',
111
+ email: 'lambda@example.com',
112
+ groups: ['Premium', 'Verified']
113
+ }
114
+ }
115
+ }
116
+ };
117
+
118
+ const identity = await extractIdentity(event);
119
+
120
+ expect(identity?.userId).toBe('lambda-user-789');
121
+ expect(identity?.email).toBe('lambda@example.com');
122
+ expect(identity?.groups).toEqual(['Premium', 'Verified']);
123
+ });
124
+
125
+ // REST API with Custom Lambda Authorizer (flat structure)
126
+ it('should extract identity from custom Lambda Authorizer with flat structure', async () => {
127
+ const event = {
128
+ requestContext: {
129
+ authorizer: {
130
+ sub: 'custom-user-101',
131
+ email: 'custom@example.com',
132
+ iss: 'https://custom-issuer.com'
133
+ }
134
+ }
135
+ };
136
+
137
+ const identity = await extractIdentity(event);
138
+
139
+ expect(identity?.userId).toBe('custom-user-101');
140
+ expect(identity?.email).toBe('custom@example.com');
141
+ expect(identity?.issuer).toBe('https://custom-issuer.com');
142
+ });
143
+
144
+ it('should handle userId fallback from user_id in custom authorizer', async () => {
145
+ const event = {
146
+ requestContext: {
147
+ authorizer: {
148
+ user_id: 'underscore-user',
149
+ email: 'test@example.com'
150
+ }
151
+ }
152
+ };
153
+
154
+ const identity = await extractIdentity(event);
155
+ expect(identity?.userId).toBe('underscore-user');
156
+ });
157
+
158
+ it('should return undefined when authorizer has no identity properties', async () => {
159
+ const event = {
160
+ requestContext: {
161
+ authorizer: {
162
+ principalId: 'some-principal',
163
+ integrationLatency: 123
164
+ }
165
+ }
166
+ };
167
+
168
+ expect(await extractIdentity(event)).toBeUndefined();
169
+ });
170
+
171
+ it('should extract identity from Authorization header with autoExtract using decode-only fallback', async () => {
172
+ const payload = {
173
+ sub: 'user-header',
174
+ email: 'header@example.com',
175
+ 'cognito:groups': 'Admin',
176
+ iss: 'https://cognito-idp.com'
177
+ };
178
+ const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64').replace(/=/g, '');
179
+ const mockToken = `header.${encodedPayload}.signature`;
180
+
181
+ const event = {
182
+ headers: {
183
+ Authorization: `Bearer ${mockToken}`
184
+ }
185
+ };
186
+
187
+ // Without jwtVerificationConfig, autoExtract decodes the token without signature verification
188
+ const identity = await extractIdentity(event, true);
189
+ expect(identity).toEqual({
190
+ userId: 'user-header',
191
+ email: 'header@example.com',
192
+ groups: ['Admin'],
193
+ issuer: 'https://cognito-idp.com',
194
+ claims: payload
195
+ });
196
+ });
197
+
198
+ it('should not extract from Authorization header when autoExtract is false', async () => {
199
+ const event = {
200
+ headers: {
201
+ Authorization: 'Bearer some.token.here'
202
+ }
203
+ };
204
+ expect(await extractIdentity(event, false)).toBeUndefined();
205
+ });
206
+ });
207
+
208
+ describe('extractUserPoolId', () => {
209
+ it('should extract User Pool ID from issuer URL', () => {
210
+ const issuer = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123';
211
+ expect(extractUserPoolId(issuer)).toBe('us-east-1_ABC123');
212
+ });
213
+
214
+ it('should return undefined for undefined issuer', () => {
215
+ expect(extractUserPoolId(undefined)).toBeUndefined();
216
+ });
217
+
218
+ it('should handle issuer with trailing slash', () => {
219
+ const issuer = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XYZ789';
220
+ expect(extractUserPoolId(issuer)).toBe('us-east-1_XYZ789');
221
+ });
222
+ });
223
+
224
+ describe('validateIssuer', () => {
225
+ const validIdentity: IdentityContext = {
226
+ userId: 'user-123',
227
+ issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
228
+ };
229
+
230
+ it('should return true when User Pool matches', () => {
231
+ expect(validateIssuer(validIdentity, 'us-east-1_ABC123')).toBe(true);
232
+ });
233
+
234
+ it('should return false when User Pool does not match', () => {
235
+ expect(validateIssuer(validIdentity, 'us-east-1_WRONG')).toBe(false);
236
+ });
237
+
238
+ it('should return false when identity is undefined', () => {
239
+ expect(validateIssuer(undefined, 'us-east-1_ABC123')).toBe(false);
240
+ });
241
+
242
+ it('should return false when issuer is missing', () => {
243
+ const identity: IdentityContext = { userId: 'user-123' };
244
+ expect(validateIssuer(identity, 'us-east-1_ABC123')).toBe(false);
245
+ });
246
+ });
247
+
248
+ describe('hasAnyGroup', () => {
249
+ const identity: IdentityContext = {
250
+ userId: 'user-123',
251
+ groups: ['User', 'Premium']
252
+ };
253
+
254
+ it('should return true if user has any of the allowed groups', () => {
255
+ expect(hasAnyGroup(identity, ['Admin', 'Premium'])).toBe(true);
256
+ expect(hasAnyGroup(identity, ['User'])).toBe(true);
257
+ });
258
+
259
+ it('should return false if user has none of the allowed groups', () => {
260
+ expect(hasAnyGroup(identity, ['Admin', 'SuperAdmin'])).toBe(false);
261
+ });
262
+
263
+ it('should return false for undefined identity', () => {
264
+ expect(hasAnyGroup(undefined, ['Admin'])).toBe(false);
265
+ });
266
+
267
+ it('should return false for identity with no groups', () => {
268
+ const noGroupsIdentity: IdentityContext = { userId: 'user-123' };
269
+ expect(hasAnyGroup(noGroupsIdentity, ['Admin'])).toBe(false);
270
+ });
271
+
272
+ it('should return false for empty groups array', () => {
273
+ const emptyGroupsIdentity: IdentityContext = { userId: 'user-123', groups: [] };
274
+ expect(hasAnyGroup(emptyGroupsIdentity, ['Admin'])).toBe(false);
275
+ });
276
+ });
277
+
278
+ describe('hasAllGroups', () => {
279
+ const identity: IdentityContext = {
280
+ userId: 'user-123',
281
+ groups: ['User', 'Premium', 'Verified']
282
+ };
283
+
284
+ it('should return true if user has all required groups', () => {
285
+ expect(hasAllGroups(identity, ['User', 'Premium'])).toBe(true);
286
+ expect(hasAllGroups(identity, ['Verified'])).toBe(true);
287
+ });
288
+
289
+ it('should return false if user is missing any required group', () => {
290
+ expect(hasAllGroups(identity, ['User', 'Admin'])).toBe(false);
291
+ });
292
+
293
+ it('should return false for undefined identity', () => {
294
+ expect(hasAllGroups(undefined, ['Admin'])).toBe(false);
295
+ });
296
+
297
+ it('should return false for identity with no groups', () => {
298
+ const noGroupsIdentity: IdentityContext = { userId: 'user-123' };
299
+ expect(hasAllGroups(noGroupsIdentity, ['Admin'])).toBe(false);
300
+ });
301
+ });