@astram/identity 0.2.1 → 0.3.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 (57) hide show
  1. package/dist/company-membership.object.d.ts.map +1 -1
  2. package/dist/company-membership.object.js +7 -0
  3. package/dist/company.object.d.ts.map +1 -1
  4. package/dist/company.object.js +5 -0
  5. package/dist/features/authz/catalog-bootstrap.provider.d.ts +6 -1
  6. package/dist/features/authz/catalog-bootstrap.provider.d.ts.map +1 -1
  7. package/dist/features/authz/catalog-bootstrap.provider.js +28 -4
  8. package/dist/features/authz/grant-admin.controller.d.ts +4 -3
  9. package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
  10. package/dist/features/authz/grant-admin.controller.js +22 -9
  11. package/dist/features/authz/grant-admin.service.d.ts +19 -3
  12. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  13. package/dist/features/authz/grant-admin.service.js +43 -5
  14. package/dist/features/authz/record-authorization.d.ts +11 -2
  15. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  16. package/dist/features/authz/record-authorization.js +11 -3
  17. package/dist/features/cross-tenant/grant-authorize.d.ts.map +1 -1
  18. package/dist/features/cross-tenant/grant-authorize.js +3 -0
  19. package/dist/features/identity-feature.module.d.ts +9 -1
  20. package/dist/features/identity-feature.module.d.ts.map +1 -1
  21. package/dist/features/identity-feature.module.js +147 -5
  22. package/dist/features/membership/membership-checker.d.ts +43 -0
  23. package/dist/features/membership/membership-checker.d.ts.map +1 -0
  24. package/dist/features/membership/membership-checker.js +25 -0
  25. package/dist/features/policy/policy.controller.d.ts +46 -0
  26. package/dist/features/policy/policy.controller.d.ts.map +1 -0
  27. package/dist/features/policy/policy.controller.js +150 -0
  28. package/dist/features/policy/policy.service.d.ts +129 -0
  29. package/dist/features/policy/policy.service.d.ts.map +1 -0
  30. package/dist/features/policy/policy.service.js +142 -0
  31. package/dist/features/scope/scope-closure.service.d.ts +82 -0
  32. package/dist/features/scope/scope-closure.service.d.ts.map +1 -0
  33. package/dist/features/scope/scope-closure.service.js +222 -0
  34. package/dist/features/scope/scope-subtree.resolver.d.ts +47 -0
  35. package/dist/features/scope/scope-subtree.resolver.d.ts.map +1 -0
  36. package/dist/features/scope/scope-subtree.resolver.js +24 -0
  37. package/dist/features/share/share.controller.d.ts +35 -0
  38. package/dist/features/share/share.controller.d.ts.map +1 -0
  39. package/dist/features/share/share.controller.js +143 -0
  40. package/dist/features/share/share.service.d.ts +90 -0
  41. package/dist/features/share/share.service.d.ts.map +1 -0
  42. package/dist/features/share/share.service.js +58 -0
  43. package/dist/index.d.ts +17 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +20 -1
  46. package/dist/membership-posting.object.d.ts.map +1 -1
  47. package/dist/membership-posting.object.js +10 -0
  48. package/dist/objects.d.ts.map +1 -1
  49. package/dist/objects.js +2 -1
  50. package/dist/record-share.object.d.ts +24 -0
  51. package/dist/record-share.object.d.ts.map +1 -0
  52. package/dist/record-share.object.js +60 -0
  53. package/dist/role.object.d.ts.map +1 -1
  54. package/dist/role.object.js +10 -0
  55. package/dist/user.object.d.ts.map +1 -1
  56. package/dist/user.object.js +10 -0
  57. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAOa,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,WAAW,EAAG,MAAM,CAAC;IAGrB,QAAQ,EAAG,OAAO,CAAC;IAGnB,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"company-membership.object.d.ts","sourceRoot":"","sources":["../src/company-membership.object.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,qBAca,iBAAiB;IAE5B,IAAI,EAAG,MAAM,CAAC;IAGd,QAAQ,EAAG,MAAM,CAAC;IAGlB,WAAW,EAAG,MAAM,CAAC;IAGrB,QAAQ,EAAG,OAAO,CAAC;IAGnB,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
@@ -58,6 +58,13 @@ exports.CompanyMembership = CompanyMembership = __decorate([
58
58
  table: 'company_membership',
59
59
  schema: schema_1.SCHEMA,
60
60
  access: { plane: 'data', system: true },
61
+ // Read-scope wall (ADR-0027, Task 4.1): a membership's company IS the tenant node
62
+ // (`cm_company_uuid`), so a non-root LIST/GET is bounded to memberships whose company is
63
+ // in the caller's reachable subtree. DECOUPLED from writes — NOT `trusted`, because
64
+ // marking `cm_company_uuid` trusted would force every new membership's company to the
65
+ // admin's OWN node, breaking adding a user to a SUB-company (a legit hierarchy op). This
66
+ // filters reads only. Root spans all.
67
+ readScope: { kind: 'nodeColumn', column: 'cm_company_uuid' },
61
68
  }),
62
69
  (0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'cm_uuid' })
