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
@@ -1,207 +1,207 @@
1
- import { IdentityContext, JwtVerificationPoolConfig } from '../types/routes.js';
2
- import { verifyJwt } from './jwt-verifier.js';
3
-
4
- /**
5
- * Options for extractIdentity when using JWT verification
6
- */
7
- export interface ExtractIdentityOptions {
8
- autoExtract?: boolean;
9
- jwtVerificationConfig?: JwtVerificationPoolConfig;
10
- }
11
-
12
- /**
13
- * Extracts identity context from API Gateway authorizer claims
14
- * Supports multiple API Gateway formats:
15
- * - REST API with Cognito User Pool Authorizer
16
- * - HTTP API with JWT Authorizer
17
- * - REST API with Custom Lambda Authorizer
18
- * - HTTP API with Lambda Authorizer
19
- */
20
-
21
- /**
22
- * Extracts claims from the authorizer context, handling multiple API Gateway formats
23
- * @param authorizer - The authorizer object from requestContext
24
- * @returns Claims object or undefined
25
- */
26
- function extractClaims(authorizer: any): Record<string, any> | undefined {
27
- if (!authorizer) return undefined;
28
-
29
- // 1. REST API with Cognito User Pool Authorizer: authorizer.claims
30
- if (authorizer.claims && typeof authorizer.claims === 'object') {
31
- return authorizer.claims;
32
- }
33
-
34
- // 2. HTTP API with JWT Authorizer: authorizer.jwt.claims
35
- if (authorizer.jwt?.claims && typeof authorizer.jwt.claims === 'object') {
36
- return authorizer.jwt.claims;
37
- }
38
-
39
- // 3. HTTP API with Lambda Authorizer: authorizer.lambda
40
- if (authorizer.lambda && typeof authorizer.lambda === 'object') {
41
- return authorizer.lambda;
42
- }
43
-
44
- // 4. REST API with Custom Lambda Authorizer: claims directly in authorizer
45
- // Check if authorizer has identity-like properties (sub, userId, email, etc.)
46
- if (hasIdentityProperties(authorizer)) {
47
- return authorizer;
48
- }
49
-
50
- return undefined;
51
- }
52
-
53
- /**
54
- * Checks if an object has identity-like properties
55
- */
56
- function hasIdentityProperties(obj: any): boolean {
57
- if (!obj || typeof obj !== 'object') return false;
58
- const identityKeys = ['sub', 'userId', 'user_id', 'email', 'cognito:username', 'iss', 'aud'];
59
- return identityKeys.some(key => key in obj);
60
- }
61
-
62
- /**
63
- * Extracts identity information from the event's authorizer context or Authorization header.
64
- * When jwtVerificationConfig is provided, JWT signatures are cryptographically verified
65
- * against the Cognito JWKS endpoint. Without it, identity is only extracted from
66
- * API Gateway authorizer claims (already verified by API Gateway).
67
- *
68
- * @param event - Raw API Gateway event
69
- * @param optionsOrAutoExtract - Boolean for backwards compatibility, or ExtractIdentityOptions
70
- * @returns Identity context or undefined if not authenticated
71
- */
72
- export async function extractIdentity(event: any, optionsOrAutoExtract?: boolean | ExtractIdentityOptions): Promise<IdentityContext | undefined> {
73
- const options: ExtractIdentityOptions = typeof optionsOrAutoExtract === 'boolean'
74
- ? { autoExtract: optionsOrAutoExtract }
75
- : optionsOrAutoExtract ?? {};
76
-
77
- const { autoExtract = false, jwtVerificationConfig } = options;
78
-
79
- const authorizer = event?.requestContext?.authorizer;
80
- const claims = extractClaims(authorizer);
81
-
82
- // If authorizer claims exist, API Gateway already verified the token — use them directly
83
- if (claims) {
84
- return buildIdentity(claims);
85
- }
86
-
87
- // If autoExtract is enabled, extract identity from Authorization header
88
- if (autoExtract) {
89
- const authHeader = event?.headers?.authorization || event?.headers?.Authorization;
90
- if (authHeader) {
91
- // If jwtVerificationConfig is provided, verify signature cryptographically
92
- if (jwtVerificationConfig) {
93
- const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
94
- const verifiedClaims = await verifyJwt(token, jwtVerificationConfig);
95
- if (verifiedClaims) {
96
- return buildIdentity(verifiedClaims);
97
- }
98
- // Verification failed → return undefined (caller will return 401)
99
- return undefined;
100
- }
101
-
102
- // No jwtVerificationConfig → decode-only fallback (no signature verification)
103
- const decoded = decodeJwtFromHeader(authHeader);
104
- if (decoded) {
105
- return buildIdentity(decoded);
106
- }
107
- }
108
- }
109
-
110
- return undefined;
111
- }
112
-
113
- /**
114
- * Decodes a JWT from the Authorization header without validating signature
115
- * @param authHeader - The Authorization header value (e.g., "Bearer eyJ...")
116
- */
117
- function decodeJwtFromHeader(authHeader: string): Record<string, any> | undefined {
118
- try {
119
- const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
120
- const parts = token.split('.');
121
- if (parts.length !== 3) return undefined;
122
-
123
- const payload = parts[1];
124
- const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
125
- const jsonPayload = Buffer.from(base64, 'base64').toString('utf8');
126
-
127
- return JSON.parse(jsonPayload);
128
- } catch (error) {
129
- console.error('[SEO] Error decoding JWT from header:', error);
130
- return undefined;
131
- }
132
- }
133
-
134
- /**
135
- * Builds an IdentityContext from raw claims
136
- */
137
- function buildIdentity(claims: Record<string, any>): IdentityContext {
138
- return {
139
- userId: claims.userId || claims.user_id || claims['cognito:username'] || claims.sub,
140
- email: claims.email,
141
- groups: parseGroups(claims['cognito:groups'] || claims.groups),
142
- issuer: claims.iss,
143
- claims,
144
- };
145
- }
146
-
147
- /**
148
- * Parses Cognito groups from claims
149
- * Groups can come as a string or array depending on configuration
150
- */
151
- function parseGroups(groups: string | string[] | undefined): string[] {
152
- if (!groups) return [];
153
- if (Array.isArray(groups)) return groups;
154
-
155
- // Cognito sometimes sends groups as a comma-separated string
156
- return groups.split(',').map(g => g.trim()).filter(Boolean);
157
- }
158
-
159
- /**
160
- * Extracts the User Pool ID from the issuer URL
161
- * @param issuer - Cognito issuer URL (e.g., https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxx)
162
- * @returns User Pool ID or undefined
163
- */
164
- export function extractUserPoolId(issuer: string | undefined): string | undefined {
165
- if (!issuer) return undefined;
166
-
167
- // Extract the last segment of the issuer URL
168
- const parts = issuer.split('/');
169
- return parts[parts.length - 1];
170
- }
171
-
172
- /**
173
- * Validates that the token issuer matches the expected User Pool
174
- * @param identity - Extracted identity context
175
- * @param expectedUserPoolId - Expected User Pool ID
176
- * @returns True if issuer matches
177
- */
178
- export function validateIssuer(identity: IdentityContext | undefined, expectedUserPoolId: string): boolean {
179
- if (!identity?.issuer) return false;
180
-
181
- const actualUserPoolId = extractUserPoolId(identity.issuer);
182
- return actualUserPoolId === expectedUserPoolId;
183
- }
184
-
185
- /**
186
- * Checks if the user belongs to any of the specified groups
187
- * @param identity - Extracted identity context
188
- * @param allowedGroups - Groups that grant access
189
- * @returns True if user is in at least one allowed group
190
- */
191
- export function hasAnyGroup(identity: IdentityContext | undefined, allowedGroups: string[]): boolean {
192
- if (!identity?.groups || identity.groups.length === 0) return false;
193
-
194
- return allowedGroups.some(group => identity.groups?.includes(group));
195
- }
196
-
197
- /**
198
- * Checks if the user belongs to all specified groups
199
- * @param identity - Extracted identity context
200
- * @param requiredGroups - Groups required for access
201
- * @returns True if user is in all required groups
202
- */
203
- export function hasAllGroups(identity: IdentityContext | undefined, requiredGroups: string[]): boolean {
204
- if (!identity?.groups || identity.groups.length === 0) return false;
205
-
206
- return requiredGroups.every(group => identity.groups?.includes(group));
207
- }
1
+ import { IdentityContext, JwtVerificationPoolConfig } from '../types/routes.js';
2
+ import { verifyJwt } from './jwt-verifier.js';
3
+
4
+ /**
5
+ * Options for extractIdentity when using JWT verification
6
+ */
7
+ export interface ExtractIdentityOptions {
8
+ autoExtract?: boolean;
9
+ jwtVerificationConfig?: JwtVerificationPoolConfig;
10
+ }
11
+
12
+ /**
13
+ * Extracts identity context from API Gateway authorizer claims
14
+ * Supports multiple API Gateway formats:
15
+ * - REST API with Cognito User Pool Authorizer
16
+ * - HTTP API with JWT Authorizer
17
+ * - REST API with Custom Lambda Authorizer
18
+ * - HTTP API with Lambda Authorizer
19
+ */
20
+
21
+ /**
22
+ * Extracts claims from the authorizer context, handling multiple API Gateway formats
23
+ * @param authorizer - The authorizer object from requestContext
24
+ * @returns Claims object or undefined
25
+ */
26
+ function extractClaims(authorizer: any): Record<string, any> | undefined {
27
+ if (!authorizer) return undefined;
28
+
29
+ // 1. REST API with Cognito User Pool Authorizer: authorizer.claims
30
+ if (authorizer.claims && typeof authorizer.claims === 'object') {
31
+ return authorizer.claims;
32
+ }
33
+
34
+ // 2. HTTP API with JWT Authorizer: authorizer.jwt.claims
35
+ if (authorizer.jwt?.claims && typeof authorizer.jwt.claims === 'object') {
36
+ return authorizer.jwt.claims;
37
+ }
38
+
39
+ // 3. HTTP API with Lambda Authorizer: authorizer.lambda
40
+ if (authorizer.lambda && typeof authorizer.lambda === 'object') {
41
+ return authorizer.lambda;
42
+ }
43
+
44
+ // 4. REST API with Custom Lambda Authorizer: claims directly in authorizer
45
+ // Check if authorizer has identity-like properties (sub, userId, email, etc.)
46
+ if (hasIdentityProperties(authorizer)) {
47
+ return authorizer;
48
+ }
49
+
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * Checks if an object has identity-like properties
55
+ */
56
+ function hasIdentityProperties(obj: any): boolean {
57
+ if (!obj || typeof obj !== 'object') return false;
58
+ const identityKeys = ['sub', 'userId', 'user_id', 'email', 'cognito:username', 'iss', 'aud'];
59
+ return identityKeys.some(key => key in obj);
60
+ }
61
+
62
+ /**
63
+ * Extracts identity information from the event's authorizer context or Authorization header.
64
+ * When jwtVerificationConfig is provided, JWT signatures are cryptographically verified
65
+ * against the Cognito JWKS endpoint. Without it, identity is only extracted from
66
+ * API Gateway authorizer claims (already verified by API Gateway).
67
+ *
68
+ * @param event - Raw API Gateway event
69
+ * @param optionsOrAutoExtract - Boolean for backwards compatibility, or ExtractIdentityOptions
70
+ * @returns Identity context or undefined if not authenticated
71
+ */
72
+ export async function extractIdentity(event: any, optionsOrAutoExtract?: boolean | ExtractIdentityOptions): Promise<IdentityContext | undefined> {
73
+ const options: ExtractIdentityOptions = typeof optionsOrAutoExtract === 'boolean'
74
+ ? { autoExtract: optionsOrAutoExtract }
75
+ : optionsOrAutoExtract ?? {};
76
+
77
+ const { autoExtract = false, jwtVerificationConfig } = options;
78
+
79
+ const authorizer = event?.requestContext?.authorizer;
80
+ const claims = extractClaims(authorizer);
81
+
82
+ // If authorizer claims exist, API Gateway already verified the token — use them directly
83
+ if (claims) {
84
+ return buildIdentity(claims);
85
+ }
86
+
87
+ // If autoExtract is enabled, extract identity from Authorization header
88
+ if (autoExtract) {
89
+ const authHeader = event?.headers?.authorization || event?.headers?.Authorization;
90
+ if (authHeader) {
91
+ // If jwtVerificationConfig is provided, verify signature cryptographically
92
+ if (jwtVerificationConfig) {
93
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
94
+ const verifiedClaims = await verifyJwt(token, jwtVerificationConfig);
95
+ if (verifiedClaims) {
96
+ return buildIdentity(verifiedClaims);
97
+ }
98
+ // Verification failed → return undefined (caller will return 401)
99
+ return undefined;
100
+ }
101
+
102
+ // No jwtVerificationConfig → decode-only fallback (no signature verification)
103
+ const decoded = decodeJwtFromHeader(authHeader);
104
+ if (decoded) {
105
+ return buildIdentity(decoded);
106
+ }
107
+ }
108
+ }
109
+
110
+ return undefined;
111
+ }
112
+
113
+ /**
114
+ * Decodes a JWT from the Authorization header without validating signature
115
+ * @param authHeader - The Authorization header value (e.g., "Bearer eyJ...")
116
+ */
117
+ function decodeJwtFromHeader(authHeader: string): Record<string, any> | undefined {
118
+ try {
119
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
120
+ const parts = token.split('.');
121
+ if (parts.length !== 3) return undefined;
122
+
123
+ const payload = parts[1];
124
+ const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
125
+ const jsonPayload = Buffer.from(base64, 'base64').toString('utf8');
126
+
127
+ return JSON.parse(jsonPayload);
128
+ } catch (error) {
129
+ console.error('[SEO] Error decoding JWT from header:', error);
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Builds an IdentityContext from raw claims
136
+ */
137
+ function buildIdentity(claims: Record<string, any>): IdentityContext {
138
+ return {
139
+ userId: claims.userId || claims.user_id || claims['cognito:username'] || claims.sub,
140
+ email: claims.email,
141
+ groups: parseGroups(claims['cognito:groups'] || claims.groups),
142
+ issuer: claims.iss,
143
+ claims,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Parses Cognito groups from claims
149
+ * Groups can come as a string or array depending on configuration
150
+ */
151
+ function parseGroups(groups: string | string[] | undefined): string[] {
152
+ if (!groups) return [];
153
+ if (Array.isArray(groups)) return groups;
154
+
155
+ // Cognito sometimes sends groups as a comma-separated string
156
+ return groups.split(',').map(g => g.trim()).filter(Boolean);
157
+ }
158
+
159
+ /**
160
+ * Extracts the User Pool ID from the issuer URL
161
+ * @param issuer - Cognito issuer URL (e.g., https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxx)
162
+ * @returns User Pool ID or undefined
163
+ */
164
+ export function extractUserPoolId(issuer: string | undefined): string | undefined {
165
+ if (!issuer) return undefined;
166
+
167
+ // Extract the last segment of the issuer URL
168
+ const parts = issuer.split('/');
169
+ return parts[parts.length - 1];
170
+ }
171
+
172
+ /**
173
+ * Validates that the token issuer matches the expected User Pool
174
+ * @param identity - Extracted identity context
175
+ * @param expectedUserPoolId - Expected User Pool ID
176
+ * @returns True if issuer matches
177
+ */
178
+ export function validateIssuer(identity: IdentityContext | undefined, expectedUserPoolId: string): boolean {
179
+ if (!identity?.issuer) return false;
180
+
181
+ const actualUserPoolId = extractUserPoolId(identity.issuer);
182
+ return actualUserPoolId === expectedUserPoolId;
183
+ }
184
+
185
+ /**
186
+ * Checks if the user belongs to any of the specified groups
187
+ * @param identity - Extracted identity context
188
+ * @param allowedGroups - Groups that grant access
189
+ * @returns True if user is in at least one allowed group
190
+ */
191
+ export function hasAnyGroup(identity: IdentityContext | undefined, allowedGroups: string[]): boolean {
192
+ if (!identity?.groups || identity.groups.length === 0) return false;
193
+
194
+ return allowedGroups.some(group => identity.groups?.includes(group));
195
+ }
196
+
197
+ /**
198
+ * Checks if the user belongs to all specified groups
199
+ * @param identity - Extracted identity context
200
+ * @param requiredGroups - Groups required for access
201
+ * @returns True if user is in all required groups
202
+ */
203
+ export function hasAllGroups(identity: IdentityContext | undefined, requiredGroups: string[]): boolean {
204
+ if (!identity?.groups || identity.groups.length === 0) return false;
205
+
206
+ return requiredGroups.every(group => identity.groups?.includes(group));
207
+ }
@@ -1,2 +1,2 @@
1
- export * from './extractor.js';
2
- export { verifyJwt } from './jwt-verifier.js';
1
+ export * from './extractor.js';
2
+ export { verifyJwt } from './jwt-verifier.js';
@@ -1,41 +1,41 @@
1
- import { CognitoJwtVerifier } from 'aws-jwt-verify';
2
- import type { JwtVerificationPoolConfig } from '../types/routes.js';
3
-
4
- /**
5
- * Module-level cache of CognitoJwtVerifier instances keyed by userPoolId.
6
- * Persists across Lambda warm invocations. Each verifier internally caches JWKS.
7
- */
8
- const verifierCache = new Map<string, ReturnType<typeof CognitoJwtVerifier.create>>();
9
-
10
- function getVerifier(poolConfig: JwtVerificationPoolConfig) {
11
- const cacheKey = poolConfig.userPoolId;
12
-
13
- if (!verifierCache.has(cacheKey)) {
14
- const verifier = CognitoJwtVerifier.create({
15
- userPoolId: poolConfig.userPoolId,
16
- clientId: poolConfig.clientId ?? null,
17
- tokenUse: poolConfig.tokenUse ?? null,
18
- });
19
- verifierCache.set(cacheKey, verifier);
20
- }
21
-
22
- return verifierCache.get(cacheKey)!;
23
- }
24
-
25
- /**
26
- * Verifies a JWT token against a Cognito User Pool's JWKS.
27
- * Returns the verified payload (claims) or undefined if verification fails.
28
- */
29
- export async function verifyJwt(
30
- token: string,
31
- poolConfig: JwtVerificationPoolConfig
32
- ): Promise<Record<string, any> | undefined> {
33
- try {
34
- const verifier = getVerifier(poolConfig);
35
- const payload = await verifier.verify(token);
36
- return payload as unknown as Record<string, any>;
37
- } catch (error: any) {
38
- console.error('[SEO] JWT verification failed:', error?.message || error);
39
- return undefined;
40
- }
41
- }
1
+ import { CognitoJwtVerifier } from 'aws-jwt-verify';
2
+ import type { JwtVerificationPoolConfig } from '../types/routes.js';
3
+
4
+ /**
5
+ * Module-level cache of CognitoJwtVerifier instances keyed by userPoolId.
6
+ * Persists across Lambda warm invocations. Each verifier internally caches JWKS.
7
+ */
8
+ const verifierCache = new Map<string, ReturnType<typeof CognitoJwtVerifier.create>>();
9
+
10
+ function getVerifier(poolConfig: JwtVerificationPoolConfig) {
11
+ const cacheKey = poolConfig.userPoolId;
12
+
13
+ if (!verifierCache.has(cacheKey)) {
14
+ const verifier = CognitoJwtVerifier.create({
15
+ userPoolId: poolConfig.userPoolId,
16
+ clientId: poolConfig.clientId ?? null,
17
+ tokenUse: poolConfig.tokenUse ?? null,
18
+ });
19
+ verifierCache.set(cacheKey, verifier);
20
+ }
21
+
22
+ return verifierCache.get(cacheKey)!;
23
+ }
24
+
25
+ /**
26
+ * Verifies a JWT token against a Cognito User Pool's JWKS.
27
+ * Returns the verified payload (claims) or undefined if verification fails.
28
+ */
29
+ export async function verifyJwt(
30
+ token: string,
31
+ poolConfig: JwtVerificationPoolConfig
32
+ ): Promise<Record<string, any> | undefined> {
33
+ try {
34
+ const verifier = getVerifier(poolConfig);
35
+ const payload = await verifier.verify(token);
36
+ return payload as unknown as Record<string, any>;
37
+ } catch (error: any) {
38
+ console.error('[SEO] JWT verification failed:', error?.message || error);
39
+ return undefined;
40
+ }
41
+ }