@astram/identity 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/branch.object.d.ts +9 -1
  2. package/dist/branch.object.d.ts.map +1 -1
  3. package/dist/branch.object.js +14 -1
  4. package/dist/company-branch.object.d.ts +5 -4
  5. package/dist/company-branch.object.d.ts.map +1 -1
  6. package/dist/company-branch.object.js +6 -4
  7. package/dist/company-department.object.d.ts +6 -6
  8. package/dist/company-department.object.d.ts.map +1 -1
  9. package/dist/company-department.object.js +7 -6
  10. package/dist/department.object.d.ts +9 -1
  11. package/dist/department.object.d.ts.map +1 -1
  12. package/dist/department.object.js +14 -1
  13. package/dist/features/access/access-admin.access.d.ts.map +1 -1
  14. package/dist/features/access/access-admin.access.js +8 -1
  15. package/dist/features/access/index.d.ts +1 -0
  16. package/dist/features/access/index.d.ts.map +1 -1
  17. package/dist/features/access/index.js +10 -2
  18. package/dist/features/access/org-structure.access.d.ts +3 -0
  19. package/dist/features/access/org-structure.access.d.ts.map +1 -0
  20. package/dist/features/access/org-structure.access.js +41 -0
  21. package/dist/features/access/user-management.access.d.ts.map +1 -1
  22. package/dist/features/access/user-management.access.js +5 -0
  23. package/dist/features/audit/audit-read.service.d.ts +32 -1
  24. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  25. package/dist/features/audit/audit-read.service.js +67 -14
  26. package/dist/features/audit/auth-audit-sink.d.ts +19 -0
  27. package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
  28. package/dist/features/audit/auth-audit-sink.js +24 -0
  29. package/dist/features/audit/directory-label-resolver.d.ts +10 -1
  30. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
  31. package/dist/features/audit/directory-label-resolver.js +28 -1
  32. package/dist/features/authz/grant-admin.controller.d.ts +4 -4
  33. package/dist/features/authz/grant-admin.controller.js +4 -4
  34. package/dist/features/authz/grant-admin.service.d.ts +9 -8
  35. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  36. package/dist/features/authz/grant-admin.service.js +20 -24
  37. package/dist/features/authz/grant-ceiling.d.ts +35 -16
  38. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  39. package/dist/features/authz/grant-ceiling.js +44 -29
  40. package/dist/features/authz/role-binding.d.ts +25 -24
  41. package/dist/features/authz/role-binding.d.ts.map +1 -1
  42. package/dist/features/authz/role-binding.js +31 -44
  43. package/dist/features/identity-feature.module.d.ts +0 -11
  44. package/dist/features/identity-feature.module.d.ts.map +1 -1
  45. package/dist/features/identity-feature.module.js +90 -1
  46. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  47. package/dist/features/membership/membership.controller.js +20 -6
  48. package/dist/features/membership/membership.service.d.ts +16 -10
  49. package/dist/features/membership/membership.service.d.ts.map +1 -1
  50. package/dist/features/membership/membership.service.js +61 -21
  51. package/dist/features/membership/posting.service.d.ts +25 -6
  52. package/dist/features/membership/posting.service.d.ts.map +1 -1
  53. package/dist/features/membership/posting.service.js +63 -16
  54. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
  55. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
  56. package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
  57. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
  58. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
  59. package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
  60. package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
  61. package/dist/features/org-bootstrap/org-state.reader.js +56 -49
  62. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
  63. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
  64. package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
  65. package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
  66. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  67. package/dist/features/org-structure/org-catalog.controller.js +105 -17
  68. package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
  69. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  70. package/dist/features/org-structure/org-catalog.service.js +327 -51
  71. package/dist/features/org-structure/org-unit-name.d.ts +31 -0
  72. package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
  73. package/dist/features/org-structure/org-unit-name.js +50 -0
  74. package/dist/features/role/role.controller.d.ts +28 -13
  75. package/dist/features/role/role.controller.d.ts.map +1 -1
  76. package/dist/features/role/role.controller.js +54 -14
  77. package/dist/features/role/role.service.d.ts +76 -21
  78. package/dist/features/role/role.service.d.ts.map +1 -1
  79. package/dist/features/role/role.service.js +204 -27
  80. package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
  81. package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
  82. package/dist/features/security-policy/security-policy-version-bump.js +65 -0
  83. package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
  84. package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
  85. package/dist/features/security-policy/security-policy.controller.js +121 -0
  86. package/dist/features/security-policy/security-policy.service.d.ts +94 -0
  87. package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
  88. package/dist/features/security-policy/security-policy.service.js +429 -0
  89. package/dist/features/service-account/service-account.service.d.ts +13 -3
  90. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  91. package/dist/features/service-account/service-account.service.js +48 -10
  92. package/dist/features/service-account/service-account.store.d.ts +11 -1
  93. package/dist/features/service-account/service-account.store.d.ts.map +1 -1
  94. package/dist/features/service-account/service-account.store.js +20 -1
  95. package/dist/features/user/signin-admin.tokens.d.ts +11 -0
  96. package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
  97. package/dist/features/user/signin-admin.tokens.js +13 -0
  98. package/dist/features/user/user-mfa-status.d.ts +38 -0
  99. package/dist/features/user/user-mfa-status.d.ts.map +1 -0
  100. package/dist/features/user/user-mfa-status.js +47 -0
  101. package/dist/features/user/user-signin-admin.service.d.ts +48 -0
  102. package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
  103. package/dist/features/user/user-signin-admin.service.js +232 -0
  104. package/dist/features/user/user.controller.d.ts +29 -2
  105. package/dist/features/user/user.controller.d.ts.map +1 -1
  106. package/dist/features/user/user.controller.js +71 -3
  107. package/dist/features/user/user.service.d.ts +40 -14
  108. package/dist/features/user/user.service.d.ts.map +1 -1
  109. package/dist/features/user/user.service.js +143 -10
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +3 -1
  113. package/dist/objects.d.ts.map +1 -1
  114. package/dist/objects.js +2 -0
  115. package/dist/role.object.d.ts +1 -1
  116. package/dist/role.object.d.ts.map +1 -1
  117. package/dist/role.object.js +6 -15
  118. package/dist/security-policy.object.d.ts +43 -0
  119. package/dist/security-policy.object.d.ts.map +1 -0
  120. package/dist/security-policy.object.js +107 -0
  121. package/dist/user.object.d.ts +6 -0
  122. package/dist/user.object.d.ts.map +1 -1
  123. package/dist/user.object.js +13 -1
  124. package/migrations/1710000000102-CreateAuditChain.cjs +71 -0
  125. package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
  126. package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
  127. package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
  128. package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
  129. package/package.json +9 -4