63
70
  ], CompanyMembership);
@@ -1 +1 @@
1
- {"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,qBAca,OAAO;IAElB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAGd,UAAU,EAAG,MAAM,CAAC;IAGpB,IAAI,EAAG,MAAM,CAAC;IAGd,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"company.object.d.ts","sourceRoot":"","sources":["../src/company.object.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,qBAmBa,OAAO;IAElB,IAAI,EAAG,MAAM,CAAC;IAGd,IAAI,EAAG,MAAM,CAAC;IAGd,UAAU,EAAG,MAAM,CAAC;IAGpB,IAAI,EAAG,MAAM,CAAC;IAGd,MAAM,EAAG,MAAM,CAAC;IAGhB,SAAS,EAAG,MAAM,CAAC;CACpB"}
@@ -59,6 +59,11 @@ exports.Company = Company = __decorate([
59
59
  // `company` row carries no business data. Consistent with company_department/company_branch,
60
60
  // which are already control (managing a company's org structure is administration too).
61
61
  access: { plane: 'control', system: true },
62
+ // Read-scope wall (ADR-0027, Task 4.1): a company IS a tenant node — its own row carries
63
+ // the node (`c_uuid`), so a non-root LIST/GET is bounded to the caller's reachable subtree
64
+ // (`c_uuid` IN my subtree). DECOUPLED from writes (no `trusted` change) — this filters
65
+ // reads only. Root spans all.
66
+ readScope: { kind: 'nodeColumn', column: 'c_uuid' },
62
67
  }),
63
68
  (0, engine_1.PrimaryKey)({ kind: 'uuid', column: 'c_uuid' })
64
69
  ], Company);
@@ -3,6 +3,7 @@ import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
3
3
  import type { ObjectType } from '@astram/engine';
4
4
  import { CatalogReconcileJob } from './catalog-reconcile.job.js';
5
5
  import { AccessBundleReconcileJob } from './access-bundle-reconcile.job.js';
6
+ import { ScopeClosureService } from '../scope/scope-closure.service.js';
6
7
  /**
7
8
  * Boot: read-only verifyGatable (fail-closed — a stranded/unclassified route aborts
8
9
  * boot). Post-boot: kick the mutating reconcile once, then on an interval (single
@@ -20,9 +21,13 @@ export declare class CatalogBootstrapProvider implements OnApplicationBootstrap
20
21
  private readonly reflector;
21
22
  private readonly job;
22
23
  private readonly accessBundleJob;
24
+ private readonly scopeClosure;
23
25
  private readonly registrations;
24
26
  private timer;
25
- constructor(discovery: DiscoveryService, scanner: MetadataScanner, reflector: Reflector, job: CatalogReconcileJob, accessBundleJob: AccessBundleReconcileJob, registrations: readonly ObjectType[]);
27
+ constructor(discovery: DiscoveryService, scanner: MetadataScanner, reflector: Reflector, job: CatalogReconcileJob, accessBundleJob: AccessBundleReconcileJob, scopeClosure: ScopeClosureService, registrations: readonly ObjectType[]);
26
28
  onApplicationBootstrap(): Promise<void>;
29
+ /** Rebuild the scope closure; a failure is logged loudly (never silently swallowed) but does
30
+ * not abort boot or a cron tick — the reconcile is idempotent and retried next interval. */
31
+ private reconcileScopeClosure;
27
32
  }
28
33
  //# sourceMappingURL=catalog-bootstrap.provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"catalog-bootstrap.provider.d.ts","sourceRoot":"","sources":["../../../src/features/authz/catalog-bootstrap.provider.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAK5E;;;;;;;;;;GAUG;AACH,qBACa,wBAAyB,YAAW,sBAAsB;IAInE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAThC,OAAO,CAAC,KAAK,CAA+B;gBAGzB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,SAAS,EACpB,GAAG,EAAE,mBAAmB,EACxB,eAAe,EAAE,wBAAwB,EAEzC,aAAa,EAAE,SAAS,UAAU,EAAE;IAGjD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;CA2B9C"}
