@astram/identity 0.4.0 → 0.5.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 (172) hide show
  1. package/dist/company-membership.object.d.ts.map +1 -1
  2. package/dist/company-membership.object.js +3 -2
  3. package/dist/company.object.d.ts.map +1 -1
  4. package/dist/company.object.js +8 -3
  5. package/dist/features/access/index.d.ts.map +1 -1
  6. package/dist/features/access/index.js +1 -5
  7. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  8. package/dist/features/audit/audit-read.service.js +13 -6
  9. package/dist/features/audit/audit.controller.d.ts.map +1 -1
  10. package/dist/features/audit/audit.controller.js +2 -0
  11. package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
  12. package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
  13. package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
  14. package/dist/features/authz/catalog-reconcile.job.js +1 -3
  15. package/dist/features/authz/db-permission-facts.adapter.d.ts +16 -0
  16. package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
  17. package/dist/features/authz/db-permission-facts.adapter.js +46 -2
  18. package/dist/features/authz/grant-admin.controller.d.ts +8 -5
  19. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
  20. package/dist/features/authz/grant-admin.controller.js +20 -18
  21. package/dist/features/authz/grant-admin.service.d.ts +49 -16
  22. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  23. package/dist/features/authz/grant-admin.service.js +155 -96
  24. package/dist/features/authz/grant-ceiling.d.ts +52 -1
  25. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  26. package/dist/features/authz/grant-ceiling.js +156 -2
  27. package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
  28. package/dist/features/authz/mariadb-catalog-store.js +15 -3
  29. package/dist/features/authz/permission-version.bumper.d.ts +45 -1
  30. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
  31. package/dist/features/authz/permission-version.bumper.js +106 -16
  32. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  33. package/dist/features/authz/role-binding.d.ts +36 -0
  34. package/dist/features/authz/role-binding.d.ts.map +1 -0
  35. package/dist/features/authz/role-binding.js +53 -0
  36. package/dist/features/company/company-hierarchy.service.d.ts +46 -0
  37. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
  38. package/dist/features/company/company-hierarchy.service.js +100 -0
  39. package/dist/features/company/company.controller.d.ts +23 -5
  40. package/dist/features/company/company.controller.d.ts.map +1 -1
  41. package/dist/features/company/company.controller.js +47 -12
  42. package/dist/features/company/company.service.d.ts +28 -12
  43. package/dist/features/company/company.service.d.ts.map +1 -1
  44. package/dist/features/company/company.service.js +61 -14
  45. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
  46. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
  47. package/dist/features/identity-feature.module.d.ts.map +1 -1
  48. package/dist/features/identity-feature.module.js +60 -3
  49. package/dist/features/membership/membership.controller.d.ts +6 -5
  50. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  51. package/dist/features/membership/membership.controller.js +34 -22
  52. package/dist/features/membership/membership.service.d.ts +20 -14
  53. package/dist/features/membership/membership.service.d.ts.map +1 -1
  54. package/dist/features/membership/membership.service.js +62 -29
  55. package/dist/features/membership/posting-version-bump.d.ts +5 -0
  56. package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
  57. package/dist/features/membership/posting.service.d.ts +20 -24
  58. package/dist/features/membership/posting.service.d.ts.map +1 -1
  59. package/dist/features/membership/posting.service.js +101 -64
  60. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  61. package/dist/features/org-structure/org-catalog.controller.js +2 -0
  62. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  63. package/dist/features/org-structure/org-catalog.service.js +4 -1
  64. package/dist/features/policy/policy.controller.d.ts.map +1 -1
  65. package/dist/features/policy/policy.controller.js +2 -0
  66. package/dist/features/role/role.controller.d.ts +3 -2
  67. package/dist/features/role/role.controller.d.ts.map +1 -1
  68. package/dist/features/role/role.controller.js +15 -10
  69. package/dist/features/role/role.service.d.ts +16 -8
  70. package/dist/features/role/role.service.d.ts.map +1 -1
  71. package/dist/features/role/role.service.js +24 -10
  72. package/dist/features/scope/scope-closure.service.d.ts +44 -4
  73. package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
  74. package/dist/features/scope/scope-closure.service.js +215 -82
  75. package/dist/features/service-account/service-account.controller.d.ts +3 -2
  76. package/dist/features/service-account/service-account.controller.d.ts.map +1 -1
  77. package/dist/features/service-account/service-account.controller.js +29 -15
  78. package/dist/features/service-account/service-account.service.d.ts +14 -7
  79. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  80. package/dist/features/service-account/service-account.service.js +56 -11
  81. package/dist/features/share/share.controller.d.ts.map +1 -1
  82. package/dist/features/share/share.controller.js +2 -0
  83. package/dist/features/share/share.service.d.ts.map +1 -1
  84. package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
  85. package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
  86. package/dist/features/shared/identity-error.interceptor.js +82 -0
  87. package/dist/features/shared/identity-errors.d.ts +23 -0
  88. package/dist/features/shared/identity-errors.d.ts.map +1 -0
  89. package/dist/features/shared/identity-errors.js +41 -0
  90. package/dist/features/user/session-revocation-check.d.ts +11 -0
  91. package/dist/features/user/session-revocation-check.d.ts.map +1 -0
  92. package/dist/features/user/session-revocation-check.js +21 -0
  93. package/dist/features/user/user.controller.d.ts +11 -6
  94. package/dist/features/user/user.controller.d.ts.map +1 -1
  95. package/dist/features/user/user.controller.js +35 -17
  96. package/dist/features/user/user.service.d.ts +55 -14
  97. package/dist/features/user/user.service.d.ts.map +1 -1
  98. package/dist/features/user/user.service.js +143 -16
  99. package/dist/membership-posting.object.d.ts.map +1 -1
  100. package/dist/membership-posting.object.js +4 -2
  101. package/dist/objects.d.ts.map +1 -1
  102. package/dist/objects.js +13 -3
  103. package/dist/role.object.d.ts.map +1 -1
  104. package/dist/role.object.js +3 -0
  105. package/dist/user.object.d.ts.map +1 -1
  106. package/dist/user.object.js +3 -0
  107. package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
  108. package/package.json +4 -4
  109. package/dist/auth-facts-keys.js.map +0 -1
  110. package/dist/branch.object.js.map +0 -1
  111. package/dist/company-branch.object.js.map +0 -1
  112. package/dist/company-department.object.js.map +0 -1
  113. package/dist/company-membership.object.js.map +0 -1
  114. package/dist/company.object.js.map +0 -1
  115. package/dist/cross-tenant-grant.object.js.map +0 -1
  116. package/dist/department.object.js.map +0 -1
  117. package/dist/features/access/access-admin.access.js.map +0 -1
  118. package/dist/features/access/company-admin.access.js.map +0 -1
  119. package/dist/features/access/index.js.map +0 -1
  120. package/dist/features/access/user-management.access.js.map +0 -1
  121. package/dist/features/audit/audit-read.service.js.map +0 -1
  122. package/dist/features/audit/audit.controller.js.map +0 -1
  123. package/dist/features/audit/directory-label-resolver.js.map +0 -1
  124. package/dist/features/audit/mariadb-audit-reader.js.map +0 -1
  125. package/dist/features/authz/access-bundle-reconcile.job.js.map +0 -1
  126. package/dist/features/authz/access-bundle-store.js.map +0 -1
  127. package/dist/features/authz/catalog-bootstrap.provider.js.map +0 -1
  128. package/dist/features/authz/catalog-reconcile.job.js.map +0 -1
  129. package/dist/features/authz/db-permission-facts.adapter.js.map +0 -1
  130. package/dist/features/authz/grant-admin.controller.js.map +0 -1
  131. package/dist/features/authz/grant-admin.service.js.map +0 -1
  132. package/dist/features/authz/grant-ceiling.js.map +0 -1
  133. package/dist/features/authz/mariadb-catalog-store.js.map +0 -1
  134. package/dist/features/authz/permission-version.bumper.js.map +0 -1
  135. package/dist/features/authz/record-authorization.js.map +0 -1
  136. package/dist/features/authz/session-facts.reader.js.map +0 -1
  137. package/dist/features/company/company.controller.js.map +0 -1
  138. package/dist/features/company/company.service.js.map +0 -1
  139. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js.map +0 -1
  140. package/dist/features/cross-tenant/cross-tenant-grant.controller.js.map +0 -1
  141. package/dist/features/cross-tenant/grant-authorize.js.map +0 -1
  142. package/dist/features/identity-feature.module.js.map +0 -1
  143. package/dist/features/membership/membership-checker.js.map +0 -1
  144. package/dist/features/membership/membership.controller.js.map +0 -1
  145. package/dist/features/membership/membership.service.js.map +0 -1
  146. package/dist/features/membership/posting-version-bump.js.map +0 -1
  147. package/dist/features/membership/posting.service.js.map +0 -1
  148. package/dist/features/org-structure/org-catalog.controller.js.map +0 -1
  149. package/dist/features/org-structure/org-catalog.service.js.map +0 -1
  150. package/dist/features/policy/policy.controller.js.map +0 -1
  151. package/dist/features/policy/policy.service.js.map +0 -1
  152. package/dist/features/role/role.controller.js.map +0 -1
  153. package/dist/features/role/role.service.js.map +0 -1
  154. package/dist/features/scope/scope-closure.service.js.map +0 -1
  155. package/dist/features/scope/scope-subtree.resolver.js.map +0 -1
  156. package/dist/features/share/share.controller.js.map +0 -1
  157. package/dist/features/share/share.service.js.map +0 -1
  158. package/dist/features/shared/http.js.map +0 -1
  159. package/dist/features/shared/list-query.js.map +0 -1
  160. package/dist/features/shared/named-option.js.map +0 -1
  161. package/dist/features/shared/record-resolver.js.map +0 -1
  162. package/dist/features/user/user.controller.js.map +0 -1
  163. package/dist/features/user/user.service.js.map +0 -1
  164. package/dist/index.js.map +0 -1
  165. package/dist/membership-posting.object.js.map +0 -1
  166. package/dist/migrations-dir.js.map +0 -1
  167. package/dist/objects.js.map +0 -1
  168. package/dist/record-share.object.js.map +0 -1
  169. package/dist/role.object.js.map +0 -1
  170. package/dist/schema.js.map +0 -1
  171. package/dist/shared-resource.object.js.map +0 -1
  172. package/dist/user.object.js.map +0 -1
