@nextage/era-lib 1.0.190 → 1.0.192
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.
- package/build/constants.d.ts +56 -6
- package/build/constants.d.ts.map +1 -1
- package/build/constants.js +57 -7
- package/build/constants.js.map +1 -1
- package/build/enums.d.ts +16 -0
- package/build/enums.d.ts.map +1 -1
- package/build/enums.js +18 -1
- package/build/enums.js.map +1 -1
- package/build/events/base/base-event.controller.d.ts +1 -1
- package/build/events/base/base-event.controller.js +1 -1
- package/build/events/base/base-msg-listener.d.ts +1 -1
- package/build/events/base/base-msg-listener.js +1 -1
- package/build/events/publisher-registry.d.ts +1 -1
- package/build/events/publisher-registry.js +3 -3
- package/build/events/revocation-denylist.js +1 -1
- package/build/events/transport/nats-client-manager.d.ts +1 -1
- package/build/events/transport/nats-client-manager.js +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/interfaces.d.ts +21 -1
- package/build/interfaces.d.ts.map +1 -1
- package/build/types/base-user/index.d.ts +2 -1
- package/build/types/base-user/index.d.ts.map +1 -1
- package/build/types/nx-context.d.ts +18 -0
- package/build/types/nx-context.d.ts.map +1 -0
- package/build/types/nx-context.js +14 -0
- package/build/types/nx-context.js.map +1 -0
- package/build/utils/claims.d.ts +42 -0
- package/build/utils/claims.d.ts.map +1 -0
- package/build/utils/claims.interfaces.d.ts +42 -0
- package/build/utils/claims.interfaces.d.ts.map +1 -0
- package/build/utils/claims.interfaces.js +9 -0
- package/build/utils/claims.interfaces.js.map +1 -0
- package/build/utils/claims.js +61 -0
- package/build/utils/claims.js.map +1 -0
- package/build/utils/express.js +2 -2
- package/build/utils/functs.d.ts +4 -31
- package/build/utils/functs.d.ts.map +1 -1
- package/build/utils/functs.js +3 -54
- package/build/utils/functs.js.map +1 -1
- package/build/utils/index.d.ts +6 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +6 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/scope.d.ts +75 -0
- package/build/utils/scope.d.ts.map +1 -0
- package/build/utils/scope.interfaces.d.ts +20 -0
- package/build/utils/scope.interfaces.d.ts.map +1 -0
- package/build/utils/scope.interfaces.js +10 -0
- package/build/utils/scope.interfaces.js.map +1 -0
- package/build/utils/scope.js +121 -0
- package/build/utils/scope.js.map +1 -0
- package/build/utils/start-service.js +1 -1
- package/build/utils/tenant.d.ts +98 -0
- package/build/utils/tenant.d.ts.map +1 -0
- package/build/utils/tenant.interfaces.d.ts +19 -0
- package/build/utils/tenant.interfaces.d.ts.map +1 -0
- package/build/utils/tenant.interfaces.js +9 -0
- package/build/utils/tenant.interfaces.js.map +1 -0
- package/build/utils/tenant.js +124 -0
- package/build/utils/tenant.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*********************************************************************************************
|
|
2
|
+
* *
|
|
3
|
+
* Multi-tenancy helpers - single source of truth for how a request is scoped to a tenant. *
|
|
4
|
+
* *
|
|
5
|
+
* Every rule about "which organization is this operation allowed to touch" lives here and *
|
|
6
|
+
* nowhere else: the services (app-srv, auth-srv, ...) import these helpers, they never *
|
|
7
|
+
* re-implement the scoping. See ADR-0005 section 4 (capability matrix) and section 5 *
|
|
8
|
+
* (tenant resolution). *
|
|
9
|
+
* *
|
|
10
|
+
*********************************************************************************************/
|
|
11
|
+
import { BaseUser, NxContext, NxObject } from '@nextage/nx-frame-be';
|
|
12
|
+
import { ScopedReadModel } from './tenant.interfaces';
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the organization a request operates on - ALWAYS exactly one.
|
|
15
|
+
*
|
|
16
|
+
* The selection NEVER comes from the payload: it travels out of band (`X-Org-Id`
|
|
17
|
+
* header, or `orgId` in the WebSocket connection params) and lands on
|
|
18
|
+
* `NxContext.orgId`. Reading it from the body would let a client name any tenant
|
|
19
|
+
* simply by adding a field to a mutation input (T34).
|
|
20
|
+
*
|
|
21
|
+
* Resolution order (ADR-0005 section 5.1) - the first match wins:
|
|
22
|
+
*
|
|
23
|
+
* 1. no selection -> the user's own organization, for EVERYONE
|
|
24
|
+
* 2. cross-tenant role -> the selection, whichever it is
|
|
25
|
+
* 3. selection within reach-> the selection itself (membership or grant)
|
|
26
|
+
* 4. otherwise -> throws (fail-closed: an unreachable selection is never
|
|
27
|
+
* silently downgraded to the user's own tenant)
|
|
28
|
+
*
|
|
29
|
+
* A cross-tenant role does NOT mean "sees every tenant at once": it means it may
|
|
30
|
+
* operate on ANY tenant, ONE at a time, upon selection. Without a selection it is
|
|
31
|
+
* scoped to its own organization exactly like everybody else - the provider one is
|
|
32
|
+
* an organization like the others and holds its own application data.
|
|
33
|
+
*
|
|
34
|
+
* It follows that the return type is a plain `string`: there is no unscoped outcome.
|
|
35
|
+
* A cross-tenant aggregation - a future dashboard - is a DIFFERENT query, built on
|
|
36
|
+
* `reachableOrgIds` in its own resolver, and never a relaxation of this helper.
|
|
37
|
+
*
|
|
38
|
+
* Built on `reachableOrgIds` on purpose: the set of organizations a selection may
|
|
39
|
+
* name is the same set a list may show. Two independent implementations would
|
|
40
|
+
* eventually disagree, and any disagreement is a leak.
|
|
41
|
+
*
|
|
42
|
+
* @param user the authenticated user (`NxContext.user`)
|
|
43
|
+
* @param orgId the organization named by the request, when any
|
|
44
|
+
* @returns the tenant id to enforce
|
|
45
|
+
* @throws `AppError('messages.forbidden')` when the selection is out of reach
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveTenant(user?: BaseUser, orgId?: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Forces `tenantId` on a payload or on a query filter, from the resolved organization.
|
|
50
|
+
*
|
|
51
|
+
* The field is ALWAYS overwritten, never read: whatever the client sent under that
|
|
52
|
+
* name is irrelevant, which is what makes the helper safe to call on user input.
|
|
53
|
+
* There is no way out of the scoping and no flag to opt out of it - that was exactly
|
|
54
|
+
* defect D2 of ADR-0005 section 5.1, a fail-open by default.
|
|
55
|
+
*
|
|
56
|
+
* @param item the payload or filter, mutated in place
|
|
57
|
+
* @param context the request context (authenticated user + selected organization)
|
|
58
|
+
* @throws when no tenant can be resolved
|
|
59
|
+
*/
|
|
60
|
+
export declare function setTenant(item: NxObject, context?: NxContext): void;
|
|
61
|
+
/**
|
|
62
|
+
* Returns the user's own tenant id, that is their membership organization.
|
|
63
|
+
*
|
|
64
|
+
* Membership is an invariant of exactly one entry (ADR-0005 section 3.1), so the
|
|
65
|
+
* first entry IS the organization; the array shape only exists to stay compatible
|
|
66
|
+
* with the shared application base.
|
|
67
|
+
*
|
|
68
|
+
* @param user the authenticated user
|
|
69
|
+
* @returns the tenant id
|
|
70
|
+
* @throws if the user has no organization
|
|
71
|
+
*/
|
|
72
|
+
export declare function getTenant(user?: BaseUser): string;
|
|
73
|
+
/**
|
|
74
|
+
* Checks that `id` is reachable by the context user, ahead of a destructive write.
|
|
75
|
+
*
|
|
76
|
+
* Fills the gap of **T37**: `BaseModel.remove(id)` calls `beforeRemove` and then
|
|
77
|
+
* `findByIdAndDelete(id)`, applying neither `updateFilters` nor any `tenantId`
|
|
78
|
+
* constraint - and `bulkRemove(ids)` is nothing but `ids.map(remove)`. Without this
|
|
79
|
+
* check any user deletes documents belonging to OTHER organizations, knowing only
|
|
80
|
+
* their `_id`.
|
|
81
|
+
*
|
|
82
|
+
* The check reuses `model.get`, which goes through `beforeGet` and therefore already
|
|
83
|
+
* applies the tenant scoping: one single source of truth, no tenancy rule duplicated
|
|
84
|
+
* here. It follows that a cross-tenant role deletes in the organization it SELECTED,
|
|
85
|
+
* and in its own when it selected none - never across several at once, exactly as
|
|
86
|
+
* `resolveTenant` prescribes.
|
|
87
|
+
*
|
|
88
|
+
* **Fail-closed**: a missing or out-of-scope `id` raises an error, without telling
|
|
89
|
+
* the two cases apart (the existence of someone else's document is never confirmed).
|
|
90
|
+
*
|
|
91
|
+
* @param model the model the removal happens on (`this` inside the hook)
|
|
92
|
+
* @param id the document identifier
|
|
93
|
+
* @param context the request context, carrying the authenticated user
|
|
94
|
+
* @returns the same `id`, so it can be used directly inside `beforeRemove`
|
|
95
|
+
* @throws `AppError('messages.notFound')` when the document is out of scope
|
|
96
|
+
*/
|
|
97
|
+
export declare function assertTenantScope(model: ScopedReadModel, id: string, context?: NxContext): Promise<string>;
|
|
98
|
+
//# sourceMappingURL=tenant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;+FAS+F;AAE/F,OAAO,EAAY,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG/E,OAAO,EAAE,eAAe,EAAE,MAA8B,qBAAqB,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAerE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAE,SAAc,GAAG,IAAI,CAGvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,CAMjD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAMpH"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenancy types & interfaces.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the tenancy logic in `tenant.ts` (house-style: types live in
|
|
5
|
+
* a dedicated `*.interfaces.ts`, never mixed with the logic that consumes them).
|
|
6
|
+
*/
|
|
7
|
+
import { NxContext } from '@nextage/nx-frame-be';
|
|
8
|
+
/**
|
|
9
|
+
* A model of which only the read-by-`_id` is needed, already tenant-scoped by its
|
|
10
|
+
* own `beforeGet`.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately **structural** and not `BaseModel<Attrs, Doc, Model>`: the library
|
|
13
|
+
* must not depend on the concrete model types of a single service, and any model
|
|
14
|
+
* able to read by `_id` satisfies the contract.
|
|
15
|
+
*/
|
|
16
|
+
export interface ScopedReadModel {
|
|
17
|
+
get(id: string, context: NxContext): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=tenant.interfaces.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.interfaces.d.ts","sourceRoot":"","sources":["../../src/utils/tenant.interfaces.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tenancy types & interfaces.
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from the tenancy logic in `tenant.ts` (house-style: types live in
|
|
6
|
+
* a dedicated `*.interfaces.ts`, never mixed with the logic that consumes them).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
//# sourceMappingURL=tenant.interfaces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.interfaces.js","sourceRoot":"","sources":["../../src/utils/tenant.interfaces.ts"],"names":[],"mappings":";AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*********************************************************************************************
|
|
3
|
+
* *
|
|
4
|
+
* Multi-tenancy helpers - single source of truth for how a request is scoped to a tenant. *
|
|
5
|
+
* *
|
|
6
|
+
* Every rule about "which organization is this operation allowed to touch" lives here and *
|
|
7
|
+
* nowhere else: the services (app-srv, auth-srv, ...) import these helpers, they never *
|
|
8
|
+
* re-implement the scoping. See ADR-0005 section 4 (capability matrix) and section 5 *
|
|
9
|
+
* (tenant resolution). *
|
|
10
|
+
* *
|
|
11
|
+
*********************************************************************************************/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.resolveTenant = resolveTenant;
|
|
14
|
+
exports.setTenant = setTenant;
|
|
15
|
+
exports.getTenant = getTenant;
|
|
16
|
+
exports.assertTenantScope = assertTenantScope;
|
|
17
|
+
const nx_frame_be_1 = require("@nextage/nx-frame-be");
|
|
18
|
+
const scope_1 = require("./scope");
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the organization a request operates on - ALWAYS exactly one.
|
|
21
|
+
*
|
|
22
|
+
* The selection NEVER comes from the payload: it travels out of band (`X-Org-Id`
|
|
23
|
+
* header, or `orgId` in the WebSocket connection params) and lands on
|
|
24
|
+
* `NxContext.orgId`. Reading it from the body would let a client name any tenant
|
|
25
|
+
* simply by adding a field to a mutation input (T34).
|
|
26
|
+
*
|
|
27
|
+
* Resolution order (ADR-0005 section 5.1) - the first match wins:
|
|
28
|
+
*
|
|
29
|
+
* 1. no selection -> the user's own organization, for EVERYONE
|
|
30
|
+
* 2. cross-tenant role -> the selection, whichever it is
|
|
31
|
+
* 3. selection within reach-> the selection itself (membership or grant)
|
|
32
|
+
* 4. otherwise -> throws (fail-closed: an unreachable selection is never
|
|
33
|
+
* silently downgraded to the user's own tenant)
|
|
34
|
+
*
|
|
35
|
+
* A cross-tenant role does NOT mean "sees every tenant at once": it means it may
|
|
36
|
+
* operate on ANY tenant, ONE at a time, upon selection. Without a selection it is
|
|
37
|
+
* scoped to its own organization exactly like everybody else - the provider one is
|
|
38
|
+
* an organization like the others and holds its own application data.
|
|
39
|
+
*
|
|
40
|
+
* It follows that the return type is a plain `string`: there is no unscoped outcome.
|
|
41
|
+
* A cross-tenant aggregation - a future dashboard - is a DIFFERENT query, built on
|
|
42
|
+
* `reachableOrgIds` in its own resolver, and never a relaxation of this helper.
|
|
43
|
+
*
|
|
44
|
+
* Built on `reachableOrgIds` on purpose: the set of organizations a selection may
|
|
45
|
+
* name is the same set a list may show. Two independent implementations would
|
|
46
|
+
* eventually disagree, and any disagreement is a leak.
|
|
47
|
+
*
|
|
48
|
+
* @param user the authenticated user (`NxContext.user`)
|
|
49
|
+
* @param orgId the organization named by the request, when any
|
|
50
|
+
* @returns the tenant id to enforce
|
|
51
|
+
* @throws `AppError('messages.forbidden')` when the selection is out of reach
|
|
52
|
+
*/
|
|
53
|
+
function resolveTenant(user, orgId) {
|
|
54
|
+
if (!orgId)
|
|
55
|
+
return getTenant(user);
|
|
56
|
+
const reachable = (0, scope_1.reachableOrgIds)(user);
|
|
57
|
+
// `undefined` means no constraint on WHICH organization may be named.
|
|
58
|
+
if (!reachable)
|
|
59
|
+
return orgId;
|
|
60
|
+
if (!reachable.includes(orgId))
|
|
61
|
+
throw new nx_frame_be_1.AppError('messages.forbidden');
|
|
62
|
+
return orgId;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Forces `tenantId` on a payload or on a query filter, from the resolved organization.
|
|
66
|
+
*
|
|
67
|
+
* The field is ALWAYS overwritten, never read: whatever the client sent under that
|
|
68
|
+
* name is irrelevant, which is what makes the helper safe to call on user input.
|
|
69
|
+
* There is no way out of the scoping and no flag to opt out of it - that was exactly
|
|
70
|
+
* defect D2 of ADR-0005 section 5.1, a fail-open by default.
|
|
71
|
+
*
|
|
72
|
+
* @param item the payload or filter, mutated in place
|
|
73
|
+
* @param context the request context (authenticated user + selected organization)
|
|
74
|
+
* @throws when no tenant can be resolved
|
|
75
|
+
*/
|
|
76
|
+
function setTenant(item, context = {}) {
|
|
77
|
+
item.tenantId = resolveTenant(context.user, context.orgId);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns the user's own tenant id, that is their membership organization.
|
|
81
|
+
*
|
|
82
|
+
* Membership is an invariant of exactly one entry (ADR-0005 section 3.1), so the
|
|
83
|
+
* first entry IS the organization; the array shape only exists to stay compatible
|
|
84
|
+
* with the shared application base.
|
|
85
|
+
*
|
|
86
|
+
* @param user the authenticated user
|
|
87
|
+
* @returns the tenant id
|
|
88
|
+
* @throws if the user has no organization
|
|
89
|
+
*/
|
|
90
|
+
function getTenant(user) {
|
|
91
|
+
if (!user?.organizations?.length)
|
|
92
|
+
throw new Error('Cannot proceed: organization not set');
|
|
93
|
+
return String(user.organizations[0].id);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Checks that `id` is reachable by the context user, ahead of a destructive write.
|
|
97
|
+
*
|
|
98
|
+
* Fills the gap of **T37**: `BaseModel.remove(id)` calls `beforeRemove` and then
|
|
99
|
+
* `findByIdAndDelete(id)`, applying neither `updateFilters` nor any `tenantId`
|
|
100
|
+
* constraint - and `bulkRemove(ids)` is nothing but `ids.map(remove)`. Without this
|
|
101
|
+
* check any user deletes documents belonging to OTHER organizations, knowing only
|
|
102
|
+
* their `_id`.
|
|
103
|
+
*
|
|
104
|
+
* The check reuses `model.get`, which goes through `beforeGet` and therefore already
|
|
105
|
+
* applies the tenant scoping: one single source of truth, no tenancy rule duplicated
|
|
106
|
+
* here. It follows that a cross-tenant role deletes in the organization it SELECTED,
|
|
107
|
+
* and in its own when it selected none - never across several at once, exactly as
|
|
108
|
+
* `resolveTenant` prescribes.
|
|
109
|
+
*
|
|
110
|
+
* **Fail-closed**: a missing or out-of-scope `id` raises an error, without telling
|
|
111
|
+
* the two cases apart (the existence of someone else's document is never confirmed).
|
|
112
|
+
*
|
|
113
|
+
* @param model the model the removal happens on (`this` inside the hook)
|
|
114
|
+
* @param id the document identifier
|
|
115
|
+
* @param context the request context, carrying the authenticated user
|
|
116
|
+
* @returns the same `id`, so it can be used directly inside `beforeRemove`
|
|
117
|
+
* @throws `AppError('messages.notFound')` when the document is out of scope
|
|
118
|
+
*/
|
|
119
|
+
async function assertTenantScope(model, id, context = {}) {
|
|
120
|
+
if (!await model.get(id, context))
|
|
121
|
+
throw new nx_frame_be_1.AppError('messages.notFound');
|
|
122
|
+
return id;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=tenant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.js","sourceRoot":"","sources":["../../src/utils/tenant.ts"],"names":[],"mappings":";AAAA;;;;;;;;;+FAS+F;;AAyC/F,sCAeC;AAcD,8BAGC;AAaD,8BAMC;AA0BD,8CAMC;AA1HD,sDAA+E;AAE/E,mCAAkE;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,SAAgB,aAAa,CAAC,IAAe,EAAE,KAAc;IAE3D,IAAI,CAAC,KAAK;QACR,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;IAEzB,MAAM,SAAS,GAAG,IAAA,uBAAe,EAAC,IAAI,CAAC,CAAC;IAExC,sEAAsE;IACtE,IAAI,CAAC,SAAS;QACZ,OAAO,KAAK,CAAC;IAEf,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC5B,MAAM,IAAI,sBAAQ,CAAC,oBAAoB,CAAC,CAAC;IAE3C,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,SAAS,CAAC,IAAc,EAAE,UAAqB,EAAE;IAE/D,IAAI,CAAC,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,SAAS,CAAC,IAAe;IAEvC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM;QAC9B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAE1D,OAAO,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACI,KAAK,UAAU,iBAAiB,CAAC,KAAsB,EAAE,EAAU,EAAE,UAAqB,EAAE;IAEjG,IAAI,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC;QAC/B,MAAM,IAAI,sBAAQ,CAAC,mBAAmB,CAAC,CAAC;IAE1C,OAAO,EAAE,CAAC;AACZ,CAAC"}
|