@astram/identity 0.6.0 → 0.7.1

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 (50) hide show
  1. package/README.md +15 -0
  2. package/dist/bin/sa-keys-report.d.ts +3 -0
  3. package/dist/bin/sa-keys-report.d.ts.map +1 -0
  4. package/dist/bin/sa-keys-report.js +18 -0
  5. package/dist/features/authz/grant-admin.service.d.ts +22 -5
  6. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  7. package/dist/features/authz/grant-admin.service.js +45 -10
  8. package/dist/features/authz/grant-ceiling.d.ts +19 -6
  9. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  10. package/dist/features/authz/grant-ceiling.js +36 -8
  11. package/dist/features/authz/record-authorization.d.ts +15 -32
  12. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  13. package/dist/features/authz/record-authorization.js +15 -69
  14. package/dist/features/company/company-hierarchy.service.d.ts +13 -1
  15. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
  16. package/dist/features/company/company-hierarchy.service.js +36 -9
  17. package/dist/features/company/company.controller.d.ts +1 -1
  18. package/dist/features/company/company.controller.d.ts.map +1 -1
  19. package/dist/features/company/company.controller.js +9 -5
  20. package/dist/features/company/company.service.d.ts +9 -2
  21. package/dist/features/company/company.service.d.ts.map +1 -1
  22. package/dist/features/company/company.service.js +18 -2
  23. package/dist/features/identity-feature.module.d.ts +15 -8
  24. package/dist/features/identity-feature.module.d.ts.map +1 -1
  25. package/dist/features/identity-feature.module.js +75 -28
  26. package/dist/features/membership/posting.service.d.ts +2 -0
  27. package/dist/features/membership/posting.service.d.ts.map +1 -1
  28. package/dist/features/membership/posting.service.js +9 -0
  29. package/dist/features/role/role.service.d.ts +14 -7
  30. package/dist/features/role/role.service.d.ts.map +1 -1
  31. package/dist/features/role/role.service.js +15 -8
  32. package/dist/features/service-account/lock-service-accounts.d.ts +29 -0
  33. package/dist/features/service-account/lock-service-accounts.d.ts.map +1 -0
  34. package/dist/features/service-account/lock-service-accounts.js +35 -0
  35. package/dist/features/service-account/no-expiry-key-report.d.ts +39 -0
  36. package/dist/features/service-account/no-expiry-key-report.d.ts.map +1 -0
  37. package/dist/features/service-account/no-expiry-key-report.js +91 -0
  38. package/dist/features/shared/http.d.ts +6 -0
  39. package/dist/features/shared/http.d.ts.map +1 -1
  40. package/dist/features/shared/http.js +9 -0
  41. package/dist/features/user/user.service.d.ts +4 -4
  42. package/dist/features/user/user.service.d.ts.map +1 -1
  43. package/dist/features/user/user.service.js +9 -6
  44. package/dist/index.d.ts +1 -11
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +8 -12
  47. package/dist/internal.d.ts +3 -2
  48. package/dist/internal.d.ts.map +1 -1
  49. package/dist/internal.js +3 -2
  50. package/package.json +7 -4
package/README.md CHANGED
@@ -14,3 +14,18 @@ destination for the identity model currently declared in consuming applications
14
14
  once relocated, apps declare only role-maps, extra columns, and session
15
15
  extensions, while the canonical identity objects, migrations, and resolver live
16
16
  here in the framework.
