@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
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * G-012 (spec §10): the ONE name rule for departments and branches, shared by the create and rename
4
+ * routes, `OrgCatalogService.ensure*` and the org bootstrap planner, so "Quality Assurance" in a
5
+ * bootstrap file and "Quality Assurance" typed in the UI are the same name.
6
+ *
7
+ * Order (spec §10, as written): Unicode NFC; trim and collapse every whitespace run (JS `\s`, so
8
+ * tab, CR and LF too) to one space; refuse any control character left (U+0000–U+001F,
9
+ * U+007F–U+009F, e.g. NUL or NEL); refuse an empty result or one longer than
10
+ * {@link ORG_UNIT_NAME_MAX} characters (code points, the column width). The template's
11
+ * `validateName` (`pages/org-structure/orgStructure.js`) applies the same order, so the client
12
+ * never accepts a name the server refuses.
13
+ *
14
+ * Case, accents and width are NOT folded here: the stored name keeps what the user typed; the
15
+ * column collation (`utf8mb4_unicode_ci`) decides uniqueness, and the bootstrap planner compares
16
+ * with `nameKey` on top of this.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.ORG_UNIT_NAME_MAX = void 0;
20
+ exports.normaliseOrgUnitName = normaliseOrgUnitName;
21
+ exports.orgUnitNameMessage = orgUnitNameMessage;
22
+ /** The column width of `d_name` / `b_name` (VARCHAR(150), counted in characters). */
23
+ exports.ORG_UNIT_NAME_MAX = 150;
24
+ // eslint-disable-next-line no-control-regex
25
+ const CONTROL = /[\u0000-\u001f\u007f-\u009f]/u;
26
+ /** Normalise a department or branch name, or say why it is refused. Pure. */
27
+ function normaliseOrgUnitName(raw) {
28
+ if (typeof raw !== 'string')
29
+ return { ok: false, reason: 'not_text' };
30
+ const nfc = raw.normalize('NFC');
31
+ const value = nfc.trim().replace(/\s+/gu, ' ');
32
+ if (CONTROL.test(value))
33
+ return { ok: false, reason: 'control_character' };
34
+ if (value.length === 0)
35
+ return { ok: false, reason: 'empty' };
36
+ if ([...value].length > exports.ORG_UNIT_NAME_MAX)
37
+ return { ok: false, reason: 'too_long' };
38
+ return { ok: true, value };
39
+ }
40
+ /** The fixed, user-facing 400 text for a refusal (`kind` is "department" or "branch"). */
41
+ function orgUnitNameMessage(kind, reason) {
42
+ switch (reason) {
43
+ case 'control_character':
44
+ return `A ${kind} name cannot contain control characters.`;
45
+ case 'too_long':
46
+ return `A ${kind} name can be at most ${exports.ORG_UNIT_NAME_MAX} characters.`;
47
+ default:
48
+ return `Enter a ${kind} name.`;
49
+ }
50
+ }
@@ -5,9 +5,26 @@ export declare class RoleController {
5
5
  private readonly roles;
6
6
  constructor(roles: RoleService);
7
7
  /**
8
- * List roles — gated on `identity:role:read` (see class doc). Server-side DataTable:
9
- * `?page&limit&search&sortBy&sortOrder`. Returns `{ total, rows, page, limit }` —
10
- * `total` reflects the FILTERED set so the client can page the true result count.
8
+ * One company's own roles — `identity:role:read`. The company is reached through the walled
9
+ * `company` read first (404 out of reach). `?page&limit&search&sortBy&sortOrder` as `GET /roles`
10
+ * ⇒ `{ total, rows, page, limit }`.
11
+ */
12
+ listCompanyRoles(companyUuid: string, query: ListQueryDto): Promise<{
13
+ total: number;
14
+ rows: readonly import("packages/engine/dist/types.js").RecordView[];
15
+ page: number;
16
+ limit: number;
17
+ }>;
18
+ /** Create a role in a company — `identity:role:create` ⇒ 201 `{ uuid, name }`. */
19
+ createCompanyRole(companyUuid: string, body: unknown): Promise<{
20
+ uuid: string;
21
+ name: string;
22
+ }>;
23
+ /**
24
+ * List roles — gated on `identity:role:read` (see class doc): the roles owned in the caller's
25
+ * reachable subtree (root: every role, ownerless ones included). Server-side DataTable:
26
+ * `?page&limit&search&sortBy&sortOrder`. Returns `{ total, rows, page, limit }` — `total`
27
+ * reflects the FILTERED set so the client can page the true result count.
11
28
  */
12
29
  listRoles(query: ListQueryDto): Promise<{
13
30
  total: number;
@@ -15,19 +32,17 @@ export declare class RoleController {
15
32
  page: number;
16
33
  limit: number;
17
34
  }>;
18
- /** Create a role — gated on `identity:role:create`. */
19
- createRole(req: AuthedRequest, body: {
20
- name?: string;
21
- }): Promise<{
35
+ /**
36
+ * Create a role in the caller's active company (else its tenant) — `identity:role:create`. Kept
37
+ * for existing clients; a caller with no company ⇒ 400. Any owner in the body is ignored.
38
+ */
39
+ createRole(req: AuthedRequest, body: unknown): Promise<{
22
40
  id: import("packages/engine/dist/types.js").RecordId;
23
41
  }>;
24
- /** Read one role — gated on `identity:role:read` (CONTROL-plane; root, the identity
25
- * administrator, may read roles). */
42
+ /** Read one role — gated on `identity:role:read`; walled by owner (404 outside the subtree). */
26
43
  getRole(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
27
- /** Update a role — gated on `identity:role:update`. */
28
- updateRole(req: AuthedRequest, uuid: string, body: {
29
- name?: string;
30
- }): Promise<{
44
+ /** Rename a role — gated on `identity:role:update`. */
45
+ updateRole(req: AuthedRequest, uuid: string, body: unknown): Promise<{
31
46
  success: boolean;
32
47
  }>;
33
48
  /** Delete a role — gated on `identity:role:delete`. */
@@ -1 +1 @@
1
- {"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAM3E;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMjC,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
1
+ {"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAiBvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGH,gBAAgB,CACkB,WAAW,EAAE,MAAM,EAC1C,KAAK,EAAE,YAAY;;;;;;IAK9B,kFAAkF;IAGlF,iBAAiB,CAAiC,WAAW,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO;;;;IAI5F;;;;;OAKG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C;;;OAGG;IAGG,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAKjE,gGAAgG;IAGhG,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,OAAO;;;IAMvB,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
@@ -18,39 +18,61 @@ const guard_1 = require("@astram/guard");
18
18
  const role_service_js_1 = require("./role.service.js");
19
19
  const list_query_js_1 = require("../shared/list-query.js");
20
20
  const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
21
- // Role management is gated on `identity:role:<action>` grants. `role` is CONTROL-plane
22
- // for ALL actions — including read: the capped root is the identity administrator (the
23
- // identity-administrator model), so it may list/read roles AND manage them. Update/delete
24
- // also pass the role-binding rule (a role used outside the caller's subtree is root-only).
21
+ // Role management is gated on `identity:role:<action>` grants. `role` is CONTROL-plane for ALL
22
+ // actions — including read: the capped root is the identity administrator, so it may list/read
23
+ // roles AND manage them. G-013 (ADR-0047): every role is OWNED by one company; reads and writes are
24
+ // walled by that owner (the role object's readScope: out of reach ⇒ 404, ownerless ⇒ root only),
25
+ // and rename/delete also pass the owner rule (`GrantCeiling.assertRoleAdministrable`). The owner is
26
+ // stamped server-side from the reached company (or the caller's active company on `POST /roles`),
27
+ // never from the body. The company sub-paths are static segments declared before `roles/:uuid`;
28
+ // this controller stays registered before `GrantAdminController`.
29
+ /** The body's `name`, untouched (the service normalises and validates it; non-text ⇒ 400). */
30
+ function nameFrom(body) {
31
+ return body?.name;
32
+ }
25
33
  let RoleController = class RoleController {
26
34
  roles;
27
35
  constructor(roles) {
28
36
  this.roles = roles;
29
37
  }
30
38
  /**
31
- * List roles — gated on `identity:role:read` (see class doc). Server-side DataTable:
32
- * `?page&limit&search&sortBy&sortOrder`. Returns `{ total, rows, page, limit }` —
33
- * `total` reflects the FILTERED set so the client can page the true result count.
39
+ * One company's own roles — `identity:role:read`. The company is reached through the walled
40
+ * `company` read first (404 out of reach). `?page&limit&search&sortBy&sortOrder` as `GET /roles`
41
+ * ⇒ `{ total, rows, page, limit }`.
42
+ */
43
+ listCompanyRoles(companyUuid, query) {
44
+ return this.roles.listCompanyRoles(companyUuid, query);
45
+ }
46
+ /** Create a role in a company — `identity:role:create` ⇒ 201 `{ uuid, name }`. */
47
+ createCompanyRole(companyUuid, body) {
48
+ return this.roles.createCompanyRole(companyUuid, nameFrom(body));
49
+ }
50
+ /**
51
+ * List roles — gated on `identity:role:read` (see class doc): the roles owned in the caller's
52
+ * reachable subtree (root: every role, ownerless ones included). Server-side DataTable:
53
+ * `?page&limit&search&sortBy&sortOrder`. Returns `{ total, rows, page, limit }` — `total`
54
+ * reflects the FILTERED set so the client can page the true result count.
34
55
  */
35
56
  async listRoles(query) {
36
57
  const { page, limit, params } = (0, list_query_js_1.parseListQuery)(query);
37
58
  const result = await this.roles.listRoles(params);
38
59
  return { total: result.total, rows: result.rows, page, limit };
39
60
  }
40
- /** Create a role — gated on `identity:role:create`. */
61
+ /**
62
+ * Create a role in the caller's active company (else its tenant) — `identity:role:create`. Kept
63
+ * for existing clients; a caller with no company ⇒ 400. Any owner in the body is ignored.
64
+ */
41
65
  async createRole(req, body) {
42
- // The owner is derived from the principal in the service; any owner in the body is ignored.
43
- const id = await this.roles.createRole(body?.name ?? 'Untitled', req.principal);
66
+ const id = await this.roles.createRole(nameFrom(body), req.principal);
44
67
  return { id };
45
68
  }
46
- /** Read one role — gated on `identity:role:read` (CONTROL-plane; root, the identity
47
- * administrator, may read roles). */
69
+ /** Read one role — gated on `identity:role:read`; walled by owner (404 outside the subtree). */
48
70
  getRole(uuid) {
49
71
  return this.roles.getRole(uuid);
50
72
  }
51
- /** Update a role — gated on `identity:role:update`. */
73
+ /** Rename a role — gated on `identity:role:update`. */
52
74
  async updateRole(req, uuid, body) {
53
- await this.roles.updateRole(req.principal, uuid, { name: body?.name });
75
+ await this.roles.updateRole(req.principal, uuid, { name: nameFrom(body) });
54
76
  return { success: true };
55
77
  }
56
78
  /** Delete a role — gated on `identity:role:delete`. */
@@ -60,6 +82,24 @@ let RoleController = class RoleController {
60
82
  }
61
83
  };
62
84
  exports.RoleController = RoleController;
85
+ __decorate([
86
+ (0, common_1.Get)('companies/:companyUuid/roles'),
87
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
88
+ __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
89
+ __param(1, (0, common_1.Query)()),
90
+ __metadata("design:type", Function),
91
+ __metadata("design:paramtypes", [String, Object]),
92
+ __metadata("design:returntype", void 0)
93
+ ], RoleController.prototype, "listCompanyRoles", null);
94
+ __decorate([
95
+ (0, common_1.Post)('companies/:companyUuid/roles'),
96
+ (0, guard_1.Classified)({ typeKey: 'role', action: 'create' }),
97
+ __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
98
+ __param(1, (0, common_1.Body)()),
99
+ __metadata("design:type", Function),
100
+ __metadata("design:paramtypes", [String, Object]),
101
+ __metadata("design:returntype", void 0)
102
+ ], RoleController.prototype, "createCompanyRole", null);
63
103
  __decorate([
64
104
  (0, common_1.Get)('roles'),
65
105
  (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
@@ -1,18 +1,26 @@
1
- import { RecordsService, type RecordId } from '@astram/engine';
1
+ import { RecordsService, type RecordId, type RecordView } from '@astram/engine';
2
2
  import type { Principal } from '@astram/core';
3
- import { type ListParams } from '../shared/list-query.js';
3
+ import { type ListParams, type ListQueryDto } from '../shared/list-query.js';
4
4
  import { GrantCeiling } from '../authz/grant-ceiling.js';
5
5
  import { GrantAdminService } from '../authz/grant-admin.service.js';
6
6
  import { type PostingVersionBump } from '../membership/posting-version-bump.js';
7
+ import { type OrgUnitNameRefusal } from '../org-structure/org-unit-name.js';
8
+ /** The 400 of `POST /identity/roles` for a caller with no company to own the role (spec §10). */
9
+ export declare const NO_ROLE_COMPANY = "Choose a company for the role.";
10
+ /** The fixed, user-facing 400 text for a refused role name (the G-012 rule, spec §10). */
11
+ export declare function roleNameMessage(reason: OrgUnitNameRefusal): string;
7
12
  /**
8
- * RoleService — role CRUD on the paved road, via the framework's RecordsService
9
- * facade. The facade supplies the acting identity (the request principal) for the pipe;
10
- * no raw SQL, no RECORD_ENGINE.
13
+ * RoleService — role CRUD on the paved road, via the framework's RecordsService facade (the engine
14
+ * pipe: gate, wall, validate, write, audit). No raw SQL, no RECORD_ENGINE.
11
15
  *
12
- * Roles are a GLOBAL catalog, so renaming or deleting one changes every tenant that uses it (a
13
- * delete cascades its postings). Security hardening #1 (D1 interim rule): update/delete first pass
14
- * `GrantCeiling.assertRoleAdministrable` — the role must be bound inside the actor's subtree and
15
- * nowhere outside it (a shared role is root-only). The engine's write wall still applies after.
16
+ * G-013 (spec §5.A, ADR-0047): every role is OWNED by one company (`ownerCompanyUuid`, immutable),
17
+ * stamped here from the reached company (or the caller's active company on the legacy route), never
18
+ * from a request body (R1). The role object's `readScope` walls every read and write by that owner
19
+ * (R3: out of reach ⇒ 404; an ownerless legacy row is root only, R5). Names follow the G-012 rule
20
+ * (`normaliseOrgUnitName`) and are unique within the owner company under the column collation; the
21
+ * check runs under a lock inside one transaction and the per-company unique index is the backstop
22
+ * (a driver 1062 ⇒ the same 409, R6). Rename and delete first pass
23
+ * `GrantCeiling.assertRoleAdministrable` (owner in the actor's subtree, else 404).
16
24
  */
17
25
  export declare class RoleService {
18
26
  private readonly records;
@@ -21,26 +29,55 @@ export declare class RoleService {
21
29
  private readonly grants?;
22
30
  constructor(records: RecordsService, ceiling: GrantCeiling, versionBump?: (PostingVersionBump | null) | undefined, grants?: (GrantAdminService | null) | undefined);
23
31
  /**
24
- * Create a role. The owner is derived here from the actor, never from a client value: a non-root
25
- * actor's role is owned by its active company (else its tenant); root's role is shared (no owner).
26
- * A root-flagged actor bound to a declared operation (the org bootstrap's `roles` step, Shield #9)
27
- * is anchored at the tenant it creates roles for, so its role is owned by that tenant.
28
- * No actor (internal callers) ⇒ no owner field.
32
+ * Create a role (`POST /identity/roles`, kept for existing clients). Owner = the caller's active
33
+ * company, else its tenant; a caller with neither ⇒ 400 "Choose a company for the role." The
34
+ * company is then reached and locked exactly as {@link createCompanyRole} does.
35
+ *
36
+ * `companyUuid` is for the org bootstrap only (spec §5.E): the bound `roles` step names the
37
+ * DECLARING company explicitly and holds no company key (gate 7), so that company is not read or
38
+ * locked here; the engine's create wall checks the owner against the actor's reach, the duplicate
39
+ * check runs in the transaction and the unique index is the backstop.
40
+ */
41
+ createRole(rawName: unknown, actor: Principal, companyUuid?: string): Promise<RecordId>;
42
+ /**
43
+ * Create a role in `companyUuid` (`POST /identity/companies/:companyUuid/roles`): normalise the
44
+ * name; in one transaction lock the company, reach it through the wall (404), refuse a name the
45
+ * company already holds (409 naming the stored role) and create the role owned by the REACHED
46
+ * company. Returns `{ uuid, name }` (the stored name).
29
47
  */
30
- createRole(name: string, actor?: Principal): Promise<RecordId>;
31
- /** List roles (tenant-scoped by the pipe), with paging/search/sort. */
48
+ createCompanyRole(companyUuid: string, rawName: unknown): Promise<{
49
+ uuid: string;
50
+ name: string;
51
+ }>;
52
+ /**
53
+ * One company's own roles (`GET /identity/companies/:companyUuid/roles`): the company is reached
54
+ * through the walled read first (404 when out of reach), then its roles are listed with the same
55
+ * paging, search and sort allowlists as {@link listRoles}.
56
+ */
57
+ listCompanyRoles(companyUuid: string, query?: ListQueryDto): Promise<{
58
+ total: number;
59
+ rows: readonly RecordView[];
60
+ page: number;
61
+ limit: number;
62
+ }>;
63
+ /** List roles (walled by owner through the role object's readScope), with paging/search/sort. */
32
64
  listRoles(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
33
- /** Read a single role. */
34
- getRole(uuid: string): Promise<import("@astram/engine").RecordView | null>;
35
- /** Rename a role — only a role the actor may administer (binding rule). Patch whitelisted. */
65
+ /** Read a single role (walled by owner). */
66
+ getRole(uuid: string): Promise<RecordView | null>;
67
+ /**
68
+ * Rename a role — only a role the actor may administer (owner in reach, else 404). The name goes
69
+ * through the name rule (400); in one transaction the role row is locked, re-read through the wall
70
+ * (404; ownerless ⇒ root only), and a name another role of the same company holds is refused (409
71
+ * naming the stored role). The same name ⇒ no write. Only `name` is ever written.
72
+ */
36
73
  updateRole(actor: Principal, uuid: string, patch: {
37
74
  name?: unknown;
38
75
  }): Promise<void>;
39
76
  /**
40
- * Delete a role — only a role the actor may administer (binding rule) AND whose every holder the
77
+ * Delete a role — only a role the actor may administer (owner rule) AND whose every holder the
41
78
  * actor dominates (Phase 8 #2: the delete cascades the holders' postings, stripping their
42
79
  * authority). In ONE transaction: lock the role row (the posting and grant paths lock the same
43
- * row first), re-check the binding rule and holder dominance against what is committed now, read
80
+ * row first), re-check the owner rule and holder dominance against what is committed now, read
44
81
  * the holder tenants (before the cascade removes the postings), then delete. After the commit,
45
82
  * every holder tenant's permission version is bumped so cached roles drop at the next request.
46
83
  * The bump runs after the commit, never before: an earlier bump would let a session reload the
@@ -64,5 +101,23 @@ export declare class RoleService {
64
101
  * (the engine mints it), so it can never pick up anything left behind.
65
102
  */
66
103
  deleteRole(actor: Principal, uuid: string): Promise<void>;
104
+ private validName;
105
+ /** Lock the company FIRST, reach it through the wall (404), then create the role it owns. */
106
+ private createInCompany;
107
+ /** Duplicate check, then the role owned by `owner`, on the caller's transaction. */
108
+ private insertRole;
109
+ /**
110
+ * Refuse a name another role of the same company already holds. The engine's equality runs under
111
+ * the column collation (`utf8mb4_unicode_ci`: case, accents and width fold), so "manager" finds
112
+ * "Manager"; the 409 names the STORED clashing role.
113
+ */
114
+ private assertNameFree;
115
+ private findByName;
116
+ /**
117
+ * Run a create/rename; a driver duplicate key (the per-company unique index, the backstop for a
118
+ * concurrent write) becomes the same 409 naming the stored role. Errno only: the driver's message
119
+ * holds the value and is never echoed (KI #87).
120
+ */
121
+ private mappingDuplicates;
67
122
  }
68
123
  //# sourceMappingURL=role.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAe,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAK/C;;;;;;;;;GASG;AACH,qBACa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAXP,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAKrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,MAAM,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA;IAGpD;;;;;;OAMG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQ9D,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,8FAA8F;IACxF,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1F;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBhE"}
1
+ {"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EAGd,KAAK,QAAQ,EACb,KAAK,UAAU,EAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,YAAY,EAClB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAM/C,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,mCAAmC,CAAC;AAK3C,iGAAiG;AACjG,eAAO,MAAM,eAAe,mCAAmC,CAAC;AAEhE,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CASlE;AAmCD;;;;;;;;;;;;GAYG;AACH,qBACa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAXP,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAKrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,MAAM,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA;IAGpD;;;;;;;;;OASG;IACG,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwB7F;;;;;OAKG;IACG,iBAAiB,CACrB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAI1C;;;;OAIG;IACG,gBAAgB,CACpB,WAAW,EAAE,MAAM,EACnB,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,UAAU,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAYvF,iGAAiG;IACjG,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,4CAA4C;IAC5C,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB;;;;;OAKG;IACG,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB1F;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACG,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB/D,OAAO,CAAC,SAAS;IAMjB,6FAA6F;YAC/E,eAAe;IAoB7B,oFAAoF;YACtE,UAAU;IAKxB;;;;OAIG;YACW,cAAc;YAUd,UAAU;IAWxB;;;;OAIG;YACW,iBAAiB;CAsBhC"}