@astram/identity 0.4.0 → 0.6.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 (184) hide show
  1. package/dist/company-membership.object.d.ts.map +1 -1
  2. package/dist/company-membership.object.js +3 -2
  3. package/dist/company.object.d.ts.map +1 -1
  4. package/dist/company.object.js +8 -3
  5. package/dist/features/access/index.d.ts.map +1 -1
  6. package/dist/features/access/index.js +1 -5
  7. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  8. package/dist/features/audit/audit-read.service.js +13 -6
  9. package/dist/features/audit/audit.controller.d.ts.map +1 -1
  10. package/dist/features/audit/audit.controller.js +2 -0
  11. package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
  12. package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
  13. package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
  14. package/dist/features/authz/catalog-reconcile.job.js +1 -3
  15. package/dist/features/authz/db-permission-facts.adapter.d.ts +16 -0
  16. package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
  17. package/dist/features/authz/db-permission-facts.adapter.js +46 -2
  18. package/dist/features/authz/grant-admin.controller.d.ts +8 -5
  19. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
  20. package/dist/features/authz/grant-admin.controller.js +20 -18
  21. package/dist/features/authz/grant-admin.service.d.ts +72 -16
  22. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  23. package/dist/features/authz/grant-admin.service.js +232 -86
  24. package/dist/features/authz/grant-ceiling.d.ts +100 -1
  25. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  26. package/dist/features/authz/grant-ceiling.js +264 -2
  27. package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
  28. package/dist/features/authz/mariadb-catalog-store.js +15 -3
  29. package/dist/features/authz/permission-version.bumper.d.ts +52 -1
  30. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
  31. package/dist/features/authz/permission-version.bumper.js +119 -20
  32. package/dist/features/authz/record-authorization.d.ts +12 -0
  33. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  34. package/dist/features/authz/record-authorization.js +49 -0
  35. package/dist/features/authz/role-binding.d.ts +36 -0
  36. package/dist/features/authz/role-binding.d.ts.map +1 -0
  37. package/dist/features/authz/role-binding.js +53 -0
  38. package/dist/features/company/company-hierarchy.service.d.ts +108 -0
  39. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
  40. package/dist/features/company/company-hierarchy.service.js +277 -0
  41. package/dist/features/company/company.controller.d.ts +23 -5
  42. package/dist/features/company/company.controller.d.ts.map +1 -1
  43. package/dist/features/company/company.controller.js +47 -12
  44. package/dist/features/company/company.service.d.ts +28 -12
  45. package/dist/features/company/company.service.d.ts.map +1 -1
  46. package/dist/features/company/company.service.js +61 -14
  47. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
  48. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
  49. package/dist/features/identity-feature.module.d.ts +2 -0
  50. package/dist/features/identity-feature.module.d.ts.map +1 -1
  51. package/dist/features/identity-feature.module.js +72 -5
  52. package/dist/features/membership/membership.controller.d.ts +6 -5
  53. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  54. package/dist/features/membership/membership.controller.js +34 -22
  55. package/dist/features/membership/membership.service.d.ts +20 -14
  56. package/dist/features/membership/membership.service.d.ts.map +1 -1
  57. package/dist/features/membership/membership.service.js +62 -29
  58. package/dist/features/membership/posting-version-bump.d.ts +12 -0
  59. package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
  60. package/dist/features/membership/posting.service.d.ts +20 -24
  61. package/dist/features/membership/posting.service.d.ts.map +1 -1
  62. package/dist/features/membership/posting.service.js +101 -64
  63. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  64. package/dist/features/org-structure/org-catalog.controller.js +2 -0
  65. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  66. package/dist/features/org-structure/org-catalog.service.js +4 -1
  67. package/dist/features/policy/policy.controller.d.ts.map +1 -1
  68. package/dist/features/policy/policy.controller.js +2 -0
  69. package/dist/features/role/role.controller.d.ts +3 -2
  70. package/dist/features/role/role.controller.d.ts.map +1 -1
  71. package/dist/features/role/role.controller.js +15 -10
  72. package/dist/features/role/role.service.d.ts +39 -8
  73. package/dist/features/role/role.service.d.ts.map +1 -1
  74. package/dist/features/role/role.service.js +77 -10
  75. package/dist/features/scope/scope-closure.service.d.ts +54 -4
  76. package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
  77. package/dist/features/scope/scope-closure.service.js +237 -85
  78. package/dist/features/service-account/service-account.controller.d.ts +3 -2
  79. package/dist/features/service-account/service-account.controller.d.ts.map +1 -1
  80. package/dist/features/service-account/service-account.controller.js +29 -15
  81. package/dist/features/service-account/service-account.service.d.ts +29 -7
  82. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  83. package/dist/features/service-account/service-account.service.js +73 -12
  84. package/dist/features/service-account/service-account.store.d.ts +15 -0
  85. package/dist/features/service-account/service-account.store.d.ts.map +1 -1
  86. package/dist/features/service-account/service-account.store.js +29 -0
  87. package/dist/features/share/share.controller.d.ts.map +1 -1
  88. package/dist/features/share/share.controller.js +2 -0
  89. package/dist/features/share/share.service.d.ts.map +1 -1
  90. package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
  91. package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
  92. package/dist/features/shared/identity-error.interceptor.js +82 -0
  93. package/dist/features/shared/identity-errors.d.ts +23 -0
  94. package/dist/features/shared/identity-errors.d.ts.map +1 -0
  95. package/dist/features/shared/identity-errors.js +41 -0
  96. package/dist/features/user/session-revocation-check.d.ts +11 -0
  97. package/dist/features/user/session-revocation-check.d.ts.map +1 -0
  98. package/dist/features/user/session-revocation-check.js +21 -0
  99. package/dist/features/user/user.controller.d.ts +11 -6
  100. package/dist/features/user/user.controller.d.ts.map +1 -1
  101. package/dist/features/user/user.controller.js +35 -17
  102. package/dist/features/user/user.service.d.ts +55 -14
  103. package/dist/features/user/user.service.d.ts.map +1 -1
  104. package/dist/features/user/user.service.js +143 -16
  105. package/dist/index.d.ts +7 -5
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +7 -6
  108. package/dist/internal.d.ts +10 -0
  109. package/dist/internal.d.ts.map +1 -0
  110. package/dist/internal.js +13 -0
  111. package/dist/membership-posting.object.d.ts.map +1 -1
  112. package/dist/membership-posting.object.js +4 -2
  113. package/dist/objects.d.ts.map +1 -1
  114. package/dist/objects.js +13 -3
  115. package/dist/role.object.d.ts.map +1 -1
  116. package/dist/role.object.js +3 -0
  117. package/dist/user.object.d.ts.map +1 -1
  118. package/dist/user.object.js +3 -0
  119. package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
  120. package/package.json +15 -4
  121. package/dist/auth-facts-keys.js.map +0 -1
  122. package/dist/branch.object.js.map +0 -1
  123. package/dist/company-branch.object.js.map +0 -1
  124. package/dist/company-department.object.js.map +0 -1
  125. package/dist/company-membership.object.js.map +0 -1
  126. package/dist/company.object.js.map +0 -1
  127. package/dist/cross-tenant-grant.object.js.map +0 -1
  128. package/dist/department.object.js.map +0 -1
  129. package/dist/features/access/access-admin.access.js.map +0 -1
  130. package/dist/features/access/company-admin.access.js.map +0 -1
  131. package/dist/features/access/index.js.map +0 -1
  132. package/dist/features/access/user-management.access.js.map +0 -1
  133. package/dist/features/audit/audit-read.service.js.map +0 -1
  134. package/dist/features/audit/audit.controller.js.map +0 -1
  135. package/dist/features/audit/directory-label-resolver.js.map +0 -1
  136. package/dist/features/audit/mariadb-audit-reader.js.map +0 -1
  137. package/dist/features/authz/access-bundle-reconcile.job.js.map +0 -1
  138. package/dist/features/authz/access-bundle-store.js.map +0 -1
  139. package/dist/features/authz/catalog-bootstrap.provider.js.map +0 -1
  140. package/dist/features/authz/catalog-reconcile.job.js.map +0 -1
  141. package/dist/features/authz/db-permission-facts.adapter.js.map +0 -1
  142. package/dist/features/authz/grant-admin.controller.js.map +0 -1
  143. package/dist/features/authz/grant-admin.service.js.map +0 -1
  144. package/dist/features/authz/grant-ceiling.js.map +0 -1
  145. package/dist/features/authz/mariadb-catalog-store.js.map +0 -1
  146. package/dist/features/authz/permission-version.bumper.js.map +0 -1
  147. package/dist/features/authz/record-authorization.js.map +0 -1
  148. package/dist/features/authz/session-facts.reader.js.map +0 -1
  149. package/dist/features/company/company.controller.js.map +0 -1
  150. package/dist/features/company/company.service.js.map +0 -1
  151. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js.map +0 -1
  152. package/dist/features/cross-tenant/cross-tenant-grant.controller.js.map +0 -1
  153. package/dist/features/cross-tenant/grant-authorize.js.map +0 -1
  154. package/dist/features/identity-feature.module.js.map +0 -1
  155. package/dist/features/membership/membership-checker.js.map +0 -1
  156. package/dist/features/membership/membership.controller.js.map +0 -1
  157. package/dist/features/membership/membership.service.js.map +0 -1
  158. package/dist/features/membership/posting-version-bump.js.map +0 -1
  159. package/dist/features/membership/posting.service.js.map +0 -1
  160. package/dist/features/org-structure/org-catalog.controller.js.map +0 -1
  161. package/dist/features/org-structure/org-catalog.service.js.map +0 -1
  162. package/dist/features/policy/policy.controller.js.map +0 -1
  163. package/dist/features/policy/policy.service.js.map +0 -1
  164. package/dist/features/role/role.controller.js.map +0 -1
  165. package/dist/features/role/role.service.js.map +0 -1
  166. package/dist/features/scope/scope-closure.service.js.map +0 -1
  167. package/dist/features/scope/scope-subtree.resolver.js.map +0 -1
  168. package/dist/features/share/share.controller.js.map +0 -1
  169. package/dist/features/share/share.service.js.map +0 -1
  170. package/dist/features/shared/http.js.map +0 -1
  171. package/dist/features/shared/list-query.js.map +0 -1
  172. package/dist/features/shared/named-option.js.map +0 -1
  173. package/dist/features/shared/record-resolver.js.map +0 -1
  174. package/dist/features/user/user.controller.js.map +0 -1
  175. package/dist/features/user/user.service.js.map +0 -1
  176. package/dist/index.js.map +0 -1
  177. package/dist/membership-posting.object.js.map +0 -1
  178. package/dist/migrations-dir.js.map +0 -1
  179. package/dist/objects.js.map +0 -1
  180. package/dist/record-share.object.js.map +0 -1
  181. package/dist/role.object.js.map +0 -1
  182. package/dist/schema.js.map +0 -1
  183. package/dist/shared-resource.object.js.map +0 -1
  184. package/dist/user.object.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAca,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,WAAW,EAAG,MAAM,CAAC;IAGrB,QAAQ,EAAG,OAAO,CAAC;IAGnB,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAca,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAId,QAAQ,EAAG,MAAM,CAAC;IAGlB,WAAW,EAAG,MAAM,CAAC;IAGrB,QAAQ,EAAG,OAAO,CAAC;IAGnB,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
