@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
@@ -11,10 +11,11 @@
11
11
  * A posting IS a role assignment, so before ANY write the actor (the request principal) must:
12
12
  * - reach the membership (a walled `records.get` → null ⇒ 404; the engine's ADR-0034 create
13
13
  * wall re-checks the posting's membership company inside the transaction);
14
- * - for every ADDED role: the role is bound within the actor's subtree or not bound at all
15
- * (`GrantCeiling.assertRoleAssignable` — a role also used elsewhere is never assignable),
16
- * and the role's keys are all held by the actor (`assertCanAssignRoles`, ADR-0029 applied to
17
- * assignment). Removals need no ceiling: self-demotion stays allowed.
14
+ * - for every ADDED role: the role is owned by the membership's company (G-013,
15
+ * `GrantCeiling.assertRoleAssignable` — another company's or an ownerless role is a 400, for
16
+ * every actor, root included), and the role's keys are all held by the actor
17
+ * (`assertCanAssignRoles`, ADR-0029 applied to assignment). Removals need no ceiling:
18
+ * self-demotion stays allowed.
18
19
  * - for every ADDED posting: its department and branch belong to the membership's company
19
20
  * (`assertOrgUnitsInCompany`, Phase 9 #13b) — else 400.
20
21
  *
@@ -150,7 +151,7 @@ let PostingService = class PostingService {
150
151
  seenAdded.add(k);
151
152
  return true;
152
153
  });
153
- // 2) Authority for every ADDED role, before any write: binding rule, then the ceiling.
154
+ // 2) Authority for every ADDED role, before any write: owner rule (G-013), then the ceiling.
154
155
  const addedRoles = await this.assertMayAdd(actor, tenantUuid, added, undefined);
155
156
  // Removing a PEER's postings strips their authority: the actor must dominate the member
156
157
  // (security review). Removing your OWN postings (self-demotion) stays allowed.
