@astram/identity 0.15.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 (129) 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/1710000000102-CreateAuditChain.cjs +71 -0
  125. package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
  126. package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
  127. package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
  128. package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
  129. package/package.json +9 -4
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OrgBootstrapError = void 0;
3
+ exports.CLAIM_UNOWNED_NOTICE = exports.OrgBootstrapError = void 0;
4
4
  exports.runOrgBootstrap = runOrgBootstrap;
5
5
  /**
6
6
  * G-009 T10: the org bootstrap runner (SPEC R2, R3, R5-R11, R13; ADR-0042).
7
7
  *
8
8
  * `runOrgBootstrap(app, opts)` applies a declarative org file against the consumer's own application
9
- * context, through the framework's existing services, never raw SQL (the exceptions are the role-owner
10
- * claim and the root flag, both under the run lock and audited). Each step runs as its own principal,
9
+ * context, through the framework's existing services, never raw SQL (the exception is the root flag,
10
+ * under the run lock and audited). Each step runs as its own principal,
11
11
  * bound to the declared operation `shield:org-bootstrap` (Shield #9): control-plane steps root-flagged,
12
12
  * people steps non-root and anchored at one company. The principal reaches the request-scoped services
13
13
  * through `registerRequestByContextId`, so no service signature changes.
@@ -27,7 +27,6 @@ const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
27
27
  const membership_service_js_1 = require("../membership/membership.service.js");
28
28
  const posting_service_js_1 = require("../membership/posting.service.js");
29
29
  const org_catalog_service_js_1 = require("../org-structure/org-catalog.service.js");
30
- const role_owner_transition_js_1 = require("../role/role-owner-transition.js");
31
30
  const role_service_js_1 = require("../role/role.service.js");
32
31
  const credential_provisioner_token_js_1 = require("../user/credential-provisioner.token.js");
33
32
  const user_service_js_1 = require("../user/user.service.js");
@@ -35,6 +34,7 @@ const invite_message_js_1 = require("../user/invite-message.js");
35
34
  const org_bootstrap_file_js_1 = require("./org-bootstrap-file.js");
36
35
  const org_bootstrap_error_js_1 = require("./org-bootstrap-error.js");
37
36
  const org_bootstrap_plan_js_1 = require("./org-bootstrap-plan.js");
37
+ const org_unit_name_js_1 = require("../org-structure/org-unit-name.js");
38
38
  const org_bootstrap_manifest_js_1 = require("./org-bootstrap.manifest.js");
39
39
  const org_bootstrap_tokens_js_1 = require("./org-bootstrap.tokens.js");
40
40
  const org_names_js_1 = require("./org-names.js");
@@ -51,6 +51,8 @@ const STEP = {
51
51
  users: 'users',
52
52
  postings: 'postings',
53
53
  };
54
+ /** Printed when `claimUnownedRoles` is passed (G-013, spec §5.E): the flag now does nothing. */
55
+ exports.CLAIM_UNOWNED_NOTICE = '--claim-unowned-roles is no longer needed: roles belong to the company that declares them.';
54
56
  // ---------------------------------------------------------------------------------------------
55
57
  function loadFile(file) {
56
58
  if (typeof file !== 'string')
@@ -84,11 +86,20 @@ function flatten(file) {
84
86
  company: u.company === undefined ? c.path : `${c.tenant}/${u.company}`,
85
87
  tenant: c.tenant,
86
88
  postings: u.postings ?? [],
89
+ ...(u.loginName !== undefined ? { loginName: u.loginName } : {}),
87
90
  });
88
91
  }
89
92
  }
90
93
  return { companies, users };
91
94
  }
95
+ /**
96
+ * A department/branch name as stored (the shared rule, spec §10). The planner already refused a
97
+ * name the rule refuses, so the fallback is never written.
98
+ */
99
+ function orgUnitName(raw) {
100
+ const n = (0, org_unit_name_js_1.normaliseOrgUnitName)(raw);
101
+ return n.ok ? n.value : raw;
102
+ }
92
103
  const postingPath = (user, company, p) => `user:${user}@${company} role:${p.role}${p.department ? ` dept:${p.department}` : ''}${p.branch ? ` branch:${p.branch}` : ''}`;
