@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
@@ -1 +1 @@
1
- {"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;gBADpB,KAAK,EAAE,WAAW,EACN,OAAO,CAAC,EAAE,aAAa,YAAA;IAGtD;;;;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,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IAY3E;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,iGAAiG;IAG3F,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAUtE;;;;;OAKG;IAIH,UAAU,CACD,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,GACpC,OAAO,CAAC,YAAY,CAAC;IAKxB,0FAA0F;IAGpF,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
1
+ {"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,KAAK,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAC9F,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAFxB,KAAK,EAAE,WAAW,EACN,OAAO,CAAC,EAAE,aAAa,YAAA,EACvB,WAAW,CAAC,EAAE,sBAAsB,YAAA;IAGnE;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EAEzB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAc9F;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAErC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;IAWzF;;;;;OAKG;IAIH,UAAU,CACD,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,GACpC,OAAO,CAAC,YAAY,CAAC;IAKxB;;;;;;;OAOG;IAIH,YAAY,CACH,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,eAAe,GAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAI7B,2FAA2F;IAI3F,UAAU,CACD,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,eAAe,GAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAI7B,qFAAqF;IAIrF,eAAe,CACN,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,eAAe,GAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC;IAI7B,OAAO,CAAC,kBAAkB;IAK1B,0FAA0F;IAGpF,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
@@ -17,6 +17,7 @@ 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 invite_service_js_1 = require("./invite.service.js");
20
+ const user_signin_admin_service_js_1 = require("./user-signin-admin.service.js");
20
21
  const list_query_js_1 = require("../shared/list-query.js");
21
22
  const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
22
23
  // User management is gated on `identity:user:<action>` grants. `user` is CONTROL-plane
@@ -26,9 +27,11 @@ const identity_error_interceptor_js_1 = require("../shared/identity-error.interc
26
27
  let UserController = class UserController {
27
28
  users;
28
29
  invites;
29
- constructor(users, invites) {
30
+ signInAdmin;
31
+ constructor(users, invites, signInAdmin) {
30
32
  this.users = users;
31
33
  this.invites = invites;
34
+ this.signInAdmin = signInAdmin;
32
35
  }
33
36
  /**
34
37
  * List users — gated on `identity:user:read` (see class doc). Server-side DataTable:
@@ -51,6 +54,8 @@ let UserController = class UserController {
51
54
  // KI #94: the template form sends it; the service canonicalises and validates it.
52
55
  ...(typeof body?.email === 'string' ? { email: body.email } : {}),
53
56
  ...(companyUuid !== undefined ? { companyUuid } : {}),
57
+ // R1.7: optional login name (validated by the service; admin-only route).
58
+ ...(body?.loginName !== undefined ? { loginName: body.loginName } : {}),
54
59
  });
55
60
  return result;
56
61
  }
@@ -59,12 +64,16 @@ let UserController = class UserController {
59
64
  getUser(uuid) {
60
65
  return this.users.getUser(uuid);
61
66
  }
62
- /** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
67
+ /**
68
+ * Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` and
69
+ * `loginName` (R1.7; `null` clears; needs dominance) apply.
70
+ */
63
71
  async updateUser(req, uuid, body) {
64
72
  await this.users.updateUser(req.principal, uuid, {
65
73
  username: body?.username,
66
74
  email: body?.email,
67
75
  status: body?.status,
76
+ loginName: body?.loginName,
68
77
  });
69
78
  return { success: true };
70
79
  }
@@ -79,6 +88,30 @@ let UserController = class UserController {
79
88
  throw new Error('UserController: InviteService is not wired');
80
89
  return this.invites.invite(req.principal, uuid);
81
90
  }
91
+ /**
92
+ * Auth capabilities R11 (task 18): admin sign-in controls, each a custom action on `user`
93
+ * (ADR-0038) with the admin's own TOTP in the body `{ code }`, record-level check and dominance in
94
+ * the service. 200 `{ success: true }`; 400 `{error:'mfa_required'}` no code, 400
95
+ * `{error:'invalid_request'}` wrong code, 409 `{error:'mfa_required'}` admin has no 2FA, 403 not
96
+ * allowed (yourself, no grant, other tenant, not dominated), 429 step-up budget, 503 unavailable
97
+ * (`{error:'partially_applied', message}` when the action was applied but a later step failed).
98
+ */
99
+ resetUserMfa(req, uuid, body) {
100
+ return this.signInAdminService().resetMfa(req.principal, uuid, body);
101
+ }
102
+ /** R11: clear the user's sign-in lockout counters — `identity:user:unlock` (see above). */
103
+ unlockUser(req, uuid, body) {
104
+ return this.signInAdminService().unlock(req.principal, uuid, body);
105
+ }
106
+ /** R11: end every session of the user — `identity:user:end_sessions` (see above). */
107
+ endUserSessions(req, uuid, body) {
108
+ return this.signInAdminService().endSessions(req.principal, uuid, body);
109
+ }
110
+ signInAdminService() {
111
+ if (!this.signInAdmin)
112
+ throw new Error('UserController: UserSignInAdminService is not wired');
113
+ return this.signInAdmin;
114
+ }
82
115
  /** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
83
116
  async deleteUser(req, uuid) {
84
117
  await this.users.deleteUser(req.principal, uuid);
@@ -131,6 +164,39 @@ __decorate([
131
164
  __metadata("design:paramtypes", [Object, String]),
132
165
  __metadata("design:returntype", Promise)
133
166
  ], UserController.prototype, "inviteUser", null);
167
+ __decorate([
168
+ (0, common_1.Post)('users/:uuid/reset-mfa'),
169
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
170
+ (0, guard_1.Classified)({ typeKey: 'user', action: 'reset_mfa' }),
171
+ __param(0, (0, common_1.Req)()),
172
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
173
+ __param(2, (0, common_1.Body)()),
174
+ __metadata("design:type", Function),
175
+ __metadata("design:paramtypes", [Object, String, Object]),
176
+ __metadata("design:returntype", Promise)
177
+ ], UserController.prototype, "resetUserMfa", null);
178
+ __decorate([
179
+ (0, common_1.Post)('users/:uuid/unlock'),
180
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
181
+ (0, guard_1.Classified)({ typeKey: 'user', action: 'unlock' }),
182
+ __param(0, (0, common_1.Req)()),
183
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
184
+ __param(2, (0, common_1.Body)()),
185
+ __metadata("design:type", Function),
186
+ __metadata("design:paramtypes", [Object, String, Object]),
187
+ __metadata("design:returntype", Promise)
188
+ ], UserController.prototype, "unlockUser", null);
189
+ __decorate([
190
+ (0, common_1.Post)('users/:uuid/end-sessions'),
191
+ (0, common_1.HttpCode)(common_1.HttpStatus.OK),
192
+ (0, guard_1.Classified)({ typeKey: 'user', action: 'end_sessions' }),
193
+ __param(0, (0, common_1.Req)()),
194
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
195
+ __param(2, (0, common_1.Body)()),
196
+ __metadata("design:type", Function),
197
+ __metadata("design:paramtypes", [Object, String, Object]),
198
+ __metadata("design:returntype", Promise)
199
+ ], UserController.prototype, "endUserSessions", null);
134
200
  __decorate([
135
201
  (0, common_1.Delete)('users/:uuid'),
136
202
  (0, guard_1.Classified)({ typeKey: 'user', action: 'delete' }),
@@ -144,6 +210,8 @@ exports.UserController = UserController = __decorate([
144
210
  (0, common_1.Controller)('identity'),
145
211
  (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
146
212
  __param(1, (0, common_1.Optional)()),
213
+ __param(2, (0, common_1.Optional)()),
147
214
  __metadata("design:paramtypes", [user_service_js_1.UserService,
148
- invite_service_js_1.InviteService])
215
+ invite_service_js_1.InviteService,
216
+ user_signin_admin_service_js_1.UserSignInAdminService])
149
217
  ], UserController);
@@ -1,9 +1,10 @@
1
1
  import { RecordsService, type RecordId, type RecordView } from '@astram/engine';
2
- import type { FrameworkSchemas, Principal } from '@astram/core';
2
+ import { type FrameworkSchemas, type Principal } from '@astram/core';
3
3
  import { type EmailAddressChanger, type CredentialProvisioner, type EmailSender, type MessageDispatcher, type ResetTokenStore, type SessionSweeper } from '@astram/guard';
4
4
  import { type PostingVersionBump } from '../membership/posting-version-bump.js';
5
5
  import { type ListParams } from '../shared/list-query.js';
6
6
  import { GrantCeiling } from '../authz/grant-ceiling.js';
7
+ import type { UserMfaStatus, UserMfaStatusReader } from './user-mfa-status.js';
7
8
  /** The outcome of {@link UserService.createUserWithCredential}. */
8
9
  export type CreateUserResult = {
9
10
  id: RecordId;
@@ -18,15 +19,29 @@ export type CreateUserResult = {
18
19
  * locked → `pending` (invite-able), usable → `active`, none → `none`. Never any credential data.
19
20
  */
20
21
  export type SignInStatus = 'pending' | 'active' | 'none';
21
- /** A user record as `GET /identity/users[/:uuid]` returns it: the engine row plus `signInStatus`. */
22
+ /**
23
+ * A user record as `GET /identity/users[/:uuid]` returns it: the engine row plus `signInStatus` and
24
+ * (auth capabilities R11.3) `mfaStatus` — never device names, secrets or counts.
25
+ */
22
26
  export type UserRecord = RecordView & {
23
27
  readonly signInStatus?: SignInStatus;
28
+ readonly mfaStatus?: UserMfaStatus;
24
29
  };
25
30
  /** The patch keys a generic user update accepts (security hardening #3). */
26
31
  export interface UserPatch {
27
32
  username?: unknown;
28
33
  email?: unknown;
29
34
  status?: unknown;
35
+ /** R1.7: the login name (`null` clears); admin-only, needs dominance. */
36
+ loginName?: unknown;
37
+ }
38
+ /** The input of {@link UserService.createUserWithCredential}. */
39
+ export interface CreateUserInput {
40
+ username: string;
41
+ email?: string | undefined;
42
+ companyUuid?: string | undefined;
43
+ /** R1.7: optional login name (normalized and validated; blank ⇒ none). */
44
+ loginName?: string | null | undefined;
30
45
  }
31
46
  /**
32
47
  * UserService — user (principal) CRUD on the paved road, via the framework's RecordsService facade
@@ -57,6 +72,7 @@ export declare class UserService {
57
72
  private readonly emailAddresses?;
58
73
  private readonly provisioner?;
59
74
  private readonly dispatcher?;
75
+ private readonly mfaStatus?;
60
76
  private readonly logger;
61
77
  /**
62
78
  * The engine's audit sink, used on the email helper's transaction (no pool of its own), on the
@@ -64,16 +80,12 @@ export declare class UserService {
64
80
  * service) ⇒ the sink resolves `SHIELD_DB_NAMESPACE` itself.
65
81
  */
66
82
  private readonly emailAudit;
67
- constructor(records: RecordsService, ceiling: GrantCeiling, resetTokens?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, sessions?: (SessionSweeper | null) | undefined, versionBump?: (PostingVersionBump | null) | undefined, emailAddresses?: (EmailAddressChanger | null) | undefined, schemas?: FrameworkSchemas | null, provisioner?: (CredentialProvisioner | null) | undefined, dispatcher?: (MessageDispatcher | null) | undefined);
83
+ constructor(records: RecordsService, ceiling: GrantCeiling, resetTokens?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, sessions?: (SessionSweeper | null) | undefined, versionBump?: (PostingVersionBump | null) | undefined, emailAddresses?: (EmailAddressChanger | null) | undefined, schemas?: FrameworkSchemas | null, provisioner?: (CredentialProvisioner | null) | undefined, dispatcher?: (MessageDispatcher | null) | undefined, mfaStatus?: (UserMfaStatusReader | null) | undefined);
68
84
  /**
69
85
  * Create a user; returns its id. Delegates to {@link createUserWithCredential} (same behaviour;
70
86
  * the detailed credential outcome is dropped to keep this public signature stable).
71
87
  */
72
- createUser(actor: Principal, input: {
73
- username: string;
74
- email?: string | undefined;
75
- companyUuid?: string | undefined;
76
- }): Promise<RecordId>;
88
+ createUser(actor: Principal, input: CreateUserInput): Promise<RecordId>;
77
89
  /**
78
90
  * Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
79
91
  * ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
@@ -89,11 +101,7 @@ export declare class UserService {
89
101
  * transaction's own connection; a provisioner failure rolls everything back. Otherwise no
90
102
  * credential is created and the result says why (no email takes precedence).
91
103
  */
92
- createUserWithCredential(actor: Principal, input: {
93
- username: string;
94
- email?: string | undefined;
95
- companyUuid?: string | undefined;
96
- }): Promise<CreateUserResult>;
104
+ createUserWithCredential(actor: Principal, input: CreateUserInput): Promise<CreateUserResult>;
97
105
  /**
98
106
  * List users (tenant-scoped by the pipe), with paging/search/sort. Each row gains a top-level
99
107
  * `signInStatus` (T10), from ONE batched credential read for the page.
@@ -104,10 +112,23 @@ export declare class UserService {
104
112
  }>;
105
113
  /** Read a single user, with its top-level `signInStatus` (T10). */
106
114
  getUser(uuid: string): Promise<UserRecord | null>;
107
- /** One `statesOf` call for the given rows; no provisioner (hand-built service) ⇒ rows unchanged. */
115
+ /**
116
+ * One `statesOf` call for the given rows (no provisioner, a hand-built service ⇒ no
117
+ * `signInStatus`), and one batched `mfaStatus` read (R11.3; no reader ⇒ no `mfaStatus`).
118
+ */
108
119
  private withSignInStatus;
120
+ /** R11.3: the page's `mfaStatus`; a store error is a fixed 503, never a guessed value. */
121
+ private mfaStatusesOf;
109
122
  /** Update a user (whitelisted patch; username/email/status changes need dominance — class doc). */
110
123
  updateUser(actor: Principal, uuid: string, patch: UserPatch): Promise<void>;
124
+ /**
125
+ * Review round 1, item F: a login name must not equal another principal's legacy sign-in hint
126
+ * (`identity_credential.ic_identity_hint`, compared under the column's case-insensitive
127
+ * collation); the sign-in lookup would then match two accounts. Same fixed 409 as a login-name
128
+ * clash (no new oracle). Read on the write's transaction through guard's credential provisioner;
129
+ * a hand-built service without one (or a custom provisioner without `hintHolderOf`) skips it.
130
+ */
131
+ private assertLoginNameNotAHint;
111
132
  /**
112
133
  * Phase 12 (KI #14): write a dominated user's new (canonical) email through guard's
113
134
  * email-change helper — ONE transaction that locks the user and credential rows, refuses a
@@ -116,6 +137,11 @@ export declare class UserService {
116
137
  * (engine shape, fail closed). Reset links are burned BEFORE (a failure aborts the change).
117
138
  */
118
139
  private changeEmail;
140
+ /**
141
+ * R1.7 / R12.2: the `auth.handle.changed` row (type `authentication`) on the write's own
142
+ * transaction. Actor and target only: no login name (old or new) is written.
143
+ */
144
+ private auditHandleChanged;
119
145
  /** The helper call with the audit row on its transaction (see {@link changeEmail}). */
120
146
  private applyEmailChange;
121
147
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"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"}
@@ -17,14 +17,17 @@ exports.UserService = void 0;
17
17
  const common_1 = require("@nestjs/common");
18
18
  const engine_1 = require("@astram/engine");
19
19
  const internal_1 = require("@astram/engine/internal");
20
+ const core_1 = require("@astram/core");
20
21
  const guard_1 = require("@astram/guard");
21
22
  const posting_version_bump_js_1 = require("../membership/posting-version-bump.js");
22
23
  const list_query_js_1 = require("../shared/list-query.js");
23
24
  const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
25
+ const http_js_1 = require("../shared/http.js");
24
26
  const identity_errors_js_1 = require("../shared/identity-errors.js");
25
27
  const email_address_changer_token_js_1 = require("./email-address-changer.token.js");
26
28
  const credential_provisioner_token_js_1 = require("./credential-provisioner.token.js");
27
29
  const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
30
+ const signin_admin_tokens_js_1 = require("./signin-admin.tokens.js");
28
31
  /**
29
32
  * KI #87: `users` has two unique keys besides the PK, `IDX_users_username` (migration 001) and
30
33
  * `uq_users_email` (021, `utf8mb4_unicode_ci`: folds case, accents, width, trailing spaces). The
@@ -39,12 +42,32 @@ function isDuplicateKey(err) {
39
42
  return e.errno === 1062 || e.code === 'ER_DUP_ENTRY';
40
43
  }
41
44
  const USERNAME_OR_EMAIL_TAKEN = 'That username or email is already in use.';
45
+ /** Auth capabilities R1.10: the fixed 409 for a login-name clash (never the value, no tenant). */
46
+ const LOGIN_NAME_TAKEN = 'That sign-in name is already in use.';
47
+ /** R1.3: the fixed 400 for a login name outside the rule (never echoes the value). */
48
+ const LOGIN_NAME_INVALID = 'Enter a valid sign-in name: 3–64 characters, letters a–z, digits, dot, underscore or hyphen.';
49
+ /** Migration 103's unique index on `users.u_login_name`. */
50
+ const LOGIN_NAME_INDEX = 'uq_users_login_name';
51
+ /** The audit action for a login-name set / change / clear (R1.7, R12.2). */
52
+ const HANDLE_CHANGED = 'auth.handle.changed';
53
+ /**
54
+ * The duplicate key is the login-name index. The driver names the key in its message; only that
55
+ * name is matched (the message itself, which carries the value, is never echoed or logged).
56
+ */
57
+ function isLoginNameDuplicate(err) {
58
+ if (!isDuplicateKey(err))
59
+ return false;
60
+ const e = err;
61
+ return [e.sqlMessage, e.message].some((m) => typeof m === 'string' && m.includes(LOGIN_NAME_INDEX));
62
+ }
42
63
  /** Run a user write; a duplicate key becomes {@link IdentityConflictError}, all else rethrows. */
43
64
  async function mappingDuplicates(write) {
44
65
  try {
45
66
  return await write();
46
67
  }
47
68
  catch (err) {
69
+ if (isLoginNameDuplicate(err))
70
+ throw new identity_errors_js_1.IdentityConflictError(LOGIN_NAME_TAKEN);
48
71
  // R1-4: an orphan credential already holds this login hint (deleteUser leaves it): same fixed
49
72
  // 409 as a duplicate email, never the hint.
50
73
  if (isDuplicateKey(err) || err instanceof guard_1.CredentialExistsError) {
@@ -53,6 +76,25 @@ async function mappingDuplicates(write) {
53
76
  throw err;
54
77
  }
55
78
  }
79
+ /**
80
+ * R1.3 / R1.10: the requested login name. `undefined` ⇒ not part of the request; `null` (or a
81
+ * blank string) ⇒ clear; otherwise the normalized (trim + lower-case) value, which must pass core
82
+ * `isValidLoginName` (else a fixed 400, before any write).
83
+ */
84
+ function requestedLoginName(value) {
85
+ if (value === undefined)
86
+ return undefined;
87
+ if (value === null)
88
+ return null;
89
+ if (typeof value !== 'string')
90
+ throw new identity_errors_js_1.IdentityBadRequestError(LOGIN_NAME_INVALID);
91
+ const name = (0, core_1.normalizeLoginName)(value);
92
+ if (name === '')
93
+ return null;
94
+ if (!(0, core_1.isValidLoginName)(name))
95
+ throw new identity_errors_js_1.IdentityBadRequestError(LOGIN_NAME_INVALID);
96
+ return name;
97
+ }
56
98
  const SIGN_IN_STATUS = {
57
99
  locked: 'pending',
58
100
  usable: 'active',
@@ -89,6 +131,7 @@ let UserService = UserService_1 = class UserService {
89
131
  emailAddresses;
90
132
  provisioner;
91
133
  dispatcher;
134
+ mfaStatus;
92
135
  logger = new common_1.Logger(UserService_1.name);
93
136
  /**
94
137
  * The engine's audit sink, used on the email helper's transaction (no pool of its own), on the
@@ -96,7 +139,7 @@ let UserService = UserService_1 = class UserService {
96
139
  * service) ⇒ the sink resolves `SHIELD_DB_NAMESPACE` itself.
97
140
  */
98
141
  emailAudit;
99
- constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas, provisioner, dispatcher) {
142
+ constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses, schemas, provisioner, dispatcher, mfaStatus) {
100
143
  this.records = records;
101
144
  this.ceiling = ceiling;
102
145
  this.resetTokens = resetTokens;
@@ -106,6 +149,7 @@ let UserService = UserService_1 = class UserService {
106
149
  this.emailAddresses = emailAddresses;
107
150
  this.provisioner = provisioner;
108
151
  this.dispatcher = dispatcher;
152
+ this.mfaStatus = mfaStatus;
109
153
  this.emailAudit = new engine_1.MariaDbAuditSink(schemas ? { schema: schemas.audit } : {});
110
154
  }
111
155
  /**
@@ -138,9 +182,20 @@ let UserService = UserService_1 = class UserService {
138
182
  throw new identity_errors_js_1.IdentityBadRequestError('Enter a valid email address.');
139
183
  fields.email = email;
140
184
  }
185
+ // R1.7: an optional login name, written with the user and audited on the same transaction.
186
+ const loginName = requestedLoginName(input.loginName) ?? null;
187
+ if (loginName !== null)
188
+ fields.loginName = loginName;
141
189
  const companyUuid = input.companyUuid;
142
190
  if (!companyUuid) {
143
- const id = await mappingDuplicates(() => this.records.create('user', { fields }));
191
+ const id = await mappingDuplicates(() => loginName === null
192
+ ? this.records.create('user', { fields })
193
+ : this.records.transaction(async (tx) => {
194
+ const created = await tx.create('user', { fields });
195
+ await this.assertLoginNameNotAHint(tx, loginName, String(created));
196
+ await this.auditHandleChanged(tx, actor, String(created), false);
197
+ return created;
198
+ }));
144
199
  return { id, credential: 'none', reason: email === '' ? 'no_email' : 'no_company' };
145
200
  }
146
201
  const provisioner = this.provisioner;
@@ -150,6 +205,10 @@ let UserService = UserService_1 = class UserService {
150
205
  await tx.create('company_membership', {
151
206
  fields: { userUuid: created, companyUuid, isActive: true, status: 'active' },
152
207
  });
208
+ if (loginName !== null) {
209
+ await this.assertLoginNameNotAHint(tx, loginName, String(created));
210
+ await this.auditHandleChanged(tx, actor, String(created), false);
211
+ }
153
212
  if (provision) {
154
213
  await provisioner.provisionLocked({ principalId: created, hint: email, tenantId: companyUuid }, (0, internal_1.transactionQueryOf)(tx));
155
214
  }
@@ -178,17 +237,38 @@ let UserService = UserService_1 = class UserService {
178
237
  return null;
179
238
  return (await this.withSignInStatus([record]))[0];
180
239
  }
181
- /** One `statesOf` call for the given rows; no provisioner (hand-built service) ⇒ rows unchanged. */
240
+ /**
241
+ * One `statesOf` call for the given rows (no provisioner, a hand-built service ⇒ no
242
+ * `signInStatus`), and one batched `mfaStatus` read (R11.3; no reader ⇒ no `mfaStatus`).
243
+ */
182
244
  async withSignInStatus(rows) {
183
- const provisioner = this.provisioner;
184
- if (rows.length === 0 || provisioner == null)
245
+ if (rows.length === 0)
185
246
  return [...rows];
186
- const states = await provisioner.statesOf(rows.map((r) => String(r.id)));
247
+ const ids = rows.map((r) => String(r.id));
248
+ const provisioner = this.provisioner;
249
+ const states = provisioner != null ? await provisioner.statesOf(ids) : null;
250
+ const mfa = await this.mfaStatusesOf(ids);
187
251
  return rows.map((r) => ({
188
252
  ...r,
189
- signInStatus: SIGN_IN_STATUS[states.get(String(r.id)) ?? 'none'],
253
+ ...(states !== null
254
+ ? { signInStatus: SIGN_IN_STATUS[states.get(String(r.id)) ?? 'none'] }
255
+ : {}),
256
+ ...(mfa !== null ? { mfaStatus: mfa.get(String(r.id)) ?? 'not_required' } : {}),
190
257
  }));
191
258
  }
259
+ /** R11.3: the page's `mfaStatus`; a store error is a fixed 503, never a guessed value. */
260
+ async mfaStatusesOf(ids) {
261
+ const reader = this.mfaStatus;
262
+ if (reader == null)
263
+ return null;
264
+ try {
265
+ return await reader.statusesOf(ids);
266
+ }
267
+ catch {
268
+ this.logger.warn('user mfaStatus could not be read (store error)');
269
+ throw new identity_errors_js_1.IdentityUnavailableError('Sign-in status is temporarily unavailable. Try again.');
270
+ }
271
+ }
192
272
  /** Update a user (whitelisted patch; username/email/status changes need dominance — class doc). */
193
273
  async updateUser(actor, uuid, patch) {
194
274
  const current = await this.records.get('user', uuid);
@@ -214,13 +294,31 @@ let UserService = UserService_1 = class UserService {
214
294
  const wantsInactive = typeof patch?.status === 'string' && patch.status !== 'active';
215
295
  if (statusChanged)
216
296
  fields.status = patch.status;
217
- if (usernameChanged || emailChanged || statusChanged || wantsInactive) {
297
+ // R1.7: the login name is a sign-in identifier like the email: admin-only, under dominance,
298
+ // audited (`auth.handle.changed`, no names), sessions untouched. Validated before any write.
299
+ const newLoginName = requestedLoginName(patch?.loginName);
300
+ const oldLoginName = typeof current.fields.loginName === 'string' && current.fields.loginName !== ''
301
+ ? (0, core_1.normalizeLoginName)(current.fields.loginName)
302
+ : null;
303
+ const loginNameChanged = newLoginName !== undefined && newLoginName !== oldLoginName;
304
+ if (loginNameChanged)
305
+ fields.loginName = newLoginName;
306
+ if (usernameChanged || emailChanged || statusChanged || wantsInactive || loginNameChanged) {
218
307
  await this.ceiling.assertDominates(actor, uuid);
219
308
  }
220
309
  // The email first (the step most likely to be refused, 409), through the ONE helper.
221
310
  if (emailChanged)
222
311
  await this.changeEmail(actor, uuid, newEmail);
223
- if (Object.keys(fields).length > 0) {
312
+ if (loginNameChanged) {
313
+ // The field write and its audit row commit together (fail closed).
314
+ await mappingDuplicates(() => this.records.transaction(async (tx) => {
315
+ if (newLoginName !== null)
316
+ await this.assertLoginNameNotAHint(tx, newLoginName, uuid);
317
+ await tx.update('user', uuid, { fields });
318
+ await this.auditHandleChanged(tx, actor, uuid, newLoginName === null);
319
+ }));
320
+ }
321
+ else if (Object.keys(fields).length > 0) {
224
322
  await mappingDuplicates(() => this.records.update('user', uuid, { fields }));
225
323
  }
226
324
  if (statusChanged || wantsInactive)
@@ -228,6 +326,22 @@ let UserService = UserService_1 = class UserService {
228
326
  if (emailChanged && oldEmail)
229
327
  await this.notifyEmailChanged(actor, oldEmail);
230
328
  }
329
+ /**
330
+ * Review round 1, item F: a login name must not equal another principal's legacy sign-in hint
331
+ * (`identity_credential.ic_identity_hint`, compared under the column's case-insensitive
332
+ * collation); the sign-in lookup would then match two accounts. Same fixed 409 as a login-name
333
+ * clash (no new oracle). Read on the write's transaction through guard's credential provisioner;
334
+ * a hand-built service without one (or a custom provisioner without `hintHolderOf`) skips it.
335
+ */
336
+ async assertLoginNameNotAHint(tx, loginName, principalId) {
337
+ const provisioner = this.provisioner;
338
+ if (provisioner == null || typeof provisioner.hintHolderOf !== 'function')
339
+ return;
340
+ const holder = await provisioner.hintHolderOf(loginName, (0, internal_1.transactionQueryOf)(tx));
341
+ if (holder !== null && !(0, http_js_1.sameUuid)(holder, principalId)) {
342
+ throw new identity_errors_js_1.IdentityConflictError(LOGIN_NAME_TAKEN);
343
+ }
344
+ }
231
345
  /**
232
346
  * Phase 12 (KI #14): write a dominated user's new (canonical) email through guard's
233
347
  * email-change helper — ONE transaction that locks the user and credential rows, refuses a
@@ -260,6 +374,23 @@ let UserService = UserService_1 = class UserService {
260
374
  if (outcome.status !== 'changed')
261
375
  throw new identity_errors_js_1.IdentityNotFoundError('User not found.');
262
376
  }
377
+ /**
378
+ * R1.7 / R12.2: the `auth.handle.changed` row (type `authentication`) on the write's own
379
+ * transaction. Actor and target only: no login name (old or new) is written.
380
+ */
381
+ async auditHandleChanged(tx, actor, targetId, cleared) {
382
+ const query = (0, internal_1.transactionQueryOf)(tx);
383
+ const event = {
384
+ type: 'authentication',
385
+ occurredAtMs: Date.now(),
386
+ actorId: actor.id,
387
+ tenantId: actor.tenantId,
388
+ action: { key: HANDLE_CHANGED },
389
+ resource: { level: 'record', typeKey: 'user', tenantId: actor.tenantId, recordId: targetId },
390
+ attributes: { changed: true, cleared },
391
+ };
392
+ await this.emailAudit.record(event, (sql, params) => query(sql, params));
393
+ }
263
394
  /** The helper call with the audit row on its transaction (see {@link changeEmail}). */
264
395
  applyEmailChange(changer, actor, uuid, newEmail) {
265
396
  return changer.change({
@@ -365,6 +496,8 @@ exports.UserService = UserService = UserService_1 = __decorate([
365
496
  __param(8, (0, common_1.Inject)(credential_provisioner_token_js_1.IDENTITY_CREDENTIAL_PROVISIONER)),
366
497
  __param(9, (0, common_1.Optional)()),
367
498
  __param(9, (0, common_1.Inject)(guard_1.SHIELD_MESSAGE_DISPATCHER)),
499
+ __param(10, (0, common_1.Optional)()),
500
+ __param(10, (0, common_1.Inject)(signin_admin_tokens_js_1.IDENTITY_MFA_STATUS_READER)),
368
501
  __metadata("design:paramtypes", [engine_1.RecordsService,
369
- grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object, Object, Object])
502
+ grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object, Object, Object, Object, Object])
370
503
  ], UserService);
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { CompanyDepartment } from './company-department.object.js';
15
15
  export { CompanyBranch } from './company-branch.object.js';
16
16
  export { ServiceAccount } from './service-account.object.js';
17
17
  export { StoredFile } from './stored-file.object.js';
18
+ export { SecurityPolicy } from './security-policy.object.js';
18
19
  export { identityMigrationsDir } from './migrations-dir.js';
19
20
  /**
20
21
  * All identity objects, registered with the engine at boot (same set + order as before).
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF;0DAC0D;AAC1D,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;GAGG;AACH,YAAY,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAEhF;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,+CAA+C,CAAC;AAEvD;;;GAGG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,mCAAmC,CAAC;AAU3C;;;GAGG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACxF;0DAC0D;AAC1D,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEpF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;GAGG;AACH,YAAY,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAEhF;;;;GAIG;AACH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,GAC5B,MAAM,+CAA+C,CAAC;AAEvD;;;GAGG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,UAAU,GAChB,MAAM,mCAAmC,CAAC;AAU3C;;;GAGG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.FilesService = exports.OrgBootstrapError = exports.runOrgBootstrap = exports.IdentityFeatureModule = exports.buildIdentityAuthKeys = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.StoredFile = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
3
+ exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.FilesService = exports.OrgBootstrapError = exports.runOrgBootstrap = exports.IdentityFeatureModule = exports.buildIdentityAuthKeys = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.SecurityPolicy = exports.StoredFile = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
4
4
  /**
5
5
  * @astram/identity — the framework-owned identity + tenancy model.
6
6
  * The 9 identity object classes (relocated from the consumer app in Sub-plan B) +
@@ -29,6 +29,8 @@ var service_account_object_js_1 = require("./service-account.object.js");
29
29
  Object.defineProperty(exports, "ServiceAccount", { enumerable: true, get: function () { return service_account_object_js_1.ServiceAccount; } });
30
30
  var stored_file_object_js_1 = require("./stored-file.object.js");
31
31
  Object.defineProperty(exports, "StoredFile", { enumerable: true, get: function () { return stored_file_object_js_1.StoredFile; } });
32
+ var security_policy_object_js_1 = require("./security-policy.object.js");
33
+ Object.defineProperty(exports, "SecurityPolicy", { enumerable: true, get: function () { return security_policy_object_js_1.SecurityPolicy; } });
32
34
  var migrations_dir_js_1 = require("./migrations-dir.js");
33
35
  Object.defineProperty(exports, "identityMigrationsDir", { enumerable: true, get: function () { return migrations_dir_js_1.identityMigrationsDir; } });
34
36
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"objects.d.ts","sourceRoot":"","sources":["../src/objects.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAgBtD,6FAA6F;AAC7F,eAAO,MAAM,eAAe,EAAE,SAAS,eAAe,EAerD,CAAC"}
1
+ {"version":3,"file":"objects.d.ts","sourceRoot":"","sources":["../src/objects.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAiBtD,6FAA6F;AAC7F,eAAO,MAAM,eAAe,EAAE,SAAS,eAAe,EAgBrD,CAAC"}
package/dist/objects.js CHANGED
@@ -15,6 +15,7 @@ const record_share_object_js_1 = require("./record-share.object.js");
15
15
  const shared_resource_object_js_1 = require("./shared-resource.object.js");
16
16
  const service_account_object_js_1 = require("./service-account.object.js");
17
17
  const stored_file_object_js_1 = require("./stored-file.object.js");
18
+ const security_policy_object_js_1 = require("./security-policy.object.js");
18
19
  /** All identity objects, registered with the engine at boot (same set + order as before). */
19
20
  exports.identityObjects = [
20
21
  company_object_js_1.Company,
@@ -31,4 +32,5 @@ exports.identityObjects = [
31
32
  shared_resource_object_js_1.SharedResource,
32
33
  service_account_object_js_1.ServiceAccount,
33
34
  stored_file_object_js_1.StoredFile,
35
+ security_policy_object_js_1.SecurityPolicy,
34
36
  ];
@@ -1,4 +1,4 @@
1
- /** roles — global catalog. */
1
+ /** roles — each role is owned by one company (G-013); names are unique per company. */
2
2
  export declare class Role {
3
3
  uuid: string;
4
4
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"role.object.d.ts","sourceRoot":"","sources":["../src/role.object.ts"],"names":[],"mappings":"AAGA,8BAA8B;AAC9B,qBAoBa,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAMd,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAGjC,SAAS,EAAG,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"role.object.d.ts","sourceRoot":"","sources":["../src/role.object.ts"],"names":[],"mappings":"AAGA,uFAAuF;AACvF,qBAWa,IAAI;IAEf,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAMd,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAGjC,SAAS,EAAG,MAAM,CAAC;CACpB"}