@@ -163,11 +164,12 @@ let PostingService = class PostingService {
163
164
  // 3) Write atomically: creates first, then deletes (KI #16). Only whitelisted fields are sent,
164
165
  // and `membershipUuid` always comes from the path (review F4).
165
166
  await this.records.transaction(async (tx) => {
166
- // Review F2: serialise against a concurrent grant/posting of the same role. Lock each
167
- // added role row on THIS transaction (grant administration locks the same row), then
168
- // re-run the binding rule: a binding another tenant committed while we waited is now
169
- // visible, and a role that became shared is refused before the INSERT (rolled back).
170
- await this.lockAndRecheck(tx, actor, addedRoles, undefined);
167
+ // Review F2: serialise against a concurrent grant/delete of the same role. Lock each added
168
+ // role row on THIS transaction (grant administration and role delete lock the same row),
169
+ // then re-run the owner rule (G-013): a role deleted, or made ownerless by its company's
170
+ // delete, while we waited is refused before the INSERT (rolled back).
171
+ await this.lockAndRecheck(tx, actor, tenantUuid, addedRoles, undefined);
172
+ await this.lockOrgUnits(tx, tenantUuid, added);
171
173
  for (const d of added) {
172
174
  await tx.create('membership_posting', { fields: { ...stripEmpty(d), membershipUuid } });
173
175
  }
@@ -208,7 +210,8 @@ let PostingService = class PostingService {
208
210
  return; // nothing missing: no write, no bump
209
211
  const addedRoles = await this.assertMayAdd(actor, tenantUuid, added, ceilingOpts);
210
212
  await this.records.transaction(async (tx) => {
211
- await this.lockAndRecheck(tx, actor, addedRoles, ceilingOpts);
213
+ await this.lockAndRecheck(tx, actor, tenantUuid, addedRoles, ceilingOpts);
214
+ await this.lockOrgUnits(tx, tenantUuid, added);
212
215
  for (const d of added) {
213
216
  await tx.create('membership_posting', { fields: { ...stripEmpty(d), membershipUuid } });
214
217
  }
@@ -232,11 +235,13 @@ let PostingService = class PostingService {
232
235
  : null;
233
236
  return { membership, tenantUuid };
234
237
  }
235
- /** Authority for ADDED postings (binding rule, ceiling, org units); returns the distinct roles. */
238
+ /** Authority for ADDED postings (owner rule, ceiling, org units); returns the distinct roles. */
236
239
  async assertMayAdd(actor, tenantUuid, added, opts) {
237
240
  const addedRoles = [...new Set(added.map((d) => d.roleUuid))];
241
+ // G-013 (spec §5.D, R2): every added role is the membership company's own, for every actor
242
+ // (root and the bootstrap operation included) — 400 otherwise, before the ceiling.
238
243
  for (const roleUuid of addedRoles) {
239
- await this.ceiling.assertRoleAssignable(actor, roleUuid, opts);
244
+ await this.ceiling.assertRoleAssignable(actor, roleUuid, tenantUuid, opts);
240
245
  }
241
246
  await this.ceiling.assertCanAssignRoles(actor, addedRoles, opts);
242
247
  // Phase 9 #13b (review F4): an added posting's department/branch must be the membership
@@ -248,13 +253,55 @@ let PostingService = class PostingService {
248
253
  });
249
254
  return addedRoles;
250
255
  }
251
- /** Lock each added role row on the transaction, then re-run the binding rule (review F2). */
252
- async lockAndRecheck(tx, actor, addedRoles, opts) {
256
+ /**
257
+ * Lock each added role row on the transaction, then re-check the owner rule (review F2, G-013).
258
+ * A role changes owner only through its company's delete (owner cleared), which also removes the
259
+ * memberships of that company, so the re-check after the lock sees the committed outcome.
260
+ */
261
+ async lockAndRecheck(tx, actor, tenantUuid, addedRoles, opts) {
253
262
  for (const roleUuid of addedRoles) {
254
263
  await tx.lockForUpdate('role', roleUuid);
255
- await this.ceiling.assertRoleAssignable(actor, roleUuid, opts);
264
+ await this.ceiling.assertRoleAssignable(actor, roleUuid, tenantUuid, opts);
256
265
  }
257
266
  }
267
+ /**
268
+ * G-012 (spec §5.A, NAES review D): lock every ADDED posting's department and branch row on THIS
269
+ * transaction (departments then branches, each sorted and de-duplicated: one lock order), then
270
+ * re-check that each still belongs to the membership's company. A department/branch delete locks
271
+ * the same row and counts postings after its lock, so the two serialise: delete first ⇒ the unit
272
+ * (and its junction row, which cascades) is gone and this add is refused with 400; add first ⇒
273
+ * the delete counts this posting and answers 409. The FK is never reached, so no driver error.
274
+ *
275
+ * The re-check reuses `assertOrgUnitsInCompany` (fresh reads on the authz pool, after the lock is
276
+ * held, so they see the committed delete) rather than an engine read: the org bootstrap's
277
+ * `postings` step is gate-7 capped to membership types and may not read `department`/`branch`.
278
+ * The pre-transaction check already passed, so a failure here can only mean the row is gone.
279
+ */
280
+ async lockOrgUnits(tx, tenantUuid, added) {
281
+ const ids = (pick) => [
282
+ ...new Set(added.map(pick).filter((v) => typeof v === 'string' && v.length > 0)),
283
+ ].sort();
284
+ const departmentUuids = ids((d) => d.departmentUuid);
285
+ const branchUuids = ids((d) => d.branchUuid);
286
+ for (const id of departmentUuids)
287
+ await tx.lockForUpdate('department', id);
288
+ for (const id of branchUuids)
289
+ await tx.lockForUpdate('branch', id);
290
+ const recheck = async (units, message) => {
291
+ if (units.departmentUuids.length === 0 && units.branchUuids.length === 0)
292
+ return;
293
+ try {
294
+ await this.ceiling.assertOrgUnitsInCompany(tenantUuid, units);
295
+ }
296
+ catch (err) {
297
+ if (err instanceof identity_errors_js_1.IdentityBadRequestError)
298
+ throw new identity_errors_js_1.IdentityBadRequestError(message);
299
+ throw err;
300
+ }
301
+ };
302
+ await recheck({ departmentUuids, branchUuids: [] }, 'That department no longer exists.');
303
+ await recheck({ departmentUuids: [], branchUuids }, 'That branch no longer exists.');
304
+ }
258
305
  async bumpTenant(tenantUuid) {
259
306
  // ADR-0011 STALENESS FIX: a posting add/remove changes the effective roles (→ coarse
260
307
  // grants) of the membership's user in that company, so bump the tenant's permission
@@ -3,6 +3,8 @@ export interface OrgBootstrapRoot {
3
3
  email: string;
4
4
  /** Tenant path: `A` or `A/B` (tenant name, then child names). */
5
5
  homeCompany: string;
6
+ /** Optional sign-in name (auth capabilities R1.7), normalized: lower-case `[a-z0-9._-]{3,64}`. */
7
+ loginName?: string;
6
8
  }
7
9
  export interface OrgBootstrapRole {
8
10
  name: string;
@@ -19,6 +21,8 @@ export interface OrgBootstrapUser {
19
21
  /** Path of child names relative to the tenant (`A/B` means tenant > A > B). */
20
22
  company?: string;
21
23
  postings?: OrgBootstrapPosting[];
24
+ /** Optional sign-in name (auth capabilities R1.7), normalized: lower-case `[a-z0-9._-]{3,64}`. */
25
+ loginName?: string;
22
26
  }
23
27
  export interface OrgBootstrapCompany {
24
28
  name: string;
@@ -44,10 +48,10 @@ export interface OrgBootstrapFileOptions {
44
48
  /** The closed key sets, exported so the schema parity test compares against the parser's own. */
45
49
  export declare const ORG_FILE_KEYS: {
46
50
  readonly file: readonly ["version", "roots", "tenants"];
47
- readonly root: readonly ["username", "email", "homeCompany"];
51
+ readonly root: readonly ["username", "email", "homeCompany", "loginName"];
48
52
  readonly company: readonly ["name", "departments", "branches", "roles", "users", "children"];
49
53
  readonly role: readonly ["name", "permissions"];
50
- readonly user: readonly ["username", "email", "company", "postings"];
54
+ readonly user: readonly ["username", "email", "company", "postings", "loginName"];
51
55
  readonly posting: readonly ["role", "department", "branch"];
52
56
  };
53
57
  export declare class OrgBootstrapFileError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"org-bootstrap-file.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-file.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,iGAAiG;AACjG,eAAO,MAAM,aAAa;;;;;;;CAOhB,CAAC;AAEX,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;gBAC7B,MAAM,EAAE,qBAAqB,EAAE;CAO5C;AAmBD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,uBAAuB,GAC/B,gBAAgB,CAqRlB"}
1
+ {"version":3,"file":"org-bootstrap-file.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-file.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,iGAAiG;AACjG,eAAO,MAAM,aAAa;;;;;;;CAOhB,CAAC;AAEX,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;gBAC7B,MAAM,EAAE,qBAAqB,EAAE;CAO5C;AAoCD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,uBAAuB,GAC/B,gBAAgB,CA4SlB"}
@@ -8,17 +8,20 @@ exports.parseOrgBootstrapFile = parseOrgBootstrapFile;
8
8
  * Hand-written (the repo carries no validator library) with closed key sets at every level;
9
9
  * `org-bootstrap.schema.json` documents the same shape and a parity test keeps them in step.
10
10
  * No I/O. Every problem is collected as `{ path, message }`; messages never echo input values, so a
11
- * stray `password` field is reported by name only.
11
+ * stray `password` field is reported by name only. The one exception is the G-013 posting-role
12
+ * message (spec §5.E), which names the role and the company path the operator's own file declares.
12
13
  */
14
+ const core_1 = require("@astram/core");
13
15
  const guard_1 = require("@astram/guard");
16
+ const org_unit_name_js_1 = require("../org-structure/org-unit-name.js");
14
17
  const org_names_js_1 = require("./org-names.js");
15
18
  /** The closed key sets, exported so the schema parity test compares against the parser's own. */
16
19
  exports.ORG_FILE_KEYS = {
17
20
  file: ['version', 'roots', 'tenants'],
18
- root: ['username', 'email', 'homeCompany'],
21
+ root: ['username', 'email', 'homeCompany', 'loginName'],
19
22
  company: ['name', 'departments', 'branches', 'roles', 'users', 'children'],
20
23
  role: ['name', 'permissions'],
21
- user: ['username', 'email', 'company', 'postings'],
24
+ user: ['username', 'email', 'company', 'postings', 'loginName'],
22
25
  posting: ['role', 'department', 'branch'],
23
26
  };
24
27
  class OrgBootstrapFileError extends Error {
@@ -32,6 +35,22 @@ class OrgBootstrapFileError extends Error {
32
35
  exports.OrgBootstrapFileError = OrgBootstrapFileError;
33
36
  const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
34
37
  const join = (base, key) => (base === '' ? key : `${base}.${key}`);
38
+ /**
39
+ * G-013: the comparison key of a role name — the shared name rule (`normaliseOrgUnitName`: NFC,
40
+ * trim, collapse whitespace), then `nameKey` (case, accents), as the planner and the per-company
41
+ * unique index compare. A name the rule refuses falls back to `nameKey` (the planner reports it).
42
+ */
43
+ function roleKey(name) {
44
+ const n = (0, org_unit_name_js_1.normaliseOrgUnitName)(name);
45
+ return (0, org_names_js_1.nameKey)(n.ok ? n.value : name);
46
+ }
47
+ /** A company's `/`-joined name path from its tenant (`Acme/Sales`), as the file names it. */
48
+ function namePath(node) {
49
+ const names = [];
50
+ for (let n = node; n; n = n.parent)
51
+ names.unshift(n.name);
52
+ return names.join('/');
53
+ }
35
54
  function parseOrgBootstrapFile(input, options) {
36
55
  const issues = [];
37
56
  const add = (path, message) => issues.push({ path, message });
@@ -85,7 +104,6 @@ function parseOrgBootstrapFile(input, options) {
85
104
  if (f.version !== 1)
86
105
  add('version', 'must be the number 1');
87
106
  // ---- pass 1: companies (structure, roles, name uniqueness), building the lookup tree ----
88
- const globalRoles = new Set();
89
107
  const tenantNodes = new Map();
90
108
  const nodes = []; // depth-first, parent before children
91
109
  const walkCompany = (raw, path, parent, siblings) => {
@@ -97,6 +115,9 @@ function parseOrgBootstrapFile(input, options) {
97
115
  add(`${path}.name`, 'must not contain "/"');
98
116
  const departments = nameList(c.departments, `${path}.departments`);
99
117
  const branches = nameList(c.branches, `${path}.branches`);
118
+ // G-013 (spec §5.E): roles are owned per company, so a role name is unique within its company
119
+ // only (compared as stored: the name rule, then the collation); another company may declare
120
+ // the same name and gets its own role.
100
121
  const roles = [];
101
122
  const roleNames = new Set();
102
123
  (optionalArray(c.roles, `${path}.roles`) ?? []).forEach((rr, i) => {
@@ -121,10 +142,10 @@ function parseOrgBootstrapFile(input, options) {
121
142
  });
122
143
  }
123
144
  if (rname !== undefined) {
124
- if (globalRoles.has((0, org_names_js_1.nameKey)(rname)))
125
- add(`${rp}.name`, 'duplicate role name (roles are global by name)');
126
- globalRoles.add((0, org_names_js_1.nameKey)(rname));
127
- roleNames.add(rname);
145
+ const key = roleKey(rname);
146
+ if (roleNames.has(key))
147
+ add(`${rp}.name`, 'duplicate role name within this company');
148
+ roleNames.add(key);
128
149
  roles.push({ name: rname, permissions: perms });
129
150
  }
130
151
  });
@@ -204,6 +225,21 @@ function parseOrgBootstrapFile(input, options) {
204
225
  emails.add(k);
205
226
  }
206
227
  };
228
+ // Auth capabilities R1.3 / R1.10: optional login name, normalized, validated, unique in the file.
229
+ const loginNames = new Set();
230
+ const loginNameOf = (v, path) => {
231
+ if (v === undefined)
232
+ return undefined;
233
+ const name = typeof v === 'string' ? (0, core_1.normalizeLoginName)(v) : '';
234
+ if (!(0, core_1.isValidLoginName)(name)) {
235
+ add(path, 'must be a valid sign-in name: 3-64 characters, letters a-z, digits, dot, underscore or hyphen');
236
+ return undefined;
237
+ }
238
+ if (loginNames.has(name))
239
+ add(path, 'duplicate login name (case-insensitive)');
240
+ loginNames.add(name);
241
+ return name;
242
+ };
207
243
  const emailOf = (v, path) => {
208
244
  if (typeof v !== 'string' || !(0, guard_1.isEmailShaped)(v.trim())) {
209
245
  add(path, 'must be a valid email address');
@@ -224,8 +260,9 @@ function parseOrgBootstrapFile(input, options) {
224
260
  add(`${p}.homeCompany`, 'does not name a company declared in this file');
225
261
  }
226
262
  claim(username, email, p);
263
+ const loginName = loginNameOf(r.loginName, `${p}.loginName`);
227
264
  if (username !== undefined && email !== undefined && home !== undefined) {
228
- roots.push({ username, email, homeCompany: home });
265
+ roots.push({ username, email, homeCompany: home, ...(loginName ? { loginName } : {}) });
229
266
  }
230
267
  });
231
268
  const inScope = (start, pick, value) => {
@@ -246,6 +283,7 @@ function parseOrgBootstrapFile(input, options) {
246
283
  const username = text(u.username, `${p}.username`);
247
284
  const email = emailOf(u.email, `${p}.email`);
248
285
  claim(username, email, p);
286
+ const loginName = loginNameOf(u.loginName, `${p}.loginName`);
249
287
  let home = node;
250
288
  let companyRaw;
251
289
  if (u.company !== undefined) {
@@ -269,8 +307,11 @@ function parseOrgBootstrapFile(input, options) {
269
307
  if (!po)
270
308
  return;
271
309
  const role = text(po.role, `${q}.role`);
272
- if (role !== undefined && !inScope(home, (n) => n.roles, role)) {
273
- add(`${q}.role`, 'not declared on this company or an ancestor');
310
+ // G-013 (spec §5.E, R2): a role is assigned only in the company that owns it, so a
311
+ // posting's role must be declared on the posting's OWN company (an ancestor's role is
312
+ // that ancestor's, and the server would refuse it with 400).
313
+ if (role !== undefined && !home.roles.has(roleKey(role))) {
314
+ add(`${q}.role`, `Role "${role}" is not declared on company "${namePath(home)}".`);
274
315
  }
275
316
  const posting = { role: role ?? '' };
276
317
  for (const [key, pick] of [
@@ -299,6 +340,8 @@ function parseOrgBootstrapFile(input, options) {
299
340
  user.company = companyRaw;
300
341
  if (u.postings !== undefined)
301
342
  user.postings = postings;
343
+ if (loginName !== undefined)
344
+ user.loginName = loginName;
302
345
  users.push(user);
303
346
  }
304
347
  });
@@ -7,8 +7,17 @@
7
7
  * subtrees are looked at, so onboarding tenant B never reports tenant A.
8
8
  */
9
9
  import type { OrgBootstrapFile } from './org-bootstrap-file.js';
10
+ /**
11
+ * G-012 (spec §5.A/§5.D, NAES review C): the comparison key of a department/branch name — the
12
+ * shared name rule (`normaliseOrgUnitName`: NFC, trim, collapse whitespace) then `nameKey` (case,
13
+ * accents). "Quality Assurance" in a file therefore matches "Quality Assurance" in the database.
14
+ * A name the rule refuses falls back to `nameKey` alone (the planner reports it as a conflict).
15
+ */
16
+ export declare function orgUnitKey(name: string): string;
17
+ /** The drift note of a stored department/branch the file does not list (spec §5.D). */
18
+ export declare const orgUnitDriftNote: (name: string) => string;
10
19
  export type CredentialState = 'none' | 'locked' | 'usable';
11
- export declare const PLAN_KIND_ORDER: readonly ["company", "department", "branch", "company_department", "company_branch", "role", "role_claim", "grant", "user", "membership", "posting", "credential", "root_flag"];
20
+ export declare const PLAN_KIND_ORDER: readonly ["company", "department", "branch", "company_department", "company_branch", "role", "grant", "user", "membership", "posting", "credential", "root_flag"];
12
21
  export type PlanKind = (typeof PLAN_KIND_ORDER)[number];
13
22
  export type PlanStatus = 'create' | 'unchanged' | 'drift' | 'not-in-file' | 'conflict';
14
23
  export interface PlanEntry {
@@ -36,6 +45,8 @@ export interface OrgStateMembership {
36
45
  export interface OrgStateUser {
37
46
  username: string;
38
47
  email: string;
48
+ /** The stored login name (`u_login_name`); absent or null = none. */
49
+ loginName?: string | null;
39
50
  credential: CredentialState;
40
51
  isPlatformSuperuser: boolean;
41
52
  /** Memberships INSIDE the file's tenants' subtrees only. */
@@ -43,37 +54,45 @@ export interface OrgStateUser {
43
54
  /** Holds a membership OUTSIDE the file's tenants' subtrees (another tenant's user). */
44
55
  membershipsOutside: boolean;
45
56
  }
57
+ /**
58
+ * A role OWNED by a company of the file's tenants' subtrees (G-013: every role belongs to one
59
+ * company; a name is unique within that company only).
60
+ */
46
61
  export interface OrgStateRole {
62
+ /** The owner company's path (`Tenant/Child/...`, names as stored). */
63
+ companyPath: string;
64
+ /** The role name as stored. */
47
65
  name: string;
48
- /** Name of the owner's top-level tenant; null = no owner. */
49
- ownerTenant: string | null;
50
- /** Held by a posting outside the file's tenants' subtrees. */
51
- heldOutside: boolean;
52
66
  permissions: string[];
53
67
  }
54
68
  export interface OrgStateCompany {
55
69
  path: string;
70
+ /** The company's OWN departments (G-012: owner column), names as stored. */
56
71
  departments: string[];
72
+ /** The company's OWN branches, names as stored. */
57
73
  branches: string[];
58
74
  }
59
75
  export interface OrgState {
60
76
  companies: OrgStateCompany[];
61
- departments: string[];
62
- branches: string[];
77
+ /** The roles owned by the companies of the file's tenants' subtrees (G-013). */
63
78
  roles: OrgStateRole[];
64
79
  users: OrgStateUser[];
65
80
  emailHolders: {
66
81
  email: string;
67
82
  username: string;
68
83
  }[];
84
+ /** Holders of the file's login names (read only when the file declares any). */
85
+ loginNameHolders?: {
86
+ loginName: string;
87
+ username: string;
88
+ }[];
69
89
  /**
70
90
  * File company paths whose name matches more than one stored company (several top-level companies
71
91
  * of the tenant's name, or several children of one parent with the child's name): never guessed.
72
92
  */
73
93
  ambiguousCompanies: string[];
74
94
  }
75
- export interface PlanOptions {
76
- claimUnownedRoles: boolean;
77
- }
78
- export declare function planOrgBootstrap(file: OrgBootstrapFile, state: OrgState, options: PlanOptions): OrgPlan;
95
+ /** The plan path of a company's own role (G-013, spec §5.E): `<company path>#role:<name>`. */
96
+ export declare const rolePath: (companyPath: string, name: string) => string;
97
+ export declare function planOrgBootstrap(file: OrgBootstrapFile, state: OrgState): OrgPlan;
79
98
  //# sourceMappingURL=org-bootstrap-plan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"org-bootstrap-plan.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3D,eAAO,MAAM,eAAe,iLAclB,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC;AAEvF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,4FAA4F;IAC5F,SAAS,EAAE,SAAS,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,4DAA4D;IAC5D,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,uFAAuF;IACvF,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,8DAA8D;IAC9D,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpD;;;OAGG;IACH,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAQD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,WAAW,GACnB,OAAO,CA+WT"}
1
+ {"version":3,"file":"org-bootstrap-plan.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,yBAAyB,CAAC;AAKjC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MACkC,CAAC;AAEnF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3D,eAAO,MAAM,eAAe,mKAalB,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC;AAEvF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,4FAA4F;IAC5F,SAAS,EAAE,SAAS,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,eAAe,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,4DAA4D;IAC5D,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,uFAAuF;IACvF,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,gFAAgF;IAChF,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpD,gFAAgF;IAChF,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7D;;;OAGG;IACH,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,8FAA8F;AAC9F,eAAO,MAAM,QAAQ,GAAI,aAAa,MAAM,EAAE,MAAM,MAAM,KAAG,MAC9B,CAAC;AAQhC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAobjF"}