@nextage/era-lib 1.0.190 → 1.0.192

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 (64) hide show
  1. package/build/constants.d.ts +56 -6
  2. package/build/constants.d.ts.map +1 -1
  3. package/build/constants.js +57 -7
  4. package/build/constants.js.map +1 -1
  5. package/build/enums.d.ts +16 -0
  6. package/build/enums.d.ts.map +1 -1
  7. package/build/enums.js +18 -1
  8. package/build/enums.js.map +1 -1
  9. package/build/events/base/base-event.controller.d.ts +1 -1
  10. package/build/events/base/base-event.controller.js +1 -1
  11. package/build/events/base/base-msg-listener.d.ts +1 -1
  12. package/build/events/base/base-msg-listener.js +1 -1
  13. package/build/events/publisher-registry.d.ts +1 -1
  14. package/build/events/publisher-registry.js +3 -3
  15. package/build/events/revocation-denylist.js +1 -1
  16. package/build/events/transport/nats-client-manager.d.ts +1 -1
  17. package/build/events/transport/nats-client-manager.js +1 -1
  18. package/build/index.d.ts +1 -0
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js +1 -0
  21. package/build/index.js.map +1 -1
  22. package/build/interfaces.d.ts +21 -1
  23. package/build/interfaces.d.ts.map +1 -1
  24. package/build/types/base-user/index.d.ts +2 -1
  25. package/build/types/base-user/index.d.ts.map +1 -1
  26. package/build/types/nx-context.d.ts +18 -0
  27. package/build/types/nx-context.d.ts.map +1 -0
  28. package/build/types/nx-context.js +14 -0
  29. package/build/types/nx-context.js.map +1 -0
  30. package/build/utils/claims.d.ts +42 -0
  31. package/build/utils/claims.d.ts.map +1 -0
  32. package/build/utils/claims.interfaces.d.ts +42 -0
  33. package/build/utils/claims.interfaces.d.ts.map +1 -0
  34. package/build/utils/claims.interfaces.js +9 -0
  35. package/build/utils/claims.interfaces.js.map +1 -0
  36. package/build/utils/claims.js +61 -0
  37. package/build/utils/claims.js.map +1 -0
  38. package/build/utils/express.js +2 -2
  39. package/build/utils/functs.d.ts +4 -31
  40. package/build/utils/functs.d.ts.map +1 -1
  41. package/build/utils/functs.js +3 -54
  42. package/build/utils/functs.js.map +1 -1
  43. package/build/utils/index.d.ts +6 -0
  44. package/build/utils/index.d.ts.map +1 -1
  45. package/build/utils/index.js +6 -0
  46. package/build/utils/index.js.map +1 -1
  47. package/build/utils/scope.d.ts +75 -0
  48. package/build/utils/scope.d.ts.map +1 -0
  49. package/build/utils/scope.interfaces.d.ts +20 -0
  50. package/build/utils/scope.interfaces.d.ts.map +1 -0
  51. package/build/utils/scope.interfaces.js +10 -0
  52. package/build/utils/scope.interfaces.js.map +1 -0
  53. package/build/utils/scope.js +121 -0
  54. package/build/utils/scope.js.map +1 -0
  55. package/build/utils/start-service.js +1 -1
  56. package/build/utils/tenant.d.ts +98 -0
  57. package/build/utils/tenant.d.ts.map +1 -0
  58. package/build/utils/tenant.interfaces.d.ts +19 -0
  59. package/build/utils/tenant.interfaces.d.ts.map +1 -0
  60. package/build/utils/tenant.interfaces.js +9 -0
  61. package/build/utils/tenant.interfaces.js.map +1 -0
  62. package/build/utils/tenant.js +124 -0
  63. package/build/utils/tenant.js.map +1 -0
  64. package/package.json +1 -1