1
+ {"version":3,"file":"catalog-bootstrap.provider.d.ts","sourceRoot":"","sources":["../../../src/features/authz/catalog-bootstrap.provider.ts"],"names":[],"mappings":"AAIA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAO5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAKxE;;;;;;;;;;GAUG;AACH,qBACa,wBAAyB,YAAW,sBAAsB;IAInE,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAVhC,OAAO,CAAC,KAAK,CAA+B;gBAGzB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,SAAS,EACpB,GAAG,EAAE,mBAAmB,EACxB,eAAe,EAAE,wBAAwB,EACzC,YAAY,EAAE,mBAAmB,EAEjC,aAAa,EAAE,SAAS,UAAU,EAAE;IAGjD,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC7C;iGAC6F;YAC/E,qBAAqB;CAOpC"}
@@ -23,6 +23,7 @@ const guard_1 = require("@astram/guard");
23
23
  const engine_1 = require("@astram/engine");
24
24
  const catalog_reconcile_job_js_1 = require("./catalog-reconcile.job.js");
25
25
  const access_bundle_reconcile_job_js_1 = require("./access-bundle-reconcile.job.js");
26
+ const scope_closure_service_js_1 = require("../scope/scope-closure.service.js");
26
27
  const MODULE_KEY = 'identity';
27
28
  const RECONCILE_INTERVAL_MS = 60 * 60 * 1000; // hourly; honors DISABLE_ALL_CRONS below
28
29
  /**
@@ -42,14 +43,16 @@ let CatalogBootstrapProvider = class CatalogBootstrapProvider {
42
43
  reflector;
43
44
  job;
44
45
  accessBundleJob;
46
+ scopeClosure;
45
47
  registrations;
46
48
  timer = null;
47
- constructor(discovery, scanner, reflector, job, accessBundleJob, registrations) {
49
+ constructor(discovery, scanner, reflector, job, accessBundleJob, scopeClosure, registrations) {
48
50
  this.discovery = discovery;
49
51
  this.scanner = scanner;
50
52
  this.reflector = reflector;
51
53
  this.job = job;
52
54
  this.accessBundleJob = accessBundleJob;
55
+ this.scopeClosure = scopeClosure;
53
56
  this.registrations = registrations;
54
57
  }
55
58
  async onApplicationBootstrap() {
@@ -64,22 +67,43 @@ let CatalogBootstrapProvider = class CatalogBootstrapProvider {
64
67
  // Bundles reconcile AFTER permissions (same post-boot pass): a bundle validates
65
68
  // its keys against the live permission catalog, so permissions must reconcile first.
66
69
  await this.accessBundleJob.runOnce(nowIso);
70
+ // Scope-closure reconcile (Task 1.2): rebuild `scope_closure` from the live company tree so
71
+ // it is consistent with `companies.c_parent_uuid` at boot (mirrors the catalog reconcile).
72
+ // Independent of the catalog/bundles (it derives from the node tree, not the permission
73
+ // catalog). Fail-loud but NON-blocking: a corrupt tree (cycle/orphan) surfaces here as a
74
+ // logged error rather than aborting boot — the reconcile is idempotent and retried on the
75
+ // interval below once the data is fixed.
76
+ await this.reconcileScopeClosure();
67
77
  if (process.env.DISABLE_ALL_CRONS !== 'true') {
68
78
  this.timer = setInterval(() => {
69
79
  const tickIso = new Date().toISOString();
70
- void this.job.runOnce(tickIso).then(() => this.accessBundleJob.runOnce(tickIso));
80
+ void this.job
81
+ .runOnce(tickIso)
82
+ .then(() => this.accessBundleJob.runOnce(tickIso))
83
+ .then(() => this.reconcileScopeClosure());
71
84
  }, RECONCILE_INTERVAL_MS);
72
85
  this.timer.unref?.();
73
86
  }
74
87
  }
88
+ /** Rebuild the scope closure; a failure is logged loudly (never silently swallowed) but does
89
+ * not abort boot or a cron tick — the reconcile is idempotent and retried next interval. */
90
+ async reconcileScopeClosure() {
91
+ try {
92
+ await this.scopeClosure.reconcileClosure();
93
+ }
94
+ catch (cause) {
95
+ console.error('[scope] closure reconcile FAILED (tree may be corrupt):', cause);
96
+ }
97
+ }
75
98
  };
76
99
  exports.CatalogBootstrapProvider = CatalogBootstrapProvider;