@@ -193,7 +193,10 @@ class ServiceAccountService {
193
193
  * described generically: the role/company catalog reads are not caller-scoped, so resolving a
194
194
  * name for an arbitrary id would turn the 403 into a cross-tenant name/existence oracle.
195
195
  */
196
- async assertWithinCeiling(ceiling, scopeSubtreeNodeIds, roleIds, onAgent = { nodes: [], roles: [] }) {
196
+ async assertWithinCeiling(ceiling, scopeSubtreeNodeIds, roleIds, onAgent = {
197
+ nodes: [],
198
+ roles: [],
199
+ }) {
197
200
  if (ceiling.isRoot)
198
201
  return;
199
202
  const badNodes = uniqueIds(scopeSubtreeNodeIds).filter((n) => !ceilingAllowsNode(ceiling, n));
@@ -213,13 +216,17 @@ class ServiceAccountService {
213
216
  async describeOffenders(bad, onAgent, lookup, noun, plural) {
214
217
  const known = new Set(onAgent);
215
218
  const nameable = bad.filter((id) => known.has(id));
216
- const found = nameable.length > 0 ? new Map((await lookup(nameable)).map((x) => [x.id, x.name])) : new Map();
219
+ const found = nameable.length > 0
220
+ ? new Map((await lookup(nameable)).map((x) => [x.id, x.name]))
221
+ : new Map();
217
222
  const names = uniqueIds(nameable.map((id) => found.get(id) ?? '').filter((n) => n.length > 0));
218
223
  const rest = bad.length - names.length;
219
224
  if (names.length === 0)
220
225
  return rest === 1 ? `a ${noun}` : `${rest} ${plural}`;
221
226
  const named = `the ${names.length === 1 ? noun : plural} ${quoteNames(names)}`;
222
- return rest === 0 ? named : `${named} and ${rest === 1 ? `1 other ${noun}` : `${rest} other ${plural}`}`;
227
+ return rest === 0
228
+ ? named
229
+ : `${named} and ${rest === 1 ? `1 other ${noun}` : `${rest} other ${plural}`}`;
223
230
  }
224
231
  /**
225
232
  * Resolve names for audit (ids already authorised for this caller: within ceiling, or on the
@@ -261,7 +268,9 @@ class ServiceAccountService {
261
268
  ]);
262
269
  return {
263
270
  // Defensive re-filter through the same predicates (a store that over-returns can't widen it).
264
- roles: roles.filter((r) => ceilingAllowsRole(ceiling, r.id)).map((r) => ({ id: r.id, name: r.name })),
271
+ roles: roles
272
+ .filter((r) => ceilingAllowsRole(ceiling, r.id))
273
+ .map((r) => ({ id: r.id, name: r.name })),
265
274
  scopeNodes: treeOrder(nodes.filter((n) => ceilingAllowsNode(ceiling, n.id))),
266
275
  };
267
276
  }
@@ -374,8 +383,12 @@ class ServiceAccountService {
374
383
  const updatedAtMs = Math.max(this.now(), (existing.updatedAtMs ?? 0) + 1);
375
384
  const normalized = {
376
385
  ...(patch.label !== undefined ? { label: patch.label.trim() } : {}),
377
- ...(requestedScope !== undefined ? { scopeSubtreeNodeIds: mergeIds(requestedScope, preservedScope) } : {}),
378
- ...(requestedRoles !== undefined ? { roleIds: mergeIds(requestedRoles, preservedRoles) } : {}),
386
+ ...(requestedScope !== undefined
387
+ ? { scopeSubtreeNodeIds: mergeIds(requestedScope, preservedScope) }
388
+ : {}),
389
+ ...(requestedRoles !== undefined
390
+ ? { roleIds: mergeIds(requestedRoles, preservedRoles) }
391
+ : {}),
379
392
  ...(patch.status !== undefined ? { status: patch.status } : {}),
380
393
  };
381
394
  // The preserve-merge is a read-modify-write: guard it so a concurrent edit (e.g. root revoking an
@@ -393,7 +406,10 @@ class ServiceAccountService {
393
406
  // WHAT changed, by name: diff the stored sets (added ⊆ ceiling or root; removed were on the agent).
394
407
  const diff = (before, after) => after === undefined
395
408
  ? { added: [], removed: [] }
396
- : { added: after.filter((x) => !before.includes(x)), removed: before.filter((x) => !after.includes(x)) };
409
+ : {
410
+ added: after.filter((x) => !before.includes(x)),
411
+ removed: before.filter((x) => !after.includes(x)),
412
+ };
397
413
  const roleDiff = diff(existing.roleIds, normalized.roleIds);
398
414
  const scopeDiff = diff(existing.scopeSubtreeNodeIds, normalized.scopeSubtreeNodeIds);
399
415
  const names = await this.auditNames([...roleDiff.added, ...roleDiff.removed], [...scopeDiff.added, ...scopeDiff.removed]);
@@ -414,13 +430,35 @@ class ServiceAccountService {
414
430
  * Issue a NEW key for an account (own-tenant). Mints `ask_<id>_<secret>`, hashes the secret, and
415
431
  * persists the hash as the next version — carrying the ACCOUNT's current scope/roles onto the key
416
432
  * (so the key's Principal reflects the account as it stands). Returns the raw key ONCE.
417
- * `expiresAtMs` is optional (a non-expiring key is discouraged; the caller opts in to no expiry).
433
+ * ADR-0033 §4 (security hardening): the caller must give a FUTURE `expiresAtMs`, or opt in to a
434
+ * non-expiring key explicitly with `neverExpires: true` — otherwise 400, nothing minted.
418
435
  */
419
436
  async issueKey(admin, id, opts = {}) {
437
+ const expiresAtMs = this.resolveExpiry(opts);
420
438
  const account = await this.accounts.getById(id, scopeOf(admin));
421
439
  if (account === null)
422
440
  throw new ServiceAccountNotFoundError();
423
- return this.mintFor(account, admin, 'service_account.key_issued', opts.expiresAtMs);
441
+ return this.mintFor(account, admin, 'service_account.key_issued', expiresAtMs);
442
+ }
443
+ /**
444
+ * The key's expiry from the request: a future epoch-ms, or `undefined` when the caller said
445
+ * `neverExpires: true`. Neither, both, or a past/invalid time → ServiceAccountInputError (400).
446
+ */
447
+ resolveExpiry(opts) {
448
+ const hasExpiry = opts.expiresAtMs !== undefined;
449
+ if (hasExpiry && opts.neverExpires === true) {
450
+ throw new ServiceAccountInputError('Give either an expiry or neverExpires: true, not both.');
451
+ }
452
+ if (opts.neverExpires === true)
453
+ return undefined;
454
+ if (!hasExpiry) {
455
+ throw new ServiceAccountInputError('Choose when the key expires (expiresAtMs), or set neverExpires: true for a key that never expires.');
456
+ }
457
+ const ms = opts.expiresAtMs;
458
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms <= this.now()) {
459
+ throw new ServiceAccountInputError('expiresAtMs must be a time in the future (epoch-ms).');
460
+ }
461
+ return ms;
424
462
  }
425
463
  /**
426
464
  * Rotate an account's key: mint a new version (the old version stops being the current one —
@@ -428,10 +466,11 @@ class ServiceAccountService {
428
466
  * new raw key ONCE.
429
467
  */
430
468
  async rotateKey(admin, id, opts = {}) {
469
+ const expiresAtMs = this.resolveExpiry(opts);
431
470
  const account = await this.accounts.getById(id, scopeOf(admin));
432
471
  if (account === null)
433
472
  throw new ServiceAccountNotFoundError();
434
- return this.mintFor(account, admin, 'service_account.key_rotated', opts.expiresAtMs);
473
+ return this.mintFor(account, admin, 'service_account.key_rotated', expiresAtMs);
435
474
  }
436
475
  /** Revoke ALL of an account's keys (own-tenant) — fail-closed on the next request. Idempotent. */
437
476
  async revokeKeys(admin, id) {
@@ -473,7 +512,13 @@ class ServiceAccountService {
473
512
  /** Best-effort audit — a sink throw must never fail a legitimate operation. */
474
513
  async safeAudit(action, admin, occurredAtMs, attributes) {
475
514
  try {
476
- await this.audit.record({ action, actorId: admin.id, tenantId: admin.tenantId, occurredAtMs, attributes });
515
+ await this.audit.record({
516
+ action,
517
+ actorId: admin.id,
518
+ tenantId: admin.tenantId,
519
+ occurredAtMs,
520
+ attributes,
521
+ });
477
522
  }
478
523
  catch {
479
524
  // swallow — auditing is best-effort here (the durable sink already logs its own failures).
@@ -1 +1 @@
1
- {"version":3,"file":"share.controller.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.controller.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAErE,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAqBD,qBACa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAEjD;;;;OAIG;IAGG,IAAI,CACD,GAAG,EAAE,aAAa,EACP,OAAO,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;;;IAYtC;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAW5D;;;OAGG;IAGG,OAAO,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;CAU/D"}
1
+ {"version":3,"file":"share.controller.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.controller.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAErE,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAwBD,qBAEa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAEjD;;;;OAIG;IAGG,IAAI,CACD,GAAG,EAAE,aAAa,EACP,OAAO,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;;;IAYtC;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAW5D;;;OAGG;IAGG,OAAO,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;CAU/D"}
@@ -28,6 +28,7 @@ exports.ShareController = void 0;
28
28
  */
29
29
  const common_1 = require("@nestjs/common");
30
30
  const guard_1 = require("@astram/guard");
31
+ const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
31
32
  const share_service_js_1 = require("./share.service.js");
32
33
  /** Validate + narrow a share/unshare body to a {@link ShareRequest} (grantee + action only). */
33
34
  function toShareRequest(body) {
@@ -139,5 +140,6 @@ __decorate([
139
140
  ], ShareController.prototype, "unshare", null);
140
141
  exports.ShareController = ShareController = __decorate([
141
142
  (0, common_1.Controller)('identity'),
143
+ (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
142
144
  __metadata("design:paramtypes", [share_service_js_1.ShareService])
143
145
  ], ShareController);
@@ -1 +1 @@
1
- {"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,eAAe,CAAC;AAEvB,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,KAAK,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,mBAAmB,CAAC,KAAK,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CACT,aAAa,CAAC;QACZ,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CACH,CAAC;CACH;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,kGAAkG;AAClG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,iBAAiB,GAAG,eAAe,EAC3C,MAAM,EAAE,kBAAkB;IAG7C;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/D,qFAAqF;IAC/E,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjE;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC;CAoBjC"}
1
+ {"version":3,"file":"share.service.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEzE,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrF;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,KAAK,EAAE;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,OAAO,CAAC,KAAK,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,mBAAmB,CAAC,KAAK,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CACT,aAAa,CAAC;QACZ,WAAW,EAAE,gBAAgB,CAAC;QAC9B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CACH,CAAC;CACH;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,kGAAkG;AAClG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,MAAM,EAAE,iBAAiB,GAAG,eAAe,EAC3C,MAAM,EAAE,kBAAkB;IAG7C;;;;OAIG;IACG,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/D,qFAAqF;IAC/E,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjE;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC;CAoBjC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * IdentityErrorInterceptor — maps engine, ceiling and identity domain errors to HTTP for every
3
+ * identity route, in ONE place (security hardening Phase 1, ADR-0034 follow-up).
4
+ *
5
+ * Before this, an engine `AuthorizationDeniedError` (for example the ADR-0034 create wall's
6
+ * `create_outside_write_scope`) escaped a controller as an unmapped error → HTTP 500. Each mapping
7
+ * below uses a fixed, id-free message (ADR-0012): the engine's own message carries reason codes and
8
+ * detail that are for logs, not clients.
9
+ *
10
+ * Applied with `@UseInterceptors(IdentityErrorInterceptor)` on each identity controller. An
11
+ * `HttpException` (a controller's own 400/403/404) passes through untouched; anything unmapped is
12
+ * rethrown as-is (Nest's default handling).
13
+ */
14
+ import { type CallHandler, type ExecutionContext, type NestInterceptor } from '@nestjs/common';
15
+ import { type Observable } from 'rxjs';
16
+ /**
17
+ * Map one error to the HTTP exception identity answers with, or return it unchanged when it is
18
+ * not an identity-mapped type. Exported for unit tests and for callers that map by hand.
19
+ */
20
+ export declare function toIdentityHttpError(err: unknown): unknown;
21
+ export declare class IdentityErrorInterceptor implements NestInterceptor {
22
+ intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown>;
23
+ }
24
+ //# sourceMappingURL=identity-error.interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-error.interceptor.d.ts","sourceRoot":"","sources":["../../../src/features/shared/identity-error.interceptor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAA0B,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAqB/D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAwBzD;AAED,qBACa,wBAAyB,YAAW,eAAe;IAC9D,SAAS,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC;CAK9E"}
@@ -0,0 +1,82 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IdentityErrorInterceptor = void 0;
10
+ exports.toIdentityHttpError = toIdentityHttpError;
11
+ /**
12
+ * IdentityErrorInterceptor — maps engine, ceiling and identity domain errors to HTTP for every
13
+ * identity route, in ONE place (security hardening Phase 1, ADR-0034 follow-up).
14
+ *
15
+ * Before this, an engine `AuthorizationDeniedError` (for example the ADR-0034 create wall's
16
+ * `create_outside_write_scope`) escaped a controller as an unmapped error → HTTP 500. Each mapping
17
+ * below uses a fixed, id-free message (ADR-0012): the engine's own message carries reason codes and
18
+ * detail that are for logs, not clients.
19
+ *
20
+ * Applied with `@UseInterceptors(IdentityErrorInterceptor)` on each identity controller. An
21
+ * `HttpException` (a controller's own 400/403/404) passes through untouched; anything unmapped is
22
+ * rethrown as-is (Nest's default handling).
23
+ */
24
+ const common_1 = require("@nestjs/common");
25
+ const rxjs_1 = require("rxjs");
26
+ const engine_1 = require("@astram/engine");
27
+ const core_1 = require("@astram/core");
28
+ const guard_1 = require("@astram/guard");
29
+ const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
30
+ const scope_closure_service_js_1 = require("../scope/scope-closure.service.js");
31
+ const identity_errors_js_1 = require("./identity-errors.js");
32
+ /** The fixed client message for an engine deny, by reason code (never the engine's own text). */
33
+ function deniedMessage(reason) {
34
+ if (reason === 'create_outside_write_scope') {
35
+ return 'You cannot create this record: it is outside the organisations you manage.';
36
+ }
37
+ return 'You do not have access to perform this action.';
38
+ }
39
+ /**
40
+ * Map one error to the HTTP exception identity answers with, or return it unchanged when it is
41
+ * not an identity-mapped type. Exported for unit tests and for callers that map by hand.
42
+ */
43
+ function toIdentityHttpError(err) {
44
+ if (err instanceof common_1.HttpException)
45
+ return err;
46
+ if (err instanceof engine_1.AuthorizationDeniedError)
47
+ return new common_1.ForbiddenException(deniedMessage(err.reason));
48
+ if (err instanceof grant_ceiling_js_1.GrantCeilingError)
49
+ return new common_1.ForbiddenException(err.message);
50
+ if (err instanceof identity_errors_js_1.IdentityForbiddenError)
51
+ return new common_1.ForbiddenException(err.message);
52
+ if (err instanceof identity_errors_js_1.IdentityNotFoundError)
53
+ return new common_1.NotFoundException(err.message);
54
+ if (err instanceof identity_errors_js_1.IdentityConflictError)
55
+ return new common_1.ConflictException(err.message);
56
+ if (err instanceof identity_errors_js_1.IdentityBadRequestError)
57
+ return new common_1.BadRequestException(err.message);
58
+ if (err instanceof guard_1.ScopeCycleError) {
59
+ return new common_1.ConflictException('That move would put an organisation inside its own sub-tree.');
60
+ }
61
+ if (err instanceof scope_closure_service_js_1.NodeHasChildrenError) {
62
+ return new common_1.ConflictException('This organisation still has sub-organisations. Move or delete them first.');
63
+ }
64
+ if (err instanceof engine_1.ValidationError) {
65
+ return new common_1.BadRequestException(`Invalid value for '${err.field}'.`);
66
+ }
67
+ if (err instanceof core_1.AuthorizationUnavailableError || err instanceof engine_1.EngineUnavailableError) {
68
+ return new common_1.ServiceUnavailableException('Authorization is temporarily unavailable. Try again.');
69
+ }
70
+ return err;
71
+ }
72
+ let IdentityErrorInterceptor = class IdentityErrorInterceptor {
73
+ intercept(_context, next) {
74
+ return next
75
+ .handle()
76
+ .pipe((0, rxjs_1.catchError)((err) => (0, rxjs_1.throwError)(() => toIdentityHttpError(err))));
77
+ }
78
+ };
79
+ exports.IdentityErrorInterceptor = IdentityErrorInterceptor;
80
+ exports.IdentityErrorInterceptor = IdentityErrorInterceptor = __decorate([
81
+ (0, common_1.Injectable)()
82
+ ], IdentityErrorInterceptor);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Identity domain errors (security hardening Phase 1). Thrown by identity services, mapped to HTTP
3
+ * by {@link IdentityErrorInterceptor} in ONE place so every identity route answers the same way.
4
+ *
5
+ * Messages are user-facing: they name no raw ids (ADR-0012) and carry no store detail.
6
+ */
7
+ /** The caller may not perform this action on this target (→ 403). */
8
+ export declare class IdentityForbiddenError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /** The target does not exist or is outside the caller's reach (→ 404, no existence leak). */
12
+ export declare class IdentityNotFoundError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ /** The request conflicts with the current state (cycle, children present, … → 409). */
16
+ export declare class IdentityConflictError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ /** The request is malformed (→ 400). */
20
+ export declare class IdentityBadRequestError extends Error {
21
+ constructor(message: string);
22
+ }
23
+ //# sourceMappingURL=identity-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-errors.d.ts","sourceRoot":"","sources":["../../../src/features/shared/identity-errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,qEAAqE;AACrE,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,6FAA6F;AAC7F,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,uFAAuF;AACvF,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,wCAAwC;AACxC,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Identity domain errors (security hardening Phase 1). Thrown by identity services, mapped to HTTP
4
+ * by {@link IdentityErrorInterceptor} in ONE place so every identity route answers the same way.
5
+ *
6
+ * Messages are user-facing: they name no raw ids (ADR-0012) and carry no store detail.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IdentityBadRequestError = exports.IdentityConflictError = exports.IdentityNotFoundError = exports.IdentityForbiddenError = void 0;
10
+ /** The caller may not perform this action on this target (→ 403). */
11
+ class IdentityForbiddenError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'IdentityForbiddenError';
15
+ }
16
+ }
17
+ exports.IdentityForbiddenError = IdentityForbiddenError;
18
+ /** The target does not exist or is outside the caller's reach (→ 404, no existence leak). */
19
+ class IdentityNotFoundError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = 'IdentityNotFoundError';
23
+ }
24
+ }
25
+ exports.IdentityNotFoundError = IdentityNotFoundError;
26
+ /** The request conflicts with the current state (cycle, children present, … → 409). */
27
+ class IdentityConflictError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'IdentityConflictError';
31
+ }
32
+ }
33
+ exports.IdentityConflictError = IdentityConflictError;
34
+ /** The request is malformed (→ 400). */
35
+ class IdentityBadRequestError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = 'IdentityBadRequestError';
39
+ }
40
+ }
41
+ exports.IdentityBadRequestError = IdentityBadRequestError;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Boot check (security hardening Phase 3 review P2). Disabling a user revokes its sessions through
3
+ * guard's `SHIELD_SESSION_SWEEPER` (provided by the auth surface). When the auth surface is on —
4
+ * guard's session token binder is visible — but no sweeper is, a disable still drops the user's
5
+ * roles (version bump + `[]` roles) but cannot end its sessions: say so at boot instead of failing
6
+ * silently. Auth off → no sessions exist → nothing to warn about.
7
+ *
8
+ * @returns `true` when the warning was emitted.
9
+ */
10
+ export declare function warnIfSessionsUnrevocable(binder: unknown, sweeper: unknown, warn: (message: string) => void): boolean;
11
+ //# sourceMappingURL=session-revocation-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-revocation-check.d.ts","sourceRoot":"","sources":["../../../src/features/user/session-revocation-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,OAAO,CAQT"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.warnIfSessionsUnrevocable = warnIfSessionsUnrevocable;
4
+ /**
5
+ * Boot check (security hardening Phase 3 review P2). Disabling a user revokes its sessions through
6
+ * guard's `SHIELD_SESSION_SWEEPER` (provided by the auth surface). When the auth surface is on —
7
+ * guard's session token binder is visible — but no sweeper is, a disable still drops the user's
8
+ * roles (version bump + `[]` roles) but cannot end its sessions: say so at boot instead of failing
9
+ * silently. Auth off → no sessions exist → nothing to warn about.
10
+ *
11
+ * @returns `true` when the warning was emitted.
12
+ */
13
+ function warnIfSessionsUnrevocable(binder, sweeper, warn) {
14
+ if (binder === undefined || binder === null)
15
+ return false;
16
+ if (sweeper !== undefined && sweeper !== null)
17
+ return false;
18
+ warn('SHIELD_SESSION_SWEEPER is not available: disabling a user will not revoke its sessions ' +
19
+ '(its roles still drop on the next request). Provide a sessionSweeper to ShieldModule.forRoot.');
20
+ return true;
21
+ }
@@ -1,5 +1,6 @@
1
1
  import { UserService } from './user.service.js';
2
2
  import { type ListQueryDto } from '../shared/list-query.js';
3
+ import type { AuthedRequest } from '../shared/http.js';
3
4
  export declare class UserController {
4
5
  private readonly users;
5
6
  constructor(users: UserService);
@@ -14,25 +15,29 @@ export declare class UserController {
14
15
  page: number;
15
16
  limit: number;
16
17
  }>;
17
- /** Create a user — gated on `identity:user:create`. */
18
- createUser(body: {
18
+ /**
19
+ * Create a user — gated on `identity:user:create`. Optional `companyUuid` creates the user's
20
+ * first (active) membership in the same transaction; it must be a company the caller manages.
21
+ */
22
+ createUser(req: AuthedRequest, body: {
19
23
  username?: string;
24
+ companyUuid?: string;
20
25
  }): Promise<{
21
26
  id: import("packages/engine/dist/types.js").RecordId;
22
27
  }>;
23
28
  /** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
24
29
  * administrator, may read users). */
25
30
  getUser(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
26
- /** Update a user — gated on `identity:user:update`. */
27
- updateUser(uuid: string, body: {
31
+ /** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
32
+ updateUser(req: AuthedRequest, uuid: string, body: {
28
33
  username?: string;
29
34
  email?: string;
30
35
  status?: string;
31
36
  }): Promise<{
32
37
  success: boolean;
33
38
  }>;
34
- /** Delete a user — gated on `identity:user:delete`. */
35
- deleteUser(uuid: string): Promise<{
39
+ /** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
40
+ deleteUser(req: AuthedRequest, uuid: string): Promise<{
36
41
  success: boolean;
37
42
  }>;
38
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAK5E,qBACa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAS,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKpD;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,uDAAuD;IAGjD,UAAU,CACC,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMtE,uDAAuD;IAGjD,UAAU,CAAgB,IAAI,EAAE,MAAM;;;CAI7C"}
1
+ {"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EACjB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAW3D;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,iGAAiG;IAG3F,UAAU,CACP,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAUtE,0FAA0F;IAGpF,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAIxE"}
@@ -17,9 +17,11 @@ const common_1 = require("@nestjs/common");
17
17
  const guard_1 = require("@astram/guard");
18
18
  const user_service_js_1 = require("./user.service.js");
19
19
  const list_query_js_1 = require("../shared/list-query.js");
20
+ const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
20
21
  // User management is gated on `identity:user:<action>` grants. `user` is CONTROL-plane
21
22
  // for ALL actions — including read: the capped root is the identity administrator (the
22
- // identity-administrator model), so it may list/read users AND manage them.
23
+ // identity-administrator model), so it may list/read users AND manage them. Changing a user's
24
+ // email or status, or deleting them, also needs the caller to dominate that user (D2).
23
25
  let UserController = class UserController {
24
26
  users;
25
27
  constructor(users) {
@@ -35,9 +37,17 @@ let UserController = class UserController {
35
37
  const result = await this.users.listUsers(params);
36
38
  return { total: result.total, rows: result.rows, page, limit };
37
39
  }
38
- /** Create a user — gated on `identity:user:create`. */
39
- async createUser(body) {
40
- const id = await this.users.createUser(body?.username ?? 'Untitled');
40
+ /**
41
+ * Create a user — gated on `identity:user:create`. Optional `companyUuid` creates the user's
42
+ * first (active) membership in the same transaction; it must be a company the caller manages.
43
+ */
44
+ async createUser(req, body) {
45
+ const id = await this.users.createUser(req.principal, {
46
+ username: body?.username ?? 'Untitled',
47
+ ...(typeof body?.companyUuid === 'string' && body.companyUuid.length > 0
48
+ ? { companyUuid: body.companyUuid }
49
+ : {}),
50
+ });
41
51
  return { id };
42
52
  }
43
53
  /** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
@@ -45,14 +55,18 @@ let UserController = class UserController {
45
55
  getUser(uuid) {
46
56
  return this.users.getUser(uuid);
47
57
  }
48
- /** Update a user — gated on `identity:user:update`. */
49
- async updateUser(uuid, body) {
50
- await this.users.updateUser(uuid, body ?? {});
58
+ /** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
59
+ async updateUser(req, uuid, body) {
60
+ await this.users.updateUser(req.principal, uuid, {
61
+ username: body?.username,
62
+ email: body?.email,
63
+ status: body?.status,
64
+ });
51
65
  return { success: true };
52
66
  }
53
- /** Delete a user — gated on `identity:user:delete`. */
54
- async deleteUser(uuid) {
55
- await this.users.deleteUser(uuid);
67
+ /** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
68
+ async deleteUser(req, uuid) {
69
+ await this.users.deleteUser(req.principal, uuid);
56
70
  return { success: true };
57
71
  }
58
72
  };
@@ -68,9 +82,10 @@ __decorate([
68
82
  __decorate([
69
83
  (0, common_1.Post)('users'),
70
84
  (0, guard_1.Classified)({ typeKey: 'user', action: 'create' }),
71
- __param(0, (0, common_1.Body)()),
85
+ __param(0, (0, common_1.Req)()),
86
+ __param(1, (0, common_1.Body)()),
72
87
  __metadata("design:type", Function),
73
- __metadata("design:paramtypes", [Object]),
88
+ __metadata("design:paramtypes", [Object, Object]),
74
89
  __metadata("design:returntype", Promise)
75
90
  ], UserController.prototype, "createUser", null);
76
91
  __decorate([
@@ -84,21 +99,24 @@ __decorate([
84
99
  __decorate([
85
100
  (0, common_1.Put)('users/:uuid'),
86
101
  (0, guard_1.Classified)({ typeKey: 'user', action: 'update' }),
87
- __param(0, (0, common_1.Param)('uuid')),
88
- __param(1, (0, common_1.Body)()),
102
+ __param(0, (0, common_1.Req)()),
103
+ __param(1, (0, common_1.Param)('uuid')),
104
+ __param(2, (0, common_1.Body)()),
89
105
  __metadata("design:type", Function),
90
- __metadata("design:paramtypes", [String, Object]),
106
+ __metadata("design:paramtypes", [Object, String, Object]),
91
107
  __metadata("design:returntype", Promise)
92
108
  ], UserController.prototype, "updateUser", null);
93
109
  __decorate([
94
110
  (0, common_1.Delete)('users/:uuid'),
95
111
  (0, guard_1.Classified)({ typeKey: 'user', action: 'delete' }),
96
- __param(0, (0, common_1.Param)('uuid')),
112
+ __param(0, (0, common_1.Req)()),
113
+ __param(1, (0, common_1.Param)('uuid')),
97
114
  __metadata("design:type", Function),
98
- __metadata("design:paramtypes", [String]),
115
+ __metadata("design:paramtypes", [Object, String]),
99
116
  __metadata("design:returntype", Promise)
100
117
  ], UserController.prototype, "deleteUser", null);
101
118
  exports.UserController = UserController = __decorate([
102
119
  (0, common_1.Controller)('identity'),
120
+ (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
103
121
  __metadata("design:paramtypes", [user_service_js_1.UserService])
104
122
  ], UserController);
@@ -1,26 +1,67 @@
1
1
  import { RecordsService, type RecordId } from '@astram/engine';
2
+ import type { Principal } from '@astram/core';
3
+ import { type EmailSender, type ResetTokenStore, type SessionSweeper } from '@astram/guard';
4
+ import { type PostingVersionBump } from '../membership/posting-version-bump.js';
2
5
  import { type ListParams } from '../shared/list-query.js';
6
+ import { GrantCeiling } from '../authz/grant-ceiling.js';
7
+ /** The patch keys a generic user update accepts (security hardening #3). */
8
+ export interface UserPatch {
9
+ username?: unknown;
10
+ email?: unknown;
11
+ status?: unknown;
12
+ }
3
13
  /**
4
- * UserService — user (principal) CRUD on the paved road, via the framework's
5
- * RecordsService facade. The facade supplies the acting identity (the request
6
- * principal) — this service never threads a Principal. No raw SQL, no RECORD_ENGINE.
14
+ * UserService — user (principal) CRUD on the paved road, via the framework's RecordsService facade
15
+ * (the request principal acts). No raw SQL, no RECORD_ENGINE.
16
+ *
17
+ * Users are GLOBAL (one user can belong to many tenants) and the email is a credential attribute:
18
+ * forgot-password mails the reset link to it. So (security hardening #3, D2):
19
+ * - the patch is whitelisted to `{username, email, status}`;
20
+ * - changing `email` or `status`, and deleting a user, require the actor to DOMINATE the target
21
+ * (`GrantCeiling.assertDominates`: every membership of the target inside the actor's subtree,
22
+ * and every key the target could hold is held by the actor);
23
+ * - an email change burns the target's outstanding password-reset tokens BEFORE the change and
24
+ * notifies the OLD address after it (best-effort), when guard's reset-token store and email
25
+ * sender are wired (`ShieldModule.forRoot({ auth: true })`).
7
26
  */
8
27
  export declare class UserService {
9
28
  private readonly records;
10
- constructor(records: RecordsService);
11
- /** Create a user. */
12
- createUser(username: string): Promise<RecordId>;
29
+ private readonly ceiling;
30
+ private readonly resetTokens?;
31
+ private readonly emailSender?;
32
+ private readonly sessions?;
33
+ private readonly versionBump?;
34
+ private readonly logger;
35
+ constructor(records: RecordsService, ceiling: GrantCeiling, resetTokens?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, sessions?: (SessionSweeper | null) | undefined, versionBump?: (PostingVersionBump | null) | undefined);
36
+ /**
37
+ * Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
38
+ * ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
39
+ * by the engine's create wall (a company outside the caller's subtree ⇒ 403, nothing persists).
40
+ */
41
+ createUser(actor: Principal, input: {
42
+ username: string;
43
+ companyUuid?: string | undefined;
44
+ }): Promise<RecordId>;
13
45
  /** List users (tenant-scoped by the pipe), with paging/search/sort. */
14
46
  listUsers(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
15
47
  /** Read a single user. */
16
48
  getUser(uuid: string): Promise<import("@astram/engine").RecordView | null>;
17
- /** Update a user. */
18
- updateUser(uuid: string, patch: {
19
- username?: string;
20
- email?: string;
21
- status?: string;
22
- }): Promise<void>;
23
- /** Delete a user. */
24
- deleteUser(uuid: string): Promise<void>;
49
+ /** Update a user (whitelisted patch; email/status changes need dominance — see the class doc). */
50
+ updateUser(actor: Principal, uuid: string, patch: UserPatch): Promise<void>;
51
+ /**
52
+ * After a status change (security hardening Phase 3): bump every tenant the user is a member of
53
+ * (the next request reloads its grants: none while disabled, back when re-enabled), and, when the
54
+ * account is not `active`, durably revoke all its sessions (Redis unbound too).
55
+ *
56
+ * Both are ATTEMPTED INDEPENDENTLY (Phase 3 review P1): a failed bump never skips the revoke, or
57
+ * the reverse. If either failed, the first error is rethrown, so the admin sees it and can
58
+ * re-send the same status (which re-runs both). The status itself is already committed, so login
59
+ * and role reads refuse the account regardless.
60
+ */
61
+ private applyStatusChange;
62
+ /** Delete a user — only a user the actor dominates. */
63
+ deleteUser(actor: Principal, uuid: string): Promise<void>;
64
+ /** Tell the OLD address its account email changed (best-effort; never names the new one). */
65
+ private notifyEmailChanged;
25
66
  }
26
67
  //# sourceMappingURL=user.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAK1E;;;;GAIG;AACH,qBACa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,qBAAqB;IACrB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAI/C,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,qBAAqB;IACrB,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5D,OAAO,CAAC,IAAI,CAAC;IAIhB,qBAAqB;IACrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC"}
1
+ {"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAMzD,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,qBACa,WAAW;IAIpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAGxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IACY,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAMtE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAK1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAnB/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAGpC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAGrB,WAAW,CAAC,GAAE,eAAe,GAAG,IAAI,aAAA,EACK,WAAW,CAAC,GAAE,WAAW,GAAG,IAAI,aAAA,EAMzE,QAAQ,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA,EAKhC,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA;IAG1D;;;;OAIG;IACG,UAAU,CACd,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAC5D,OAAO,CAAC,QAAQ,CAAC;IAcpB,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,kGAAkG;IAC5F,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BjF;;;;;;;;;OASG;YACW,iBAAiB;IAY/B,uDAAuD;IACjD,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/D,6FAA6F;YAC/E,kBAAkB;CAkBjC"}