@astram/identity 0.7.1 → 0.8.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 (40) hide show
  1. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
  2. package/dist/features/authz/grant-admin.controller.js +5 -5
  3. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  4. package/dist/features/authz/grant-ceiling.js +7 -3
  5. package/dist/features/company/company.controller.d.ts.map +1 -1
  6. package/dist/features/company/company.controller.js +9 -5
  7. package/dist/features/company/company.service.d.ts +5 -0
  8. package/dist/features/company/company.service.d.ts.map +1 -1
  9. package/dist/features/company/company.service.js +11 -3
  10. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
  11. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +5 -10
  12. package/dist/features/identity-feature.module.d.ts.map +1 -1
  13. package/dist/features/identity-feature.module.js +7 -0
  14. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  15. package/dist/features/membership/membership.controller.js +25 -8
  16. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  17. package/dist/features/org-structure/org-catalog.controller.js +2 -2
  18. package/dist/features/policy/policy.controller.d.ts.map +1 -1
  19. package/dist/features/policy/policy.controller.js +1 -1
  20. package/dist/features/role/role.controller.d.ts.map +1 -1
  21. package/dist/features/role/role.controller.js +3 -3
  22. package/dist/features/service-account/service-account.controller.d.ts.map +1 -1
  23. package/dist/features/service-account/service-account.controller.js +12 -18
  24. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  25. package/dist/features/service-account/service-account.service.js +27 -8
  26. package/dist/features/share/share.controller.d.ts.map +1 -1
  27. package/dist/features/share/share.controller.js +7 -7
  28. package/dist/features/shared/http.d.ts +11 -5
  29. package/dist/features/shared/http.d.ts.map +1 -1
  30. package/dist/features/shared/http.js +17 -5
  31. package/dist/features/user/email-address-changer.token.d.ts +8 -0
  32. package/dist/features/user/email-address-changer.token.d.ts.map +1 -0
  33. package/dist/features/user/email-address-changer.token.js +10 -0
  34. package/dist/features/user/user.controller.d.ts.map +1 -1
  35. package/dist/features/user/user.controller.js +5 -6
  36. package/dist/features/user/user.service.d.ts +18 -2
  37. package/dist/features/user/user.service.d.ts.map +1 -1
  38. package/dist/features/user/user.service.js +66 -8
  39. package/migrations/1710000000020-CreateEmailChangeToken.cjs +62 -0
  40. package/package.json +3 -3
