@astram/identity 0.1.2 → 0.2.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 (139) hide show
  1. package/dist/auth-facts-keys.d.ts +3 -0
  2. package/dist/auth-facts-keys.d.ts.map +1 -0
  3. package/dist/auth-facts-keys.js +24 -0
  4. package/dist/cross-tenant-grant.object.d.ts +28 -0
  5. package/dist/cross-tenant-grant.object.d.ts.map +1 -0
  6. package/dist/cross-tenant-grant.object.js +64 -0
  7. package/dist/features/access/access-admin.access.d.ts +3 -0
  8. package/dist/features/access/access-admin.access.d.ts.map +1 -0
  9. package/dist/features/access/access-admin.access.js +27 -0
  10. package/dist/features/access/company-admin.access.d.ts +3 -0
  11. package/dist/features/access/company-admin.access.d.ts.map +1 -0
  12. package/dist/features/access/company-admin.access.js +31 -0
  13. package/dist/features/access/index.d.ts +15 -0
  14. package/dist/features/access/index.d.ts.map +1 -0
  15. package/dist/features/access/index.js +26 -0
  16. package/dist/features/access/user-management.access.d.ts +3 -0
  17. package/dist/features/access/user-management.access.d.ts.map +1 -0
  18. package/dist/features/access/user-management.access.js +35 -0
  19. package/dist/features/audit/audit-read.service.d.ts +92 -0
  20. package/dist/features/audit/audit-read.service.d.ts.map +1 -0
  21. package/dist/features/audit/audit-read.service.js +97 -0
  22. package/dist/features/audit/audit.controller.d.ts +35 -0
  23. package/dist/features/audit/audit.controller.d.ts.map +1 -0
  24. package/dist/features/audit/audit.controller.js +73 -0
  25. package/dist/features/audit/directory-label-resolver.d.ts +14 -0
  26. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -0
  27. package/dist/features/audit/directory-label-resolver.js +46 -0
  28. package/dist/features/audit/mariadb-audit-reader.d.ts +46 -0
  29. package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -0
  30. package/dist/features/audit/mariadb-audit-reader.js +116 -0
  31. package/dist/features/authz/access-bundle-reconcile.job.d.ts +35 -0
  32. package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -0
  33. package/dist/features/authz/access-bundle-reconcile.job.js +84 -0
  34. package/dist/features/authz/access-bundle-store.d.ts +65 -0
  35. package/dist/features/authz/access-bundle-store.d.ts.map +1 -0
  36. package/dist/features/authz/access-bundle-store.js +154 -0
  37. package/dist/features/authz/catalog-bootstrap.provider.d.ts +28 -0
  38. package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -0
  39. package/dist/features/authz/catalog-bootstrap.provider.js +85 -0
  40. package/dist/features/authz/catalog-reconcile.job.d.ts +23 -0
  41. package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -0
  42. package/dist/features/authz/catalog-reconcile.job.js +47 -0
  43. package/dist/features/authz/db-permission-facts.adapter.d.ts +60 -0
  44. package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -0
  45. package/dist/features/authz/db-permission-facts.adapter.js +116 -0
  46. package/dist/features/authz/grant-admin.controller.d.ts +96 -0
  47. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -0
  48. package/dist/features/authz/grant-admin.controller.js +209 -0
  49. package/dist/features/authz/grant-admin.service.d.ts +107 -0
  50. package/dist/features/authz/grant-admin.service.d.ts.map +1 -0
  51. package/dist/features/authz/grant-admin.service.js +297 -0
  52. package/dist/features/authz/mariadb-catalog-store.d.ts +16 -0
  53. package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -0
  54. package/dist/features/authz/mariadb-catalog-store.js +44 -0
  55. package/dist/features/authz/permission-version.bumper.d.ts +47 -0
  56. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -0
  57. package/dist/features/authz/permission-version.bumper.js +68 -0
  58. package/dist/features/authz/record-authorization.d.ts +31 -0
  59. package/dist/features/authz/record-authorization.d.ts.map +1 -0
  60. package/dist/features/authz/record-authorization.js +59 -0
  61. package/dist/features/authz/session-facts.reader.d.ts +41 -0
  62. package/dist/features/authz/session-facts.reader.d.ts.map +1 -0
  63. package/dist/features/authz/session-facts.reader.js +27 -0
  64. package/dist/features/company/company.controller.d.ts +37 -0
  65. package/dist/features/company/company.controller.d.ts.map +1 -0
  66. package/dist/features/company/company.controller.js +100 -0
  67. package/dist/features/company/company.service.d.ts +25 -0
  68. package/dist/features/company/company.service.d.ts.map +1 -0
  69. package/dist/features/company/company.service.js +53 -0
  70. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts +126 -0
  71. package/dist/features/cross-tenant/cross-tenant-grant-read.service.d.ts.map +1 -0
  72. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js +276 -0
  73. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts +65 -0
  74. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -0
  75. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +250 -0
  76. package/dist/features/cross-tenant/grant-authorize.d.ts +42 -0
  77. package/dist/features/cross-tenant/grant-authorize.d.ts.map +1 -0
  78. package/dist/features/cross-tenant/grant-authorize.js +17 -0
  79. package/dist/features/identity-feature.module.d.ts +26 -0
  80. package/dist/features/identity-feature.module.d.ts.map +1 -0
  81. package/dist/features/identity-feature.module.js +247 -0
  82. package/dist/features/membership/membership.controller.d.ts +77 -0
  83. package/dist/features/membership/membership.controller.d.ts.map +1 -0
  84. package/dist/features/membership/membership.controller.js +174 -0
  85. package/dist/features/membership/membership.service.d.ts +73 -0
  86. package/dist/features/membership/membership.service.d.ts.map +1 -0
  87. package/dist/features/membership/membership.service.js +173 -0
  88. package/dist/features/membership/posting-version-bump.d.ts +10 -0
  89. package/dist/features/membership/posting-version-bump.d.ts.map +1 -0
  90. package/dist/features/membership/posting-version-bump.js +4 -0
  91. package/dist/features/membership/posting.service.d.ts +59 -0
  92. package/dist/features/membership/posting.service.d.ts.map +1 -0
  93. package/dist/features/membership/posting.service.js +181 -0
  94. package/dist/features/org-structure/org-catalog.controller.d.ts +14 -0
  95. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -0
  96. package/dist/features/org-structure/org-catalog.controller.js +62 -0
  97. package/dist/features/org-structure/org-catalog.service.d.ts +19 -0
  98. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -0
  99. package/dist/features/org-structure/org-catalog.service.js +53 -0
  100. package/dist/features/role/role.controller.d.ts +37 -0
  101. package/dist/features/role/role.controller.d.ts.map +1 -0
  102. package/dist/features/role/role.controller.js +104 -0
  103. package/dist/features/role/role.service.d.ts +24 -0
  104. package/dist/features/role/role.service.d.ts.map +1 -0
  105. package/dist/features/role/role.service.js +53 -0
  106. package/dist/features/shared/http.d.ts +15 -0
  107. package/dist/features/shared/http.d.ts.map +1 -0
  108. package/dist/features/shared/http.js +25 -0
  109. package/dist/features/shared/list-query.d.ts +44 -0
  110. package/dist/features/shared/list-query.d.ts.map +1 -0
  111. package/dist/features/shared/list-query.js +53 -0
  112. package/dist/features/shared/named-option.d.ts +6 -0
  113. package/dist/features/shared/named-option.d.ts.map +1 -0
  114. package/dist/features/shared/named-option.js +2 -0
  115. package/dist/features/shared/record-resolver.d.ts +11 -0
  116. package/dist/features/shared/record-resolver.d.ts.map +1 -0
  117. package/dist/features/shared/record-resolver.js +32 -0
  118. package/dist/features/user/user.controller.d.ts +39 -0
  119. package/dist/features/user/user.controller.d.ts.map +1 -0
  120. package/dist/features/user/user.controller.js +104 -0
  121. package/dist/features/user/user.service.d.ts +26 -0
  122. package/dist/features/user/user.service.d.ts.map +1 -0
  123. package/dist/features/user/user.service.js +53 -0
  124. package/dist/index.d.ts +34 -3
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +66 -33
  127. package/dist/objects.d.ts +13 -0
  128. package/dist/objects.d.ts.map +1 -0
  129. package/dist/objects.js +20 -0
  130. package/dist/shared-resource.object.d.ts +28 -0
  131. package/dist/shared-resource.object.d.ts.map +1 -0
  132. package/dist/shared-resource.object.js +63 -0
  133. package/migrations/1710000000010-CreateAuditLog.cjs +42 -0
  134. package/migrations/1710000000011-CreateCrossTenantGrant.cjs +66 -0
  135. package/migrations/1710000000012-SeedCrossTenantGrantPerms.cjs +86 -0
  136. package/migrations/1710000000013-CreateSharedResource.cjs +30 -0
  137. package/migrations/1710000000099-SeedDevIdentity.cjs +130 -0
  138. package/migrations/1710000000100-SeedDevTenantBAndRoot.cjs +212 -0
  139. package/package.json +4 -3
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CrossTenantGrantReadService = void 0;
13
+ // packages/identity/src/features/cross-tenant/cross-tenant-grant-read.service.ts
14
+ /**
15
+ * CrossTenantGrantReadService — the read side of the cross-tenant grant UI,
16
+ * backing the inbox / mine / incoming / offerable-types surfaces the controller
17
+ * exposes. Built over the shared `authzPool` (a `useValue` provider — the mariadb
18
+ * Pool is NOT a Nest provider in this app, matching the non-DI adapter pattern the
19
+ * IdentityFeatureModule already uses for its audit/grant-admin stacks), never
20
+ * through the RecordEngine pipe (`cross_tenant_grant` is a framework-owned table,
21
+ * not a registered object type resolved via the engine).
22
+ *
23
+ * ⛔ TENANT SCOPING (the list-leak class of bug): `listInbox`/`listMine`/
24
+ * `listIncoming` all take `tenantId` from the CALLER (the controller passes
25
+ * `req.principal.tenantId` — never a query/body value) and filter ON it in the SQL
26
+ * WHERE clause. A tenant-A caller can never see tenant-B's grants through these reads,
27
+ * regardless of what a client might try to pass.
28
+ *
29
+ * ⛔ NO RAW UUIDS: every row is resolved via LEFT JOINs to `companies`/`users`/`roles`
30
+ * (COALESCE to a `'Deleted …'` fallback for an orphaned reference) — the uuid columns
31
+ * ride in the row ONLY as API keys (`uuid`, `granteeUuid` are never rendered directly;
32
+ * the UI shows `*Name`/`*Label` fields). `ctg_allowed_actions` is parsed defensively:
33
+ * the mariadb driver auto-parses the JSON column to a JS array in production, but a
34
+ * string is accepted too (the writer/adapter lesson — never a bare `JSON.parse` that
35
+ * assumes one shape).
36
+ */
37
+ const common_1 = require("@nestjs/common");
38
+ const engine_1 = require("@astram/engine");
39
+ const AUTHZ = 'shield_authz';
40
+ const APP = 'shield_app';
41
+ /**
42
+ * Parse `ctg_allowed_actions` to a `string[]`, accepting BOTH an already-parsed array
43
+ * (production — the mariadb driver auto-parses the JSON column) and a JSON string
44
+ * (test doubles / a raw driver mode). Anything else/malformed -> `[]` (fail-closed,
45
+ * never throws on a malformed cell). Mirrors the writer's `parseCeiling` in
46
+ * `@astram/guard`'s cross-tenant-grant-writer (the ceiling-parse lesson).
47
+ */
48
+ function parseAllowedActions(raw) {
49
+ if (Array.isArray(raw)) {
50
+ return raw.filter((a) => typeof a === 'string');
51
+ }
52
+ if (typeof raw === 'string' && raw.length > 0) {
53
+ try {
54
+ const parsed = JSON.parse(raw);
55
+ if (Array.isArray(parsed))
56
+ return parsed.filter((a) => typeof a === 'string');
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
62
+ return [];
63
+ }
64
+ /** The grantee's resolved display name: the matching user/role LEFT JOIN, else a fallback. */
65
+ function granteeName(row) {
66
+ const name = row.ctg_grantee_kind === 'role' ? row.grantee_role_name : row.grantee_user_name;
67
+ return name ?? 'Deleted grantee';
68
+ }
69
+ function granteeKind(row) {
70
+ return row.ctg_grantee_kind === 'role' ? 'role' : 'user';
71
+ }
72
+ /**
73
+ * Epoch-ms comes back from the BIGINT column as a JS `bigint` (the mariadb driver's default
74
+ * for BIGINT), or occasionally a `string`/`number`. Normalize ALL three to a plain `number` —
75
+ * crucially a `bigint` must be coerced, or it reaches `JSON.stringify` (in the HTTP response
76
+ * serializer) and throws `TypeError: Do not know how to serialize a BigInt` (a 500). `Number()`
77
+ * handles bigint | string | number uniformly; epoch-ms is well within `Number.MAX_SAFE_INTEGER`.
78
+ */
79
+ function expiresAtMs(raw) {
80
+ return Number(raw);
81
+ }
82
+ /** The columns + JOINs shared by every list read (only the WHERE differs per method). */
83
+ const SELECT_COLUMNS = `
84
+ ctg.ctg_uuid,
85
+ ctg.ctg_owning_tenant_uuid,
86
+ owner_co.c_name AS owning_tenant_name,
87
+ ctg.ctg_grantee_tenant_uuid,
88
+ grantee_co.c_name AS grantee_tenant_name,
89
+ ctg.ctg_grantee_kind,
90
+ ctg.ctg_grantee_uuid,
91
+ gu.u_username AS grantee_user_name,
92
+ gr.r_name AS grantee_role_name,
93
+ ctg.ctg_type_key,
94
+ ctg.ctg_scope,
95
+ ctg.ctg_allowed_actions,
96
+ ctg.ctg_status,
97
+ ctg.ctg_offered_by,
98
+ offerer.u_username AS offered_by_name,
99
+ ctg.ctg_expires_at_ms,
100
+ ctg.ctg_created_at
101
+ `;
102
+ const JOINS = `
103
+ FROM \`${AUTHZ}\`.\`cross_tenant_grant\` ctg
104
+ LEFT JOIN \`${APP}\`.\`companies\` owner_co ON owner_co.c_uuid = ctg.ctg_owning_tenant_uuid
105
+ LEFT JOIN \`${APP}\`.\`companies\` grantee_co ON grantee_co.c_uuid = ctg.ctg_grantee_tenant_uuid
106
+ LEFT JOIN \`${APP}\`.\`users\` gu ON ctg.ctg_grantee_kind = 'user' AND gu.u_uuid = ctg.ctg_grantee_uuid
107
+ LEFT JOIN \`${APP}\`.\`roles\` gr ON ctg.ctg_grantee_kind = 'role' AND gr.r_uuid = ctg.ctg_grantee_uuid
108
+ LEFT JOIN \`${APP}\`.\`users\` offerer ON offerer.u_uuid = ctg.ctg_offered_by
109
+ `;
110
+ let CrossTenantGrantReadService = class CrossTenantGrantReadService {
111
+ pool;
112
+ offerableTypeInputs;
113
+ actionsByType;
114
+ /**
115
+ * @param pool the shared resolver pool.
116
+ * @param offerableTypeInputs the ENGINE registrations to filter to `access.tenantScoped
117
+ * === true` types for the offer form's picker (decision 1). Passed in (not imported
118
+ * directly) so this service stays a plain constructor-arg class like its siblings —
119
+ * the module supplies the same merged `[...identityObjects]` set the share-type
120
+ * surface uses, so a future tenantScoped type is picked up the same way it becomes
121
+ * shareable.
122
+ * @param actionsByType per-typeKey action vocabulary for the offerable-types read
123
+ * (decision 1: "the actions per type come from the type's own action set"). Callers
124
+ * pass `actionsFor`, which defaults every type to the CRUD `IDENTITY_ACTIONS` except
125
+ * a type with its own override (e.g. `cross_tenant_grant` -> share/accept/revoke) —
126
+ * the single source every action-vocabulary consumer uses, so a per-type set can
127
+ * never drift between callers.
128
+ */
129
+ constructor(pool, offerableTypeInputs, actionsByType) {
130
+ this.pool = pool;
131
+ this.offerableTypeInputs = offerableTypeInputs;
132
+ this.actionsByType = actionsByType;
133
+ }
134
+ /**
135
+ * Pending offers sitting in the CALLER's tenant inbox: `ctg_grantee_tenant_uuid =
136
+ * tenantId AND ctg_status = 'offered' AND ctg_expires_at_ms > nowMs` (an expired
137
+ * offer is dead — never surfaced as actionable, mirroring the writer's own active-
138
+ * grant filter). Ordered soonest-expiring first (most urgent to act on).
139
+ */
140
+ async listInbox(tenantId, nowMs) {
141
+ if (!tenantId)
142
+ return [];
143
+ const rows = (await this.pool.query(`SELECT ${SELECT_COLUMNS}
144
+ ${JOINS}
145
+ WHERE ctg.ctg_grantee_tenant_uuid = ?
146
+ AND ctg.ctg_status = 'offered'
147
+ AND ctg.ctg_expires_at_ms > ?
148
+ ORDER BY ctg.ctg_expires_at_ms ASC`, [tenantId, nowMs]));
149
+ return rows.map((row) => ({
150
+ uuid: row.ctg_uuid,
151
+ owningTenantName: row.owning_tenant_name ?? 'Deleted tenant',
152
+ granteeKind: granteeKind(row),
153
+ granteeName: granteeName(row),
154
+ typeKey: row.ctg_type_key,
155
+ typeLabel: row.ctg_type_key,
156
+ scope: row.ctg_scope,
157
+ allowedActions: parseAllowedActions(row.ctg_allowed_actions),
158
+ offeredByName: row.offered_by_name ?? 'Deleted user',
159
+ expiresAtMs: expiresAtMs(row.ctg_expires_at_ms),
160
+ }));
161
+ }
162
+ /**
163
+ * Every grant MY tenant OWNS (`ctg_owning_tenant_uuid = tenantId`), ANY status — the
164
+ * "outgoing" list (what I've offered, whether still pending, accepted, or revoked).
165
+ * Ordered most-recently-offered first (`ctg_created_at DESC`) — the natural order
166
+ * for "what have I done lately" on an owner's own outgoing list.
167
+ */
168
+ async listMine(tenantId) {
169
+ if (!tenantId)
170
+ return [];
171
+ const rows = (await this.pool.query(`SELECT ${SELECT_COLUMNS}
172
+ ${JOINS}
173
+ WHERE ctg.ctg_owning_tenant_uuid = ?
174
+ ORDER BY ctg.ctg_created_at DESC`, [tenantId]));
175
+ return rows.map((row) => ({
176
+ uuid: row.ctg_uuid,
177
+ granteeTenantName: row.grantee_tenant_name ?? 'Deleted tenant',
178
+ granteeKind: granteeKind(row),
179
+ granteeName: granteeName(row),
180
+ typeKey: row.ctg_type_key,
181
+ typeLabel: row.ctg_type_key,
182
+ scope: row.ctg_scope,
183
+ allowedActions: parseAllowedActions(row.ctg_allowed_actions),
184
+ status: row.ctg_status,
185
+ offeredByName: row.offered_by_name ?? 'Deleted user',
186
+ expiresAtMs: expiresAtMs(row.ctg_expires_at_ms),
187
+ }));
188
+ }
189
+ /**
190
+ * Grants ACCEPTED into my tenant (`ctg_grantee_tenant_uuid = tenantId AND ctg_status
191
+ * = 'accepted'`) — the "incoming" (currently-active reach-in) list, distinct from the
192
+ * pending inbox. Includes grants whose expiry has since passed (this reports status as
193
+ * stored — `CrossTenantGrantPort.findActiveGrant` is the ACTUAL enforcement of
194
+ * liveness; this is a read-model list, not a re-implementation of that check), so the
195
+ * UI can show "expired" via `expiresAtMs` if desired. Ordered soonest-expiring first.
196
+ */
197
+ async listIncoming(tenantId) {
198
+ if (!tenantId)
199
+ return [];
200
+ const rows = (await this.pool.query(`SELECT ${SELECT_COLUMNS}
201
+ ${JOINS}
202
+ WHERE ctg.ctg_grantee_tenant_uuid = ?
203
+ AND ctg.ctg_status = 'accepted'
204
+ ORDER BY ctg.ctg_expires_at_ms ASC`, [tenantId]));
205
+ return rows.map((row) => ({
206
+ uuid: row.ctg_uuid,
207
+ owningTenantName: row.owning_tenant_name ?? 'Deleted tenant',
208
+ granteeKind: granteeKind(row),
209
+ granteeName: granteeName(row),
210
+ typeKey: row.ctg_type_key,
211
+ typeLabel: row.ctg_type_key,
212
+ scope: row.ctg_scope,
213
+ allowedActions: parseAllowedActions(row.ctg_allowed_actions),
214
+ offeredByName: row.offered_by_name ?? 'Deleted user',
215
+ expiresAtMs: expiresAtMs(row.ctg_expires_at_ms),
216
+ }));
217
+ }
218
+ /**
219
+ * The ONE source of truth for "which typeKeys are offerable across tenants" (decision
220
+ * 1: `access.tenantScoped === true`). Reflects the SAME engine registrations the
221
+ * share surface merges (`[...identityObjects]`, passed in by the module) and returns
222
+ * the set of tenantScoped typeKeys. Both the picker read (`listOfferableTypes`) AND the
223
+ * offer-endpoint guard (`isOfferableType`) derive from this — so there is no way for
224
+ * the two to drift (a system type can never be BOTH shown in the picker AND accepted
225
+ * by the offer endpoint, or neither).
226
+ */
227
+ offerableTypeKeys() {
228
+ const keys = new Set();
229
+ for (const input of this.offerableTypeInputs) {
230
+ const reg = typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
231
+ if (reg.access?.tenantScoped === true)
232
+ keys.add(reg.typeKey); // decision 1: tenantScoped ONLY
233
+ }
234
+ return keys;
235
+ }
236
+ /**
237
+ * Is `typeKey` offerable across tenants (i.e. a registered tenantScoped type)? The
238
+ * offer controller calls this BEFORE `writer.offer()` so an un-offerable typeKey (any
239
+ * `system` type — e.g. `company`) is rejected with a 400 before any row is written.
240
+ *
241
+ * This closes the H3 defense-in-depth gap: the writer's `assertValidOffer` only checks
242
+ * `typeKey` is a non-empty string, so without this guard a legit own-tenant admin could
243
+ * mint a junk `offered` grant row for a `system` type. It's inert at evaluation TODAY
244
+ * only because no type is both `system` AND `tenantScoped` (system types have no
245
+ * `trusted:'tenantId'`, so the tenant gate never consults a cross-tenant grant for
246
+ * them) — but that safety is a coincidence, not an explicit rule. This makes it
247
+ * explicit, using the SAME tenantScoped filter as the picker (`offerableTypeKeys`).
248
+ */
249
+ isOfferableType(typeKey) {
250
+ return this.offerableTypeKeys().has(typeKey);
251
+ }
252
+ /**
253
+ * The object-type picker source for the offer form (decision 1): the tenantScoped
254
+ * registered types (from `offerableTypeKeys` — the same source `isOfferableType`
255
+ * uses) paired with each type's action vocabulary. No DB query — pure in-memory
256
+ * reflection, wrapped in a resolved promise so the controller's read-surface shape is
257
+ * uniform. The identity types are all `system` and are excluded by construction —
258
+ * only a tenantScoped type (e.g. `shared_resource`) qualifies.
259
+ */
260
+ async listOfferableTypes() {
261
+ const out = [];
262
+ for (const typeKey of this.offerableTypeKeys()) {
263
+ out.push({
264
+ typeKey,
265
+ label: typeKey, // no separate label registry today — mirrors the module-registry `label: r.typeKey`
266
+ actions: [...this.actionsByType(typeKey)],
267
+ });
268
+ }
269
+ return out;
270
+ }
271
+ };
272
+ exports.CrossTenantGrantReadService = CrossTenantGrantReadService;
273
+ exports.CrossTenantGrantReadService = CrossTenantGrantReadService = __decorate([
274
+ (0, common_1.Injectable)(),
275
+ __metadata("design:paramtypes", [Object, Array, Function])
276
+ ], CrossTenantGrantReadService);
@@ -0,0 +1,65 @@
1
+ import { CrossTenantGrantWriter } from '@astram/guard';
2
+ import type { Principal } from '@astram/engine';
3
+ import { CrossTenantGrantReadService } from './cross-tenant-grant-read.service.js';
4
+ /** The request shape after the enforcement guard attaches the resolved principal. */
5
+ interface AuthedRequest {
6
+ principal: Principal;
7
+ }
8
+ /** The offer request body — grantee + type + ceiling + expiry ONLY (never authority fields). */
9
+ interface OfferGrantBodyDto {
10
+ granteeTenantId?: string;
11
+ granteeKind?: string;
12
+ granteeUuid?: string;
13
+ typeKey?: string;
14
+ allowedActions?: unknown;
15
+ expiresAtMs?: unknown;
16
+ }
17
+ export declare class CrossTenantGrantController {
18
+ private readonly grantWriter;
19
+ private readonly reads;
20
+ constructor(grantWriter: CrossTenantGrantWriter, reads: CrossTenantGrantReadService);
21
+ /**
22
+ * Offer a cross-tenant grant. `owningTenantId`/`offeredBy` come ONLY from the
23
+ * authenticated principal; the body supplies the grantee + type + ceiling + expiry.
24
+ * `scope` is hard-coded 'object_type' (decision 4) — no recordId accepted in v1.
25
+ */
26
+ offer(req: AuthedRequest, body: OfferGrantBodyDto): Promise<{
27
+ success: boolean;
28
+ uuid: string;
29
+ }>;
30
+ /** Accept an offered grant. `acceptedBy` comes ONLY from the authenticated principal. */
31
+ accept(req: AuthedRequest, uuid: string): Promise<{
32
+ success: boolean;
33
+ }>;
34
+ /**
35
+ * Revoke a grant. `revokedBy` comes ONLY from the authenticated principal. Either the
36
+ * owning-tenant admin OR a grantee-tenant member may call this (the writer decides) —
37
+ * the grantee side calling this on an `offered` grant IS the "Decline" action; no
38
+ * separate endpoint or status exists for decline.
39
+ */
40
+ revoke(req: AuthedRequest, uuid: string): Promise<{
41
+ success: boolean;
42
+ }>;
43
+ /**
44
+ * The caller's tenant's pending-offers inbox. Scoped to `req.principal.tenantId` —
45
+ * NEVER a query param — so a caller only ever sees offers made TO their own tenant.
46
+ */
47
+ inbox(req: AuthedRequest): Promise<{
48
+ rows: import("./cross-tenant-grant-read.service.js").InboxGrantRow[];
49
+ }>;
50
+ /**
51
+ * The caller's tenant's grant book: `mine` (grants my tenant OWNS, any status — the
52
+ * outgoing list) + `incoming` (grants ACCEPTED into my tenant — distinct from the
53
+ * pending inbox). Both scoped to `req.principal.tenantId` server-side.
54
+ */
55
+ list(req: AuthedRequest): Promise<{
56
+ rows: import("./cross-tenant-grant-read.service.js").MineGrantRow[];
57
+ incoming: import("./cross-tenant-grant-read.service.js").IncomingGrantRow[];
58
+ }>;
59
+ /** The tenantScoped object types (+ their actions) the offer form's picker may show. */
60
+ offerableTypes(): Promise<{
61
+ types: import("./cross-tenant-grant-read.service.js").OfferableTypeRow[];
62
+ }>;
63
+ }
64
+ export {};
65
+ //# sourceMappingURL=cross-tenant-grant.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cross-tenant-grant.controller.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/cross-tenant-grant.controller.ts"],"names":[],"mappings":"AA4CA,OAAO,EAGL,sBAAsB,EAKvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,gGAAgG;AAChG,UAAU,iBAAiB;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAmDD,qBACa,0BAA0B;IAEO,OAAO,CAAC,QAAQ,CAAC,WAAW;IACtE,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADqC,WAAW,EAAE,sBAAsB,EAC7E,KAAK,EAAE,2BAA2B;IAGrD;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,iBAAiB;;;;IA+BtE,yFAAyF;IAGnF,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IASnE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IASnE;;;OAGG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa;;;IAKrC;;;;OAIG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa;;;;IAQpC,wFAAwF;IAGlF,cAAc;;;CAIrB"}
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.CrossTenantGrantController = void 0;
16
+ // packages/identity/src/features/cross-tenant/cross-tenant-grant.controller.ts
17
+ /**
18
+ * CrossTenantGrantController — the HTTP surface over the framework-owned
19
+ * `CrossTenantGrantWriter` (ADR-0009, Phase 4) + `CrossTenantGrantReadService` (Phase
20
+ * C). Mirrors `notes/share.controller.ts`'s shape (principal-derived authority body
21
+ * fields + a `mapGrantError` twin of `mapShareError`).
22
+ *
23
+ * Gated on the dedicated `cross_tenant_grant` object type's own action vocabulary
24
+ * (`share`/`accept`/`revoke` — `features/authz/actions.ts`'s `CROSS_TENANT_GRANT_ACTIONS`),
25
+ * NOT the identity CRUD verbs: `@Classified({ typeKey:'cross_tenant_grant', action:'share'|
26
+ * 'accept'|'revoke' })`. This is the SAME permission key the writer's `authorize` closure
27
+ * checks via `decide()`, so the route guard and the writer agree on exactly one gate — no
28
+ * bespoke admin notion (the deleted `DbGrantAuthorityResolver`/`COMPANY_ADMIN_KEYS`)
29
+ * survives anywhere in this path.
30
+ *
31
+ * ⛔ AUTHORITY FIELDS COME ONLY FROM `req.principal` — NEVER a body field:
32
+ * - offer: owningTenantId = req.principal.tenantId, offeredBy = req.principal.id
33
+ * - accept: acceptedBy = req.principal.id
34
+ * - revoke: revokedBy = req.principal.id
35
+ * The writer independently re-verifies the real authority (via `decide()`) server-side
36
+ * regardless of what the controller passes — but the controller itself must never even
37
+ * READ an authority field from the body, so a malicious body can't influence which
38
+ * tenant/actor is being checked.
39
+ *
40
+ * ⛔ SCOPE IS HARD-CODED 'object_type' IN V1 (decision 4) — the offer body carries no
41
+ * `recordId`/`scope` field; the controller always passes `scope: 'object_type'` to the
42
+ * writer. Record-scoped grants are a writer capability the UI does not expose yet.
43
+ *
44
+ * Decline (grantee side) = the grantee tenant calling `revoke` on an `offered` grant —
45
+ * the writer already permits either side to revoke an offered grant (no new status, no
46
+ * writer change).
47
+ */
48
+ const common_1 = require("@nestjs/common");
49
+ const guard_1 = require("@astram/guard");
50
+ const cross_tenant_grant_read_service_js_1 = require("./cross-tenant-grant-read.service.js");
51
+ const VALID_GRANTEE_KINDS = ['user', 'role'];
52
+ /** Validate + narrow the offer body, throwing 400 on any missing/malformed field. */
53
+ function parseOfferBody(body) {
54
+ const granteeTenantId = body?.granteeTenantId;
55
+ if (typeof granteeTenantId !== 'string' || granteeTenantId.length === 0) {
56
+ throw new common_1.BadRequestException('granteeTenantId is required');
57
+ }
58
+ const granteeKind = body?.granteeKind;
59
+ if (!VALID_GRANTEE_KINDS.includes(granteeKind)) {
60
+ throw new common_1.BadRequestException("granteeKind must be 'user' or 'role'");
61
+ }
62
+ const granteeUuid = body?.granteeUuid;
63
+ if (typeof granteeUuid !== 'string' || granteeUuid.length === 0) {
64
+ throw new common_1.BadRequestException('granteeUuid is required');
65
+ }
66
+ const typeKey = body?.typeKey;
67
+ if (typeof typeKey !== 'string' || typeKey.length === 0) {
68
+ throw new common_1.BadRequestException('typeKey is required');
69
+ }
70
+ const allowedActions = body?.allowedActions;
71
+ if (!Array.isArray(allowedActions) ||
72
+ allowedActions.length === 0 ||
73
+ !allowedActions.every((a) => typeof a === 'string' && a.length > 0)) {
74
+ throw new common_1.BadRequestException('allowedActions must be a non-empty array of action keys');
75
+ }
76
+ const expiresAtMs = body?.expiresAtMs;
77
+ if (typeof expiresAtMs !== 'number' || !Number.isFinite(expiresAtMs)) {
78
+ throw new common_1.BadRequestException('expiresAtMs is required and must be a finite epoch-ms timestamp');
79
+ }
80
+ return {
81
+ granteeTenantId,
82
+ granteeKind: granteeKind,
83
+ granteeUuid,
84
+ typeKey,
85
+ allowedActions: allowedActions,
86
+ expiresAtMs,
87
+ };
88
+ }
89
+ let CrossTenantGrantController = class CrossTenantGrantController {
90
+ grantWriter;
91
+ reads;
92
+ constructor(grantWriter, reads) {
93
+ this.grantWriter = grantWriter;
94
+ this.reads = reads;
95
+ }
96
+ /**
97
+ * Offer a cross-tenant grant. `owningTenantId`/`offeredBy` come ONLY from the
98
+ * authenticated principal; the body supplies the grantee + type + ceiling + expiry.
99
+ * `scope` is hard-coded 'object_type' (decision 4) — no recordId accepted in v1.
100
+ */
101
+ async offer(req, body) {
102
+ const parsed = parseOfferBody(body);
103
+ // H3 defense-in-depth: only a tenantScoped ("offerable") type may be granted across
104
+ // tenants. Reject a `system` typeKey (e.g. 'company') with 400 BEFORE the writer runs,
105
+ // so no junk 'offered' row is written. Uses the SAME tenantScoped filter the picker
106
+ // (`listOfferableTypes`) uses — one source of truth, no drift.
107
+ if (!this.reads.isOfferableType(parsed.typeKey)) {
108
+ throw new common_1.BadRequestException(`typeKey '${parsed.typeKey}' is not offerable for cross-tenant grants — ` +
109
+ `only tenant-scoped types can be granted across tenants`);
110
+ }
111
+ let uuid;
112
+ try {
113
+ uuid = await this.grantWriter.offer({
114
+ owningTenantId: req.principal.tenantId,
115
+ granteeTenantId: parsed.granteeTenantId,
116
+ granteeKind: parsed.granteeKind,
117
+ granteeUuid: parsed.granteeUuid,
118
+ typeKey: parsed.typeKey,
119
+ scope: 'object_type',
120
+ allowedActions: parsed.allowedActions,
121
+ expiresAtMs: parsed.expiresAtMs,
122
+ offeredBy: req.principal.id,
123
+ });
124
+ }
125
+ catch (err) {
126
+ throw mapGrantError(err);
127
+ }
128
+ return { success: true, uuid };
129
+ }
130
+ /** Accept an offered grant. `acceptedBy` comes ONLY from the authenticated principal. */
131
+ async accept(req, uuid) {
132
+ try {
133
+ await this.grantWriter.accept({ grantUuid: uuid, acceptedBy: req.principal.id });
134
+ }
135
+ catch (err) {
136
+ throw mapGrantError(err);
137
+ }
138
+ return { success: true };
139
+ }
140
+ /**
141
+ * Revoke a grant. `revokedBy` comes ONLY from the authenticated principal. Either the
142
+ * owning-tenant admin OR a grantee-tenant member may call this (the writer decides) —
143
+ * the grantee side calling this on an `offered` grant IS the "Decline" action; no
144
+ * separate endpoint or status exists for decline.
145
+ */
146
+ async revoke(req, uuid) {
147
+ try {
148
+ await this.grantWriter.revoke({ grantUuid: uuid, revokedBy: req.principal.id });
149
+ }
150
+ catch (err) {
151
+ throw mapGrantError(err);
152
+ }
153
+ return { success: true };
154
+ }
155
+ /**
156
+ * The caller's tenant's pending-offers inbox. Scoped to `req.principal.tenantId` —
157
+ * NEVER a query param — so a caller only ever sees offers made TO their own tenant.
158
+ */
159
+ async inbox(req) {
160
+ const rows = await this.reads.listInbox(req.principal.tenantId, Date.now());
161
+ return { rows };
162
+ }
163
+ /**
164
+ * The caller's tenant's grant book: `mine` (grants my tenant OWNS, any status — the
165
+ * outgoing list) + `incoming` (grants ACCEPTED into my tenant — distinct from the
166
+ * pending inbox). Both scoped to `req.principal.tenantId` server-side.
167
+ */
168
+ async list(req) {
169
+ const [mine, incoming] = await Promise.all([
170
+ this.reads.listMine(req.principal.tenantId),
171
+ this.reads.listIncoming(req.principal.tenantId),
172
+ ]);
173
+ return { rows: mine, incoming };
174
+ }
175
+ /** The tenantScoped object types (+ their actions) the offer form's picker may show. */
176
+ async offerableTypes() {
177
+ const types = await this.reads.listOfferableTypes();
178
+ return { types };
179
+ }
180
+ };
181
+ exports.CrossTenantGrantController = CrossTenantGrantController;
182
+ __decorate([
183
+ (0, common_1.Post)('cross-tenant-grants'),
184
+ (0, guard_1.Classified)({ typeKey: 'cross_tenant_grant', action: 'share' }),
185
+ __param(0, (0, common_1.Req)()),
186
+ __param(1, (0, common_1.Body)()),
187
+ __metadata("design:type", Function),
188
+ __metadata("design:paramtypes", [Object, Object]),
189
+ __metadata("design:returntype", Promise)
190
+ ], CrossTenantGrantController.prototype, "offer", null);
191
+ __decorate([
192
+ (0, common_1.Post)('cross-tenant-grants/:uuid/accept'),
193
+ (0, guard_1.Classified)({ typeKey: 'cross_tenant_grant', action: 'accept' }),
194
+ __param(0, (0, common_1.Req)()),
195
+ __param(1, (0, common_1.Param)('uuid')),
196
+ __metadata("design:type", Function),
197
+ __metadata("design:paramtypes", [Object, String]),
198
+ __metadata("design:returntype", Promise)
199
+ ], CrossTenantGrantController.prototype, "accept", null);
200
+ __decorate([
201
+ (0, common_1.Post)('cross-tenant-grants/:uuid/revoke'),
202
+ (0, guard_1.Classified)({ typeKey: 'cross_tenant_grant', action: 'revoke' }),
203
+ __param(0, (0, common_1.Req)()),
204
+ __param(1, (0, common_1.Param)('uuid')),
205
+ __metadata("design:type", Function),
206
+ __metadata("design:paramtypes", [Object, String]),
207
+ __metadata("design:returntype", Promise)
208
+ ], CrossTenantGrantController.prototype, "revoke", null);
209
+ __decorate([
210
+ (0, common_1.Get)('cross-tenant-grants/inbox'),
211
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
212
+ __param(0, (0, common_1.Req)()),
213
+ __metadata("design:type", Function),
214
+ __metadata("design:paramtypes", [Object]),
215
+ __metadata("design:returntype", Promise)
216
+ ], CrossTenantGrantController.prototype, "inbox", null);
217
+ __decorate([
218
+ (0, common_1.Get)('cross-tenant-grants'),
219
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
220
+ __param(0, (0, common_1.Req)()),
221
+ __metadata("design:type", Function),
222
+ __metadata("design:paramtypes", [Object]),
223
+ __metadata("design:returntype", Promise)
224
+ ], CrossTenantGrantController.prototype, "list", null);
225
+ __decorate([
226
+ (0, common_1.Get)('cross-tenant-grants/offerable-types'),
227
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
228
+ __metadata("design:type", Function),
229
+ __metadata("design:paramtypes", []),
230
+ __metadata("design:returntype", Promise)
231
+ ], CrossTenantGrantController.prototype, "offerableTypes", null);
232
+ exports.CrossTenantGrantController = CrossTenantGrantController = __decorate([
233
+ (0, common_1.Controller)('identity'),
234
+ __param(0, (0, common_1.Inject)(guard_1.SHIELD_CROSS_TENANT_GRANT_WRITER)),
235
+ __metadata("design:paramtypes", [guard_1.CrossTenantGrantWriter,
236
+ cross_tenant_grant_read_service_js_1.CrossTenantGrantReadService])
237
+ ], CrossTenantGrantController);
238
+ /**
239
+ * Map the writer's typed errors to HTTP (mirrors `share.controller.ts`'s
240
+ * `mapShareError`): not-permitted -> 403, invalid input -> 400, store outage -> 503.
241
+ */
242
+ function mapGrantError(err) {
243
+ if (err instanceof guard_1.GrantNotPermittedError)
244
+ return new common_1.ForbiddenException(err.message);
245
+ if (err instanceof guard_1.InvalidGrantError)
246
+ return new common_1.BadRequestException(err.message);
247
+ if (err instanceof guard_1.GrantStoreUnavailableError)
248
+ return new common_1.ServiceUnavailableException(err.message);
249
+ return err;
250
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `makeAuthorizeCrossTenantGrant` — builds the `GrantAuthorizeFn` guard's
3
+ * `CrossTenantGrantWriter` calls to answer "may this principal perform `action` in
4
+ * `tenantId`?" for the cross-tenant grant lifecycle (offer -> 'share', accept ->
5
+ * 'accept', revoke -> 'revoke').
6
+ *
7
+ * Extracted out of the module wiring (which eagerly opens a real DB pool at forRoot()
8
+ * time, making it unsafe to import from a lightweight unit test) so the closure's
9
+ * LOGIC — build the principal, call `decide()`, return `effect === 'allow'` — is
10
+ * independently testable with fake `readRoleIds`/`decide` collaborators.
11
+ *
12
+ * Cross-tenant sharing is NOT a bespoke authority question; it is the SAME "may this
13
+ * person do this?" question every other action asks, answered by the SAME `decide()`
14
+ * over the `cross_tenant_grant` object type (`plane: 'control'`, actions
15
+ * `share`/`accept`/`revoke`).
16
+ *
17
+ * ⚠️ ROOT CORRECTNESS: `cross_tenant_grant` is `plane:'control'`, so root MAY
18
+ * share/accept/revoke — and the ROUTE guard already ran `decide()` with the caller's
19
+ * REAL principal (incl. its true `isRoot`) before this closure is ever reached. If
20
+ * this closure built its principal with `isRoot: false` unconditionally, a root
21
+ * caller who PASSED the route would then be DENIED here (a route-allows/writer-denies
22
+ * split for the exact same principal). We avoid it by resolving `isRoot` the SAME way
23
+ * the framework's Internal identity adapter does — membership in the boot-loaded
24
+ * `rootIds` allowlist — so this closure's principal is root-correct too.
25
+ *
26
+ * FAIL-CLOSED: `readRoleIds`/`decide` throwing propagates un-caught — the writer's
27
+ * `resolveAuthority` converts a throw to `GrantStoreUnavailableError` (retryable
28
+ * 503). We must NOT catch and return `false`, which would mask an outage as a deny.
29
+ */
30
+ import type { DecisionAuthority } from '@astram/core';
31
+ import type { GrantAuthorizeFn } from '@astram/guard';
32
+ export interface MakeAuthorizeCrossTenantGrantDeps {
33
+ /** The (possibly second, independently-built) DecisionAuthority to call `decide()` on. */
34
+ readonly authority: Pick<DecisionAuthority, 'decide'>;
35
+ /** The SAME in-effect membership->posting roleIds read every other gate uses. */
36
+ readonly readRoleIds: (principalId: string, tenantId: string) => Promise<string[]>;
37
+ /** True iff `principalId` is in the boot-loaded root allowlist (SAME source `principal.isRoot`
38
+ * was set from on the route's own decide() call). */
39
+ readonly isRoot: (principalId: string) => boolean;
40
+ }
41
+ export declare function makeAuthorizeCrossTenantGrant(deps: MakeAuthorizeCrossTenantGrantDeps): GrantAuthorizeFn;
42
+ //# sourceMappingURL=grant-authorize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-authorize.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/grant-authorize.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,iCAAiC;IAChD,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IACtD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;CACnD;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iCAAiC,GACtC,gBAAgB,CAkBlB"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeAuthorizeCrossTenantGrant = makeAuthorizeCrossTenantGrant;
4
+ function makeAuthorizeCrossTenantGrant(deps) {
5
+ return async (principalId, tenantId, action) => {
6
+ const roleIds = await deps.readRoleIds(principalId, tenantId);
7
+ const principal = {
8
+ kind: 'user',
9
+ id: principalId,
10
+ tenantId,
11
+ roleIds,
12
+ isRoot: deps.isRoot(principalId),
13
+ };
14
+ const decision = await deps.authority.decide(principal, { key: action }, { level: 'object_type', typeKey: 'cross_tenant_grant', tenantId }, { isAuthenticated: true });
15
+ return decision.effect === 'allow';
16
+ };
17
+ }