@@ -0,0 +1,98 @@
1
+ /*********************************************************************************************
2
+ * *
3
+ * Multi-tenancy helpers - single source of truth for how a request is scoped to a tenant. *
4
+ * *
5
+ * Every rule about "which organization is this operation allowed to touch" lives here and *
6
+ * nowhere else: the services (app-srv, auth-srv, ...) import these helpers, they never *
7
+ * re-implement the scoping. See ADR-0005 section 4 (capability matrix) and section 5 *
8
+ * (tenant resolution). *
9
+ * *
10
+ *********************************************************************************************/
11
+ import { BaseUser, NxContext, NxObject } from '@nextage/nx-frame-be';
12
+ import { ScopedReadModel } from './tenant.interfaces';
13
+ /**
14
+ * Resolves the organization a request operates on - ALWAYS exactly one.
15
+ *
16
+ * The selection NEVER comes from the payload: it travels out of band (`X-Org-Id`
17
+ * header, or `orgId` in the WebSocket connection params) and lands on
18
+ * `NxContext.orgId`. Reading it from the body would let a client name any tenant
19
+ * simply by adding a field to a mutation input (T34).
20
+ *
21
+ * Resolution order (ADR-0005 section 5.1) - the first match wins:
22
+ *
23
+ * 1. no selection -> the user's own organization, for EVERYONE
24
+ * 2. cross-tenant role -> the selection, whichever it is
25
+ * 3. selection within reach-> the selection itself (membership or grant)
26
+ * 4. otherwise -> throws (fail-closed: an unreachable selection is never
27
+ * silently downgraded to the user's own tenant)
28
+ *
29
+ * A cross-tenant role does NOT mean "sees every tenant at once": it means it may
30
+ * operate on ANY tenant, ONE at a time, upon selection. Without a selection it is
31
+ * scoped to its own organization exactly like everybody else - the provider one is
32
+ * an organization like the others and holds its own application data.
33
+ *
34
+ * It follows that the return type is a plain `string`: there is no unscoped outcome.
35
+ * A cross-tenant aggregation - a future dashboard - is a DIFFERENT query, built on
36
+ * `reachableOrgIds` in its own resolver, and never a relaxation of this helper.
37
+ *
38
+ * Built on `reachableOrgIds` on purpose: the set of organizations a selection may
39
+ * name is the same set a list may show. Two independent implementations would
40
+ * eventually disagree, and any disagreement is a leak.
41
+ *
42
+ * @param user the authenticated user (`NxContext.user`)
43
+ * @param orgId the organization named by the request, when any
44
+ * @returns the tenant id to enforce
45
+ * @throws `AppError('messages.forbidden')` when the selection is out of reach
46
+ */
47
+ export declare function resolveTenant(user?: BaseUser, orgId?: string): string;
48
+ /**
49
+ * Forces `tenantId` on a payload or on a query filter, from the resolved organization.
50
+ *
51
+ * The field is ALWAYS overwritten, never read: whatever the client sent under that
52
+ * name is irrelevant, which is what makes the helper safe to call on user input.
53
+ * There is no way out of the scoping and no flag to opt out of it - that was exactly
54
+ * defect D2 of ADR-0005 section 5.1, a fail-open by default.
55
+ *
56
+ * @param item the payload or filter, mutated in place
57
+ * @param context the request context (authenticated user + selected organization)
58
+ * @throws when no tenant can be resolved
59
+ */
60
+ export declare function setTenant(item: NxObject, context?: NxContext): void;
61
+ /**
62
+ * Returns the user's own tenant id, that is their membership organization.
63
+ *
64
+ * Membership is an invariant of exactly one entry (ADR-0005 section 3.1), so the
65
+ * first entry IS the organization; the array shape only exists to stay compatible
66
+ * with the shared application base.
67
+ *
68
+ * @param user the authenticated user
69
+ * @returns the tenant id
70
+ * @throws if the user has no organization
71
+ */
72
+ export declare function getTenant(user?: BaseUser): string;
73
+ /**
74
+ * Checks that `id` is reachable by the context user, ahead of a destructive write.
75
+ *
76
+ * Fills the gap of **T37**: `BaseModel.remove(id)` calls `beforeRemove` and then
77
+ * `findByIdAndDelete(id)`, applying neither `updateFilters` nor any `tenantId`
78
+ * constraint - and `bulkRemove(ids)` is nothing but `ids.map(remove)`. Without this
79
+ * check any user deletes documents belonging to OTHER organizations, knowing only
80
+ * their `_id`.
81
+ *
82
+ * The check reuses `model.get`, which goes through `beforeGet` and therefore already
83
+ * applies the tenant scoping: one single source of truth, no tenancy rule duplicated
84
+ * here. It follows that a cross-tenant role deletes in the organization it SELECTED,
85
+ * and in its own when it selected none - never across several at once, exactly as
86
+ * `resolveTenant` prescribes.
87
+ *
88
+ * **Fail-closed**: a missing or out-of-scope `id` raises an error, without telling
89
+ * the two cases apart (the existence of someone else's document is never confirmed).
90
+ *
91
+ * @param model the model the removal happens on (`this` inside the hook)
92
+ * @param id the document identifier
93
+ * @param context the request context, carrying the authenticated user
94
+ * @returns the same `id`, so it can be used directly inside `beforeRemove`
95
+ * @throws `AppError('messages.notFound')` when the document is out of scope
96
+ */
97
+ export declare function assertTenantScope(model: ScopedReadModel, id: string, context?: NxContext): Promise<string>;
98
+ //# sourceMappingURL=tenant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;+FAS+F;AAE/F,OAAO,EAAY,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG/E,OAAO,EAAE,eAAe,EAAE,MAA8B,qBAAqB,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAerE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAE,SAAc,GAAG,IAAI,CAGvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAMjD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMpH"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Tenancy types & interfaces.
3
+ *
4
+ * Kept separate from the tenancy logic in `tenant.ts` (house-style: types live in
5
+ * a dedicated `*.interfaces.ts`, never mixed with the logic that consumes them).
6
+ */
7
+ import { NxContext } from '@nextage/nx-frame-be';
8
+ /**
9
+ * A model of which only the read-by-`_id` is needed, already tenant-scoped by its
10
+ * own `beforeGet`.
11
+ *
12
+ * Deliberately **structural** and not `BaseModel<Attrs, Doc, Model>`: the library
13
+ * must not depend on the concrete model types of a single service, and any model
14
+ * able to read by `_id` satisfies the contract.
15
+ */
16
+ export interface ScopedReadModel {
17
+ get(id: string, context: NxContext): Promise<unknown>;
18
+ }
19
+ //# sourceMappingURL=tenant.interfaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.interfaces.d.ts","sourceRoot":"","sources":["../../src/utils/tenant.interfaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Tenancy types & interfaces.
4
+ *
5
+ * Kept separate from the tenancy logic in `tenant.ts` (house-style: types live in
6
+ * a dedicated `*.interfaces.ts`, never mixed with the logic that consumes them).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ //# sourceMappingURL=tenant.interfaces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.interfaces.js","sourceRoot":"","sources":["../../src/utils/tenant.interfaces.ts"],"names":[],"mappings":";AAAA;;;;;GAKG"}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /*********************************************************************************************
3
+ * *
4
+ * Multi-tenancy helpers - single source of truth for how a request is scoped to a tenant. *
5
+ * *
6
+ * Every rule about "which organization is this operation allowed to touch" lives here and *
7
+ * nowhere else: the services (app-srv, auth-srv, ...) import these helpers, they never *
8
+ * re-implement the scoping. See ADR-0005 section 4 (capability matrix) and section 5 *
9
+ * (tenant resolution). *
10
+ * *
11
+ *********************************************************************************************/
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.resolveTenant = resolveTenant;
14
+ exports.setTenant = setTenant;
15
+ exports.getTenant = getTenant;
16
+ exports.assertTenantScope = assertTenantScope;
17
+ const nx_frame_be_1 = require("@nextage/nx-frame-be");
18
+ const scope_1 = require("./scope");
19
+ /**
20
+ * Resolves the organization a request operates on - ALWAYS exactly one.
21
+ *
22
+ * The selection NEVER comes from the payload: it travels out of band (`X-Org-Id`
23
+ * header, or `orgId` in the WebSocket connection params) and lands on
24
+ * `NxContext.orgId`. Reading it from the body would let a client name any tenant
25
+ * simply by adding a field to a mutation input (T34).
26
+ *
27
+ * Resolution order (ADR-0005 section 5.1) - the first match wins:
28
+ *
29
+ * 1. no selection -> the user's own organization, for EVERYONE
30
+ * 2. cross-tenant role -> the selection, whichever it is
31
+ * 3. selection within reach-> the selection itself (membership or grant)
32
+ * 4. otherwise -> throws (fail-closed: an unreachable selection is never
33
+ * silently downgraded to the user's own tenant)
34
+ *
35
+ * A cross-tenant role does NOT mean "sees every tenant at once": it means it may
36
+ * operate on ANY tenant, ONE at a time, upon selection. Without a selection it is
37
+ * scoped to its own organization exactly like everybody else - the provider one is
38
+ * an organization like the others and holds its own application data.
39
+ *
40
+ * It follows that the return type is a plain `string`: there is no unscoped outcome.
41
+ * A cross-tenant aggregation - a future dashboard - is a DIFFERENT query, built on
42
+ * `reachableOrgIds` in its own resolver, and never a relaxation of this helper.
43
+ *
44
+ * Built on `reachableOrgIds` on purpose: the set of organizations a selection may
45
+ * name is the same set a list may show. Two independent implementations would
46
+ * eventually disagree, and any disagreement is a leak.
47
+ *
48
+ * @param user the authenticated user (`NxContext.user`)
49
+ * @param orgId the organization named by the request, when any
50
+ * @returns the tenant id to enforce
51
+ * @throws `AppError('messages.forbidden')` when the selection is out of reach
52
+ */
53
+ function resolveTenant(user, orgId) {
54
+ if (!orgId)
55
+ return getTenant(user);
56
+ const reachable = (0, scope_1.reachableOrgIds)(user);
57
+ // `undefined` means no constraint on WHICH organization may be named.
58
+ if (!reachable)
59
+ return orgId;
60
+ if (!reachable.includes(orgId))
61
+ throw new nx_frame_be_1.AppError('messages.forbidden');
62
+ return orgId;
63
+ }
64
+ /**
65
+ * Forces `tenantId` on a payload or on a query filter, from the resolved organization.
66
+ *
67
+ * The field is ALWAYS overwritten, never read: whatever the client sent under that
68
+ * name is irrelevant, which is what makes the helper safe to call on user input.
69
+ * There is no way out of the scoping and no flag to opt out of it - that was exactly
70
+ * defect D2 of ADR-0005 section 5.1, a fail-open by default.
71
+ *
72
+ * @param item the payload or filter, mutated in place
73
+ * @param context the request context (authenticated user + selected organization)
74
+ * @throws when no tenant can be resolved
75
+ */
76
+ function setTenant(item, context = {}) {
77
+ item.tenantId = resolveTenant(context.user, context.orgId);
78
+ }
79
+ /**
80
+ * Returns the user's own tenant id, that is their membership organization.
81
+ *
82
+ * Membership is an invariant of exactly one entry (ADR-0005 section 3.1), so the
83
+ * first entry IS the organization; the array shape only exists to stay compatible
84
+ * with the shared application base.
85
+ *
86
+ * @param user the authenticated user
87
+ * @returns the tenant id
88
+ * @throws if the user has no organization
89
+ */
90
+ function getTenant(user) {
91
+ if (!user?.organizations?.length)
92
+ throw new Error('Cannot proceed: organization not set');
93
+ return String(user.organizations[0].id);
94
+ }
95
+ /**
96
+ * Checks that `id` is reachable by the context user, ahead of a destructive write.
97
+ *
98
+ * Fills the gap of **T37**: `BaseModel.remove(id)` calls `beforeRemove` and then
99
+ * `findByIdAndDelete(id)`, applying neither `updateFilters` nor any `tenantId`
100
+ * constraint - and `bulkRemove(ids)` is nothing but `ids.map(remove)`. Without this
101
+ * check any user deletes documents belonging to OTHER organizations, knowing only
102
+ * their `_id`.
103
+ *
104
+ * The check reuses `model.get`, which goes through `beforeGet` and therefore already
105
+ * applies the tenant scoping: one single source of truth, no tenancy rule duplicated
106
+ * here. It follows that a cross-tenant role deletes in the organization it SELECTED,
107
+ * and in its own when it selected none - never across several at once, exactly as
108
+ * `resolveTenant` prescribes.
109
+ *
110
+ * **Fail-closed**: a missing or out-of-scope `id` raises an error, without telling
111
+ * the two cases apart (the existence of someone else's document is never confirmed).
112
+ *
113
+ * @param model the model the removal happens on (`this` inside the hook)
114
+ * @param id the document identifier
115
+ * @param context the request context, carrying the authenticated user
116
+ * @returns the same `id`, so it can be used directly inside `beforeRemove`
117
+ * @throws `AppError('messages.notFound')` when the document is out of scope
118
+ */
119
+ async function assertTenantScope(model, id, context = {}) {
120
+ if (!await model.get(id, context))
121
+ throw new nx_frame_be_1.AppError('messages.notFound');
122
+ return id;
123
+ }
124
+ //# sourceMappingURL=tenant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.js","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":";AAAA;;;;;;;;;+FAS+F;;AAyC/F,sCAeC;AAcD,8BAGC;AAaD,8BAMC;AA0BD,8CAMC;AA1HD,sDAA+E;AAE/E,mCAAkE;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,SAAgB,aAAa,CAAC,IAAe,EAAE,KAAc;IAE3D,IAAI,CAAC,KAAK;QACR,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;IAEzB,MAAM,SAAS,GAAG,IAAA,uBAAe,EAAC,IAAI,CAAC,CAAC;IAExC,sEAAsE;IACtE,IAAI,CAAC,SAAS;QACZ,OAAO,KAAK,CAAC;IAEf,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC5B,MAAM,IAAI,sBAAQ,CAAC,oBAAoB,CAAC,CAAC;IAE3C,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,SAAS,CAAC,IAAc,EAAE,UAAqB,EAAE;IAE/D,IAAI,CAAC,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,SAAS,CAAC,IAAe;IAEvC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM;QAC9B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAE1D,OAAO,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACI,KAAK,UAAU,iBAAiB,CAAC,KAAsB,EAAE,EAAU,EAAE,UAAqB,EAAE;IAEjG,IAAI,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC;QAC/B,MAAM,IAAI,sBAAQ,CAAC,mBAAmB,CAAC,CAAC;IAE1C,OAAO,EAAE,CAAC;AACZ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextage/era-lib",
3
- "version": "1.0.190",
3
+ "version": "1.0.192",
4
4
  "main": "./build/index.js",
5
5
  "types": "./build/index.d.ts",
6
6
  "files": [