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
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
import type { TenantInfo } from './types.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* TenantContext provides thread-safe (async-safe) access to the current tenant context.
|
|
6
|
-
*
|
|
7
|
-
* Uses Node.js AsyncLocalStorage to maintain TenantInfo throughout the
|
|
8
|
-
* execution of a request without needing to pass it as a parameter.
|
|
9
|
-
*
|
|
10
|
-
* Initialization:
|
|
11
|
-
* - In HTTP requests: the `initTenantContext` middleware extracts the tenant
|
|
12
|
-
* from JWT claims or headers and calls `TenantContext.set()`.
|
|
13
|
-
* - In EventBridge/SQS: the handler extracts tenantId from event.detail and calls `TenantContext.run()`.
|
|
14
|
-
*
|
|
15
|
-
* Consumption:
|
|
16
|
-
* - TenantAwareDynamoRepository: `TenantContext.current().tenantId`
|
|
17
|
-
* - ApiInvoker: `TenantContext.currentOptional()` to propagate headers
|
|
18
|
-
* - Use cases: `TenantContext.current()` when they need the tenant explicitly
|
|
19
|
-
*
|
|
20
|
-
* IMPORTANT:
|
|
21
|
-
* - `current()` is FAIL-CLOSED: throws error if no context (prevents data leaks).
|
|
22
|
-
* - `currentOptional()` returns undefined without error (for code that works with/without tenant).
|
|
23
|
-
* - `run()` is for scenarios where an explicit scope is needed (EventBridge handlers).
|
|
24
|
-
* - `set()` is for the orchestrator middleware that operates in the same async scope.
|
|
25
|
-
*/
|
|
26
|
-
export class TenantContext {
|
|
27
|
-
private static storage = new AsyncLocalStorage<TenantInfo>();
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Executes a callback within a tenant context.
|
|
31
|
-
* Useful for EventBridge/SQS handlers where there's no orchestrator middleware.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* await TenantContext.run(tenantInfo, async () => {
|
|
36
|
-
* const props = await propertiesRepo.findByStatus('PUBLISHED');
|
|
37
|
-
* // propertiesRepo automatically filters by tenantInfo.tenantId
|
|
38
|
-
* });
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
static run<T>(tenant: TenantInfo, callback: () => T): T {
|
|
42
|
-
return this.storage.run(tenant, callback);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Sets the tenant context in the current AsyncLocalStorage store.
|
|
47
|
-
* ONLY should be called by the initTenantContext middleware.
|
|
48
|
-
*
|
|
49
|
-
* NOTE: Requires an active store (created by AsyncLocalStorage.run()
|
|
50
|
-
* or by the Lambda runtime). If called outside an async context,
|
|
51
|
-
* the value is lost. For those cases, use `run()`.
|
|
52
|
-
*
|
|
53
|
-
* Internally uses enterWith() which replaces the current store.
|
|
54
|
-
*/
|
|
55
|
-
static set(tenant: TenantInfo): void {
|
|
56
|
-
this.storage.enterWith(tenant);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Gets the current TenantInfo. FAIL-CLOSED: throws error if not initialized.
|
|
61
|
-
* Use in code that REQUIRES a tenant (repositories, guards).
|
|
62
|
-
*
|
|
63
|
-
* @throws Error if TenantContext is not initialized
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```typescript
|
|
67
|
-
* const { tenantId } = TenantContext.current();
|
|
68
|
-
* // Safe: if we get here, tenantId exists
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
static current(): TenantInfo {
|
|
72
|
-
const tenant = this.storage.getStore();
|
|
73
|
-
if (!tenant) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
'TenantContext not initialized. Ensure initTenantContext middleware is configured ' +
|
|
76
|
-
'in globalMiddleware, or use TenantContext.run() for non-HTTP triggers.'
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
return tenant;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Gets the current TenantInfo or undefined if not initialized.
|
|
84
|
-
* Use in code that works with or without tenant (ApiInvoker, loggers, public routes).
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```typescript
|
|
88
|
-
* const tenant = TenantContext.currentOptional();
|
|
89
|
-
* if (tenant) {
|
|
90
|
-
* headers['x-tenant-id'] = tenant.tenantId;
|
|
91
|
-
* }
|
|
92
|
-
* ```
|
|
93
|
-
*/
|
|
94
|
-
static currentOptional(): TenantInfo | undefined {
|
|
95
|
-
return this.storage.getStore();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Checks if there's an active tenant context.
|
|
100
|
-
* Useful for conditionals without getting the full object.
|
|
101
|
-
*/
|
|
102
|
-
static isActive(): boolean {
|
|
103
|
-
return this.storage.getStore() !== undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Clears the current context. Only for testing.
|
|
108
|
-
* DO NOT use in production — the context is automatically cleaned when exiting the scope.
|
|
109
|
-
* @internal
|
|
110
|
-
*/
|
|
111
|
-
static _reset(): void {
|
|
112
|
-
this.storage.disable();
|
|
113
|
-
(this as any).storage = new AsyncLocalStorage<TenantInfo>();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { TenantInfo } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TenantContext provides thread-safe (async-safe) access to the current tenant context.
|
|
6
|
+
*
|
|
7
|
+
* Uses Node.js AsyncLocalStorage to maintain TenantInfo throughout the
|
|
8
|
+
* execution of a request without needing to pass it as a parameter.
|
|
9
|
+
*
|
|
10
|
+
* Initialization:
|
|
11
|
+
* - In HTTP requests: the `initTenantContext` middleware extracts the tenant
|
|
12
|
+
* from JWT claims or headers and calls `TenantContext.set()`.
|
|
13
|
+
* - In EventBridge/SQS: the handler extracts tenantId from event.detail and calls `TenantContext.run()`.
|
|
14
|
+
*
|
|
15
|
+
* Consumption:
|
|
16
|
+
* - TenantAwareDynamoRepository: `TenantContext.current().tenantId`
|
|
17
|
+
* - ApiInvoker: `TenantContext.currentOptional()` to propagate headers
|
|
18
|
+
* - Use cases: `TenantContext.current()` when they need the tenant explicitly
|
|
19
|
+
*
|
|
20
|
+
* IMPORTANT:
|
|
21
|
+
* - `current()` is FAIL-CLOSED: throws error if no context (prevents data leaks).
|
|
22
|
+
* - `currentOptional()` returns undefined without error (for code that works with/without tenant).
|
|
23
|
+
* - `run()` is for scenarios where an explicit scope is needed (EventBridge handlers).
|
|
24
|
+
* - `set()` is for the orchestrator middleware that operates in the same async scope.
|
|
25
|
+
*/
|
|
26
|
+
export class TenantContext {
|
|
27
|
+
private static storage = new AsyncLocalStorage<TenantInfo>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Executes a callback within a tenant context.
|
|
31
|
+
* Useful for EventBridge/SQS handlers where there's no orchestrator middleware.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* await TenantContext.run(tenantInfo, async () => {
|
|
36
|
+
* const props = await propertiesRepo.findByStatus('PUBLISHED');
|
|
37
|
+
* // propertiesRepo automatically filters by tenantInfo.tenantId
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
static run<T>(tenant: TenantInfo, callback: () => T): T {
|
|
42
|
+
return this.storage.run(tenant, callback);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sets the tenant context in the current AsyncLocalStorage store.
|
|
47
|
+
* ONLY should be called by the initTenantContext middleware.
|
|
48
|
+
*
|
|
49
|
+
* NOTE: Requires an active store (created by AsyncLocalStorage.run()
|
|
50
|
+
* or by the Lambda runtime). If called outside an async context,
|
|
51
|
+
* the value is lost. For those cases, use `run()`.
|
|
52
|
+
*
|
|
53
|
+
* Internally uses enterWith() which replaces the current store.
|
|
54
|
+
*/
|
|
55
|
+
static set(tenant: TenantInfo): void {
|
|
56
|
+
this.storage.enterWith(tenant);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Gets the current TenantInfo. FAIL-CLOSED: throws error if not initialized.
|
|
61
|
+
* Use in code that REQUIRES a tenant (repositories, guards).
|
|
62
|
+
*
|
|
63
|
+
* @throws Error if TenantContext is not initialized
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const { tenantId } = TenantContext.current();
|
|
68
|
+
* // Safe: if we get here, tenantId exists
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
static current(): TenantInfo {
|
|
72
|
+
const tenant = this.storage.getStore();
|
|
73
|
+
if (!tenant) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'TenantContext not initialized. Ensure initTenantContext middleware is configured ' +
|
|
76
|
+
'in globalMiddleware, or use TenantContext.run() for non-HTTP triggers.'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return tenant;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets the current TenantInfo or undefined if not initialized.
|
|
84
|
+
* Use in code that works with or without tenant (ApiInvoker, loggers, public routes).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const tenant = TenantContext.currentOptional();
|
|
89
|
+
* if (tenant) {
|
|
90
|
+
* headers['x-tenant-id'] = tenant.tenantId;
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
static currentOptional(): TenantInfo | undefined {
|
|
95
|
+
return this.storage.getStore();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if there's an active tenant context.
|
|
100
|
+
* Useful for conditionals without getting the full object.
|
|
101
|
+
*/
|
|
102
|
+
static isActive(): boolean {
|
|
103
|
+
return this.storage.getStore() !== undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Clears the current context. Only for testing.
|
|
108
|
+
* DO NOT use in production — the context is automatically cleaned when exiting the scope.
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
static _reset(): void {
|
|
112
|
+
this.storage.disable();
|
|
113
|
+
(this as any).storage = new AsyncLocalStorage<TenantInfo>();
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/tenant/helpers.ts
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import type { TenantInfo } from './types.js';
|
|
2
|
-
import { isTenantType, isPlan, TENANT_HEADERS } from './types.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolves a claim value by checking both `custom:{key}` and `{key}` formats.
|
|
6
|
-
* Cognito Pre Token Generation V2 adds claims WITHOUT the `custom:` prefix,
|
|
7
|
-
* while Cognito user pool custom attributes use the `custom:` prefix.
|
|
8
|
-
* This helper supports both conventions transparently.
|
|
9
|
-
*/
|
|
10
|
-
function getClaim(claims: Record<string, any>, key: string): any {
|
|
11
|
-
return claims[`custom:${key}`] ?? claims[key];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Builds a TenantInfo from TenantClaims in the JWT.
|
|
16
|
-
* Used by initTenantContext middleware in serverless-event-orchestrator.
|
|
17
|
-
*
|
|
18
|
-
* Supports claims with or without the `custom:` prefix:
|
|
19
|
-
* - `custom:tenantId` (Cognito user pool custom attributes)
|
|
20
|
-
* - `tenantId` (Cognito Pre Token Generation V2 added claims)
|
|
21
|
-
*
|
|
22
|
-
* @param claims - Partial claims object from JWT (event.context.identity.claims)
|
|
23
|
-
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
24
|
-
*/
|
|
25
|
-
export function tenantInfoFromClaims(claims: Record<string, any>): TenantInfo | undefined {
|
|
26
|
-
const tenantId = getClaim(claims, 'tenantId');
|
|
27
|
-
const tenantType = getClaim(claims, 'tenantType');
|
|
28
|
-
const userId = getClaim(claims, 'userId') || claims['sub'];
|
|
29
|
-
const countryCode = getClaim(claims, 'countryCode');
|
|
30
|
-
const personProfileId = getClaim(claims, 'personProfileId');
|
|
31
|
-
const orgProfileId = getClaim(claims, 'orgProfileId');
|
|
32
|
-
const hasCRM = getClaim(claims, 'hasCRM');
|
|
33
|
-
|
|
34
|
-
if (!tenantId || !tenantType || !userId || !countryCode) {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!isTenantType(tenantType)) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
tenantId,
|
|
44
|
-
tenantType,
|
|
45
|
-
userId,
|
|
46
|
-
personProfileId,
|
|
47
|
-
orgProfileId,
|
|
48
|
-
countryCode,
|
|
49
|
-
plan: isPlan(getClaim(claims, 'plan')) ? getClaim(claims, 'plan') : undefined,
|
|
50
|
-
hasCRM,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Builds a TenantInfo from headers (Lambda-to-Lambda).
|
|
56
|
-
* Used by initTenantContext middleware for internal routes.
|
|
57
|
-
*
|
|
58
|
-
* @param headers - Headers object (may have original or lowercase keys)
|
|
59
|
-
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
60
|
-
*/
|
|
61
|
-
export function tenantInfoFromHeaders(
|
|
62
|
-
headers: Record<string, string | undefined>
|
|
63
|
-
): TenantInfo | undefined {
|
|
64
|
-
const get = (key: string) => headers[key] || headers[key.toLowerCase()];
|
|
65
|
-
|
|
66
|
-
const tenantId = get(TENANT_HEADERS.TENANT_ID);
|
|
67
|
-
const tenantType = get(TENANT_HEADERS.TENANT_TYPE);
|
|
68
|
-
const userId = get(TENANT_HEADERS.USER_ID);
|
|
69
|
-
const countryCode = get(TENANT_HEADERS.COUNTRY_CODE);
|
|
70
|
-
|
|
71
|
-
if (!tenantId || !tenantType || !userId || !countryCode) {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!isTenantType(tenantType)) {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
tenantId,
|
|
81
|
-
tenantType: tenantType as 'ORG' | 'PERSON',
|
|
82
|
-
userId,
|
|
83
|
-
personProfileId: get(TENANT_HEADERS.PERSON_PROFILE_ID),
|
|
84
|
-
orgProfileId: get(TENANT_HEADERS.ORG_PROFILE_ID),
|
|
85
|
-
countryCode,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Converts TenantInfo to headers for Lambda-to-Lambda propagation.
|
|
91
|
-
* Used by ApiInvoker to propagate the context.
|
|
92
|
-
*
|
|
93
|
-
* @param tenant - TenantInfo to serialize
|
|
94
|
-
* @returns Headers object with X-Tenant-* headers
|
|
95
|
-
*/
|
|
96
|
-
export function tenantInfoToHeaders(tenant: TenantInfo): Record<string, string> {
|
|
97
|
-
const headers: Record<string, string> = {
|
|
98
|
-
[TENANT_HEADERS.TENANT_ID]: tenant.tenantId,
|
|
99
|
-
[TENANT_HEADERS.TENANT_TYPE]: tenant.tenantType,
|
|
100
|
-
[TENANT_HEADERS.USER_ID]: tenant.userId,
|
|
101
|
-
[TENANT_HEADERS.COUNTRY_CODE]: tenant.countryCode,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
if (tenant.personProfileId) {
|
|
105
|
-
headers[TENANT_HEADERS.PERSON_PROFILE_ID] = tenant.personProfileId;
|
|
106
|
-
}
|
|
107
|
-
if (tenant.orgProfileId) {
|
|
108
|
-
headers[TENANT_HEADERS.ORG_PROFILE_ID] = tenant.orgProfileId;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return headers;
|
|
112
|
-
}
|
|
1
|
+
import type { TenantInfo } from './types.js';
|
|
2
|
+
import { isTenantType, isPlan, TENANT_HEADERS } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a claim value by checking both `custom:{key}` and `{key}` formats.
|
|
6
|
+
* Cognito Pre Token Generation V2 adds claims WITHOUT the `custom:` prefix,
|
|
7
|
+
* while Cognito user pool custom attributes use the `custom:` prefix.
|
|
8
|
+
* This helper supports both conventions transparently.
|
|
9
|
+
*/
|
|
10
|
+
function getClaim(claims: Record<string, any>, key: string): any {
|
|
11
|
+
return claims[`custom:${key}`] ?? claims[key];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds a TenantInfo from TenantClaims in the JWT.
|
|
16
|
+
* Used by initTenantContext middleware in serverless-event-orchestrator.
|
|
17
|
+
*
|
|
18
|
+
* Supports claims with or without the `custom:` prefix:
|
|
19
|
+
* - `custom:tenantId` (Cognito user pool custom attributes)
|
|
20
|
+
* - `tenantId` (Cognito Pre Token Generation V2 added claims)
|
|
21
|
+
*
|
|
22
|
+
* @param claims - Partial claims object from JWT (event.context.identity.claims)
|
|
23
|
+
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
24
|
+
*/
|
|
25
|
+
export function tenantInfoFromClaims(claims: Record<string, any>): TenantInfo | undefined {
|
|
26
|
+
const tenantId = getClaim(claims, 'tenantId');
|
|
27
|
+
const tenantType = getClaim(claims, 'tenantType');
|
|
28
|
+
const userId = getClaim(claims, 'userId') || claims['sub'];
|
|
29
|
+
const countryCode = getClaim(claims, 'countryCode');
|
|
30
|
+
const personProfileId = getClaim(claims, 'personProfileId');
|
|
31
|
+
const orgProfileId = getClaim(claims, 'orgProfileId');
|
|
32
|
+
const hasCRM = getClaim(claims, 'hasCRM');
|
|
33
|
+
|
|
34
|
+
if (!tenantId || !tenantType || !userId || !countryCode) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!isTenantType(tenantType)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
tenantId,
|
|
44
|
+
tenantType,
|
|
45
|
+
userId,
|
|
46
|
+
personProfileId,
|
|
47
|
+
orgProfileId,
|
|
48
|
+
countryCode,
|
|
49
|
+
plan: isPlan(getClaim(claims, 'plan')) ? getClaim(claims, 'plan') : undefined,
|
|
50
|
+
hasCRM,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Builds a TenantInfo from headers (Lambda-to-Lambda).
|
|
56
|
+
* Used by initTenantContext middleware for internal routes.
|
|
57
|
+
*
|
|
58
|
+
* @param headers - Headers object (may have original or lowercase keys)
|
|
59
|
+
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
60
|
+
*/
|
|
61
|
+
export function tenantInfoFromHeaders(
|
|
62
|
+
headers: Record<string, string | undefined>
|
|
63
|
+
): TenantInfo | undefined {
|
|
64
|
+
const get = (key: string) => headers[key] || headers[key.toLowerCase()];
|
|
65
|
+
|
|
66
|
+
const tenantId = get(TENANT_HEADERS.TENANT_ID);
|
|
67
|
+
const tenantType = get(TENANT_HEADERS.TENANT_TYPE);
|
|
68
|
+
const userId = get(TENANT_HEADERS.USER_ID);
|
|
69
|
+
const countryCode = get(TENANT_HEADERS.COUNTRY_CODE);
|
|
70
|
+
|
|
71
|
+
if (!tenantId || !tenantType || !userId || !countryCode) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!isTenantType(tenantType)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
tenantId,
|
|
81
|
+
tenantType: tenantType as 'ORG' | 'PERSON',
|
|
82
|
+
userId,
|
|
83
|
+
personProfileId: get(TENANT_HEADERS.PERSON_PROFILE_ID),
|
|
84
|
+
orgProfileId: get(TENANT_HEADERS.ORG_PROFILE_ID),
|
|
85
|
+
countryCode,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Converts TenantInfo to headers for Lambda-to-Lambda propagation.
|
|
91
|
+
* Used by ApiInvoker to propagate the context.
|
|
92
|
+
*
|
|
93
|
+
* @param tenant - TenantInfo to serialize
|
|
94
|
+
* @returns Headers object with X-Tenant-* headers
|
|
95
|
+
*/
|
|
96
|
+
export function tenantInfoToHeaders(tenant: TenantInfo): Record<string, string> {
|
|
97
|
+
const headers: Record<string, string> = {
|
|
98
|
+
[TENANT_HEADERS.TENANT_ID]: tenant.tenantId,
|
|
99
|
+
[TENANT_HEADERS.TENANT_TYPE]: tenant.tenantType,
|
|
100
|
+
[TENANT_HEADERS.USER_ID]: tenant.userId,
|
|
101
|
+
[TENANT_HEADERS.COUNTRY_CODE]: tenant.countryCode,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (tenant.personProfileId) {
|
|
105
|
+
headers[TENANT_HEADERS.PERSON_PROFILE_ID] = tenant.personProfileId;
|
|
106
|
+
}
|
|
107
|
+
if (tenant.orgProfileId) {
|
|
108
|
+
headers[TENANT_HEADERS.ORG_PROFILE_ID] = tenant.orgProfileId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return headers;
|
|
112
|
+
}
|
package/src/tenant/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
export { TenantContext } from './TenantContext.js';
|
|
2
|
-
|
|
3
|
-
export type {
|
|
4
|
-
TenantInfo,
|
|
5
|
-
TenantType,
|
|
6
|
-
Plan,
|
|
7
|
-
TenantFeatures,
|
|
8
|
-
TenantClaims,
|
|
9
|
-
} from './types.js';
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
isTenantType,
|
|
13
|
-
isPlan,
|
|
14
|
-
TENANT_HEADERS,
|
|
15
|
-
} from './types.js';
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
tenantInfoFromClaims,
|
|
19
|
-
tenantInfoFromHeaders,
|
|
20
|
-
tenantInfoToHeaders,
|
|
21
|
-
} from './helpers.js';
|
|
1
|
+
export { TenantContext } from './TenantContext.js';
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
TenantInfo,
|
|
5
|
+
TenantType,
|
|
6
|
+
Plan,
|
|
7
|
+
TenantFeatures,
|
|
8
|
+
TenantClaims,
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
isTenantType,
|
|
13
|
+
isPlan,
|
|
14
|
+
TENANT_HEADERS,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
tenantInfoFromClaims,
|
|
19
|
+
tenantInfoFromHeaders,
|
|
20
|
+
tenantInfoToHeaders,
|
|
21
|
+
} from './helpers.js';
|