@astram/identity 0.4.0 → 0.5.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 (172) 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 +49 -16
  22. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  23. package/dist/features/authz/grant-admin.service.js +155 -96
  24. package/dist/features/authz/grant-ceiling.d.ts +52 -1
  25. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  26. package/dist/features/authz/grant-ceiling.js +156 -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 +45 -1
  30. package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
  31. package/dist/features/authz/permission-version.bumper.js +106 -16
  32. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  33. package/dist/features/authz/role-binding.d.ts +36 -0
  34. package/dist/features/authz/role-binding.d.ts.map +1 -0
  35. package/dist/features/authz/role-binding.js +53 -0
  36. package/dist/features/company/company-hierarchy.service.d.ts +46 -0
  37. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
  38. package/dist/features/company/company-hierarchy.service.js +100 -0
  39. package/dist/features/company/company.controller.d.ts +23 -5
  40. package/dist/features/company/company.controller.d.ts.map +1 -1
  41. package/dist/features/company/company.controller.js +47 -12
  42. package/dist/features/company/company.service.d.ts +28 -12
  43. package/dist/features/company/company.service.d.ts.map +1 -1
  44. package/dist/features/company/company.service.js +61 -14
  45. package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
  46. package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
  47. package/dist/features/identity-feature.module.d.ts.map +1 -1
  48. package/dist/features/identity-feature.module.js +60 -3
  49. package/dist/features/membership/membership.controller.d.ts +6 -5
  50. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  51. package/dist/features/membership/membership.controller.js +34 -22
  52. package/dist/features/membership/membership.service.d.ts +20 -14
  53. package/dist/features/membership/membership.service.d.ts.map +1 -1
  54. package/dist/features/membership/membership.service.js +62 -29
  55. package/dist/features/membership/posting-version-bump.d.ts +5 -0
  56. package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
  57. package/dist/features/membership/posting.service.d.ts +20 -24
  58. package/dist/features/membership/posting.service.d.ts.map +1 -1
  59. package/dist/features/membership/posting.service.js +101 -64
  60. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  61. package/dist/features/org-structure/org-catalog.controller.js +2 -0
  62. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  63. package/dist/features/org-structure/org-catalog.service.js +4 -1
  64. package/dist/features/policy/policy.controller.d.ts.map +1 -1
  65. package/dist/features/policy/policy.controller.js +2 -0
  66. package/dist/features/role/role.controller.d.ts +3 -2
  67. package/dist/features/role/role.controller.d.ts.map +1 -1
  68. package/dist/features/role/role.controller.js +15 -10
  69. package/dist/features/role/role.service.d.ts +16 -8
  70. package/dist/features/role/role.service.d.ts.map +1 -1
  71. package/dist/features/role/role.service.js +24 -10
  72. package/dist/features/scope/scope-closure.service.d.ts +44 -4
  73. package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
  74. package/dist/features/scope/scope-closure.service.js +215 -82
  75. package/dist/features/service-account/service-account.controller.d.ts +3 -2
  76. package/dist/features/service-account/service-account.controller.d.ts.map +1 -1
  77. package/dist/features/service-account/service-account.controller.js +29 -15
  78. package/dist/features/service-account/service-account.service.d.ts +14 -7
  79. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  80. package/dist/features/service-account/service-account.service.js +56 -11
  81. package/dist/features/share/share.controller.d.ts.map +1 -1
  82. package/dist/features/share/share.controller.js +2 -0
  83. package/dist/features/share/share.service.d.ts.map +1 -1
  84. package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
  85. package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
  86. package/dist/features/shared/identity-error.interceptor.js +82 -0
  87. package/dist/features/shared/identity-errors.d.ts +23 -0
  88. package/dist/features/shared/identity-errors.d.ts.map +1 -0
  89. package/dist/features/shared/identity-errors.js +41 -0
  90. package/dist/features/user/session-revocation-check.d.ts +11 -0
  91. package/dist/features/user/session-revocation-check.d.ts.map +1 -0
  92. package/dist/features/user/session-revocation-check.js +21 -0
  93. package/dist/features/user/user.controller.d.ts +11 -6
  94. package/dist/features/user/user.controller.d.ts.map +1 -1
  95. package/dist/features/user/user.controller.js +35 -17
  96. package/dist/features/user/user.service.d.ts +55 -14
  97. package/dist/features/user/user.service.d.ts.map +1 -1
  98. package/dist/features/user/user.service.js +143 -16
  99. package/dist/membership-posting.object.d.ts.map +1 -1
  100. package/dist/membership-posting.object.js +4 -2
  101. package/dist/objects.d.ts.map +1 -1
  102. package/dist/objects.js +13 -3
  103. package/dist/role.object.d.ts.map +1 -1
  104. package/dist/role.object.js +3 -0
  105. package/dist/user.object.d.ts.map +1 -1
  106. package/dist/user.object.js +3 -0
  107. package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
  108. package/package.json +4 -4
  109. package/dist/auth-facts-keys.js.map +0 -1
  110. package/dist/branch.object.js.map +0 -1
  111. package/dist/company-branch.object.js.map +0 -1
  112. package/dist/company-department.object.js.map +0 -1
  113. package/dist/company-membership.object.js.map +0 -1
  114. package/dist/company.object.js.map +0 -1
  115. package/dist/cross-tenant-grant.object.js.map +0 -1
  116. package/dist/department.object.js.map +0 -1
  117. package/dist/features/access/access-admin.access.js.map +0 -1
  118. package/dist/features/access/company-admin.access.js.map +0 -1
  119. package/dist/features/access/index.js.map +0 -1
  120. package/dist/features/access/user-management.access.js.map +0 -1
  121. package/dist/features/audit/audit-read.service.js.map +0 -1
  122. package/dist/features/audit/audit.controller.js.map +0 -1
  123. package/dist/features/audit/directory-label-resolver.js.map +0 -1
  124. package/dist/features/audit/mariadb-audit-reader.js.map +0 -1
  125. package/dist/features/authz/access-bundle-reconcile.job.js.map +0 -1
  126. package/dist/features/authz/access-bundle-store.js.map +0 -1
  127. package/dist/features/authz/catalog-bootstrap.provider.js.map +0 -1
  128. package/dist/features/authz/catalog-reconcile.job.js.map +0 -1
  129. package/dist/features/authz/db-permission-facts.adapter.js.map +0 -1
  130. package/dist/features/authz/grant-admin.controller.js.map +0 -1
  131. package/dist/features/authz/grant-admin.service.js.map +0 -1
  132. package/dist/features/authz/grant-ceiling.js.map +0 -1
  133. package/dist/features/authz/mariadb-catalog-store.js.map +0 -1
  134. package/dist/features/authz/permission-version.bumper.js.map +0 -1
  135. package/dist/features/authz/record-authorization.js.map +0 -1
  136. package/dist/features/authz/session-facts.reader.js.map +0 -1
  137. package/dist/features/company/company.controller.js.map +0 -1
  138. package/dist/features/company/company.service.js.map +0 -1
  139. package/dist/features/cross-tenant/cross-tenant-grant-read.service.js.map +0 -1
  140. package/dist/features/cross-tenant/cross-tenant-grant.controller.js.map +0 -1
  141. package/dist/features/cross-tenant/grant-authorize.js.map +0 -1
  142. package/dist/features/identity-feature.module.js.map +0 -1
  143. package/dist/features/membership/membership-checker.js.map +0 -1
  144. package/dist/features/membership/membership.controller.js.map +0 -1
  145. package/dist/features/membership/membership.service.js.map +0 -1
  146. package/dist/features/membership/posting-version-bump.js.map +0 -1
  147. package/dist/features/membership/posting.service.js.map +0 -1
  148. package/dist/features/org-structure/org-catalog.controller.js.map +0 -1
  149. package/dist/features/org-structure/org-catalog.service.js.map +0 -1
  150. package/dist/features/policy/policy.controller.js.map +0 -1
  151. package/dist/features/policy/policy.service.js.map +0 -1
  152. package/dist/features/role/role.controller.js.map +0 -1
  153. package/dist/features/role/role.service.js.map +0 -1
  154. package/dist/features/scope/scope-closure.service.js.map +0 -1
  155. package/dist/features/scope/scope-subtree.resolver.js.map +0 -1
  156. package/dist/features/share/share.controller.js.map +0 -1
  157. package/dist/features/share/share.service.js.map +0 -1
  158. package/dist/features/shared/http.js.map +0 -1
  159. package/dist/features/shared/list-query.js.map +0 -1
  160. package/dist/features/shared/named-option.js.map +0 -1
  161. package/dist/features/shared/record-resolver.js.map +0 -1
  162. package/dist/features/user/user.controller.js.map +0 -1
  163. package/dist/features/user/user.service.js.map +0 -1
  164. package/dist/index.js.map +0 -1
  165. package/dist/membership-posting.object.js.map +0 -1
  166. package/dist/migrations-dir.js.map +0 -1
  167. package/dist/objects.js.map +0 -1
  168. package/dist/record-share.object.js.map +0 -1
  169. package/dist/role.object.js.map +0 -1
  170. package/dist/schema.js.map +0 -1
  171. package/dist/shared-resource.object.js.map +0 -1
  172. package/dist/user.object.js.map +0 -1
