@astram/identity 0.16.0 → 0.17.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 (128) hide show
  1. package/dist/branch.object.d.ts +9 -1
  2. package/dist/branch.object.d.ts.map +1 -1
  3. package/dist/branch.object.js +14 -1
  4. package/dist/company-branch.object.d.ts +5 -4
  5. package/dist/company-branch.object.d.ts.map +1 -1
  6. package/dist/company-branch.object.js +6 -4
  7. package/dist/company-department.object.d.ts +6 -6
  8. package/dist/company-department.object.d.ts.map +1 -1
  9. package/dist/company-department.object.js +7 -6
  10. package/dist/department.object.d.ts +9 -1
  11. package/dist/department.object.d.ts.map +1 -1
  12. package/dist/department.object.js +14 -1
  13. package/dist/features/access/access-admin.access.d.ts.map +1 -1
  14. package/dist/features/access/access-admin.access.js +8 -1
  15. package/dist/features/access/index.d.ts +1 -0
  16. package/dist/features/access/index.d.ts.map +1 -1
  17. package/dist/features/access/index.js +10 -2
  18. package/dist/features/access/org-structure.access.d.ts +3 -0
  19. package/dist/features/access/org-structure.access.d.ts.map +1 -0
  20. package/dist/features/access/org-structure.access.js +41 -0
  21. package/dist/features/access/user-management.access.d.ts.map +1 -1
  22. package/dist/features/access/user-management.access.js +5 -0
  23. package/dist/features/audit/audit-read.service.d.ts +32 -1
  24. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  25. package/dist/features/audit/audit-read.service.js +67 -14
  26. package/dist/features/audit/auth-audit-sink.d.ts +19 -0
  27. package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
  28. package/dist/features/audit/auth-audit-sink.js +24 -0
  29. package/dist/features/audit/directory-label-resolver.d.ts +10 -1
  30. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
  31. package/dist/features/audit/directory-label-resolver.js +28 -1
  32. package/dist/features/authz/grant-admin.controller.d.ts +4 -4
  33. package/dist/features/authz/grant-admin.controller.js +4 -4
  34. package/dist/features/authz/grant-admin.service.d.ts +9 -8
  35. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  36. package/dist/features/authz/grant-admin.service.js +20 -24
  37. package/dist/features/authz/grant-ceiling.d.ts +35 -16
  38. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  39. package/dist/features/authz/grant-ceiling.js +44 -29
  40. package/dist/features/authz/role-binding.d.ts +25 -24
  41. package/dist/features/authz/role-binding.d.ts.map +1 -1
  42. package/dist/features/authz/role-binding.js +31 -44
  43. package/dist/features/identity-feature.module.d.ts +0 -11
  44. package/dist/features/identity-feature.module.d.ts.map +1 -1
  45. package/dist/features/identity-feature.module.js +90 -1
  46. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  47. package/dist/features/membership/membership.controller.js +20 -6
  48. package/dist/features/membership/membership.service.d.ts +16 -10
  49. package/dist/features/membership/membership.service.d.ts.map +1 -1
  50. package/dist/features/membership/membership.service.js +61 -21
  51. package/dist/features/membership/posting.service.d.ts +25 -6
  52. package/dist/features/membership/posting.service.d.ts.map +1 -1
  53. package/dist/features/membership/posting.service.js +63 -16
  54. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
  55. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
  56. package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
  57. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
  58. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
  59. package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
  60. package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
  61. package/dist/features/org-bootstrap/org-state.reader.js +56 -49
  62. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
  63. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
  64. package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
  65. package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
  66. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  67. package/dist/features/org-structure/org-catalog.controller.js +105 -17
  68. package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
  69. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  70. package/dist/features/org-structure/org-catalog.service.js +327 -51
  71. package/dist/features/org-structure/org-unit-name.d.ts +31 -0
  72. package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
  73. package/dist/features/org-structure/org-unit-name.js +50 -0
  74. package/dist/features/role/role.controller.d.ts +28 -13
  75. package/dist/features/role/role.controller.d.ts.map +1 -1
  76. package/dist/features/role/role.controller.js +54 -14
  77. package/dist/features/role/role.service.d.ts +76 -21
  78. package/dist/features/role/role.service.d.ts.map +1 -1
  79. package/dist/features/role/role.service.js +204 -27
  80. package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
  81. package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
  82. package/dist/features/security-policy/security-policy-version-bump.js +65 -0
  83. package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
  84. package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
  85. package/dist/features/security-policy/security-policy.controller.js +121 -0
  86. package/dist/features/security-policy/security-policy.service.d.ts +94 -0
  87. package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
  88. package/dist/features/security-policy/security-policy.service.js +429 -0
  89. package/dist/features/service-account/service-account.service.d.ts +13 -3
  90. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  91. package/dist/features/service-account/service-account.service.js +48 -10
  92. package/dist/features/service-account/service-account.store.d.ts +11 -1
  93. package/dist/features/service-account/service-account.store.d.ts.map +1 -1
  94. package/dist/features/service-account/service-account.store.js +20 -1
  95. package/dist/features/user/signin-admin.tokens.d.ts +11 -0
  96. package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
  97. package/dist/features/user/signin-admin.tokens.js +13 -0
  98. package/dist/features/user/user-mfa-status.d.ts +38 -0
  99. package/dist/features/user/user-mfa-status.d.ts.map +1 -0
  100. package/dist/features/user/user-mfa-status.js +47 -0
  101. package/dist/features/user/user-signin-admin.service.d.ts +48 -0
  102. package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
  103. package/dist/features/user/user-signin-admin.service.js +232 -0
  104. package/dist/features/user/user.controller.d.ts +29 -2
  105. package/dist/features/user/user.controller.d.ts.map +1 -1
  106. package/dist/features/user/user.controller.js +71 -3
  107. package/dist/features/user/user.service.d.ts +40 -14
  108. package/dist/features/user/user.service.d.ts.map +1 -1
  109. package/dist/features/user/user.service.js +143 -10
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +3 -1
  113. package/dist/objects.d.ts.map +1 -1
  114. package/dist/objects.js +2 -0
  115. package/dist/role.object.d.ts +1 -1
  116. package/dist/role.object.d.ts.map +1 -1
  117. package/dist/role.object.js +6 -15
  118. package/dist/security-policy.object.d.ts +43 -0
  119. package/dist/security-policy.object.d.ts.map +1 -0
  120. package/dist/security-policy.object.js +107 -0
  121. package/dist/user.object.d.ts +6 -0
  122. package/dist/user.object.d.ts.map +1 -1
  123. package/dist/user.object.js +13 -1
  124. package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
  125. package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
  126. package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
  127. package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
  128. package/package.json +4 -4