@@ -21,6 +21,7 @@ const schema_1 = require("./schema");
21
21
  */
22
22
  let CompanyMembership = class CompanyMembership {
23
23
  uuid;
24
+ // immutable (ADR-0034): a membership never changes its user or company after create.
24
25
  userUuid;
25
26
  companyUuid;
26
27
  isActive;
@@ -33,11 +34,11 @@ __decorate([
33
34
  __metadata("design:type", String)
34
35
  ], CompanyMembership.prototype, "uuid", void 0);
35
36
  __decorate([
36
- (0, engine_1.Field)({ column: 'cm_user_uuid', type: 'uuid', required: true }),
37
+ (0, engine_1.Field)({ column: 'cm_user_uuid', type: 'uuid', required: true, immutable: true }),
37
38
  __metadata("design:type", String)
38
39
  ], CompanyMembership.prototype, "userUuid", void 0);
39
40
  __decorate([
40
- (0, engine_1.Field)({ column: 'cm_company_uuid', type: 'uuid', required: true }),
41
+ (0, engine_1.Field)({ column: 'cm_company_uuid', type: 'uuid', required: true, immutable: true }),
41
42
  __metadata("design:type", String)
42
43
  ], CompanyMembership.prototype, "companyUuid", void 0);
43
44
  __decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,qBAmBa,OAAO;IAElB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAGd,UAAU,EAAG,MAAM,CAAC;IAGpB,IAAI,EAAG,MAAM,CAAC;IAGd,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,qBAsBa,OAAO;IAElB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAKd,UAAU,EAAG,MAAM,CAAC;IAGpB,IAAI,EAAG,MAAM,CAAC;IAGd,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
@@ -16,6 +16,8 @@ const schema_1 = require("./schema");
16
16
  let Company = class Company {
17
17
  uuid;
18
18
  name;
19
+ // immutable (ADR-0034): re-parenting is a hierarchy move (cycle check, closure rebuild,
20
+ // session bump), never a side effect of a generic patch.
19
21
  parentUuid;
20
22
  type;
21
23
  status;
@@ -31,11 +33,11 @@ __decorate([
31
33
  __metadata("design:type", String)
32
34
  ], Company.prototype, "name", void 0);
33
35
  __decorate([
34
- (0, engine_1.Field)({ column: 'c_parent_uuid', type: 'uuid' }),
36
+ (0, engine_1.Field)({ column: 'c_parent_uuid', type: 'uuid', immutable: true }),
35
37
  __metadata("design:type", String)
36
38
  ], Company.prototype, "parentUuid", void 0);
37
39
  __decorate([
38
- (0, engine_1.Field)({ column: 'c_type', type: 'string' }),
40
+ (0, engine_1.Field)({ column: 'c_type', type: 'string', immutable: true }),
39
41
  __metadata("design:type", String)
40
42
  ], Company.prototype, "type", void 0);
41
43
  __decorate([
@@ -63,7 +65,10 @@ exports.Company = Company = __decorate([
63
65
  // the node (`c_uuid`), so a non-root LIST/GET is bounded to the caller's reachable subtree
64
66
  // (`c_uuid` IN my subtree). DECOUPLED from writes (no `trusted` change) — this filters
65
67
  // reads only. Root spans all.
66
- readScope: { kind: 'nodeColumn', column: 'c_uuid' },
68
+ // create:'unscoped' (ADR-0034): a NEW company's c_uuid is not in anyone's subtree until the
69
+ // scope closure lists it, so the post-insert create wall would deny every create. Opted out
70
+ // explicitly; the company service owns where a non-root create may attach (parent stamping).
71
+ readScope: { kind: 'nodeColumn', column: 'c_uuid', create: 'unscoped' },
67
72
  }),
68
73
  (0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'c_uuid' })
69
74
  ], Company);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/features/access/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,iCAIjC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/features/access/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,iCAAgE,CAAC"}
@@ -19,8 +19,4 @@ Object.defineProperty(exports, "CompanyAdminAccess", { enumerable: true, get: fu
19
19
  var access_admin_access_2 = require("./access-admin.access");
20
20
  Object.defineProperty(exports, "AccessAdminAccess", { enumerable: true, get: function () { return access_admin_access_2.AccessAdminAccess; } });
21
21
  /** All identity access bundles, collected into the engine at boot. */
22
- exports.identityAccessBundles = [
23
- user_management_access_1.UserManagementAccess,
24
- company_admin_access_1.CompanyAdminAccess,
25
- access_admin_access_1.AccessAdminAccess,
26
- ];
22
+ exports.identityAccessBundles = [user_management_access_1.UserManagementAccess, company_admin_access_1.CompanyAdminAccess, access_admin_access_1.AccessAdminAccess];
@@ -1 +1 @@
1
- {"version":3,"file":"audit-read.service.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit-read.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,wFAAwF;AACxF,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3C;AAMD;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAFjB,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,aAAa,EACrB,iBAAiB,EAAE,iBAAiB;IAGvD;;;;;;OAMG;IACG,QAAQ,CACZ,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GACnG,OAAO,CAAC;QAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAyD5E;;;OAGG;IACH,OAAO,CAAC,QAAQ;CAUjB"}
1
+ {"version":3,"file":"audit-read.service.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit-read.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,wFAAwF;AACxF,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3C;AAMD;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAFjB,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,aAAa,EACrB,iBAAiB,EAAE,iBAAiB;IAGvD;;;;;;OAMG;IACG,QAAQ,CACZ,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE;QACP,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,GACA,OAAO,CAAC;QAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAkE5E;;;OAGG;IACH,OAAO,CAAC,QAAQ;CAUjB"}
@@ -33,7 +33,10 @@ class AuditReadService {
33
33
  // and tenantFilter is ignored (cannot widen or cross).
34
34
  let tenantId;
35
35
  if (principal.isRoot) {
36
- tenantId = filters.tenantFilter && filters.tenantFilter.length > 0 ? filters.tenantFilter : ALL_TENANTS;
36
+ tenantId =
37
+ filters.tenantFilter && filters.tenantFilter.length > 0
38
+ ? filters.tenantFilter
39
+ : ALL_TENANTS;
37
40
  }
38
41
  else {
39
42
  tenantId = principal.tenantId;
@@ -47,9 +50,13 @@ class AuditReadService {
47
50
  // Clamp the client-supplied limit to [1, MAX_LIMIT]; a non-finite value falls back to
48
51
  // DEFAULT_LIMIT (only an in-range-but-out-of-bounds number gets floored/capped). Floor the
49
52
  // page to >= 1. offset uses the CLAMPED limit, so the window never diverges from read.limit.
50
- const requestedLimit = Number.isFinite(filters.limit) ? filters.limit : DEFAULT_LIMIT;
53
+ const requestedLimit = Number.isFinite(filters.limit)
54
+ ? filters.limit
55
+ : DEFAULT_LIMIT;
51
56
  const limit = Math.min(Math.max(1, requestedLimit), MAX_LIMIT);
52
- const page = Number.isFinite(filters.page) && filters.page >= 1 ? Math.floor(filters.page) : 1;
57
+ const page = Number.isFinite(filters.page) && filters.page >= 1
58
+ ? Math.floor(filters.page)
59
+ : 1;
53
60
  const offset = (page - 1) * limit;
54
61
  // ONE scope query — spread into BOTH the count and the page read so their tenant/type/actor
55
62
  // WHERE is identical. A non-root's total therefore counts ONLY its own tenant (invariant #4).
@@ -69,9 +76,9 @@ class AuditReadService {
69
76
  const rows = await Promise.all(events.map(async (event) => ({
70
77
  occurredAt: event.occurredAtMs,
71
78
  actorLabel: await this.labels.resolve('principal', event.actorId),
72
- action: event.action?.key
73
- ?? event.action
74
- ?? '(event)',
79
+ action: event.action?.key ??
80
+ event.action ??
81
+ '(event)',
75
82
  tenantLabel: await this.labels.resolve('tenant', event.tenantId),
76
83
  detail: this.stripIds(event.attributes ?? {}),
77
84
  })));
@@ -1 +1 @@
1
- {"version":3,"file":"audit.controller.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit.controller.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAIhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAW3D;;;gEAGgE;AAChE,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC7C;AAED;;;;GAIG;AACH,qBACa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,gBAAgB;IAIhD,IAAI,CACD,GAAG,EAAE,aAAa,EAChB,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5F,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CA8B3F"}
1
+ {"version":3,"file":"audit.controller.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit.controller.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAIhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAW3D;;;gEAGgE;AAChE,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,GAAG;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC7C;AAED;;;;GAIG;AACH,qBAEa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,gBAAgB;IAIhD,IAAI,CACD,GAAG,EAAE,aAAa,EAChB,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5F,OAAO,CAAC;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CA8B3F"}
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.AuditController = void 0;
16
16
  // audit.controller.ts
17
17
  const common_1 = require("@nestjs/common");
18
+ const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
18
19
  const guard_1 = require("@astram/guard");
19
20
  // `AuditReadService` is the DI token for the constructor injection below, so it MUST be a
20
21
  // value import — a type-only import is erased at compile time, leaving NestJS with
@@ -69,5 +70,6 @@ __decorate([
69
70
  ], AuditController.prototype, "list", null);
70
71
  exports.AuditController = AuditController = __decorate([
71
72
  (0, common_1.Controller)('identity'),
73
+ (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
72
74
  __metadata("design:paramtypes", [audit_read_service_js_1.AuditReadService])
73
75
  ], AuditController);
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-audit-reader.d.ts","sourceRoot":"","sources":["../../../src/features/audit/mariadb-audit-reader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAgBD;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,WAAW;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,EAAE,EAAE,cAAc,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAKxD,IAAI,CAAC,KAAK,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAgClC;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1F;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,OAAO;CA+BhB"}
1
+ {"version":3,"file":"mariadb-audit-reader.d.ts","sourceRoot":"","sources":["../../../src/features/audit/mariadb-audit-reader.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAgBD;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,WAAW;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,EAAE,EAAE,cAAc,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO;IAKxD,IAAI,CAAC,KAAK,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC;IAgClC;;;;;;OAMG;IACG,KAAK,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAY1F;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,OAAO;CA+BhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"access-bundle-reconcile.job.d.ts","sourceRoot":"","sources":["../../../src/features/authz/access-bundle-reconcile.job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKvD,yCAAyC;AACzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,wBAAwB;IAKjC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,OAAO,CAAS;gBAGL,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,SAAS,gBAAgB,EAAE;IAKvD,6EAA6E;IACvE,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAuDpE,sEAAsE;YACxD,wBAAwB;CAMvC"}
1
+ {"version":3,"file":"access-bundle-reconcile.job.d.ts","sourceRoot":"","sources":["../../../src/features/authz/access-bundle-reconcile.job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKvD,yCAAyC;AACzC,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,wBAAwB;IAKjC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,OAAO,CAAS;gBAGL,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,SAAS,gBAAgB,EAAE;IAKvD,6EAA6E;IACvE,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAqDpE,sEAAsE;YACxD,wBAAwB;CAMvC"}
@@ -1 +1 @@
1
- {"version":3,"file":"catalog-reconcile.job.d.ts","sourceRoot":"","sources":["../../../src/features/authz/catalog-reconcile.job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD;;;;;;;GAOG;AACH,qBAAa,mBAAmB;IAMN,OAAO,CAAC,QAAQ,CAAC,QAAQ;IALjD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,IAAI,EAAmB,QAAQ,EAAE,cAAc;IAIjE,6EAA6E;IACvE,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAmB9D,SAAS,IAAI,eAAe,GAAG,IAAI;CAGpC"}
1
+ {"version":3,"file":"catalog-reconcile.job.d.ts","sourceRoot":"","sources":["../../../src/features/authz/catalog-reconcile.job.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD;;;;;;;GAOG;AACH,qBAAa,mBAAmB;IAQ5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAP3B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAS;gBAGtB,IAAI,EAAE,IAAI,EACO,QAAQ,EAAE,cAAc;IAK3C,6EAA6E;IACvE,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAiB9D,SAAS,IAAI,eAAe,GAAG,IAAI;CAGpC"}
@@ -29,9 +29,7 @@ class CatalogReconcileJob {
29
29
  try {
30
30
  const report = await this.reconciler.reconcile(this.registry, this.store, nowIso);
31
31
  this.latest = report;
32
- const orphanNote = report.orphaned.length
33
- ? ` — ORPHANED: ${report.orphaned.join(', ')}`
34
- : '';
32
+ const orphanNote = report.orphaned.length ? ` — ORPHANED: ${report.orphaned.join(', ')}` : '';
35
33
  console.log(`[catalog] reconcile: +${report.added.length} added, ${report.orphaned.length} orphaned, ` +
36
34
  `${report.reactivated.length} reactivated${orphanNote}`);
37
35
  return report;
@@ -35,6 +35,14 @@ export declare function expandGrantKeys(rawKeys: string[], allKeys: string[]): s
35
35
  * integer against the BIGINT `mp_effective_*_ms` columns (timezone-immune).
36
36
  */
37
37
  export declare function readRoleIdsForTenant(pool: Pool, principalId: string, tenantId: string, now?: () => number): Promise<string[]>;
38
+ /**
39
+ * The next instant (epoch-ms, strictly after `now`) at which this USER's in-effect roles in
40
+ * `tenantId` change by themselves — the earliest future `mp_effective_from_ms` (a posting starts)
41
+ * or `mp_effective_until_ms` (a posting ends) of the active membership. No version bump marks
42
+ * these moments, so cached facts must expire there (ADR-0035). `null` when there is none, and for
43
+ * a service account (its roles have no windows). A query failure propagates (fail closed).
44
+ */
45
+ export declare function readNextPostingBoundaryMs(pool: Pool, principalId: string, tenantId: string, now?: () => number): Promise<number | null>;
38
46
  export declare class DbPermissionFactsAdapter implements PermissionFactsPort {
39
47
  private readonly pool;
40
48
  private readonly registry;
@@ -52,6 +60,14 @@ export declare class DbPermissionFactsAdapter implements PermissionFactsPort {
52
60
  */
53
61
  now?: () => number);
54
62
  currentPermissionsVersion(_principalId: string, tenantId: string): Promise<number>;
63
+ /**
64
+ * The coarse grants + (ADR-0035) the LIVE role ids they were derived from + the time bound of
65
+ * those roles. The version is read FIRST, so a bump racing this load can only make the facts look
66
+ * older (one extra reload), never newer. `roleIds` is always reported (empty = genuinely none),
67
+ * so the session layer binds the in-effect roles, never the credential's `ic_roles`.
68
+ * `validUntilMs` = the next posting window boundary (absent when there is none); an outage on
69
+ * that read fails closed like every other read here.
70
+ */
55
71
  loadCoarseGrants(principalId: string, tenantId: string): Promise<CoarseGrants>;
56
72
  private readVersion;
57
73
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"db-permission-facts.adapter.d.ts","sourceRoot":"","sources":["../../../src/features/authz/db-permission-facts.adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKtF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAe9E;AA0CD;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,CAiBnB;AAED,qBAAa,wBAAyB,YAAW,mBAAmB;IAEhE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAPH,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,cAAc;IACzC;;;;OAIG;IACc,GAAG,GAAE,MAAM,MAAiB;IAGzC,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAalF,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YAgBtE,WAAW;IASzB;;;OAGG;YACW,WAAW;YAIX,aAAa;CAS5B"}
1
+ {"version":3,"file":"db-permission-facts.adapter.d.ts","sourceRoot":"","sources":["../../../src/features/authz/db-permission-facts.adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKtF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAe9E;AA0CD;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,CAqBnB;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,GAC3B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkBxB;AAED,qBAAa,wBAAyB,YAAW,mBAAmB;IAEhE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAPH,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,cAAc;IACzC;;;;OAIG;IACc,GAAG,GAAE,MAAM,MAAiB;IAGzC,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAaxF;;;;;;;OAOG;IACG,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YAuBtE,WAAW;IASzB;;;OAGG;YACW,WAAW;YAIX,aAAa;CAS5B"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DbPermissionFactsAdapter = void 0;
4
4
  exports.expandGrantKeys = expandGrantKeys;
5
5
  exports.readRoleIdsForTenant = readRoleIdsForTenant;
6
+ exports.readNextPostingBoundaryMs = readNextPostingBoundaryMs;
6
7
  const core_1 = require("@astram/core");
7
8
  const MODULE_PREFIX = 'identity';
8
9
  /**
@@ -85,16 +86,44 @@ async function readRoleIdsForTenant(pool, principalId, tenantId, now = Date.now)
85
86
  const saRoles = await readServiceAccountRoleIds(pool, principalId, tenantId);
86
87
  if (saRoles !== null)
87
88
  return saRoles;
88
- // Human path: roles from the active membership's in-effect postings.
89
+ // Human path: roles from the active membership's in-effect postings — of an ACTIVE account only
90
+ // (security hardening Phase 3: a disabled user holds no roles, so gate 5 and every role-keyed
91
+ // path deny at once; `company_membership.cm_user_uuid` is an FK to `users`, so a member always
92
+ // has a users row).
89
93
  const nowMs = now();
90
94
  const rows = (await pool.query(`SELECT DISTINCT mp.mp_role_uuid AS role_uuid
91
95
  FROM \`shield_app\`.\`company_membership\` cm
96
+ JOIN \`shield_app\`.\`users\` u ON u.u_uuid = cm.cm_user_uuid AND u.u_status = 'active'
92
97
  JOIN \`shield_app\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
93
98
  WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ? AND cm.cm_is_active = 1
94
99
  AND (mp.mp_effective_from_ms IS NULL OR mp.mp_effective_from_ms <= ?)
95
100
  AND (mp.mp_effective_until_ms IS NULL OR mp.mp_effective_until_ms > ?)`, [principalId, tenantId, nowMs, nowMs]));
96
101
  return rows.map((r) => r.role_uuid);
97
102
  }
103
+ /**
104
+ * The next instant (epoch-ms, strictly after `now`) at which this USER's in-effect roles in
105
+ * `tenantId` change by themselves — the earliest future `mp_effective_from_ms` (a posting starts)
106
+ * or `mp_effective_until_ms` (a posting ends) of the active membership. No version bump marks
107
+ * these moments, so cached facts must expire there (ADR-0035). `null` when there is none, and for
108
+ * a service account (its roles have no windows). A query failure propagates (fail closed).
109
+ */
110
+ async function readNextPostingBoundaryMs(pool, principalId, tenantId, now = Date.now) {
111
+ const nowMs = now();
112
+ const rows = (await pool.query(`SELECT MIN(CASE
113
+ WHEN mp.mp_effective_from_ms IS NOT NULL AND mp.mp_effective_from_ms > ?
114
+ THEN mp.mp_effective_from_ms
115
+ WHEN mp.mp_effective_until_ms IS NOT NULL AND mp.mp_effective_until_ms > ?
116
+ THEN mp.mp_effective_until_ms
117
+ END) AS next_boundary_ms
118
+ FROM \`shield_app\`.\`company_membership\` cm
119
+ JOIN \`shield_app\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
120
+ WHERE cm.cm_user_uuid = ? AND cm.cm_company_uuid = ? AND cm.cm_is_active = 1`, [nowMs, nowMs, principalId, tenantId]));
121
+ const raw = rows[0]?.next_boundary_ms;
122
+ if (raw === null || raw === undefined)
123
+ return null;
124
+ const ms = Number(raw);
125
+ return Number.isFinite(ms) && ms > nowMs ? ms : null;
126
+ }
98
127
  class DbPermissionFactsAdapter {
99
128
  pool;
100
129
  registry;
@@ -120,14 +149,24 @@ class DbPermissionFactsAdapter {
120
149
  throw new core_1.AuthorizationUnavailableError('permission version store unavailable', cause);
121
150
  }
122
151
  }
152
+ /**
153
+ * The coarse grants + (ADR-0035) the LIVE role ids they were derived from + the time bound of
154
+ * those roles. The version is read FIRST, so a bump racing this load can only make the facts look
155
+ * older (one extra reload), never newer. `roleIds` is always reported (empty = genuinely none),
156
+ * so the session layer binds the in-effect roles, never the credential's `ic_roles`.
157
+ * `validUntilMs` = the next posting window boundary (absent when there is none); an outage on
158
+ * that read fails closed like every other read here.
159
+ */
123
160
  async loadCoarseGrants(principalId, tenantId) {
124
161
  let version;
125
162
  let roleIds;
126
163
  let rawKeys;
164
+ let validUntilMs;
127
165
  try {
128
166
  version = await this.readVersion(tenantId);
129
167
  roleIds = await this.readRoleIds(principalId, tenantId);
130
168
  rawKeys = roleIds.length === 0 ? [] : await this.readGrantKeys(roleIds);
169
+ validUntilMs = await readNextPostingBoundaryMs(this.pool, principalId, tenantId, this.now);
131
170
  }
132
171
  catch (cause) {
133
172
  if (cause instanceof core_1.AuthorizationUnavailableError)
@@ -135,7 +174,12 @@ class DbPermissionFactsAdapter {
135
174
  throw new core_1.AuthorizationUnavailableError('permission facts store unavailable', cause);
136
175
  }
137
176
  const allKeys = this.registry.listActions().map((a) => a.permissionKey);
138
- return { permissionsVersion: version, grantedPermissionKeys: expandGrantKeys(rawKeys, allKeys) };
177
+ return {
178
+ permissionsVersion: version,
179
+ grantedPermissionKeys: expandGrantKeys(rawKeys, allKeys),
180
+ roleIds,
181
+ ...(validUntilMs !== null ? { validUntilMs } : {}),
182
+ };
139
183
  }
140
184
  async readVersion(tenantId) {
141
185
  const rows = (await this.pool.query(`SELECT apv_version FROM \`shield_authz\`.\`authz_permission_version\`
@@ -13,8 +13,11 @@ interface AuthedRequest {
13
13
  * grant; a non-root caller needs the grant. (`role_permission` itself is not a
14
14
  * registered object type — it is administered THROUGH `role`, never classified directly.)
15
15
  *
16
- * Mutating routes pass `req.principal.id` as the grantor so `role_permission` records
17
- * who granted what (surfaced by the grants-summary listing as "by <name>").
16
+ * Mutating routes pass `req.principal` as the actor: it stamps who granted what (surfaced by the
17
+ * grants-summary listing as "by <name>"), and grant AND revoke run the role-binding rule (a role
18
+ * also used outside the caller's subtree is root-only) and the ADR-0029 ceiling ("only grant or
19
+ * revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling and
20
+ * binding → 403, a role outside the caller's subtree → 404, unknown key/bundle → 400.
18
21
  */
19
22
  export declare class GrantAdminController {
20
23
  private readonly grants;
@@ -25,7 +28,7 @@ export declare class GrantAdminController {
25
28
  * grantor NAME (resolved server-side — no raw uuid). Gated `role:read`.
26
29
  *
27
30
  * ⚠️ Path is `access/role-grants` (NOT under `roles/...`) on purpose: a `roles/<x>` path
28
- * would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
31
+ * would be captured by RoleController's `@Get('roles/:uuid')` (registered first),
29
32
  * routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
30
33
  */
31
34
  grantsSummary(req: AuthedRequest, query: {
@@ -63,7 +66,7 @@ export declare class GrantAdminController {
63
66
  success: boolean;
64
67
  }>;
65
68
  /** Revoke one permission key from a role. `:key` is URL-encoded (contains ':'). */
66
- revoke(uuid: string, key: string): Promise<{
69
+ revoke(req: AuthedRequest, uuid: string, key: string): Promise<{
67
70
  success: boolean;
68
71
  }>;
69
72
  /**
@@ -89,7 +92,7 @@ export declare class GrantAdminController {
89
92
  * `role_permission` (see design §5.1 caveat re: shared keys). `:key` is the bundle slug.
90
93
  * Unknown bundle → 400. Gated `role:update`.
91
94
  */
92
- revokeBundle(uuid: string, key: string): Promise<{
95
+ revokeBundle(req: AuthedRequest, uuid: string, key: string): Promise<{
93
96
  success: boolean;
94
97
  }>;
95
98
  }
@@ -1 +1 @@
1
- {"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;AAI1E,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAaD;;;;;;;;;;;GAWG;AACH,qBACa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAEtD;;;;;;;;OAQG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EAEzB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;IAcH,sDAAsD;IAGhD,OAAO;;;;;;;;;;IAIb;kGAC8F;IAGxF,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IAIjE,0CAA0C;IAGpC,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAalG,mFAAmF;IAG7E,MAAM,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;IAOnE;;;;OAIG;IAGG,OAAO;;;IAIb;;;;OAIG;IAGG,WAAW,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAaxG;;;;OAIG;IAGG,YAAY,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;CAS1E"}
1
+ {"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;AAI1E,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAaD;;;;;;;;;;;;;;GAcG;AACH,qBAEa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAEtD;;;;;;;;OAQG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EAEzB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;IAcH,sDAAsD;IAGhD,OAAO;;;;;;;;;;IAIb;kGAC8F;IAGxF,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IAIjE,0CAA0C;IAGpC,KAAK,CACF,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAahC,mFAAmF;IAG7E,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;IAO9F;;;;OAIG;IAGG,OAAO;;;IAIb;;;;OAIG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAahC;;;;OAIG;IAGG,YAAY,CACT,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACb,GAAG,EAAE,MAAM;;;CAU5B"}
@@ -19,7 +19,7 @@ exports.GrantAdminController = void 0;
19
19
  const common_1 = require("@nestjs/common");
20
20
  const guard_1 = require("@astram/guard");
21
21
  const grant_admin_service_js_1 = require("./grant-admin.service.js");
22
- const grant_ceiling_js_1 = require("./grant-ceiling.js");
22
+ const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
23
23
  /**
24
24
  * Derive the caller's subtree scope from the resolved principal (Task 4.2). Uses the SAME cached
25
25
  * `scopeSubtreeNodeIds` the RecordEngine's list-scope uses (one source of truth). Fail-safe to the
@@ -39,8 +39,11 @@ function subtreeScopeOf(req) {
39
39
  * grant; a non-root caller needs the grant. (`role_permission` itself is not a
40
40
  * registered object type — it is administered THROUGH `role`, never classified directly.)
41
41
  *
42
- * Mutating routes pass `req.principal.id` as the grantor so `role_permission` records
43
- * who granted what (surfaced by the grants-summary listing as "by <name>").
42
+ * Mutating routes pass `req.principal` as the actor: it stamps who granted what (surfaced by the
43
+ * grants-summary listing as "by <name>"), and grant AND revoke run the role-binding rule (a role
44
+ * also used outside the caller's subtree is root-only) and the ADR-0029 ceiling ("only grant or
45
+ * revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling and
46
+ * binding → 403, a role outside the caller's subtree → 404, unknown key/bundle → 400.
44
47
  */
45
48
  let GrantAdminController = class GrantAdminController {
46
49
  grants;
@@ -53,7 +56,7 @@ let GrantAdminController = class GrantAdminController {
53
56
  * grantor NAME (resolved server-side — no raw uuid). Gated `role:read`.
54
57
  *
55
58
  * ⚠️ Path is `access/role-grants` (NOT under `roles/...`) on purpose: a `roles/<x>` path
56
- * would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
59
+ * would be captured by RoleController's `@Get('roles/:uuid')` (registered first),
57
60
  * routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
58
61
  */
59
62
  async grantsSummary(req, query) {
@@ -92,15 +95,13 @@ let GrantAdminController = class GrantAdminController {
92
95
  catch (e) {
93
96
  if (e instanceof grant_admin_service_js_1.BadKeyError)
94
97
  throw new common_1.BadRequestException(e.message);
95
- if (e instanceof grant_ceiling_js_1.GrantCeilingError)
96
- throw new common_1.ForbiddenException(e.message);
97
98
  throw e;
98
99
  }
99
100
  return { success: true };
100
101
  }
101
102
  /** Revoke one permission key from a role. `:key` is URL-encoded (contains ':'). */
102
- async revoke(uuid, key) {
103
- await this.grants.revoke(uuid, decodeURIComponent(key));
103
+ async revoke(req, uuid, key) {
104
+ await this.grants.revoke(uuid, decodeURIComponent(key), req.principal);
104
105
  return { success: true };
105
106
  }
106
107
  // ── Access bundles ────────────────────────────────────────────────────────
@@ -127,8 +128,6 @@ let GrantAdminController = class GrantAdminController {
127
128
  catch (e) {
128
129
  if (e instanceof grant_admin_service_js_1.BadKeyError)
129
130
  throw new common_1.BadRequestException(e.message);
130
- if (e instanceof grant_ceiling_js_1.GrantCeilingError)
131
- throw new common_1.ForbiddenException(e.message);
132
131
  throw e;
133
132
  }
134
133
  return { success: true };
@@ -138,9 +137,9 @@ let GrantAdminController = class GrantAdminController {
138
137
  * `role_permission` (see design §5.1 caveat re: shared keys). `:key` is the bundle slug.
139
138
  * Unknown bundle → 400. Gated `role:update`.
140
139
  */
141
- async revokeBundle(uuid, key) {
140
+ async revokeBundle(req, uuid, key) {
142
141
  try {
143
- await this.grants.revokeBundle(uuid, decodeURIComponent(key));
142
+ await this.grants.revokeBundle(uuid, decodeURIComponent(key), req.principal);
144
143
  }
145
144
  catch (e) {
146
145
  if (e instanceof grant_admin_service_js_1.BadKeyError)
@@ -189,10 +188,11 @@ __decorate([
189
188
  __decorate([
190
189
  (0, common_1.Delete)('roles/:uuid/permissions/:key'),
191
190
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
192
- __param(0, (0, common_1.Param)('uuid')),
193
- __param(1, (0, common_1.Param)('key')),
191
+ __param(0, (0, common_1.Req)()),
192
+ __param(1, (0, common_1.Param)('uuid')),
193
+ __param(2, (0, common_1.Param)('key')),
194
194
  __metadata("design:type", Function),
195
- __metadata("design:paramtypes", [String, String]),
195
+ __metadata("design:paramtypes", [Object, String, String]),
196
196
  __metadata("design:returntype", Promise)
197
197
  ], GrantAdminController.prototype, "revoke", null);
198
198
  __decorate([
@@ -215,13 +215,15 @@ __decorate([
215
215
  __decorate([
216
216
  (0, common_1.Delete)('roles/:uuid/access-bundles/:key'),
217
217
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
218
- __param(0, (0, common_1.Param)('uuid')),
219
- __param(1, (0, common_1.Param)('key')),
218
+ __param(0, (0, common_1.Req)()),
219
+ __param(1, (0, common_1.Param)('uuid')),
220
+ __param(2, (0, common_1.Param)('key')),
220
221
  __metadata("design:type", Function),
221
- __metadata("design:paramtypes", [String, String]),
222
+ __metadata("design:paramtypes", [Object, String, String]),
222
223
  __metadata("design:returntype", Promise)
223
224
  ], GrantAdminController.prototype, "revokeBundle", null);
224
225
  exports.GrantAdminController = GrantAdminController = __decorate([
225
226
  (0, common_1.Controller)('identity'),
227
+ (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
226
228
  __metadata("design:paramtypes", [grant_admin_service_js_1.GrantAdminService])
227
229
  ], GrantAdminController);
@@ -1,8 +1,9 @@
1
1
  import type { Pool } from 'mariadb';
2
2
  import type { Principal } from '@astram/core';
3
+ import type { AuditPort } from '@astram/engine';
3
4
  import { PermissionVersionBumper } from './permission-version.bumper.js';
4
5
  import { AccessBundleStore } from './access-bundle-store.js';
5
- import type { GrantCeiling } from './grant-ceiling.js';
6
+ import { GrantCeiling } from './grant-ceiling.js';
6
7
  /**
7
8
  * The caller's reachable subtree, threaded from `req.principal` (Task 4.2). It scopes the
8
9
  * grant-admin reads so a caller sees ONLY roles bound within their subtree — the SAME cached
@@ -52,14 +53,18 @@ export declare class GrantAdminService {
52
53
  private readonly pool;
53
54
  private readonly bumper;
54
55
  private readonly ceiling?;
56
+ private readonly audit?;
55
57
  private readonly bundles;
56
58
  /**
57
- * @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold"). Enforced
58
- * on `grant()`/`grantBundle()` for a non-root grantor. OPTIONAL for back-compat: when absent
59
- * (legacy wiring / tests that don't exercise the ceiling), the ceiling is not applied — the
60
- * identity module ALWAYS wires it, so production always enforces. Root bypasses regardless.
59
+ * @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold") plus the
60
+ * security-hardening role-binding rule. Enforced on grant/revoke/grantBundle/revokeBundle for a
61
+ * non-root actor. OPTIONAL for back-compat: when absent (legacy wiring / tests that don't
62
+ * exercise it), neither check runs — the identity module ALWAYS wires it, so production always
63
+ * enforces. Root bypasses regardless.
64
+ * @param audit The grant-administration audit sink (D6): written on the grant's own transaction
65
+ * connection, fail-closed. The identity module ALWAYS wires the engine `MariaDbAuditSink`.
61
66
  */
62
- constructor(pool: Pool, bumper: PermissionVersionBumper, bundleStore?: AccessBundleStore, ceiling?: GrantCeiling | undefined);
67
+ constructor(pool: Pool, bumper: PermissionVersionBumper, bundleStore?: AccessBundleStore, ceiling?: GrantCeiling | undefined, audit?: AuditPort | undefined);
63
68
  /** All grantable exact keys (from the catalog) + the computed wildcard set. */
64
69
  listCatalog(): Promise<{
65
70
  catalog: CatalogRow[];
@@ -76,15 +81,65 @@ export declare class GrantAdminService {
76
81
  /** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
77
82
  private assertValidKey;
78
83
  /**
79
- * Grant a key to a role (idempotent) + bump the version, atomically. The `grantor` Principal
80
- * stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a no-op and
81
- * its existing stamp is left as-is) AND is subjected to the ADR-0029 self-escalation ceiling:
82
- * a non-root grantor may only grant a key they themselves effectively hold. Order: catalog
83
- * validation (400) → ceiling (403) → write. Root bypasses the ceiling.
84
+ * Security hardening #1 (D1 interim rule) + ADR-0029, in this order, BEFORE any write: the role
85
+ * must be administrable by the actor (bound inside its subtree and nowhere outside — a shared
86
+ * role is root-only), then every key must be covered by the actor's own effective keys ("only
87
+ * grant — or revoke — what you hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒
88
+ * neither check runs; the identity module always wires it.
89
+ */
90
+ private assertMayChangeGrants;
91
+ /**
92
+ * Run one grant-administration write in ONE transaction: the `role_permission` statements, the
93
+ * permission-version bump of every tenant using the role, and the audit row(s) (D6 — audited in
94
+ * the SAME transaction, fail-closed: an audit failure rolls the grant back). Store failures map
95
+ * to AuthorizationUnavailableError (503).
96
+ */
97
+ private writeGrants;
98
+ /**
99
+ * Write the grant-administration audit row(s) on the transaction connection (D6). One row per
100
+ * distinct tenant among the actor's tenant and every tenant the change bumped, so each affected
101
+ * tenant's auditor sees it. Names, never ids, in the detail (ADR-0012): the role name and the
102
+ * bumped tenants' names; the viewer resolves the actor and row tenant itself. No sink wired
103
+ * (legacy/tests) ⇒ nothing is written.
104
+ */
105
+ private auditGrantChange;
106
+ /**
107
+ * Grant a key to a role (idempotent) + bump the version + audit, atomically. The `grantor`
108
+ * Principal stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a
109
+ * no-op and its existing stamp is left as-is). Order: catalog validation (400) → role binding
110
+ * (404/403) → ceiling (403) → write. Root bypasses the binding rule and the ceiling.
84
111
  */
85
112
  grant(roleUuid: string, key: string, grantor: Principal): Promise<void>;
86
- /** Revoke a key from a role (no-op if absent) + bump the version, atomically. */
87
- revoke(roleUuid: string, key: string): Promise<void>;
113
+ /**
114
+ * Revoke a key from a role (no-op if absent) + bump + audit, atomically. Security hardening #1:
115
+ * the actor must be able to administer the role (binding rule) and must hold the key itself —
116
+ * "only revoke what you hold" — so a delegated admin can no longer strip a role of authority it
117
+ * never had. Phase 8 #2: it must also dominate every user holding the role (checked under the
118
+ * role lock in `writeGrants`). Root bypasses.
119
+ */
120
+ revoke(roleUuid: string, key: string, actor: Principal): Promise<void>;
121
+ /**
122
+ * Remove a role that is being DELETED from authorization (Phase 8 #2 follow-up): every
123
+ * `role_permission` row of the role, and the role from every service account's `sa_role_ids`
124
+ * (guard's API-key store reads that column live, so a stale entry would keep the deleted role's
125
+ * keys on the agent). ONE transaction on the shared authz pool — the same pool, bump, audit and
126
+ * error mapping as {@link revoke}:
127
+ * 1. lock the holding accounts (`FOR UPDATE`), then check the actor may strip the role from
128
+ * each (`GrantCeiling.assertMayStripRoleFromServiceAccounts`, root bypasses);
129
+ * 2. bump every tenant using the role — postings AND the accounts' tenants — before the strip,
130
+ * so the account tenants are still found;
131
+ * 3. delete the role's grants; rewrite each account's role list without it (edit stamp moved
132
+ * forward, so a concurrent preserve-merge edit conflicts instead of re-adding it);
133
+ * 4. audit as `role.permission_revoked` (`roleDeleted: true`), commit, publish the versions.
134
+ * Returns the bumped tenants.
135
+ *
136
+ * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection and
137
+ * has already run the binding rule and user-holder dominance; this method therefore does NOT
138
+ * lock the role row (that would wait on the caller). It commits BEFORE the caller deletes the
139
+ * role, so a failure in between leaves a role with no grants and no agents: fewer permissions,
140
+ * never live grants for a deleted role (fail closed).
141
+ */
142
+ purgeRole(roleUuid: string, actor: Principal): Promise<string[]>;
88
143
  /** All active bundles + their resolved atomic keys, for the Access UI. */
89
144
  listBundles(): Promise<{
90
145
  bundles: BundleView[];
@@ -99,11 +154,12 @@ export declare class GrantAdminService {
99
154
  grantBundle(roleUuid: string, bundleKey: string, grantor: Principal): Promise<void>;
100
155
  /**
101
156
  * Revoke a bundle from a role: resolve → DELETE each of the bundle's atomic keys, in
102
- * ONE transaction with ONE version bump. Caveat (design §5.1): this removes exactly the
157
+ * ONE transaction with ONE version bump + audit. Caveat (design §5.1): this removes exactly the
103
158
  * bundle's keys even if a manual grant or another bundle shares one — the model is
104
- * key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`.
159
+ * key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`. Security hardening
160
+ * #1: the binding rule + "only revoke what you hold" apply (root bypasses).
105
161
  */
106
- revokeBundle(roleUuid: string, bundleKey: string): Promise<void>;
162
+ revokeBundle(roleUuid: string, bundleKey: string, actor: Principal): Promise<void>;
107
163
  /** Resolve a bundle key → its atomic keys; throw BadKeyError when unknown/orphaned. */
108
164
  private resolveBundleKeysOrThrow;
109
165
  /**