@@ -1 +1 @@
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"}
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,EAA2B,IAAI,EAAE,MAAM;;;IAI3E,0CAA0C;IAGpC,KAAK,CACF,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAahC,mFAAmF;IAG7E,MAAM,CACH,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EACvB,GAAG,EAAE,MAAM;;;IAQ3B;;;;OAIG;IAGG,OAAO;;;IAIb;;;;OAIG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAahC;;;;OAIG;IAGG,YAAY,CACT,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EACvB,GAAG,EAAE,MAAM;;;CAU5B"}
@@ -170,7 +170,7 @@ __decorate([
170
170
  (0, common_1.Get)('roles/:uuid/permissions'),
171
171
  (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
172
172
  __param(0, (0, common_1.Req)()),
173
- __param(1, (0, common_1.Param)('uuid')),
173
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
174
174
  __metadata("design:type", Function),
175
175
  __metadata("design:paramtypes", [Object, String]),
176
176
  __metadata("design:returntype", Promise)
@@ -179,7 +179,7 @@ __decorate([
179
179
  (0, common_1.Post)('roles/:uuid/permissions'),
180
180
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
181
181
  __param(0, (0, common_1.Req)()),
182
- __param(1, (0, common_1.Param)('uuid')),
182
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
183
183
  __param(2, (0, common_1.Body)()),
184
184
  __metadata("design:type", Function),
185
185
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -189,7 +189,7 @@ __decorate([
189
189
  (0, common_1.Delete)('roles/:uuid/permissions/:key'),
190
190
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
191
191
  __param(0, (0, common_1.Req)()),
192
- __param(1, (0, common_1.Param)('uuid')),
192
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
193
193
  __param(2, (0, common_1.Param)('key')),
194
194
  __metadata("design:type", Function),
195
195
  __metadata("design:paramtypes", [Object, String, String]),
@@ -206,7 +206,7 @@ __decorate([
206
206
  (0, common_1.Post)('roles/:uuid/access-bundles'),
207
207
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
208
208
  __param(0, (0, common_1.Req)()),
209
- __param(1, (0, common_1.Param)('uuid')),
209
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
210
210
  __param(2, (0, common_1.Body)()),
211
211
  __metadata("design:type", Function),
212
212
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -216,7 +216,7 @@ __decorate([
216
216
  (0, common_1.Delete)('roles/:uuid/access-bundles/:key'),
217
217
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
218
218
  __param(0, (0, common_1.Req)()),
219
- __param(1, (0, common_1.Param)('uuid')),
219
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
220
220
  __param(2, (0, common_1.Param)('key')),
221
221
  __metadata("design:type", Function),
222
222
  __metadata("design:paramtypes", [Object, String, String]),
@@ -1 +1 @@
1
- {"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
@@ -197,8 +197,8 @@ class GrantCeiling {
197
197
  throw new identity_errors_js_1.IdentityForbiddenError('You cannot change this user: they are a platform administrator.');
198
198
  }
199
199
  const subtree = new Set(subtreeOf(actor));
200
- const { companies, roleIds, superuser } = await this.facts('target memberships', async () => {
201
- const su = (await this.pool.query(`SELECT u_is_platform_superuser AS su FROM \`${APP}\`.\`users\` WHERE u_uuid = ? LIMIT 1`, [targetUserId]));
200
+ const { companies, roleIds, superuser, storedId } = await this.facts('target memberships', async () => {
201
+ const su = (await this.pool.query(`SELECT u_uuid, u_is_platform_superuser AS su FROM \`${APP}\`.\`users\` WHERE u_uuid = ? LIMIT 1`, [targetUserId]));
202
202
  const memberships = (await this.pool.query(`SELECT DISTINCT cm_company_uuid AS company
203
203
  FROM \`${APP}\`.\`company_membership\`
204
204
  WHERE cm_user_uuid = ?`, [targetUserId]));
@@ -210,9 +210,13 @@ class GrantCeiling {
210
210
  companies: memberships.map((m) => m.company),
211
211
  roleIds: postings.map((p) => p.role_uuid),
212
212
  superuser: Number(su[0]?.su ?? 0) === 1,
213
+ storedId: typeof su[0]?.u_uuid === 'string' ? su[0].u_uuid : targetUserId,
213
214
  };
214
215
  });
215
- if (superuser) {
216
+ // Phase 11 #1: `u_uuid` is `utf8mb4_unicode_ci` (case, trailing spaces and character width are
217
+ // ignored), so the caller's spelling may reach a platform administrator's row without matching
218
+ // `rootIds`. Judge the STORED uuid too.
219
+ if (superuser || this.rootIds.some((r) => (0, http_js_1.sameUuid)(r, storedId))) {
216
220
  throw new identity_errors_js_1.IdentityForbiddenError('You cannot change this user: they are a platform administrator.');
217
221
  }
218
222
  // Security review F5: a user with NO membership is in nobody's subtree — refuse like the
@@ -1 +1 @@
1
- {"version":3,"file":"company.controller.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.controller.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,qBAEa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,cAAc;IAEtD;;;;OAIG;IAGG,aAAa,CAAU,KAAK,EAAE,YAAY;;;;;;IAMhD;;;OAGG;IAGG,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAK9E,2DAA2D;IAG3D,UAAU,CAAgB,IAAI,EAAE,MAAM;IAItC;;;;;OAKG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;IAa9C;;;OAGG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IASlD,iGAAiG;IAG3F,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAI3E"}
1
+ {"version":3,"file":"company.controller.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.controller.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,qBAEa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,cAAc;IAEtD;;;;OAIG;IAGG,aAAa,CAAU,KAAK,EAAE,YAAY;;;;;;IAMhD;;;OAGG;IAGG,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAK9E,2DAA2D;IAG3D,UAAU,CAA0B,IAAI,EAAE,MAAM;IAIhD;;;;;OAKG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;IAiB9C;;;OAGG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IASlD,iGAAiG;IAG3F,aAAa,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIrF"}
@@ -57,7 +57,11 @@ let CompanyController = class CompanyController {
57
57
  (body.parentUuid !== null && typeof body.parentUuid !== 'string')) {
58
58
  throw new common_1.BadRequestException('parentUuid (a company id, or null) is required');
59
59
  }
60
- await this.companies.moveCompany(req.principal, uuid, body.parentUuid ?? null);
60
+ const parentUuid = (0, guard_1.optionalUuidField)(body.parentUuid, 'parentUuid', { keepNull: true });
61
+ if (parentUuid === undefined) {
62
+ throw new common_1.BadRequestException('parentUuid (a company id, or null) is required');
63
+ }
64
+ await this.companies.moveCompany(req.principal, uuid, parentUuid);
61
65
  return { success: true };
62
66
  }
63
67
  /**
@@ -98,7 +102,7 @@ __decorate([
98
102
  __decorate([
99
103
  (0, common_1.Get)('companies/:uuid'),
100
104
  (0, guard_1.Classified)({ typeKey: 'company', action: 'read' }),
101
- __param(0, (0, common_1.Param)('uuid')),
105
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
102
106
  __metadata("design:type", Function),
103
107
  __metadata("design:paramtypes", [String]),
104
108
  __metadata("design:returntype", void 0)
@@ -107,7 +111,7 @@ __decorate([
107
111
  (0, common_1.Put)('companies/:uuid/parent'),
108
112
  (0, guard_1.Classified)({ typeKey: 'company', action: 'update' }),
109
113
  __param(0, (0, common_1.Req)()),
110
- __param(1, (0, common_1.Param)('uuid')),
114
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
111
115
  __param(2, (0, common_1.Body)()),
112
116
  __metadata("design:type", Function),
113
117
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -117,7 +121,7 @@ __decorate([
117
121
  (0, common_1.Put)('companies/:uuid'),
118
122
  (0, guard_1.Classified)({ typeKey: 'company', action: 'update' }),
119
123
  __param(0, (0, common_1.Req)()),
120
- __param(1, (0, common_1.Param)('uuid')),
124
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
121
125
  __param(2, (0, common_1.Body)()),
122
126
  __metadata("design:type", Function),
123
127
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -127,7 +131,7 @@ __decorate([
127
131
  (0, common_1.Delete)('companies/:uuid'),
128
132
  (0, guard_1.Classified)({ typeKey: 'company', action: 'delete' }),
129
133
  __param(0, (0, common_1.Req)()),
130
- __param(1, (0, common_1.Param)('uuid')),
134
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
131
135
  __metadata("design:type", Function),
132
136
  __metadata("design:paramtypes", [Object, String]),
133
137
  __metadata("design:returntype", Promise)
@@ -33,6 +33,11 @@ export declare class CompanyService {
33
33
  * rooted at (the same own-root guard as move and delete, F6): deactivating it would act on the
34
34
  * whole tenant above the actor's authority. Re-sending the current status (the company form
35
35
  * always sends both fields) stays allowed, so the own root can still be renamed.
36
+ *
37
+ * Phase 11 #1: the guard compares the STORED uuid, never the caller's spelling. `c_uuid` is
38
+ * `utf8mb4_unicode_ci`, which also ignores trailing spaces and character width, so a padded or
39
+ * fullwidth route uuid reaches the same row. A non-root status change therefore reads the company
40
+ * first (the walled read; out of reach → 404) and judges and writes by the id that read returns.
36
41
  */
37
42
  updateCompany(actor: Principal, uuid: string, patch: Readonly<Record<string, unknown>>): Promise<void>;
38
43
  /** Move a company under a new parent (hierarchy rules + cycle check, see the hierarchy service). */
@@ -1 +1 @@
1
- {"version":3,"file":"company.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAa,MAAM,gCAAgC,CAAC;AAUpF;;;;;;;GAOG;AACH,qBACa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,uBAAuB;IAGrD;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAetE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,2EAA2E;IAC3E,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU;IAOjC,6BAA6B;IAC7B,UAAU,CAAC,IAAI,EAAE,MAAM;IAIvB;;;;;;;OAOG;IACG,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC;IAiBhB,oGAAoG;IACpG,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKnE"}
1
+ {"version":3,"file":"company.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAa,MAAM,gCAAgC,CAAC;AAUpF;;;;;;;GAOG;AACH,qBACa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,uBAAuB;IAGrD;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAetE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,2EAA2E;IAC3E,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU;IAOjC,6BAA6B;IAC7B,UAAU,CAAC,IAAI,EAAE,MAAM;IAIvB;;;;;;;;;;;;OAYG;IACG,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC;IAsBhB,oGAAoG;IACpG,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKnE"}
@@ -74,6 +74,11 @@ let CompanyService = CompanyService_1 = class CompanyService {
74
74
  * rooted at (the same own-root guard as move and delete, F6): deactivating it would act on the
75
75
  * whole tenant above the actor's authority. Re-sending the current status (the company form
76
76
  * always sends both fields) stays allowed, so the own root can still be renamed.
77
+ *
78
+ * Phase 11 #1: the guard compares the STORED uuid, never the caller's spelling. `c_uuid` is
79
+ * `utf8mb4_unicode_ci`, which also ignores trailing spaces and character width, so a padded or
80
+ * fullwidth route uuid reaches the same row. A non-root status change therefore reads the company
81
+ * first (the walled read; out of reach → 404) and judges and writes by the id that read returns.
77
82
  */
78
83
  async updateCompany(actor, uuid, patch) {
79
84
  const fields = {};
@@ -81,15 +86,18 @@ let CompanyService = CompanyService_1 = class CompanyService {
81
86
  if (typeof patch?.[key] === 'string')
82
87
  fields[key] = patch[key];
83
88
  }
84
- if (actor.isRoot !== true && fields.status !== undefined && (0, http_js_1.sameUuid)(uuid, (0, company_hierarchy_service_js_1.ownRootOf)(actor))) {
89
+ let target = uuid;
90
+ if (actor.isRoot !== true && fields.status !== undefined) {
85
91
  const current = await this.records.get('company', uuid);
86
92
  if (current === null)
87
93
  throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
88
- if (current.fields.status !== fields.status) {
94
+ target = current.id;
95
+ if ((0, http_js_1.sameUuid)(String(current.id), (0, company_hierarchy_service_js_1.ownRootOf)(actor)) &&
96
+ current.fields.status !== fields.status) {
89
97
  throw new identity_errors_js_1.IdentityForbiddenError('You cannot change the status of the organisation your access is rooted at.');
90
98
  }
91
99
  }
92
- return this.records.update('company', uuid, { fields });
100
+ return this.records.update('company', target, { fields });
93
101
  }
94
102
  /** Move a company under a new parent (hierarchy rules + cycle check, see the hierarchy service). */
95
103
  moveCompany(actor, uuid, parentUuid) {
@@ -1 +1 @@
1
- {"version":3,"file":"cross-tenant-grant.controller.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/cross-tenant-grant.controller.ts"],"names":[],"mappings":"AA8CA,OAAO,EAGL,sBAAsB,EAKvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,gGAAgG;AAChG,UAAU,iBAAiB;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAqDD,qBAEa,0BAA0B;IAEO,OAAO,CAAC,QAAQ,CAAC,WAAW;IACtE,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADqC,WAAW,EAAE,sBAAsB,EAC7E,KAAK,EAAE,2BAA2B;IAGrD;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,iBAAiB;;;;IA+BtE,yFAAyF;IAGnF,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IASnE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IASnE;;;OAGG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa;;;IAKrC;;;;OAIG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa;;;;IAQpC,wFAAwF;IAGlF,cAAc;;;CAIrB"}
1
+ {"version":3,"file":"cross-tenant-grant.controller.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/cross-tenant-grant.controller.ts"],"names":[],"mappings":"AA8CA,OAAO,EAGL,sBAAsB,EAOvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,gGAAgG;AAChG,UAAU,iBAAiB;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAgDD,qBAEa,0BAA0B;IAEO,OAAO,CAAC,QAAQ,CAAC,WAAW;IACtE,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADqC,WAAW,EAAE,sBAAsB,EAC7E,KAAK,EAAE,2BAA2B;IAGrD;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,iBAAiB;;;;IA+BtE,yFAAyF;IAGnF,MAAM,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;IAS7E;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;IAS7E;;;OAGG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa;;;IAKrC;;;;OAIG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa;;;;IAQpC,wFAAwF;IAGlF,cAAc;;;CAIrB"}
@@ -52,18 +52,13 @@ const cross_tenant_grant_read_service_js_1 = require("./cross-tenant-grant-read.
52
52
  const VALID_GRANTEE_KINDS = ['user', 'role'];
53
53
  /** Validate + narrow the offer body, throwing 400 on any missing/malformed field. */
54
54
  function parseOfferBody(body) {
55
- const granteeTenantId = body?.granteeTenantId;
56
- if (typeof granteeTenantId !== 'string' || granteeTenantId.length === 0) {
57
- throw new common_1.BadRequestException('granteeTenantId is required');
58
- }
55
+ // Phase 11 #1: ids are canonical uuids (400 otherwise) before they reach the writer.
56
+ const granteeTenantId = (0, guard_1.uuidField)(body?.granteeTenantId, 'granteeTenantId');
59
57
  const granteeKind = body?.granteeKind;
60
58
  if (!VALID_GRANTEE_KINDS.includes(granteeKind)) {
61
59
  throw new common_1.BadRequestException("granteeKind must be 'user' or 'role'");
62
60
  }
63
- const granteeUuid = body?.granteeUuid;
64
- if (typeof granteeUuid !== 'string' || granteeUuid.length === 0) {
65
- throw new common_1.BadRequestException('granteeUuid is required');
66
- }
61
+ const granteeUuid = (0, guard_1.uuidField)(body?.granteeUuid, 'granteeUuid');
67
62
  const typeKey = body?.typeKey;
68
63
  if (typeof typeKey !== 'string' || typeKey.length === 0) {
69
64
  throw new common_1.BadRequestException('typeKey is required');
@@ -193,7 +188,7 @@ __decorate([
193
188
  (0, common_1.Post)('cross-tenant-grants/:uuid/accept'),
194
189
  (0, guard_1.Classified)({ typeKey: 'cross_tenant_grant', action: 'accept' }),
195
190
  __param(0, (0, common_1.Req)()),
196
- __param(1, (0, common_1.Param)('uuid')),
191
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
197
192
  __metadata("design:type", Function),
198
193
  __metadata("design:paramtypes", [Object, String]),
199
194
  __metadata("design:returntype", Promise)
@@ -202,7 +197,7 @@ __decorate([
202
197
  (0, common_1.Post)('cross-tenant-grants/:uuid/revoke'),
203
198
  (0, guard_1.Classified)({ typeKey: 'cross_tenant_grant', action: 'revoke' }),
204
199
  __param(0, (0, common_1.Req)()),
205
- __param(1, (0, common_1.Param)('uuid')),
200
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
206
201
  __metadata("design:type", Function),
207
202
  __metadata("design:paramtypes", [Object, String]),
208
203
  __metadata("design:returntype", Promise)
@@ -1 +1 @@
1
- {"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAYL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AA6FxB;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAUnF;AAiBD;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,GAClB,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;KACtC,GAAG,aAAa;CA8dvB"}
1
+ {"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAYL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AA+FxB;;;;;;GAMG;AACH,wBAAgB,0CAA0C,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAUnF;AAiBD;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,GAClB,GAAE;QACD,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC5B,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;KACtC,GAAG,aAAa;CAoevB"}
@@ -21,6 +21,7 @@ const company_hierarchy_service_js_1 = require("./company/company-hierarchy.serv
21
21
  const role_service_js_1 = require("./role/role.service.js");
22
22
  const role_controller_js_1 = require("./role/role.controller.js");
23
23
  const user_service_js_1 = require("./user/user.service.js");
24
+ const email_address_changer_token_js_1 = require("./user/email-address-changer.token.js");
24
25
  const user_controller_js_1 = require("./user/user.controller.js");
25
26
  const membership_controller_js_1 = require("./membership/membership.controller.js");
26
27
  const membership_service_js_1 = require("./membership/membership.service.js");
@@ -475,6 +476,12 @@ let IdentityFeatureModule = IdentityFeatureModule_1 = class IdentityFeatureModul
475
476
  org_catalog_service_js_1.OrgCatalogService,
476
477
  record_resolver_js_1.RecordResolverService,
477
478
  { provide: posting_version_bump_js_1.POSTING_VERSION_BUMP, useValue: postingVersionBump },
479
+ // Phase 12 (KI #14): the admin email change goes through guard's ONE email-change helper
480
+ // (collision refusal + login-hint follow in one transaction), over the shared authz pool.
481
+ {
482
+ provide: email_address_changer_token_js_1.IDENTITY_EMAIL_ADDRESS_CHANGER,
483
+ useValue: new guard_1.MariaDbEmailAddressChanger(authzPool),
484
+ },
478
485
  // ADR-0035 (GAPS #6): attach the guard's Redis permission-version cache to the ONE shared
479
486
  // bumper, so every committed bump (postings, grants, hierarchy) is published to Redis,
480
487
  // where gate 5 reads it. Provided by the auth surface under `auth: true` (through the
@@ -1 +1 @@
1
- {"version":3,"file":"membership.controller.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership.controller.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG1E,iFAAiF;AACjF,UAAU,iBAAiB;IACzB,uFAAuF;IACvF,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,4EAA4E;AAC5E,UAAU,cAAc;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kFAAkF;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAYD,qBAEa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAFR,WAAW,EAAE,iBAAiB,EAC9B,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,qBAAqB;IAGlD;;;;;OAKG;IAGG,mBAAmB,CAAoB,QAAQ,EAAE,MAAM;;;IA0C7D,uEAAuE;IAGjE,iBAAiB,CACd,GAAG,EAAE,aAAa,EACN,QAAQ,EAAE,MAAM,EAC3B,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAUxC;;;;OAIG;IAGG,gBAAgB,CACb,GAAG,EAAE,aAAa,EACA,cAAc,EAAE,MAAM,EACvC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAgB1E,2FAA2F;IAGrF,gBAAgB,CACb,GAAG,EAAE,aAAa,EACA,cAAc,EAAE,MAAM;;;IAMjD,0EAA0E;IAGpE,eAAe,CACZ,GAAG,EAAE,aAAa,EACA,cAAc,EAAE,MAAM,EACvC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAA;KAAE;;;IAO7C;;;;OAIG;YACW,WAAW;CAM1B"}
1
+ {"version":3,"file":"membership.controller.d.ts","sourceRoot":"","sources":["../../../src/features/membership/membership.controller.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG1E,iFAAiF;AACjF,UAAU,iBAAiB;IACzB,uFAAuF;IACvF,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,4EAA4E;AAC5E,UAAU,cAAc;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kFAAkF;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAYD,qBAEa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAFR,WAAW,EAAE,iBAAiB,EAC9B,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,qBAAqB;IAGlD;;;;;OAKG;IAGG,mBAAmB,CAA8B,QAAQ,EAAE,MAAM;;;IA0CvE,uEAAuE;IAGjE,iBAAiB,CACd,GAAG,EAAE,aAAa,EACI,QAAQ,EAAE,MAAM,EACrC,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAUxC;;;;OAIG;IAGG,gBAAgB,CACb,GAAG,EAAE,aAAa,EACU,cAAc,EAAE,MAAM,EACjD,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAoB1E,2FAA2F;IAGrF,gBAAgB,CACb,GAAG,EAAE,aAAa,EACU,cAAc,EAAE,MAAM;;;IAM3D,0EAA0E;IAGpE,eAAe,CACZ,GAAG,EAAE,aAAa,EACU,cAAc,EAAE,MAAM,EACjD,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAA;KAAE;;;IA4B7C;;;;OAIG;YACW,WAAW;CAM1B"}
@@ -83,7 +83,7 @@ let MembershipController = class MembershipController {
83
83
  }
84
84
  /** Add the user to a company (creates an inactive membership card). */
85
85
  async addUserMembership(req, userUuid, body) {
86
- const id = await this.memberships.addMembership(req.principal, userUuid, body?.companyUuid ?? '');
86
+ const id = await this.memberships.addMembership(req.principal, userUuid, (0, guard_1.uuidField)(body?.companyUuid, 'companyUuid'));
87
87
  return { id };
88
88
  }
89
89
  /**
@@ -96,7 +96,7 @@ let MembershipController = class MembershipController {
96
96
  if (!body.userUuid) {
97
97
  throw new common_1.ForbiddenException('userUuid is required to activate a membership');
98
98
  }
99
- await this.memberships.setActive(req.principal, membershipUuid, body.userUuid);
99
+ await this.memberships.setActive(req.principal, membershipUuid, (0, guard_1.uuidField)(body.userUuid, 'userUuid'));
100
100
  }
101
101
  if (typeof body?.status === 'string') {
102
102
  await this.memberships.updateMembership(req.principal, membershipUuid, {
@@ -112,7 +112,24 @@ let MembershipController = class MembershipController {
112
112
  }
113
113
  /** Atomically replace a membership's full posting set (the diff sync). */
114
114
  async replacePostings(req, membershipUuid, body) {
115
- const postings = Array.isArray(body?.postings) ? body.postings : [];
115
+ const raw = Array.isArray(body?.postings) ? body.postings : [];
116
+ // Phase 11 #1: every posting id is a canonical uuid (400 otherwise); the window bounds and any
117
+ // other key pass through to the service's own whitelist.
118
+ const postings = raw.map((p, i) => {
119
+ const entry = (p ?? {});
120
+ const departmentUuid = (0, guard_1.optionalUuidField)(entry.departmentUuid, `postings[${i}].departmentUuid`, {
121
+ keepNull: true,
122
+ });
123
+ const branchUuid = (0, guard_1.optionalUuidField)(entry.branchUuid, `postings[${i}].branchUuid`, {
124
+ keepNull: true,
125
+ });
126
+ return {
127
+ ...entry,
128
+ roleUuid: (0, guard_1.uuidField)(entry.roleUuid, `postings[${i}].roleUuid`),
129
+ ...(departmentUuid !== undefined ? { departmentUuid } : {}),
130
+ ...(branchUuid !== undefined ? { branchUuid } : {}),
131
+ };
132
+ });
116
133
  await this.postings.replacePostings(req.principal, membershipUuid, postings);
117
134
  return { success: true };
118
135
  }
@@ -133,7 +150,7 @@ exports.MembershipController = MembershipController;
133
150
  __decorate([
134
151
  (0, common_1.Get)('users/:userUuid/memberships'),
135
152
  (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'read' }),
136
- __param(0, (0, common_1.Param)('userUuid')),
153
+ __param(0, (0, common_1.Param)('userUuid', guard_1.UuidPipe)),
137
154
  __metadata("design:type", Function),
138
155
  __metadata("design:paramtypes", [String]),
139
156
  __metadata("design:returntype", Promise)
@@ -142,7 +159,7 @@ __decorate([
142
159
  (0, common_1.Post)('users/:userUuid/memberships'),
143
160
  (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'create' }),
144
161
  __param(0, (0, common_1.Req)()),
145
- __param(1, (0, common_1.Param)('userUuid')),
162
+ __param(1, (0, common_1.Param)('userUuid', guard_1.UuidPipe)),
146
163
  __param(2, (0, common_1.Body)()),
147
164
  __metadata("design:type", Function),
148
165
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -152,7 +169,7 @@ __decorate([
152
169
  (0, common_1.Put)('memberships/:membershipUuid'),
153
170
  (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'update' }),
154
171
  __param(0, (0, common_1.Req)()),
155
- __param(1, (0, common_1.Param)('membershipUuid')),
172
+ __param(1, (0, common_1.Param)('membershipUuid', guard_1.UuidPipe)),
156
173
  __param(2, (0, common_1.Body)()),
157
174
  __metadata("design:type", Function),
158
175
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -162,7 +179,7 @@ __decorate([
162
179
  (0, common_1.Delete)('memberships/:membershipUuid'),
163
180
  (0, guard_1.Classified)({ typeKey: 'company_membership', action: 'delete' }),
164
181
  __param(0, (0, common_1.Req)()),
165
- __param(1, (0, common_1.Param)('membershipUuid')),
182
+ __param(1, (0, common_1.Param)('membershipUuid', guard_1.UuidPipe)),
166
183
  __metadata("design:type", Function),
167
184
  __metadata("design:paramtypes", [Object, String]),
168
185
  __metadata("design:returntype", Promise)
@@ -171,7 +188,7 @@ __decorate([
171
188
  (0, common_1.Put)('memberships/:membershipUuid/postings'),
172
189
  (0, guard_1.Classified)({ typeKey: 'membership_posting', action: 'update' }),
173
190
  __param(0, (0, common_1.Req)()),
174
- __param(1, (0, common_1.Param)('membershipUuid')),
191
+ __param(1, (0, common_1.Param)('membershipUuid', guard_1.UuidPipe)),
175
192
  __param(2, (0, common_1.Body)()),
176
193
  __metadata("design:type", Function),
177
194
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -1 +1 @@
1
- {"version":3,"file":"org-catalog.controller.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAU7D,qBAEa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iBAAiB;IAEvD,kFAAkF;IAG5E,sBAAsB,CAAuB,WAAW,EAAE,MAAM;;;IAKtE,+EAA+E;IAGzE,mBAAmB,CAAuB,WAAW,EAAE,MAAM;;;CAIpE"}
1
+ {"version":3,"file":"org-catalog.controller.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAU7D,qBAEa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iBAAiB;IAEvD,kFAAkF;IAG5E,sBAAsB,CAAiC,WAAW,EAAE,MAAM;;;IAKhF,+EAA+E;IAGzE,mBAAmB,CAAiC,WAAW,EAAE,MAAM;;;CAI9E"}
@@ -44,7 +44,7 @@ exports.OrgCatalogController = OrgCatalogController;
44
44
  __decorate([
45
45
  (0, common_1.Get)('companies/:companyUuid/departments'),
46
46
  (0, guard_1.Classified)({ typeKey: 'company_department', action: 'read' }),
47
- __param(0, (0, common_1.Param)('companyUuid')),
47
+ __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
48
48
  __metadata("design:type", Function),
49
49
  __metadata("design:paramtypes", [String]),
50
50
  __metadata("design:returntype", Promise)
@@ -52,7 +52,7 @@ __decorate([
52
52
  __decorate([
53
53
  (0, common_1.Get)('companies/:companyUuid/branches'),
54
54
  (0, guard_1.Classified)({ typeKey: 'company_branch', action: 'read' }),
55
- __param(0, (0, common_1.Param)('companyUuid')),
55
+ __param(0, (0, common_1.Param)('companyUuid', guard_1.UuidPipe)),
56
56
  __metadata("design:type", Function),
57
57
  __metadata("design:paramtypes", [String]),
58
58
  __metadata("design:returntype", Promise)
@@ -1 +1 @@
1
- {"version":3,"file":"policy.controller.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.controller.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,aAAa,EAId,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,qBAEa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,aAAa;IAEpD;;;;;;;;OAQG;IAGG,MAAM,CAAmB,OAAO,CAAC,EAAE,MAAM;IAY/C;;;OAGG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAoB,OAAO,CAAC,EAAE,MAAM;;;IAQxE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,YAAY;;;IAWlE;;;;OAIG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM;;;CAShE"}
1
+ {"version":3,"file":"policy.controller.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.controller.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,aAAa,EAId,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,qBAEa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,aAAa;IAEpD;;;;;;;;OAQG;IAGG,MAAM,CAAmB,OAAO,CAAC,EAAE,MAAM;IAY/C;;;OAGG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAoB,OAAO,CAAC,EAAE,MAAM;;;IAQxE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,YAAY;;;IAWlE;;;;OAIG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAyB,EAAE,EAAE,MAAM;;;CAS1E"}
@@ -140,7 +140,7 @@ __decorate([
140
140
  (0, common_1.Delete)('record-policies/:id'),
141
141
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
142
142
  __param(0, (0, common_1.Req)()),
143
- __param(1, (0, common_1.Param)('id')),
143
+ __param(1, (0, common_1.Param)('id', guard_1.UuidPipe)),
144
144
  __metadata("design:type", Function),
145
145
  __metadata("design:paramtypes", [Object, String]),
146
146
  __metadata("design:returntype", Promise)
@@ -1 +1 @@
1
- {"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAS,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKhD;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMjC,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAIxE"}
1
+ {"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAS,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKhD;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMjC,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
@@ -78,7 +78,7 @@ __decorate([
78
78
  __decorate([
79
79
  (0, common_1.Get)('roles/:uuid'),
80
80
  (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
81
- __param(0, (0, common_1.Param)('uuid')),
81
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
82
82
  __metadata("design:type", Function),
83
83
  __metadata("design:paramtypes", [String]),
84
84
  __metadata("design:returntype", void 0)
@@ -87,7 +87,7 @@ __decorate([
87
87
  (0, common_1.Put)('roles/:uuid'),
88
88
  (0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
89
89
  __param(0, (0, common_1.Req)()),
90
- __param(1, (0, common_1.Param)('uuid')),
90
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
91
91
  __param(2, (0, common_1.Body)()),
92
92
  __metadata("design:type", Function),
93
93
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -97,7 +97,7 @@ __decorate([
97
97
  (0, common_1.Delete)('roles/:uuid'),
98
98
  (0, guard_1.Classified)({ typeKey: 'role', action: 'delete' }),
99
99
  __param(0, (0, common_1.Req)()),
100
- __param(1, (0, common_1.Param)('uuid')),
100
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
101
101
  __metadata("design:type", Function),
102
102
  __metadata("design:paramtypes", [Object, String]),
103
103
  __metadata("design:returntype", Promise)
@@ -1 +1 @@
1
- {"version":3,"file":"service-account.controller.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.controller.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,qBAAqB,EAOtB,MAAM,8BAA8B,CAAC;AAGtC,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAwDD,qBAEa,wBAAwB;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,qBAAqB;IAEvD,8FAA8F;IAGxF,IAAI,CAAQ,GAAG,EAAE,aAAa;;;IAQpC;;;;;OAKG;IAGG,UAAU,CAAQ,GAAG,EAAE,aAAa;IAQ1C,8FAA8F;IAGxF,GAAG,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM;;;IAQ5D;;;OAGG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAiB7D;;;OAGG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO;;;IAuBtF;;;;OAIG;IAGG,QAAQ,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO;;;IAUxF;;;OAGG;IAGG,SAAS,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO;;;IAUzF;;;OAGG;IAGG,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM;;;CAQpE"}
1
+ {"version":3,"file":"service-account.controller.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.controller.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,qBAAqB,EAOtB,MAAM,8BAA8B,CAAC;AAGtC,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAmDD,qBAEa,wBAAwB;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,qBAAqB;IAEvD,8FAA8F;IAGxF,IAAI,CAAQ,GAAG,EAAE,aAAa;;;IAQpC;;;;;OAKG;IAGG,UAAU,CAAQ,GAAG,EAAE,aAAa;IAQ1C,8FAA8F;IAGxF,GAAG,CAAQ,GAAG,EAAE,aAAa,EAAyB,EAAE,EAAE,MAAM;;;IAQtE;;;OAGG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAiB7D;;;OAGG;IAGG,MAAM,CACH,GAAG,EAAE,aAAa,EACF,EAAE,EAAE,MAAM,EACzB,IAAI,EAAE,OAAO;;;IAwBvB;;;;OAIG;IAGG,QAAQ,CACL,GAAG,EAAE,aAAa,EACF,EAAE,EAAE,MAAM,EACzB,IAAI,EAAE,OAAO;;;IAWvB;;;OAGG;IAGG,SAAS,CACN,GAAG,EAAE,aAAa,EACF,EAAE,EAAE,MAAM,EACzB,IAAI,EAAE,OAAO;;;IAWvB;;;OAGG;IAGG,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAyB,EAAE,EAAE,MAAM;;;CAQ9E"}
@@ -54,15 +54,9 @@ function mapError(e) {
54
54
  throw new common_1.ConflictException(e.message);
55
55
  throw e;
56
56
  }
57
- /** Narrow the OPTIONAL string[] fields of a body (scope/roles) — reject a non-string-array. */
58
- function optStringArray(v, field) {
59
- if (v === undefined)
60
- return undefined;
61
- if (!Array.isArray(v) || v.some((x) => typeof x !== 'string')) {
62
- throw new common_1.BadRequestException(`${field} must be an array of strings`);
63
- }
64
- return v;
65
- }
57
+ // The OPTIONAL id-array fields of a body (scope/roles) go through `uuidArrayField`: each item a
58
+ // canonical uuid (Phase 11 #1), 400 otherwise. The service re-checks (service-level callers bypass
59
+ // this controller).
66
60
  /**
67
61
  * Narrow a key-issue/rotate body to its expiry choice (ADR-0033 §4): `expiresAtMs` (epoch-ms) or
68
62
  * `neverExpires: true`. Shape errors are 400 here; "neither", "both" and "not in the future" are
@@ -130,8 +124,8 @@ let ServiceAccountController = class ServiceAccountController {
130
124
  const b = (body ?? {});
131
125
  if (typeof b.label !== 'string')
132
126
  throw new common_1.BadRequestException('label is required');
133
- const scope = optStringArray(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
134
- const roles = optStringArray(b.roleIds, 'roleIds');
127
+ const scope = (0, guard_1.uuidArrayField)(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
128
+ const roles = (0, guard_1.uuidArrayField)(b.roleIds, 'roleIds');
135
129
  try {
136
130
  const account = await this.svc.create(adminOf(req), {
137
131
  label: b.label,
@@ -156,8 +150,8 @@ let ServiceAccountController = class ServiceAccountController {
156
150
  if (b.status !== undefined && b.status !== 'active' && b.status !== 'disabled') {
157
151
  throw new common_1.BadRequestException("status must be 'active' or 'disabled'");
158
152
  }
159
- const scope = optStringArray(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
160
- const roles = optStringArray(b.roleIds, 'roleIds');
153
+ const scope = (0, guard_1.uuidArrayField)(b.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds');
154
+ const roles = (0, guard_1.uuidArrayField)(b.roleIds, 'roleIds');
161
155
  try {
162
156
  const account = await this.svc.update(adminOf(req), id, {
163
157
  ...(b.label !== undefined ? { label: b.label } : {}),
@@ -235,7 +229,7 @@ __decorate([
235
229
  (0, common_1.Get)('service-accounts/:id'),
236
230
  (0, guard_1.Classified)({ typeKey: 'service_account', action: 'read' }),
237
231
  __param(0, (0, common_1.Req)()),
238
- __param(1, (0, common_1.Param)('id')),
232
+ __param(1, (0, common_1.Param)('id', guard_1.UuidPipe)),
239
233
  __metadata("design:type", Function),
240
234
  __metadata("design:paramtypes", [Object, String]),
241
235
  __metadata("design:returntype", Promise)
@@ -253,7 +247,7 @@ __decorate([
253
247
  (0, common_1.Patch)('service-accounts/:id'),
254
248
  (0, guard_1.Classified)({ typeKey: 'service_account', action: 'update' }),
255
249
  __param(0, (0, common_1.Req)()),
256
- __param(1, (0, common_1.Param)('id')),
250
+ __param(1, (0, common_1.Param)('id', guard_1.UuidPipe)),
257
251
  __param(2, (0, common_1.Body)()),
258
252
  __metadata("design:type", Function),
259
253
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -263,7 +257,7 @@ __decorate([
263
257
  (0, common_1.Post)('service-accounts/:id/keys'),
264
258
  (0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
265
259
  __param(0, (0, common_1.Req)()),
266
- __param(1, (0, common_1.Param)('id')),
260
+ __param(1, (0, common_1.Param)('id', guard_1.UuidPipe)),
267
261
  __param(2, (0, common_1.Body)()),
268
262
  __metadata("design:type", Function),
269
263
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -273,7 +267,7 @@ __decorate([
273
267
  (0, common_1.Post)('service-accounts/:id/keys/rotate'),
274
268
  (0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
275
269
  __param(0, (0, common_1.Req)()),
276
- __param(1, (0, common_1.Param)('id')),
270
+ __param(1, (0, common_1.Param)('id', guard_1.UuidPipe)),
277
271
  __param(2, (0, common_1.Body)()),
278
272
  __metadata("design:type", Function),
279
273
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -283,7 +277,7 @@ __decorate([
283
277
  (0, common_1.Post)('service-accounts/:id/keys/revoke'),
284
278
  (0, guard_1.Classified)({ typeKey: 'service_account', action: 'manage_keys' }),
285
279
  __param(0, (0, common_1.Req)()),
286
- __param(1, (0, common_1.Param)('id')),
280
+ __param(1, (0, common_1.Param)('id', guard_1.UuidPipe)),
287
281
  __metadata("design:type", Function),
288
282
  __metadata("design:paramtypes", [Object, String]),
289
283
  __metadata("design:returntype", Promise)
@@ -1 +1 @@
1
- {"version":3,"file":"service-account.service.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service-account.service.d.ts","sourceRoot":"","sources":["../../../src/features/service-account/service-account.service.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,7 @@ exports.ceilingAllowsRole = ceilingAllowsRole;
23
23
  */
24
24
  const node_crypto_1 = require("node:crypto");
25
25
  const core_1 = require("@astram/core");
26
+ const engine_1 = require("@astram/engine");
26
27
  /** The account id was not in the caller's subtree (unknown or cross-tenant) → 404 (no leak). */
27
28
  class ServiceAccountNotFoundError extends Error {
28
29
  constructor() {
@@ -65,23 +66,39 @@ function scopeOf(admin) {
65
66
  const nodeIds = admin.scopeSubtreeNodeIds.length ? admin.scopeSubtreeNodeIds : [admin.tenantId];
66
67
  return { nodeIds, isRoot: admin.isRoot === true };
67
68
  }
69
+ /** An id in the ceiling's comparison form: canonical when it is a uuid (Phase 11 #1). */
70
+ const ceilingKey = (id) => (0, engine_1.canonicalUuid)(id) ?? id;
68
71
  function ceilingOf(admin, heldRoleIds) {
69
72
  return {
70
73
  isRoot: admin.isRoot === true,
71
- nodeIds: new Set(scopeOf(admin).nodeIds),
72
- roleIds: new Set(heldRoleIds),
74
+ nodeIds: new Set(scopeOf(admin).nodeIds.map(ceilingKey)),
75
+ roleIds: new Set(heldRoleIds.map(ceilingKey)),
73
76
  };
74
77
  }
75
78
  function ceilingAllowsNode(c, nodeId) {
76
- return c.isRoot || c.nodeIds.has(nodeId);
79
+ return c.isRoot || c.nodeIds.has(ceilingKey(nodeId));
77
80
  }
78
81
  function ceilingAllowsRole(c, roleId) {
79
- return c.isRoot || c.roleIds.has(roleId);
82
+ return c.isRoot || c.roleIds.has(ceilingKey(roleId));
80
83
  }
81
84
  /** Dedupe an id list keeping first-seen order (drops empty strings). */
82
85
  function uniqueIds(ids) {
83
86
  return [...new Set(ids.filter((x) => typeof x === 'string' && x.length > 0))];
84
87
  }
88
+ /**
89
+ * REQUESTED ids (create/update input) → canonical uuids, deduped (Phase 11 #1). The id columns
90
+ * compare under `utf8mb4_unicode_ci` (case, trailing spaces and width ignored) while the ceiling
91
+ * Sets, `sa_role_ids` / `sa_scope_subtree` JSON and every `JSON_CONTAINS` are exact, so a padded or
92
+ * fullwidth id would be judged one way and stored another. Anything but a strict uuid → 400. This
93
+ * holds for service-level callers too (the controller's `uuidArrayField` is the HTTP edge).
94
+ */
95
+ function requestIds(ids, field) {
96
+ const canonical = ids.map((x) => (0, engine_1.canonicalUuid)(x));
97
+ if (canonical.some((x) => x === null)) {
98
+ throw new ServiceAccountInputError(`${field} must be a list of ids`);
99
+ }
100
+ return uniqueIds(canonical);
101
+ }
85
102
  /** `requested` then the `preserved` ids not already in it (first-seen order, no duplicates). */
86
103
  function mergeIds(requested, preserved) {
87
104
  return uniqueIds([...requested, ...preserved]);
@@ -313,13 +330,13 @@ class ServiceAccountService {
313
330
  if (!label)
314
331
  throw new ServiceAccountInputError('a label is required');
315
332
  // Default the scope to the admin's own subtree (least surprise; still least-privilege-bounded).
316
- const requestedScope = uniqueIds(input.scopeSubtreeNodeIds ?? []);
333
+ const requestedScope = requestIds(input.scopeSubtreeNodeIds ?? [], 'scopeSubtreeNodeIds');
317
334
  const scopeSubtree = requestedScope.length > 0
318
335
  ? requestedScope
319
336
  : admin.scopeSubtreeNodeIds.length
320
337
  ? uniqueIds(admin.scopeSubtreeNodeIds)
321
338
  : [admin.tenantId];
322
- const roleIds = uniqueIds(input.roleIds ?? []);
339
+ const roleIds = requestIds(input.roleIds ?? [], 'roleIds');
323
340
  await this.assertWithinCeiling(await this.ceilingFor(admin), scopeSubtree, roleIds);
324
341
  const id = (0, node_crypto_1.randomUUID)();
325
342
  const createdAtMs = this.now();
@@ -371,8 +388,10 @@ class ServiceAccountService {
371
388
  if (patch.status !== undefined && patch.status !== 'active' && patch.status !== 'disabled') {
372
389
  throw new ServiceAccountInputError("status must be 'active' or 'disabled'");
373
390
  }
374
- const requestedScope = patch.scopeSubtreeNodeIds !== undefined ? uniqueIds(patch.scopeSubtreeNodeIds) : undefined;
375
- const requestedRoles = patch.roleIds !== undefined ? uniqueIds(patch.roleIds) : undefined;
391
+ const requestedScope = patch.scopeSubtreeNodeIds !== undefined
392
+ ? requestIds(patch.scopeSubtreeNodeIds, 'scopeSubtreeNodeIds')
393
+ : undefined;
394
+ const requestedRoles = patch.roleIds !== undefined ? requestIds(patch.roleIds, 'roleIds') : undefined;
376
395
  if (requestedScope !== undefined && requestedScope.length === 0) {
377
396
  throw new ServiceAccountInputError('choose at least one scope node');
378
397
  }
@@ -1 +1 @@
1
- {"version":3,"file":"share.controller.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.controller.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAErE,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAwBD,qBAEa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAEjD;;;;OAIG;IAGG,IAAI,CACD,GAAG,EAAE,aAAa,EACP,OAAO,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;;;IAYtC;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAW5D;;;OAGG;IAGG,OAAO,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;CAU/D"}
1
+ {"version":3,"file":"share.controller.d.ts","sourceRoot":"","sources":["../../../src/features/share/share.controller.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AAErE,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAuBD,qBAEa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAEjD;;;;OAIG;IAGG,IAAI,CACD,GAAG,EAAE,aAAa,EACP,OAAO,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;;;IAatC;;;;OAIG;IAGG,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;IAW5D;;;OAGG;IAGG,OAAO,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,OAAO;;;CAU/D"}
@@ -35,20 +35,19 @@ function toShareRequest(body) {
35
35
  const b = (body ?? {});
36
36
  if (!b.typeKey || typeof b.typeKey !== 'string')
37
37
  throw new common_1.BadRequestException('typeKey required');
38
- if (!b.recordId || typeof b.recordId !== 'string')
39
- throw new common_1.BadRequestException('recordId required');
38
+ // Phase 11 #1: `rs_record_uuid` / `rs_grantee_uuid` are uuid columns — canonical or 400.
39
+ const recordId = (0, guard_1.uuidField)(b.recordId, 'recordId');
40
40
  if (b.granteeKind !== 'user' && b.granteeKind !== 'role') {
41
41
  throw new common_1.BadRequestException("granteeKind must be 'user' or 'role'");
42
42
  }
43
- if (!b.granteeUuid || typeof b.granteeUuid !== 'string')
44
- throw new common_1.BadRequestException('granteeUuid required');
43
+ const granteeUuid = (0, guard_1.uuidField)(b.granteeUuid, 'granteeUuid');
45
44
  if (!b.action || typeof b.action !== 'string')
46
45
  throw new common_1.BadRequestException('action required');
47
46
  return {
48
47
  typeKey: b.typeKey,
49
- recordId: b.recordId,
48
+ recordId,
50
49
  granteeKind: b.granteeKind,
51
- granteeUuid: b.granteeUuid,
50
+ granteeUuid,
52
51
  action: b.action,
53
52
  };
54
53
  }
@@ -65,8 +64,9 @@ let ShareController = class ShareController {
65
64
  async list(req, typeKey, recordId) {
66
65
  if (!typeKey || !recordId)
67
66
  throw new common_1.BadRequestException('typeKey and recordId required');
67
+ const record = (0, guard_1.uuidField)(recordId, 'recordId'); // Phase 11 #1
68
68
  try {
69
- const shares = await this.shares.listShares(req.principal, typeKey, recordId);
69
+ const shares = await this.shares.listShares(req.principal, typeKey, record);
70
70
  return { shares };
71
71
  }
72
72
  catch (e) {
@@ -1,15 +1,21 @@
1
- import type { Principal } from '@astram/engine';
1
+ import { type Principal } from '@astram/engine';
2
2
  /** The authenticated request shape: the framework guard attaches `principal`. */
3
3
  export interface AuthedRequest {
4
4
  principal: Principal;
5
5
  }
6
6
  /**
7
- * Same uuid, compared the way MariaDB does: the uuid columns are `utf8mb4_unicode_ci`, so a route
8
- * param that differs only in case reaches the same row. Every JS-side id guard (own root, platform
9
- * administrators) must use this, never `===` (Phase 10 #1).
7
+ * Same id: identical strings, or the same uuid in canonical form (`canonicalUuid`, so letter case
8
+ * is ignored and anything padded or fullwidth is never "the same"). This is NOT how MariaDB
9
+ * compares: the uuid columns are `utf8mb4_unicode_ci`, which also ignores trailing spaces (PAD
10
+ * SPACE) and character width, so a caller's spelling can reach a row this function says differs
11
+ * (Phase 11 #1). Only compare ids that are already STORED or canonical (resolve the caller's id to
12
+ * the stored one first; route params and body fields are canonicalised by `UuidPipe`/`uuidField`).
10
13
  */
11
14
  export declare function sameUuid(a: string | null | undefined, b: string | null | undefined): boolean;
12
- /** Coerce an unknown field value to a string uuid or null. */
15
+ /**
16
+ * Coerce a STORED field value (a record view read back from the engine) to a string uuid or null.
17
+ * Not an input validator: request ids go through `UuidPipe` / `uuidField` (Phase 11 #1).
18
+ */
13
19
  export declare function asUuid(v: unknown): string | null;
14
20
  /**
15
21
  * Coerce a stored effective-window bound to an epoch-ms number, or null. The
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/features/shared/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5F;AAED,8DAA8D;AAC9D,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAQnD"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/features/shared/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE/D,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAK5F;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAQnD"}
@@ -3,15 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sameUuid = sameUuid;
4
4
  exports.asUuid = asUuid;
5
5
  exports.asEpochMs = asEpochMs;
6
+ const engine_1 = require("@astram/engine");
6
7
  /**
7
- * Same uuid, compared the way MariaDB does: the uuid columns are `utf8mb4_unicode_ci`, so a route
8
- * param that differs only in case reaches the same row. Every JS-side id guard (own root, platform
9
- * administrators) must use this, never `===` (Phase 10 #1).
8
+ * Same id: identical strings, or the same uuid in canonical form (`canonicalUuid`, so letter case
9
+ * is ignored and anything padded or fullwidth is never "the same"). This is NOT how MariaDB
10
+ * compares: the uuid columns are `utf8mb4_unicode_ci`, which also ignores trailing spaces (PAD
11
+ * SPACE) and character width, so a caller's spelling can reach a row this function says differs
12
+ * (Phase 11 #1). Only compare ids that are already STORED or canonical (resolve the caller's id to
13
+ * the stored one first; route params and body fields are canonicalised by `UuidPipe`/`uuidField`).
10
14
  */
11
15
  function sameUuid(a, b) {
12
- return typeof a === 'string' && typeof b === 'string' && a.toLowerCase() === b.toLowerCase();
16
+ if (typeof a !== 'string' || typeof b !== 'string')
17
+ return false;
18
+ if (a === b)
19
+ return true;
20
+ const canonical = (0, engine_1.canonicalUuid)(a);
21
+ return canonical !== null && canonical === (0, engine_1.canonicalUuid)(b);
13
22
  }
14
- /** Coerce an unknown field value to a string uuid or null. */
23
+ /**
24
+ * Coerce a STORED field value (a record view read back from the engine) to a string uuid or null.
25
+ * Not an input validator: request ids go through `UuidPipe` / `uuidField` (Phase 11 #1).
26
+ */
15
27
  function asUuid(v) {
16
28
  return typeof v === 'string' && v.length > 0 ? v : null;
17
29
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DI token for the administrator path's email changer (security hardening Phase 12, KI #14):
3
+ * guard's `MariaDbEmailAddressChanger`, built by `IdentityFeatureModule.forRoot()` over the ONE
4
+ * shared authz pool and injected into `UserService`. It is the same class guard's
5
+ * `POST /auth/confirm-email` uses, so the collision and login-hint rules never drift.
6
+ */
7
+ export declare const IDENTITY_EMAIL_ADDRESS_CHANGER: unique symbol;
8
+ //# sourceMappingURL=email-address-changer.token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-address-changer.token.d.ts","sourceRoot":"","sources":["../../../src/features/user/email-address-changer.token.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,eAA2C,CAAC"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IDENTITY_EMAIL_ADDRESS_CHANGER = void 0;
4
+ /**
5
+ * DI token for the administrator path's email changer (security hardening Phase 12, KI #14):
6
+ * guard's `MariaDbEmailAddressChanger`, built by `IdentityFeatureModule.forRoot()` over the ONE
7
+ * shared authz pool and injected into `UserService`. It is the same class guard's
8
+ * `POST /auth/confirm-email` uses, so the collision and login-hint rules never drift.
9
+ */
10
+ exports.IDENTITY_EMAIL_ADDRESS_CHANGER = Symbol('IDENTITY_EMAIL_ADDRESS_CHANGER');
@@ -1 +1 @@
1
- {"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EACjB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAW3D;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,iGAAiG;IAG3F,UAAU,CACP,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAUtE,0FAA0F;IAGpF,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAIxE"}
1
+ {"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C;;;OAGG;IAGG,UAAU,CACP,GAAG,EAAE,aAAa,EACjB,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAU3D;0CACsC;IAGtC,OAAO,CAA0B,IAAI,EAAE,MAAM;IAI7C,iGAAiG;IAG3F,UAAU,CACP,GAAG,EAAE,aAAa,EACA,IAAI,EAAE,MAAM,EAC7B,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAUtE,0FAA0F;IAGpF,UAAU,CAAQ,GAAG,EAAE,aAAa,EAA2B,IAAI,EAAE,MAAM;;;CAIlF"}
@@ -42,11 +42,10 @@ let UserController = class UserController {
42
42
  * first (active) membership in the same transaction; it must be a company the caller manages.
43
43
  */
44
44
  async createUser(req, body) {
45
+ const companyUuid = (0, guard_1.optionalUuidField)(body?.companyUuid, 'companyUuid');
45
46
  const id = await this.users.createUser(req.principal, {
46
47
  username: body?.username ?? 'Untitled',
47
- ...(typeof body?.companyUuid === 'string' && body.companyUuid.length > 0
48
- ? { companyUuid: body.companyUuid }
49
- : {}),
48
+ ...(companyUuid !== undefined ? { companyUuid } : {}),
50
49
  });
51
50
  return { id };
52
51
  }
@@ -91,7 +90,7 @@ __decorate([
91
90
  __decorate([
92
91
  (0, common_1.Get)('users/:uuid'),
93
92
  (0, guard_1.Classified)({ typeKey: 'user', action: 'read' }),
94
- __param(0, (0, common_1.Param)('uuid')),
93
+ __param(0, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
95
94
  __metadata("design:type", Function),
96
95
  __metadata("design:paramtypes", [String]),
97
96
  __metadata("design:returntype", void 0)
@@ -100,7 +99,7 @@ __decorate([
100
99
  (0, common_1.Put)('users/:uuid'),
101
100
  (0, guard_1.Classified)({ typeKey: 'user', action: 'update' }),
102
101
  __param(0, (0, common_1.Req)()),
103
- __param(1, (0, common_1.Param)('uuid')),
102
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
104
103
  __param(2, (0, common_1.Body)()),
105
104
  __metadata("design:type", Function),
106
105
  __metadata("design:paramtypes", [Object, String, Object]),
@@ -110,7 +109,7 @@ __decorate([
110
109
  (0, common_1.Delete)('users/:uuid'),
111
110
  (0, guard_1.Classified)({ typeKey: 'user', action: 'delete' }),
112
111
  __param(0, (0, common_1.Req)()),
113
- __param(1, (0, common_1.Param)('uuid')),
112
+ __param(1, (0, common_1.Param)('uuid', guard_1.UuidPipe)),
114
113
  __metadata("design:type", Function),
115
114
  __metadata("design:paramtypes", [Object, String]),
116
115
  __metadata("design:returntype", Promise)
@@ -1,6 +1,6 @@
1
1
  import { RecordsService, type RecordId } from '@astram/engine';
2
2
  import type { Principal } from '@astram/core';
3
- import { type EmailSender, type ResetTokenStore, type SessionSweeper } from '@astram/guard';
3
+ import { type EmailAddressChanger, type EmailSender, type ResetTokenStore, type SessionSweeper } from '@astram/guard';
4
4
  import { type PostingVersionBump } from '../membership/posting-version-bump.js';
5
5
  import { type ListParams } from '../shared/list-query.js';
6
6
  import { GrantCeiling } from '../authz/grant-ceiling.js';
@@ -23,6 +23,11 @@ export interface UserPatch {
23
23
  * - an email change burns the target's outstanding password-reset tokens BEFORE the change and
24
24
  * notifies the OLD address after it (best-effort), when guard's reset-token store and email
25
25
  * sender are wired (`ShieldModule.forRoot({ auth: true })`).
26
+ * - Phase 12 (KI #14): the email itself is written through guard's ONE email-change helper
27
+ * (`MariaDbEmailAddressChanger`, the same one `POST /auth/confirm-email` uses): canonical
28
+ * address (trim → NFKC → lower-case), collision refusal (409) and login-hint follow in ONE
29
+ * transaction, the user's pending self-service tokens burned in it, and an audit row in the
30
+ * engine's shape written on that transaction (fail closed). Not wired → the change is refused.
26
31
  */
27
32
  export declare class UserService {
28
33
  private readonly records;
@@ -31,8 +36,11 @@ export declare class UserService {
31
36
  private readonly emailSender?;
32
37
  private readonly sessions?;
33
38
  private readonly versionBump?;
39
+ private readonly emailAddresses?;
34
40
  private readonly logger;
35
- constructor(records: RecordsService, ceiling: GrantCeiling, resetTokens?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, sessions?: (SessionSweeper | null) | undefined, versionBump?: (PostingVersionBump | null) | undefined);
41
+ /** The engine's audit sink, used on the email helper's transaction (no pool of its own). */
42
+ private readonly emailAudit;
43
+ constructor(records: RecordsService, ceiling: GrantCeiling, resetTokens?: (ResetTokenStore | null) | undefined, emailSender?: (EmailSender | null) | undefined, sessions?: (SessionSweeper | null) | undefined, versionBump?: (PostingVersionBump | null) | undefined, emailAddresses?: (EmailAddressChanger | null) | undefined);
36
44
  /**
37
45
  * Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
38
46
  * ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
@@ -48,6 +56,14 @@ export declare class UserService {
48
56
  getUser(uuid: string): Promise<import("@astram/engine").RecordView | null>;
49
57
  /** Update a user (whitelisted patch; username/email/status changes need dominance — class doc). */
50
58
  updateUser(actor: Principal, uuid: string, patch: UserPatch): Promise<void>;
59
+ /**
60
+ * Phase 12 (KI #14): write a dominated user's new (canonical) email through guard's
61
+ * email-change helper — ONE transaction that locks the user and credential rows, refuses a
62
+ * collision with another user's email or login hint (→ 409), moves the login hint when it
63
+ * equalled the old email, burns the user's pending self-service tokens, and writes the audit row
64
+ * (engine shape, fail closed). Reset links are burned BEFORE (a failure aborts the change).
65
+ */
66
+ private changeEmail;
51
67
  /**
52
68
  * After a status change (security hardening Phase 3): bump every tenant the user is a member of
53
69
  * (the next request reloads its grants: none while disabled, back when re-enabled), and, when the
@@ -1 +1 @@
1
- {"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAIL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAMzD,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,qBACa,WAAW;IAIpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAGxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IACY,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAMtE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAK1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAnB/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAGpC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAGrB,WAAW,CAAC,GAAE,eAAe,GAAG,IAAI,aAAA,EACK,WAAW,CAAC,GAAE,WAAW,GAAG,IAAI,aAAA,EAMzE,QAAQ,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA,EAKhC,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA;IAG1D;;;;OAIG;IACG,UAAU,CACd,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAC5D,OAAO,CAAC,QAAQ,CAAC;IAcpB,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,mGAAmG;IAC7F,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCjF;;;;;;;;;OASG;YACW,iBAAiB;IAY/B,uDAAuD;IACjD,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/D,6FAA6F;YAC/E,kBAAkB;CAkBjC"}
1
+ {"version":3,"file":"user.service.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACjF,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAOL,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAWzD,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,WAAW;IAMpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAGxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IACY,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAMtE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAK1B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAI7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAzBlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,4FAA4F;IAC5F,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;gBAGpC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAGrB,WAAW,CAAC,GAAE,eAAe,GAAG,IAAI,aAAA,EACK,WAAW,CAAC,GAAE,WAAW,GAAG,IAAI,aAAA,EAMzE,QAAQ,CAAC,GAAE,cAAc,GAAG,IAAI,aAAA,EAKhC,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAIvC,cAAc,CAAC,GAAE,mBAAmB,GAAG,IAAI,aAAA;IAG9D;;;;OAIG;IACG,UAAU,CACd,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAC5D,OAAO,CAAC,QAAQ,CAAC;IAcpB,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,mGAAmG;IAC7F,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCjF;;;;;;OAMG;YACW,WAAW;IAmCzB;;;;;;;;;OASG;YACW,iBAAiB;IAY/B,uDAAuD;IACjD,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/D,6FAA6F;YAC/E,kBAAkB;CAkBjC"}
@@ -21,6 +21,7 @@ const posting_version_bump_js_1 = require("../membership/posting-version-bump.js
21
21
  const list_query_js_1 = require("../shared/list-query.js");
22
22
  const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
23
23
  const identity_errors_js_1 = require("../shared/identity-errors.js");
24
+ const email_address_changer_token_js_1 = require("./email-address-changer.token.js");
24
25
  const USER_SEARCH_FIELDS = ['username', 'email'];
25
26
  const USER_SORT_FIELDS = ['username', 'email', 'status', 'createdAt'];
26
27
  /**
@@ -36,6 +37,11 @@ const USER_SORT_FIELDS = ['username', 'email', 'status', 'createdAt'];
36
37
  * - an email change burns the target's outstanding password-reset tokens BEFORE the change and
37
38
  * notifies the OLD address after it (best-effort), when guard's reset-token store and email
38
39
  * sender are wired (`ShieldModule.forRoot({ auth: true })`).
40
+ * - Phase 12 (KI #14): the email itself is written through guard's ONE email-change helper
41
+ * (`MariaDbEmailAddressChanger`, the same one `POST /auth/confirm-email` uses): canonical
42
+ * address (trim → NFKC → lower-case), collision refusal (409) and login-hint follow in ONE
43
+ * transaction, the user's pending self-service tokens burned in it, and an audit row in the
44
+ * engine's shape written on that transaction (fail closed). Not wired → the change is refused.
39
45
  */
40
46
  let UserService = UserService_1 = class UserService {
41
47
  records;
@@ -44,14 +50,18 @@ let UserService = UserService_1 = class UserService {
44
50
  emailSender;
45
51
  sessions;
46
52
  versionBump;
53
+ emailAddresses;
47
54
  logger = new common_1.Logger(UserService_1.name);
48
- constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump) {
55
+ /** The engine's audit sink, used on the email helper's transaction (no pool of its own). */
56
+ emailAudit = new engine_1.MariaDbAuditSink({});
57
+ constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump, emailAddresses) {
49
58
  this.records = records;
50
59
  this.ceiling = ceiling;
51
60
  this.resetTokens = resetTokens;
52
61
  this.emailSender = emailSender;
53
62
  this.sessions = sessions;
54
63
  this.versionBump = versionBump;
64
+ this.emailAddresses = emailAddresses;
55
65
  }
56
66
  /**
57
67
  * Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
@@ -91,23 +101,26 @@ let UserService = UserService_1 = class UserService {
91
101
  if (usernameChanged)
92
102
  fields.username = patch.username;
93
103
  const oldEmail = typeof current.fields.email === 'string' ? current.fields.email : '';
94
- const emailChanged = typeof patch?.email === 'string' && patch.email !== oldEmail;
104
+ // Phase 12 (A5): the address is canonicalised once; that value is compared, checked and written.
105
+ const newEmail = typeof patch?.email === 'string' ? (0, guard_1.canonicalEmail)(patch.email) : undefined;
106
+ const emailChanged = newEmail !== undefined && newEmail !== (0, guard_1.canonicalEmail)(oldEmail);
107
+ if (emailChanged && !(0, guard_1.isEmailShaped)(newEmail)) {
108
+ throw new identity_errors_js_1.IdentityBadRequestError('Enter a valid email address.');
109
+ }
95
110
  const statusChanged = typeof patch?.status === 'string' && patch.status !== current.fields.status;
96
111
  // A request for a non-active status ALWAYS runs the follow-up (Phase 3 review P1), even when
97
112
  // the stored status already matches: re-sending "disable" repairs a disable whose bump or
98
113
  // session revoke failed earlier (idempotent).
99
114
  const wantsInactive = typeof patch?.status === 'string' && patch.status !== 'active';
100
- if (emailChanged)
101
- fields.email = patch.email;
102
115
  if (statusChanged)
103
116
  fields.status = patch.status;
104
117
  if (usernameChanged || emailChanged || statusChanged || wantsInactive) {
105
118
  await this.ceiling.assertDominates(actor, uuid);
106
119
  }
120
+ // The email first (the step most likely to be refused, 409), through the ONE helper.
121
+ if (emailChanged)
122
+ await this.changeEmail(actor, uuid, newEmail);
107
123
  if (Object.keys(fields).length > 0) {
108
- // Burn outstanding reset links BEFORE the address changes (a failure aborts the change).
109
- if (emailChanged && this.resetTokens)
110
- await this.resetTokens.invalidateAllForPrincipal(uuid);
111
124
  await this.records.update('user', uuid, { fields });
112
125
  }
113
126
  if (statusChanged || wantsInactive)
@@ -115,6 +128,49 @@ let UserService = UserService_1 = class UserService {
115
128
  if (emailChanged && oldEmail)
116
129
  await this.notifyEmailChanged(oldEmail);
117
130
  }
131
+ /**
132
+ * Phase 12 (KI #14): write a dominated user's new (canonical) email through guard's
133
+ * email-change helper — ONE transaction that locks the user and credential rows, refuses a
134
+ * collision with another user's email or login hint (→ 409), moves the login hint when it
135
+ * equalled the old email, burns the user's pending self-service tokens, and writes the audit row
136
+ * (engine shape, fail closed). Reset links are burned BEFORE (a failure aborts the change).
137
+ */
138
+ async changeEmail(actor, uuid, newEmail) {
139
+ const changer = this.emailAddresses;
140
+ if (changer == null) {
141
+ // Fail closed: never an unchecked write that could collide or strand the login hint.
142
+ throw new Error('UserService: no email-address changer is wired; email changes are refused');
143
+ }
144
+ if (this.resetTokens)
145
+ await this.resetTokens.invalidateAllForPrincipal(uuid);
146
+ const outcome = await changer.change({
147
+ principalId: uuid,
148
+ newEmail,
149
+ withinTransaction: async (query, result) => {
150
+ const event = {
151
+ type: 'admin_action',
152
+ occurredAtMs: Date.now(),
153
+ actorId: actor.id,
154
+ tenantId: actor.tenantId,
155
+ action: { key: 'update' },
156
+ resource: { level: 'record', typeKey: 'user', tenantId: actor.tenantId, recordId: uuid },
157
+ attributes: {
158
+ op: 'update',
159
+ typeKey: 'user',
160
+ fields: ['email'],
161
+ newEmail: (0, guard_1.maskEmail)(newEmail),
162
+ loginHintMoved: result.hintMoved,
163
+ },
164
+ };
165
+ await this.emailAudit.record(event, (sql, params) => query(sql, params));
166
+ },
167
+ });
168
+ if (outcome.status === 'unavailable') {
169
+ throw new identity_errors_js_1.IdentityConflictError('That email address is already in use.');
170
+ }
171
+ if (outcome.status !== 'changed')
172
+ throw new identity_errors_js_1.IdentityNotFoundError('User not found.');
173
+ }
118
174
  /**
119
175
  * After a status change (security hardening Phase 3): bump every tenant the user is a member of
120
176
  * (the next request reloads its grants: none while disabled, back when re-enabled), and, when the
@@ -178,6 +234,8 @@ exports.UserService = UserService = UserService_1 = __decorate([
178
234
  __param(4, (0, common_1.Inject)(guard_1.SHIELD_SESSION_SWEEPER)),
179
235
  __param(5, (0, common_1.Optional)()),
180
236
  __param(5, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
237
+ __param(6, (0, common_1.Optional)()),
238
+ __param(6, (0, common_1.Inject)(email_address_changer_token_js_1.IDENTITY_EMAIL_ADDRESS_CHANGER)),
181
239
  __metadata("design:paramtypes", [engine_1.RecordsService,
182
- grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object])
240
+ grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object, Object])
183
241
  ], UserService);
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Framework identity migration — `shield_identity.email_change_token` (security hardening
3
+ * Phase 12, KI #14: verified self-service email change).
4
+ *
5
+ * `PATCH /account/profile` with a new email no longer writes `shield_app.users.u_email`; it
6
+ * stores a single-use, 30-minute token here and mails a confirm link to the NEW address.
7
+ * `POST /auth/confirm-email` consumes the token and applies the change.
8
+ *
9
+ * Columns:
10
+ * - ect_token_hash: SHA-256 hex of the raw token (the raw token is never stored). PK, and
11
+ * stated UNIQUE explicitly (A11).
12
+ * - ect_principal_id: the user (`shield_app.users.u_uuid`). FK with ON DELETE CASCADE so a
13
+ * deleted user leaves no pending change behind. The type matches the parent exactly
14
+ * (VARCHAR(36), utf8mb4 / utf8mb4_unicode_ci) — InnoDB refuses an FK across mismatched
15
+ * string types or collations.
16
+ * - ect_new_email: the canonical new address (trim, NFKC, lower-case; A5).
17
+ * - ect_old_email: the user's email at request time (A3): confirm refuses when the current
18
+ * email no longer equals it (an admin or another flow changed it meanwhile).
19
+ * - ect_session_id: the requesting session (`session.s_session_id` is CHAR(64)); confirm keeps
20
+ * it and revokes every other session of the user (A7). No FK: a revoked/pruned session must
21
+ * not block or cascade the token.
22
+ * - ect_tenant_id: the requesting session's tenant (same type as `session.s_tenant_id`). The
23
+ * confirm step may run with no session, so its audit rows take the tenant from here
24
+ * (`shield_audit.audit_log.al_tenant_id` is NOT NULL).
25
+ * - ect_expires_at_ms / ect_consumed_at_ms / ect_created_at_ms: TTL, single use, audit time.
26
+ * The expiry index serves the cleanup that runs on every insert (A11).
27
+ *
28
+ * The runtime `shield_core` account already holds DML on `shield_identity.*` (no new grant).
29
+ * NOT in the @astram/identity object registry (engine-internal auth table) — literal column
30
+ * names. Idempotent (CREATE TABLE IF NOT EXISTS / DROP TABLE IF EXISTS).
31
+ */
32
+
33
+ const SCHEMA = 'shield_identity';
34
+
35
+ async function up({ query }) {
36
+ await query(
37
+ `CREATE TABLE IF NOT EXISTS \`${SCHEMA}\`.\`email_change_token\` (
38
+ ect_token_hash VARCHAR(64) NOT NULL,
39
+ ect_principal_id VARCHAR(36) NOT NULL,
40
+ ect_new_email VARCHAR(255) NOT NULL,
41
+ ect_old_email VARCHAR(255) NULL,
42
+ ect_session_id CHAR(64) NOT NULL,
43
+ ect_tenant_id VARCHAR(255) NOT NULL,
44
+ ect_expires_at_ms BIGINT NOT NULL,
45
+ ect_consumed_at_ms BIGINT NULL,
46
+ ect_created_at_ms BIGINT NOT NULL,
47
+ PRIMARY KEY (ect_token_hash),
48
+ UNIQUE KEY UQ_ect_token_hash (ect_token_hash),
49
+ INDEX idx_ect_principal (ect_principal_id),
50
+ INDEX idx_ect_expires (ect_expires_at_ms),
51
+ CONSTRAINT FK_email_change_token_user
52
+ FOREIGN KEY (ect_principal_id) REFERENCES \`shield_app\`.\`users\` (u_uuid)
53
+ ON DELETE CASCADE
54
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
55
+ );
56
+ }
57
+
58
+ async function down({ query }) {
59
+ await query(`DROP TABLE IF EXISTS \`${SCHEMA}\`.\`email_change_token\``);
60
+ }
61
+
62
+ module.exports = { up, down };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/identity",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the framework-owned identity + tenancy model. Hosts the identity @ObjectType classes (user, company, membership, posting), framework-authored identity migrations, and the identity resolver. Top of the dependency graph: depends on @astram/engine (for @ObjectType/@Field decorators) + @astram/guard / @astram/core (for the auth ports).",
6
6
  "type": "commonjs",
@@ -36,9 +36,9 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "mariadb": "^3.5.4",
39
+ "@astram/guard": "0.11.0",
39
40
  "@astram/core": "1.4.0",
40
- "@astram/guard": "0.10.1",
41
- "@astram/engine": "0.5.1"
41
+ "@astram/engine": "0.5.2"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@nestjs/common": "^11",