serverless-event-orchestrator 1.0.1
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 -0
- package/README.md +377 -0
- package/dist/dispatcher.d.ts +18 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +345 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/http/body-parser.d.ts +27 -0
- package/dist/http/body-parser.d.ts.map +1 -0
- package/dist/http/body-parser.js +56 -0
- package/dist/http/body-parser.js.map +1 -0
- package/dist/http/cors.d.ts +32 -0
- package/dist/http/cors.d.ts.map +1 -0
- package/dist/http/cors.js +69 -0
- package/dist/http/cors.js.map +1 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +20 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/response.d.ts +104 -0
- package/dist/http/response.d.ts.map +1 -0
- package/dist/http/response.js +164 -0
- package/dist/http/response.js.map +1 -0
- package/dist/identity/extractor.d.ts +39 -0
- package/dist/identity/extractor.d.ts.map +1 -0
- package/dist/identity/extractor.js +88 -0
- package/dist/identity/extractor.js.map +1 -0
- package/dist/identity/index.d.ts +2 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +18 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/types/event-type.enum.d.ts +20 -0
- package/dist/types/event-type.enum.d.ts.map +1 -0
- package/dist/types/event-type.enum.js +25 -0
- package/dist/types/event-type.enum.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +19 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/routes.d.ts +163 -0
- package/dist/types/routes.d.ts.map +1 -0
- package/dist/types/routes.js +3 -0
- package/dist/types/routes.js.map +1 -0
- package/dist/utils/headers.d.ts +28 -0
- package/dist/utils/headers.d.ts.map +1 -0
- package/dist/utils/headers.js +61 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +19 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/path-matcher.d.ts +33 -0
- package/dist/utils/path-matcher.d.ts.map +1 -0
- package/dist/utils/path-matcher.js +74 -0
- package/dist/utils/path-matcher.js.map +1 -0
- package/jest.config.js +32 -0
- package/package.json +68 -0
- package/src/dispatcher.ts +415 -0
- package/src/http/body-parser.ts +60 -0
- package/src/http/cors.ts +76 -0
- package/src/http/index.ts +3 -0
- package/src/http/response.ts +194 -0
- package/src/identity/extractor.ts +89 -0
- package/src/identity/index.ts +1 -0
- package/src/index.ts +92 -0
- package/src/types/event-type.enum.ts +20 -0
- package/src/types/index.ts +2 -0
- package/src/types/routes.ts +182 -0
- package/src/utils/headers.ts +72 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/path-matcher.ts +79 -0
- package/tests/cors.test.ts +133 -0
- package/tests/dispatcher.test.ts +425 -0
- package/tests/headers.test.ts +99 -0
- package/tests/identity.test.ts +171 -0
- package/tests/path-matcher.test.ts +102 -0
- package/tests/response.test.ts +155 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response utilities for consistent HTTP responses
|
|
3
|
+
* Agnostic to domain-specific error codes - allows injection of custom codes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Standard HTTP status codes
|
|
8
|
+
*/
|
|
9
|
+
export enum HttpStatus {
|
|
10
|
+
OK = 200,
|
|
11
|
+
CREATED = 201,
|
|
12
|
+
NO_CONTENT = 204,
|
|
13
|
+
BAD_REQUEST = 400,
|
|
14
|
+
UNAUTHORIZED = 401,
|
|
15
|
+
FORBIDDEN = 403,
|
|
16
|
+
NOT_FOUND = 404,
|
|
17
|
+
CONFLICT = 409,
|
|
18
|
+
UNPROCESSABLE_ENTITY = 422,
|
|
19
|
+
INTERNAL_SERVER_ERROR = 500,
|
|
20
|
+
SERVICE_UNAVAILABLE = 503,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Standard response structure
|
|
25
|
+
*/
|
|
26
|
+
export interface StandardResponse<T = unknown, C = string> {
|
|
27
|
+
status: number;
|
|
28
|
+
code: C;
|
|
29
|
+
data?: T;
|
|
30
|
+
message?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Lambda HTTP response format
|
|
35
|
+
*/
|
|
36
|
+
export interface HttpResponse {
|
|
37
|
+
statusCode: number;
|
|
38
|
+
body: string;
|
|
39
|
+
headers?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default response codes for standard HTTP statuses
|
|
44
|
+
*/
|
|
45
|
+
export const DefaultResponseCode = {
|
|
46
|
+
SUCCESS: 'SUCCESS',
|
|
47
|
+
CREATED: 'CREATED',
|
|
48
|
+
BAD_REQUEST: 'BAD_REQUEST',
|
|
49
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
50
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
51
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
52
|
+
CONFLICT: 'CONFLICT',
|
|
53
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
54
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a standardized HTTP response
|
|
59
|
+
* @param statusCode - HTTP status code
|
|
60
|
+
* @param data - Response payload
|
|
61
|
+
* @param code - Custom response code
|
|
62
|
+
* @param message - Optional message
|
|
63
|
+
* @param headers - Optional headers
|
|
64
|
+
*/
|
|
65
|
+
export function createStandardResponse<T, C = string>(
|
|
66
|
+
statusCode: number,
|
|
67
|
+
data?: T,
|
|
68
|
+
code?: C,
|
|
69
|
+
message?: string,
|
|
70
|
+
headers?: Record<string, string>
|
|
71
|
+
): HttpResponse {
|
|
72
|
+
const responseCode = code ?? getDefaultCodeForStatus(statusCode);
|
|
73
|
+
|
|
74
|
+
const body: StandardResponse<T, C | string> = {
|
|
75
|
+
status: statusCode,
|
|
76
|
+
code: responseCode,
|
|
77
|
+
...(data !== undefined && { data }),
|
|
78
|
+
...(message && { message }),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
statusCode,
|
|
83
|
+
body: JSON.stringify(body, null, 2),
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
...headers,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Gets default response code for a status
|
|
93
|
+
*/
|
|
94
|
+
function getDefaultCodeForStatus(status: number): string {
|
|
95
|
+
switch (status) {
|
|
96
|
+
case HttpStatus.OK:
|
|
97
|
+
return DefaultResponseCode.SUCCESS;
|
|
98
|
+
case HttpStatus.CREATED:
|
|
99
|
+
return DefaultResponseCode.CREATED;
|
|
100
|
+
case HttpStatus.BAD_REQUEST:
|
|
101
|
+
return DefaultResponseCode.BAD_REQUEST;
|
|
102
|
+
case HttpStatus.UNAUTHORIZED:
|
|
103
|
+
return DefaultResponseCode.UNAUTHORIZED;
|
|
104
|
+
case HttpStatus.FORBIDDEN:
|
|
105
|
+
return DefaultResponseCode.FORBIDDEN;
|
|
106
|
+
case HttpStatus.NOT_FOUND:
|
|
107
|
+
return DefaultResponseCode.NOT_FOUND;
|
|
108
|
+
case HttpStatus.CONFLICT:
|
|
109
|
+
return DefaultResponseCode.CONFLICT;
|
|
110
|
+
case HttpStatus.UNPROCESSABLE_ENTITY:
|
|
111
|
+
return DefaultResponseCode.VALIDATION_ERROR;
|
|
112
|
+
default:
|
|
113
|
+
return DefaultResponseCode.INTERNAL_ERROR;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Success response (200 OK)
|
|
119
|
+
*/
|
|
120
|
+
export function successResponse<T>(data?: T, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
121
|
+
return createStandardResponse(HttpStatus.OK, data, code ?? DefaultResponseCode.SUCCESS, undefined, headers);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Created response (201 Created)
|
|
126
|
+
*/
|
|
127
|
+
export function createdResponse<T>(data?: T, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
128
|
+
return createStandardResponse(HttpStatus.CREATED, data, code ?? DefaultResponseCode.CREATED, undefined, headers);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Bad request response (400)
|
|
133
|
+
*/
|
|
134
|
+
export function badRequestResponse(message?: string, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
135
|
+
return createStandardResponse(HttpStatus.BAD_REQUEST, undefined, code ?? DefaultResponseCode.BAD_REQUEST, message, headers);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Unauthorized response (401)
|
|
140
|
+
*/
|
|
141
|
+
export function unauthorizedResponse(message?: string, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
142
|
+
return createStandardResponse(HttpStatus.UNAUTHORIZED, undefined, code ?? DefaultResponseCode.UNAUTHORIZED, message, headers);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Forbidden response (403)
|
|
147
|
+
*/
|
|
148
|
+
export function forbiddenResponse(message?: string, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
149
|
+
return createStandardResponse(HttpStatus.FORBIDDEN, undefined, code ?? DefaultResponseCode.FORBIDDEN, message, headers);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Not found response (404)
|
|
154
|
+
*/
|
|
155
|
+
export function notFoundResponse(message?: string, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
156
|
+
return createStandardResponse(HttpStatus.NOT_FOUND, undefined, code ?? DefaultResponseCode.NOT_FOUND, message, headers);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Conflict response (409)
|
|
161
|
+
*/
|
|
162
|
+
export function conflictResponse(message?: string, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
163
|
+
return createStandardResponse(HttpStatus.CONFLICT, undefined, code ?? DefaultResponseCode.CONFLICT, message, headers);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validation error response (422)
|
|
168
|
+
*/
|
|
169
|
+
export function validationErrorResponse(message?: string, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
170
|
+
return createStandardResponse(HttpStatus.UNPROCESSABLE_ENTITY, undefined, code ?? DefaultResponseCode.VALIDATION_ERROR, message, headers);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Internal server error response (500)
|
|
175
|
+
*/
|
|
176
|
+
export function internalErrorResponse(message?: string, code?: string, headers?: Record<string, string>): HttpResponse {
|
|
177
|
+
return createStandardResponse(HttpStatus.INTERNAL_SERVER_ERROR, undefined, code ?? DefaultResponseCode.INTERNAL_ERROR, message, headers);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Custom error response with automatic status resolution
|
|
182
|
+
* @param customCode - Your domain-specific error code
|
|
183
|
+
* @param message - Error message
|
|
184
|
+
* @param codeToStatusMap - Mapping of custom codes to HTTP statuses
|
|
185
|
+
*/
|
|
186
|
+
export function customErrorResponse<C extends string>(
|
|
187
|
+
customCode: C,
|
|
188
|
+
message?: string,
|
|
189
|
+
codeToStatusMap?: Record<C, HttpStatus>,
|
|
190
|
+
headers?: Record<string, string>
|
|
191
|
+
): HttpResponse {
|
|
192
|
+
const status = codeToStatusMap?.[customCode] ?? HttpStatus.INTERNAL_SERVER_ERROR;
|
|
193
|
+
return createStandardResponse(status, undefined, customCode, message, headers);
|
|
194
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { IdentityContext } from '../types/routes.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts identity context from API Gateway authorizer claims
|
|
5
|
+
* Supports Cognito User Pools and custom authorizers
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts identity information from the event's authorizer context
|
|
10
|
+
* @param event - Raw API Gateway event
|
|
11
|
+
* @returns Identity context or undefined if not authenticated
|
|
12
|
+
*/
|
|
13
|
+
export function extractIdentity(event: any): IdentityContext | undefined {
|
|
14
|
+
const claims = event?.requestContext?.authorizer?.claims;
|
|
15
|
+
|
|
16
|
+
if (!claims) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
userId: claims.sub || claims['cognito:username'],
|
|
22
|
+
email: claims.email,
|
|
23
|
+
groups: parseGroups(claims['cognito:groups']),
|
|
24
|
+
issuer: claims.iss,
|
|
25
|
+
claims,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses Cognito groups from claims
|
|
31
|
+
* Groups can come as a string or array depending on configuration
|
|
32
|
+
*/
|
|
33
|
+
function parseGroups(groups: string | string[] | undefined): string[] {
|
|
34
|
+
if (!groups) return [];
|
|
35
|
+
if (Array.isArray(groups)) return groups;
|
|
36
|
+
|
|
37
|
+
// Cognito sometimes sends groups as a comma-separated string
|
|
38
|
+
return groups.split(',').map(g => g.trim()).filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extracts the User Pool ID from the issuer URL
|
|
43
|
+
* @param issuer - Cognito issuer URL (e.g., https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxx)
|
|
44
|
+
* @returns User Pool ID or undefined
|
|
45
|
+
*/
|
|
46
|
+
export function extractUserPoolId(issuer: string | undefined): string | undefined {
|
|
47
|
+
if (!issuer) return undefined;
|
|
48
|
+
|
|
49
|
+
// Extract the last segment of the issuer URL
|
|
50
|
+
const parts = issuer.split('/');
|
|
51
|
+
return parts[parts.length - 1];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates that the token issuer matches the expected User Pool
|
|
56
|
+
* @param identity - Extracted identity context
|
|
57
|
+
* @param expectedUserPoolId - Expected User Pool ID
|
|
58
|
+
* @returns True if issuer matches
|
|
59
|
+
*/
|
|
60
|
+
export function validateIssuer(identity: IdentityContext | undefined, expectedUserPoolId: string): boolean {
|
|
61
|
+
if (!identity?.issuer) return false;
|
|
62
|
+
|
|
63
|
+
const actualUserPoolId = extractUserPoolId(identity.issuer);
|
|
64
|
+
return actualUserPoolId === expectedUserPoolId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if the user belongs to any of the specified groups
|
|
69
|
+
* @param identity - Extracted identity context
|
|
70
|
+
* @param allowedGroups - Groups that grant access
|
|
71
|
+
* @returns True if user is in at least one allowed group
|
|
72
|
+
*/
|
|
73
|
+
export function hasAnyGroup(identity: IdentityContext | undefined, allowedGroups: string[]): boolean {
|
|
74
|
+
if (!identity?.groups || identity.groups.length === 0) return false;
|
|
75
|
+
|
|
76
|
+
return allowedGroups.some(group => identity.groups?.includes(group));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Checks if the user belongs to all specified groups
|
|
81
|
+
* @param identity - Extracted identity context
|
|
82
|
+
* @param requiredGroups - Groups required for access
|
|
83
|
+
* @returns True if user is in all required groups
|
|
84
|
+
*/
|
|
85
|
+
export function hasAllGroups(identity: IdentityContext | undefined, requiredGroups: string[]): boolean {
|
|
86
|
+
if (!identity?.groups || identity.groups.length === 0) return false;
|
|
87
|
+
|
|
88
|
+
return requiredGroups.every(group => identity.groups?.includes(group));
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './extractor.js';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serverless-event-orchestrator
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, type-safe event dispatcher and middleware orchestrator for AWS Lambda.
|
|
5
|
+
* Designed for hexagonal architectures with support for segmented routing,
|
|
6
|
+
* Cognito User Pool validation, and built-in infrastructure middlewares.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Core dispatcher
|
|
10
|
+
export { dispatchEvent, detectEventType, createOrchestrator } from './dispatcher.js';
|
|
11
|
+
|
|
12
|
+
// Types
|
|
13
|
+
export {
|
|
14
|
+
EventType,
|
|
15
|
+
RouteSegment,
|
|
16
|
+
} from './types/event-type.enum.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
HttpMethod,
|
|
20
|
+
MiddlewareFn,
|
|
21
|
+
RouteConfig,
|
|
22
|
+
CorsConfig,
|
|
23
|
+
RateLimitConfig,
|
|
24
|
+
HttpRouter,
|
|
25
|
+
SegmentedHttpRouter,
|
|
26
|
+
SegmentConfig,
|
|
27
|
+
AdvancedSegmentedRouter,
|
|
28
|
+
EventBridgeRoutes,
|
|
29
|
+
LambdaRoutes,
|
|
30
|
+
SqsRoutes,
|
|
31
|
+
DispatchRoutes,
|
|
32
|
+
IdentityContext,
|
|
33
|
+
RouteMatch,
|
|
34
|
+
NormalizedEvent,
|
|
35
|
+
OrchestratorConfig,
|
|
36
|
+
} from './types/routes.js';
|
|
37
|
+
|
|
38
|
+
// HTTP utilities
|
|
39
|
+
export {
|
|
40
|
+
HttpStatus,
|
|
41
|
+
StandardResponse,
|
|
42
|
+
HttpResponse,
|
|
43
|
+
DefaultResponseCode,
|
|
44
|
+
createStandardResponse,
|
|
45
|
+
successResponse,
|
|
46
|
+
createdResponse,
|
|
47
|
+
badRequestResponse,
|
|
48
|
+
unauthorizedResponse,
|
|
49
|
+
forbiddenResponse,
|
|
50
|
+
notFoundResponse,
|
|
51
|
+
conflictResponse,
|
|
52
|
+
validationErrorResponse,
|
|
53
|
+
internalErrorResponse,
|
|
54
|
+
customErrorResponse,
|
|
55
|
+
} from './http/response.js';
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
parseJsonBody,
|
|
59
|
+
parseQueryParams,
|
|
60
|
+
withJsonBodyParser,
|
|
61
|
+
} from './http/body-parser.js';
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
isPreflightRequest,
|
|
65
|
+
createPreflightResponse,
|
|
66
|
+
applyCorsHeaders,
|
|
67
|
+
withCors,
|
|
68
|
+
} from './http/cors.js';
|
|
69
|
+
|
|
70
|
+
// Identity utilities
|
|
71
|
+
export {
|
|
72
|
+
extractIdentity,
|
|
73
|
+
extractUserPoolId,
|
|
74
|
+
validateIssuer,
|
|
75
|
+
hasAnyGroup,
|
|
76
|
+
hasAllGroups,
|
|
77
|
+
} from './identity/extractor.js';
|
|
78
|
+
|
|
79
|
+
// Path utilities
|
|
80
|
+
export {
|
|
81
|
+
matchPath,
|
|
82
|
+
patternToRegex,
|
|
83
|
+
hasPathParameters,
|
|
84
|
+
normalizePath,
|
|
85
|
+
} from './utils/path-matcher.js';
|
|
86
|
+
|
|
87
|
+
// Header utilities
|
|
88
|
+
export {
|
|
89
|
+
normalizeHeaders,
|
|
90
|
+
getHeader,
|
|
91
|
+
getCorsHeaders,
|
|
92
|
+
} from './utils/headers.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported AWS event types for dispatching
|
|
3
|
+
*/
|
|
4
|
+
export enum EventType {
|
|
5
|
+
EventBridge = 'eventbridge',
|
|
6
|
+
ApiGateway = 'apigateway',
|
|
7
|
+
Lambda = 'lambda',
|
|
8
|
+
Sqs = 'sqs',
|
|
9
|
+
Unknown = 'unknown',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Route segments for access control categorization
|
|
14
|
+
*/
|
|
15
|
+
export enum RouteSegment {
|
|
16
|
+
Public = 'public',
|
|
17
|
+
Private = 'private',
|
|
18
|
+
Backoffice = 'backoffice',
|
|
19
|
+
Internal = 'internal',
|
|
20
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { RouteSegment } from './event-type.enum.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP methods supported by the router
|
|
5
|
+
*/
|
|
6
|
+
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Middleware function signature
|
|
10
|
+
* Returns the modified event or throws an error to halt execution
|
|
11
|
+
*/
|
|
12
|
+
export type MiddlewareFn = (event: NormalizedEvent) => Promise<NormalizedEvent | void>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Route configuration for a single endpoint
|
|
16
|
+
*/
|
|
17
|
+
export interface RouteConfig {
|
|
18
|
+
handler: (event: NormalizedEvent) => Promise<any>;
|
|
19
|
+
middleware?: MiddlewareFn[];
|
|
20
|
+
cors?: boolean | CorsConfig;
|
|
21
|
+
rateLimit?: RateLimitConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* CORS configuration options
|
|
26
|
+
*/
|
|
27
|
+
export interface CorsConfig {
|
|
28
|
+
origins: string[] | '*';
|
|
29
|
+
methods?: string[];
|
|
30
|
+
headers?: string[];
|
|
31
|
+
credentials?: boolean;
|
|
32
|
+
maxAge?: number;
|
|
33
|
+
exposedHeaders?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rate limit configuration
|
|
38
|
+
*/
|
|
39
|
+
export interface RateLimitConfig {
|
|
40
|
+
burstLimit: number;
|
|
41
|
+
rateLimit: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Standard HTTP router structure
|
|
46
|
+
* Maps HTTP methods to path-handler pairs
|
|
47
|
+
*/
|
|
48
|
+
export type HttpRouter = {
|
|
49
|
+
[K in HttpMethod]?: {
|
|
50
|
+
[path: string]: RouteConfig;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Segmented HTTP router for access control categorization
|
|
56
|
+
* Allows organizing routes by security context
|
|
57
|
+
*/
|
|
58
|
+
export interface SegmentedHttpRouter {
|
|
59
|
+
public?: HttpRouter;
|
|
60
|
+
private?: HttpRouter;
|
|
61
|
+
backoffice?: HttpRouter;
|
|
62
|
+
internal?: HttpRouter;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Segment configuration with optional middleware
|
|
67
|
+
*/
|
|
68
|
+
export interface SegmentConfig {
|
|
69
|
+
routes: HttpRouter;
|
|
70
|
+
middleware?: MiddlewareFn[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Advanced segmented router with per-segment middleware
|
|
75
|
+
*/
|
|
76
|
+
export interface AdvancedSegmentedRouter {
|
|
77
|
+
public?: SegmentConfig | HttpRouter;
|
|
78
|
+
private?: SegmentConfig | HttpRouter;
|
|
79
|
+
backoffice?: SegmentConfig | HttpRouter;
|
|
80
|
+
internal?: SegmentConfig | HttpRouter;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* EventBridge routes configuration
|
|
85
|
+
* Maps operation names to handlers
|
|
86
|
+
*/
|
|
87
|
+
export type EventBridgeRoutes = Record<string, (event: NormalizedEvent) => Promise<any>>;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Lambda invocation routes
|
|
91
|
+
*/
|
|
92
|
+
export type LambdaRoutes = Record<string, (event: NormalizedEvent) => Promise<any>>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SQS queue routes
|
|
96
|
+
* Maps queue names to handlers
|
|
97
|
+
*/
|
|
98
|
+
export type SqsRoutes = Record<string, (event: NormalizedEvent) => Promise<any>>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Complete dispatch routes configuration
|
|
102
|
+
*/
|
|
103
|
+
export interface DispatchRoutes {
|
|
104
|
+
apigateway?: HttpRouter | SegmentedHttpRouter | AdvancedSegmentedRouter;
|
|
105
|
+
eventbridge?: EventBridgeRoutes;
|
|
106
|
+
lambda?: LambdaRoutes;
|
|
107
|
+
sqs?: SqsRoutes;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Identity context extracted from the event
|
|
112
|
+
*/
|
|
113
|
+
export interface IdentityContext {
|
|
114
|
+
userId?: string;
|
|
115
|
+
email?: string;
|
|
116
|
+
groups?: string[];
|
|
117
|
+
issuer?: string;
|
|
118
|
+
claims?: Record<string, any>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Route match result with extracted parameters
|
|
123
|
+
*/
|
|
124
|
+
export interface RouteMatch {
|
|
125
|
+
handler: (event: NormalizedEvent) => Promise<any>;
|
|
126
|
+
params: Record<string, string>;
|
|
127
|
+
segment: RouteSegment;
|
|
128
|
+
middleware?: MiddlewareFn[];
|
|
129
|
+
config: RouteConfig;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Normalized event structure passed to handlers
|
|
134
|
+
*/
|
|
135
|
+
export interface NormalizedEvent {
|
|
136
|
+
eventRaw: any;
|
|
137
|
+
eventType: string;
|
|
138
|
+
payload: {
|
|
139
|
+
body?: Record<string, any>;
|
|
140
|
+
pathParameters?: Record<string, string>;
|
|
141
|
+
queryStringParameters?: Record<string, string>;
|
|
142
|
+
headers?: Record<string, string>;
|
|
143
|
+
};
|
|
144
|
+
params: Record<string, string>;
|
|
145
|
+
context: {
|
|
146
|
+
segment: RouteSegment;
|
|
147
|
+
identity?: IdentityContext;
|
|
148
|
+
requestId?: string;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Orchestrator configuration options
|
|
154
|
+
*/
|
|
155
|
+
export interface OrchestratorConfig {
|
|
156
|
+
/**
|
|
157
|
+
* Enable verbose logging for debugging
|
|
158
|
+
*/
|
|
159
|
+
debug?: boolean;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* User Pool ID mappings for segment-based validation
|
|
163
|
+
*/
|
|
164
|
+
userPools?: {
|
|
165
|
+
[K in RouteSegment]?: string;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Global middleware applied to all routes
|
|
170
|
+
*/
|
|
171
|
+
globalMiddleware?: MiddlewareFn[];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Custom response handlers
|
|
175
|
+
*/
|
|
176
|
+
responses?: {
|
|
177
|
+
notFound?: () => any;
|
|
178
|
+
forbidden?: () => any;
|
|
179
|
+
badRequest?: (message?: string) => any;
|
|
180
|
+
internalError?: (message?: string) => any;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header normalization utilities
|
|
3
|
+
* HTTP headers are case-insensitive, this ensures consistent access
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes headers to lowercase keys for consistent access
|
|
8
|
+
* @param headers - Original headers object
|
|
9
|
+
* @returns Headers with lowercase keys
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeHeaders(headers: Record<string, string> | undefined): Record<string, string> {
|
|
12
|
+
if (!headers) return {};
|
|
13
|
+
|
|
14
|
+
const normalized: Record<string, string> = {};
|
|
15
|
+
|
|
16
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
17
|
+
normalized[key.toLowerCase()] = value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets a header value case-insensitively
|
|
25
|
+
* @param headers - Headers object
|
|
26
|
+
* @param name - Header name to find
|
|
27
|
+
* @returns Header value or undefined
|
|
28
|
+
*/
|
|
29
|
+
export function getHeader(headers: Record<string, string> | undefined, name: string): string | undefined {
|
|
30
|
+
if (!headers) return undefined;
|
|
31
|
+
|
|
32
|
+
const normalizedName = name.toLowerCase();
|
|
33
|
+
|
|
34
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
35
|
+
if (key.toLowerCase() === normalizedName) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Standard CORS headers for preflight responses
|
|
45
|
+
*/
|
|
46
|
+
export function getCorsHeaders(config?: {
|
|
47
|
+
origins?: string[] | '*';
|
|
48
|
+
methods?: string[];
|
|
49
|
+
headers?: string[];
|
|
50
|
+
credentials?: boolean;
|
|
51
|
+
maxAge?: number;
|
|
52
|
+
}): Record<string, string> {
|
|
53
|
+
const origin = config?.origins === '*' ? '*' : (config?.origins?.join(', ') || '*');
|
|
54
|
+
const methods = config?.methods?.join(', ') || 'GET,POST,PUT,DELETE,PATCH,OPTIONS';
|
|
55
|
+
const headers = config?.headers?.join(', ') || 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token';
|
|
56
|
+
|
|
57
|
+
const corsHeaders: Record<string, string> = {
|
|
58
|
+
'Access-Control-Allow-Origin': origin,
|
|
59
|
+
'Access-Control-Allow-Methods': methods,
|
|
60
|
+
'Access-Control-Allow-Headers': headers,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (config?.credentials) {
|
|
64
|
+
corsHeaders['Access-Control-Allow-Credentials'] = 'true';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (config?.maxAge) {
|
|
68
|
+
corsHeaders['Access-Control-Max-Age'] = String(config.maxAge);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return corsHeaders;
|
|
72
|
+
}
|