@@ -16,8 +16,12 @@ class MariaDbCatalogStore {
16
16
  const rows = (await this.pool.query(`SELECT perm_key, perm_module, perm_type_key, perm_action, perm_label, perm_tier, perm_status
17
17
  FROM \`${AUTHZ}\`.\`permission\``));
18
18
  return rows.map((r) => ({
19
- permissionKey: r.perm_key, moduleKey: r.perm_module, typeKey: r.perm_type_key,
20
- action: r.perm_action, label: r.perm_label, tier: r.perm_tier,
19
+ permissionKey: r.perm_key,
20
+ moduleKey: r.perm_module,
21
+ typeKey: r.perm_type_key,
22
+ action: r.perm_action,
23
+ label: r.perm_label,
24
+ tier: r.perm_tier,
21
25
  status: r.perm_status === 'orphaned' ? 'orphaned' : 'active',
22
26
  }));
23
27
  }
@@ -25,7 +29,15 @@ class MariaDbCatalogStore {
25
29
  const res = (await this.pool.query(`INSERT INTO \`${AUTHZ}\`.\`permission\`
26
30
  (perm_uuid, perm_key, perm_module, perm_type_key, perm_action, perm_label, perm_tier, perm_status)
27
31
  SELECT UUID(), ?, ?, ?, ?, ?, ?, 'active'
28
- WHERE NOT EXISTS (SELECT 1 FROM \`${AUTHZ}\`.\`permission\` WHERE perm_key = ?)`, [row.permissionKey, row.moduleKey, row.typeKey, row.action, row.label, row.tier, row.permissionKey]));
32
+ WHERE NOT EXISTS (SELECT 1 FROM \`${AUTHZ}\`.\`permission\` WHERE perm_key = ?)`, [
33
+ row.permissionKey,
34
+ row.moduleKey,
35
+ row.typeKey,
36
+ row.action,
37
+ row.label,
38
+ row.tier,
39
+ row.permissionKey,
40
+ ]));
29
41
  return Number(res?.affectedRows ?? 0) > 0;
