@astram/identity 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/branch.object.d.ts +9 -1
  2. package/dist/branch.object.d.ts.map +1 -1
  3. package/dist/branch.object.js +14 -1
  4. package/dist/company-branch.object.d.ts +5 -4
  5. package/dist/company-branch.object.d.ts.map +1 -1
  6. package/dist/company-branch.object.js +6 -4
  7. package/dist/company-department.object.d.ts +6 -6
  8. package/dist/company-department.object.d.ts.map +1 -1
  9. package/dist/company-department.object.js +7 -6
  10. package/dist/department.object.d.ts +9 -1
  11. package/dist/department.object.d.ts.map +1 -1
  12. package/dist/department.object.js +14 -1
  13. package/dist/features/access/access-admin.access.d.ts.map +1 -1
  14. package/dist/features/access/access-admin.access.js +8 -1
  15. package/dist/features/access/index.d.ts +1 -0
  16. package/dist/features/access/index.d.ts.map +1 -1
  17. package/dist/features/access/index.js +10 -2
  18. package/dist/features/access/org-structure.access.d.ts +3 -0
  19. package/dist/features/access/org-structure.access.d.ts.map +1 -0
  20. package/dist/features/access/org-structure.access.js +41 -0
  21. package/dist/features/access/user-management.access.d.ts.map +1 -1
  22. package/dist/features/access/user-management.access.js +5 -0
  23. package/dist/features/audit/audit-read.service.d.ts +32 -1
  24. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  25. package/dist/features/audit/audit-read.service.js +67 -14
  26. package/dist/features/audit/auth-audit-sink.d.ts +19 -0
  27. package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
  28. package/dist/features/audit/auth-audit-sink.js +24 -0
  29. package/dist/features/audit/directory-label-resolver.d.ts +10 -1
  30. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
  31. package/dist/features/audit/directory-label-resolver.js +28 -1
  32. package/dist/features/authz/grant-admin.controller.d.ts +4 -4
  33. package/dist/features/authz/grant-admin.controller.js +4 -4
  34. package/dist/features/authz/grant-admin.service.d.ts +9 -8
  35. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  36. package/dist/features/authz/grant-admin.service.js +20 -24
  37. package/dist/features/authz/grant-ceiling.d.ts +35 -16
  38. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  39. package/dist/features/authz/grant-ceiling.js +44 -29
  40. package/dist/features/authz/role-binding.d.ts +25 -24
  41. package/dist/features/authz/role-binding.d.ts.map +1 -1
  42. package/dist/features/authz/role-binding.js +31 -44
  43. package/dist/features/identity-feature.module.d.ts +0 -11
  44. package/dist/features/identity-feature.module.d.ts.map +1 -1
  45. package/dist/features/identity-feature.module.js +90 -1
  46. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  47. package/dist/features/membership/membership.controller.js +20 -6
  48. package/dist/features/membership/membership.service.d.ts +16 -10
  49. package/dist/features/membership/membership.service.d.ts.map +1 -1
  50. package/dist/features/membership/membership.service.js +61 -21
  51. package/dist/features/membership/posting.service.d.ts +25 -6
  52. package/dist/features/membership/posting.service.d.ts.map +1 -1
  53. package/dist/features/membership/posting.service.js +63 -16
  54. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
  55. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
  56. package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
  57. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
  58. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
  59. package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
  60. package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
  61. package/dist/features/org-bootstrap/org-state.reader.js +56 -49
  62. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
  63. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
  64. package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
  65. package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
  66. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  67. package/dist/features/org-structure/org-catalog.controller.js +105 -17
  68. package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
  69. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  70. package/dist/features/org-structure/org-catalog.service.js +327 -51
  71. package/dist/features/org-structure/org-unit-name.d.ts +31 -0
  72. package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
  73. package/dist/features/org-structure/org-unit-name.js +50 -0
  74. package/dist/features/role/role.controller.d.ts +28 -13
  75. package/dist/features/role/role.controller.d.ts.map +1 -1
  76. package/dist/features/role/role.controller.js +54 -14
  77. package/dist/features/role/role.service.d.ts +76 -21
  78. package/dist/features/role/role.service.d.ts.map +1 -1
  79. package/dist/features/role/role.service.js +204 -27
  80. package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
  81. package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
  82. package/dist/features/security-policy/security-policy-version-bump.js +65 -0
  83. package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
  84. package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
  85. package/dist/features/security-policy/security-policy.controller.js +121 -0
  86. package/dist/features/security-policy/security-policy.service.d.ts +94 -0
  87. package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
  88. package/dist/features/security-policy/security-policy.service.js +429 -0
  89. package/dist/features/service-account/service-account.service.d.ts +13 -3
  90. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  91. package/dist/features/service-account/service-account.service.js +48 -10
  92. package/dist/features/service-account/service-account.store.d.ts +11 -1
  93. package/dist/features/service-account/service-account.store.d.ts.map +1 -1
  94. package/dist/features/service-account/service-account.store.js +20 -1
  95. package/dist/features/user/signin-admin.tokens.d.ts +11 -0
  96. package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
  97. package/dist/features/user/signin-admin.tokens.js +13 -0
  98. package/dist/features/user/user-mfa-status.d.ts +38 -0
  99. package/dist/features/user/user-mfa-status.d.ts.map +1 -0
  100. package/dist/features/user/user-mfa-status.js +47 -0
  101. package/dist/features/user/user-signin-admin.service.d.ts +48 -0
  102. package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
  103. package/dist/features/user/user-signin-admin.service.js +232 -0
  104. package/dist/features/user/user.controller.d.ts +29 -2
  105. package/dist/features/user/user.controller.d.ts.map +1 -1
  106. package/dist/features/user/user.controller.js +71 -3
  107. package/dist/features/user/user.service.d.ts +40 -14
  108. package/dist/features/user/user.service.d.ts.map +1 -1
  109. package/dist/features/user/user.service.js +143 -10
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +3 -1
  113. package/dist/objects.d.ts.map +1 -1
  114. package/dist/objects.js +2 -0
  115. package/dist/role.object.d.ts +1 -1
  116. package/dist/role.object.d.ts.map +1 -1
  117. package/dist/role.object.js +6 -15
  118. package/dist/security-policy.object.d.ts +43 -0
  119. package/dist/security-policy.object.d.ts.map +1 -0
  120. package/dist/security-policy.object.js +107 -0
  121. package/dist/user.object.d.ts +6 -0
  122. package/dist/user.object.d.ts.map +1 -1
  123. package/dist/user.object.js +13 -1
  124. package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
  125. package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
  126. package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
  127. package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
  128. package/package.json +4 -4