17
+
18
+ ## `astram-sa-keys-report`
19
+
20
+ Service-account keys issued before an expiry became mandatory never expire. This read-only command
21
+ lists each agent's current, unrevoked key with no expiry (agent label, company name, status, key
22
+ version, issue date; never a secret, hash or id), so you can rotate it with an expiry:
23
+
24
+ ```sh
25
+ APP_DB_HOST=… APP_DB_PORT=… APP_DB_USER=… APP_DB_PASSWORD=… APP_DB_NAME=shield_app \
26
+ npx astram-sa-keys-report # exit 0
27
+ npx astram-sa-keys-report --fail-on-found # exit 1 if any key is listed (CI)
28
+ ```
29
+
30
+ It uses the same `APP_DB_*` variables as the identity module's authz pool. Exit code 2 means it
31
+ could not run (bad option, no database).
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=sa-keys-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sa-keys-report.d.ts","sourceRoot":"","sources":["../../src/bin/sa-keys-report.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * `astram-sa-keys-report` — list service-account keys that never expire (security hardening
6
+ * Phase 8 #13 / Phase 9 #8). Read-only; connects with the identity authz pool's `APP_DB_*` env.
7
+ * `--fail-on-found` exits 1 when any key is listed. Logic: `no-expiry-key-report.ts`.
8
+ */
9
+ const record_authorization_js_1 = require("../features/authz/record-authorization.js");
10
+ const no_expiry_key_report_js_1 = require("../features/service-account/no-expiry-key-report.js");
11
+ void (0, no_expiry_key_report_js_1.runNonExpiringKeyReport)({
12
+ argv: process.argv.slice(2),
13
+ openDb: record_authorization_js_1.createIdentityAuthzPool,
14
+ out: (text) => process.stdout.write(text),
15
+ err: (text) => process.stderr.write(text),
16
+ }).then((code) => {
17
+ process.exitCode = code;
18
+ });
@@ -133,13 +133,30 @@ export declare class GrantAdminService {
133
133
  * 4. audit as `role.permission_revoked` (`roleDeleted: true`), commit, publish the versions.
134
134
  * Returns the bumped tenants.
135
135
  *
136
- * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection and
137
- * has already run the binding rule and user-holder dominance; this method therefore does NOT
138
- * lock the role row (that would wait on the caller). It commits BEFORE the caller deletes the
139
- * role, so a failure in between leaves a role with no grants and no agents: fewer permissions,
140
- * never live grants for a deleted role (fail closed).
136
+ * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection,
137
+ * has already run the binding rule and user-holder dominance, and has already run the engine's
138
+ * delete (gate included, Phase 9 #5) on that still-open transaction; this method therefore does
139
+ * NOT lock the role row (that would wait on the caller) and reads only committed state. It
140
+ * commits BEFORE the caller does, so a failure in between leaves a role with no grants and no
141
+ * agents: fewer permissions, never live grants for a deleted role (fail closed).
141
142
  */
142
143
  purgeRole(roleUuid: string, actor: Principal): Promise<string[]>;
144
+ /**
145
+ * The service accounts holding `roleUuid`, read with a row lock on the transaction `conn` (a
146
+ * concurrent edit of one of them waits until this transaction ends). Phase 9 #11: only those
147
+ * rows are locked, never the whole table (`lockServiceAccountsWhere`). An agent that gains the
148
+ * role after the candidate read is not seen; the outcome equals "this change, then that edit"
149
+ * (a role delete leaves that agent an inert uuid: the role and its grants are gone).
150
+ */
151
+ /**
152
+ * The role's uuid as stored (Phase 10 #1): `r_uuid` is `utf8mb4_unicode_ci`, so a route uuid in
153
+ * another letter case reaches the row, but `JSON_CONTAINS(sa_role_ids, …)` and the `sa_role_ids`
154
+ * rewrite are exact — an upper-cased uuid would find no holding agent (skipping their check) and
155
+ * write grants under a second spelling. Unknown → the input unchanged (later steps 404). A
156
+ * consistent read: `purgeRole` runs while the caller's delete of this row is uncommitted.
157
+ */
158
+ private storedRoleUuid;
159
+ private lockAccountsHoldingRole;
143
160
  /** All active bundles + their resolved atomic keys, for the Access UI. */
144
161
  listBundles(): Promise<{
145
162
  bundles: BundleView[];
@@ -1 +1 @@
1
- {"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.
7
7
  const grant_ceiling_js_1 = require("./grant-ceiling.js");
8
8
  const role_binding_js_1 = require("./role-binding.js");
9
9
  const identity_errors_js_1 = require("../shared/identity-errors.js");
10
+ const lock_service_accounts_js_1 = require("../service-account/lock-service-accounts.js");
10
11
  const AUTHZ = 'shield_authz';
11
12
  const APP = 'shield_app';
12
13
  /** The grant-admin actions that REMOVE authority from a role's holders (Phase 8 #2). */
@@ -153,8 +154,13 @@ class GrantAdminService {
153
154
  grant_ceiling_js_1.GrantCeiling.checkAdministrable(await (0, role_binding_js_1.readRoleBinding)(conn, roleUuid, (0, grant_ceiling_js_1.subtreeOf)(actor)));
154
155
  // Phase 8 #2: a revocation strips authority from every holder of the role, so the actor
155
156
  // must dominate each of them — checked under the role lock (no holder can appear unseen).
157
+ // Phase 9 #9: that includes the service accounts holding the role, judged as a role
158
+ // delete judges them (each account's tenant in the actor's subtree, the role held by the
159
+ // actor), over the accounts read under a row lock.
156
160
  if (REVOKE_ACTIONS.has(action)) {
157
161
  await this.ceiling.assertDominatesRoleHolders(actor, roleUuid);
162
+ const accounts = await this.lockAccountsHoldingRole(conn, roleUuid);
163
+ await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id), 'revoke');
158
164
  }
159
165
  }
160
166
  for (const key of keys)
@@ -236,6 +242,7 @@ class GrantAdminService {
236
242
  * (404/403) → ceiling (403) → write. Root bypasses the binding rule and the ceiling.
237
243
  */
238
244
  async grant(roleUuid, key, grantor) {
245
+ roleUuid = await this.storedRoleUuid(roleUuid);
239
246
  await this.assertValidKey(key); // throws BadKeyError (400) before touching the store
240
247
  await this.assertMayChangeGrants(roleUuid, [key], grantor);
241
248
  const actorUuid = grantor.id;
@@ -257,6 +264,7 @@ class GrantAdminService {
257
264
  * role lock in `writeGrants`). Root bypasses.
258
265
  */
259
266
  async revoke(roleUuid, key, actor) {
267
+ roleUuid = await this.storedRoleUuid(roleUuid);
260
268
  await this.assertMayChangeGrants(roleUuid, [key], actor);
261
269
  await this.writeGrants('role.permission_revoked', roleUuid, [key], actor, async (conn, k) => {
262
270
  await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, k]);
@@ -277,22 +285,21 @@ class GrantAdminService {
277
285
  * 4. audit as `role.permission_revoked` (`roleDeleted: true`), commit, publish the versions.
278
286
  * Returns the bumped tenants.
279
287
  *
280
- * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection and
281
- * has already run the binding rule and user-holder dominance; this method therefore does NOT
282
- * lock the role row (that would wait on the caller). It commits BEFORE the caller deletes the
283
- * role, so a failure in between leaves a role with no grants and no agents: fewer permissions,
284
- * never live grants for a deleted role (fail closed).
288
+ * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection,
289
+ * has already run the binding rule and user-holder dominance, and has already run the engine's
290
+ * delete (gate included, Phase 9 #5) on that still-open transaction; this method therefore does
291
+ * NOT lock the role row (that would wait on the caller) and reads only committed state. It
292
+ * commits BEFORE the caller does, so a failure in between leaves a role with no grants and no
293
+ * agents: fewer permissions, never live grants for a deleted role (fail closed).
285
294
  */
286
295
  async purgeRole(roleUuid, actor) {
296
+ roleUuid = await this.storedRoleUuid(roleUuid);
287
297
  const conn = await this.pool.getConnection();
288
298
  try {
289
299
  await conn.beginTransaction();
290
- const accounts = (await conn.query(`SELECT sa_id, sa_tenant_id, sa_role_ids, sa_updated_at_ms
291
- FROM \`${APP}\`.\`service_account\`
292
- WHERE JSON_CONTAINS(sa_role_ids, JSON_QUOTE(?))
293
- FOR UPDATE`, [roleUuid]));
300
+ const accounts = await this.lockAccountsHoldingRole(conn, roleUuid);
294
301
  if (this.ceiling && actor.isRoot !== true) {
295
- await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id));
302
+ await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id), 'delete');
296
303
  }
297
304
  const keys = (await conn.query(`SELECT rp_perm_key FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid])).map((r) => r.rp_perm_key);
298
305
  const bumped = await this.bumper.bumpForRole(roleUuid, conn);
@@ -325,6 +332,32 @@ class GrantAdminService {
325
332
  await conn.release();
326
333
  }
327
334
  }
335
+ /**
336
+ * The service accounts holding `roleUuid`, read with a row lock on the transaction `conn` (a
337
+ * concurrent edit of one of them waits until this transaction ends). Phase 9 #11: only those
338
+ * rows are locked, never the whole table (`lockServiceAccountsWhere`). An agent that gains the
339
+ * role after the candidate read is not seen; the outcome equals "this change, then that edit"
340
+ * (a role delete leaves that agent an inert uuid: the role and its grants are gone).
341
+ */
342
+ /**
343
+ * The role's uuid as stored (Phase 10 #1): `r_uuid` is `utf8mb4_unicode_ci`, so a route uuid in
344
+ * another letter case reaches the row, but `JSON_CONTAINS(sa_role_ids, …)` and the `sa_role_ids`
345
+ * rewrite are exact — an upper-cased uuid would find no holding agent (skipping their check) and
346
+ * write grants under a second spelling. Unknown → the input unchanged (later steps 404). A
347
+ * consistent read: `purgeRole` runs while the caller's delete of this row is uncommitted.
348
+ */
349
+ async storedRoleUuid(roleUuid) {
350
+ if (typeof roleUuid !== 'string' || roleUuid.length === 0)
351
+ return roleUuid;
352
+ const rows = (await this.pool.query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_uuid = ? LIMIT 1`, [roleUuid]));
353
+ return rows[0]?.r_uuid ?? roleUuid;
354
+ }
355
+ lockAccountsHoldingRole(conn, roleUuid) {
356
+ return (0, lock_service_accounts_js_1.lockServiceAccountsWhere)(conn, 'sa_id, sa_tenant_id, sa_role_ids, sa_updated_at_ms', {
357
+ sql: 'JSON_CONTAINS(sa_role_ids, JSON_QUOTE(?))',
358
+ params: [roleUuid],
359
+ });
360
+ }
328
361
  // ── Access bundles ────────────────────────────────────────────────────────
329
362
  /** All active bundles + their resolved atomic keys, for the Access UI. */
330
363
  async listBundles() {
@@ -344,6 +377,7 @@ class GrantAdminService {
344
377
  * to; otherwise `GrantCeilingError` (403), checked before any write. Root bypasses.
345
378
  */
346
379
  async grantBundle(roleUuid, bundleKey, grantor) {
380
+ roleUuid = await this.storedRoleUuid(roleUuid);
347
381
  const keys = await this.resolveBundleKeysOrThrow(bundleKey);
348
382
  // Binding rule + ADR-0029 ceiling: a non-root grantor must hold EVERY atomic key the bundle
349
383
  // expands to — checked before any write (no partial bundle escalation).
@@ -367,6 +401,7 @@ class GrantAdminService {
367
401
  * #1: the binding rule + "only revoke what you hold" apply (root bypasses).
368
402
  */
369
403
  async revokeBundle(roleUuid, bundleKey, actor) {
404
+ roleUuid = await this.storedRoleUuid(roleUuid);
370
405
  const keys = await this.resolveBundleKeysOrThrow(bundleKey);
371
406
  await this.assertMayChangeGrants(roleUuid, [...keys], actor);
372
407
  await this.writeGrants('role.bundle_revoked', roleUuid, keys, actor, async (conn, key) => {
@@ -107,10 +107,9 @@ export declare class GrantCeiling {
107
107
  * {@link assertDominates} the actor would need to change any one of them directly. Holders are
108
108
  * the users with a posting of the role in any membership and any window (potential authority).
109
109
  * The actor itself is skipped: giving up your own authority stays allowed (self-demotion).
110
- * Service-account holders: a revoke is covered by the binding rule (`assertRoleAdministrable`:
111
- * no binding outside the actor's subtree) plus "only revoke what you hold"; a role DELETE also
112
- * strips the role from each account and checks {@link assertMayStripRoleFromServiceAccounts}.
113
- * Root bypasses. Store outage → 503.
110
+ * Service-account holders are checked separately by {@link assertMayStripRoleFromServiceAccounts}
111
+ * (a role delete strips the role from each account; a revoke narrows what each account holds —
112
+ * Phase 9 #9). Root bypasses. Store outage → 503.
114
113
  *
115
114
  * Callers serialise against a concurrent posting by locking the role row first (the posting
116
115
  * path locks the same row before it inserts), then call this — so no holder appears unseen.
@@ -123,9 +122,10 @@ export declare class GrantCeiling {
123
122
  * {@link ceilingOf}): each account's tenant is inside the actor's subtree, and the role is one
124
123
  * the actor holds itself — a non-root admin's edit preserves (cannot strip) a role it does not
125
124
  * hold. `accountTenantIds` are the holding accounts' tenants, read by the caller under a row lock.
126
- * Root bypasses. Refusals name no account. Store outage → 503.
125
+ * Phase 9 #9: a grant revoke (`operation: 'revoke'`) narrows every holding account the same way
126
+ * and is judged by the same rule. Root bypasses. Refusals name no account. Store outage → 503.
127
127
  */
128
- assertMayStripRoleFromServiceAccounts(actor: Principal, roleUuid: string, accountTenantIds: readonly string[]): Promise<void>;
128
+ assertMayStripRoleFromServiceAccounts(actor: Principal, roleUuid: string, accountTenantIds: readonly string[], operation?: 'delete' | 'revoke'): Promise<void>;
129
129
  /**
130
130
  * Assert the actor may RETIRE (disable + revoke the keys of) each of these service accounts
131
131
  * (Phase 8 #3: a company delete retires the deleted company's own agents). Judged by the SAME
@@ -140,6 +140,19 @@ export declare class GrantCeiling {
140
140
  readonly scopeSubtreeNodeIds: readonly string[];
141
141
  readonly roleIds: readonly string[];
142
142
  }>): Promise<void>;
143
+ /**
144
+ * Assert every department and branch a posting names belongs to the membership's company
145
+ * (Phase 9 #13b, review F4): a `company_departments` / `company_branches` junction row links it
146
+ * to `companyUuid`. Otherwise a posting could carry another tenant's department or branch.
147
+ * Applies to every actor, root included (it is the data's shape, not a privilege). A missing
148
+ * company (`null`) admits none. A foreign and a nonexistent unit give the same 400. Read through
149
+ * the shared authz pool, not the engine: the posting admin need not hold `company_department` /
150
+ * `company_branch` read. Store outage → 503.
151
+ */
152
+ assertOrgUnitsInCompany(companyUuid: string | null, units: {
153
+ readonly departmentUuids: readonly string[];
154
+ readonly branchUuids: readonly string[];
155
+ }): Promise<void>;
143
156
  /** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
144
157
  roleBinding(actor: Principal, roleUuid: string): Promise<RoleBinding>;
145
158
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ exports.GrantCeiling = exports.GrantCeilingError = void 0;
4
4
  exports.subtreeOf = subtreeOf;
5
5
  exports.coversKey = coversKey;
6
6
  const core_1 = require("@astram/core");
7
+ const http_js_1 = require("../shared/http.js");
7
8
  const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.js");
8
9
  const role_binding_js_1 = require("./role-binding.js");
9
10
  const identity_errors_js_1 = require("../shared/identity-errors.js");
@@ -192,7 +193,7 @@ class GrantCeiling {
192
193
  async assertDominates(actor, targetUserId) {
193
194
  if (actor.isRoot === true)
194
195
  return;
195
- if (this.rootIds.includes(targetUserId)) {
196
+ if (this.rootIds.some((r) => (0, http_js_1.sameUuid)(r, targetUserId))) {
196
197
  throw new identity_errors_js_1.IdentityForbiddenError('You cannot change this user: they are a platform administrator.');
197
198
  }
198
199
  const subtree = new Set(subtreeOf(actor));
@@ -236,10 +237,9 @@ class GrantCeiling {
236
237
  * {@link assertDominates} the actor would need to change any one of them directly. Holders are
237
238
  * the users with a posting of the role in any membership and any window (potential authority).
238
239
  * The actor itself is skipped: giving up your own authority stays allowed (self-demotion).
239
- * Service-account holders: a revoke is covered by the binding rule (`assertRoleAdministrable`:
240
- * no binding outside the actor's subtree) plus "only revoke what you hold"; a role DELETE also
241
- * strips the role from each account and checks {@link assertMayStripRoleFromServiceAccounts}.
242
- * Root bypasses. Store outage → 503.
240
+ * Service-account holders are checked separately by {@link assertMayStripRoleFromServiceAccounts}
241
+ * (a role delete strips the role from each account; a revoke narrows what each account holds —
242
+ * Phase 9 #9). Root bypasses. Store outage → 503.
243
243
  *
244
244
  * Callers serialise against a concurrent posting by locking the role row first (the posting
245
245
  * path locks the same row before it inserts), then call this — so no holder appears unseen.
@@ -276,9 +276,10 @@ class GrantCeiling {
276
276
  * {@link ceilingOf}): each account's tenant is inside the actor's subtree, and the role is one
277
277
  * the actor holds itself — a non-root admin's edit preserves (cannot strip) a role it does not
278
278
  * hold. `accountTenantIds` are the holding accounts' tenants, read by the caller under a row lock.
279
- * Root bypasses. Refusals name no account. Store outage → 503.
279
+ * Phase 9 #9: a grant revoke (`operation: 'revoke'`) narrows every holding account the same way
280
+ * and is judged by the same rule. Root bypasses. Refusals name no account. Store outage → 503.
280
281
  */
281
- async assertMayStripRoleFromServiceAccounts(actor, roleUuid, accountTenantIds) {
282
+ async assertMayStripRoleFromServiceAccounts(actor, roleUuid, accountTenantIds, operation = 'delete') {
282
283
  if (actor.isRoot === true || accountTenantIds.length === 0)
283
284
  return;
284
285
  const admin = {
@@ -287,7 +288,9 @@ class GrantCeiling {
287
288
  scopeSubtreeNodeIds: subtreeOf(actor),
288
289
  isRoot: false,
289
290
  };
290
- const refuse = () => new identity_errors_js_1.IdentityForbiddenError('You cannot delete this role: it is assigned to an agent you cannot change.');
291
+ const refuse = () => new identity_errors_js_1.IdentityForbiddenError(operation === 'revoke'
292
+ ? 'You cannot change this role: it is assigned to an agent you cannot change.'
293
+ : 'You cannot delete this role: it is assigned to an agent you cannot change.');
291
294
  const nodes = (0, service_account_service_js_1.ceilingOf)(admin, []);
292
295
  if (accountTenantIds.some((t) => !(0, service_account_service_js_1.ceilingAllowsNode)(nodes, t)))
293
296
  throw refuse();
@@ -327,6 +330,31 @@ class GrantCeiling {
327
330
  throw refuse();
328
331
  }
329
332
  }
333
+ /**
334
+ * Assert every department and branch a posting names belongs to the membership's company
335
+ * (Phase 9 #13b, review F4): a `company_departments` / `company_branches` junction row links it
336
+ * to `companyUuid`. Otherwise a posting could carry another tenant's department or branch.
337
+ * Applies to every actor, root included (it is the data's shape, not a privilege). A missing
338
+ * company (`null`) admits none. A foreign and a nonexistent unit give the same 400. Read through
339
+ * the shared authz pool, not the engine: the posting admin need not hold `company_department` /
340
+ * `company_branch` read. Store outage → 503.
341
+ */
342
+ async assertOrgUnitsInCompany(companyUuid, units) {
343
+ const check = async (ids, table, companyCol, unitCol, message) => {
344
+ const wanted = [...new Set(ids)];
345
+ if (wanted.length === 0)
346
+ return;
347
+ if (companyUuid === null)
348
+ throw new identity_errors_js_1.IdentityBadRequestError(message);
349
+ const rows = await this.facts('company org units', async () => (await this.pool.query(`SELECT ${unitCol} AS unit FROM \`${APP}\`.\`${table}\`
350
+ WHERE ${companyCol} = ? AND ${unitCol} IN (${wanted.map(() => '?').join(', ')})`, [companyUuid, ...wanted])));
351
+ const linked = new Set(rows.map((r) => r.unit));
352
+ if (wanted.some((u) => !linked.has(u)))
353
+ throw new identity_errors_js_1.IdentityBadRequestError(message);
354
+ };
355
+ await check(units.departmentUuids, 'company_departments', 'cd_company_uuid', 'cd_department_uuid', 'The department is not one of this company’s departments.');
356
+ await check(units.branchUuids, 'company_branches', 'cb_company_uuid', 'cb_branch_uuid', 'The branch is not one of this company’s branches.');
357
+ }
330
358
  /** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
331
359
  async roleBinding(actor, roleUuid) {
332
360
  return this.facts('role binding', () => (0, role_binding_js_1.readRoleBinding)(this.pool, roleUuid, subtreeOf(actor)));
@@ -1,52 +1,35 @@
1
1
  /**
2
- * Assemble the record-tier authorization wiring for `@astram/identity`: the lean grant-read
3
- * pool factory + the `recordAuthorization` inputs the consumer passes to
4
- * `ShieldModule.forRoot({ recordAuthorization })`. Once wired, `decide()` becomes REAL (the
5
- * framework's four record-tier adapters are built internally over the supplied pool) instead
6
- * of the deny-everything stub.
2
+ * The record-tier authorization inputs for `@astram/identity`: the lean grant-read pool factory and
3
+ * the `RecordAuthorizationInputs` builder. `IdentityFeatureModule.forRoot({ extraObjects })` builds
4
+ * the inputs ONCE and uses them everywhere (security hardening Phase 9 #7):
5
+ * - provided app-wide under guard's `SHIELD_RECORD_AUTHORIZATION`, so `ShieldModule` builds the
6
+ * request path's decision authority from them (the framework's four record-tier adapters are
7
+ * built inside guard);
8
+ * - the module's own cross-tenant / policy authority, list scope and grant ceiling.
9
+ * One registry, one source of `extraObjects`: nothing can disagree.
7
10
  *
8
11
  * Modeled on astram3's `buildAuthorizationInputs` (`server/src/features/authz/authz.wiring.ts`),
9
- * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix
10
- * (`APP_DB_*`, per the Task-7 rename).
12
+ * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix (`APP_DB_*`).
11
13
  */
12
14
  import { type Pool } from 'mariadb';
13
15
  import { type RecordAuthorizationInputs } from '@astram/guard';
14
16
  import type { ObjectTypeInput } from '@astram/engine';
15
- import type { ModuleRegistry } from '@astram/core';
16
17
  /**
17
18
  * A lean `mariadb` pool for the authz grant-reads (built from `APP_DB_*` env via guard's
18
19
  * `loadDbConfig`). Kept small — these are pre-gate coarse-grant reads, not the main CRUD pool.
19
- * Opens the pool at CALL time (never at import) — the consumer (app lifecycle) owns closing it.
20
+ * Opens the pool at CALL time (never at import) — the caller owns closing it.
20
21
  */
21
22
  export declare function createIdentityAuthzPool(): Pool;
22
23
  /**
23
- * Build the `RecordAuthorizationInputs` the consumer passes to
24
- * `ShieldModule.forRoot({ recordAuthorization })`. The framework owns the four record-tier
25
- * adapters internally; `@astram/identity` supplies only the app-schema ports:
24
+ * Build the `RecordAuthorizationInputs` over `pool`. `@astram/identity` supplies only the
25
+ * app-schema ports:
26
26
  * - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
27
27
  * `company_membership`/`membership_posting`,
28
28
  * - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
29
- * - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
29
+ * - `registry` — bridged from `identityObjects` PLUS the consumer's `extraObjects` (their
30
+ * `<module>:<type>:<action>` keys project into a principal's grants at gate 5; without them a
31
+ * consumer type is `resource_not_registered`),
30
32
  * - `operationManifest` — empty (identity declares no trusted delegated operations).
31
- *
32
- * `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
33
- * `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
34
- * coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
35
- * and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
36
- * never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
37
- * the same object types the app registered with the engine closes that gap: the app's grants on
38
- * those keys now project, and the framework still owns the four record-tier adapters + scope fn.
39
- */
40
- export declare function buildIdentityRecordAuthorization(pool: Pool, extraObjects?: readonly ObjectTypeInput[]): RecordAuthorizationInputs;
41
- /**
42
- * The inputs builder WITHOUT the boot parity record — for the identity module's own internal
43
- * authorities (they are not the decision path the consumer hands `ShieldModule`).
44
33
  */
45
34
  export declare function buildRecordAuthorizationInputs(pool: Pool, extraObjects?: readonly ObjectTypeInput[]): RecordAuthorizationInputs;
46
- /** The boot error: the two registries' action-key sets differ (names the first differing keys). */
47
- export declare class IdentityRegistryMismatchError extends Error {
48
- constructor(onlyDecision: readonly string[], onlyCeiling: readonly string[]);
49
- }
50
- /** Called by `IdentityFeatureModule.forRoot` with the grant ceiling's registry (Phase 8 #4). */
51
- export declare function noteIdentityCeilingRegistry(registry: ModuleRegistry): void;
52
35
  //# sourceMappingURL=record-authorization.d.ts.map
@@ -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,EAGL,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAI3B;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAS3B;AAmBD,mGAAmG;AACnG,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,YAAY,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE;CAW5E;AAoBD,gGAAgG;AAChG,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAE1E"}
1
+ {"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;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;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAS3B"}
@@ -3,21 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.IdentityRegistryMismatchError = void 0;
7
6
  exports.createIdentityAuthzPool = createIdentityAuthzPool;
8
- exports.buildIdentityRecordAuthorization = buildIdentityRecordAuthorization;
9
7
  exports.buildRecordAuthorizationInputs = buildRecordAuthorizationInputs;
10
- exports.noteIdentityCeilingRegistry = noteIdentityCeilingRegistry;
11
8
  /**
12
- * Assemble the record-tier authorization wiring for `@astram/identity`: the lean grant-read
13
- * pool factory + the `recordAuthorization` inputs the consumer passes to
14
- * `ShieldModule.forRoot({ recordAuthorization })`. Once wired, `decide()` becomes REAL (the
15
- * framework's four record-tier adapters are built internally over the supplied pool) instead
16
- * of the deny-everything stub.
9
+ * The record-tier authorization inputs for `@astram/identity`: the lean grant-read pool factory and
10
+ * the `RecordAuthorizationInputs` builder. `IdentityFeatureModule.forRoot({ extraObjects })` builds
11
+ * the inputs ONCE and uses them everywhere (security hardening Phase 9 #7):
12
+ * - provided app-wide under guard's `SHIELD_RECORD_AUTHORIZATION`, so `ShieldModule` builds the
13
+ * request path's decision authority from them (the framework's four record-tier adapters are
14
+ * built inside guard);
15
+ * - the module's own cross-tenant / policy authority, list scope and grant ceiling.
16
+ * One registry, one source of `extraObjects`: nothing can disagree.
17
17
  *
18
18
  * Modeled on astram3's `buildAuthorizationInputs` (`server/src/features/authz/authz.wiring.ts`),
19
- * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix
20
- * (`APP_DB_*`, per the Task-7 rename).
19
+ * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix (`APP_DB_*`).
21
20
  */
22
21
  const mariadb_1 = __importDefault(require("mariadb"));
23
22
  const guard_1 = require("@astram/guard");
@@ -27,7 +26,7 @@ const session_facts_reader_js_1 = require("./session-facts.reader.js");
27
26
  /**
28
27
  * A lean `mariadb` pool for the authz grant-reads (built from `APP_DB_*` env via guard's
29
28
  * `loadDbConfig`). Kept small — these are pre-gate coarse-grant reads, not the main CRUD pool.
30
- * Opens the pool at CALL time (never at import) — the consumer (app lifecycle) owns closing it.
29
+ * Opens the pool at CALL time (never at import) — the caller owns closing it.
31
30
  */
32
31
  function createIdentityAuthzPool() {
33
32
  const db = (0, guard_1.loadDbConfig)();
@@ -41,31 +40,15 @@ function createIdentityAuthzPool() {
41
40
  });
42
41
  }
43
42
  /**
44
- * Build the `RecordAuthorizationInputs` the consumer passes to
45
- * `ShieldModule.forRoot({ recordAuthorization })`. The framework owns the four record-tier
46
- * adapters internally; `@astram/identity` supplies only the app-schema ports:
43
+ * Build the `RecordAuthorizationInputs` over `pool`. `@astram/identity` supplies only the
44
+ * app-schema ports:
47
45
  * - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
48
46
  * `company_membership`/`membership_posting`,
49
47
  * - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
50
- * - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
48
+ * - `registry` — bridged from `identityObjects` PLUS the consumer's `extraObjects` (their
49
+ * `<module>:<type>:<action>` keys project into a principal's grants at gate 5; without them a
50
+ * consumer type is `resource_not_registered`),
51
51
  * - `operationManifest` — empty (identity declares no trusted delegated operations).
52
- *
53
- * `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
54
- * `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
55
- * coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
56
- * and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
57
- * never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
58
- * the same object types the app registered with the engine closes that gap: the app's grants on
59
- * those keys now project, and the framework still owns the four record-tier adapters + scope fn.
60
- */
61
- function buildIdentityRecordAuthorization(pool, extraObjects = []) {
62
- const inputs = buildRecordAuthorizationInputs(pool, extraObjects);
63
- noteRegistry('decision', inputs.registry);
64
- return inputs;
65
- }
66
- /**
67
- * The inputs builder WITHOUT the boot parity record — for the identity module's own internal
68
- * authorities (they are not the decision path the consumer hands `ShieldModule`).
69
52
  */
70
53
  function buildRecordAuthorizationInputs(pool, extraObjects = []) {
71
54
  const registry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects, ...extraObjects]);
@@ -77,40 +60,3 @@ function buildRecordAuthorizationInputs(pool, extraObjects = []) {
77
60
  operationManifest: [],
78
61
  };
79
62
  }
80
- const recordedKeySets = {
81
- decision: new Map(),
82
- ceiling: new Map(),
83
- };
84
- /** The boot error: the two registries' action-key sets differ (names the first differing keys). */
85
- class IdentityRegistryMismatchError extends Error {
86
- constructor(onlyDecision, onlyCeiling) {
87
- const sample = (keys) => keys.length === 0 ? 'none' : keys.slice(0, 10).join(', ') + (keys.length > 10 ? ', …' : '');
88
- super('IdentityFeatureModule.forRoot({ extraObjects }) and buildIdentityRecordAuthorization(pool, ' +
89
- 'extraObjects) were given different object types. Pass the SAME array to both. ' +
90
- `Only in the decision path: ${sample(onlyDecision)}. ` +
91
- `Only in IdentityFeatureModule: ${sample(onlyCeiling)}.`);
92
- this.name = 'IdentityRegistryMismatchError';
93
- }
94
- }
95
- exports.IdentityRegistryMismatchError = IdentityRegistryMismatchError;
96
- /** Record one side's registry and throw if it disagrees with any recorded registry of the other. */
97
- function noteRegistry(role, registry) {
98
- const keys = [...new Set(registry.listActions().map((a) => a.permissionKey))].sort();
99
- const other = recordedKeySets[role === 'decision' ? 'ceiling' : 'decision'];
100
- for (const otherKeys of other.values()) {
101
- const mine = new Set(keys);
102
- const theirs = new Set(otherKeys);
103
- const onlyMine = keys.filter((k) => !theirs.has(k));
104
- const onlyTheirs = otherKeys.filter((k) => !mine.has(k));
105
- if (onlyMine.length > 0 || onlyTheirs.length > 0) {
106
- throw role === 'decision'
107
- ? new IdentityRegistryMismatchError(onlyMine, onlyTheirs)
108
- : new IdentityRegistryMismatchError(onlyTheirs, onlyMine);
109
- }
110
- }
111
- recordedKeySets[role].set(keys.join('\n'), keys);
112
- }
113
- /** Called by `IdentityFeatureModule.forRoot` with the grant ceiling's registry (Phase 8 #4). */
114
- function noteIdentityCeilingRegistry(registry) {
115
- noteRegistry('ceiling', registry);
116
- }
@@ -37,6 +37,11 @@ export interface MemberDominance {
37
37
  readonly roleIds: readonly string[];
38
38
  }>): Promise<void>;
39
39
  }
40
+ /**
41
+ * The actor's own root node (the active node its subtree hangs from). A non-root actor may not
42
+ * move, delete (F6) or change the status of (Phase 9 #13c) this node.
43
+ */
44
+ export declare function ownRootOf(actor: Principal): string;
40
45
  export declare class CompanyHierarchyService {
41
46
  private readonly pool;
42
47
  private readonly closure;
@@ -47,8 +52,15 @@ export declare class CompanyHierarchyService {
47
52
  constructor(pool: HierarchyQueryable, closure: ScopeClosureService,
48
53
  /** Phase 8 #3: required — a company delete must dominate every member (no silent skip). */
49
54
  ceiling: MemberDominance, audit?: AuditPort | undefined, now?: () => number);
50
- /** The actor's own root node (the active node its subtree hangs from). */
55
+ /** The actor's own root node (see {@link ownRootOf}). */
51
56
  private ownRoot;
57
+ /**
58
+ * The company's uuid as stored (Phase 10 #1): `c_uuid` is `utf8mb4_unicode_ci`, so a route uuid
59
+ * in another letter case still reaches the row, while JS-side comparisons (own root, agent
60
+ * tenants, `JSON_CONTAINS` over scope lists, name maps) are exact. Every entry resolves the
61
+ * stored spelling once and passes that down. Unknown → the input unchanged (later steps 404).
62
+ */
63
+ private storedUuid;
52
64
  /** Names of the given companies (unknown → absent). */
53
65
  private names;
54
66
  private recordAudit;
@@ -1 +1 @@
1
- {"version":3,"file":"company-hierarchy.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company-hierarchy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;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;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,8BAA8B,CAC5B,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,aAAa,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;QAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;KACrC,CAAC,GACD,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAuCD,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,2FAA2F;IAC3F,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBALH,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,mBAAmB;IAC7C,2FAA2F;IAC1E,OAAO,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,SAAS,YAAA,EACjB,GAAG,GAAE,MAAM,MAAiB;IAG/C,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAIf,uDAAuD;YACzC,KAAK;YAUL,WAAW;IAqBzB,iGAAiG;IAC3F,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkD3F;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB/D;;;;;;;;;;;;;;;;;;;;;;;OAuBG;YACW,gBAAgB;IA6E9B,0FAA0F;YAC5E,kBAAkB;IAmDhC,wFAAwF;YAC1E,sBAAsB;IA+BpC,wFAAwF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
1
+ {"version":3,"file":"company-hierarchy.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company-hierarchy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAE,mBAAmB,EAAkB,MAAM,mCAAmC,CAAC;AAWxF,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;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,8BAA8B,CAC5B,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,aAAa,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;QAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;KACrC,CAAC,GACD,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAuCD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAElD;AAED,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,2FAA2F;IAC3F,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBALH,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,mBAAmB;IAC7C,2FAA2F;IAC1E,OAAO,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,SAAS,YAAA,EACjB,GAAG,GAAE,MAAM,MAAiB;IAG/C,yDAAyD;IACzD,OAAO,CAAC,OAAO;IAIf;;;;;OAKG;YACW,UAAU;IAQxB,uDAAuD;YACzC,KAAK;YAUL,WAAW;IAqBzB,iGAAiG;IAC3F,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAoD3F;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB/D;;;;;;;;;;;;;;;;;;;;;;;OAuBG;YACW,gBAAgB;IAiF9B,0FAA0F;YAC5E,kBAAkB;IAmDhC,wFAAwF;YAC1E,sBAAsB;IA+BpC,wFAAwF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}