77
100
  exports.CatalogBootstrapProvider = CatalogBootstrapProvider = __decorate([
78
101
  (0, common_1.Injectable)(),
79
- __param(5, (0, common_1.Inject)(engine_1.ENGINE_ALL_REGISTRATIONS)),
102
+ __param(6, (0, common_1.Inject)(engine_1.ENGINE_ALL_REGISTRATIONS)),
80
103
  __metadata("design:paramtypes", [core_1.DiscoveryService,
81
104
  core_1.MetadataScanner,
82
105
  core_1.Reflector,
83
106
  catalog_reconcile_job_js_1.CatalogReconcileJob,
84
- access_bundle_reconcile_job_js_1.AccessBundleReconcileJob, Array])
107
+ access_bundle_reconcile_job_js_1.AccessBundleReconcileJob,
108
+ scope_closure_service_js_1.ScopeClosureService, Array])
85
109
  ], CatalogBootstrapProvider);
@@ -28,7 +28,7 @@ export declare class GrantAdminController {
28
28
  * would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
29
29
  * routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
30
30
  */
31
- grantsSummary(query: {
31
+ grantsSummary(req: AuthedRequest, query: {
32
32
  page?: string;
33
33
  limit?: string;
34
34
  search?: string;
@@ -51,8 +51,9 @@ export declare class GrantAdminController {
51
51
  }[];
52
52
  wildcards: string[];
53
53
  }>;
54
- /** List a role's current grants. */
55
- list(uuid: string): Promise<{
54
+ /** List a role's current grants. Subtree-guarded (Task 4.2): a non-root caller reads a role's
55
+ * grants only if the role is bound within their reachable subtree, else `{ grants: [] }`. */
56
+ list(req: AuthedRequest, uuid: string): Promise<{
56
57
  grants: string[];
57
58
  }>;
58
59
  /** Grant one permission key to a role. */
@@ -1 +1 @@
1
- {"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;AAE1E,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,qBACa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAEtD;;;;;;;;OAQG;IAGG,aAAa,CAEjB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;IAcH,sDAAsD;IAGhD,OAAO;;;;;;;;;;IAIb,oCAAoC;IAG9B,IAAI,CAAgB,IAAI,EAAE,MAAM;;;IAItC,0CAA0C;IAGpC,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAYlG,mFAAmF;IAG7E,MAAM,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;IAOnE;;;;OAIG;IAGG,OAAO;;;IAIb;;;;OAIG;IAGG,WAAW,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAYxG;;;;OAIG;IAGG,YAAY,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;CAS1E"}
1
+ {"version":3,"file":"grant-admin.controller.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.controller.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAe,MAAM,0BAA0B,CAAC;AAG1E,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAaD;;;;;;;;;;;GAWG;AACH,qBACa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAEtD;;;;;;;;OAQG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EAEzB,KAAK,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;IAcH,sDAAsD;IAGhD,OAAO;;;;;;;;;;IAIb;kGAC8F;IAGxF,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;IAIjE,0CAA0C;IAGpC,KAAK,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAYlG,mFAAmF;IAG7E,MAAM,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;IAOnE;;;;OAIG;IAGG,OAAO;;;IAIb;;;;OAIG;IAGG,WAAW,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM,EAAU,IAAI,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAYxG;;;;OAIG;IAGG,YAAY,CAAgB,IAAI,EAAE,MAAM,EAAgB,GAAG,EAAE,MAAM;;;CAS1E"}
@@ -19,6 +19,16 @@ exports.GrantAdminController = void 0;
19
19
  const common_1 = require("@nestjs/common");
20
20
  const guard_1 = require("@astram/guard");
21
21
  const grant_admin_service_js_1 = require("./grant-admin.service.js");
22
+ /**
23
+ * Derive the caller's subtree scope from the resolved principal (Task 4.2). Uses the SAME cached
24
+ * `scopeSubtreeNodeIds` the RecordEngine's list-scope uses (one source of truth). Fail-safe to the
25
+ * active node `[tenantId]` if the subtree is absent (self-node, non-widening — never a leak).
26
+ */
27
+ function subtreeScopeOf(req) {
28
+ const p = req.principal;
29
+ const nodeIds = p.scopeSubtreeNodeIds?.length ? p.scopeSubtreeNodeIds : [p.tenantId];
30
+ return { nodeIds, isRoot: p.isRoot === true };
31
+ }
22
32
  /**
23
33
  * Grant administration — list/grant/revoke a role's permission keys. Gated purely on
24
34
  * `@Classified({ typeKey: 'role', ... })` + `decide()` (G1: no handler-level root
@@ -45,7 +55,7 @@ let GrantAdminController = class GrantAdminController {
45
55
  * would be captured by IdentityController's `@Get('roles/:uuid')` (registered first),
46
56
  * routing this to getRole('role-grants') → a 500. Keep it off the `roles/:uuid` shape.
47
57
  */
48
- async grantsSummary(query) {
58
+ async grantsSummary(req, query) {
49
59
  const page = Number.parseInt(query?.page ?? '', 10);
50
60
  const limit = Number.parseInt(query?.limit ?? '', 10);
51
61
  const params = {
@@ -59,15 +69,16 @@ let GrantAdminController = class GrantAdminController {
59
69
  params.search = query.search;
60
70
  if (typeof query?.sortBy === 'string')
61
71
  params.sortBy = query.sortBy;
62
- return this.grants.listGrantsSummary(params);
72
+ return this.grants.listGrantsSummary(params, subtreeScopeOf(req));
63
73
  }
64
74
  /** List all grantable permission keys + wildcards. */
65
75
  async catalog() {
66
76
  return this.grants.listCatalog();
67
77
  }
68
- /** List a role's current grants. */
69
- async list(uuid) {
70
- return this.grants.listRoleGrants(uuid);
78
+ /** List a role's current grants. Subtree-guarded (Task 4.2): a non-root caller reads a role's
79
+ * grants only if the role is bound within their reachable subtree, else `{ grants: [] }`. */
80
+ async list(req, uuid) {
81
+ return this.grants.listRoleGrants(uuid, subtreeScopeOf(req));
71
82
  }
72
83
  /** Grant one permission key to a role. */
73
84
  async grant(req, uuid, body) {
@@ -138,9 +149,10 @@ exports.GrantAdminController = GrantAdminController;
138
149
  __decorate([
139
150
  (0, common_1.Get)('access/role-grants'),
140
151
  (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
141
- __param(0, (0, common_1.Query)()),
152
+ __param(0, (0, common_1.Req)()),
153
+ __param(1, (0, common_1.Query)()),
142
154
  __metadata("design:type", Function),
143
- __metadata("design:paramtypes", [Object]),
155
+ __metadata("design:paramtypes", [Object, Object]),
144
156
  __metadata("design:returntype", Promise)
145
157
  ], GrantAdminController.prototype, "grantsSummary", null);
146
158
  __decorate([
@@ -153,9 +165,10 @@ __decorate([
153
165
  __decorate([
154
166
  (0, common_1.Get)('roles/:uuid/permissions'),
155
167
  (0, guard_1.Classified)({ typeKey: 'role', action: 'read' }),
156
- __param(0, (0, common_1.Param)('uuid')),
168
+ __param(0, (0, common_1.Req)()),
169
+ __param(1, (0, common_1.Param)('uuid')),
157
170
  __metadata("design:type", Function),
158
- __metadata("design:paramtypes", [String]),
171
+ __metadata("design:paramtypes", [Object, String]),
159
172
  __metadata("design:returntype", Promise)
160
173
  ], GrantAdminController.prototype, "list", null);
161
174
  __decorate([
@@ -1,6 +1,17 @@
1
1
  import type { Pool } from 'mariadb';
2
2
  import { PermissionVersionBumper } from './permission-version.bumper.js';
3
3
  import { AccessBundleStore } from './access-bundle-store.js';
4
+ /**
5
+ * The caller's reachable subtree, threaded from `req.principal` (Task 4.2). It scopes the
6
+ * grant-admin reads so a caller sees ONLY roles bound within their subtree — the SAME cached
7
+ * `scopeSubtreeNodeIds` the RecordEngine's list-scope uses (one source of truth for "a node's
8
+ * subtree"). `isRoot` spans all tenants (the tenant registry is root's scope) → NO predicate.
9
+ * A non-root with an EMPTY `nodeIds` matches nothing → an empty list (fail-closed, not a leak).
10
+ */
11
+ export interface SubtreeScope {
12
+ readonly nodeIds: readonly string[];
13
+ readonly isRoot: boolean;
14
+ }
4
15
  /** A granted key was not in the catalog and is not a recognized wildcard. The
5
16
  * controller maps this to HTTP 400 (distinct from a store outage → 503). */
6
17
  export declare class BadKeyError extends Error {
@@ -45,7 +56,12 @@ export declare class GrantAdminService {
45
56
  catalog: CatalogRow[];
46
57
  wildcards: string[];
47
58
  }>;
48
- listRoleGrants(roleUuid: string): Promise<{
59
+ /**
60
+ * A role's current grants. Task 4.2: subtree-guarded — a non-root caller may read a role's
61
+ * grants ONLY IF that role is bound within their reachable subtree; otherwise `{ grants: [] }`
62
+ * (fail-closed — never leak another tenant's role's permissions by uuid). Root skips the guard.
63
+ */
64
+ listRoleGrants(roleUuid: string, subtreeScope: SubtreeScope): Promise<{
49
65
  grants: string[];
50
66
  }>;
51
67
  /** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
@@ -90,13 +106,13 @@ export declare class GrantAdminService {
90
106
  * page (a per-role aggregate not cheaply sortable in SQL). Defaults: page 1, limit 25,
91
107
  * sort name asc.
92
108
  */
93
- listGrantsSummary(params?: {
109
+ listGrantsSummary(params: {
94
110
  page?: number;
95
111
  limit?: number;
96
112
  search?: string;
97
113
  sortBy?: string;
98
114
  sortOrder?: 'asc' | 'desc';
99
- }): Promise<{
115
+ } | undefined, subtreeScope: SubtreeScope): Promise<{
100
116
  rows: RoleGrantSummary[];
101
117
  total: number;
102
118
  page: number;
@@ -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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"}
@@ -5,6 +5,22 @@ const core_1 = require("@astram/core");
5
5
  const access_bundle_store_js_1 = require("./access-bundle-store.js");
6
6
  const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.js");
7
7
  const AUTHZ = 'shield_authz';
8
+ const APP = 'shield_app';
9
+ /**
10
+ * Build the shared EXISTS-subtree fragment for a non-root caller. A role qualifies only if it
11
+ * is bound (`membership_posting → company_membership`) to a node in the caller's subtree. All
12
+ * node ids are `?`-bound (never interpolated → injection-free); the caller splices the returned
13
+ * `params` at the position where `sql` sits in the surrounding query. Root / empty scope callers
14
+ * are handled by the caller (root ⇒ no fragment; the engine guarantees non-empty subtrees).
15
+ */
16
+ function subtreeExists(nodeIds) {
17
+ const ph = nodeIds.map(() => '?').join(', ');
18
+ const sql = ` AND EXISTS (` +
19
+ `SELECT 1 FROM \`${APP}\`.\`membership_posting\` mp` +
20
+ ` JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid` +
21
+ ` WHERE mp.mp_role_uuid = r.r_uuid AND cm.cm_company_uuid IN (${ph}))`;
22
+ return { sql, params: [...nodeIds] };
23
+ }
8
24
  /** A granted key was not in the catalog and is not a recognized wildcard. The
9
25
  * controller maps this to HTTP 400 (distinct from a store outage → 503). */
10
26
  class BadKeyError extends Error {
@@ -48,8 +64,25 @@ class GrantAdminService {
48
64
  ];
49
65
  return { catalog, wildcards };
50
66
  }
51
- async listRoleGrants(roleUuid) {
67
+ /**
68
+ * A role's current grants. Task 4.2: subtree-guarded — a non-root caller may read a role's
69
+ * grants ONLY IF that role is bound within their reachable subtree; otherwise `{ grants: [] }`
70
+ * (fail-closed — never leak another tenant's role's permissions by uuid). Root skips the guard.
71
+ */
72
+ async listRoleGrants(roleUuid, subtreeScope) {
52
73
  try {
74
+ if (!subtreeScope.isRoot) {
75
+ const ph = subtreeScope.nodeIds.map(() => '?').join(', ');
76
+ // Empty subtree → no placeholders → the check can't match → fail-closed empty.
77
+ const bound = subtreeScope.nodeIds.length
78
+ ? (await this.pool.query(`SELECT 1 FROM \`${APP}\`.\`membership_posting\` mp
79
+ JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
80
+ WHERE mp.mp_role_uuid = ? AND cm.cm_company_uuid IN (${ph})
81
+ LIMIT 1`, [roleUuid, ...subtreeScope.nodeIds]))
82
+ : [];
83
+ if (bound.length === 0)
84
+ return { grants: [] };
85
+ }
53
86
  const rows = (await this.pool.query(`SELECT rp_perm_key FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid]));
54
87
  return { grants: rows.map((r) => r.rp_perm_key) };
55
88
  }
@@ -203,7 +236,7 @@ class GrantAdminService {
203
236
  * page (a per-role aggregate not cheaply sortable in SQL). Defaults: page 1, limit 25,
204
237
  * sort name asc.
205
238
  */
206
- async listGrantsSummary(params) {
239
+ async listGrantsSummary(params, subtreeScope) {
207
240
  const page = Math.max(1, Math.trunc(params?.page ?? 1));
208
241
  const limit = Math.min(200, Math.max(1, Math.trunc(params?.limit ?? 25)));
209
242
  const offset = (page - 1) * limit;
@@ -214,6 +247,11 @@ class GrantAdminService {
214
247
  const { catalog } = await this.listCatalog();
215
248
  const allKeys = catalog.map((c) => c.key);
216
249
  const { bundles } = await this.listBundles();
250
+ // Task 4.2 subtree scope: a non-root caller sees ONLY roles bound within their reachable
251
+ // subtree (the EXISTS predicate); root spans all tenants (no predicate). The scope clause
252
+ // sits FIRST in the WHERE (before the LIKE), so its ?-bound node params lead the arg list —
253
+ // identical on BOTH the count and page queries so they can never disagree.
254
+ const scope = subtreeScope.isRoot ? { sql: '', params: [] } : subtreeExists(subtreeScope.nodeIds);
217
255
  // Distinct granted roles (a role appears iff it has ≥1 role_permission row), filtered
218
256
  // by name search. Bind the LIKE term as a param (wildcards escaped).
219
257
  const likeClause = search ? `AND r.r_name LIKE ?` : '';
@@ -221,7 +259,7 @@ class GrantAdminService {
221
259
  const [{ total = 0 } = {}] = (await this.pool.query(`SELECT COUNT(DISTINCT rp.rp_role_uuid) AS total
222
260
  FROM \`${AUTHZ}\`.\`role_permission\` rp
223
261
  JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = rp.rp_role_uuid
224
- WHERE 1 = 1 ${likeClause}`, likeArgs));
262
+ WHERE 1 = 1${scope.sql} ${likeClause}`, [...scope.params, ...likeArgs]));
225
263
  // The page of role uuids + name + latest-grant metadata. name/grantedAt sort in SQL;
226
264
  // permCount falls back to a stable name sort here (re-sorted after aggregation).
227
265
  const orderCol = sortBy === 'grantedAt' ? 'latest_at' : 'r.r_name';
@@ -230,10 +268,10 @@ class GrantAdminService {
230
268
  MAX(rp.rp_granted_at) AS latest_at
231
269
  FROM \`${AUTHZ}\`.\`role_permission\` rp
232
270
  JOIN \`shield_app\`.\`roles\` r ON r.r_uuid = rp.rp_role_uuid
233
- WHERE 1 = 1 ${likeClause}
271
+ WHERE 1 = 1${scope.sql} ${likeClause}
234
272
  GROUP BY rp.rp_role_uuid, r.r_name
235
273
  ORDER BY ${orderCol} ${dir}, r.r_name ASC
236
- LIMIT ? OFFSET ?`, [...likeArgs, limit, offset]));
274
+ LIMIT ? OFFSET ?`, [...scope.params, ...likeArgs, limit, offset]));
237
275
  if (pageRoles.length === 0) {
238
276
  return { rows: [], total: Number(total), page, limit };
239
277
  }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { type Pool } from 'mariadb';
13
13
  import { type RecordAuthorizationInputs } from '@astram/guard';
14
+ import type { ObjectTypeInput } from '@astram/engine';
14
15
  /**
15
16
  * A lean `mariadb` pool for the authz grant-reads (built from `APP_DB_*` env via guard's
16
17
  * `loadDbConfig`). Kept small — these are pre-gate coarse-grant reads, not the main CRUD pool.
@@ -24,8 +25,16 @@ export declare function createIdentityAuthzPool(): Pool;
24
25
  * - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
25
26
  * `company_membership`/`membership_posting`,
26
27
  * - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
27
- * - `registry` — bridged from `identityObjects`,
28
+ * - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
28
29
  * - `operationManifest` — empty (identity declares no trusted delegated operations).
30
+ *
31
+ * `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
32
+ * `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
33
+ * coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
34
+ * and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
35
+ * never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
36
+ * the same object types the app registered with the engine closes that gap: the app's grants on
37
+ * those keys now project, and the framework still owns the four record-tier adapters + scope fn.
29
38
  */
30
- export declare function buildIdentityRecordAuthorization(pool: Pool): RecordAuthorizationInputs;
39
+ export declare function buildIdentityRecordAuthorization(pool: Pool, extraObjects?: readonly ObjectTypeInput[]): RecordAuthorizationInputs;
31
40
  //# 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,EAA6C,KAAK,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAK1G;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,IAAI,GAAG,yBAAyB,CAStF"}
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"}
@@ -44,11 +44,19 @@ function createIdentityAuthzPool() {
44
44
  * - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
45
45
  * `company_membership`/`membership_posting`,
46
46
  * - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
47
- * - `registry` — bridged from `identityObjects`,
47
+ * - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
48
48
  * - `operationManifest` — empty (identity declares no trusted delegated operations).
49
+ *
50
+ * `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
51
+ * `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
52
+ * coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
53
+ * and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
54
+ * never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
55
+ * the same object types the app registered with the engine closes that gap: the app's grants on
56
+ * those keys now project, and the framework still owns the four record-tier adapters + scope fn.
49
57
  */
50
- function buildIdentityRecordAuthorization(pool) {
51
- const registry = (0, guard_1.buildIdentityModuleRegistry)(objects_js_1.identityObjects);
58
+ function buildIdentityRecordAuthorization(pool, extraObjects = []) {
59
+ const registry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects, ...extraObjects]);
52
60
  return {
53
61
  pool,
54
62
  permissionFacts: new db_permission_facts_adapter_js_1.DbPermissionFactsAdapter(pool, registry),
@@ -1 +1 @@
1
- {"version":3,"file":"grant-authorize.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/grant-authorize.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,iCAAiC;IAChD,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IACtD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;CACnD;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iCAAiC,GACtC,gBAAgB,CAkBlB"}
1
+ {"version":3,"file":"grant-authorize.d.ts","sourceRoot":"","sources":["../../../src/features/cross-tenant/grant-authorize.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,iCAAiC;IAChD,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;IACtD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF;0DACsD;IACtD,QAAQ,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC;CACnD;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,iCAAiC,GACtC,gBAAgB,CAqBlB"}
@@ -7,6 +7,9 @@ function makeAuthorizeCrossTenantGrant(deps) {
7
7
  const principal = {
8
8
  kind: 'user',
9
9
  id: principalId,
10
+ // Scope (ADR-0027) — INTERIM self-node default (Task 2.1): non-widening, = today's isolation.
11
+ activeScopeNodeId: tenantId,
12
+ scopeSubtreeNodeIds: [tenantId],
10
13
  tenantId,
11
14
  roleIds,
12
15
  isRoot: deps.isRoot(principalId),
@@ -1,4 +1,5 @@
1
1
  import { type DynamicModule } from '@nestjs/common';
2
+ import { type ObjectTypeInput } from '@astram/engine';
2
3
  /**
3
4
  * IdentityFeatureModule — the framework-owned identity FEATURE surface (company/user/role CRUD;
4
5
  * more in later phases) as thin controllers over the engine's RecordsService. The CONSUMER imports
@@ -18,9 +19,16 @@ export declare class IdentityFeatureModule {
18
19
  * by the writer's own `decide()` (a route-allows/writer-denies split). Omitted ⇒
19
20
  * `isRoot` is always false (a non-root consumer works; only a root offering
20
21
  * cross-tenant grants needs it). See ADR-0026 + the template wiring.
22
+ * @param extraObjects the consumer's OWN registered object types — pass the SAME array
23
+ * given to `buildIdentityRecordAuthorization(pool, extraObjects)`. Without them, the
24
+ * guided rule-builder SCHEMA (Phase 5) and the OWNER-SHARE resolver (Phase 6) only know
25
+ * the framework identity types, so a consumer's own OWNED type (e.g. `note`) is NOT
26
+ * policy-authorable and NOT shareable ("type '…' is not registered — cannot share").
27
+ * Passing them here lets an owner share their own records + author policies over them.
21
28
  */
22
- static forRoot({ rootIds }?: {
29
+ static forRoot({ rootIds, extraObjects, }?: {
23
30
  rootIds?: readonly string[];
31
+ extraObjects?: readonly ObjectTypeInput[];
24
32
  }): DynamicModule;
25
33
  }
26
34
  //# sourceMappingURL=identity-feature.module.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAoE5D;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,GAAE;QAAE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAO,GAAG,aAAa;CAkLjF"}
1
+ {"version":3,"file":"identity-feature.module.d.ts","sourceRoot":"","sources":["../../src/features/identity-feature.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE5D,OAAO,EAYL,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AAsExB;;;;;;;;;;GAUG;AACH,qBACa,qBAAqB;IAChC;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,OAAO,CAAC,EACb,OAAO,EACP,YAAiB,GAClB,GAAE;QAAE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,YAAY,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;KAAO,GAAG,aAAa;CAwTnG"}