93
104
  function printPlan(plan, out) {
94
105
  for (const e of plan.entries) {
@@ -140,7 +151,9 @@ async function runOrgBootstrap(app, opts) {
140
151
  query: async (sql, params) => (await pool.query(sql, params)),
141
152
  schemas,
142
153
  }).read(file);
143
- const planFor = (state) => (0, org_bootstrap_plan_js_1.planOrgBootstrap)(file, state, { claimUnownedRoles: opts.claimUnownedRoles === true });
154
+ const planFor = (state) => (0, org_bootstrap_plan_js_1.planOrgBootstrap)(file, state);
155
+ if (opts.claimUnownedRoles === true)
156
+ out(exports.CLAIM_UNOWNED_NOTICE);
144
157
  // ---- dry run: plan only, no lock, no write ----
145
158
  if (!apply) {
146
159
  const state = await readState();
@@ -348,81 +361,53 @@ async function runOrgBootstrap(app, opts) {
348
361
  count('company');
349
362
  }
350
363
  });
351
- // 2. org units: catalog rows, then the company junctions
364
+ // 2. org units (G-012): each company's OWN departments and branches. `ensure*` creates the row
365
+ // with its owner and its one junction row in one transaction, so both are counted.
352
366
  await step(STEP.orgUnits, async () => {
353
- const seenDept = new Set();
354
- const seenBranch = new Set();
355
367
  for (const c of flat.companies) {
356
368
  const actor = await rootAt(STEP.orgUnits, c.tenant);
357
369
  const catalogSvc = await service(org_catalog_service_js_1.OrgCatalogService, actor);
358
370
  for (const d of c.node.departments ?? []) {
359
- if (seenDept.has((0, org_names_js_1.nameKey)(d)))
371
+ if (st('department', `${c.path}#dept:${d}`) !== 'create')
360
372
  continue;
361
- seenDept.add((0, org_names_js_1.nameKey)(d));
362
- if (st('department', `dept:${d}`) !== 'create')
363
- continue;
364
- ids.set('department', d, String(await catalogSvc.ensureDepartment(d)));
373
+ const companyUuid = await ids.company(c.path);
374
+ const id = await catalogSvc.ensureDepartment(companyUuid, orgUnitName(d));
375
+ ids.setUnit('department', companyUuid, d, String(id));
365
376
  count('department');
366
- }
367
- for (const b of c.node.branches ?? []) {
368
- if (seenBranch.has((0, org_names_js_1.nameKey)(b)))
369
- continue;
370
- seenBranch.add((0, org_names_js_1.nameKey)(b));
371
- if (st('branch', `branch:${b}`) !== 'create')
372
- continue;
373
- ids.set('branch', b, String(await catalogSvc.ensureBranch(b)));
374
- count('branch');
375
- }
376
- }
377
- for (const c of flat.companies) {
378
- const actor = await rootAt(STEP.orgUnits, c.tenant);
379
- const catalogSvc = await service(org_catalog_service_js_1.OrgCatalogService, actor);
380
- for (const d of c.node.departments ?? []) {
381
- if (st('company_department', `${c.path}#dept:${d}`) !== 'create')
382
- continue;
383
- await catalogSvc.attachDepartment(await ids.company(c.path), await ids.department(d));
384
377
  count('company_department');
385
378
  }
386
379
  for (const b of c.node.branches ?? []) {
387
- if (st('company_branch', `${c.path}#branch:${b}`) !== 'create')
380
+ if (st('branch', `${c.path}#branch:${b}`) !== 'create')
388
381
  continue;
389
- await catalogSvc.attachBranch(await ids.company(c.path), await ids.branch(b));
382
+ const companyUuid = await ids.company(c.path);
383
+ const id = await catalogSvc.ensureBranch(companyUuid, orgUnitName(b));
384
+ ids.setUnit('branch', companyUuid, b, String(id));
385
+ count('branch');
390
386
  count('company_branch');
391
387
  }
392
388
  }
393
389
  });