30
42
  }
31
43
  async markOrphaned(keys) {
@@ -20,7 +20,37 @@ export interface Queryable {
20
20
  * - `bumpForRole` wraps a failure in `AuthorizationUnavailableError` and THROWS (see its own
21
21
  * doc comment below) — do not assume it is also fail-safe.
22
22
  */
23
+ /**
24
+ * Where a committed version is published (ADR-0035, GAPS #6) — `@astram/guard`'s
25
+ * `RedisPermissionVersionCache` satisfies it structurally (monotonic, TTL'd, never throws).
26
+ */
27
+ export interface VersionPublisher {
28
+ publish(tenantUuid: string, version: number): Promise<void>;
29
+ }
23
30
  export declare class PermissionVersionBumper {
31
+ private publisher?;
32
+ /**
33
+ * Bumped versions waiting for their transaction to commit, per connection (ADR-0035). A
34
+ * transaction connection is a pooled object reused across requests, so callers either publish
35
+ * (`publishCommitted`, after `commit()`) or drop (`discardPending`, after `rollback()`) them.
36
+ */
37
+ private readonly pending;
38
+ private readonly logger;
39
+ /**
40
+ * @param publisher OPTIONAL Redis publisher (ADR-0035). Absent → DB-only bumps, exactly as
41
+ * before (no read-back). `IdentityFeatureModule` attaches the guard's cache through DI.
42
+ */
43
+ constructor(publisher?: VersionPublisher | undefined);
44
+ /** Attach the Redis publisher once it is resolved through DI (first attach wins). */
45
+ attachVersionPublisher(publisher: VersionPublisher): void;
46
+ /**
47
+ * Publish the versions this transaction connection bumped — call right AFTER `commit()`.
48
+ * Best-effort: a failure is logged (no tenant id) and swallowed; the key TTL bounds the delay.
49
+ */
50
+ publishCommitted(conn: Queryable): Promise<void>;
51
+ /** Drop the versions a ROLLED-BACK transaction bumped (nothing was committed). */
52
+ discardPending(conn: Queryable): void;
53
+ private safePublish;
24
54
  /**
25
55
  * Increment the permission version for a SINGLE tenant — for events that change a
26
56
  * principal's EFFECTIVE grants without touching `role_permission` directly (a
@@ -41,7 +71,21 @@ export declare class PermissionVersionBumper {
41
71
  * so both roll back together).
42
72
  */
43
73
  bumpForRole(roleUuid: string, conn: Queryable): Promise<string[]>;
44
- /** The raw per-tenant increment (INSERT-or-increment). */
74
+ /**
75
+ * Increment the permission version for EVERY tenant where the user has a membership (any status)
76
+ * — for a change to the USER itself that alters its effective grants everywhere (security
77
+ * hardening Phase 3: disabling or re-enabling an account). Returns the bumped tenants. Like
78
+ * `bumpForRole`, a failure is wrapped in `AuthorizationUnavailableError` and THROWS: a silently
79
+ * missed bump would leave a disabled user's cached grants honoured.
80
+ */
81
+ bumpForUser(userUuid: string, conn: Queryable): Promise<string[]>;
82
+ /**
83
+ * The raw per-tenant increment (INSERT-or-increment). With a publisher attached (ADR-0035) the
84
+ * new version is read back on the SAME connection (inside a transaction that is the
85
+ * transaction's own value) and published to Redis AFTER the commit: at once over the autocommit
86
+ * pool, at `publishCommitted(conn)` for a transaction. A read-back failure skips the publish
87
+ * (the TTL bounds it) and never fails the bump.
88
+ */
45
89
  private bumpTenant;
46
90
  }
47
91
  //# sourceMappingURL=permission-version.bumper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"permission-version.bumper.d.ts","sourceRoot":"","sources":["../../../src/features/authz/permission-version.bumper.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,uBAAuB;IAClC;;;;OAIG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvE;;;;;;;;;;;;OAYG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmBvE,0DAA0D;YAC5C,UAAU;CAQzB"}
1
+ {"version":3,"file":"permission-version.bumper.d.ts","sourceRoot":"","sources":["../../../src/features/authz/permission-version.bumper.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;;;;;;;;;;;GAaG;AACH;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D;AAOD,qBAAa,uBAAuB;IAatB,OAAO,CAAC,SAAS,CAAC;IAZ9B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8C;IACtE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAEhE;;;OAGG;gBACiB,SAAS,CAAC,EAAE,gBAAgB,YAAA;IAEhD,qFAAqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI;IAIzD;;;OAGG;IACG,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAOtD,kFAAkF;IAClF,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;YAIvB,WAAW;IAUzB;;;;OAIG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvE;;;;;;;;;;;;OAYG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA2BvE;;;;;;OAMG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAgBvE;;;;;;OAMG;YACW,UAAU;CA4BzB"}
@@ -1,26 +1,64 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PermissionVersionBumper = void 0;
4
+ const common_1 = require("@nestjs/common");
4
5
  const core_1 = require("@astram/core");
5
6
  /** Schema the `authz_permission_version` table lives in (ADR-0011). */
6
7
  const AUTHZ = 'shield_authz';
7
8
  /** Schema the membership/posting tables live in — the join `bumpForRole` walks. */
8
9
  const APP = 'shield_app';
9
- /**
10
- * PermissionVersionBumper — the WRITER side of ADR-0011: increments a tenant's
11
- * `shield_authz.authz_permission_version` so `loadCoarseGrants` marks cached facts stale →
12
- * gate 5 reloads → a role gain/loss (from a posting change) lands WITHOUT re-login. Runs the
13
- * INSERT-or-increment over a supplied connection (the sealed authz pool, or a transaction
14
- * connection so the bump joins the caller's write). Fail-safe: the caller wraps a bump failure
15
- * so it never fails the membership/posting write. Never logs the tenant or the query.
16
- *
17
- * NOTE — the two bump methods do NOT share an error contract:
18
- * - `bumpForTenant` propagates the raw underlying error unwrapped; it is up to ITS caller
19
- * (the Phase 3 membership/posting write) to swallow it fail-safe, per the paragraph above.
20
- * - `bumpForRole` wraps a failure in `AuthorizationUnavailableError` and THROWS (see its own
21
- * doc comment below) — do not assume it is also fail-safe.
22
- */
10
+ /** An autocommit pool (it hands out connections) rather than one transaction's connection. */
11
+ function isAutocommitPool(conn) {
12
+ return typeof conn.getConnection === 'function';
13
+ }
23
14
  class PermissionVersionBumper {
15
+ publisher;
16
+ /**
17
+ * Bumped versions waiting for their transaction to commit, per connection (ADR-0035). A
18
+ * transaction connection is a pooled object reused across requests, so callers either publish
19
+ * (`publishCommitted`, after `commit()`) or drop (`discardPending`, after `rollback()`) them.
20
+ */
21
+ pending = new WeakMap();
22
+ logger = new common_1.Logger('PermissionVersionBumper');
23
+ /**
24
+ * @param publisher OPTIONAL Redis publisher (ADR-0035). Absent → DB-only bumps, exactly as
25
+ * before (no read-back). `IdentityFeatureModule` attaches the guard's cache through DI.
26
+ */
27
+ constructor(publisher) {
28
+ this.publisher = publisher;
29
+ }
30
+ /** Attach the Redis publisher once it is resolved through DI (first attach wins). */
31
+ attachVersionPublisher(publisher) {
32
+ if (this.publisher === undefined)
33
+ this.publisher = publisher;
34
+ }
35
+ /**
36
+ * Publish the versions this transaction connection bumped — call right AFTER `commit()`.
37
+ * Best-effort: a failure is logged (no tenant id) and swallowed; the key TTL bounds the delay.
38
+ */
39
+ async publishCommitted(conn) {
40
+ const versions = this.pending.get(conn);
41
+ if (versions === undefined)
42
+ return;
43
+ this.pending.delete(conn);
44
+ for (const [tenantUuid, version] of versions)
45
+ await this.safePublish(tenantUuid, version);
46
+ }
47
+ /** Drop the versions a ROLLED-BACK transaction bumped (nothing was committed). */
48
+ discardPending(conn) {
49
+ this.pending.delete(conn);
50
+ }
51
+ async safePublish(tenantUuid, version) {
52
+ const publisher = this.publisher;
53
+ if (publisher === undefined)
54
+ return;
55
+ try {
56
+ await publisher.publish(tenantUuid, version);
57
+ }
58
+ catch {
59
+ this.logger.warn('permission version publish failed; cached sessions refresh within the TTL');
60
+ }
61
+ }
24
62
  /**
25
63
  * Increment the permission version for a SINGLE tenant — for events that change a
26
64
  * principal's EFFECTIVE grants without touching `role_permission` directly (a
@@ -48,7 +86,12 @@ class PermissionVersionBumper {
48
86
  FROM \`${APP}\`.\`company_membership\` cm
49
87
  JOIN \`${APP}\`.\`membership_posting\` mp ON mp.mp_membership_uuid = cm.cm_uuid
50
88
  WHERE mp.mp_role_uuid = ? AND cm.cm_is_active = 1`, [roleUuid]));
51
- const tenants = rows.map((r) => r.tenant);
89
+ // Service accounts (ADR-0033) hold roles on `sa_role_ids`, not postings: a role held only by
90
+ // machine principals must still invalidate their tenants' cached grants (security hardening).
91
+ const saRows = (await conn.query(`SELECT DISTINCT sa.sa_tenant_id AS tenant
92
+ FROM \`${APP}\`.\`service_account\` sa
93
+ WHERE JSON_CONTAINS(sa.sa_role_ids, JSON_QUOTE(?))`, [roleUuid]));
94
+ const tenants = [...new Set([...rows, ...saRows].map((r) => r.tenant))];
52
95
  for (const tenant of tenants) {
53
96
  await this.bumpTenant(tenant, conn);
54
97
  }
@@ -58,11 +101,58 @@ class PermissionVersionBumper {
58
101
  throw new core_1.AuthorizationUnavailableError('permission version bump failed', cause);
59
102
  }
60
103
  }
61
- /** The raw per-tenant increment (INSERT-or-increment). */
104
+ /**
105
+ * Increment the permission version for EVERY tenant where the user has a membership (any status)
106
+ * — for a change to the USER itself that alters its effective grants everywhere (security
107
+ * hardening Phase 3: disabling or re-enabling an account). Returns the bumped tenants. Like
108
+ * `bumpForRole`, a failure is wrapped in `AuthorizationUnavailableError` and THROWS: a silently
109
+ * missed bump would leave a disabled user's cached grants honoured.
110
+ */
111
+ async bumpForUser(userUuid, conn) {
112
+ try {
113
+ const rows = (await conn.query(`SELECT DISTINCT cm.cm_company_uuid AS tenant
114
+ FROM \`${APP}\`.\`company_membership\` cm
115
+ WHERE cm.cm_user_uuid = ?`, [userUuid]));
116
+ const tenants = [...new Set(rows.map((r) => r.tenant))];
117
+ for (const tenant of tenants)
118
+ await this.bumpTenant(tenant, conn);
119
+ return tenants;
120
+ }
121
+ catch (cause) {
122
+ throw new core_1.AuthorizationUnavailableError('permission version bump failed', cause);
123
+ }
124
+ }
125
+ /**
126
+ * The raw per-tenant increment (INSERT-or-increment). With a publisher attached (ADR-0035) the
127
+ * new version is read back on the SAME connection (inside a transaction that is the
128
+ * transaction's own value) and published to Redis AFTER the commit: at once over the autocommit
129
+ * pool, at `publishCommitted(conn)` for a transaction. A read-back failure skips the publish
130
+ * (the TTL bounds it) and never fails the bump.
131
+ */
62
132
  async bumpTenant(tenantUuid, conn) {
63
133
  await conn.query(`INSERT INTO \`${AUTHZ}\`.\`authz_permission_version\` (apv_tenant_uuid, apv_version)
64
134
  VALUES (?, 1)
65
135
  ON DUPLICATE KEY UPDATE apv_version = apv_version + 1`, [tenantUuid]);
136
+ if (this.publisher === undefined)
137
+ return;
138
+ let version;
139
+ try {
140
+ const rows = (await conn.query(`SELECT apv_version FROM \`${AUTHZ}\`.\`authz_permission_version\`
141
+ WHERE apv_tenant_uuid = ? LIMIT 1`, [tenantUuid]));
142
+ version = Number(rows[0]?.apv_version);
143
+ }
144
+ catch {
145
+ return;
146
+ }
147
+ if (!Number.isSafeInteger(version) || version < 0)
148
+ return;
149
+ if (isAutocommitPool(conn)) {
150
+ await this.safePublish(tenantUuid, version);
151
+ return;
152
+ }
153
+ const versions = this.pending.get(conn) ?? new Map();
154
+ versions.set(tenantUuid, Math.max(version, versions.get(tenantUuid) ?? 0));
155
+ this.pending.set(conn, versions);
66
156
  }
67
157
  }
68
158
  exports.PermissionVersionBumper = PermissionVersionBumper;
@@ -1 +1 @@
1
- {"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAA6C,KAAK,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1G,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKtD;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAS3B"}
1
+ {"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAGL,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKtD;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAS3B"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Role binding — where a GLOBAL role (`shield_app.roles`) is in use, relative to a caller's subtree.
3
+ *
4
+ * Roles are a global catalog, so one `role_permission` edit changes every tenant that uses the role
5
+ * (review finding #1). Until roles are tenant-owned (D1 follow-up), "may this caller administer the
6
+ * role" is decided by WHERE the role is bound:
7
+ * - a human binding: a `membership_posting` on a `company_membership` of company X;
8
+ * - a machine binding: a `service_account` of tenant X whose `sa_role_ids` holds the role.
9
+ * `inside` = some binding's node is in the caller's subtree; `outside` = some binding's node is not.
10
+ *
11
+ * ONE definition, shared by grant administration (the `subtreeExists` read predicate below and the
12
+ * write rule), role update/delete, and posting creation, via {@link GrantCeiling}. All node ids and
13
+ * the role id are `?`-bound (never interpolated).
14
+ */
15
+ import type { Queryable } from './permission-version.bumper.js';
16
+ /** Where a role is bound relative to a set of subtree nodes. */
17
+ export interface RoleBinding {
18
+ readonly inside: boolean;
19
+ readonly outside: boolean;
20
+ }
21
+ /**
22
+ * The shared EXISTS-subtree fragment for a non-root caller's READS (Task 4.2): a role row `r`
23
+ * qualifies only if it is bound (`membership_posting → company_membership`) to a node in the
24
+ * subtree. The caller splices `params` where `sql` sits in the surrounding query.
25
+ */
26
+ export declare function subtreeExists(nodeIds: readonly string[]): {
27
+ sql: string;
28
+ params: string[];
29
+ };
30
+ /**
31
+ * Read where `roleUuid` is bound relative to `nodeIds` (human postings + service accounts) in ONE
32
+ * query. An empty `nodeIds` is treated as "nothing is inside" (every binding is outside) — fail
33
+ * closed, never an `IN ()` syntax error.
34
+ */
35
+ export declare function readRoleBinding(q: Queryable, roleUuid: string, nodeIds: readonly string[]): Promise<RoleBinding>;
36
+ //# sourceMappingURL=role-binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-binding.d.ts","sourceRoot":"","sources":["../../../src/features/authz/role-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAIhE,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAQ3F;AASD;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,CAAC,EAAE,SAAS,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,OAAO,CAAC,WAAW,CAAC,CAwBtB"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subtreeExists = subtreeExists;
4
+ exports.readRoleBinding = readRoleBinding;
5
+ const APP = 'shield_app';
6
+ /**
7
+ * The shared EXISTS-subtree fragment for a non-root caller's READS (Task 4.2): a role row `r`
8
+ * qualifies only if it is bound (`membership_posting → company_membership`) to a node in the
9
+ * subtree. The caller splices `params` where `sql` sits in the surrounding query.
10
+ */
11
+ function subtreeExists(nodeIds) {
12
+ const ph = nodeIds.map(() => '?').join(', ');
13
+ const sql = ` AND EXISTS (` +
14
+ `SELECT 1 FROM \`${APP}\`.\`membership_posting\` mp` +
15
+ ` JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid` +
16
+ ` WHERE mp.mp_role_uuid = r.r_uuid AND cm.cm_company_uuid IN (${ph}))`;
17
+ return { sql, params: [...nodeIds] };
18
+ }
19
+ /** Coerce a driver aggregate (number | bigint | string | null) to a boolean. */
20
+ function truthy(v) {
21
+ if (v === null || v === undefined)
22
+ return false;
23
+ if (typeof v === 'bigint')
24
+ return v !== 0n;
25
+ return Number(v) > 0;
26
+ }
27
+ /**
28
+ * Read where `roleUuid` is bound relative to `nodeIds` (human postings + service accounts) in ONE
29
+ * query. An empty `nodeIds` is treated as "nothing is inside" (every binding is outside) — fail
30
+ * closed, never an `IN ()` syntax error.
31
+ */
32
+ async function readRoleBinding(q, roleUuid, nodeIds) {
33
+ const ids = nodeIds.length > 0 ? [...nodeIds] : [];
34
+ const inList = ids.length > 0 ? ids.map(() => '?').join(', ') : 'NULL';
35
+ // `x IN (NULL)` is NULL (never true) and `x NOT IN (NULL)` is NULL too, so for an empty list the
36
+ // outside test is written as a plain existence check instead.
37
+ const insideExpr = ids.length > 0 ? `b.node IN (${inList})` : '0';
38
+ const outsideExpr = ids.length > 0 ? `b.node NOT IN (${inList})` : '1';
39
+ const rows = (await q.query(`SELECT COALESCE(MAX(${insideExpr}), 0) AS bound_inside,
40
+ COALESCE(MAX(${outsideExpr}), 0) AS bound_outside
41
+ FROM (
42
+ SELECT cm.cm_company_uuid AS node
43
+ FROM \`${APP}\`.\`membership_posting\` mp
44
+ JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
45
+ WHERE mp.mp_role_uuid = ?
46
+ UNION ALL
47
+ SELECT sa.sa_tenant_id AS node
48
+ FROM \`${APP}\`.\`service_account\` sa
49
+ WHERE JSON_CONTAINS(sa.sa_role_ids, JSON_QUOTE(?))
50
+ ) b`, [...ids, ...ids, roleUuid, roleUuid]));
51
+ const row = rows[0];
52
+ return { inside: truthy(row?.bound_inside), outside: truthy(row?.bound_outside) };
53
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CompanyHierarchyService — the ONLY way a company's place in the tree changes (security hardening
3
+ * #2, D5). The generic company patch can no longer move a company (`parentUuid`/`type` are
4
+ * immutable at the engine, ADR-0034, and the patch is whitelisted); every move, delete and new-node
5
+ * attach goes through `ScopeClosureService`, which owns the cycle check, the closure rebuild and
6
+ * the permission-version bump (ADR-0027 E-1/E-2).
7
+ *
8
+ * Authority (non-root; root bypasses), decided from the session's cached subtree — the SAME
9
+ * `scopeSubtreeNodeIds` the engine's write wall uses:
10
+ * - move: the node is in the actor's subtree, is NOT the actor's own root node (a sub-admin can
11
+ * never re-home its whole tenant), and the new parent is in the actor's subtree and non-null
12
+ * (only root creates or promotes top-level tenants). A cycle → ScopeCycleError (409).
13
+ * - delete: the caller (CompanyService) has already read the node through the subtree wall.
14
+ *
15
+ * Move and delete are audited ON their own transaction (a failed audit rolls them back). Names,
16
+ * never ids, in the audit detail (ADR-0012).
17
+ */
18
+ import type { Principal } from '@astram/core';
19
+ import type { AuditPort } from '@astram/engine';
20
+ import { ScopeClosureService } from '../scope/scope-closure.service.js';
21
+ /** The read surface used for existence/name lookups (the shared authz pool satisfies it). */
22
+ export interface HierarchyQueryable {
23
+ query(sql: string, params?: readonly unknown[]): Promise<unknown>;
24
+ }
25
+ export declare class CompanyHierarchyService {
26
+ private readonly pool;
27
+ private readonly closure;
28
+ private readonly audit?;
29
+ private readonly now;
30
+ constructor(pool: HierarchyQueryable, closure: ScopeClosureService, audit?: AuditPort | undefined, now?: () => number);
31
+ /** The actor's own root node (the active node its subtree hangs from). */
32
+ private ownRoot;
33
+ /** Names of the given companies (unknown → absent). */
34
+ private names;
35
+ private recordAudit;
36
+ /** Re-parent `nodeUuid` under `newParentUuid` (null = make it a top-level tenant; root only). */
37
+ move(actor: Principal, nodeUuid: string, newParentUuid: string | null): Promise<void>;
38
+ /**
39
+ * Delete a company through `deleteNode` (refuses a node with children; closure rows + ancestor
40
+ * bump in one transaction). The caller has already checked reach (walled read).
41
+ */
42
+ remove(actor: Principal, nodeUuid: string): Promise<void>;
43
+ /** A company was just created: give it closure rows now and bump its ancestors (D5). */
44
+ attached(nodeUuid: string): Promise<void>;
45
+ }
46
+ //# sourceMappingURL=company-hierarchy.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"company-hierarchy.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company-hierarchy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAkB,MAAM,mCAAmC,CAAC;AAUxF,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAHH,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,mBAAmB,EAC5B,KAAK,CAAC,EAAE,SAAS,YAAA,EACjB,GAAG,GAAE,MAAM,MAAiB;IAG/C,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAIf,uDAAuD;YACzC,KAAK;YAUL,WAAW;IAoBzB,iGAAiG;IAC3F,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkD3F;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/D,wFAAwF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CompanyHierarchyService = void 0;
4
+ const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
5
+ const identity_errors_js_1 = require("../shared/identity-errors.js");
6
+ const APP = 'shield_app';
7
+ class CompanyHierarchyService {
8
+ pool;
9
+ closure;
10
+ audit;
11
+ now;
12
+ constructor(pool, closure, audit, now = Date.now) {
13
+ this.pool = pool;
14
+ this.closure = closure;
15
+ this.audit = audit;
16
+ this.now = now;
17
+ }
18
+ /** The actor's own root node (the active node its subtree hangs from). */
19
+ ownRoot(actor) {
20
+ return actor.activeScopeNodeId ?? actor.tenantId;
21
+ }
22
+ /** Names of the given companies (unknown → absent). */
23
+ async names(q, ids) {
24
+ const unique = [...new Set(ids)];
25
+ if (unique.length === 0)
26
+ return new Map();
27
+ const rows = (await q.query(`SELECT c_uuid, c_name FROM \`${APP}\`.\`companies\` WHERE c_uuid IN (${unique.map(() => '?').join(', ')})`, unique));
28
+ return new Map(rows.map((r) => [r.c_uuid, r.c_name]));
29
+ }
30
+ async recordAudit(conn, actor, action, attributes) {
31
+ if (!this.audit)
32
+ return;
33
+ await this.audit.record({
34
+ type: 'admin_action',
35
+ occurredAtMs: this.now(),
36
+ actorId: actor.id,
37
+ tenantId: actor.tenantId,
38
+ action: { key: action },
39
+ attributes: { eventType: 'company_hierarchy', ...attributes },
40
+ }, (sql, params) => conn.query(sql, params));
41
+ }
42
+ /** Re-parent `nodeUuid` under `newParentUuid` (null = make it a top-level tenant; root only). */
43
+ async move(actor, nodeUuid, newParentUuid) {
44
+ if (typeof nodeUuid !== 'string' || nodeUuid.length === 0) {
45
+ throw new identity_errors_js_1.IdentityBadRequestError('A company is required.');
46
+ }
47
+ if (newParentUuid !== null &&
48
+ (typeof newParentUuid !== 'string' || newParentUuid.length === 0)) {
49
+ throw new identity_errors_js_1.IdentityBadRequestError('parentUuid must be a company or null.');
50
+ }
51
+ if (actor.isRoot !== true) {
52
+ const subtree = new Set((0, grant_ceiling_js_1.subtreeOf)(actor));
53
+ if (!subtree.has(nodeUuid))
54
+ throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
55
+ if (nodeUuid === this.ownRoot(actor)) {
56
+ throw new identity_errors_js_1.IdentityForbiddenError('You cannot move the organisation your access is rooted at.');
57
+ }
58
+ if (newParentUuid === null || !subtree.has(newParentUuid)) {
59
+ throw new identity_errors_js_1.IdentityForbiddenError('You can only move a company under another company you manage.');
60
+ }
61
+ }
62
+ const names = await this.names(this.pool, [
63
+ nodeUuid,
64
+ ...(newParentUuid ? [newParentUuid] : []),
65
+ ]);
66
+ if (!names.has(nodeUuid))
67
+ throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
68
+ if (newParentUuid !== null && !names.has(newParentUuid)) {
69
+ throw new identity_errors_js_1.IdentityNotFoundError('Parent company not found.');
70
+ }
71
+ const oldParentRows = (await this.pool.query(`SELECT c_parent_uuid FROM \`${APP}\`.\`companies\` WHERE c_uuid = ? LIMIT 1`, [nodeUuid]));
72
+ const oldParent = oldParentRows[0]?.c_parent_uuid ?? null;
73
+ const oldParentName = oldParent
74
+ ? (await this.names(this.pool, [oldParent])).get(oldParent)
75
+ : undefined;
76
+ await this.closure.moveNode(nodeUuid, newParentUuid, (conn) => this.recordAudit(conn, actor, 'company.moved', {
77
+ company: names.get(nodeUuid),
78
+ from: oldParent ? (oldParentName ?? '(deleted organisation)') : '(top level)',
79
+ to: newParentUuid ? names.get(newParentUuid) : '(top level)',
80
+ }));
81
+ }
82
+ /**
83
+ * Delete a company through `deleteNode` (refuses a node with children; closure rows + ancestor
84
+ * bump in one transaction). The caller has already checked reach (walled read).
85
+ */
86
+ async remove(actor, nodeUuid) {
87
+ // Security review F6: the same own-root guard as `move` — a sub-admin never deletes the
88
+ // organisation its access is rooted at.
89
+ if (actor.isRoot !== true && nodeUuid === this.ownRoot(actor)) {
90
+ throw new identity_errors_js_1.IdentityForbiddenError('You cannot delete the organisation your access is rooted at.');
91
+ }
92
+ const name = (await this.names(this.pool, [nodeUuid])).get(nodeUuid);
93
+ await this.closure.deleteNode(nodeUuid, (conn) => this.recordAudit(conn, actor, 'company.deleted', { company: name ?? '(unknown)' }));
94
+ }
95
+ /** A company was just created: give it closure rows now and bump its ancestors (D5). */
96
+ async attached(nodeUuid) {
97
+ await this.closure.attachNode(nodeUuid);
98
+ }
99
+ }
100
+ exports.CompanyHierarchyService = CompanyHierarchyService;
@@ -1,5 +1,6 @@
1
1
  import { CompanyService } from './company.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 CompanyController {
4
5
  private readonly companies;
5
6
  constructor(companies: CompanyService);
@@ -14,23 +15,40 @@ export declare class CompanyController {
14
15
  page: number;
15
16
  limit: number;
16
17
  }>;
17
- /** Create a company (a new tenant) — gated on `identity:company:create`. */
18
- createCompany(body: {
18
+ /**
19
+ * Create a company — gated on `identity:company:create`. A non-root caller's company is created
20
+ * UNDER the caller's own organisation (D5); only root creates a top-level tenant.
21
+ */
22
+ createCompany(req: AuthedRequest, body: {
19
23
  name?: string;
20
24
  }): Promise<{
21
25
  id: import("packages/engine/dist/types.js").RecordId;
22
26
  }>;
23
27
  /** Read one company — gated on `identity:company:read`. */
24
28
  getCompany(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
25
- /** Update a company — gated on `identity:company:update`. */
29
+ /**
30
+ * Move a company under a new parent — gated on `identity:company:update`. Body
31
+ * `{ parentUuid: string | null }`. Non-root: the company and the new parent must both be in the
32
+ * caller's subtree, the company is not the caller's own root, and the parent is not null. A move
33
+ * that would form a cycle → 409. Declared BEFORE `companies/:uuid` (static sub-path first).
34
+ */
35
+ moveCompany(req: AuthedRequest, uuid: string, body: {
36
+ parentUuid?: string | null;
37
+ }): Promise<{
38
+ success: boolean;
39
+ }>;
40
+ /**
41
+ * Update a company's name/status — gated on `identity:company:update`. Any other key
42
+ * (parentUuid, type, …) is ignored: use `PUT companies/:uuid/parent` to move a company.
43
+ */
26
44
  updateCompany(uuid: string, body: {
27
45
  name?: string;
28
46
  status?: string;
29
47
  }): Promise<{
30
48
  success: boolean;
31
49
  }>;
32
- /** Delete a company — gated on `identity:company:delete`. */
33
- deleteCompany(uuid: string): Promise<{
50
+ /** Delete a company — gated on `identity:company:delete`. A company with sub-companies → 409. */
51
+ deleteCompany(req: AuthedRequest, uuid: string): Promise<{
34
52
  success: boolean;
35
53
  }>;
36
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"company.controller.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.controller.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5E,qBACa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,cAAc;IAEtD;;;;OAIG;IAGG,aAAa,CAAU,KAAK,EAAE,YAAY;;;;;;IAMhD,4EAA4E;IAGtE,aAAa,CAAS,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKnD,2DAA2D;IAG3D,UAAU,CAAgB,IAAI,EAAE,MAAM;IAItC,6DAA6D;IAGvD,aAAa,CAAgB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKjG,6DAA6D;IAGvD,aAAa,CAAgB,IAAI,EAAE,MAAM;;;CAIhD"}
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,CACF,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMlD,iGAAiG;IAG3F,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAI3E"}