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.
Files changed (58) hide show
  1. package/dist/dispatcher.d.ts.map +1 -1
  2. package/dist/dispatcher.js +30 -7
  3. package/dist/dispatcher.js.map +1 -1
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +17 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/middleware/crm-guard.d.ts +25 -0
  9. package/dist/middleware/crm-guard.d.ts.map +1 -0
  10. package/dist/middleware/crm-guard.js +45 -0
  11. package/dist/middleware/crm-guard.js.map +1 -0
  12. package/dist/middleware/index.d.ts +4 -0
  13. package/dist/middleware/index.d.ts.map +1 -0
  14. package/dist/middleware/index.js +10 -0
  15. package/dist/middleware/index.js.map +1 -0
  16. package/dist/middleware/init-tenant-context.d.ts +21 -0
  17. package/dist/middleware/init-tenant-context.d.ts.map +1 -0
  18. package/dist/middleware/init-tenant-context.js +50 -0
  19. package/dist/middleware/init-tenant-context.js.map +1 -0
  20. package/dist/middleware/tenant-guard.d.ts +26 -0
  21. package/dist/middleware/tenant-guard.d.ts.map +1 -0
  22. package/dist/middleware/tenant-guard.js +48 -0
  23. package/dist/middleware/tenant-guard.js.map +1 -0
  24. package/dist/tenant/TenantContext.d.ts +88 -0
  25. package/dist/tenant/TenantContext.d.ts.map +1 -0
  26. package/dist/tenant/TenantContext.js +110 -0
  27. package/dist/tenant/TenantContext.js.map +1 -0
  28. package/dist/tenant/helpers.d.ts +26 -0
  29. package/dist/tenant/helpers.d.ts.map +1 -0
  30. package/dist/tenant/helpers.js +86 -0
  31. package/dist/tenant/helpers.js.map +1 -0
  32. package/dist/tenant/index.d.ts +5 -0
  33. package/dist/tenant/index.d.ts.map +1 -0
  34. package/dist/tenant/index.js +14 -0
  35. package/dist/tenant/index.js.map +1 -0
  36. package/dist/tenant/types.d.ts +84 -0
  37. package/dist/tenant/types.d.ts.map +1 -0
  38. package/dist/tenant/types.js +29 -0
  39. package/dist/tenant/types.js.map +1 -0
  40. package/dist/types/routes.d.ts +2 -0
  41. package/dist/types/routes.d.ts.map +1 -1
  42. package/package.json +11 -1
  43. package/src/dispatcher.ts +34 -9
  44. package/src/index.ts +30 -0
  45. package/src/middleware/crm-guard.ts +51 -0
  46. package/src/middleware/index.ts +3 -0
  47. package/src/middleware/init-tenant-context.ts +59 -0
  48. package/src/middleware/tenant-guard.ts +54 -0
  49. package/src/tenant/TenantContext.ts +115 -0
  50. package/src/tenant/helpers.ts +95 -0
  51. package/src/tenant/index.ts +21 -0
  52. package/src/tenant/types.ts +101 -0
  53. package/src/types/routes.ts +2 -0
  54. package/tests/middleware/crm-guard.test.ts +69 -0
  55. package/tests/middleware/init-tenant-context.test.ts +147 -0
  56. package/tests/middleware/tenant-guard.test.ts +100 -0
  57. package/tests/tenant/TenantContext.test.ts +134 -0
  58. 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"}
@@ -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":"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"}
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.2.7",
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
- * Executes middleware chain
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 global middleware
406
- if (config.globalMiddleware?.length) {
407
- normalized = await executeMiddleware(config.globalMiddleware, normalized);
408
- }
409
-
410
- // Execute segment middleware
411
- if (routeMatch.middleware?.length) {
412
- normalized = await executeMiddleware(routeMatch.middleware, normalized);
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,3 @@
1
+ export { initTenantContext } from './init-tenant-context.js';
2
+ export { tenantGuard } from './tenant-guard.js';
3
+ export { crmGuard } from './crm-guard.js';
@@ -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
+ };