@@ -24,6 +24,7 @@ exports.ceilingAllowsRole = ceilingAllowsRole;
24
24
  const node_crypto_1 = require("node:crypto");
25
25
  const core_1 = require("@astram/core");
26
26
  const engine_1 = require("@astram/engine");
27
+ const role_binding_js_1 = require("../authz/role-binding.js");
27
28
  /** The account id was not in the caller's subtree (unknown or cross-tenant) → 404 (no leak). */
28
29
  class ServiceAccountNotFoundError extends Error {
29
30
  constructor() {
@@ -285,20 +286,48 @@ class ServiceAccountService {
285
286
  nodes: (ids) => ids.map((id) => nodeName.get(id) ?? DELETED_NODE),
286
287
  };
287
288
  }
289
+ /**
290
+ * G-013 (spec §5.D, R2): every requested role is OWNED by the account's company — another
291
+ * company's role, an ownerless role or an unknown id is a 400 with one fixed message (no name, no
292
+ * id: the role catalog read is not caller-scoped). Applies to every admin, root included, and runs
293
+ * BEFORE the ceiling. Store errors propagate (→ 503).
294
+ */
295
+ /** The ids among `roleIds` owned by `companyUuid` (G-013), in their order. Store errors propagate. */
296
+ async ownedBy(roleIds, companyUuid) {
297
+ if (roleIds.length === 0)
298
+ return [];
299
+ const owners = new Map((await this.accounts.roleOwners(roleIds)).map((r) => [ceilingKey(r.id), r.owner]));
300
+ const company = ceilingKey(companyUuid);
301
+ return roleIds.filter((id) => {
302
+ const owner = owners.get(ceilingKey(id));
303
+ return typeof owner === 'string' && ceilingKey(owner) === company;
304
+ });
305
+ }
306
+ async assertRolesOwnedBy(roleIds, companyUuid) {
307
+ if (roleIds.length === 0)
308
+ return;
309
+ const owners = new Map((await this.accounts.roleOwners(roleIds)).map((r) => [ceilingKey(r.id), r.owner]));
310
+ const company = ceilingKey(companyUuid);
311
+ for (const id of roleIds) {
312
+ const owner = owners.get(ceilingKey(id));
313
+ if (owner === undefined || owner === null || ceilingKey(owner) !== company) {
314
+ throw new ServiceAccountInputError(role_binding_js_1.ROLE_NOT_IN_COMPANY);
315
+ }
316
+ }
317
+ }
288
318
  /**
289
319
  * What the caller may grant (the picker source for create/edit), computed from the SAME
290
- * {@link ceilingOf} the write check enforces. Root → every role + every company; non-root → the
291
- * roles they hold + the nodes of their reachable subtree. Nodes come back tree-ordered with a
292
- * `parentId` only when the parent is also assignable.
320
+ * {@link ceilingOf} the write check enforces. Roles: only those OWNED by the company a new agent
321
+ * is created in (the admin's tenant, G-013) — root gets all of them, a non-root admin those it
322
+ * also holds. Companies: root every company; non-root the nodes of its reachable subtree. Nodes
323
+ * come back tree-ordered with a `parentId` only when the parent is also assignable.
293
324
  */
294
325
  async assignable(admin) {
295
326
  const ceiling = await this.ceilingFor(admin);
296
- const [roles, nodes] = ceiling.isRoot
297
- ? await Promise.all([this.accounts.allRoles(), this.accounts.allNodes()])
298
- : await Promise.all([
299
- this.accounts.rolesByIds([...ceiling.roleIds]),
300
- this.accounts.nodesByIds([...ceiling.nodeIds]),
301
- ]);
327
+ const [roles, nodes] = await Promise.all([
328
+ this.accounts.rolesOwnedBy(admin.tenantId),
329
+ ceiling.isRoot ? this.accounts.allNodes() : this.accounts.nodesByIds([...ceiling.nodeIds]),
330
+ ]);
302
331
  return {
303
332
  // Defensive re-filter through the same predicates (a store that over-returns can't widen it).
304
333
  roles: roles
@@ -337,6 +366,8 @@ class ServiceAccountService {
337
366
  ? uniqueIds(admin.scopeSubtreeNodeIds)
338
367
  : [admin.tenantId];
339
368
  const roleIds = requestIds(input.roleIds ?? [], 'roleIds');
369
+ // G-013: the agent is created in the admin's tenant, so its roles must be that company's own.
370
+ await this.assertRolesOwnedBy(roleIds, admin.tenantId);
340
371
  await this.assertWithinCeiling(await this.ceilingFor(admin), scopeSubtree, roleIds);
341
372
  const id = (0, node_crypto_1.randomUUID)();
342
373
  const createdAtMs = this.now();
@@ -399,6 +430,11 @@ class ServiceAccountService {
399
430
  const existing = await this.accounts.getById(id, scopeOf(admin));
400
431
  if (existing === null)
401
432
  throw new ServiceAccountNotFoundError();
433
+ // G-013: every REQUESTED role is the account's company's own (400, every admin), before the
434
+ // ceiling. Preserved out-of-ceiling roles come from the stored row and are not re-judged.
435
+ if (requestedRoles !== undefined) {
436
+ await this.assertRolesOwnedBy(requestedRoles, existing.tenantId);
437
+ }
402
438
  // Re-check the ceiling for whatever is REQUESTED (an edit can't escalate either).
403
439
  const ceiling = await this.ceilingFor(admin);
404
440
  await this.assertWithinCeiling(ceiling, requestedScope ?? [], requestedRoles ?? [], {
@@ -410,8 +446,10 @@ class ServiceAccountService {
410
446
  const preservedScope = requestedScope !== undefined && !ceiling.isRoot
411
447
  ? existing.scopeSubtreeNodeIds.filter((n) => !ceilingAllowsNode(ceiling, n))
412
448
  : [];
449
+ // G-013: a preserved role must still be owned by the agent's company; one that is not (e.g. its
450
+ // company was deleted, leaving it ownerless) is dropped, never carried forward.
413
451
  const preservedRoles = requestedRoles !== undefined && !ceiling.isRoot
414
- ? existing.roleIds.filter((r) => !ceilingAllowsRole(ceiling, r))
452
+ ? await this.ownedBy(existing.roleIds.filter((r) => !ceilingAllowsRole(ceiling, r)), existing.tenantId)
415
453
  : [];
416
454
  // Strictly after the row's last edit, so two edits in the same millisecond still differ and the
417
455
  // optimistic guard below can tell them apart.
@@ -60,7 +60,17 @@ export declare class ServiceAccountStore {
60
60
  constructor(pool: Pool, schemas?: IdentitySchemaNames);
61
61
  /** Roles by id (unknown ids are simply absent — the caller supplies a fallback label). */
62
62
  rolesByIds(ids: readonly string[]): Promise<RoleOption[]>;
63
- /** Every role in the catalog (ROOT's assignable set). */
63
+ /**
64
+ * G-013: the owner company of each role id that exists (`null` = ownerless legacy row). Unknown ids
65
+ * are absent. The ids come back as STORED (the caller compares with `sameUuid`).
66
+ */
67
+ roleOwners(ids: readonly string[]): Promise<Array<{
68
+ id: string;
69
+ owner: string | null;
70
+ }>>;
71
+ /** G-013: the roles OWNED by one company (an agent of that company may hold only these). */
72
+ rolesOwnedBy(companyUuid: string): Promise<RoleOption[]>;
73
+ /** Every role in the catalog. */
64
74
  allRoles(): Promise<RoleOption[]>;
65
75
  /** Companies (scope nodes) by id, with their parent handle. Unknown ids are absent. */
66
76
  nodesByIds(ids: readonly string[]): Promise<ScopeNodeOption[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"service-account.store.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service-account.store.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEpF,+FAA+F;AAC/F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAUD,kGAAkG;AAClG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,qGAAqG;AACrG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AA2DD,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,iGAAiG;AACjG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAOD,qBAAa,mBAAmB;IAO5B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAP1B;;;;OAIG;gBAEgB,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,mBAA+C;IAO3E,0FAA0F;IACpF,UAAU,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAU/D;;;OAGG;IACG,UAAU,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAa9F,4FAA4F;IACtF,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAQ9D,iCAAiC;IAC3B,QAAQ,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAQvC,uFAAuF;IACjF,UAAU,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAUpE,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAQ5C;;;;;OAKG;IACG,WAAW,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAwBjF,2EAA2E;IACrE,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAS7D,kGAAkG;IAC5F,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IASjF;;;;OAIG;IACG,MAAM,CAAC,KAAK,EAAE;QAClB,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;QACvC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;QAC3B,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBjB;;;;OAIG;IACG,MAAM,CACV,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACxC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,EACD,WAAW,EAAE,MAAM;IACnB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,GAClC,OAAO,CAAC,MAAM,CAAC;CAgCnB"}
@@ -72,7 +72,26 @@ class ServiceAccountStore {
72
72
  const rows = (await this.pool.query(`SELECT r_uuid AS id, r_name AS name FROM \`${this.schemas.app}\`.\`roles\` WHERE r_uuid IN (${u.map(() => '?').join(', ')}) ORDER BY r_name`, u));
73
73
  return rows.map((r) => ({ id: r.id, name: r.name }));
74
74
  }
75
- /** Every role in the catalog (ROOT's assignable set). */
75
+ /**
76
+ * G-013: the owner company of each role id that exists (`null` = ownerless legacy row). Unknown ids
77
+ * are absent. The ids come back as STORED (the caller compares with `sameUuid`).
78
+ */
79
+ async roleOwners(ids) {
80
+ const u = uniqueIds(ids);
81
+ if (u.length === 0)
82
+ return [];
83
+ const rows = (await this.pool.query(`SELECT r_uuid AS id, r_owner_company_uuid AS owner FROM \`${this.schemas.app}\`.\`roles\` WHERE r_uuid IN (${u.map(() => '?').join(', ')})`, u));
84
+ return rows.map((r) => ({
85
+ id: r.id,
86
+ owner: typeof r.owner === 'string' && r.owner.length > 0 ? r.owner : null,
87
+ }));
88
+ }
89
+ /** G-013: the roles OWNED by one company (an agent of that company may hold only these). */
90
+ async rolesOwnedBy(companyUuid) {
91
+ const rows = (await this.pool.query(`SELECT r_uuid AS id, r_name AS name FROM \`${this.schemas.app}\`.\`roles\` WHERE r_owner_company_uuid = ? ORDER BY r_name`, [companyUuid]));
92
+ return rows.map((r) => ({ id: r.id, name: r.name }));
93
+ }
94
+ /** Every role in the catalog. */
76
95
  async allRoles() {
77
96
  const rows = (await this.pool.query(`SELECT r_uuid AS id, r_name AS name FROM \`${this.schemas.app}\`.\`roles\` ORDER BY r_name`, []));
78
97
  return rows.map((r) => ({ id: r.id, name: r.name }));
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Auth capabilities R11 (task 18): module-internal DI tokens for the admin sign-in controls and the
3
+ * users list's `mfaStatus` (built in `IdentityFeatureModule.forRoot`). Not exported from the package.
4
+ */
5
+ /** The `MfaResetter` (guard's `MariaDbMfaResetter` over identity's runtime pool). */
6
+ export declare const IDENTITY_MFA_RESETTER: unique symbol;
7
+ /** The autocommit audit writer for `auth.unlocked_by_admin` / `auth.sessions_ended`. */
8
+ export declare const IDENTITY_SIGNIN_ADMIN_AUDIT: unique symbol;
9
+ /** The batched `mfaStatus` reader (`null` when nothing can answer). */
10
+ export declare const IDENTITY_MFA_STATUS_READER: unique symbol;
11
+ //# sourceMappingURL=signin-admin.tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signin-admin.tokens.d.ts","sourceRoot":"","sources":["../../../src/features/user/signin-admin.tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qFAAqF;AACrF,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AACrE,wFAAwF;AACxF,eAAO,MAAM,2BAA2B,eAAwC,CAAC;AACjF,uEAAuE;AACvE,eAAO,MAAM,0BAA0B,eAAuC,CAAC"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IDENTITY_MFA_STATUS_READER = exports.IDENTITY_SIGNIN_ADMIN_AUDIT = exports.IDENTITY_MFA_RESETTER = void 0;
4
+ /**
5
+ * Auth capabilities R11 (task 18): module-internal DI tokens for the admin sign-in controls and the
6
+ * users list's `mfaStatus` (built in `IdentityFeatureModule.forRoot`). Not exported from the package.
7
+ */
8
+ /** The `MfaResetter` (guard's `MariaDbMfaResetter` over identity's runtime pool). */
9
+ exports.IDENTITY_MFA_RESETTER = Symbol('IDENTITY_MFA_RESETTER');
10
+ /** The autocommit audit writer for `auth.unlocked_by_admin` / `auth.sessions_ended`. */
11
+ exports.IDENTITY_SIGNIN_ADMIN_AUDIT = Symbol('IDENTITY_SIGNIN_ADMIN_AUDIT');
12
+ /** The batched `mfaStatus` reader (`null` when nothing can answer). */
13
+ exports.IDENTITY_MFA_STATUS_READER = Symbol('IDENTITY_MFA_STATUS_READER');
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Auth capabilities R11.3 (task 18, AC-17): the `mfaStatus` of a page of users for
3
+ * `GET /identity/users[/:uuid]` — `enrolled` | `required` | `grace` | `not_required`, never device
4
+ * names, secrets or counts.
5
+ *
6
+ * Batched per page: ONE active-membership read here (`?`-bound `IN` list over the runtime pool), then
7
+ * ONE call to guard's `MfaRequirementService.statusesFor` (core `evaluateMfaRequirement`; its own
8
+ * reads are batched too, see there). Root accounts (the `rootIds` allowlist) are flagged so only
9
+ * the platform floor applies to them (D-11).
10
+ *
11
+ * Without the policy feature (guard's service `null`): `enrolled` when a confirmed device exists,
12
+ * else `not_required`, from ONE batched device read. Nothing wired at all ⇒ no reader (rows carry
13
+ * no `mfaStatus`). Errors propagate; `UserService` answers 503 (never a guessed status).
14
+ */
15
+ import type { MfaAdminStatus, MfaStatusSubject } from '@astram/guard/internal';
16
+ export type UserMfaStatus = MfaAdminStatus;
17
+ export interface UserMfaStatusReader {
18
+ statusesOf(userIds: readonly string[]): Promise<ReadonlyMap<string, UserMfaStatus>>;
19
+ }
20
+ export interface UserMfaStatusDeps {
21
+ /** A query on identity's runtime pool. */
22
+ readonly query: (sql: string, params?: unknown[]) => Promise<unknown>;
23
+ readonly schemas: {
24
+ readonly app: string;
25
+ };
26
+ readonly rootIds: readonly string[];
27
+ /** Guard's mandatory-2FA service (`SHIELD_MFA_REQUIREMENT`); `null` when the feature is off. */
28
+ readonly service: {
29
+ statusesFor(subjects: readonly MfaStatusSubject[]): Promise<ReadonlyMap<string, MfaAdminStatus>>;
30
+ } | null;
31
+ /** Guard's MariaDB source (`SHIELD_MFA_REQUIREMENT_SOURCE`), for the no-feature fallback. */
32
+ readonly devices: {
33
+ confirmedFactorsOf(principalIds: readonly string[]): Promise<ReadonlySet<string>>;
34
+ } | null;
35
+ }
36
+ /** The reader, or `null` when nothing that can answer is wired. */
37
+ export declare function createUserMfaStatusReader(deps: UserMfaStatusDeps): UserMfaStatusReader | null;
38
+ //# sourceMappingURL=user-mfa-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-mfa-status.d.ts","sourceRoot":"","sources":["../../../src/features/user/user-mfa-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/E,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;CACrF;AAED,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACtE,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE;QAChB,WAAW,CACT,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GACpC,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;KACjD,GAAG,IAAI,CAAC;IACT,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,EAAE;QAChB,kBAAkB,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;KACnF,GAAG,IAAI,CAAC;CACV;AAED,mEAAmE;AACnE,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,IAAI,CA0C7F"}
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createUserMfaStatusReader = createUserMfaStatusReader;
4
+ const http_js_1 = require("../shared/http.js");
5
+ /** The reader, or `null` when nothing that can answer is wired. */
6
+ function createUserMfaStatusReader(deps) {
7
+ const { service, devices } = deps;
8
+ if (service) {
9
+ const table = `\`${deps.schemas.app}\`.\`company_membership\``;
10
+ return {
11
+ async statusesOf(userIds) {
12
+ const ids = [...new Set(userIds.filter((i) => typeof i === 'string' && i.length > 0))];
13
+ if (ids.length === 0)
14
+ return new Map();
15
+ const rows = (await deps.query(`SELECT cm_user_uuid, cm_company_uuid FROM ${table}
16
+ WHERE cm_user_uuid IN (${ids.map(() => '?').join(', ')}) AND cm_is_active = 1`, ids));
17
+ const nodes = new Map(ids.map((i) => [i, []]));
18
+ for (const r of rows) {
19
+ if (typeof r.cm_user_uuid !== 'string' || typeof r.cm_company_uuid !== 'string')
20
+ continue;
21
+ // The stored uuid may differ in case from the page's; match it back to the page id.
22
+ const key = ids.find((i) => (0, http_js_1.sameUuid)(i, r.cm_user_uuid));
23
+ if (key !== undefined)
24
+ nodes.get(key).push(r.cm_company_uuid);
25
+ }
26
+ const subjects = ids.map((id) => ({
27
+ principalId: id,
28
+ isRoot: deps.rootIds.some((r) => (0, http_js_1.sameUuid)(r, id)),
29
+ nodeIds: nodes.get(id) ?? [],
30
+ }));
31
+ return service.statusesFor(subjects);
32
+ },
33
+ };
34
+ }
35
+ if (devices) {
36
+ return {
37
+ async statusesOf(userIds) {
38
+ const ids = [...new Set(userIds.filter((i) => typeof i === 'string' && i.length > 0))];
39
+ if (ids.length === 0)
40
+ return new Map();
41
+ const enrolled = await devices.confirmedFactorsOf(ids);
42
+ return new Map(ids.map((id) => [id, enrolled.has(id) ? 'enrolled' : 'not_required']));
43
+ },
44
+ };
45
+ }
46
+ return null;
47
+ }
@@ -0,0 +1,48 @@
1
+ import { RecordsService } from '@astram/engine';
2
+ import type { AuditEvent, FrameworkSchemas, Principal } from '@astram/core';
3
+ import { type SessionSweeper } from '@astram/guard';
4
+ import { type AccountUnlocker, type AdminStepUp, type MfaResetter } from '@astram/guard/internal';
5
+ import { GrantCeiling } from '../authz/grant-ceiling.js';
6
+ /** The three custom actions on `user` (ADR-0038). */
7
+ export type SignInAdminAction = 'reset_mfa' | 'unlock' | 'end_sessions';
8
+ /** The audit writer the Redis/session actions use (autocommit). */
9
+ export interface SignInAdminAuditWriter {
10
+ record(event: AuditEvent): Promise<void>;
11
+ }
12
+ /** The route body. */
13
+ export interface SignInAdminBody {
14
+ code?: unknown;
15
+ }
16
+ export declare const AUTH_MFA_RESET_BY_ADMIN = "auth.mfa.reset_by_admin";
17
+ export declare const AUTH_UNLOCKED_BY_ADMIN = "auth.unlocked_by_admin";
18
+ export declare const AUTH_SESSIONS_ENDED = "auth.sessions_ended";
19
+ export declare class UserSignInAdminService {
20
+ private readonly records;
21
+ private readonly ceiling;
22
+ private readonly stepUp?;
23
+ private readonly unlocker?;
24
+ private readonly sweeper?;
25
+ private readonly resetter?;
26
+ private readonly audit?;
27
+ private readonly logger;
28
+ private readonly txAudit;
29
+ constructor(records: RecordsService, ceiling: GrantCeiling, stepUp?: (AdminStepUp | null) | undefined, unlocker?: (AccountUnlocker | null) | undefined, sweeper?: (SessionSweeper | null) | undefined, resetter?: (MfaResetter | null) | undefined, audit?: (SignInAdminAuditWriter | null) | undefined, schemas?: FrameworkSchemas | null);
30
+ /** R11.1: delete the user's devices, recovery codes and grace clock; end their sessions. */
31
+ resetMfa(actor: Principal, uuid: string, body: SignInAdminBody): Promise<{
32
+ success: true;
33
+ }>;
34
+ /** R11.1: clear the user's login, MFA, recovery and CAPTCHA account counters. */
35
+ unlock(actor: Principal, uuid: string, body: SignInAdminBody): Promise<{
36
+ success: true;
37
+ }>;
38
+ /** R11.1: revoke every session of the user. */
39
+ endSessions(actor: Principal, uuid: string, body: SignInAdminBody): Promise<{
40
+ success: true;
41
+ }>;
42
+ /** Steps a–e of the class doc. */
43
+ private gate;
44
+ /** The audit row after a Redis/session action; a failed write is reported, not hidden. */
45
+ private recordAfter;
46
+ private event;
47
+ }
48
+ //# sourceMappingURL=user-signin-admin.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-signin-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user-signin-admin.service.ts"],"names":[],"mappings":"AAuCA,OAAO,EAAoB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAMzD,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,QAAQ,GAAG,cAAc,CAAC;AAExE,mEAAmE;AACnE,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,sBAAsB;AACtB,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AACjE,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAC/D,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAoBzD,qBACa,sBAAsB;IAK/B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACkB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAGlE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACkB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAGrE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IAdzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2C;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;gBAGxB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EACqB,MAAM,CAAC,GAAE,WAAW,GAAG,IAAI,aAAA,EAGrE,QAAQ,CAAC,GAAE,eAAe,GAAG,IAAI,aAAA,EACW,OAAO,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA,EAChC,QAAQ,CAAC,GAAE,WAAW,GAAG,IAAI,aAAA,EAGxE,KAAK,CAAC,GAAE,sBAAsB,GAAG,IAAI,aAAA,EACN,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI;IAKnF,4FAA4F;IACtF,QAAQ,CACZ,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAkC7B,iFAAiF;IAC3E,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAa/F,+CAA+C;IACzC,WAAW,CACf,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAa7B,kCAAkC;YACpB,IAAI;IAqClB,0FAA0F;YAC5E,WAAW;IASzB,OAAO,CAAC,KAAK;CAWd"}
@@ -0,0 +1,232 @@
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
+ var UserSignInAdminService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.UserSignInAdminService = exports.AUTH_SESSIONS_ENDED = exports.AUTH_UNLOCKED_BY_ADMIN = exports.AUTH_MFA_RESET_BY_ADMIN = void 0;
17
+ /**
18
+ * Auth capabilities R11.1, R11.2 (task 18, AC-16): admin sign-in controls on one user.
19
+ *
20
+ * POST /identity/users/:uuid/reset-mfa `identity:user:reset_mfa`
21
+ * POST /identity/users/:uuid/unlock `identity:user:unlock`
22
+ * POST /identity/users/:uuid/end-sessions `identity:user:end_sessions`
23
+ * body `{ code }`: the ADMIN's own current TOTP code.
24
+ *
25
+ * Order (fail closed, nothing acts before every check passed):
26
+ * a. not on yourself (403): your own sign-in is managed under `/account/*` (devices, sessions),
27
+ * and an admin route must never be a way around the self-service step-ups;
28
+ * b. every port the action needs is wired, else 503 before the code is spent;
29
+ * c. step-up (R11.2): the admin's own TOTP on the admin's `mfa:pid` budget through guard's
30
+ * replay-guarded verifier (`SHIELD_ADMIN_STEP_UP`). No confirmed device ⇒ 409 `mfa_required`;
31
+ * no code ⇒ 400 `mfa_required`; wrong code ⇒ 400 `invalid_request`; budget spent ⇒ 429
32
+ * `too_many_requests`; store down ⇒ 503 `authentication_unavailable` (the `/account` step-up
33
+ * answers, with 409 for "set up 2FA first");
34
+ * d. record-level `authorize('user', uuid, <action>)` (ADR-0038): a user outside the admin's
35
+ * scope (another tenant) or a missing grant is the engine's 403;
36
+ * e. `GrantCeiling.assertDominates(actor, uuid)` (403, fixed message, no ids);
37
+ * f. act, then audit (`admin_action`; actor = admin, tenant = the admin's active tenant, resource
38
+ * = the user record, `attributes.userUuid` as the invite row has it):
39
+ * - reset-mfa (review round 1, item B): every session is revoked FIRST (sweeper); a sweep
40
+ * failure is a 503 with nothing deleted. Then guard's `MariaDbMfaResetter` deletes devices,
41
+ * recovery codes and the grace clock and writes `auth.mfa.reset_by_admin` on the SAME
42
+ * transaction (runtime pool, the identity tables are not the engine's); a failure there is
43
+ * `partially_applied` (sessions ended, 2FA not reset). A closing sweep then ends any session
44
+ * signed in with an old device in between; its failure is `partially_applied` too. The user
45
+ * re-enrols at next sign-in when a policy requires 2FA (restricted session, R2.3);
46
+ * - unlock: guard's unlocker clears `login:pid`, `mfa:pid`, `recovery:` and the CAPTCHA account
47
+ * counters (Redis), then `auth.unlocked_by_admin` (autocommit sink);
48
+ * - end-sessions: sweeper `revokeAllForPrincipal`, then `auth.sessions_ended` (autocommit).
49
+ * Any store error ⇒ 503 with a fixed body. When part of the action was applied but a later step
50
+ * failed (reset-mfa: sessions ended but the reset failed, or the closing sweep failed; the audit
51
+ * row after a Redis/session action), the 503
52
+ * says so: `{ error: 'partially_applied', message }`. Every action is idempotent, so a retry is
53
+ * safe.
54
+ */
55
+ const common_1 = require("@nestjs/common");
56
+ const engine_1 = require("@astram/engine");
57
+ const guard_1 = require("@astram/guard");
58
+ const internal_1 = require("@astram/guard/internal");
59
+ const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
60
+ const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
61
+ const http_js_1 = require("../shared/http.js");
62
+ const identity_errors_js_1 = require("../shared/identity-errors.js");
63
+ const signin_admin_tokens_js_1 = require("./signin-admin.tokens.js");
64
+ exports.AUTH_MFA_RESET_BY_ADMIN = 'auth.mfa.reset_by_admin';
65
+ exports.AUTH_UNLOCKED_BY_ADMIN = 'auth.unlocked_by_admin';
66
+ exports.AUTH_SESSIONS_ENDED = 'auth.sessions_ended';
67
+ const MSG_SELF = 'Use your account settings to manage your own sign-in.';
68
+ const MSG_UNAVAILABLE = 'This action is temporarily unavailable. Nothing was changed. Try again.';
69
+ const MSG_RESET_NOT_STARTED = "The user's sessions could not be ended, so two-step verification was not reset. Try again.";
70
+ const MSG_RESET_NOT_APPLIED = "The user's sessions were ended, but two-step verification could not be reset. Try again.";
71
+ const MSG_RESET_SESSIONS = "Two-step verification was reset, but the user's sessions could not be ended. Use End sessions to retry.";
72
+ const MSG_NOT_RECORDED = 'The action was applied but could not be recorded. Try again.';
73
+ function httpError(status, error) {
74
+ throw new common_1.HttpException({ error }, status);
75
+ }
76
+ function partiallyApplied(message) {
77
+ throw new common_1.HttpException({ error: 'partially_applied', message }, common_1.HttpStatus.SERVICE_UNAVAILABLE);
78
+ }
79
+ let UserSignInAdminService = UserSignInAdminService_1 = class UserSignInAdminService {
80
+ records;
81
+ ceiling;
82
+ stepUp;
83
+ unlocker;
84
+ sweeper;
85
+ resetter;
86
+ audit;
87
+ logger = new common_1.Logger(UserSignInAdminService_1.name);
88
+ txAudit;
89
+ constructor(records, ceiling, stepUp, unlocker, sweeper, resetter, audit, schemas) {
90
+ this.records = records;
91
+ this.ceiling = ceiling;
92
+ this.stepUp = stepUp;
93
+ this.unlocker = unlocker;
94
+ this.sweeper = sweeper;
95
+ this.resetter = resetter;
96
+ this.audit = audit;
97
+ this.txAudit = new engine_1.MariaDbAuditSink(schemas ? { schema: schemas.audit } : {});
98
+ }
99
+ /** R11.1: delete the user's devices, recovery codes and grace clock; end their sessions. */
100
+ async resetMfa(actor, uuid, body) {
101
+ const resetter = this.resetter;
102
+ const sweeper = this.sweeper;
103
+ await this.gate(actor, uuid, 'reset_mfa', body, resetter != null && sweeper != null);
104
+ // Review round 1, item B: end every session FIRST, so no live session outlasts the reset and a
105
+ // sweep failure leaves nothing deleted.
106
+ try {
107
+ await sweeper.revokeAllForPrincipal(uuid);
108
+ }
109
+ catch {
110
+ this.logger.warn('admin 2FA reset refused: the session sweep failed; nothing was deleted');
111
+ throw new identity_errors_js_1.IdentityUnavailableError(MSG_RESET_NOT_STARTED);
112
+ }
113
+ try {
114
+ await resetter.reset({
115
+ principalId: uuid,
116
+ withinTransaction: (query) => this.txAudit.record(this.event(actor, uuid, exports.AUTH_MFA_RESET_BY_ADMIN), (sql, params) => query(sql, params)),
117
+ });
118
+ }
119
+ catch {
120
+ this.logger.warn('admin 2FA reset: sessions ended, but the reset could not be applied');
121
+ partiallyApplied(MSG_RESET_NOT_APPLIED);
122
+ }
123
+ // Closing sweep: a sign-in with an old device between the first sweep and the commit.
124
+ try {
125
+ await sweeper.revokeAllForPrincipal(uuid);
126
+ }
127
+ catch {
128
+ this.logger.warn('admin 2FA reset applied, but the closing session sweep failed');
129
+ partiallyApplied(MSG_RESET_SESSIONS);
130
+ }
131
+ return { success: true };
132
+ }
133
+ /** R11.1: clear the user's login, MFA, recovery and CAPTCHA account counters. */
134
+ async unlock(actor, uuid, body) {
135
+ const unlocker = this.unlocker;
136
+ await this.gate(actor, uuid, 'unlock', body, unlocker != null && this.audit != null);
137
+ try {
138
+ await unlocker.unlock(uuid);
139
+ }
140
+ catch {
141
+ this.logger.warn('admin unlock could not clear the counters (store error)');
142
+ throw new identity_errors_js_1.IdentityUnavailableError(MSG_UNAVAILABLE);
143
+ }
144
+ await this.recordAfter(actor, uuid, exports.AUTH_UNLOCKED_BY_ADMIN);
145
+ return { success: true };
146
+ }
147
+ /** R11.1: revoke every session of the user. */
148
+ async endSessions(actor, uuid, body) {
149
+ const sweeper = this.sweeper;
150
+ await this.gate(actor, uuid, 'end_sessions', body, sweeper != null && this.audit != null);
151
+ try {
152
+ await sweeper.revokeAllForPrincipal(uuid);
153
+ }
154
+ catch {
155
+ this.logger.warn('admin end-sessions could not revoke the sessions (store error)');
156
+ throw new identity_errors_js_1.IdentityUnavailableError(MSG_UNAVAILABLE);
157
+ }
158
+ await this.recordAfter(actor, uuid, exports.AUTH_SESSIONS_ENDED);
159
+ return { success: true };
160
+ }
161
+ /** Steps a–e of the class doc. */
162
+ async gate(actor, uuid, action, body, wired) {
163
+ if ((0, http_js_1.sameUuid)(actor.id, uuid))
164
+ throw new identity_errors_js_1.IdentityForbiddenError(MSG_SELF);
165
+ const stepUp = this.stepUp;
166
+ if (stepUp == null || !wired) {
167
+ this.logger.warn(`admin sign-in control ${action} refused: a required port is not wired`);
168
+ throw new identity_errors_js_1.IdentityUnavailableError(MSG_UNAVAILABLE);
169
+ }
170
+ const code = typeof body?.code === 'string' ? body.code : '';
171
+ const outcome = await stepUp.verify(actor.id, code);
172
+ switch (outcome) {
173
+ case 'verified':
174
+ break;
175
+ case 'no_factor':
176
+ httpError(common_1.HttpStatus.CONFLICT, 'mfa_required');
177
+ break;
178
+ case 'missing':
179
+ httpError(common_1.HttpStatus.BAD_REQUEST, 'mfa_required');
180
+ break;
181
+ case 'rejected':
182
+ httpError(common_1.HttpStatus.BAD_REQUEST, 'invalid_request');
183
+ break;
184
+ case 'locked':
185
+ httpError(common_1.HttpStatus.TOO_MANY_REQUESTS, 'too_many_requests');
186
+ break;
187
+ default:
188
+ httpError(common_1.HttpStatus.SERVICE_UNAVAILABLE, 'authentication_unavailable');
189
+ }
190
+ await this.records.authorize('user', uuid, action);
191
+ await this.ceiling.assertDominates(actor, uuid);
192
+ }
193
+ /** The audit row after a Redis/session action; a failed write is reported, not hidden. */
194
+ async recordAfter(actor, uuid, key) {
195
+ try {
196
+ await this.audit.record(this.event(actor, uuid, key));
197
+ }
198
+ catch {
199
+ this.logger.warn(`${key} applied, but its audit row could not be written`);
200
+ partiallyApplied(MSG_NOT_RECORDED);
201
+ }
202
+ }
203
+ event(actor, uuid, key) {
204
+ return {
205
+ type: 'admin_action',
206
+ occurredAtMs: Date.now(),
207
+ actorId: actor.id,
208
+ tenantId: actor.tenantId,
209
+ action: { key },
210
+ resource: { level: 'record', typeKey: 'user', tenantId: actor.tenantId, recordId: uuid },
211
+ attributes: { userUuid: uuid },
212
+ };
213
+ }
214
+ };
215
+ exports.UserSignInAdminService = UserSignInAdminService;
216
+ exports.UserSignInAdminService = UserSignInAdminService = UserSignInAdminService_1 = __decorate([
217
+ (0, common_1.Injectable)(),
218
+ __param(2, (0, common_1.Optional)()),
219
+ __param(2, (0, common_1.Inject)(internal_1.SHIELD_ADMIN_STEP_UP)),
220
+ __param(3, (0, common_1.Optional)()),
221
+ __param(3, (0, common_1.Inject)(internal_1.SHIELD_ACCOUNT_UNLOCKER)),
222
+ __param(4, (0, common_1.Optional)()),
223
+ __param(4, (0, common_1.Inject)(guard_1.SHIELD_SESSION_SWEEPER)),
224
+ __param(5, (0, common_1.Optional)()),
225
+ __param(5, (0, common_1.Inject)(signin_admin_tokens_js_1.IDENTITY_MFA_RESETTER)),
226
+ __param(6, (0, common_1.Optional)()),
227
+ __param(6, (0, common_1.Inject)(signin_admin_tokens_js_1.IDENTITY_SIGNIN_ADMIN_AUDIT)),
228
+ __param(7, (0, common_1.Optional)()),
229
+ __param(7, (0, common_1.Inject)(framework_schemas_token_js_1.IDENTITY_FRAMEWORK_SCHEMAS)),
230
+ __metadata("design:paramtypes", [engine_1.RecordsService,
231
+ grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object])
232
+ ], UserSignInAdminService);
@@ -1,11 +1,13 @@
1
1
  import { UserService } from './user.service.js';
2
2
  import { InviteService, type InviteResult } from './invite.service.js';
3
+ import { UserSignInAdminService, type SignInAdminBody } from './user-signin-admin.service.js';
3
4
  import { type ListQueryDto } from '../shared/list-query.js';
4
5
  import type { AuthedRequest } from '../shared/http.js';
5
6
  export declare class UserController {
6
7
  private readonly users;
7
8
  private readonly invites?;
8
- constructor(users: UserService, invites?: InviteService | undefined);
9
+ private readonly signInAdmin?;
10
+ constructor(users: UserService, invites?: InviteService | undefined, signInAdmin?: UserSignInAdminService | undefined);
9
11
  /**
10
12
  * List users — gated on `identity:user:read` (see class doc). Server-side DataTable:
11
13
  * `?page&limit&search&sortBy&sortOrder`. Returns `{ total, rows, page, limit }` —
@@ -25,15 +27,20 @@ export declare class UserController {
25
27
  username?: string;
26
28
  email?: string;
27
29
  companyUuid?: string;
30
+ loginName?: string | null;
28
31
  }): Promise<import("./user.service.js").CreateUserResult>;
29
32
  /** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
30
33
  * administrator, may read users). */
31
34
  getUser(uuid: string): Promise<import("./user.service.js").UserRecord | null>;
32
- /** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
35
+ /**
36
+ * Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` and
37
+ * `loginName` (R1.7; `null` clears; needs dominance) apply.
38
+ */
33
39
  updateUser(req: AuthedRequest, uuid: string, body: {
34
40
  username?: string;
35
41
  email?: string;
36
42
  status?: string;
43
+ loginName?: string | null;
37
44
  }): Promise<{
38
45
  success: boolean;
39
46
  }>;
@@ -44,6 +51,26 @@ export declare class UserController {
44
51
  * 409 not locked, 429 throttled, 501 no delivery channel, 503 send failed.
45
52
  */
46
53
  inviteUser(req: AuthedRequest, uuid: string): Promise<InviteResult>;
54
+ /**
55
+ * Auth capabilities R11 (task 18): admin sign-in controls, each a custom action on `user`
56
+ * (ADR-0038) with the admin's own TOTP in the body `{ code }`, record-level check and dominance in
57
+ * the service. 200 `{ success: true }`; 400 `{error:'mfa_required'}` no code, 400
58
+ * `{error:'invalid_request'}` wrong code, 409 `{error:'mfa_required'}` admin has no 2FA, 403 not
59
+ * allowed (yourself, no grant, other tenant, not dominated), 429 step-up budget, 503 unavailable
60
+ * (`{error:'partially_applied', message}` when the action was applied but a later step failed).
61
+ */
62
+ resetUserMfa(req: AuthedRequest, uuid: string, body: SignInAdminBody): Promise<{
63
+ success: true;
64
+ }>;
65
+ /** R11: clear the user's sign-in lockout counters — `identity:user:unlock` (see above). */
66
+ unlockUser(req: AuthedRequest, uuid: string, body: SignInAdminBody): Promise<{
67
+ success: true;
68
+ }>;
69
+ /** R11: end every session of the user — `identity:user:end_sessions` (see above). */
70
+ endUserSessions(req: AuthedRequest, uuid: string, body: SignInAdminBody): Promise<{
71
+ success: true;
72
+ }>;
73
+ private signInAdminService;
47
74
  /** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
48
75
  deleteUser(req: AuthedRequest, uuid: string): Promise<{
49
76
  success: boolean;