serverless-event-orchestrator 2.0.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +489 -434
- package/dist/dispatcher.d.ts +6 -1
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +66 -7
- package/dist/dispatcher.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/event-type.enum.d.ts +1 -0
- package/dist/types/event-type.enum.d.ts.map +1 -1
- package/dist/types/event-type.enum.js +1 -0
- package/dist/types/event-type.enum.js.map +1 -1
- package/dist/types/routes.d.ts +6 -0
- package/dist/types/routes.d.ts.map +1 -1
- package/jest.config.js +32 -32
- package/package.json +82 -81
- package/src/dispatcher.ts +586 -519
- package/src/http/body-parser.ts +60 -60
- package/src/http/cors.ts +76 -76
- package/src/http/index.ts +3 -3
- package/src/http/response.ts +209 -209
- package/src/identity/extractor.ts +207 -207
- package/src/identity/index.ts +2 -2
- package/src/identity/jwt-verifier.ts +41 -41
- package/src/index.ts +128 -127
- package/src/middleware/crm-guard.ts +51 -51
- package/src/middleware/index.ts +3 -3
- package/src/middleware/init-tenant-context.ts +59 -59
- package/src/middleware/tenant-guard.ts +54 -54
- package/src/tenant/TenantContext.ts +115 -115
- package/src/tenant/helpers.ts +112 -112
- package/src/tenant/index.ts +21 -21
- package/src/tenant/types.ts +101 -101
- package/src/types/event-type.enum.ts +21 -20
- package/src/types/index.ts +2 -2
- package/src/types/routes.ts +218 -211
- package/src/utils/headers.ts +72 -72
- package/src/utils/index.ts +2 -2
- package/src/utils/path-matcher.ts +84 -84
- package/tests/cors.test.ts +133 -133
- package/tests/dispatcher.test.ts +795 -715
- package/tests/headers.test.ts +99 -99
- package/tests/identity.test.ts +301 -301
- package/tests/middleware/crm-guard.test.ts +69 -69
- package/tests/middleware/init-tenant-context.test.ts +147 -147
- package/tests/middleware/tenant-guard.test.ts +100 -100
- package/tests/path-matcher.test.ts +102 -102
- package/tests/response.test.ts +155 -155
- package/tests/tenant/TenantContext.test.ts +134 -134
- package/tests/tenant/helpers.test.ts +187 -187
- package/tsconfig.json +24 -24
package/src/index.ts
CHANGED
|
@@ -1,127 +1,128 @@
|
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
ScheduledRoutes,
|
|
32
|
+
DispatchRoutes,
|
|
33
|
+
IdentityContext,
|
|
34
|
+
RouteMatch,
|
|
35
|
+
NormalizedEvent,
|
|
36
|
+
OrchestratorConfig,
|
|
37
|
+
JwtVerificationPoolConfig,
|
|
38
|
+
} from './types/routes.js';
|
|
39
|
+
|
|
40
|
+
// HTTP utilities
|
|
41
|
+
export {
|
|
42
|
+
HttpStatus,
|
|
43
|
+
StandardResponse,
|
|
44
|
+
HttpResponse,
|
|
45
|
+
DefaultResponseCode,
|
|
46
|
+
createStandardResponse,
|
|
47
|
+
successResponse,
|
|
48
|
+
createdResponse,
|
|
49
|
+
badRequestResponse,
|
|
50
|
+
unauthorizedResponse,
|
|
51
|
+
forbiddenResponse,
|
|
52
|
+
notFoundResponse,
|
|
53
|
+
conflictResponse,
|
|
54
|
+
validationErrorResponse,
|
|
55
|
+
internalErrorResponse,
|
|
56
|
+
customErrorResponse,
|
|
57
|
+
} from './http/response.js';
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
parseJsonBody,
|
|
61
|
+
parseQueryParams,
|
|
62
|
+
withJsonBodyParser,
|
|
63
|
+
} from './http/body-parser.js';
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
isPreflightRequest,
|
|
67
|
+
createPreflightResponse,
|
|
68
|
+
applyCorsHeaders,
|
|
69
|
+
withCors,
|
|
70
|
+
} from './http/cors.js';
|
|
71
|
+
|
|
72
|
+
// Identity utilities
|
|
73
|
+
export {
|
|
74
|
+
extractIdentity,
|
|
75
|
+
extractUserPoolId,
|
|
76
|
+
validateIssuer,
|
|
77
|
+
hasAnyGroup,
|
|
78
|
+
hasAllGroups,
|
|
79
|
+
} from './identity/extractor.js';
|
|
80
|
+
|
|
81
|
+
export type { ExtractIdentityOptions } from './identity/extractor.js';
|
|
82
|
+
|
|
83
|
+
export { verifyJwt } from './identity/jwt-verifier.js';
|
|
84
|
+
|
|
85
|
+
// Path utilities
|
|
86
|
+
export {
|
|
87
|
+
matchPath,
|
|
88
|
+
patternToRegex,
|
|
89
|
+
hasPathParameters,
|
|
90
|
+
normalizePath,
|
|
91
|
+
} from './utils/path-matcher.js';
|
|
92
|
+
|
|
93
|
+
// Header utilities
|
|
94
|
+
export {
|
|
95
|
+
normalizeHeaders,
|
|
96
|
+
getHeader,
|
|
97
|
+
getCorsHeaders,
|
|
98
|
+
} from './utils/headers.js';
|
|
99
|
+
|
|
100
|
+
// Tenant context
|
|
101
|
+
export { TenantContext } from './tenant/TenantContext.js';
|
|
102
|
+
|
|
103
|
+
export type {
|
|
104
|
+
TenantInfo,
|
|
105
|
+
TenantType,
|
|
106
|
+
Plan,
|
|
107
|
+
TenantFeatures,
|
|
108
|
+
TenantClaims,
|
|
109
|
+
} from './tenant/types.js';
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
isTenantType,
|
|
113
|
+
isPlan,
|
|
114
|
+
TENANT_HEADERS,
|
|
115
|
+
} from './tenant/types.js';
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
tenantInfoFromClaims,
|
|
119
|
+
tenantInfoFromHeaders,
|
|
120
|
+
tenantInfoToHeaders,
|
|
121
|
+
} from './tenant/helpers.js';
|
|
122
|
+
|
|
123
|
+
// Tenant middleware
|
|
124
|
+
export {
|
|
125
|
+
initTenantContext,
|
|
126
|
+
tenantGuard,
|
|
127
|
+
crmGuard,
|
|
128
|
+
} from './middleware/index.js';
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import type { NormalizedEvent, MiddlewareFn } from '../types/routes.js';
|
|
2
|
-
import { forbiddenResponse } from '../http/response.js';
|
|
3
|
-
import { hasAnyGroup } from '../identity/extractor.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Roles that bypass CRM plan check.
|
|
7
|
-
*/
|
|
8
|
-
const CRM_BYPASS_ROLES = ['PLATFORM_ADMIN'];
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Middleware that enforces CRM access based on the tenant's subscription plan.
|
|
12
|
-
*
|
|
13
|
-
* Checks event.context.tenantInfo.hasCRM (set by initTenantContext from JWT claims).
|
|
14
|
-
* If the tenant doesn't have CRM access, returns 403 with upgrade message.
|
|
15
|
-
*
|
|
16
|
-
* IMPORTANT: This middleware MUST run AFTER tenantGuard (which ensures tenantInfo exists).
|
|
17
|
-
*
|
|
18
|
-
* Usage: Add as route-level or segment middleware for CRM-only endpoints.
|
|
19
|
-
*
|
|
20
|
-
* ```typescript
|
|
21
|
-
* private: {
|
|
22
|
-
* middleware: [tenantGuard], // ← runs first
|
|
23
|
-
* routes: {
|
|
24
|
-
* get: {
|
|
25
|
-
* '/crm/dashboard': { handler: getCRMDashboard, middleware: [crmGuard] }, // ← runs second
|
|
26
|
-
* '/crm/leads': { handler: getLeads, middleware: [crmGuard] },
|
|
27
|
-
* }
|
|
28
|
-
* }
|
|
29
|
-
* }
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export const crmGuard: MiddlewareFn = async (
|
|
33
|
-
event: NormalizedEvent
|
|
34
|
-
): Promise<NormalizedEvent> => {
|
|
35
|
-
// PLATFORM_ADMIN bypasses CRM check
|
|
36
|
-
if (hasAnyGroup(event.context.identity, CRM_BYPASS_ROLES)) {
|
|
37
|
-
return event;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const tenantInfo = event.context.tenantInfo;
|
|
41
|
-
|
|
42
|
-
// Check hasCRM from tenantInfo (set from JWT claim custom:hasCRM)
|
|
43
|
-
if (!tenantInfo?.hasCRM) {
|
|
44
|
-
throw forbiddenResponse(
|
|
45
|
-
'CRM access requires a paid plan. Please upgrade your subscription.',
|
|
46
|
-
'CRM_ACCESS_DENIED' as any
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return event;
|
|
51
|
-
};
|
|
1
|
+
import type { NormalizedEvent, MiddlewareFn } from '../types/routes.js';
|
|
2
|
+
import { forbiddenResponse } from '../http/response.js';
|
|
3
|
+
import { hasAnyGroup } from '../identity/extractor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Roles that bypass CRM plan check.
|
|
7
|
+
*/
|
|
8
|
+
const CRM_BYPASS_ROLES = ['PLATFORM_ADMIN'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Middleware that enforces CRM access based on the tenant's subscription plan.
|
|
12
|
+
*
|
|
13
|
+
* Checks event.context.tenantInfo.hasCRM (set by initTenantContext from JWT claims).
|
|
14
|
+
* If the tenant doesn't have CRM access, returns 403 with upgrade message.
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT: This middleware MUST run AFTER tenantGuard (which ensures tenantInfo exists).
|
|
17
|
+
*
|
|
18
|
+
* Usage: Add as route-level or segment middleware for CRM-only endpoints.
|
|
19
|
+
*
|
|
20
|
+
* ```typescript
|
|
21
|
+
* private: {
|
|
22
|
+
* middleware: [tenantGuard], // ← runs first
|
|
23
|
+
* routes: {
|
|
24
|
+
* get: {
|
|
25
|
+
* '/crm/dashboard': { handler: getCRMDashboard, middleware: [crmGuard] }, // ← runs second
|
|
26
|
+
* '/crm/leads': { handler: getLeads, middleware: [crmGuard] },
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const crmGuard: MiddlewareFn = async (
|
|
33
|
+
event: NormalizedEvent
|
|
34
|
+
): Promise<NormalizedEvent> => {
|
|
35
|
+
// PLATFORM_ADMIN bypasses CRM check
|
|
36
|
+
if (hasAnyGroup(event.context.identity, CRM_BYPASS_ROLES)) {
|
|
37
|
+
return event;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const tenantInfo = event.context.tenantInfo;
|
|
41
|
+
|
|
42
|
+
// Check hasCRM from tenantInfo (set from JWT claim custom:hasCRM)
|
|
43
|
+
if (!tenantInfo?.hasCRM) {
|
|
44
|
+
throw forbiddenResponse(
|
|
45
|
+
'CRM access requires a paid plan. Please upgrade your subscription.',
|
|
46
|
+
'CRM_ACCESS_DENIED' as any
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return event;
|
|
51
|
+
};
|
package/src/middleware/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { initTenantContext } from './init-tenant-context.js';
|
|
2
|
-
export { tenantGuard } from './tenant-guard.js';
|
|
3
|
-
export { crmGuard } from './crm-guard.js';
|
|
1
|
+
export { initTenantContext } from './init-tenant-context.js';
|
|
2
|
+
export { tenantGuard } from './tenant-guard.js';
|
|
3
|
+
export { crmGuard } from './crm-guard.js';
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import type { NormalizedEvent, MiddlewareFn } from '../types/routes.js';
|
|
2
|
-
import {
|
|
3
|
-
TenantContext,
|
|
4
|
-
tenantInfoFromClaims,
|
|
5
|
-
tenantInfoFromHeaders,
|
|
6
|
-
type TenantInfo,
|
|
7
|
-
} from '../tenant/index.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Middleware that initializes the TenantContext from JWT claims or headers.
|
|
11
|
-
*
|
|
12
|
-
* Execution order: globalMiddleware (runs on ALL routes, before segment middleware).
|
|
13
|
-
*
|
|
14
|
-
* Resolution strategy:
|
|
15
|
-
* 1. Private/Backoffice segments → extract from identity.claims (JWT)
|
|
16
|
-
* 2. Internal segment → extract from headers (X-Tenant-Id, Lambda-to-Lambda)
|
|
17
|
-
* 3. Public segment → try claims first (authenticated public), then headers, then skip
|
|
18
|
-
*
|
|
19
|
-
* If tenant info is found:
|
|
20
|
-
* - Sets TenantContext via AsyncLocalStorage (for repositories)
|
|
21
|
-
* - Adds tenantInfo to event.context (for handlers and logger)
|
|
22
|
-
*
|
|
23
|
-
* If tenant info is NOT found:
|
|
24
|
-
* - Does NOT throw (public routes are allowed without tenant)
|
|
25
|
-
* - tenantGuard (segment middleware) will enforce tenant requirement where needed
|
|
26
|
-
*/
|
|
27
|
-
export const initTenantContext: MiddlewareFn = async (
|
|
28
|
-
event: NormalizedEvent
|
|
29
|
-
): Promise<NormalizedEvent> => {
|
|
30
|
-
let tenantInfo: TenantInfo | undefined;
|
|
31
|
-
|
|
32
|
-
// Strategy 1: From JWT claims (private, backoffice, or authenticated public)
|
|
33
|
-
if (event.context.identity?.claims) {
|
|
34
|
-
tenantInfo = tenantInfoFromClaims(event.context.identity.claims);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Strategy 2: From headers (internal Lambda-to-Lambda, or fallback)
|
|
38
|
-
if (!tenantInfo && event.payload.headers) {
|
|
39
|
-
tenantInfo = tenantInfoFromHeaders(event.payload.headers);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// If we found tenant info, set it everywhere
|
|
43
|
-
if (tenantInfo) {
|
|
44
|
-
// 1. Set AsyncLocalStorage (for TenantAwareDynamoRepository and ApiInvoker)
|
|
45
|
-
TenantContext.set(tenantInfo);
|
|
46
|
-
|
|
47
|
-
// 2. Enrich event.context (for handlers, logger, and downstream middleware)
|
|
48
|
-
return {
|
|
49
|
-
...event,
|
|
50
|
-
context: {
|
|
51
|
-
...event.context,
|
|
52
|
-
tenantInfo,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// No tenant info found — this is normal for public routes
|
|
58
|
-
return event;
|
|
59
|
-
};
|
|
1
|
+
import type { NormalizedEvent, MiddlewareFn } from '../types/routes.js';
|
|
2
|
+
import {
|
|
3
|
+
TenantContext,
|
|
4
|
+
tenantInfoFromClaims,
|
|
5
|
+
tenantInfoFromHeaders,
|
|
6
|
+
type TenantInfo,
|
|
7
|
+
} from '../tenant/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Middleware that initializes the TenantContext from JWT claims or headers.
|
|
11
|
+
*
|
|
12
|
+
* Execution order: globalMiddleware (runs on ALL routes, before segment middleware).
|
|
13
|
+
*
|
|
14
|
+
* Resolution strategy:
|
|
15
|
+
* 1. Private/Backoffice segments → extract from identity.claims (JWT)
|
|
16
|
+
* 2. Internal segment → extract from headers (X-Tenant-Id, Lambda-to-Lambda)
|
|
17
|
+
* 3. Public segment → try claims first (authenticated public), then headers, then skip
|
|
18
|
+
*
|
|
19
|
+
* If tenant info is found:
|
|
20
|
+
* - Sets TenantContext via AsyncLocalStorage (for repositories)
|
|
21
|
+
* - Adds tenantInfo to event.context (for handlers and logger)
|
|
22
|
+
*
|
|
23
|
+
* If tenant info is NOT found:
|
|
24
|
+
* - Does NOT throw (public routes are allowed without tenant)
|
|
25
|
+
* - tenantGuard (segment middleware) will enforce tenant requirement where needed
|
|
26
|
+
*/
|
|
27
|
+
export const initTenantContext: MiddlewareFn = async (
|
|
28
|
+
event: NormalizedEvent
|
|
29
|
+
): Promise<NormalizedEvent> => {
|
|
30
|
+
let tenantInfo: TenantInfo | undefined;
|
|
31
|
+
|
|
32
|
+
// Strategy 1: From JWT claims (private, backoffice, or authenticated public)
|
|
33
|
+
if (event.context.identity?.claims) {
|
|
34
|
+
tenantInfo = tenantInfoFromClaims(event.context.identity.claims);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Strategy 2: From headers (internal Lambda-to-Lambda, or fallback)
|
|
38
|
+
if (!tenantInfo && event.payload.headers) {
|
|
39
|
+
tenantInfo = tenantInfoFromHeaders(event.payload.headers);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If we found tenant info, set it everywhere
|
|
43
|
+
if (tenantInfo) {
|
|
44
|
+
// 1. Set AsyncLocalStorage (for TenantAwareDynamoRepository and ApiInvoker)
|
|
45
|
+
TenantContext.set(tenantInfo);
|
|
46
|
+
|
|
47
|
+
// 2. Enrich event.context (for handlers, logger, and downstream middleware)
|
|
48
|
+
return {
|
|
49
|
+
...event,
|
|
50
|
+
context: {
|
|
51
|
+
...event.context,
|
|
52
|
+
tenantInfo,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// No tenant info found — this is normal for public routes
|
|
58
|
+
return event;
|
|
59
|
+
};
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import type { NormalizedEvent, MiddlewareFn } from '../types/routes.js';
|
|
2
|
-
import { forbiddenResponse } from '../http/response.js';
|
|
3
|
-
import { hasAnyGroup } from '../identity/extractor.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Roles that can operate cross-tenant (bypass tenant check).
|
|
7
|
-
* PLATFORM_ADMIN = MLHolding employees in the Backoffice pool.
|
|
8
|
-
*/
|
|
9
|
-
const CROSS_TENANT_ROLES = ['PLATFORM_ADMIN'];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Middleware that enforces tenant context on protected routes.
|
|
13
|
-
* FAIL-CLOSED: If no tenant context exists and user is not PLATFORM_ADMIN, request is denied.
|
|
14
|
-
*
|
|
15
|
-
* Usage: Add as segment middleware for private and backoffice segments.
|
|
16
|
-
*
|
|
17
|
-
* ```typescript
|
|
18
|
-
* const routes: AdvancedSegmentedRouter = {
|
|
19
|
-
* public: { routes: { ... } }, // ← NO tenantGuard
|
|
20
|
-
* private: { middleware: [tenantGuard], routes: { ... } }, // ← YES
|
|
21
|
-
* backoffice: { middleware: [tenantGuard], routes: { ... } }, // ← YES
|
|
22
|
-
* internal: { routes: { ... } }, // ← NO (uses headers, initTenantContext handles it)
|
|
23
|
-
* };
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* Throws forbiddenResponse (403) if:
|
|
27
|
-
* - No tenantInfo in context AND user is not PLATFORM_ADMIN
|
|
28
|
-
* - tenantInfo exists but tenantId is empty or whitespace-only
|
|
29
|
-
*
|
|
30
|
-
* Allows through if:
|
|
31
|
-
* - tenantInfo exists with valid non-empty tenantId
|
|
32
|
-
* - User is PLATFORM_ADMIN (cross-tenant access)
|
|
33
|
-
*/
|
|
34
|
-
export const tenantGuard: MiddlewareFn = async (
|
|
35
|
-
event: NormalizedEvent
|
|
36
|
-
): Promise<NormalizedEvent> => {
|
|
37
|
-
const tenantInfo = event.context.tenantInfo;
|
|
38
|
-
const identity = event.context.identity;
|
|
39
|
-
|
|
40
|
-
// PLATFORM_ADMIN can operate without tenant context (cross-tenant)
|
|
41
|
-
if (hasAnyGroup(identity, CROSS_TENANT_ROLES)) {
|
|
42
|
-
return event;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Fail-closed: validate tenantInfo exists AND tenantId is not empty
|
|
46
|
-
if (!tenantInfo?.tenantId || tenantInfo.tenantId.trim() === '') {
|
|
47
|
-
throw forbiddenResponse(
|
|
48
|
-
'Tenant context required. Ensure you are authenticated with a valid tenant.',
|
|
49
|
-
'TENANT_CONTEXT_MISSING' as any
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return event;
|
|
54
|
-
};
|
|
1
|
+
import type { NormalizedEvent, MiddlewareFn } from '../types/routes.js';
|
|
2
|
+
import { forbiddenResponse } from '../http/response.js';
|
|
3
|
+
import { hasAnyGroup } from '../identity/extractor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Roles that can operate cross-tenant (bypass tenant check).
|
|
7
|
+
* PLATFORM_ADMIN = MLHolding employees in the Backoffice pool.
|
|
8
|
+
*/
|
|
9
|
+
const CROSS_TENANT_ROLES = ['PLATFORM_ADMIN'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Middleware that enforces tenant context on protected routes.
|
|
13
|
+
* FAIL-CLOSED: If no tenant context exists and user is not PLATFORM_ADMIN, request is denied.
|
|
14
|
+
*
|
|
15
|
+
* Usage: Add as segment middleware for private and backoffice segments.
|
|
16
|
+
*
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const routes: AdvancedSegmentedRouter = {
|
|
19
|
+
* public: { routes: { ... } }, // ← NO tenantGuard
|
|
20
|
+
* private: { middleware: [tenantGuard], routes: { ... } }, // ← YES
|
|
21
|
+
* backoffice: { middleware: [tenantGuard], routes: { ... } }, // ← YES
|
|
22
|
+
* internal: { routes: { ... } }, // ← NO (uses headers, initTenantContext handles it)
|
|
23
|
+
* };
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Throws forbiddenResponse (403) if:
|
|
27
|
+
* - No tenantInfo in context AND user is not PLATFORM_ADMIN
|
|
28
|
+
* - tenantInfo exists but tenantId is empty or whitespace-only
|
|
29
|
+
*
|
|
30
|
+
* Allows through if:
|
|
31
|
+
* - tenantInfo exists with valid non-empty tenantId
|
|
32
|
+
* - User is PLATFORM_ADMIN (cross-tenant access)
|
|
33
|
+
*/
|
|
34
|
+
export const tenantGuard: MiddlewareFn = async (
|
|
35
|
+
event: NormalizedEvent
|
|
36
|
+
): Promise<NormalizedEvent> => {
|
|
37
|
+
const tenantInfo = event.context.tenantInfo;
|
|
38
|
+
const identity = event.context.identity;
|
|
39
|
+
|
|
40
|
+
// PLATFORM_ADMIN can operate without tenant context (cross-tenant)
|
|
41
|
+
if (hasAnyGroup(identity, CROSS_TENANT_ROLES)) {
|
|
42
|
+
return event;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fail-closed: validate tenantInfo exists AND tenantId is not empty
|
|
46
|
+
if (!tenantInfo?.tenantId || tenantInfo.tenantId.trim() === '') {
|
|
47
|
+
throw forbiddenResponse(
|
|
48
|
+
'Tenant context required. Ensure you are authenticated with a valid tenant.',
|
|
49
|
+
'TENANT_CONTEXT_MISSING' as any
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return event;
|
|
54
|
+
};
|