@@ -1,6 +1,14 @@
1
- /** branches — global catalog. */
1
+ /**
2
+ * branches — a company's own branches (G-012, spec §4/§5.B). Each row belongs to exactly one
3
+ * company (`b_company_uuid`, migration `1710000000105-OwnDepartmentsAndBranches`), names are unique
4
+ * per company, and no row is shared between companies. The owner is stamped server-side from the
5
+ * reached route company and never changes (`immutable`). Read wall (ADR-0027): a non-root
6
+ * list/get sees only rows owned by a company in its reachable subtree; a non-root create is
7
+ * walled the same way (ADR-0034). A legacy row with no owner (NULL) is visible to root only.
8
+ */
2
9
  export declare class Branch {
3
10
  uuid: string;
4
11
  name: string;
12
+ companyUuid: string;
5
13
  }
6
14
  //# sourceMappingURL=branch.object.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch.object.d.ts","sourceRoot":"","sources":["../src/branch.object.ts"],"names":[],"mappings":"AAGA,iCAAiC;AACjC,qBAOa,MAAM;IAEjB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;CACf"}
1
+ {"version":3,"file":"branch.object.d.ts","sourceRoot":"","sources":["../src/branch.object.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,qBAQa,MAAM;IAEjB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAGd,WAAW,EAAG,MAAM,CAAC;CACtB"}
@@ -12,10 +12,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.Branch = void 0;
13
13
  const engine_1 = require("@astram/engine");
14
14
  const schema_1 = require("./schema");
15
- /** branches — global catalog. */
15
+ /**
16
+ * branches — a company's own branches (G-012, spec §4/§5.B). Each row belongs to exactly one
17
+ * company (`b_company_uuid`, migration `1710000000105-OwnDepartmentsAndBranches`), names are unique
18
+ * per company, and no row is shared between companies. The owner is stamped server-side from the
19
+ * reached route company and never changes (`immutable`). Read wall (ADR-0027): a non-root
20
+ * list/get sees only rows owned by a company in its reachable subtree; a non-root create is
21
+ * walled the same way (ADR-0034). A legacy row with no owner (NULL) is visible to root only.
22
+ */
16
23
  let Branch = class Branch {
17
24
  uuid;
18
25
  name;
26
+ companyUuid;
19
27
  };
20
28
  exports.Branch = Branch;
21
29
  __decorate([
@@ -26,12 +34,17 @@ __decorate([
26
34
  (0, engine_1.Field)({ column: 'b_name', type: 'string', required: true }),
27
35
  __metadata("design:type", String)
28
36
  ], Branch.prototype, "name", void 0);
37
+ __decorate([
38
+ (0, engine_1.Field)({ column: 'b_company_uuid', type: 'uuid', immutable: true }),
39
+ __metadata("design:type", String)
40
+ ], Branch.prototype, "companyUuid", void 0);
29
41
  exports.Branch = Branch = __decorate([
30
42
  (0, engine_1.ObjectType)({
31
43
  typeKey: 'branch',
32
44
  table: 'branches',
33
45
  schema: schema_1.SCHEMA,
34
46
  access: { plane: 'control', system: true },
47
+ readScope: { kind: 'nodeColumn', column: 'b_company_uuid' },
35
48
  }),
36
49
  (0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'b_uuid' })
37
50
  ], Branch);
@@ -1,8 +1,9 @@
1
1
  /**
2
- * company_branches — the per-company branch catalog junction (the branch dropdown's
3
- * option source). Same shape/role as `company_department`: listed by `companyUuid`, each
4
- * `branchUuid` resolved to a name server-side for the editor's company-scoped Branch
5
- * dropdown.
2
+ * company_branches — the company → branch junction. Same shape and role as
3
+ * `company_department`: since G-012 (migration `1710000000105-OwnDepartmentsAndBranches`) every
4
+ * branch is owned by one company and has exactly one junction row, to its owner. Read wall
5
+ * (ADR-0027) on `cb_company_uuid`, so a non-root junction read or write is bounded to the caller's
6
+ * reachable subtree (create walled too, ADR-0034).
6
7
  */
7
8
  export declare class CompanyBranch {
8
9
  companyUuid: string;
@@ -1 +1 @@
1
- {"version":3,"file":"company-branch.object.d.ts","sourceRoot":"","sources":["../src/company-branch.object.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,qBAOa,aAAa;IAExB,WAAW,EAAG,MAAM,CAAC;IAGrB,UAAU,EAAG,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"company-branch.object.d.ts","sourceRoot":"","sources":["../src/company-branch.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAQa,aAAa;IAExB,WAAW,EAAG,MAAM,CAAC;IAGrB,UAAU,EAAG,MAAM,CAAC;CACrB"}
@@ -13,10 +13,11 @@ exports.CompanyBranch = void 0;
13
13
  const engine_1 = require("@astram/engine");
14
14
  const schema_1 = require("./schema");
15
15
  /**
16
- * company_branches — the per-company branch catalog junction (the branch dropdown's
17
- * option source). Same shape/role as `company_department`: listed by `companyUuid`, each
18
- * `branchUuid` resolved to a name server-side for the editor's company-scoped Branch
19
- * dropdown.
16
+ * company_branches — the company → branch junction. Same shape and role as
17
+ * `company_department`: since G-012 (migration `1710000000105-OwnDepartmentsAndBranches`) every
18
+ * branch is owned by one company and has exactly one junction row, to its owner. Read wall
19
+ * (ADR-0027) on `cb_company_uuid`, so a non-root junction read or write is bounded to the caller's
20
+ * reachable subtree (create walled too, ADR-0034).
20
21
  */
21
22
  let CompanyBranch = class CompanyBranch {
22
23
  companyUuid;
@@ -37,6 +38,7 @@ exports.CompanyBranch = CompanyBranch = __decorate([
37
38
  table: 'company_branches',
38
39
  schema: schema_1.SCHEMA,
39
40
  access: { plane: 'control', system: true },
41
+ readScope: { kind: 'nodeColumn', column: 'cb_company_uuid' },
40
42
  }),
41
43
  (0, engine_1.PrimaryKey)({ kind: 'composite', columns: ['cb_company_uuid', 'cb_branch_uuid'] })
42
44
  ], CompanyBranch);
@@ -1,10 +1,10 @@
1
1
  /**
2
- * company_departments — the per-company department catalog junction (which departments a
3
- * company offers). Registered (Phase 5, Task 14 seam) so the Assignments editor's
4
- * company-scoped Department dropdown can be read THROUGH THE PIPE: list this junction by
5
- * `companyUuid`, then resolve each `departmentUuid` → a display name via `engine.get`
6
- * (never a raw uuid label). A composite-PK, read-only-through-the-engine catalog; the
7
- * service only lists it — no create/update from this feature.
2
+ * company_departments — the company → department junction. Since G-012 (migration
3
+ * `1710000000105-OwnDepartmentsAndBranches`) every department is owned by one company and has
4
+ * exactly one junction row, to its owner; the junction stays so existing readers
5
+ * (`GrantCeiling.assertOrgUnitsInCompany`, the org bootstrap) keep working. A composite-PK type;
6
+ * read wall (ADR-0027) on `cd_company_uuid`, so a non-root junction read or write is bounded to
7
+ * the caller's reachable subtree (create walled too, ADR-0034).
8
8
  */
9
9
  export declare class CompanyDepartment {
10
10
  companyUuid: string;
@@ -1 +1 @@
1
- {"version":3,"file":"company-department.object.d.ts","sourceRoot":"","sources":["../src/company-department.object.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,qBAOa,iBAAiB;IAE5B,WAAW,EAAG,MAAM,CAAC;IAGrB,cAAc,EAAG,MAAM,CAAC;CACzB"}
1
+ {"version":3,"file":"company-department.object.d.ts","sourceRoot":"","sources":["../src/company-department.object.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,qBAQa,iBAAiB;IAE5B,WAAW,EAAG,MAAM,CAAC;IAGrB,cAAc,EAAG,MAAM,CAAC;CACzB"}
@@ -13,12 +13,12 @@ exports.CompanyDepartment = void 0;
13
13
  const engine_1 = require("@astram/engine");
14
14
  const schema_1 = require("./schema");
15
15
  /**
16
- * company_departments — the per-company department catalog junction (which departments a
17
- * company offers). Registered (Phase 5, Task 14 seam) so the Assignments editor's
18
- * company-scoped Department dropdown can be read THROUGH THE PIPE: list this junction by
19
- * `companyUuid`, then resolve each `departmentUuid` → a display name via `engine.get`
20
- * (never a raw uuid label). A composite-PK, read-only-through-the-engine catalog; the
21
- * service only lists it — no create/update from this feature.
16
+ * company_departments — the company → department junction. Since G-012 (migration
17
+ * `1710000000105-OwnDepartmentsAndBranches`) every department is owned by one company and has
18
+ * exactly one junction row, to its owner; the junction stays so existing readers
19
+ * (`GrantCeiling.assertOrgUnitsInCompany`, the org bootstrap) keep working. A composite-PK type;
20
+ * read wall (ADR-0027) on `cd_company_uuid`, so a non-root junction read or write is bounded to
21
+ * the caller's reachable subtree (create walled too, ADR-0034).
22
22
  */
23
23
  let CompanyDepartment = class CompanyDepartment {
24
24
  companyUuid;
@@ -39,6 +39,7 @@ exports.CompanyDepartment = CompanyDepartment = __decorate([
39
39
  table: 'company_departments',
40
40
  schema: schema_1.SCHEMA,
41
41
  access: { plane: 'control', system: true },
42
+ readScope: { kind: 'nodeColumn', column: 'cd_company_uuid' },
42
43
  }),
43
44
  (0, engine_1.PrimaryKey)({ kind: 'composite', columns: ['cd_company_uuid', 'cd_department_uuid'] })
44
45
  ], CompanyDepartment);
@@ -1,6 +1,14 @@
1
- /** departments — global catalog. */
1
+ /**
2
+ * departments — a company's own departments (G-012, spec §4/§5.B). Each row belongs to exactly one
3
+ * company (`d_company_uuid`, migration `1710000000105-OwnDepartmentsAndBranches`), names are unique
4
+ * per company, and no row is shared between companies. The owner is stamped server-side from the
5
+ * reached route company and never changes (`immutable`). Read wall (ADR-0027): a non-root
6
+ * list/get sees only rows owned by a company in its reachable subtree; a non-root create is
7
+ * walled the same way (ADR-0034). A legacy row with no owner (NULL) is visible to root only.
8
+ */
2
9
  export declare class Department {
3
10
  uuid: string;
4
11
  name: string;
12
+ companyUuid: string;
5
13
  }
6
14
  //# sourceMappingURL=department.object.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"department.object.d.ts","sourceRoot":"","sources":["../src/department.object.ts"],"names":[],"mappings":"AAGA,oCAAoC;AACpC,qBAOa,UAAU;IAErB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;CACf"}
1
+ {"version":3,"file":"department.object.d.ts","sourceRoot":"","sources":["../src/department.object.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,qBAQa,UAAU;IAErB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAGd,WAAW,EAAG,MAAM,CAAC;CACtB"}
@@ -12,10 +12,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.Department = void 0;
13
13
  const engine_1 = require("@astram/engine");
14
14
  const schema_1 = require("./schema");
15
- /** departments — global catalog. */
15
+ /**
16
+ * departments — a company's own departments (G-012, spec §4/§5.B). Each row belongs to exactly one
17
+ * company (`d_company_uuid`, migration `1710000000105-OwnDepartmentsAndBranches`), names are unique
18
+ * per company, and no row is shared between companies. The owner is stamped server-side from the
19
+ * reached route company and never changes (`immutable`). Read wall (ADR-0027): a non-root
20
+ * list/get sees only rows owned by a company in its reachable subtree; a non-root create is
21
+ * walled the same way (ADR-0034). A legacy row with no owner (NULL) is visible to root only.
22
+ */
16
23
  let Department = class Department {
17
24
  uuid;
18
25
  name;
26
+ companyUuid;
19
27
  };
20
28
  exports.Department = Department;
21
29
  __decorate([
@@ -26,12 +34,17 @@ __decorate([
26
34
  (0, engine_1.Field)({ column: 'd_name', type: 'string', required: true }),
27
35
  __metadata("design:type", String)
28
36
  ], Department.prototype, "name", void 0);
37
+ __decorate([
38
+ (0, engine_1.Field)({ column: 'd_company_uuid', type: 'uuid', immutable: true }),
39
+ __metadata("design:type", String)
40
+ ], Department.prototype, "companyUuid", void 0);
29
41
  exports.Department = Department = __decorate([
30
42
  (0, engine_1.ObjectType)({
31
43
  typeKey: 'department',
32
44
  table: 'departments',
33
45
  schema: schema_1.SCHEMA,
34
46
  access: { plane: 'control', system: true },
47
+ readScope: { kind: 'nodeColumn', column: 'd_company_uuid' },
35
48
  }),
36
49
  (0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'd_uuid' })
37
50
  ], Department);
@@ -1 +1 @@
1
- {"version":3,"file":"access-admin.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/access-admin.access.ts"],"names":[],"mappings":"AAOA,qBAOa,iBAAiB;CAAG"}
1
+ {"version":3,"file":"access-admin.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/access-admin.access.ts"],"names":[],"mappings":"AAWA,qBAUa,iBAAiB;CAAG"}
@@ -11,6 +11,10 @@ exports.AccessAdminAccess = void 0;
11
11
  * `access-admin` — the role-administration seed bundle from design §6. Full role
12
12
  * lifecycle (managing roles + the access granted to them). Every key resolves to a
13
13
  * REAL registered `identity:<type>:<action>`.
14
+ *
15
+ * G-013 (spec §6): roles are owned per company, so every company role route reaches the company
16
+ * through the walled `company` read first, and the Roles page's company picker lists companies:
17
+ * the bundle carries `company: read` (the G-012 KI #13 lesson).
14
18
  */
15
19
  const engine_1 = require("@astram/engine");
16
20
  let AccessAdminAccess = class AccessAdminAccess {
@@ -22,6 +26,9 @@ exports.AccessAdminAccess = AccessAdminAccess = __decorate([
22
26
  module: 'identity',
23
27
  label: 'Access Administration',
24
28
  description: 'Manage roles and the access granted to them.',
25
- permissions: [{ typeKey: 'role', actions: ['read', 'create', 'update', 'delete'] }],
29
+ permissions: [
30
+ { typeKey: 'role', actions: ['read', 'create', 'update', 'delete'] },
31
+ { typeKey: 'company', actions: ['read'] },
32
+ ],
26
33
  })
27
34
  ], AccessAdminAccess);
@@ -10,6 +10,7 @@ import { UserManagementAccess } from './user-management.access';
10
10
  export { UserManagementAccess } from './user-management.access';
11
11
  export { CompanyAdminAccess } from './company-admin.access';
12
12
  export { AccessAdminAccess } from './access-admin.access';
13
+ export { OrgStructureAccess } from './org-structure.access';
13
14
  /** All identity access bundles, collected into the engine at boot. */
14
15
  export declare const identityAccessBundles: (typeof UserManagementAccess)[];
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -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,iCAAgE,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;AAKhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,iCAKjC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.identityAccessBundles = exports.AccessAdminAccess = exports.CompanyAdminAccess = exports.UserManagementAccess = void 0;
3
+ exports.identityAccessBundles = exports.OrgStructureAccess = exports.AccessAdminAccess = exports.CompanyAdminAccess = exports.UserManagementAccess = void 0;
4
4
  /**
5
5
  * Identity access-bundles barrel. Each `@AccessBundle` class is declared in its own
6
6
  * `<bundle>.access.ts` (co-located with the feature it grants, per the design's
@@ -12,11 +12,19 @@ exports.identityAccessBundles = exports.AccessAdminAccess = exports.CompanyAdmin
12
12
  const user_management_access_1 = require("./user-management.access");
13
13
  const company_admin_access_1 = require("./company-admin.access");
14
14
  const access_admin_access_1 = require("./access-admin.access");
15
+ const org_structure_access_1 = require("./org-structure.access");
15
16
  var user_management_access_2 = require("./user-management.access");
16
17
  Object.defineProperty(exports, "UserManagementAccess", { enumerable: true, get: function () { return user_management_access_2.UserManagementAccess; } });
17
18
  var company_admin_access_2 = require("./company-admin.access");
18
19
  Object.defineProperty(exports, "CompanyAdminAccess", { enumerable: true, get: function () { return company_admin_access_2.CompanyAdminAccess; } });
19
20
  var access_admin_access_2 = require("./access-admin.access");
20
21
  Object.defineProperty(exports, "AccessAdminAccess", { enumerable: true, get: function () { return access_admin_access_2.AccessAdminAccess; } });
22
+ var org_structure_access_2 = require("./org-structure.access");
23
+ Object.defineProperty(exports, "OrgStructureAccess", { enumerable: true, get: function () { return org_structure_access_2.OrgStructureAccess; } });
21
24
  /** All identity access bundles, collected into the engine at boot. */
22
- exports.identityAccessBundles = [user_management_access_1.UserManagementAccess, company_admin_access_1.CompanyAdminAccess, access_admin_access_1.AccessAdminAccess];
25
+ exports.identityAccessBundles = [
26
+ user_management_access_1.UserManagementAccess,
27
+ company_admin_access_1.CompanyAdminAccess,
28
+ access_admin_access_1.AccessAdminAccess,
29
+ org_structure_access_1.OrgStructureAccess,
30
+ ];
@@ -0,0 +1,3 @@
1
+ export declare class OrgStructureAccess {
2
+ }
3
+ //# sourceMappingURL=org-structure.access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-structure.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/org-structure.access.ts"],"names":[],"mappings":"AASA,qBAmBa,kBAAkB;CAAG"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.OrgStructureAccess = void 0;
10
+ /**
11
+ * `org-structure` (G-012, spec §5.C) — create, rename and delete a company's own departments and
12
+ * branches, as ONE named unit. `company:read` lets every route reach the company and the page pick
13
+ * one; the junction reads gate the two list routes, the junction creates the create routes; the posting read lets
14
+ * the list show `usedBy` (without it the list still answers, with `usedBy` omitted). The delete's
15
+ * in-use check needs no posting key: it is a complete count inside the service (spec §5.A).
16
+ */
17
+ const engine_1 = require("@astram/engine");
18
+ let OrgStructureAccess = class OrgStructureAccess {
19
+ };
20
+ exports.OrgStructureAccess = OrgStructureAccess;
21
+ exports.OrgStructureAccess = OrgStructureAccess = __decorate([
22
+ (0, engine_1.AccessBundle)({
23
+ key: 'org-structure',
24
+ module: 'identity',
25
+ label: 'Org Structure',
26
+ description: 'Create, rename and delete departments and branches.',
27
+ permissions: [
28
+ // `company:read`: every route reaches the company through the walled read first, and the Org
29
+ // structure page's company picker lists the companies the caller can reach (G-012 live proof).
30
+ { typeKey: 'company', actions: ['read'] },
31
+ { typeKey: 'department', actions: ['read', 'create', 'update', 'delete'] },
32
+ { typeKey: 'branch', actions: ['read', 'create', 'update', 'delete'] },
33
+ // `create` on the junctions: creating a department or branch also writes its one junction row
34
+ // through the engine pipe, which gates `identity:company_department|company_branch:create`
35
+ // (security review P1). Deletes need no junction key: the junction row cascades.
36
+ { typeKey: 'company_department', actions: ['read', 'create'] },
37
+ { typeKey: 'company_branch', actions: ['read', 'create'] },
38
+ { typeKey: 'membership_posting', actions: ['read'] },
39
+ ],
40
+ })
41
+ ], OrgStructureAccess);
@@ -1 +1 @@
1
- {"version":3,"file":"user-management.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/user-management.access.ts"],"names":[],"mappings":"AAQA,qBAca,oBAAoB;CAAG"}
1
+ {"version":3,"file":"user-management.access.d.ts","sourceRoot":"","sources":["../../../src/features/access/user-management.access.ts"],"names":[],"mappings":"AAQA,qBAmBa,oBAAoB;CAAG"}
@@ -25,9 +25,14 @@ exports.UserManagementAccess = UserManagementAccess = __decorate([
25
25
  description: 'Create, edit and assign users to companies.',
26
26
  permissions: [
27
27
  { typeKey: 'user', actions: ['read', 'create', 'update', 'delete'] },
28
+ // G-012: the department/branch lists reach the company through the walled company read first.
29
+ { typeKey: 'company', actions: ['read'] },
28
30
  { typeKey: 'role', actions: ['read'] },
29
31
  { typeKey: 'department', actions: ['read'] },
30
32
  { typeKey: 'branch', actions: ['read'] },
33
+ // G-012: the posting editor lists a company's departments and branches through these.
34
+ { typeKey: 'company_department', actions: ['read'] },
35
+ { typeKey: 'company_branch', actions: ['read'] },
31
36
  { typeKey: 'company_membership', actions: ['read', 'create', 'update', 'delete'] },
32
37
  { typeKey: 'membership_posting', actions: ['read', 'update'] },
33
38
  ],
@@ -1,7 +1,24 @@
1
1
  import type { AuditEvent, LabelResolver } from '@astram/core';
2
+ /**
3
+ * What kind of actor wrote an audit row (KI #140): a user, a service account (API key), a framework
4
+ * operation (org bootstrap, audit retention prune), or an id found nowhere (`'unknown'`, shown as
5
+ * "Deleted user").
6
+ */
7
+ export type AuditActorKind = 'user' | 'service_account' | 'operation' | 'unknown';
8
+ /** A directory lookup of an actor id: never an operation (those are labelled without a lookup). */
9
+ export interface ResolvedActor {
10
+ readonly kind: Exclude<AuditActorKind, 'operation'>;
11
+ readonly label: string;
12
+ }
13
+ /** The actor directory port (`DirectoryLabelResolver` implements it). A throw propagates. */
14
+ export interface ActorLabelResolver {
15
+ resolveActor(actorId: string): Promise<ResolvedActor>;
16
+ }
2
17
  export interface AuditRow {
3
18
  readonly occurredAt: number;
4
19
  readonly actorLabel: string;
20
+ /** Additive (KI #140): lets the viewer badge non-user actors. */
21
+ readonly actorKind: AuditActorKind;
5
22
  readonly action: string;
6
23
  readonly tenantLabel: string;
7
24
  readonly detail: Record<string, unknown>;
@@ -63,7 +80,11 @@ export declare class AuditReadService {
63
80
  private readonly reader;
64
81
  private readonly labels;
65
82
  private readonly actorNameResolver;
66
- constructor(reader: PagedAuditReader, labels: LabelResolver, actorNameResolver: ActorNameResolver);
83
+ /** Absent ⇒ every non-operation actor resolves as a user via `labels` (the pre-KI #140 shape). */
84
+ private readonly actors?;
85
+ constructor(reader: PagedAuditReader, labels: LabelResolver, actorNameResolver: ActorNameResolver,
86
+ /** Absent ⇒ every non-operation actor resolves as a user via `labels` (the pre-KI #140 shape). */
87
+ actors?: ActorLabelResolver | undefined);
67
88
  /**
68
89
  * A numbered (offset + total) audit page. Scope is derived from the PRINCIPAL ALONE — a
69
90
  * non-root is pinned to its own tenant (tenantFilter ignored); a root gets the all-tenants
@@ -83,6 +104,16 @@ export declare class AuditReadService {
83
104
  page: number;
84
105
  limit: number;
85
106
  }>;
107
+ /**
108
+ * The actor of one row (KI #140). A reserved operation principal (guard's registry) and an
109
+ * audit-prune row are labelled without any lookup, naming the human operator from the row's own
110
+ * free text (`onBehalfOf` / the prune actor), never an id; everything else goes to the directory.
111
+ */
112
+ private resolveActor;
113
+ /** A prune row: one of the prune's own types on the platform tenant (`'*'`). */
114
+ private isAuditPrune;
115
+ /** Free-text operator name, verbatim; absent, blank or uuid-shaped ⇒ "unknown operator". */
116
+ private operatorName;
86
117
  /**
87
118
  * Remove obvious raw-uuid-bearing keys from the attributes detail so no raw id leaks.
88
119
  * Keeps non-id fields; drops keys ending in `Uuid`/`Id`/`_uuid`/`_id` and any uuid-shaped value.
@@ -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;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"}
1
+ {"version":3,"file":"audit-read.service.d.ts","sourceRoot":"","sources":["../../../src/features/audit/audit-read.service.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AuditReadService = void 0;
4
+ const guard_1 = require("@astram/guard");
4
5
  const ALL_TENANTS = '*';
5
6
  const DEFAULT_LIMIT = 50;
7
+ const UUID_SHAPED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8
+ /** `al_type` of the rows `astram-migrate audit:prune` writes (tenant `'*'`, actor = operator). */
9
+ const AUDIT_PRUNE_TYPES = new Set([
10
+ 'audit.retention_prune_started',
11
+ 'audit.retention_pruned',
12
+ ]);
13
+ const PLATFORM_TENANT_LABEL = 'Platform';
14
+ const UNKNOWN_OPERATOR = 'unknown operator';
6
15
  const MAX_LIMIT = 200;
7
16
  /**
8
17
  * The audit viewer read-model wrapper (design §2, §3): derives scope from the PRINCIPAL
@@ -15,10 +24,14 @@ class AuditReadService {
15
24
  reader;
16
25
  labels;
17
26
  actorNameResolver;
18
- constructor(reader, labels, actorNameResolver) {
27
+ actors;
28
+ constructor(reader, labels, actorNameResolver,
29
+ /** Absent ⇒ every non-operation actor resolves as a user via `labels` (the pre-KI #140 shape). */
30
+ actors) {
19
31
  this.reader = reader;
20
32
  this.labels = labels;
21
33
  this.actorNameResolver = actorNameResolver;
34
+ this.actors = actors;
22
35
  }
23
36
  /**
24
37
  * A numbered (offset + total) audit page. Scope is derived from the PRINCIPAL ALONE — a
@@ -70,26 +83,66 @@ class AuditReadService {
70
83
  this.reader.count(scopeQuery),
71
84
  this.reader.read({ ...scopeQuery, limit, offset }),
72
85
  ]);
73
- // Resolve labels DIRECTLY (the core `auditView` engine is no longer in this path): actorLabel
74
- // via labels.resolve('principal', ...) and tenantLabel via labels.resolve('tenant', ...) —
75
- // same resolver, same no-raw-id guarantee. A resolver throw propagates (fail-closed).
76
- const rows = await Promise.all(events.map(async (event) => ({
77
- occurredAt: event.occurredAtMs,
78
- actorLabel: await this.labels.resolve('principal', event.actorId),
79
- action: event.action?.key ??
80
- event.action ??
81
- '(event)',
82
- tenantLabel: await this.labels.resolve('tenant', event.tenantId),
83
- detail: this.stripIds(event.attributes ?? {}),
84
- })));
86
+ // Resolve labels DIRECTLY (the core `auditView` engine is no longer in this path): the actor by
87
+ // kind (operation registry, then the actor directory) and tenantLabel via
88
+ // labels.resolve('tenant', ...) — no raw id is ever a label. A resolver throw propagates
89
+ // (fail-closed).
90
+ const rows = await Promise.all(events.map(async (event) => {
91
+ const prune = this.isAuditPrune(event);
92
+ const actor = await this.resolveActor(event, prune);
93
+ const actionKey = event.action?.key ??
94
+ event.action;
95
+ return {
96
+ occurredAt: event.occurredAtMs,
97
+ actorLabel: actor.label,
98
+ actorKind: actor.kind,
99
+ action: actionKey ?? (prune ? String(event.type) : '(event)'),
100
+ tenantLabel: event.tenantId === ALL_TENANTS
101
+ ? PLATFORM_TENANT_LABEL
102
+ : await this.labels.resolve('tenant', event.tenantId),
103
+ detail: this.stripIds(event.attributes ?? {}),
104
+ };
105
+ }));
85
106
  return { rows, total, page, limit };
86
107
  }
108
+ /**
109
+ * The actor of one row (KI #140). A reserved operation principal (guard's registry) and an
110
+ * audit-prune row are labelled without any lookup, naming the human operator from the row's own
111
+ * free text (`onBehalfOf` / the prune actor), never an id; everything else goes to the directory.
112
+ */
113
+ async resolveActor(event, prune) {
114
+ const reserved = (0, guard_1.reservedOperationPrincipal)(event.actorId);
115
+ if (reserved) {
116
+ const operator = this.operatorName(event.attributes?.['onBehalfOf']);
117
+ return { kind: 'operation', label: `${reserved.label} (on behalf of ${operator})` };
118
+ }
119
+ if (prune) {
120
+ return {
121
+ kind: 'operation',
122
+ label: `Audit retention prune (${this.operatorName(event.actorId)})`,
123
+ };
124
+ }
125
+ if (this.actors)
126
+ return this.actors.resolveActor(event.actorId);
127
+ return { kind: 'user', label: await this.labels.resolve('principal', event.actorId) };
128
+ }
129
+ /** A prune row: one of the prune's own types on the platform tenant (`'*'`). */
130
+ isAuditPrune(event) {
131
+ return event.tenantId === ALL_TENANTS && AUDIT_PRUNE_TYPES.has(String(event.type));
132
+ }
133
+ /** Free-text operator name, verbatim; absent, blank or uuid-shaped ⇒ "unknown operator". */
134
+ operatorName(value) {
135
+ if (typeof value !== 'string')
136
+ return UNKNOWN_OPERATOR;
137
+ const name = value.trim();
138
+ return name.length === 0 || UUID_SHAPED.test(name) ? UNKNOWN_OPERATOR : name;
139
+ }
87
140
  /**
88
141
  * Remove obvious raw-uuid-bearing keys from the attributes detail so no raw id leaks.
89
142
  * Keeps non-id fields; drops keys ending in `Uuid`/`Id`/`_uuid`/`_id` and any uuid-shaped value.
90
143
  */
91
144
  stripIds(attrs) {
92
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
145
+ const UUID = UUID_SHAPED;
93
146
  const out = {};
94
147
  for (const [k, val] of Object.entries(attrs)) {
95
148
  if (/uuid$|Id$|_id$/i.test(k))
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The framework default for guard's auth `AuditSink` (R12.1, B-3).
3
+ *
4
+ * `IdentityFeatureModule.forRoot` binds it on guard's `SHIELD_DEFAULT_AUTH_AUDIT_SINK`; guard's
5
+ * `SHIELD_AUDIT_SINK` uses it when the app supplied no `auditSink`. It maps guard's
6
+ * `AuthAuditEvent` onto the engine `MariaDbAuditSink` row (`<ns>_audit.audit_log`, the table the
7
+ * G3 sealer chains per tenant, ADR-0044), written with its own autocommit query, outside any
8
+ * business transaction.
9
+ *
10
+ * - An event without an actor or a tenant is skipped: `audit_log` requires both, and a row must
11
+ * be attributable to a tenant chain. (Guard's per-IP lockout and the pre-auth forgot-password
12
+ * throttles carry neither; they stay counters.)
13
+ * - Best-effort: an error is swallowed. Guard's emit is fire-and-forget, so sign-in never waits
14
+ * on, or fails because of, this write.
15
+ */
16
+ import type { AuditSink } from '@astram/guard';
17
+ import type { MariaDbAuditSink, QueryFn } from '@astram/engine';
18
+ export declare function createDefaultAuthAuditSink(engineSink: Pick<MariaDbAuditSink, 'record'>, query: QueryFn): AuditSink;
19
+ //# sourceMappingURL=auth-audit-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-audit-sink.d.ts","sourceRoot":"","sources":["../../../src/features/audit/auth-audit-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,eAAe,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEhE,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,EAC5C,KAAK,EAAE,OAAO,GACb,SAAS,CAqBX"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDefaultAuthAuditSink = createDefaultAuthAuditSink;
4
+ function createDefaultAuthAuditSink(engineSink, query) {
5
+ return {
6
+ async record(event) {
7
+ if (!event.actorId || !event.tenantId)
8
+ return;
9
+ try {
10
+ await engineSink.record({
11
+ type: event.type,
12
+ occurredAtMs: event.occurredAtMs,
13
+ actorId: event.actorId,
14
+ tenantId: event.tenantId,
15
+ action: { key: event.action },
16
+ ...(event.attributes !== undefined ? { attributes: event.attributes } : {}),
17
+ }, query);
18
+ }
19
+ catch {
20
+ // Best-effort: an audit write failure never reaches the auth flow.
21
+ }
22
+ },
23
+ };
24
+ }