@astram/identity 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/dist/company-membership.object.d.ts.map +1 -1
  2. package/dist/company-membership.object.js +3 -2
  3. package/dist/company.object.d.ts.map +1 -1
  4. package/dist/company.object.js +8 -3
  5. package/dist/features/access/index.d.ts.map +1 -1
  6. package/dist/features/access/index.js +1 -5
  7. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  8. package/dist/features/audit/audit-read.service.js +13 -6
  9. package/dist/features/audit/audit.controller.d.ts.map +1 -1
  10. package/dist/features/audit/audit.controller.js +2 -0
  11. package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
  12. package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
  13. package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
  14. package/dist/features/authz/catalog-reconcile.job.js +1 -3
  15. package/dist/features/authz/db-permission-facts.adapter.d.ts +16 -0
  16. package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
  17. package/dist/features/authz/db-permission-facts.adapter.js +46 -2
  18. package/dist/features/authz/grant-admin.controller.d.ts +8 -5
  19. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
  20. package/dist/features/authz/grant-admin.controller.js +20 -18
  21. package/dist/features/authz/grant-admin.service.d.ts +72 -16
  22. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  23. package/dist/features/authz/grant-admin.service.js +232 -86
  24. package/dist/features/authz/grant-ceiling.d.ts +100 -1
  25. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  26. package/dist/features/authz/grant-ceiling.js +264 -2
  27. package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
  28. package/dist/features/authz/mariadb-catalog-store.js +15 -3
  29. package/dist/features/authz/permission-version.bumper.d.ts +52 -1
  30. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
  31. package/dist/features/authz/permission-version.bumper.js +119 -20
  32. package/dist/features/authz/record-authorization.d.ts +12 -0
  33. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  34. package/dist/features/authz/record-authorization.js +49 -0
  35. package/dist/features/authz/role-binding.d.ts +36 -0
  36. package/dist/features/authz/role-binding.d.ts.map +1 -0
  37. package/dist/features/authz/role-binding.js +53 -0
  38. package/dist/features/company/company-hierarchy.service.d.ts +108 -0
  39. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
  40. package/dist/features/company/company-hierarchy.service.js +277 -0
  41. package/dist/features/company/company.controller.d.ts +23 -5
  42. package/dist/features/company/company.controller.d.ts.map +1 -1
  43. package/dist/features/company/company.controller.js +47 -12
  44. package/dist/features/company/company.service.d.ts +28 -12
  45. package/dist/features/company/company.service.d.ts.map +1 -1
  46. package/dist/features/company/company.service.js +61 -14
  47. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
  48. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
  49. package/dist/features/identity-feature.module.d.ts +2 -0
  50. package/dist/features/identity-feature.module.d.ts.map +1 -1
  51. package/dist/features/identity-feature.module.js +72 -5
  52. package/dist/features/membership/membership.controller.d.ts +6 -5
  53. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  54. package/dist/features/membership/membership.controller.js +34 -22
  55. package/dist/features/membership/membership.service.d.ts +20 -14
  56. package/dist/features/membership/membership.service.d.ts.map +1 -1
  57. package/dist/features/membership/membership.service.js +62 -29
  58. package/dist/features/membership/posting-version-bump.d.ts +12 -0
  59. package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
  60. package/dist/features/membership/posting.service.d.ts +20 -24
  61. package/dist/features/membership/posting.service.d.ts.map +1 -1
  62. package/dist/features/membership/posting.service.js +101 -64
  63. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  64. package/dist/features/org-structure/org-catalog.controller.js +2 -0
  65. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  66. package/dist/features/org-structure/org-catalog.service.js +4 -1
  67. package/dist/features/policy/policy.controller.d.ts.map +1 -1
  68. package/dist/features/policy/policy.controller.js +2 -0
  69. package/dist/features/role/role.controller.d.ts +3 -2
  70. package/dist/features/role/role.controller.d.ts.map +1 -1
  71. package/dist/features/role/role.controller.js +15 -10
  72. package/dist/features/role/role.service.d.ts +39 -8
  73. package/dist/features/role/role.service.d.ts.map +1 -1
  74. package/dist/features/role/role.service.js +77 -10
  75. package/dist/features/scope/scope-closure.service.d.ts +54 -4
  76. package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
  77. package/dist/features/scope/scope-closure.service.js +237 -85
  78. package/dist/features/service-account/service-account.controller.d.ts +3 -2
  79. package/dist/features/service-account/service-account.controller.d.ts.map +1 -1
  80. package/dist/features/service-account/service-account.controller.js +29 -15
  81. package/dist/features/service-account/service-account.service.d.ts +29 -7
  82. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  83. package/dist/features/service-account/service-account.service.js +73 -12
  84. package/dist/features/service-account/service-account.store.d.ts +15 -0
  85. package/dist/features/service-account/service-account.store.d.ts.map +1 -1
  86. package/dist/features/service-account/service-account.store.js +29 -0
  87. package/dist/features/share/share.controller.d.ts.map +1 -1
  88. package/dist/features/share/share.controller.js +2 -0
  89. package/dist/features/share/share.service.d.ts.map +1 -1
  90. package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
  91. package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
  92. package/dist/features/shared/identity-error.interceptor.js +82 -0
  93. package/dist/features/shared/identity-errors.d.ts +23 -0
  94. package/dist/features/shared/identity-errors.d.ts.map +1 -0
  95. package/dist/features/shared/identity-errors.js +41 -0
  96. package/dist/features/user/session-revocation-check.d.ts +11 -0
  97. package/dist/features/user/session-revocation-check.d.ts.map +1 -0
  98. package/dist/features/user/session-revocation-check.js +21 -0
  99. package/dist/features/user/user.controller.d.ts +11 -6
  100. package/dist/features/user/user.controller.d.ts.map +1 -1
  101. package/dist/features/user/user.controller.js +35 -17
  102. package/dist/features/user/user.service.d.ts +55 -14
  103. package/dist/features/user/user.service.d.ts.map +1 -1
  104. package/dist/features/user/user.service.js +143 -16
  105. package/dist/index.d.ts +7 -5
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +7 -6
  108. package/dist/internal.d.ts +10 -0
  109. package/dist/internal.d.ts.map +1 -0
  110. package/dist/internal.js +13 -0
  111. package/dist/membership-posting.object.d.ts.map +1 -1
  112. package/dist/membership-posting.object.js +4 -2
  113. package/dist/objects.d.ts.map +1 -1
  114. package/dist/objects.js +13 -3
  115. package/dist/role.object.d.ts.map +1 -1
  116. package/dist/role.object.js +3 -0
  117. package/dist/user.object.d.ts.map +1 -1
  118. package/dist/user.object.js +3 -0
  119. package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
  120. package/package.json +15 -4
  121. package/dist/auth-facts-keys.js.map +0 -1
  122. package/dist/branch.object.js.map +0 -1
  123. package/dist/company-branch.object.js.map +0 -1
  124. package/dist/company-department.object.js.map +0 -1
  125. package/dist/company-membership.object.js.map +0 -1
  126. package/dist/company.object.js.map +0 -1
  127. package/dist/cross-tenant-grant.object.js.map +0 -1
  128. package/dist/department.object.js.map +0 -1
  129. package/dist/features/access/access-admin.access.js.map +0 -1
  130. package/dist/features/access/company-admin.access.js.map +0 -1
  131. package/dist/features/access/index.js.map +0 -1
  132. package/dist/features/access/user-management.access.js.map +0 -1
  133. package/dist/features/audit/audit-read.service.js.map +0 -1
  134. package/dist/features/audit/audit.controller.js.map +0 -1
  135. package/dist/features/audit/directory-label-resolver.js.map +0 -1
  136. package/dist/features/audit/mariadb-audit-reader.js.map +0 -1
  137. package/dist/features/authz/access-bundle-reconcile.job.js.map +0 -1
  138. package/dist/features/authz/access-bundle-store.js.map +0 -1
  139. package/dist/features/authz/catalog-bootstrap.provider.js.map +0 -1
  140. package/dist/features/authz/catalog-reconcile.job.js.map +0 -1
  141. package/dist/features/authz/db-permission-facts.adapter.js.map +0 -1
  142. package/dist/features/authz/grant-admin.controller.js.map +0 -1
  143. package/dist/features/authz/grant-admin.service.js.map +0 -1
  144. package/dist/features/authz/grant-ceiling.js.map +0 -1
  145. package/dist/features/authz/mariadb-catalog-store.js.map +0 -1
  146. package/dist/features/authz/permission-version.bumper.js.map +0 -1
  147. package/dist/features/authz/record-authorization.js.map +0 -1
  148. package/dist/features/authz/session-facts.reader.js.map +0 -1
  149. package/dist/features/company/company.controller.js.map +0 -1
  150. package/dist/features/company/company.service.js.map +0 -1
  151. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js.map +0 -1
  152. package/dist/features/cross-tenant/cross-tenant-grant.controller.js.map +0 -1
  153. package/dist/features/cross-tenant/grant-authorize.js.map +0 -1
  154. package/dist/features/identity-feature.module.js.map +0 -1
  155. package/dist/features/membership/membership-checker.js.map +0 -1
  156. package/dist/features/membership/membership.controller.js.map +0 -1
  157. package/dist/features/membership/membership.service.js.map +0 -1
  158. package/dist/features/membership/posting-version-bump.js.map +0 -1
  159. package/dist/features/membership/posting.service.js.map +0 -1
  160. package/dist/features/org-structure/org-catalog.controller.js.map +0 -1
  161. package/dist/features/org-structure/org-catalog.service.js.map +0 -1
  162. package/dist/features/policy/policy.controller.js.map +0 -1
  163. package/dist/features/policy/policy.service.js.map +0 -1
  164. package/dist/features/role/role.controller.js.map +0 -1
  165. package/dist/features/role/role.service.js.map +0 -1
  166. package/dist/features/scope/scope-closure.service.js.map +0 -1
  167. package/dist/features/scope/scope-subtree.resolver.js.map +0 -1
  168. package/dist/features/share/share.controller.js.map +0 -1
  169. package/dist/features/share/share.service.js.map +0 -1
  170. package/dist/features/shared/http.js.map +0 -1
  171. package/dist/features/shared/list-query.js.map +0 -1
  172. package/dist/features/shared/named-option.js.map +0 -1
  173. package/dist/features/shared/record-resolver.js.map +0 -1
  174. package/dist/features/user/user.controller.js.map +0 -1
  175. package/dist/features/user/user.service.js.map +0 -1
  176. package/dist/index.js.map +0 -1
  177. package/dist/membership-posting.object.js.map +0 -1
  178. package/dist/migrations-dir.js.map +0 -1
  179. package/dist/objects.js.map +0 -1
  180. package/dist/record-share.object.js.map +0 -1
  181. package/dist/role.object.js.map +0 -1
  182. package/dist/schema.js.map +0 -1
  183. package/dist/shared-resource.object.js.map +0 -1
  184. package/dist/user.object.js.map +0 -1
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Identity domain errors (security hardening Phase 1). Thrown by identity services, mapped to HTTP
4
+ * by {@link IdentityErrorInterceptor} in ONE place so every identity route answers the same way.
5
+ *
6
+ * Messages are user-facing: they name no raw ids (ADR-0012) and carry no store detail.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IdentityBadRequestError = exports.IdentityConflictError = exports.IdentityNotFoundError = exports.IdentityForbiddenError = void 0;
10
+ /** The caller may not perform this action on this target (→ 403). */
11
+ class IdentityForbiddenError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = 'IdentityForbiddenError';
15
+ }
16
+ }
17
+ exports.IdentityForbiddenError = IdentityForbiddenError;
18
+ /** The target does not exist or is outside the caller's reach (→ 404, no existence leak). */
19
+ class IdentityNotFoundError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = 'IdentityNotFoundError';
23
+ }
24
+ }
25
+ exports.IdentityNotFoundError = IdentityNotFoundError;
26
+ /** The request conflicts with the current state (cycle, children present, … → 409). */
27
+ class IdentityConflictError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'IdentityConflictError';
31
+ }
32
+ }
33
+ exports.IdentityConflictError = IdentityConflictError;
34
+ /** The request is malformed (→ 400). */
35
+ class IdentityBadRequestError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = 'IdentityBadRequestError';
39
+ }
40
+ }
41
+ exports.IdentityBadRequestError = IdentityBadRequestError;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Boot check (security hardening Phase 3 review P2). Disabling a user revokes its sessions through
3
+ * guard's `SHIELD_SESSION_SWEEPER` (provided by the auth surface). When the auth surface is on —
4
+ * guard's session token binder is visible — but no sweeper is, a disable still drops the user's
5
+ * roles (version bump + `[]` roles) but cannot end its sessions: say so at boot instead of failing
6
+ * silently. Auth off → no sessions exist → nothing to warn about.
7
+ *
8
+ * @returns `true` when the warning was emitted.
9
+ */
10
+ export declare function warnIfSessionsUnrevocable(binder: unknown, sweeper: unknown, warn: (message: string) => void): boolean;
11
+ //# sourceMappingURL=session-revocation-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-revocation-check.d.ts","sourceRoot":"","sources":["../../../src/features/user/session-revocation-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,OAAO,CAQT"}
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.warnIfSessionsUnrevocable = warnIfSessionsUnrevocable;
4
+ /**
5
+ * Boot check (security hardening Phase 3 review P2). Disabling a user revokes its sessions through
6
+ * guard's `SHIELD_SESSION_SWEEPER` (provided by the auth surface). When the auth surface is on —
7
+ * guard's session token binder is visible — but no sweeper is, a disable still drops the user's
8
+ * roles (version bump + `[]` roles) but cannot end its sessions: say so at boot instead of failing
9
+ * silently. Auth off → no sessions exist → nothing to warn about.
10
+ *
11
+ * @returns `true` when the warning was emitted.
12
+ */
13
+ function warnIfSessionsUnrevocable(binder, sweeper, warn) {
14
+ if (binder === undefined || binder === null)
15
+ return false;
16
+ if (sweeper !== undefined && sweeper !== null)
17
+ return false;
18
+ warn('SHIELD_SESSION_SWEEPER is not available: disabling a user will not revoke its sessions ' +
19
+ '(its roles still drop on the next request). Provide a sessionSweeper to ShieldModule.forRoot.');
20
+ return true;
21
+ }
@@ -1,5 +1,6 @@
1
1
  import { UserService } from './user.service.js';
