serverless-event-orchestrator 1.2.7 → 1.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/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +30 -7
- package/dist/dispatcher.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/crm-guard.d.ts +25 -0
- package/dist/middleware/crm-guard.d.ts.map +1 -0
- package/dist/middleware/crm-guard.js +45 -0
- package/dist/middleware/crm-guard.js.map +1 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +10 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/init-tenant-context.d.ts +21 -0
- package/dist/middleware/init-tenant-context.d.ts.map +1 -0
- package/dist/middleware/init-tenant-context.js +50 -0
- package/dist/middleware/init-tenant-context.js.map +1 -0
- package/dist/middleware/tenant-guard.d.ts +26 -0
- package/dist/middleware/tenant-guard.d.ts.map +1 -0
- package/dist/middleware/tenant-guard.js +48 -0
- package/dist/middleware/tenant-guard.js.map +1 -0
- package/dist/tenant/TenantContext.d.ts +88 -0
- package/dist/tenant/TenantContext.d.ts.map +1 -0
- package/dist/tenant/TenantContext.js +110 -0
- package/dist/tenant/TenantContext.js.map +1 -0
- package/dist/tenant/helpers.d.ts +26 -0
- package/dist/tenant/helpers.d.ts.map +1 -0
- package/dist/tenant/helpers.js +86 -0
- package/dist/tenant/helpers.js.map +1 -0
- package/dist/tenant/index.d.ts +5 -0
- package/dist/tenant/index.d.ts.map +1 -0
- package/dist/tenant/index.js +14 -0
- package/dist/tenant/index.js.map +1 -0
- package/dist/tenant/types.d.ts +84 -0
- package/dist/tenant/types.d.ts.map +1 -0
- package/dist/tenant/types.js +29 -0
- package/dist/tenant/types.js.map +1 -0
- package/dist/types/routes.d.ts +2 -0
- package/dist/types/routes.d.ts.map +1 -1
- package/package.json +11 -1
- package/src/dispatcher.ts +34 -9
- package/src/index.ts +30 -0
- package/src/middleware/crm-guard.ts +51 -0
- package/src/middleware/index.ts +3 -0
- package/src/middleware/init-tenant-context.ts +59 -0
- package/src/middleware/tenant-guard.ts +54 -0
- package/src/tenant/TenantContext.ts +115 -0
- package/src/tenant/helpers.ts +95 -0
- package/src/tenant/index.ts +21 -0
- package/src/tenant/types.ts +101 -0
- package/src/types/routes.ts +2 -0
- package/tests/middleware/crm-guard.test.ts +69 -0
- package/tests/middleware/init-tenant-context.test.ts +147 -0
- package/tests/middleware/tenant-guard.test.ts +100 -0
- package/tests/tenant/TenantContext.test.ts +134 -0
- package/tests/tenant/helpers.test.ts +122 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TenantContext = void 0;
|
|
4
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
+
/**
|
|
6
|
+
* TenantContext provides thread-safe (async-safe) access to the current tenant context.
|
|
7
|
+
*
|
|
8
|
+
* Uses Node.js AsyncLocalStorage to maintain TenantInfo throughout the
|
|
9
|
+
* execution of a request without needing to pass it as a parameter.
|
|
10
|
+
*
|
|
11
|
+
* Initialization:
|
|
12
|
+
* - In HTTP requests: the `initTenantContext` middleware extracts the tenant
|
|
13
|
+
* from JWT claims or headers and calls `TenantContext.set()`.
|
|
14
|
+
* - In EventBridge/SQS: the handler extracts tenantId from event.detail and calls `TenantContext.run()`.
|
|
15
|
+
*
|
|
16
|
+
* Consumption:
|
|
17
|
+
* - TenantAwareDynamoRepository: `TenantContext.current().tenantId`
|
|
18
|
+
* - ApiInvoker: `TenantContext.currentOptional()` to propagate headers
|
|
19
|
+
* - Use cases: `TenantContext.current()` when they need the tenant explicitly
|
|
20
|
+
*
|
|
21
|
+
* IMPORTANT:
|
|
22
|
+
* - `current()` is FAIL-CLOSED: throws error if no context (prevents data leaks).
|
|
23
|
+
* - `currentOptional()` returns undefined without error (for code that works with/without tenant).
|
|
24
|
+
* - `run()` is for scenarios where an explicit scope is needed (EventBridge handlers).
|
|
25
|
+
* - `set()` is for the orchestrator middleware that operates in the same async scope.
|
|
26
|
+
*/
|
|
27
|
+
class TenantContext {
|
|
28
|
+
static storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
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(tenant, callback) {
|
|
42
|
+
return this.storage.run(tenant, callback);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sets the tenant context in the current AsyncLocalStorage store.
|
|
46
|
+
* ONLY should be called by the initTenantContext middleware.
|
|
47
|
+
*
|
|
48
|
+
* NOTE: Requires an active store (created by AsyncLocalStorage.run()
|
|
49
|
+
* or by the Lambda runtime). If called outside an async context,
|
|
50
|
+
* the value is lost. For those cases, use `run()`.
|
|
51
|
+
*
|
|
52
|
+
* Internally uses enterWith() which replaces the current store.
|
|
53
|
+
*/
|
|
54
|
+
static set(tenant) {
|
|
55
|
+
this.storage.enterWith(tenant);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Gets the current TenantInfo. FAIL-CLOSED: throws error if not initialized.
|
|
59
|
+
* Use in code that REQUIRES a tenant (repositories, guards).
|
|
60
|
+
*
|
|
61
|
+
* @throws Error if TenantContext is not initialized
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const { tenantId } = TenantContext.current();
|
|
66
|
+
* // Safe: if we get here, tenantId exists
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
static current() {
|
|
70
|
+
const tenant = this.storage.getStore();
|
|
71
|
+
if (!tenant) {
|
|
72
|
+
throw new Error('TenantContext not initialized. Ensure initTenantContext middleware is configured ' +
|
|
73
|
+
'in globalMiddleware, or use TenantContext.run() for non-HTTP triggers.');
|
|
74
|
+
}
|
|
75
|
+
return tenant;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Gets the current TenantInfo or undefined if not initialized.
|
|
79
|
+
* Use in code that works with or without tenant (ApiInvoker, loggers, public routes).
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const tenant = TenantContext.currentOptional();
|
|
84
|
+
* if (tenant) {
|
|
85
|
+
* headers['x-tenant-id'] = tenant.tenantId;
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
static currentOptional() {
|
|
90
|
+
return this.storage.getStore();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Checks if there's an active tenant context.
|
|
94
|
+
* Useful for conditionals without getting the full object.
|
|
95
|
+
*/
|
|
96
|
+
static isActive() {
|
|
97
|
+
return this.storage.getStore() !== undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clears the current context. Only for testing.
|
|
101
|
+
* DO NOT use in production — the context is automatically cleaned when exiting the scope.
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
static _reset() {
|
|
105
|
+
this.storage.disable();
|
|
106
|
+
this.storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.TenantContext = TenantContext;
|
|
110
|
+
//# sourceMappingURL=TenantContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantContext.js","sourceRoot":"","sources":["../../src/tenant/TenantContext.ts"],"names":[],"mappings":";;;AAAA,uDAAqD;AAGrD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,aAAa;IAChB,MAAM,CAAC,OAAO,GAAG,IAAI,oCAAiB,EAAc,CAAC;IAE7D;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,GAAG,CAAI,MAAkB,EAAE,QAAiB;QACjD,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,GAAG,CAAC,MAAkB;QAC3B,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;IAED;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,OAAO;QACZ,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACvC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACb,mFAAmF;gBACnF,wEAAwE,CACzE,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,eAAe;QACpB,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,QAAQ;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,SAAS,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,MAAM;QACX,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACtB,IAAY,CAAC,OAAO,GAAG,IAAI,oCAAiB,EAAc,CAAC;IAC9D,CAAC;;AAxFH,sCAyFC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TenantInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a TenantInfo from TenantClaims in the JWT.
|
|
4
|
+
* Used by initTenantContext middleware in serverless-event-orchestrator.
|
|
5
|
+
*
|
|
6
|
+
* @param claims - Partial claims object from JWT (event.context.identity.claims)
|
|
7
|
+
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
8
|
+
*/
|
|
9
|
+
export declare function tenantInfoFromClaims(claims: Record<string, any>): TenantInfo | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Builds a TenantInfo from headers (Lambda-to-Lambda).
|
|
12
|
+
* Used by initTenantContext middleware for internal routes.
|
|
13
|
+
*
|
|
14
|
+
* @param headers - Headers object (may have original or lowercase keys)
|
|
15
|
+
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
16
|
+
*/
|
|
17
|
+
export declare function tenantInfoFromHeaders(headers: Record<string, string | undefined>): TenantInfo | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Converts TenantInfo to headers for Lambda-to-Lambda propagation.
|
|
20
|
+
* Used by ApiInvoker to propagate the context.
|
|
21
|
+
*
|
|
22
|
+
* @param tenant - TenantInfo to serialize
|
|
23
|
+
* @returns Headers object with X-Tenant-* headers
|
|
24
|
+
*/
|
|
25
|
+
export declare function tenantInfoToHeaders(tenant: TenantInfo): Record<string, string>;
|
|
26
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/tenant/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,UAAU,GAAG,SAAS,CAwBxF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC1C,UAAU,GAAG,SAAS,CAwBxB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgB9E"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tenantInfoFromClaims = tenantInfoFromClaims;
|
|
4
|
+
exports.tenantInfoFromHeaders = tenantInfoFromHeaders;
|
|
5
|
+
exports.tenantInfoToHeaders = tenantInfoToHeaders;
|
|
6
|
+
const types_js_1 = require("./types.js");
|
|
7
|
+
/**
|
|
8
|
+
* Builds a TenantInfo from TenantClaims in the JWT.
|
|
9
|
+
* Used by initTenantContext middleware in serverless-event-orchestrator.
|
|
10
|
+
*
|
|
11
|
+
* @param claims - Partial claims object from JWT (event.context.identity.claims)
|
|
12
|
+
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
13
|
+
*/
|
|
14
|
+
function tenantInfoFromClaims(claims) {
|
|
15
|
+
const tenantId = claims['custom:tenantId'];
|
|
16
|
+
const tenantType = claims['custom:tenantType'];
|
|
17
|
+
const userId = claims['custom:userId'] || claims['sub'];
|
|
18
|
+
const countryCode = claims['custom:countryCode'];
|
|
19
|
+
if (!tenantId || !tenantType || !userId || !countryCode) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (!(0, types_js_1.isTenantType)(tenantType)) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
tenantId,
|
|
27
|
+
tenantType,
|
|
28
|
+
userId,
|
|
29
|
+
personProfileId: claims['custom:personProfileId'],
|
|
30
|
+
orgProfileId: claims['custom:orgProfileId'],
|
|
31
|
+
countryCode,
|
|
32
|
+
plan: (0, types_js_1.isPlan)(claims['custom:plan']) ? claims['custom:plan'] : undefined,
|
|
33
|
+
hasCRM: claims['custom:hasCRM'] === 'true',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds a TenantInfo from headers (Lambda-to-Lambda).
|
|
38
|
+
* Used by initTenantContext middleware for internal routes.
|
|
39
|
+
*
|
|
40
|
+
* @param headers - Headers object (may have original or lowercase keys)
|
|
41
|
+
* @returns TenantInfo if all required fields are present, undefined otherwise
|
|
42
|
+
*/
|
|
43
|
+
function tenantInfoFromHeaders(headers) {
|
|
44
|
+
const get = (key) => headers[key] || headers[key.toLowerCase()];
|
|
45
|
+
const tenantId = get(types_js_1.TENANT_HEADERS.TENANT_ID);
|
|
46
|
+
const tenantType = get(types_js_1.TENANT_HEADERS.TENANT_TYPE);
|
|
47
|
+
const userId = get(types_js_1.TENANT_HEADERS.USER_ID);
|
|
48
|
+
const countryCode = get(types_js_1.TENANT_HEADERS.COUNTRY_CODE);
|
|
49
|
+
if (!tenantId || !tenantType || !userId || !countryCode) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (!(0, types_js_1.isTenantType)(tenantType)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
tenantId,
|
|
57
|
+
tenantType: tenantType,
|
|
58
|
+
userId,
|
|
59
|
+
personProfileId: get(types_js_1.TENANT_HEADERS.PERSON_PROFILE_ID),
|
|
60
|
+
orgProfileId: get(types_js_1.TENANT_HEADERS.ORG_PROFILE_ID),
|
|
61
|
+
countryCode,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Converts TenantInfo to headers for Lambda-to-Lambda propagation.
|
|
66
|
+
* Used by ApiInvoker to propagate the context.
|
|
67
|
+
*
|
|
68
|
+
* @param tenant - TenantInfo to serialize
|
|
69
|
+
* @returns Headers object with X-Tenant-* headers
|
|
70
|
+
*/
|
|
71
|
+
function tenantInfoToHeaders(tenant) {
|
|
72
|
+
const headers = {
|
|
73
|
+
[types_js_1.TENANT_HEADERS.TENANT_ID]: tenant.tenantId,
|
|
74
|
+
[types_js_1.TENANT_HEADERS.TENANT_TYPE]: tenant.tenantType,
|
|
75
|
+
[types_js_1.TENANT_HEADERS.USER_ID]: tenant.userId,
|
|
76
|
+
[types_js_1.TENANT_HEADERS.COUNTRY_CODE]: tenant.countryCode,
|
|
77
|
+
};
|
|
78
|
+
if (tenant.personProfileId) {
|
|
79
|
+
headers[types_js_1.TENANT_HEADERS.PERSON_PROFILE_ID] = tenant.personProfileId;
|
|
80
|
+
}
|
|
81
|
+
if (tenant.orgProfileId) {
|
|
82
|
+
headers[types_js_1.TENANT_HEADERS.ORG_PROFILE_ID] = tenant.orgProfileId;
|
|
83
|
+
}
|
|
84
|
+
return headers;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/tenant/helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { TenantContext } from './TenantContext.js';
|
|
2
|
+
export type { TenantInfo, TenantType, Plan, TenantFeatures, TenantClaims, } from './types.js';
|
|
3
|
+
export { isTenantType, isPlan, TENANT_HEADERS, } from './types.js';
|
|
4
|
+
export { tenantInfoFromClaims, tenantInfoFromHeaders, tenantInfoToHeaders, } from './helpers.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tenant/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,YAAY,EACV,UAAU,EACV,UAAU,EACV,IAAI,EACJ,cAAc,EACd,YAAY,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,YAAY,EACZ,MAAM,EACN,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tenantInfoToHeaders = exports.tenantInfoFromHeaders = exports.tenantInfoFromClaims = exports.TENANT_HEADERS = exports.isPlan = exports.isTenantType = exports.TenantContext = void 0;
|
|
4
|
+
var TenantContext_js_1 = require("./TenantContext.js");
|
|
5
|
+
Object.defineProperty(exports, "TenantContext", { enumerable: true, get: function () { return TenantContext_js_1.TenantContext; } });
|
|
6
|
+
var types_js_1 = require("./types.js");
|
|
7
|
+
Object.defineProperty(exports, "isTenantType", { enumerable: true, get: function () { return types_js_1.isTenantType; } });
|
|
8
|
+
Object.defineProperty(exports, "isPlan", { enumerable: true, get: function () { return types_js_1.isPlan; } });
|
|
9
|
+
Object.defineProperty(exports, "TENANT_HEADERS", { enumerable: true, get: function () { return types_js_1.TENANT_HEADERS; } });
|
|
10
|
+
var helpers_js_1 = require("./helpers.js");
|
|
11
|
+
Object.defineProperty(exports, "tenantInfoFromClaims", { enumerable: true, get: function () { return helpers_js_1.tenantInfoFromClaims; } });
|
|
12
|
+
Object.defineProperty(exports, "tenantInfoFromHeaders", { enumerable: true, get: function () { return helpers_js_1.tenantInfoFromHeaders; } });
|
|
13
|
+
Object.defineProperty(exports, "tenantInfoToHeaders", { enumerable: true, get: function () { return helpers_js_1.tenantInfoToHeaders; } });
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tenant/index.ts"],"names":[],"mappings":";;;AAAA,uDAAmD;AAA1C,iHAAA,aAAa,OAAA;AAUtB,uCAIoB;AAHlB,wGAAA,YAAY,OAAA;AACZ,kGAAA,MAAM,OAAA;AACN,0GAAA,cAAc,OAAA;AAGhB,2CAIsB;AAHpB,kHAAA,oBAAoB,OAAA;AACpB,mHAAA,qBAAqB,OAAA;AACrB,iHAAA,mBAAmB,OAAA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant types for the multi-tenant system.
|
|
3
|
+
* - ORG: Organization/Agency (tenantId = orgProfileId)
|
|
4
|
+
* - PERSON: Independent agent (tenantId = personProfileId)
|
|
5
|
+
*/
|
|
6
|
+
export type TenantType = 'ORG' | 'PERSON';
|
|
7
|
+
/**
|
|
8
|
+
* SaaS subscription plans.
|
|
9
|
+
* Determine the features available for a tenant.
|
|
10
|
+
* Stored in the Tenants table and injected into JWT via Pre Token Generation.
|
|
11
|
+
*/
|
|
12
|
+
export type Plan = 'FREE' | 'BASIC' | 'PRO' | 'ENTERPRISE';
|
|
13
|
+
/**
|
|
14
|
+
* Features enabled per plan.
|
|
15
|
+
* Stored in the Tenants table and queried from JWT claim or table directly.
|
|
16
|
+
*/
|
|
17
|
+
export interface TenantFeatures {
|
|
18
|
+
maxProperties: number;
|
|
19
|
+
hasWhiteLabelWebsite: boolean;
|
|
20
|
+
hasCRMAccess: boolean;
|
|
21
|
+
hasAdvancedAnalytics: boolean;
|
|
22
|
+
maxAgents: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Tenant information that travels in the context of each request.
|
|
26
|
+
* Propagated via AsyncLocalStorage and available throughout the async chain.
|
|
27
|
+
*
|
|
28
|
+
* Filled from JWT claims (private/backoffice routes) or from headers
|
|
29
|
+
* X-Tenant-Id (internal routes, Lambda-to-Lambda).
|
|
30
|
+
*/
|
|
31
|
+
export interface TenantInfo {
|
|
32
|
+
/** Tenant ID: orgProfileId (ORG) or personProfileId (PERSON) */
|
|
33
|
+
tenantId: string;
|
|
34
|
+
/** Tenant type */
|
|
35
|
+
tenantType: TenantType;
|
|
36
|
+
/** Authenticated user ID (Cognito sub or custom:userId) */
|
|
37
|
+
userId: string;
|
|
38
|
+
/** PersonProfileId of the user (always present, it's their personal profile) */
|
|
39
|
+
personProfileId?: string;
|
|
40
|
+
/** OrgProfileId if belongs to an organization (only for TenantType = ORG) */
|
|
41
|
+
orgProfileId?: string;
|
|
42
|
+
/** User's country code (ISO 3166-1 alpha-2, e.g., 'CO', 'MX', 'US') */
|
|
43
|
+
countryCode: string;
|
|
44
|
+
/** Tenant's plan (optional, filled if present in JWT) */
|
|
45
|
+
plan?: Plan;
|
|
46
|
+
/** Whether the tenant has CRM access (shortcut for features.hasCRMAccess) */
|
|
47
|
+
hasCRM?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Custom claims injected into the Cognito JWT via Pre Token Generation.
|
|
51
|
+
* These claims are available in event.context.identity.claims
|
|
52
|
+
* after the orchestrator extracts the identity.
|
|
53
|
+
*/
|
|
54
|
+
export interface TenantClaims {
|
|
55
|
+
'custom:tenantId': string;
|
|
56
|
+
'custom:tenantType': TenantType;
|
|
57
|
+
'custom:userId': string;
|
|
58
|
+
'custom:personProfileId': string;
|
|
59
|
+
'custom:orgProfileId'?: string;
|
|
60
|
+
'custom:countryCode': string;
|
|
61
|
+
'custom:plan'?: Plan;
|
|
62
|
+
'custom:hasCRM'?: string;
|
|
63
|
+
'custom:hasWhiteLabel'?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Headers used to propagate tenant in Lambda-to-Lambda calls.
|
|
67
|
+
*/
|
|
68
|
+
export declare const TENANT_HEADERS: {
|
|
69
|
+
readonly TENANT_ID: "x-tenant-id";
|
|
70
|
+
readonly TENANT_TYPE: "x-tenant-type";
|
|
71
|
+
readonly USER_ID: "x-user-id";
|
|
72
|
+
readonly COUNTRY_CODE: "x-country-code";
|
|
73
|
+
readonly PERSON_PROFILE_ID: "x-person-profile-id";
|
|
74
|
+
readonly ORG_PROFILE_ID: "x-org-profile-id";
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Type guard: checks if a value is a valid TenantType.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isTenantType(value: unknown): value is TenantType;
|
|
80
|
+
/**
|
|
81
|
+
* Type guard: checks if a value is a valid Plan.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isPlan(value: unknown): value is Plan;
|
|
84
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/tenant/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE1C;;;;GAIG;AACH,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,YAAY,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IAEjB,kBAAkB;IAClB,UAAU,EAAE,UAAU,CAAC;IAEvB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IAEf,gFAAgF;IAChF,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IAEpB,yDAAyD;IACzD,IAAI,CAAC,EAAE,IAAI,CAAC;IAEZ,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,UAAU,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,wBAAwB,EAAE,MAAM,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;;CAOjB,CAAC;AAEX;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,IAAI,CAEpD"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TENANT_HEADERS = void 0;
|
|
4
|
+
exports.isTenantType = isTenantType;
|
|
5
|
+
exports.isPlan = isPlan;
|
|
6
|
+
/**
|
|
7
|
+
* Headers used to propagate tenant in Lambda-to-Lambda calls.
|
|
8
|
+
*/
|
|
9
|
+
exports.TENANT_HEADERS = {
|
|
10
|
+
TENANT_ID: 'x-tenant-id',
|
|
11
|
+
TENANT_TYPE: 'x-tenant-type',
|
|
12
|
+
USER_ID: 'x-user-id',
|
|
13
|
+
COUNTRY_CODE: 'x-country-code',
|
|
14
|
+
PERSON_PROFILE_ID: 'x-person-profile-id',
|
|
15
|
+
ORG_PROFILE_ID: 'x-org-profile-id',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Type guard: checks if a value is a valid TenantType.
|
|
19
|
+
*/
|
|
20
|
+
function isTenantType(value) {
|
|
21
|
+
return value === 'ORG' || value === 'PERSON';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Type guard: checks if a value is a valid Plan.
|
|
25
|
+
*/
|
|
26
|
+
function isPlan(value) {
|
|
27
|
+
return value === 'FREE' || value === 'BASIC' || value === 'PRO' || value === 'ENTERPRISE';
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tenant/types.ts"],"names":[],"mappings":";;;AA2FA,oCAEC;AAKD,wBAEC;AAxBD;;GAEG;AACU,QAAA,cAAc,GAAG;IAC5B,SAAS,EAAE,aAAa;IACxB,WAAW,EAAE,eAAe;IAC5B,OAAO,EAAE,WAAW;IACpB,YAAY,EAAE,gBAAgB;IAC9B,iBAAiB,EAAE,qBAAqB;IACxC,cAAc,EAAE,kBAAkB;CAC1B,CAAC;AAEX;;GAEG;AACH,SAAgB,YAAY,CAAC,KAAc;IACzC,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,QAAQ,CAAC;AAC/C,CAAC;AAED;;GAEG;AACH,SAAgB,MAAM,CAAC,KAAc;IACnC,OAAO,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,YAAY,CAAC;AAC5F,CAAC"}
|
package/dist/types/routes.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RouteSegment } from './event-type.enum.js';
|
|
2
|
+
import type { TenantInfo } from '../tenant/types.js';
|
|
2
3
|
/**
|
|
3
4
|
* HTTP methods supported by the router
|
|
4
5
|
*/
|
|
@@ -130,6 +131,7 @@ export interface NormalizedEvent {
|
|
|
130
131
|
segment: RouteSegment;
|
|
131
132
|
identity?: IdentityContext;
|
|
132
133
|
requestId?: string;
|
|
134
|
+
tenantInfo?: TenantInfo;
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/types/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/types/routes.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverless-event-orchestrator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A lightweight, type-safe event dispatcher and middleware orchestrator for AWS Lambda. Designed for hexagonal architectures with support for segmented routing (public, private, backoffice), Cognito User Pool validation, and built-in infrastructure middlewares.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
"import": "./dist/identity/index.js",
|
|
20
20
|
"require": "./dist/identity/index.js",
|
|
21
21
|
"types": "./dist/identity/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./middleware": {
|
|
24
|
+
"import": "./dist/middleware/index.js",
|
|
25
|
+
"require": "./dist/middleware/index.js",
|
|
26
|
+
"types": "./dist/middleware/index.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./tenant": {
|
|
29
|
+
"import": "./dist/tenant/index.js",
|
|
30
|
+
"require": "./dist/tenant/index.js",
|
|
31
|
+
"types": "./dist/tenant/index.d.ts"
|
|
22
32
|
}
|
|
23
33
|
},
|
|
24
34
|
"scripts": {
|
package/src/dispatcher.ts
CHANGED
|
@@ -255,7 +255,22 @@ function validateSegmentUserPool(
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
*
|
|
258
|
+
* Checks if a thrown value is an HTTP response (used by middleware to halt execution)
|
|
259
|
+
*/
|
|
260
|
+
function isHttpResponse(value: unknown): value is { statusCode: number; body: string } {
|
|
261
|
+
return (
|
|
262
|
+
typeof value === 'object' &&
|
|
263
|
+
value !== null &&
|
|
264
|
+
'statusCode' in value &&
|
|
265
|
+
typeof (value as any).statusCode === 'number'
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Executes middleware chain.
|
|
271
|
+
* If a middleware throws an HttpResponse-like object (has statusCode),
|
|
272
|
+
* it is treated as an early return (e.g., 403 Forbidden from tenantGuard).
|
|
273
|
+
* If it throws a regular Error, it is re-thrown.
|
|
259
274
|
*/
|
|
260
275
|
async function executeMiddleware(
|
|
261
276
|
middleware: MiddlewareFn[],
|
|
@@ -402,14 +417,24 @@ export async function dispatchEvent(
|
|
|
402
417
|
return applyCorsToResponse(config.responses?.forbidden?.() ?? forbiddenResponse('Access denied: Invalid token issuer'));
|
|
403
418
|
}
|
|
404
419
|
|
|
405
|
-
// Execute
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
420
|
+
// Execute middlewares with error handling for HttpResponse throws
|
|
421
|
+
try {
|
|
422
|
+
// Execute global middleware
|
|
423
|
+
if (config.globalMiddleware?.length) {
|
|
424
|
+
normalized = await executeMiddleware(config.globalMiddleware, normalized);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Execute segment middleware
|
|
428
|
+
if (routeMatch.middleware?.length) {
|
|
429
|
+
normalized = await executeMiddleware(routeMatch.middleware, normalized);
|
|
430
|
+
}
|
|
431
|
+
} catch (thrown) {
|
|
432
|
+
// If middleware threw an HttpResponse (e.g., forbiddenResponse from tenantGuard), return it
|
|
433
|
+
if (isHttpResponse(thrown)) {
|
|
434
|
+
return applyCorsToResponse(thrown);
|
|
435
|
+
}
|
|
436
|
+
// Otherwise, re-throw as unhandled error
|
|
437
|
+
throw thrown;
|
|
413
438
|
}
|
|
414
439
|
|
|
415
440
|
// Execute handler and apply CORS headers to response
|
package/src/index.ts
CHANGED
|
@@ -90,3 +90,33 @@ export {
|
|
|
90
90
|
getHeader,
|
|
91
91
|
getCorsHeaders,
|
|
92
92
|
} from './utils/headers.js';
|
|
93
|
+
|
|
94
|
+
// Tenant context
|
|
95
|
+
export { TenantContext } from './tenant/TenantContext.js';
|
|
96
|
+
|
|
97
|
+
export type {
|
|
98
|
+
TenantInfo,
|
|
99
|
+
TenantType,
|
|
100
|
+
Plan,
|
|
101
|
+
TenantFeatures,
|
|
102
|
+
TenantClaims,
|
|
103
|
+
} from './tenant/types.js';
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
isTenantType,
|
|
107
|
+
isPlan,
|
|
108
|
+
TENANT_HEADERS,
|
|
109
|
+
} from './tenant/types.js';
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
tenantInfoFromClaims,
|
|
113
|
+
tenantInfoFromHeaders,
|
|
114
|
+
tenantInfoToHeaders,
|
|
115
|
+
} from './tenant/helpers.js';
|
|
116
|
+
|
|
117
|
+
// Tenant middleware
|
|
118
|
+
export {
|
|
119
|
+
initTenantContext,
|
|
120
|
+
tenantGuard,
|
|
121
|
+
crmGuard,
|
|
122
|
+
} from './middleware/index.js';
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +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
|
+
};
|