@@ -12,7 +12,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  return function (target, key) { decorator(target, key, paramIndex); }
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.RoleService = void 0;
15
+ exports.RoleService = exports.NO_ROLE_COMPANY = void 0;
16
+ exports.roleNameMessage = roleNameMessage;
16
17
  const common_1 = require("@nestjs/common");
17
18
  const engine_1 = require("@astram/engine");
18
19
  const core_1 = require("@astram/core");
@@ -20,17 +21,64 @@ const list_query_js_1 = require("../shared/list-query.js");
20
21
  const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
21
22
  const grant_admin_service_js_1 = require("../authz/grant-admin.service.js");
22
23
  const posting_version_bump_js_1 = require("../membership/posting-version-bump.js");
24
+ const identity_errors_js_1 = require("../shared/identity-errors.js");
25
+ const org_unit_name_js_1 = require("../org-structure/org-unit-name.js");
23
26
  const ROLE_SEARCH_FIELDS = ['name'];
24
27
  const ROLE_SORT_FIELDS = ['name', 'createdAt'];
28
+ /** The 400 of `POST /identity/roles` for a caller with no company to own the role (spec §10). */
29
+ exports.NO_ROLE_COMPANY = 'Choose a company for the role.';
30
+ /** The fixed, user-facing 400 text for a refused role name (the G-012 rule, spec §10). */
31
+ function roleNameMessage(reason) {
32
+ switch (reason) {
33
+ case 'control_character':
34
+ return 'A role name cannot contain control characters.';
35
+ case 'too_long':
36
+ return `A role name can be at most ${org_unit_name_js_1.ORG_UNIT_NAME_MAX} characters.`;
37
+ default:
38
+ return 'Enter a role name.';
39
+ }
40
+ }
41
+ /** A role or company row has a single uuid key; a composite id here is a wiring defect. */
42
+ function singleId(id) {
43
+ if (typeof id !== 'string')
44
+ throw new TypeError('role/company id is not a single uuid');
45
+ return id;
46
+ }
47
+ /** A driver foreign-key miss (1452: the owner company does not exist), by errno/code only. */
48
+ function isMissingParent(err) {
49
+ const e = err;
50
+ return e.errno === 1452 || e.code === 'ER_NO_REFERENCED_ROW_2';
51
+ }
52
+ /** A driver duplicate-key error, by errno/code only (its message names the value: never echoed). */
53
+ function isDuplicateKey(err) {
54
+ if (typeof err !== 'object' || err === null)
55
+ return false;
56
+ const e = err;
57
+ return e.errno === 1062 || e.code === 'ER_DUP_ENTRY';
58
+ }
59
+ function nameOf(view) {
60
+ const name = view.fields?.name;
61
+ return typeof name === 'string' && name.length > 0 ? name : '(unknown)';
62
+ }
63
+ function ownerOf(view) {
64
+ const owner = view.fields?.ownerCompanyUuid;
65
+ return typeof owner === 'string' && owner.length > 0 ? owner : null;
66
+ }
67
+ function duplicate(storedName) {
68
+ return new identity_errors_js_1.IdentityConflictError(`A role named "${storedName}" already exists in this company.`);
69
+ }
25
70
  /**
26
- * RoleService — role CRUD on the paved road, via the framework's RecordsService
27
- * facade. The facade supplies the acting identity (the request principal) for the pipe;
28
- * no raw SQL, no RECORD_ENGINE.
71
+ * RoleService — role CRUD on the paved road, via the framework's RecordsService facade (the engine
72
+ * pipe: gate, wall, validate, write, audit). No raw SQL, no RECORD_ENGINE.
29
73
  *
30
- * Roles are a GLOBAL catalog, so renaming or deleting one changes every tenant that uses it (a
31
- * delete cascades its postings). Security hardening #1 (D1 interim rule): update/delete first pass
32
- * `GrantCeiling.assertRoleAdministrable` — the role must be bound inside the actor's subtree and
33
- * nowhere outside it (a shared role is root-only). The engine's write wall still applies after.
74
+ * G-013 (spec §5.A, ADR-0047): every role is OWNED by one company (`ownerCompanyUuid`, immutable),
75
+ * stamped here from the reached company (or the caller's active company on the legacy route), never
76
+ * from a request body (R1). The role object's `readScope` walls every read and write by that owner
77
+ * (R3: out of reach ⇒ 404; an ownerless legacy row is root only, R5). Names follow the G-012 rule
78
+ * (`normaliseOrgUnitName`) and are unique within the owner company under the column collation; the
79
+ * check runs under a lock inside one transaction and the per-company unique index is the backstop
80
+ * (a driver 1062 ⇒ the same 409, R6). Rename and delete first pass
81
+ * `GrantCeiling.assertRoleAdministrable` (owner in the actor's subtree, else 404).
34
82
  */