2
2
  import { type ListQueryDto } from '../shared/list-query.js';
3
+ import type { AuthedRequest } from '../shared/http.js';
3
4
  export declare class UserController {
4
5
  private readonly users;
5
6
  constructor(users: UserService);
@@ -14,25 +15,29 @@ export declare class UserController {
14
15
  page: number;
15
16
  limit: number;
16
17
  }>;
17
- /** Create a user — gated on `identity:user:create`. */
18
- createUser(body: {
18
+ /**
19
+ * Create a user — gated on `identity:user:create`. Optional `companyUuid` creates the user's
20
+ * first (active) membership in the same transaction; it must be a company the caller manages.
21
+ */
22
+ createUser(req: AuthedRequest, body: {
19
23
  username?: string;
24
+ companyUuid?: string;
20
25
  }): Promise<{
21
26
  id: import("packages/engine/dist/types.js").RecordId;
22
27
  }>;
23
28
  /** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
24
29
  * administrator, may read users). */
25
30
  getUser(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
26
- /** Update a user — gated on `identity:user:update`. */
27
- updateUser(uuid: string, body: {
31
+ /** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
32
+ updateUser(req: AuthedRequest, uuid: string, body: {
28
33
  username?: string;
29
34
  email?: string;
30
35
  status?: string;
31
36
  }): Promise<{
32
37
  success: boolean;
33
38
  }>;
34
- /** Delete a user — gated on `identity:user:delete`. */
35
- deleteUser(uuid: string): Promise<{
39
+ /** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
40
+ deleteUser(req: AuthedRequest, uuid: string): Promise<{
36
41
  success: boolean;
37
42
  }>;
38
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"user.controller.d.ts","sourceRoot":"","sources":["../../../src/features/user/user.controller.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAK5E,qBACa,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,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKpD;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,uDAAuD;IAGjD,UAAU,CACC,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;;;IAMtE,uDAAuD;IAGjD,UAAU,CAAgB,IAAI,EAAE,MAAM;;;CAI7C"}
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"}
@@ -17,9 +17,11 @@ const common_1 = require("@nestjs/common");
17
17
  const guard_1 = require("@astram/guard");
18
18
  const user_service_js_1 = require("./user.service.js");
19
19
  const list_query_js_1 = require("../shared/list-query.js");
20
+ const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
20
21
  // User management is gated on `identity:user:<action>` grants. `user` is CONTROL-plane
21
22
  // for ALL actions — including read: the capped root is the identity administrator (the
22
- // identity-administrator model), so it may list/read users AND manage them.
23
+ // identity-administrator model), so it may list/read users AND manage them. Changing a user's
24
+ // email or status, or deleting them, also needs the caller to dominate that user (D2).
23
25
  let UserController = class UserController {
24
26
  users;
25
27
  constructor(users) {
@@ -35,9 +37,17 @@ let UserController = class UserController {
35
37
  const result = await this.users.listUsers(params);
36
38
  return { total: result.total, rows: result.rows, page, limit };
37
39
  }
38
- /** Create a user — gated on `identity:user:create`. */
39
- async createUser(body) {
40
- const id = await this.users.createUser(body?.username ?? 'Untitled');
40
+ /**
41
+ * Create a user — gated on `identity:user:create`. Optional `companyUuid` creates the user's
42
+ * first (active) membership in the same transaction; it must be a company the caller manages.
43
+ */
44
+ async createUser(req, body) {
45
+ const id = await this.users.createUser(req.principal, {
46
+ username: body?.username ?? 'Untitled',
47
+ ...(typeof body?.companyUuid === 'string' && body.companyUuid.length > 0
48
+ ? { companyUuid: body.companyUuid }
49
+ : {}),
50
+ });
41
51
  return { id };
42
52
  }
43
53
  /** Read one user — gated on `identity:user:read` (CONTROL-plane; root, the identity
@@ -45,14 +55,18 @@ let UserController = class UserController {
45
55
  getUser(uuid) {
46
56
  return this.users.getUser(uuid);
47
57
  }
48
- /** Update a user — gated on `identity:user:update`. */
49
- async updateUser(uuid, body) {
50
- await this.users.updateUser(uuid, body ?? {});
58
+ /** Update a user — gated on `identity:user:update`. Only `username`, `email`, `status` apply. */
59
+ async updateUser(req, uuid, body) {
60
+ await this.users.updateUser(req.principal, uuid, {
61
+ username: body?.username,
62
+ email: body?.email,
63
+ status: body?.status,
64
+ });
51
65
  return { success: true };
52
66
  }
53
- /** Delete a user — gated on `identity:user:delete`. */
54
- async deleteUser(uuid) {
55
- await this.users.deleteUser(uuid);
67
+ /** Delete a user — gated on `identity:user:delete`; the caller must dominate the user. */
68
+ async deleteUser(req, uuid) {
69
+ await this.users.deleteUser(req.principal, uuid);
56
70
  return { success: true };
57
71
  }
58
72
  };
@@ -68,9 +82,10 @@ __decorate([
68
82
  __decorate([
69
83
  (0, common_1.Post)('users'),
70
84
  (0, guard_1.Classified)({ typeKey: 'user', action: 'create' }),
71
- __param(0, (0, common_1.Body)()),
85
+ __param(0, (0, common_1.Req)()),
86
+ __param(1, (0, common_1.Body)()),
72
87
  __metadata("design:type", Function),
73
- __metadata("design:paramtypes", [Object]),
88
+ __metadata("design:paramtypes", [Object, Object]),
74
89
  __metadata("design:returntype", Promise)
75
90
  ], UserController.prototype, "createUser", null);
76
91
  __decorate([
@@ -84,21 +99,24 @@ __decorate([
84
99
  __decorate([
85
100
  (0, common_1.Put)('users/:uuid'),
86
101
  (0, guard_1.Classified)({ typeKey: 'user', action: 'update' }),
87
- __param(0, (0, common_1.Param)('uuid')),
88
- __param(1, (0, common_1.Body)()),
102
+ __param(0, (0, common_1.Req)()),
103
+ __param(1, (0, common_1.Param)('uuid')),
104
+ __param(2, (0, common_1.Body)()),
89
105
  __metadata("design:type", Function),
90
- __metadata("design:paramtypes", [String, Object]),
106
+ __metadata("design:paramtypes", [Object, String, Object]),
91
107
  __metadata("design:returntype", Promise)
92
108
  ], UserController.prototype, "updateUser", null);
93
109
  __decorate([
94
110
  (0, common_1.Delete)('users/:uuid'),
95
111
  (0, guard_1.Classified)({ typeKey: 'user', action: 'delete' }),
96
- __param(0, (0, common_1.Param)('uuid')),
112
+ __param(0, (0, common_1.Req)()),
113
+ __param(1, (0, common_1.Param)('uuid')),
97
114
  __metadata("design:type", Function),
98
- __metadata("design:paramtypes", [String]),
115
+ __metadata("design:paramtypes", [Object, String]),
99
116
  __metadata("design:returntype", Promise)
100
117
  ], UserController.prototype, "deleteUser", null);
101
118
  exports.UserController = UserController = __decorate([
102
119
  (0, common_1.Controller)('identity'),
120
+ (0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
103
121
  __metadata("design:paramtypes", [user_service_js_1.UserService])
104
122
  ], UserController);
@@ -1,26 +1,67 @@
1
1
  import { RecordsService, type RecordId } from '@astram/engine';
2
+ import type { Principal } from '@astram/core';
3
+ import { type EmailSender, type ResetTokenStore, type SessionSweeper } from '@astram/guard';
4
+ import { type PostingVersionBump } from '../membership/posting-version-bump.js';
2
5
  import { type ListParams } from '../shared/list-query.js';
6
+ import { GrantCeiling } from '../authz/grant-ceiling.js';
7
+ /** The patch keys a generic user update accepts (security hardening #3). */
8
+ export interface UserPatch {
9
+ username?: unknown;
10
+ email?: unknown;
11
+ status?: unknown;
12
+ }
3
13
  /**
4
- * UserService — user (principal) CRUD on the paved road, via the framework's
5
- * RecordsService facade. The facade supplies the acting identity (the request
6
- * principal) — this service never threads a Principal. No raw SQL, no RECORD_ENGINE.
14
+ * UserService — user (principal) CRUD on the paved road, via the framework's RecordsService facade
15
+ * (the request principal acts). No raw SQL, no RECORD_ENGINE.
16
+ *
17
+ * Users are GLOBAL (one user can belong to many tenants) and the email is a credential attribute:
18
+ * forgot-password mails the reset link to it. So (security hardening #3, D2):
19
+ * - the patch is whitelisted to `{username, email, status}`;
20
+ * - changing `email` or `status`, and deleting a user, require the actor to DOMINATE the target
21
+ * (`GrantCeiling.assertDominates`: every membership of the target inside the actor's subtree,
22
+ * and every key the target could hold is held by the actor);
23
+ * - an email change burns the target's outstanding password-reset tokens BEFORE the change and
24
+ * notifies the OLD address after it (best-effort), when guard's reset-token store and email
25
+ * sender are wired (`ShieldModule.forRoot({ auth: true })`).
7
26
  */
8
27
  export declare class UserService {
9
28
  private readonly records;
10
- constructor(records: RecordsService);
11
- /** Create a user. */
12
- createUser(username: string): Promise<RecordId>;
29
+ private readonly ceiling;
30
+ private readonly resetTokens?;
31
+ private readonly emailSender?;
32
+ private readonly sessions?;
33
+ private readonly versionBump?;
34
+ 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);
36
+ /**
37
+ * Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
38
+ * ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
39
+ * by the engine's create wall (a company outside the caller's subtree ⇒ 403, nothing persists).
40
+ */
41
+ createUser(actor: Principal, input: {
42
+ username: string;
43
+ companyUuid?: string | undefined;
44
+ }): Promise<RecordId>;
13
45
  /** List users (tenant-scoped by the pipe), with paging/search/sort. */
14
46
  listUsers(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
15
47
  /** Read a single user. */
16
48
  getUser(uuid: string): Promise<import("@astram/engine").RecordView | null>;
17
- /** Update a user. */
18
- updateUser(uuid: string, patch: {
19
- username?: string;
20
- email?: string;
21
- status?: string;
22
- }): Promise<void>;
23
- /** Delete a user. */
24
- deleteUser(uuid: string): Promise<void>;
49
+ /** Update a user (whitelisted patch; email/status changes need dominance — see the class doc). */
50
+ updateUser(actor: Principal, uuid: string, patch: UserPatch): Promise<void>;
51
+ /**
52
+ * After a status change (security hardening Phase 3): bump every tenant the user is a member of
53
+ * (the next request reloads its grants: none while disabled, back when re-enabled), and, when the
54
+ * account is not `active`, durably revoke all its sessions (Redis unbound too).
55
+ *
56
+ * Both are ATTEMPTED INDEPENDENTLY (Phase 3 review P1): a failed bump never skips the revoke, or
57
+ * the reverse. If either failed, the first error is rethrown, so the admin sees it and can
58
+ * re-send the same status (which re-runs both). The status itself is already committed, so login
59
+ * and role reads refuse the account regardless.
60
+ */
61
+ private applyStatusChange;
62
+ /** Delete a user — only a user the actor dominates. */
63
+ deleteUser(actor: Principal, uuid: string): Promise<void>;
64
+ /** Tell the OLD address its account email changed (best-effort; never names the new one). */
65
+ private notifyEmailChanged;
25
66
  }
26
67
  //# sourceMappingURL=user.service.d.ts.map
@@ -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,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAK1E;;;;GAIG;AACH,qBACa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,qBAAqB;IACrB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAI/C,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,qBAAqB;IACrB,UAAU,CACR,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5D,OAAO,CAAC,IAAI,CAAC;IAIhB,qBAAqB;IACrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC"}
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,kGAAkG;IAC5F,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BjF;;;;;;;;;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"}
@@ -8,26 +8,68 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var UserService_1;
11
15
  Object.defineProperty(exports, "__esModule", { value: true });
12
16
  exports.UserService = void 0;
13
17
  const common_1 = require("@nestjs/common");
14
18
  const engine_1 = require("@astram/engine");
19
+ const guard_1 = require("@astram/guard");
20
+ const posting_version_bump_js_1 = require("../membership/posting-version-bump.js");
15
21
  const list_query_js_1 = require("../shared/list-query.js");
22
+ const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
23
+ const identity_errors_js_1 = require("../shared/identity-errors.js");
16
24
  const USER_SEARCH_FIELDS = ['username', 'email'];
17
25
  const USER_SORT_FIELDS = ['username', 'email', 'status', 'createdAt'];
18
26
  /**
19
- * UserService — user (principal) CRUD on the paved road, via the framework's
20
- * RecordsService facade. The facade supplies the acting identity (the request
21
- * principal) — this service never threads a Principal. No raw SQL, no RECORD_ENGINE.
27
+ * UserService — user (principal) CRUD on the paved road, via the framework's RecordsService facade
28
+ * (the request principal acts). No raw SQL, no RECORD_ENGINE.
29
+ *
30
+ * Users are GLOBAL (one user can belong to many tenants) and the email is a credential attribute:
31
+ * forgot-password mails the reset link to it. So (security hardening #3, D2):
32
+ * - the patch is whitelisted to `{username, email, status}`;
33
+ * - changing `email` or `status`, and deleting a user, require the actor to DOMINATE the target
34
+ * (`GrantCeiling.assertDominates`: every membership of the target inside the actor's subtree,
35
+ * and every key the target could hold is held by the actor);
36
+ * - an email change burns the target's outstanding password-reset tokens BEFORE the change and
37
+ * notifies the OLD address after it (best-effort), when guard's reset-token store and email
38
+ * sender are wired (`ShieldModule.forRoot({ auth: true })`).
22
39
  */
23
- let UserService = class UserService {
40
+ let UserService = UserService_1 = class UserService {
24
41
  records;
25
- constructor(records) {
42
+ ceiling;
43
+ resetTokens;
44
+ emailSender;
45
+ sessions;
46
+ versionBump;
47
+ logger = new common_1.Logger(UserService_1.name);
48
+ constructor(records, ceiling, resetTokens, emailSender, sessions, versionBump) {
26
49
  this.records = records;
50
+ this.ceiling = ceiling;
51
+ this.resetTokens = resetTokens;
52
+ this.emailSender = emailSender;
53
+ this.sessions = sessions;
54
+ this.versionBump = versionBump;
27
55
  }
28
- /** Create a user. */
29
- createUser(username) {
30
- return this.records.create('user', { fields: { username } });
56
+ /**
57
+ * Create a user. With `companyUuid`, the user is created AND attached (one active membership) in
58
+ * ONE transaction — the onboarding path for a non-root admin, whose membership create is checked
59
+ * by the engine's create wall (a company outside the caller's subtree ⇒ 403, nothing persists).
60
+ */
61
+ async createUser(actor, input) {
62
+ if (!input.companyUuid) {
63
+ return this.records.create('user', { fields: { username: input.username } });
64
+ }
65
+ const companyUuid = input.companyUuid;
66
+ return this.records.transaction(async (tx) => {
67
+ const id = await tx.create('user', { fields: { username: input.username } });
68
+ await tx.create('company_membership', {
69
+ fields: { userUuid: id, companyUuid, isActive: true, status: 'active' },
70
+ });
71
+ return id;
72
+ });
31
73
  }
32
74
  /** List users (tenant-scoped by the pipe), with paging/search/sort. */
33
75
  listUsers(params) {
@@ -37,17 +79,102 @@ let UserService = class UserService {
37
79
  getUser(uuid) {
38
80
  return this.records.get('user', uuid);
39
81
  }
40
- /** Update a user. */
41
- updateUser(uuid, patch) {
42
- return this.records.update('user', uuid, { fields: patch });
82
+ /** Update a user (whitelisted patch; email/status changes need dominance — see the class doc). */
83
+ async updateUser(actor, uuid, patch) {
84
+ const current = await this.records.get('user', uuid);
85
+ if (current === null)
86
+ throw new identity_errors_js_1.IdentityNotFoundError('User not found.');
87
+ const fields = {};
88
+ if (typeof patch?.username === 'string')
89
+ fields.username = patch.username;
90
+ const oldEmail = typeof current.fields.email === 'string' ? current.fields.email : '';
91
+ const emailChanged = typeof patch?.email === 'string' && patch.email !== oldEmail;
92
+ const statusChanged = typeof patch?.status === 'string' && patch.status !== current.fields.status;
93
+ // A request for a non-active status ALWAYS runs the follow-up (Phase 3 review P1), even when
94
+ // the stored status already matches: re-sending "disable" repairs a disable whose bump or
95
+ // session revoke failed earlier (idempotent).
96
+ const wantsInactive = typeof patch?.status === 'string' && patch.status !== 'active';
97
+ if (emailChanged)
98
+ fields.email = patch.email;
99
+ if (statusChanged)
100
+ fields.status = patch.status;
101
+ if (emailChanged || statusChanged || wantsInactive) {
102
+ await this.ceiling.assertDominates(actor, uuid);
103
+ }
104
+ if (Object.keys(fields).length > 0) {
105
+ // Burn outstanding reset links BEFORE the address changes (a failure aborts the change).
106
+ if (emailChanged && this.resetTokens)
107
+ await this.resetTokens.invalidateAllForPrincipal(uuid);
108
+ await this.records.update('user', uuid, { fields });
109
+ }
110
+ if (statusChanged || wantsInactive)
111
+ await this.applyStatusChange(uuid, String(patch.status));
112
+ if (emailChanged && oldEmail)
113
+ await this.notifyEmailChanged(oldEmail);
114
+ }
115
+ /**
116
+ * After a status change (security hardening Phase 3): bump every tenant the user is a member of
117
+ * (the next request reloads its grants: none while disabled, back when re-enabled), and, when the
118
+ * account is not `active`, durably revoke all its sessions (Redis unbound too).
119
+ *
120
+ * Both are ATTEMPTED INDEPENDENTLY (Phase 3 review P1): a failed bump never skips the revoke, or
121
+ * the reverse. If either failed, the first error is rethrown, so the admin sees it and can
122
+ * re-send the same status (which re-runs both). The status itself is already committed, so login
123
+ * and role reads refuse the account regardless.
124
+ */
125
+ async applyStatusChange(uuid, status) {
126
+ const steps = [];
127
+ if (this.versionBump?.bumpForUser)
128
+ steps.push(this.versionBump.bumpForUser(uuid));
129
+ if (status !== 'active' && this.sessions)
130
+ steps.push(this.sessions.revokeAllForPrincipal(uuid));
131
+ const results = await Promise.allSettled(steps);
132
+ const failed = results.find((r) => r.status === 'rejected');
133
+ if (failed !== undefined) {
134
+ this.logger.warn('user status follow-up failed; re-send the status to retry');
135
+ throw failed.reason;
136
+ }
137
+ }
138
+ /** Delete a user — only a user the actor dominates. */
139
+ async deleteUser(actor, uuid) {
140
+ const current = await this.records.get('user', uuid);
141
+ if (current === null)
142
+ throw new identity_errors_js_1.IdentityNotFoundError('User not found.');
143
+ await this.ceiling.assertDominates(actor, uuid);
144
+ await this.records.delete('user', uuid);
43
145
  }
44
- /** Delete a user. */
45
- deleteUser(uuid) {
46
- return this.records.delete('user', uuid);
146
+ /** Tell the OLD address its account email changed (best-effort; never names the new one). */
147
+ async notifyEmailChanged(oldEmail) {
148
+ if (!this.emailSender)
149
+ return;
150
+ const text = 'The email address on your account was changed by an administrator. ' +
151
+ 'Password-reset links sent before this change no longer work. ' +
152
+ 'If you did not expect this, contact your administrator.';
153
+ try {
154
+ await this.emailSender.send({
155
+ to: oldEmail,
156
+ subject: 'Your account email address was changed',
157
+ text,
158
+ html: `<p>${text}</p>`,
159
+ });
160
+ }
161
+ catch {
162
+ // Best-effort: the change is committed; a delivery failure must not fail it.
163
+ this.logger.warn('email-change notice could not be delivered');
164
+ }
47
165
  }
48
166
  };
49
167
  exports.UserService = UserService;
50
- exports.UserService = UserService = __decorate([
168
+ exports.UserService = UserService = UserService_1 = __decorate([
51
169
  (0, common_1.Injectable)(),
52
- __metadata("design:paramtypes", [engine_1.RecordsService])
170
+ __param(2, (0, common_1.Optional)()),
171
+ __param(2, (0, common_1.Inject)(guard_1.SHIELD_RESET_TOKEN_STORE)),
172
+ __param(3, (0, common_1.Optional)()),
173
+ __param(3, (0, common_1.Inject)(guard_1.SHIELD_EMAIL_SENDER)),
174
+ __param(4, (0, common_1.Optional)()),
175
+ __param(4, (0, common_1.Inject)(guard_1.SHIELD_SESSION_SWEEPER)),
176
+ __param(5, (0, common_1.Optional)()),
177
+ __param(5, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
178
+ __metadata("design:paramtypes", [engine_1.RecordsService,
179
+ grant_ceiling_js_1.GrantCeiling, Object, Object, Object, Object])
53
180
  ], UserService);
package/dist/index.d.ts CHANGED
@@ -37,12 +37,14 @@ export { identityAuthActionKeys, identityControlPlaneKeys } from './auth-facts-k
37
37
  */
38
38
  export { IdentityFeatureModule } from './features/identity-feature.module.js';
39
39
  /**
40
- * The record-tier authorization wiring: the lean grant-read pool factory + the helper that
41
- * assembles `RecordAuthorizationInputs` for `ShieldModule.forRoot({ recordAuthorization })`.
42
- * Once wired, `decide()` is real (the framework's record-tier adapters run over the identity
43
- * app schema) instead of the deny-everything stub.
40
+ * The record-tier authorization wiring: the helper that assembles `RecordAuthorizationInputs`
41
+ * for `ShieldModule.forRoot({ recordAuthorization })`. Once wired, `decide()` is real (the
42
+ * framework's record-tier adapters run over the identity app schema) instead of the
43
+ * deny-everything stub. Its pool factory, `createIdentityAuthzPool` (it opens a raw pool), is NOT
44
+ * exported here (Phase 8 #5): import it from `@astram/identity/internal` in the composition root
45
+ * (`src/app.module.ts`).
44
46
  */
45
- export { createIdentityAuthzPool, buildIdentityRecordAuthorization, } from './features/authz/record-authorization.js';
47
+ export { buildIdentityRecordAuthorization } from './features/authz/record-authorization.js';
46
48
  /**
47
49
  * The ADR-0011 per-request session-facts reader (ALS hot path), re-exported so a consumer
48
50
  * that wants to construct it directly (rather than via `buildIdentityRecordAuthorization`)
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAExF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;;;GAKG;AACH,OAAO,EACL,uBAAuB,EACvB,gCAAgC,GACjC,MAAM,0CAA0C,CAAC;AAElD;;;;GAIG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAExF;;;;;GAKG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;;;;;GAOG;AACH,OAAO,EAAE,gCAAgC,EAAE,MAAM,0CAA0C,CAAC;AAE5F;;;;GAIG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAEpF;;;;;;;GAOG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.buildIdentityRecordAuthorization = exports.createIdentityAuthzPool = exports.IdentityFeatureModule = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
3
+ exports.DbMembershipChecker = exports.DbScopeSubtreeResolver = exports.AlsSessionFactsReader = exports.buildIdentityRecordAuthorization = exports.IdentityFeatureModule = exports.identityControlPlaneKeys = exports.identityAuthActionKeys = exports.identityObjects = exports.identityMigrationsDir = exports.ServiceAccount = exports.CompanyBranch = exports.CompanyDepartment = exports.MembershipPosting = exports.CompanyMembership = exports.Branch = exports.Department = exports.Role = exports.User = exports.Company = void 0;
4
4
  /**
5
5
  * @astram/identity — the framework-owned identity + tenancy model.
6
6
  * The 9 identity object classes (relocated from the consumer app in Sub-plan B) +
@@ -55,13 +55,14 @@ Object.defineProperty(exports, "identityControlPlaneKeys", { enumerable: true, g
55
55
  var identity_feature_module_js_1 = require("./features/identity-feature.module.js");
56
56
  Object.defineProperty(exports, "IdentityFeatureModule", { enumerable: true, get: function () { return identity_feature_module_js_1.IdentityFeatureModule; } });
57
57
  /**
58
- * The record-tier authorization wiring: the lean grant-read pool factory + the helper that
59
- * assembles `RecordAuthorizationInputs` for `ShieldModule.forRoot({ recordAuthorization })`.
60
- * Once wired, `decide()` is real (the framework's record-tier adapters run over the identity
61
- * app schema) instead of the deny-everything stub.
58
+ * The record-tier authorization wiring: the helper that assembles `RecordAuthorizationInputs`
59
+ * for `ShieldModule.forRoot({ recordAuthorization })`. Once wired, `decide()` is real (the
60
+ * framework's record-tier adapters run over the identity app schema) instead of the
61
+ * deny-everything stub. Its pool factory, `createIdentityAuthzPool` (it opens a raw pool), is NOT
62
+ * exported here (Phase 8 #5): import it from `@astram/identity/internal` in the composition root
63
+ * (`src/app.module.ts`).
62
64
  */
63
65
  var record_authorization_js_1 = require("./features/authz/record-authorization.js");
64
- Object.defineProperty(exports, "createIdentityAuthzPool", { enumerable: true, get: function () { return record_authorization_js_1.createIdentityAuthzPool; } });
65
66
  Object.defineProperty(exports, "buildIdentityRecordAuthorization", { enumerable: true, get: function () { return record_authorization_js_1.buildIdentityRecordAuthorization; } });
66
67
  /**
67
68
  * The ADR-0011 per-request session-facts reader (ALS hot path), re-exported so a consumer
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @astram/identity/internal — composition-root surface (Phase 8 #5).
3
+ *
4
+ * `createIdentityAuthzPool` opens a raw `mariadb` pool, so it is NOT on the package entry point
5
+ * (`@astram/identity`). An app's `src/app.module.ts` imports it from here to wire
6
+ * `buildIdentityRecordAuthorization(authzPool)`; the scaffolded app's lint wall rejects this
7
+ * subpath everywhere else. Feature code never holds a pool: it uses the RecordEngine.
8
+ */
9
+ export { createIdentityAuthzPool } from './features/authz/record-authorization.js';
10
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC"}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createIdentityAuthzPool = void 0;
4
+ /**
5
+ * @astram/identity/internal — composition-root surface (Phase 8 #5).
6
+ *
7
+ * `createIdentityAuthzPool` opens a raw `mariadb` pool, so it is NOT on the package entry point
8
+ * (`@astram/identity`). An app's `src/app.module.ts` imports it from here to wire
9
+ * `buildIdentityRecordAuthorization(authzPool)`; the scaffolded app's lint wall rejects this
10
+ * subpath everywhere else. Feature code never holds a pool: it uses the RecordEngine.
11
+ */
12
+ var record_authorization_js_1 = require("./features/authz/record-authorization.js");
13
+ Object.defineProperty(exports, "createIdentityAuthzPool", { enumerable: true, get: function () { return record_authorization_js_1.createIdentityAuthzPool; } });
@@ -1 +1 @@
1
- {"version":3,"file":"membership-posting.object.d.ts","sourceRoot":"","sources":["../src/membership-posting.object.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBAiBa,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAGd,cAAc,EAAG,MAAM,CAAC;IAGxB,QAAQ,EAAG,MAAM,CAAC;IAGlB,cAAc,EAAG,MAAM,CAAC;IAGxB,UAAU,EAAG,MAAM,CAAC;IAQpB,aAAa,EAAG,MAAM,CAAC;IAGvB,cAAc,EAAG,MAAM,CAAC;IAGxB,SAAS,EAAG,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"membership-posting.object.d.ts","sourceRoot":"","sources":["../src/membership-posting.object.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,qBAiBa,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAKd,cAAc,EAAG,MAAM,CAAC;IAGxB,QAAQ,EAAG,MAAM,CAAC;IAGlB,cAAc,EAAG,MAAM,CAAC;IAGxB,UAAU,EAAG,MAAM,CAAC;IAQpB,aAAa,EAAG,MAAM,CAAC;IAGvB,cAAc,EAAG,MAAM,CAAC;IAGxB,SAAS,EAAG,MAAM,CAAC;CACpB"}
@@ -19,6 +19,8 @@ const schema_1 = require("./schema");
19
19
  */
20
20
  let MembershipPosting = class MembershipPosting {
21
21
  uuid;
22
+ // immutable (ADR-0034): a posting never moves to another membership or role after create
23
+ // (a role change is a replace: delete + checked create).
22
24
  membershipUuid;
23
25
  roleUuid;
24
26
  departmentUuid;
@@ -38,11 +40,11 @@ __decorate([
38
40
  __metadata("design:type", String)
39
41
  ], MembershipPosting.prototype, "uuid", void 0);
40
42
  __decorate([
41
- (0, engine_1.Field)({ column: 'mp_membership_uuid', type: 'uuid', required: true }),
43
+ (0, engine_1.Field)({ column: 'mp_membership_uuid', type: 'uuid', required: true, immutable: true }),
42
44
  __metadata("design:type", String)
43
45
  ], MembershipPosting.prototype, "membershipUuid", void 0);
44
46
  __decorate([
45
- (0, engine_1.Field)({ column: 'mp_role_uuid', type: 'uuid', required: true }),
47
+ (0, engine_1.Field)({ column: 'mp_role_uuid', type: 'uuid', required: true, immutable: true }),
46
48
  __metadata("design:type", String)
47
49
  ], MembershipPosting.prototype, "roleUuid", void 0);
48
50
  __decorate([