394
- // 3. roles (owner stamped by RoleService), owner claims, then grants through GrantAdminService
390
+ // 3. roles, then grants through GrantAdminService. G-013 (spec §5.E): each role is created OWNED
391
+ // by the company that declares it (passed explicitly: the `roles` step holds no company key).
395
392
  await step(STEP.roles, async () => {
396
- for (const tenant of file.tenants) {
397
- const actor = await rootAt(STEP.roles, tenant.name);
393
+ for (const c of flat.companies) {
394
+ const declared = c.node.roles ?? [];
395
+ if (declared.length === 0)
396
+ continue;
397
+ const actor = await rootAt(STEP.roles, c.tenant);
398
398
  const roles = await service(role_service_js_1.RoleService, actor);
399
- const declared = [];
400
- const seen = new Set();
401
- for (const c of flat.companies.filter((x) => x.tenant === tenant.name)) {
402
- for (const r of c.node.roles ?? []) {
403
- if (seen.has((0, org_names_js_1.nameKey)(r.name)))
404
- continue;
405
- seen.add((0, org_names_js_1.nameKey)(r.name));
406
- declared.push(r);
407
- }
408
- }
399
+ const companyUuid = await ids.company(c.path);
409
400
  for (const r of declared) {
410
- if (st('role', `role:${r.name}`) !== 'create')
401
+ if (st('role', (0, org_bootstrap_plan_js_1.rolePath)(c.path, r.name)) !== 'create')
411
402
  continue;
412
- ids.set('role', r.name, String(await roles.createRole(r.name, actor)));
403
+ ids.setRole(companyUuid, r.name, String(await roles.createRole(orgUnitName(r.name), actor, companyUuid)));
413
404
  count('role');
414
405
  }
415
- for (const r of declared) {
416
- if (st('role_claim', `role:${r.name}`) !== 'create')
417
- continue;
418
- await claimRole(await ids.role(r.name), await ids.company(tenant.name));
419
- count('role_claim');
420
- }
421
406
  for (const r of declared) {
422
407
  for (const key of r.permissions) {
423
- if (st('grant', `role:${r.name}#${key}`) !== 'create')
408
+ if (st('grant', `${(0, org_bootstrap_plan_js_1.rolePath)(c.path, r.name)}#${key}`) !== 'create')
424
409
  continue;
425
- await grantAdmin.grant(await ids.role(r.name), key, actor);
410
+ await grantAdmin.grant(await ids.role(companyUuid, r.name), key, actor);
426
411
  count('grant');
427
412
  }
428
413
  }
@@ -440,6 +425,8 @@ async function runOrgBootstrap(app, opts) {
440
425
  const created = await users.createUserWithCredential(actor, {
441
426
  username: r.username,
442
427
  email: r.email,
428
+ // Auth capabilities R1.7: the service validates it again and audits it.
429
+ ...(r.loginName !== undefined ? { loginName: r.loginName } : {}),
443
430
  });
444
431
  ids.set('user', r.username, String(created.id));
445
432
  count('user');
@@ -454,6 +441,7 @@ async function runOrgBootstrap(app, opts) {
454
441
  username: u.username,
455
442
  email: u.email,
456
443
  companyUuid,
444
+ ...(u.loginName !== undefined ? { loginName: u.loginName } : {}),
457
445
  });
458
446
  ids.set('user', u.username, String(created.id));
459
447
  count('user');
@@ -480,7 +468,7 @@ async function runOrgBootstrap(app, opts) {
480
468
  }
481
469
  });
482
470
  // 5. postings: add-only, as the anchored non-root principal, with the roles the file declares
483
- // for this tenant (and no other tenant's) as `operationRoleIds`.
471
+ // on the posting's own company (G-013: the only roles assignable there) as `operationRoleIds`.
484
472
  await step(STEP.postings, async () => {
485
473
  for (const u of flat.users) {
486
474
  const toAdd = u.postings.filter((p) => st('posting', postingPath(u.username, u.company, p)) === 'create');
@@ -492,23 +480,26 @@ async function runOrgBootstrap(app, opts) {
492
480
  // A read-only lookup, not `listMemberships`: list SQL scoping does not consult gate 7, so an
493
481
  // operation-bound non-root principal could see no rows there (spec P1 "Matching").
494
482
  const membershipUuid = await ids.membership(userUuid, companyUuid);
495
- // R1-7: the roles declared on THIS company and its ancestors in the file (the resolution the
496
- // file parser uses for postings), not every role of the tenant.
483
+ // R1-7, G-013: the roles declared on THIS company in the file (the resolution the file
484
+ // parser uses for postings), never an ancestor's or another company's.
497
485
  const operationRoleIds = new Set();
498
486
  const here = (0, org_names_js_1.pathKey)(u.company);
499
487
  for (const c of flat.companies) {
500
- const there = (0, org_names_js_1.pathKey)(c.path);
501
- if (here !== there && !here.startsWith(`${there}/`))
488
+ if ((0, org_names_js_1.pathKey)(c.path) !== here)
502
489
  continue;
503
- for (const r of c.node.roles ?? [])
504
- operationRoleIds.add(await ids.role(r.name));
490
+ for (const r of c.node.roles ?? []) {
491
+ operationRoleIds.add(await ids.role(companyUuid, r.name));
492
+ }
505
493
  }
506
494
  const inputs = [];
507
495
  for (const p of toAdd) {
508
496
  inputs.push({
509
- roleUuid: await ids.role(p.role),
510
- ...(p.department ? { departmentUuid: await ids.department(p.department) } : {}),
511
- ...(p.branch ? { branchUuid: await ids.branch(p.branch) } : {}),
497
+ roleUuid: await ids.role(companyUuid, p.role),
498
+ // G-012: resolved within the posting's OWN company (the planner refused any other).
499
+ ...(p.department
500
+ ? { departmentUuid: await ids.department(companyUuid, p.department) }
501
+ : {}),
502
+ ...(p.branch ? { branchUuid: await ids.branch(companyUuid, p.branch) } : {}),
512
503
  });
513
504
  }
514
505
  const postings = await service(posting_service_js_1.PostingService, actor);
@@ -666,7 +657,7 @@ async function runOrgBootstrap(app, opts) {
666
657
  linksPrintedRoots,
667
658
  rootIds,
668
659
  };
669
- // ----- raw writes allowed by the spec (R5, R13), both under the run lock and audited -----
660
+ // ----- the raw write allowed by the spec (R5), under the run lock and audited -----
670
661
  async function inTransaction(work) {
671
662
  const conn = await pool.getConnection();
672
663
  try {
@@ -684,24 +675,6 @@ async function runOrgBootstrap(app, opts) {
684
675
  await conn.release();
685
676
  }
686
677
  }
687
- async function claimRole(roleUuid, tenantUuid) {
688
- const tenantUuids = await Promise.all(file.tenants.map((t) => ids.company(t.name)));
689
- await inTransaction(async (conn) => {
690
- // R1-8: serialise on the role row, then re-check inside the transaction that nobody outside the
691
- // file's tenants holds it (the plan-time read is not enough: it ran before other writers).
692
- await conn.query(`SELECT r_uuid FROM \`${schemas.app}\`.\`roles\` WHERE r_uuid = ? FOR UPDATE`, [roleUuid]);
693
- const held = (await conn.query(`SELECT 1 AS held FROM \`${schemas.app}\`.\`membership_posting\` mp ` +
694
- `JOIN \`${schemas.app}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid ` +
695
- `WHERE mp.mp_role_uuid = ? AND cm.cm_company_uuid NOT IN (` +
696
- `SELECT sc.descendant_uuid FROM \`${schemas.authz}\`.\`scope_closure\` sc WHERE sc.ancestor_uuid IN (${tenantUuids.map(() => '?').join(', ')})) LIMIT 1`, [roleUuid, ...tenantUuids]));
697
- if (held.length > 0) {
698
- const refusal = new Error('role is held outside the file tenants');
699
- refusal.name = 'RoleHeldOutsideError';
700
- throw refusal;
701
- }
702
- await (0, role_owner_transition_js_1.transitionRoleOwner)((sql, params) => conn.query(sql, params), roleUuid, null, tenantUuid, (action, attributes, query) => auditOn(query, tenantUuid, action, 'roles-claim', { ...attributes }), schemas);
703
- });
704
- }
705
678
  async function setPlatformSuperuser(userUuid, tenantUuid) {
706
679
  await inTransaction(async (conn) => {
707
680
  await conn.query(`UPDATE \`${schemas.app}\`.\`users\` SET u_is_platform_superuser = 1 WHERE u_uuid = ?`, [userUuid]);
@@ -725,7 +698,9 @@ class Ids {
725
698
  this.schemas = schemas;
726
699
  }
727
700
  key(kind, name) {
728
- return `${kind}\u0000${kind === 'company' ? (0, org_names_js_1.pathKey)(name) : kind === 'membership' ? name : (0, org_names_js_1.nameKey)(name)}`;
701
+ // membership, department, branch and role names are already composite keys (see `unitName`)
702
+ const raw = kind === 'membership' || kind === 'department' || kind === 'branch' || kind === 'role';
703
+ return `${kind}\u0000${kind === 'company' ? (0, org_names_js_1.pathKey)(name) : raw ? name : (0, org_names_js_1.nameKey)(name)}`;
729
704
  }
730
705
  set(kind, name, id) {
731
706
  this.known.set(this.key(kind, name), id);
@@ -756,14 +731,28 @@ class Ids {
756
731
  const parent = segments.length > 1 ? await this.company(segments.slice(0, -1).join('/')) : null;
757
732
  return this.lookup('company', path, `SELECT c_uuid FROM \`${this.schemas.app}\`.\`companies\` WHERE c_name = ? AND c_parent_uuid <=> ? LIMIT 2`, [name, parent], 'c_uuid');
758
733
  }
759
- department(name) {
760
- return this.lookup('department', name, `SELECT d_uuid FROM \`${this.schemas.app}\`.\`departments\` WHERE d_name = ? LIMIT 2`, [name], 'd_uuid');
734
+ /** G-012: the cache name of a company's own department/branch (owner + normalised name key). */
735
+ unitName(companyUuid, name) {
736
+ return `${companyUuid}\u0000${(0, org_bootstrap_plan_js_1.orgUnitKey)(name)}`;
737
+ }
738
+ setUnit(kind, companyUuid, name, id) {
739
+ this.set(kind, this.unitName(companyUuid, name), id);
740
+ }
741
+ /** `companyUuid`'s own department named `name` (normalised; the column collation matches). */
742
+ department(companyUuid, name) {
743
+ return this.lookup('department', this.unitName(companyUuid, name), `SELECT d_uuid FROM \`${this.schemas.app}\`.\`departments\` WHERE d_company_uuid = ? AND d_name = ? LIMIT 2`, [companyUuid, orgUnitName(name)], 'd_uuid');
744
+ }
745
+ /** `companyUuid`'s own branch named `name`. */
746
+ branch(companyUuid, name) {
747
+ return this.lookup('branch', this.unitName(companyUuid, name), `SELECT b_uuid FROM \`${this.schemas.app}\`.\`branches\` WHERE b_company_uuid = ? AND b_name = ? LIMIT 2`, [companyUuid, orgUnitName(name)], 'b_uuid');
761
748
  }
762
- branch(name) {
763
- return this.lookup('branch', name, `SELECT b_uuid FROM \`${this.schemas.app}\`.\`branches\` WHERE b_name = ? LIMIT 2`, [name], 'b_uuid');
749
+ /** G-013: cache a created role of `companyUuid` (owner + normalised name key). */
750
+ setRole(companyUuid, name, id) {
751
+ this.set('role', this.unitName(companyUuid, name), id);
764
752
  }
765
- role(name) {
766
- return this.lookup('role', name, `SELECT r_uuid FROM \`${this.schemas.app}\`.\`roles\` WHERE r_name = ? LIMIT 2`, [name], 'r_uuid');
753
+ /** `companyUuid`'s own role named `name` (normalised; the column collation matches). */
754
+ role(companyUuid, name) {
755
+ return this.lookup('role', this.unitName(companyUuid, name), `SELECT r_uuid FROM \`${this.schemas.app}\`.\`roles\` WHERE r_owner_company_uuid = ? AND r_name = ? LIMIT 2`, [companyUuid, orgUnitName(name)], 'r_uuid');
767
756
  }
768
757
  user(username) {
769
758
  return this.lookup('user', username, `SELECT u_uuid FROM \`${this.schemas.app}\`.\`users\` WHERE u_username = ? LIMIT 2`, [username], 'u_uuid');
@@ -1,14 +1,37 @@
1
+ import type { ListQueryDto } from '../shared/list-query.js';
1
2
  import { OrgCatalogService } from './org-catalog.service.js';
2
3
  export declare class OrgCatalogController {
3
4
  private readonly catalog;
4
5
  constructor(catalog: OrgCatalogService);
5
- /** A company's departments as `{ uuid, name }` options for the posting editor. */
6
- listCompanyDepartments(companyUuid: string): Promise<{
7
- rows: import("../shared/named-option.js").NamedOption[];
6
+ /** A company's departments — gated on `identity:company_department:read`. */
7
+ listCompanyDepartments(companyUuid: string, query: ListQueryDto): Promise<import("./org-catalog.service.js").OrgUnitList>;
8
+ /** A company's branches — gated on `identity:company_branch:read`. */
9
+ listCompanyBranches(companyUuid: string, query: ListQueryDto): Promise<import("./org-catalog.service.js").OrgUnitList>;
10
+ /** Create a department in a company — `identity:department:create` ⇒ 201 `{ uuid, name }`. */
11
+ createDepartment(companyUuid: string, body: unknown): Promise<{
12
+ uuid: string;
13
+ name: string;
8
14
  }>;
9
- /** A company's branches as `{ uuid, name }` options for the posting editor. */
10
- listCompanyBranches(companyUuid: string): Promise<{
11
- rows: import("../shared/named-option.js").NamedOption[];
15
+ /** Create a branch in a company — `identity:branch:create` ⇒ 201 `{ uuid, name }`. */
16
+ createBranch(companyUuid: string, body: unknown): Promise<{
17
+ uuid: string;
18
+ name: string;
19
+ }>;
20
+ /** Rename a department — `identity:department:update`. */
21
+ renameDepartment(uuid: string, body: unknown): Promise<{
22
+ success: boolean;
23
+ }>;
24
+ /** Rename a branch — `identity:branch:update`. */
25
+ renameBranch(uuid: string, body: unknown): Promise<{
26
+ success: boolean;
27
+ }>;
28
+ /** Delete a department (409 while any posting references it) — `identity:department:delete`. */
29
+ deleteDepartment(uuid: string): Promise<{
30
+ success: boolean;
31
+ }>;
32
+ /** Delete a branch (409 while any posting references it) — `identity:branch:delete`. */
33
+ deleteBranch(uuid: string): Promise<{
34
+ success: boolean;
12
35
  }>;
13
36
  }
14
37
  //# sourceMappingURL=org-catalog.controller.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"org-catalog.controller.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAU7D,qBAEa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iBAAiB;IAEvD,kFAAkF;IAG5E,sBAAsB,CAAiC,WAAW,EAAE,MAAM;;;IAKhF,+EAA+E;IAGzE,mBAAmB,CAAiC,WAAW,EAAE,MAAM;;;CAI9E"}
1
+ {"version":3,"file":"org-catalog.controller.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.controller.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAiB7D,qBAEa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iBAAiB;IAEvD,6EAA6E;IAG7E,sBAAsB,CACY,WAAW,EAAE,MAAM,EAC1C,KAAK,EAAE,YAAY;IAK9B,sEAAsE;IAGtE,mBAAmB,CACe,WAAW,EAAE,MAAM,EAC1C,KAAK,EAAE,YAAY;IAK9B,8FAA8F;IAGxF,gBAAgB,CACY,WAAW,EAAE,MAAM,EAC3C,IAAI,EAAE,OAAO;;;;IAKvB,sFAAsF;IAGhF,YAAY,CAAiC,WAAW,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO;;;;IAI7F,0DAA0D;IAGpD,gBAAgB,CAA0B,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO;;;IAKnF,kDAAkD;IAG5C,YAAY,CAA0B,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO;;;IAK/E,gGAAgG;IAG1F,gBAAgB,CAA0B,IAAI,EAAE,MAAM;;;IAK5D,wFAAwF;IAGlF,YAAY,CAA0B,IAAI,EAAE,MAAM;;;CAIzD"}
@@ -15,29 +15,63 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.OrgCatalogController = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
18
+ const identity_errors_js_1 = require("../shared/identity-errors.js");
18
19
  const guard_1 = require("@astram/guard");
19
20
  const org_catalog_service_js_1 = require("./org-catalog.service.js");
20
- // ── Company-scoped catalogs (dept/branch option lists for the posting editor) ──
21
+ // ── A company's own departments and branches (G-012, spec §3) ──
21
22
  //
22
- // These back the Assignments editor's company-scoped Department/Branch dropdowns.
23
- // Each returns `{ rows: [{ uuid, name }] }` — the uuid is the option VALUE the client
24
- // stores, the name is the display LABEL. Read through the pipe (list the
25
- // company_department/company_branch junction → resolve each catalog uuid to a name in
26
- // the service), so no raw uuid is ever a label.
23
+ // The two GETs back the posting editor's dropdowns and the template's Org structure page:
24
+ // `{ rows: [{ uuid, name, usedBy? }], total, page, limit }` — every row (up to the per-company cap)
25
+ // without `?page&limit&search`, one page with them. The writes create, rename and delete a
26
+ // company's own rows. The service reaches the company or row through the subtree wall first (404
27
+ // out of reach) and stamps the owner from the reached company, never from the body.
28
+ /** The body's `name`, as a string only (the service normalises and validates it). */
29
+ function nameFrom(body, kind) {
30
+ const name = body?.name;
31
+ if (typeof name !== 'string')
32
+ throw new identity_errors_js_1.IdentityBadRequestError(`Enter a ${kind} name.`);
33
+ return name;
34
+ }
27
35
  let OrgCatalogController = class OrgCatalogController {
28
36
  catalog;
29
37
  constructor(catalog) {
30
38
  this.catalog = catalog;
31
39
  }
32
- /** A company's departments as `{ uuid, name }` options for the posting editor. */
33
- async listCompanyDepartments(companyUuid) {
34
- const rows = await this.catalog.listCompanyDepartments(companyUuid);
35
- return { rows };
40
+ /** A company's departments — gated on `identity:company_department:read`. */
41
+ listCompanyDepartments(companyUuid, query) {
42
+ return this.catalog.listCompanyDepartments(companyUuid, query);
36
43
  }
37
- /** A company's branches as `{ uuid, name }` options for the posting editor. */
38
- async listCompanyBranches(companyUuid) {
39
- const rows = await this.catalog.listCompanyBranches(companyUuid);
40
- return { rows };
44
+ /** A company's branches — gated on `identity:company_branch:read`. */
45
+ listCompanyBranches(companyUuid, query) {
46
+ return this.catalog.listCompanyBranches(companyUuid, query);
47
+ }
48
+ /** Create a department in a company — `identity:department:create` ⇒ 201 `{ uuid, name }`. */
49
+ async createDepartment(companyUuid, body) {
50
+ return this.catalog.createDepartment(companyUuid, nameFrom(body, 'department'));
51
+ }
52
+ /** Create a branch in a company — `identity:branch:create` ⇒ 201 `{ uuid, name }`. */
53
+ async createBranch(companyUuid, body) {
54
+ return this.catalog.createBranch(companyUuid, nameFrom(body, 'branch'));
55
+ }
56
+ /** Rename a department — `identity:department:update`. */
57
+ async renameDepartment(uuid, body) {
58
+ await this.catalog.renameDepartment(uuid, nameFrom(body, 'department'));
59
+ return { success: true };
60
+ }
61
+ /** Rename a branch — `identity:branch:update`. */
62
+ async renameBranch(uuid, body) {
63
+ await this.catalog.renameBranch(uuid, nameFrom(body, 'branch'));
64
+ return { success: true };
65
+ }
66
+ /** Delete a department (409 while any posting references it) — `identity:department:delete`. */
67
+ async deleteDepartment(uuid) {
68
+ await this.catalog.deleteDepartment(uuid);
69
+ return { success: true };
70
+ }
71
+ /** Delete a branch (409 while any posting references it) — `identity:branch:delete`. */
72
+ async deleteBranch(uuid) {
73
+ await this.catalog.deleteBranch(uuid);
74
+ return { success: true };
41
75
  }
42
76
  };
43
77
  exports.OrgCatalogController = OrgCatalogController;
@@ -45,18 +79,72 @@ __decorate([
45
79
  (0, common_1.Get)('companies/:companyUuid/departments'),
46
80
  (0, guard_1.Classified)({ typeKey: 'company_department', action: 'read' }),
47
81
  __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
82
+ __param(1, (0, common_1.Query)()),
48
83
  __metadata("design:type", Function),
49
- __metadata("design:paramtypes", [String]),
50
- __metadata("design:returntype", Promise)
84
+ __metadata("design:paramtypes", [String, Object]),
85
+ __metadata("design:returntype", void 0)
51
86
  ], OrgCatalogController.prototype, "listCompanyDepartments", null);
52
87
  __decorate([
53
88
  (0, common_1.Get)('companies/:companyUuid/branches'),
54
89
  (0, guard_1.Classified)({ typeKey: 'company_branch', action: 'read' }),
55
90
  __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
91
+ __param(1, (0, common_1.Query)()),
92
+ __metadata("design:type", Function),
93
+ __metadata("design:paramtypes", [String, Object]),
94
+ __metadata("design:returntype", void 0)
95
+ ], OrgCatalogController.prototype, "listCompanyBranches", null);
96
+ __decorate([
97
+ (0, common_1.Post)('companies/:companyUuid/departments'),
98
+ (0, guard_1.Classified)({ typeKey: 'department', action: 'create' }),
99
+ __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
100
+ __param(1, (0, common_1.Body)()),
101
+ __metadata("design:type", Function),
102
+ __metadata("design:paramtypes", [String, Object]),
103
+ __metadata("design:returntype", Promise)
104
+ ], OrgCatalogController.prototype, "createDepartment", null);
105
+ __decorate([
106
+ (0, common_1.Post)('companies/:companyUuid/branches'),
107
+ (0, guard_1.Classified)({ typeKey: 'branch', action: 'create' }),
108
+ __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
109
+ __param(1, (0, common_1.Body)()),
110
+ __metadata("design:type", Function),
111
+ __metadata("design:paramtypes", [String, Object]),
112
+ __metadata("design:returntype", Promise)
113
+ ], OrgCatalogController.prototype, "createBranch", null);
114
+ __decorate([
115
+ (0, common_1.Put)('departments/:uuid'),
116
+ (0, guard_1.Classified)({ typeKey: 'department', action: 'update' }),
117
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
118
+ __param(1, (0, common_1.Body)()),
119
+ __metadata("design:type", Function),
120
+ __metadata("design:paramtypes", [String, Object]),
121
+ __metadata("design:returntype", Promise)
122
+ ], OrgCatalogController.prototype, "renameDepartment", null);
123
+ __decorate([
124
+ (0, common_1.Put)('branches/:uuid'),
125
+ (0, guard_1.Classified)({ typeKey: 'branch', action: 'update' }),
126
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
127
+ __param(1, (0, common_1.Body)()),
128
+ __metadata("design:type", Function),
129
+ __metadata("design:paramtypes", [String, Object]),
130
+ __metadata("design:returntype", Promise)
131
+ ], OrgCatalogController.prototype, "renameBranch", null);
132
+ __decorate([
133
+ (0, common_1.Delete)('departments/:uuid'),
134
+ (0, guard_1.Classified)({ typeKey: 'department', action: 'delete' }),
135
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
56
136
  __metadata("design:type", Function),
57
137
  __metadata("design:paramtypes", [String]),
58
138
  __metadata("design:returntype", Promise)
59
- ], OrgCatalogController.prototype, "listCompanyBranches", null);
139
+ ], OrgCatalogController.prototype, "deleteDepartment", null);
140
+ __decorate([
141
+ (0, common_1.Delete)('branches/:uuid'),
142
+ (0, guard_1.Classified)({ typeKey: 'branch', action: 'delete' }),
143
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
144
+ __metadata("design:type", Function),
145
+ __metadata("design:paramtypes", [String]),
146
+ __metadata("design:returntype", Promise)
147
+ ], OrgCatalogController.prototype, "deleteBranch", null);
60
148
  exports.OrgCatalogController = OrgCatalogController = __decorate([
61
149
  (0, common_1.Controller)('identity'),
62
150
  (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),