35
83
  let RoleService = class RoleService {
36
84
  records;
@@ -44,40 +92,102 @@ let RoleService = class RoleService {
44
92
  this.grants = grants;
45
93
  }
46
94
  /**
47
- * Create a role. The owner is derived here from the actor, never from a client value: a non-root
48
- * actor's role is owned by its active company (else its tenant); root's role is shared (no owner).
49
- * A root-flagged actor bound to a declared operation (the org bootstrap's `roles` step, Shield #9)
50
- * is anchored at the tenant it creates roles for, so its role is owned by that tenant.
51
- * No actor (internal callers) ⇒ no owner field.
95
+ * Create a role (`POST /identity/roles`, kept for existing clients). Owner = the caller's active
96
+ * company, else its tenant; a caller with neither ⇒ 400 "Choose a company for the role." The
97
+ * company is then reached and locked exactly as {@link createCompanyRole} does.
98
+ *
99
+ * `companyUuid` is for the org bootstrap only (spec §5.E): the bound `roles` step names the
100
+ * DECLARING company explicitly and holds no company key (gate 7), so that company is not read or
101
+ * locked here; the engine's create wall checks the owner against the actor's reach, the duplicate
102
+ * check runs in the transaction and the unique index is the backstop.
52
103
  */
53
- createRole(name, actor) {
54
- const fields = { name };
55
- if (actor && (actor.isRoot !== true || (0, engine_1.operationOf)(actor) !== undefined)) {
56
- fields.ownerCompanyUuid = actor.activeScopeNodeId ?? actor.tenantId;
104
+ async createRole(rawName, actor, companyUuid) {
105
+ const name = this.validName(rawName);
106
+ if (companyUuid !== undefined) {
107
+ if (companyUuid.length === 0)
108
+ throw new identity_errors_js_1.IdentityBadRequestError(exports.NO_ROLE_COMPANY);
109
+ // Bootstrap-only path (no company read: its gate-7 step holds role keys only). A company that
110
+ // does not exist fails the owner FK (1452): answered as 404, never the driver error.
111
+ try {
112
+ return await this.mappingDuplicates(() => companyUuid, name, () => this.records.transaction((tx) => this.insertRole(tx, companyUuid, name)));
113
+ }
114
+ catch (err) {
115
+ if (isMissingParent(err))
116
+ throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
117
+ throw err;
118
+ }
57
119
  }
58
- return this.records.create('role', { fields });
120
+ const company = actor.activeScopeNodeId || actor.tenantId;
121
+ if (typeof company !== 'string' || company.length === 0) {
122
+ throw new identity_errors_js_1.IdentityBadRequestError(exports.NO_ROLE_COMPANY);
123
+ }
124
+ return (await this.createInCompany(company, name)).uuid;
59
125
  }
60
- /** List roles (tenant-scoped by the pipe), with paging/search/sort. */
126
+ /**
127
+ * Create a role in `companyUuid` (`POST /identity/companies/:companyUuid/roles`): normalise the
128
+ * name; in one transaction lock the company, reach it through the wall (404), refuse a name the
129
+ * company already holds (409 naming the stored role) and create the role owned by the REACHED
130
+ * company. Returns `{ uuid, name }` (the stored name).
131
+ */
132
+ async createCompanyRole(companyUuid, rawName) {
133
+ return this.createInCompany(companyUuid, this.validName(rawName));
134
+ }
135
+ /**
136
+ * One company's own roles (`GET /identity/companies/:companyUuid/roles`): the company is reached
137
+ * through the walled read first (404 when out of reach), then its roles are listed with the same
138
+ * paging, search and sort allowlists as {@link listRoles}.
139
+ */
140
+ async listCompanyRoles(companyUuid, query) {
141
+ const company = await this.records.get('company', companyUuid);
142
+ if (company === null)
143
+ throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
144
+ const { page, limit, params } = (0, list_query_js_1.parseListQuery)(query);
145
+ const listQuery = {
146
+ ...(0, list_query_js_1.buildListQuery)(params, ROLE_SEARCH_FIELDS, ROLE_SORT_FIELDS),
147
+ where: { ownerCompanyUuid: singleId(company.id) },
148
+ };
149
+ const result = await this.records.list('role', listQuery);
150
+ return { total: result.total, rows: result.rows, page, limit };
151
+ }
152
+ /** List roles (walled by owner through the role object's readScope), with paging/search/sort. */
61
153
  listRoles(params) {
62
154
  return this.records.list('role', (0, list_query_js_1.buildListQuery)(params, ROLE_SEARCH_FIELDS, ROLE_SORT_FIELDS));
63
155
  }
64
- /** Read a single role. */
156
+ /** Read a single role (walled by owner). */
65
157
  getRole(uuid) {
66
158
  return this.records.get('role', uuid);
67
159
  }
68
- /** Rename a role — only a role the actor may administer (binding rule). Patch whitelisted. */
160
+ /**
161
+ * Rename a role — only a role the actor may administer (owner in reach, else 404). The name goes
162
+ * through the name rule (400); in one transaction the role row is locked, re-read through the wall
163
+ * (404; ownerless ⇒ root only), and a name another role of the same company holds is refused (409
164
+ * naming the stored role). The same name ⇒ no write. Only `name` is ever written.
165
+ */
69
166
  async updateRole(actor, uuid, patch) {
70
167
  await this.ceiling.assertRoleAdministrable(actor, uuid);
71
- const fields = {};
72
- if (typeof patch?.name === 'string')
73
- fields.name = patch.name;
74
- await this.records.update('role', uuid, { fields });
168
+ const name = this.validName(patch?.name);
169
+ let owner = null;
170
+ await this.mappingDuplicates(() => owner, name, () => this.records.transaction(async (tx) => {
171
+ await tx.lockForUpdate('role', uuid);
172
+ const row = await tx.get('role', uuid);
173
+ if (row === null)
174
+ throw new identity_errors_js_1.IdentityNotFoundError('Role not found.');
175
+ owner = ownerOf(row);
176
+ if (owner === null && actor.isRoot !== true) {
177
+ throw new identity_errors_js_1.IdentityNotFoundError('Role not found.');
178
+ }
179
+ if (row.fields?.name === name)
180
+ return; // same name: no write
181
+ if (owner !== null)
182
+ await this.assertNameFree(tx, owner, name, singleId(row.id));
183
+ await tx.update('role', row.id, { fields: { name } });
184
+ }));
75
185
  }
76
186
  /**
77
- * Delete a role — only a role the actor may administer (binding rule) AND whose every holder the
187
+ * Delete a role — only a role the actor may administer (owner rule) AND whose every holder the
78
188
  * actor dominates (Phase 8 #2: the delete cascades the holders' postings, stripping their
79
189
  * authority). In ONE transaction: lock the role row (the posting and grant paths lock the same
80
- * row first), re-check the binding rule and holder dominance against what is committed now, read
190
+ * row first), re-check the owner rule and holder dominance against what is committed now, read
81
191
  * the holder tenants (before the cascade removes the postings), then delete. After the commit,
82
192
  * every holder tenant's permission version is bumped so cached roles drop at the next request.
83
193
  * The bump runs after the commit, never before: an earlier bump would let a session reload the
@@ -124,6 +234,73 @@ let RoleService = class RoleService {
124
234
  throw new core_1.AuthorizationUnavailableError('permission version bump failed', cause);
125
235
  }
126
236
  }
237
+ // ------------------------------------------------------------------------------- internals
238
+ validName(raw) {
239
+ const result = (0, org_unit_name_js_1.normaliseOrgUnitName)(raw);
240
+ if (!result.ok)
241
+ throw new identity_errors_js_1.IdentityBadRequestError(roleNameMessage(result.reason));
242
+ return result.value;
243
+ }
244
+ /** Lock the company FIRST, reach it through the wall (404), then create the role it owns. */
245
+ async createInCompany(companyUuid, name) {
246
+ let owner = companyUuid;
247
+ const uuid = await this.mappingDuplicates(() => owner, name, () => this.records.transaction(async (tx) => {
248
+ await tx.lockForUpdate('company', companyUuid);
249
+ const company = await tx.get('company', companyUuid);
250
+ if (company === null)
251
+ throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
252
+ owner = singleId(company.id);
253
+ return this.insertRole(tx, owner, name);
254
+ }));
255
+ return { uuid, name };
256
+ }
257
+ /** Duplicate check, then the role owned by `owner`, on the caller's transaction. */
258
+ async insertRole(tx, owner, name) {
259
+ await this.assertNameFree(tx, owner, name, undefined);
260
+ return singleId(await tx.create('role', { fields: { name, ownerCompanyUuid: owner } }));
261
+ }
262
+ /**
263
+ * Refuse a name another role of the same company already holds. The engine's equality runs under
264
+ * the column collation (`utf8mb4_unicode_ci`: case, accents and width fold), so "manager" finds
265
+ * "Manager"; the 409 names the STORED clashing role.
266
+ */
267
+ async assertNameFree(tx, owner, name, self) {
268
+ const clash = await this.findByName(tx, owner, name, self);
269
+ if (clash !== undefined)
270
+ throw duplicate(nameOf(clash));
271
+ }
272
+ async findByName(reader, owner, name, self) {
273
+ const rows = (await reader.list('role', { where: { ownerCompanyUuid: owner, name }, limit: 2 }))
274
+ .rows;
275
+ return rows.find((r) => self === undefined || singleId(r.id) !== self);
276
+ }
277
+ /**
278
+ * Run a create/rename; a driver duplicate key (the per-company unique index, the backstop for a
279
+ * concurrent write) becomes the same 409 naming the stored role. Errno only: the driver's message
280
+ * holds the value and is never echoed (KI #87).
281
+ */
282
+ async mappingDuplicates(owner, name, write) {
283
+ try {
284
+ return await write();
285
+ }
286
+ catch (err) {
287
+ if (!isDuplicateKey(err))
288
+ throw err;
289
+ let stored = name;
290
+ const company = owner();
291
+ if (company !== null) {
292
+ try {
293
+ const clash = await this.findByName(this.records, company, name);
294
+ if (clash !== undefined)
295
+ stored = nameOf(clash);
296
+ }
297
+ catch {
298
+ // The 409 stands; the submitted name names the clash.
299
+ }
300
+ }
301
+ throw duplicate(stored);
302
+ }
303
+ }
127
304
  };
128
305
  exports.RoleService = RoleService;
129
306
  exports.RoleService = RoleService = __decorate([
@@ -0,0 +1,41 @@
1
+ import type { PermissionVersionBumper } from '../authz/permission-version.bumper.js';
2
+ import type { IdentitySchemaNames } from '../../schema.js';
3
+ /** The query surface used here: the write transaction's own connection (via `transactionQueryOf`). */
4
+ export interface Queryable {
5
+ query(sql: string, params?: readonly unknown[]): Promise<unknown>;
6
+ }
7
+ type Bumper = Pick<PermissionVersionBumper, 'bumpForTenant' | 'publishCommitted' | 'discardPending'>;
8
+ /**
9
+ * Permission-version bump for a security-policy write (auth capabilities R2.5, F17): every tenant
10
+ * whose signed-in users the policy can affect gets its version bumped ON THE WRITE'S TRANSACTION,
11
+ * so the policy and the bump commit or roll back together, and guard's recompute (task 14) sees
12
+ * the change on each user's next request. The new versions reach Redis only after the commit
13
+ * (`publishCommitted`, the ONE `PermissionVersionBumper`, ADR-0035).
14
+ *
15
+ * Which tenants:
16
+ * - a scoped policy: its node and every descendant (`scope_closure`). A policy at a node applies
17
+ * only to sessions ACTIVE at that node or below it (core rule; the version key is the active
18
+ * node's), so ancestors are not bumped (review round 1, item I).
19
+ * - a platform floor policy: every company (no global version exists). One INSERT-or-increment
20
+ * per company inside the transaction; a floor write is a rare root action.
21
+ *
22
+ * Any read or bump failure is an `AuthorizationUnavailableError` with a fixed message (503, the
23
+ * write rolls back): a policy must never commit without its bump.
24
+ */
25
+ export declare class SecurityPolicyVersionBump {
26
+ private readonly bumper;
27
+ private readonly schemas;
28
+ constructor(bumper: Bumper, schemas: IdentitySchemaNames);
29
+ /** The tenants a policy at `scopeNodeId` (`null` = platform floor) can affect. */
30
+ tenantsInScope(scopeNodeId: string | null, conn: Queryable): Promise<string[]>;
31
+ /** Increment each tenant's permission version on `conn` (published after the commit). */
32
+ bump(tenants: readonly string[], conn: Queryable): Promise<void>;
33
+ /** Publish the versions bumped on `conn` — call after the commit. Never throws. */
34
+ publishCommitted(conn: Queryable): Promise<void>;
35
+ /** Drop the versions bumped on `conn` — call after a rollback. */
36
+ discardPending(conn: Queryable): void;
37
+ }
38
+ /** DI token for the {@link SecurityPolicyVersionBump} built in `IdentityFeatureModule.forRoot`. */
39
+ export declare const SECURITY_POLICY_VERSION_BUMP: unique symbol;
40
+ export {};
41
+ //# sourceMappingURL=security-policy-version-bump.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-policy-version-bump.d.ts","sourceRoot":"","sources":["../../../src/features/security-policy/security-policy-version-bump.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,sGAAsG;AACtG,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED,KAAK,MAAM,GAAG,IAAI,CAChB,uBAAuB,EACvB,eAAe,GAAG,kBAAkB,GAAG,gBAAgB,CACxD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,yBAAyB;IAElC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBADP,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB;IAG/C,kFAAkF;IAC5E,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBpF,yFAAyF;IACnF,IAAI,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtE,mFAAmF;IACnF,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhD,kEAAkE;IAClE,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;CAGtC;AAED,mGAAmG;AACnG,eAAO,MAAM,4BAA4B,eAAyC,CAAC"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SECURITY_POLICY_VERSION_BUMP = exports.SecurityPolicyVersionBump = void 0;
4
+ const core_1 = require("@astram/core");
5
+ /**
6
+ * Permission-version bump for a security-policy write (auth capabilities R2.5, F17): every tenant
7
+ * whose signed-in users the policy can affect gets its version bumped ON THE WRITE'S TRANSACTION,
8
+ * so the policy and the bump commit or roll back together, and guard's recompute (task 14) sees
9
+ * the change on each user's next request. The new versions reach Redis only after the commit
10
+ * (`publishCommitted`, the ONE `PermissionVersionBumper`, ADR-0035).
11
+ *
12
+ * Which tenants:
13
+ * - a scoped policy: its node and every descendant (`scope_closure`). A policy at a node applies
14
+ * only to sessions ACTIVE at that node or below it (core rule; the version key is the active
15
+ * node's), so ancestors are not bumped (review round 1, item I).
16
+ * - a platform floor policy: every company (no global version exists). One INSERT-or-increment
17
+ * per company inside the transaction; a floor write is a rare root action.
18
+ *
19
+ * Any read or bump failure is an `AuthorizationUnavailableError` with a fixed message (503, the
20
+ * write rolls back): a policy must never commit without its bump.
21
+ */
22
+ class SecurityPolicyVersionBump {
23
+ bumper;
24
+ schemas;
25
+ constructor(bumper, schemas) {
26
+ this.bumper = bumper;
27
+ this.schemas = schemas;
28
+ }
29
+ /** The tenants a policy at `scopeNodeId` (`null` = platform floor) can affect. */
30
+ async tenantsInScope(scopeNodeId, conn) {
31
+ try {
32
+ if (scopeNodeId === null) {
33
+ const rows = (await conn.query(`SELECT c_uuid AS tenant FROM \`${this.schemas.app}\`.\`companies\``));
34
+ return [...new Set(rows.map((r) => r.tenant))];
35
+ }
36
+ const closure = `\`${this.schemas.authz}\`.\`scope_closure\``;
37
+ const down = (await conn.query(`SELECT descendant_uuid AS tenant FROM ${closure} WHERE ancestor_uuid = ?`, [scopeNodeId]));
38
+ return [...new Set([scopeNodeId, ...down.map((r) => r.tenant)])];
39
+ }
40
+ catch {
41
+ throw new core_1.AuthorizationUnavailableError('security policy scope unavailable');
42
+ }
43
+ }
44
+ /** Increment each tenant's permission version on `conn` (published after the commit). */
45
+ async bump(tenants, conn) {
46
+ try {
47
+ for (const tenant of tenants)
48
+ await this.bumper.bumpForTenant(tenant, conn);
49
+ }
50
+ catch {
51
+ throw new core_1.AuthorizationUnavailableError('permission version bump failed');
52
+ }
53
+ }
54
+ /** Publish the versions bumped on `conn` — call after the commit. Never throws. */
55
+ publishCommitted(conn) {
56
+ return this.bumper.publishCommitted(conn);
57
+ }
58
+ /** Drop the versions bumped on `conn` — call after a rollback. */
59
+ discardPending(conn) {
60
+ this.bumper.discardPending(conn);
61
+ }
62
+ }
63
+ exports.SecurityPolicyVersionBump = SecurityPolicyVersionBump;
64
+ /** DI token for the {@link SecurityPolicyVersionBump} built in `IdentityFeatureModule.forRoot`. */
65
+ exports.SECURITY_POLICY_VERSION_BUMP = Symbol('SECURITY_POLICY_VERSION_BUMP');
@@ -0,0 +1,51 @@
1
+ import { SecurityPolicyService } from './security-policy.service.js';
2
+ import { type ListQueryDto } from '../shared/list-query.js';
3
+ import type { AuthedRequest } from '../shared/http.js';
4
+ interface CreateBody {
5
+ scopeNodeUuid?: unknown;
6
+ platform?: unknown;
7
+ targetKind?: unknown;
8
+ targetValue?: unknown;
9
+ graceDays?: unknown;
10
+ }
11
+ interface UpdateBody {
12
+ targetValue?: unknown;
13
+ graceDays?: unknown;
14
+ }
15
+ /**
16
+ * `/identity/security-policies` — mandatory-2FA policies (auth capabilities R2.9). Every route is
17
+ * `@Classified` on `security_policy` (registered Gated, `identity:security_policy:<action>`); the
18
+ * R2.6 authority rules, audit and permission-version bump live in {@link SecurityPolicyService}.
19
+ * Bodies are reduced to the documented fields; the scope node is canonicalised here (400 if not a
20
+ * uuid). No static sub-paths, so route order does not matter.
21
+ */
22
+ export declare class SecurityPolicyController {
23
+ private readonly policies;
24
+ constructor(policies: SecurityPolicyService);
25
+ /** List policies — `identity:security_policy:read`; `{ total, rows, page, limit }`. */
26
+ listPolicies(query: ListQueryDto): Promise<{
27
+ total: number;
28
+ rows: import("packages/engine/dist/types.js").RecordView[];
29
+ page: number;
30
+ limit: number;
31
+ }>;
32
+ /** Read one policy — `identity:security_policy:read`. */
33
+ getPolicy(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
34
+ /**
35
+ * Create a policy — `identity:security_policy:create`. Body `{ scopeNodeUuid?, platform?,
36
+ * targetKind, targetValue?, graceDays? }`; no node ⇒ the caller's active organisation.
37
+ */
38
+ createPolicy(req: AuthedRequest, body: CreateBody): Promise<{
39
+ id: import("packages/engine/dist/types.js").RecordId;
40
+ }>;
41
+ /** Change a policy's target or grace — `identity:security_policy:update`. */
42
+ updatePolicy(req: AuthedRequest, uuid: string, body: UpdateBody): Promise<{
43
+ success: boolean;
44
+ }>;
45
+ /** Delete a policy — `identity:security_policy:delete`. */
46
+ deletePolicy(req: AuthedRequest, uuid: string): Promise<{
47
+ success: boolean;
48
+ }>;
49
+ }
50
+ export {};
51
+ //# sourceMappingURL=security-policy.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-policy.controller.d.ts","sourceRoot":"","sources":["../../../src/features/security-policy/security-policy.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,UAAU,UAAU;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,UAAU;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;GAMG;AACH,qBAEa,wBAAwB;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,qBAAqB;IAE5D,uFAAuF;IAGjF,YAAY,CAAU,KAAK,EAAE,YAAY;;;;;;IAM/C,yDAAyD;IAGzD,SAAS,CAA0B,IAAI,EAAE,MAAM;IAI/C;;;OAGG;IAGG,YAAY,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,UAAU;;;IAYtE,6EAA6E;IAGvE,YAAY,CACT,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE,UAAU;;;IAS1B,2DAA2D;IAGrD,YAAY,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIpF"}
@@ -0,0 +1,121 @@
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
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SecurityPolicyController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const guard_1 = require("@astram/guard");
18
+ const security_policy_service_js_1 = require("./security-policy.service.js");
19
+ const list_query_js_1 = require("../shared/list-query.js");
20
+ const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
21
+ /**
22
+ * `/identity/security-policies` — mandatory-2FA policies (auth capabilities R2.9). Every route is
23
+ * `@Classified` on `security_policy` (registered Gated, `identity:security_policy:<action>`); the
24
+ * R2.6 authority rules, audit and permission-version bump live in {@link SecurityPolicyService}.
25
+ * Bodies are reduced to the documented fields; the scope node is canonicalised here (400 if not a
26
+ * uuid). No static sub-paths, so route order does not matter.
27
+ */
28
+ let SecurityPolicyController = class SecurityPolicyController {
29
+ policies;
30
+ constructor(policies) {
31
+ this.policies = policies;
32
+ }
33
+ /** List policies — `identity:security_policy:read`; `{ total, rows, page, limit }`. */
34
+ async listPolicies(query) {
35
+ const { page, limit, params } = (0, list_query_js_1.parseListQuery)(query);
36
+ const result = await this.policies.listPolicies(params);
37
+ return { total: result.total, rows: result.rows, page, limit };
38
+ }
39
+ /** Read one policy — `identity:security_policy:read`. */
40
+ getPolicy(uuid) {
41
+ return this.policies.getPolicy(uuid);
42
+ }
43
+ /**
44
+ * Create a policy — `identity:security_policy:create`. Body `{ scopeNodeUuid?, platform?,
45
+ * targetKind, targetValue?, graceDays? }`; no node ⇒ the caller's active organisation.
46
+ */
47
+ async createPolicy(req, body) {
48
+ const scopeNodeUuid = (0, guard_1.optionalUuidField)(body?.scopeNodeUuid, 'scopeNodeUuid');
49
+ const id = await this.policies.createPolicy(req.principal, {
50
+ ...(scopeNodeUuid !== undefined ? { scopeNodeUuid } : {}),
51
+ targetKind: body?.targetKind,
52
+ targetValue: body?.targetValue,
53
+ graceDays: body?.graceDays,
54
+ platform: body?.platform,
55
+ });
56
+ return { id };
57
+ }
58
+ /** Change a policy's target or grace — `identity:security_policy:update`. */
59
+ async updatePolicy(req, uuid, body) {
60
+ await this.policies.updatePolicy(req.principal, uuid, {
61
+ graceDays: body?.graceDays,
62
+ targetValue: body?.targetValue,
63
+ });
64
+ return { success: true };
65
+ }
66
+ /** Delete a policy — `identity:security_policy:delete`. */
67
+ async deletePolicy(req, uuid) {
68
+ await this.policies.deletePolicy(req.principal, uuid);
69
+ return { success: true };
70
+ }
71
+ };
72
+ exports.SecurityPolicyController = SecurityPolicyController;
73
+ __decorate([
74
+ (0, common_1.Get)('security-policies'),
75
+ (0, guard_1.Classified)({ typeKey: 'security_policy', action: 'read' }),
76
+ __param(0, (0, common_1.Query)()),
77
+ __metadata("design:type", Function),
78
+ __metadata("design:paramtypes", [Object]),
79
+ __metadata("design:returntype", Promise)
80
+ ], SecurityPolicyController.prototype, "listPolicies", null);
81
+ __decorate([
82
+ (0, common_1.Get)('security-policies/:uuid'),
83
+ (0, guard_1.Classified)({ typeKey: 'security_policy', action: 'read' }),
84
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
85
+ __metadata("design:type", Function),
86
+ __metadata("design:paramtypes", [String]),
87
+ __metadata("design:returntype", void 0)
88
+ ], SecurityPolicyController.prototype, "getPolicy", null);
89
+ __decorate([
90
+ (0, common_1.Post)('security-policies'),
91
+ (0, guard_1.Classified)({ typeKey: 'security_policy', action: 'create' }),
92
+ __param(0, (0, common_1.Req)()),
93
+ __param(1, (0, common_1.Body)()),
94
+ __metadata("design:type", Function),
95
+ __metadata("design:paramtypes", [Object, Object]),
96
+ __metadata("design:returntype", Promise)
97
+ ], SecurityPolicyController.prototype, "createPolicy", null);
98
+ __decorate([
99
+ (0, common_1.Put)('security-policies/:uuid'),
100
+ (0, guard_1.Classified)({ typeKey: 'security_policy', action: 'update' }),
101
+ __param(0, (0, common_1.Req)()),
102
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
103
+ __param(2, (0, common_1.Body)()),
104
+ __metadata("design:type", Function),
105
+ __metadata("design:paramtypes", [Object, String, Object]),
106
+ __metadata("design:returntype", Promise)
107
+ ], SecurityPolicyController.prototype, "updatePolicy", null);
108
+ __decorate([
109
+ (0, common_1.Delete)('security-policies/:uuid'),
110
+ (0, guard_1.Classified)({ typeKey: 'security_policy', action: 'delete' }),
111
+ __param(0, (0, common_1.Req)()),
112
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
113
+ __metadata("design:type", Function),
114
+ __metadata("design:paramtypes", [Object, String]),
115
+ __metadata("design:returntype", Promise)
116
+ ], SecurityPolicyController.prototype, "deletePolicy", null);
117
+ exports.SecurityPolicyController = SecurityPolicyController = __decorate([
118
+ (0, common_1.Controller)('identity'),
119
+ (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
120
+ __metadata("design:paramtypes", [security_policy_service_js_1.SecurityPolicyService])
121
+ ], SecurityPolicyController);