@diister/quick-permission 0.9.0-beta.5
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/LICENSE +21 -0
- package/README.md +747 -0
- package/aggregation.ts +114 -0
- package/core/filtering.ts +70 -0
- package/core/matching.ts +82 -0
- package/core/merging.ts +143 -0
- package/dist/aggregation.d.ts +62 -0
- package/dist/aggregation.d.ts.map +1 -0
- package/dist/aggregation.js +97 -0
- package/dist/aggregation.js.map +1 -0
- package/dist/core/filtering.d.ts +35 -0
- package/dist/core/filtering.d.ts.map +1 -0
- package/dist/core/filtering.js +62 -0
- package/dist/core/filtering.js.map +1 -0
- package/dist/core/matching.d.ts +31 -0
- package/dist/core/matching.d.ts.map +1 -0
- package/dist/core/matching.js +75 -0
- package/dist/core/matching.js.map +1 -0
- package/dist/core/merging.d.ts +29 -0
- package/dist/core/merging.d.ts.map +1 -0
- package/dist/core/merging.js +124 -0
- package/dist/core/merging.js.map +1 -0
- package/dist/indirect-aggregation.d.ts +41 -0
- package/dist/indirect-aggregation.d.ts.map +1 -0
- package/dist/indirect-aggregation.js +185 -0
- package/dist/indirect-aggregation.js.map +1 -0
- package/dist/indirect-resource.d.ts +126 -0
- package/dist/indirect-resource.d.ts.map +1 -0
- package/dist/indirect-resource.js +109 -0
- package/dist/indirect-resource.js.map +1 -0
- package/dist/mod.d.ts +25 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +25 -0
- package/dist/mod.js.map +1 -0
- package/dist/mongo-query.d.ts +38 -0
- package/dist/mongo-query.d.ts.map +1 -0
- package/dist/mongo-query.js +88 -0
- package/dist/mongo-query.js.map +1 -0
- package/dist/permission.d.ts +57 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +60 -0
- package/dist/permission.js.map +1 -0
- package/dist/resource.d.ts +48 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +298 -0
- package/dist/resource.js.map +1 -0
- package/dist/rules.d.ts +106 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +183 -0
- package/dist/rules.js.map +1 -0
- package/dist/sift/core.d.ts +104 -0
- package/dist/sift/core.d.ts.map +1 -0
- package/dist/sift/core.js +248 -0
- package/dist/sift/core.js.map +1 -0
- package/dist/sift/index.d.ts +10 -0
- package/dist/sift/index.d.ts.map +1 -0
- package/dist/sift/index.js +18 -0
- package/dist/sift/index.js.map +1 -0
- package/dist/sift/operations.d.ts +87 -0
- package/dist/sift/operations.d.ts.map +1 -0
- package/dist/sift/operations.js +257 -0
- package/dist/sift/operations.js.map +1 -0
- package/dist/sift/utils.d.ts +12 -0
- package/dist/sift/utils.d.ts.map +1 -0
- package/dist/sift/utils.js +80 -0
- package/dist/sift/utils.js.map +1 -0
- package/dist/system.d.ts +113 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +712 -0
- package/dist/system.js.map +1 -0
- package/dist/target.d.ts +18 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/target.js +41 -0
- package/dist/target.js.map +1 -0
- package/dist/types.d.ts +345 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/indirect-aggregation.ts +216 -0
- package/indirect-resource.ts +205 -0
- package/mod.ts +81 -0
- package/mongo-query.ts +94 -0
- package/package.json +58 -0
- package/permission.ts +88 -0
- package/resource.ts +352 -0
- package/rules.ts +241 -0
- package/sift/MIT-LICENSE.txt +20 -0
- package/sift/core.ts +551 -0
- package/sift/index.ts +62 -0
- package/sift/operations.ts +449 -0
- package/sift/utils.ts +96 -0
- package/system.ts +974 -0
- package/target.ts +84 -0
- package/types.ts +408 -0
package/aggregation.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-grant aggregation primitives.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator runs each matching grant's rules independently, then folds
|
|
5
|
+
* the per-grant outputs through these functions to produce a single CanResult.
|
|
6
|
+
*
|
|
7
|
+
* Two axes:
|
|
8
|
+
* - constraints: AND intra-grant, OR cross-grant. Used for DB pushdown
|
|
9
|
+
* (`output.constraints` becomes a Mongo expression for `find()`).
|
|
10
|
+
* - filter spec: union cross-grant ("most permissive grant wins"). A grant
|
|
11
|
+
* with no filter exposes all fields and short-circuits the union.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Intra-grant: AND-merge constraint contributions from multiple match rules
|
|
16
|
+
* within the same grant.
|
|
17
|
+
*
|
|
18
|
+
* - 0 contributions → undefined (no match rule fired in this grant)
|
|
19
|
+
* - All `{}` (= "any") → `{}` (no restriction)
|
|
20
|
+
* - 1 concrete entry → that entry
|
|
21
|
+
* - N concrete entries → `{ $and: [...] }`
|
|
22
|
+
*/
|
|
23
|
+
export function combineGrantConstraints(
|
|
24
|
+
parts: ReadonlyArray<Record<string, unknown>>,
|
|
25
|
+
): Record<string, unknown> | undefined {
|
|
26
|
+
if (parts.length === 0) return undefined;
|
|
27
|
+
const nonEmpty = parts.filter((p) => Object.keys(p).length > 0);
|
|
28
|
+
if (nonEmpty.length === 0) return {};
|
|
29
|
+
if (nonEmpty.length === 1) return nonEmpty[0];
|
|
30
|
+
return { $and: [...nonEmpty] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cross-grant: OR-merge per-grant constraints with "any wins" semantics.
|
|
35
|
+
*
|
|
36
|
+
* - 0 entries → undefined (no match rule active anywhere)
|
|
37
|
+
* - any `undefined` entry → undefined (a grant without constraint trumps;
|
|
38
|
+
* pushdown stays unrestricted)
|
|
39
|
+
* - any `{}` entry → `{}` ("any" — grants exist but impose no restriction)
|
|
40
|
+
* - 1 entry → that entry as-is
|
|
41
|
+
* - N entries → `{ $or: [...] }`
|
|
42
|
+
*/
|
|
43
|
+
export function aggregateConstraints(
|
|
44
|
+
entries: ReadonlyArray<Record<string, unknown> | undefined>,
|
|
45
|
+
): Record<string, unknown> | undefined {
|
|
46
|
+
if (entries.length === 0) return undefined;
|
|
47
|
+
if (entries.some((e) => e === undefined)) return undefined;
|
|
48
|
+
const concrete = entries as ReadonlyArray<Record<string, unknown>>;
|
|
49
|
+
if (concrete.some((e) => Object.keys(e).length === 0)) return {};
|
|
50
|
+
if (concrete.length === 1) return concrete[0];
|
|
51
|
+
return { $or: [...concrete] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Project a source object through a field whitelist.
|
|
56
|
+
* Non-object sources pass through unchanged.
|
|
57
|
+
*/
|
|
58
|
+
export function projectFields(
|
|
59
|
+
source: unknown,
|
|
60
|
+
fields: Record<string, boolean>,
|
|
61
|
+
): unknown {
|
|
62
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
63
|
+
return source;
|
|
64
|
+
}
|
|
65
|
+
const out: Record<string, unknown> = {};
|
|
66
|
+
const src = source as Record<string, unknown>;
|
|
67
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
68
|
+
if (v === true && k in src) out[k] = src[k];
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cross-grant filter union state.
|
|
75
|
+
*
|
|
76
|
+
* - `undefined` : no filter rule has fired (initial state)
|
|
77
|
+
* - `null` : at least one grant exposed all fields (union saturates;
|
|
78
|
+
* any further contributions are ignored)
|
|
79
|
+
* - `Record` : union of allowed fields across grants so far
|
|
80
|
+
*/
|
|
81
|
+
export type FilterUnion = Record<string, boolean> | null | undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Merge a filter contribution into the running union.
|
|
85
|
+
* `null` contribution means "this grant has no filter" (all fields allowed)
|
|
86
|
+
* and saturates the union. Once saturated, stays saturated.
|
|
87
|
+
*/
|
|
88
|
+
export function mergeFilterSpec(
|
|
89
|
+
current: FilterUnion,
|
|
90
|
+
contribution: Record<string, boolean> | null,
|
|
91
|
+
): FilterUnion {
|
|
92
|
+
if (current === null) return null;
|
|
93
|
+
if (contribution === null) return null;
|
|
94
|
+
return { ...(current ?? {}), ...contribution };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve final `data` from the filter union state and a reference source.
|
|
99
|
+
*
|
|
100
|
+
* - union is `null` → reference source as-is (most permissive grant won)
|
|
101
|
+
* - union is `Record` → project the source through the union
|
|
102
|
+
* - union is `undefined` → fall back (no filter rule fired anywhere)
|
|
103
|
+
*/
|
|
104
|
+
export function resolveFilteredData(
|
|
105
|
+
union: FilterUnion,
|
|
106
|
+
referenceSource: unknown,
|
|
107
|
+
fallback: unknown,
|
|
108
|
+
): unknown {
|
|
109
|
+
if (union === null) return referenceSource;
|
|
110
|
+
if (union !== undefined && referenceSource !== undefined) {
|
|
111
|
+
return projectFields(referenceSource, union);
|
|
112
|
+
}
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtering utilities for applying field-level permissions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FilterSpec } from "./merging.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Apply a filter specification to an object
|
|
9
|
+
*
|
|
10
|
+
* @param obj The object to filter
|
|
11
|
+
* @param filterSpec The filter specification (field: true/false)
|
|
12
|
+
* @returns The filtered object
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* applyFilter(
|
|
16
|
+
* { _id: 1, name: "John", email: "john@example.com", password: "secret" },
|
|
17
|
+
* { _id: true, name: true, email: true }
|
|
18
|
+
* )
|
|
19
|
+
* // → { _id: 1, name: "John", email: "john@example.com" }
|
|
20
|
+
*
|
|
21
|
+
* // Nested filtering
|
|
22
|
+
* applyFilter(
|
|
23
|
+
* { user: { name: "John", password: "secret" } },
|
|
24
|
+
* { user: { name: true } }
|
|
25
|
+
* )
|
|
26
|
+
* // → { user: { name: "John" } }
|
|
27
|
+
*/
|
|
28
|
+
export function applyFilter(obj: any, filterSpec: FilterSpec): any {
|
|
29
|
+
if (!obj || typeof obj !== "object") {
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle arrays
|
|
34
|
+
if (Array.isArray(obj)) {
|
|
35
|
+
return obj.map((item) => applyFilter(item, filterSpec));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result: any = {};
|
|
39
|
+
|
|
40
|
+
for (const [key, spec] of Object.entries(filterSpec)) {
|
|
41
|
+
if (!(key in obj)) continue;
|
|
42
|
+
|
|
43
|
+
if (spec === true) {
|
|
44
|
+
// Include field as-is
|
|
45
|
+
result[key] = obj[key];
|
|
46
|
+
} else if (spec === false) {
|
|
47
|
+
// Exclude field
|
|
48
|
+
continue;
|
|
49
|
+
} else if (typeof spec === "object") {
|
|
50
|
+
// Nested filter
|
|
51
|
+
result[key] = applyFilter(obj[key], spec);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pick specific fields from an object
|
|
60
|
+
*
|
|
61
|
+
* @param obj The source object
|
|
62
|
+
* @param fields The fields to pick
|
|
63
|
+
* @returns A new object with only the specified fields
|
|
64
|
+
*/
|
|
65
|
+
export function pickFields<T extends Record<string, any>>(
|
|
66
|
+
obj: T,
|
|
67
|
+
fields: Record<string, boolean>,
|
|
68
|
+
): Partial<T> {
|
|
69
|
+
return applyFilter(obj, fields);
|
|
70
|
+
}
|
package/core/matching.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
type TargetPath = readonly unknown[];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Match a requested target path against a pattern path.
|
|
5
|
+
*
|
|
6
|
+
* Targets are always arrays of segments. Each segment is matched against the
|
|
7
|
+
* corresponding pattern segment.
|
|
8
|
+
*
|
|
9
|
+
* Segment-level wildcards:
|
|
10
|
+
* - `"*"` matches any value in that segment.
|
|
11
|
+
* - `"prefix:*"` matches any string segment that starts with `prefix:`.
|
|
12
|
+
*
|
|
13
|
+
* For "any target" semantics, leave the permission's `target` undefined at the
|
|
14
|
+
* call site rather than passing a wildcard here.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* matchPath(["article:123"], ["article:*"]) // true
|
|
18
|
+
* matchPath(["article:123", "comment:4"], ["article:*", "*"]) // true
|
|
19
|
+
* matchPath(["a"], ["a", "b"]) // false (different arity)
|
|
20
|
+
*/
|
|
21
|
+
export function matchPath(requested: TargetPath, pattern: TargetPath): boolean {
|
|
22
|
+
if (requested.length !== pattern.length) return false;
|
|
23
|
+
return requested.every((segment, i) => matchSegment(segment, pattern[i]));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function matchSegment(requested: unknown, pattern: unknown): boolean {
|
|
27
|
+
// Full-segment wildcard
|
|
28
|
+
if (pattern === "*") return true;
|
|
29
|
+
|
|
30
|
+
// Prefix wildcard on string segments: "article:*"
|
|
31
|
+
if (
|
|
32
|
+
typeof pattern === "string" &&
|
|
33
|
+
typeof requested === "string" &&
|
|
34
|
+
pattern.endsWith("*")
|
|
35
|
+
) {
|
|
36
|
+
return requested.startsWith(pattern.slice(0, -1));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Nested array segments (rare, but supported)
|
|
40
|
+
if (Array.isArray(pattern) && Array.isArray(requested)) {
|
|
41
|
+
return matchPath(requested, pattern);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return requested === pattern;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Symmetric "overlap" match: returns true if there's any concrete value that
|
|
49
|
+
* could satisfy both `a` and `b` simultaneously (treating both as patterns).
|
|
50
|
+
*
|
|
51
|
+
* Used by broad-match checks where the request target itself may carry
|
|
52
|
+
* wildcards — e.g. "do I have any badge access in this expo?" with the request
|
|
53
|
+
* `["expo:1", "*"]` overlapping a grant on `["expo:1", "badge:*"]`.
|
|
54
|
+
*/
|
|
55
|
+
export function overlapPath(a: TargetPath, b: TargetPath): boolean {
|
|
56
|
+
if (a.length !== b.length) return false;
|
|
57
|
+
return a.every((segment, i) => overlapSegment(segment, b[i]));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function overlapSegment(a: unknown, b: unknown): boolean {
|
|
61
|
+
// Full wildcards on either side → overlap
|
|
62
|
+
if (a === "*" || b === "*") return true;
|
|
63
|
+
|
|
64
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
65
|
+
const aPrefix = a.endsWith("*") ? a.slice(0, -1) : null;
|
|
66
|
+
const bPrefix = b.endsWith("*") ? b.slice(0, -1) : null;
|
|
67
|
+
|
|
68
|
+
if (aPrefix !== null && bPrefix !== null) {
|
|
69
|
+
// Two prefix wildcards: overlap iff one prefix extends the other
|
|
70
|
+
return aPrefix.startsWith(bPrefix) || bPrefix.startsWith(aPrefix);
|
|
71
|
+
}
|
|
72
|
+
if (aPrefix !== null) return b.startsWith(aPrefix);
|
|
73
|
+
if (bPrefix !== null) return a.startsWith(bPrefix);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Nested arrays
|
|
77
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
78
|
+
return overlapPath(a, b);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return a === b;
|
|
82
|
+
}
|
package/core/merging.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merging utilities for combining outputs from multiple permissions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Use interface instead of type alias to avoid circular reference error
|
|
6
|
+
export interface FilterSpec {
|
|
7
|
+
[key: string]: boolean | FilterSpec;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Merge two filter specs using union strategy (more permissive)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* mergeFilters(
|
|
15
|
+
* { _id: true, name: true },
|
|
16
|
+
* { name: true, email: true }
|
|
17
|
+
* )
|
|
18
|
+
* // → { _id: true, name: true, email: true }
|
|
19
|
+
*/
|
|
20
|
+
export function mergeFilters(
|
|
21
|
+
current: FilterSpec | undefined,
|
|
22
|
+
incoming: FilterSpec | undefined,
|
|
23
|
+
): FilterSpec {
|
|
24
|
+
if (!current) return incoming || {};
|
|
25
|
+
if (!incoming) return current;
|
|
26
|
+
|
|
27
|
+
const result: FilterSpec = { ...current };
|
|
28
|
+
|
|
29
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
30
|
+
if (typeof value === "boolean" && typeof result[key] === "boolean") {
|
|
31
|
+
// Union: if either says true, it's true
|
|
32
|
+
result[key] = result[key] || value;
|
|
33
|
+
} else if (typeof value === "object" && typeof result[key] === "object") {
|
|
34
|
+
// Recursive merge for nested filters
|
|
35
|
+
result[key] = mergeFilters(
|
|
36
|
+
result[key] as FilterSpec,
|
|
37
|
+
value as FilterSpec,
|
|
38
|
+
);
|
|
39
|
+
} else if (!(key in result)) {
|
|
40
|
+
// New key, add it
|
|
41
|
+
result[key] = value;
|
|
42
|
+
} else {
|
|
43
|
+
// Type mismatch or other case: incoming wins
|
|
44
|
+
result[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Merge two values using the "most permissive" strategy, recursing down to
|
|
53
|
+
* the leaves of nested structures.
|
|
54
|
+
*
|
|
55
|
+
* Strategies (applied at each leaf):
|
|
56
|
+
* - filter / updateFilter / writeFilter: union via mergeFilters
|
|
57
|
+
* - arrays: concat + dedup (set union)
|
|
58
|
+
* - numbers: Math.max (largest grant wins)
|
|
59
|
+
* - booleans: OR (true wins)
|
|
60
|
+
* - Date: latest wins
|
|
61
|
+
* - plain objects: recurse field-by-field with the same strategy
|
|
62
|
+
* - everything else (string, mismatched types, null): incoming wins
|
|
63
|
+
*
|
|
64
|
+
* Recursive object handling fixes the previous `{ ...current, ...incoming }`
|
|
65
|
+
* shortcut which made nested merges depend on insertion order rather than
|
|
66
|
+
* value semantics.
|
|
67
|
+
*/
|
|
68
|
+
function mergeValue(key: string, current: any, incoming: any): any {
|
|
69
|
+
// Filter-style fields are unions of allowed fields, regardless of nesting depth
|
|
70
|
+
if (key === "filter" || key === "updateFilter" || key === "writeFilter") {
|
|
71
|
+
return mergeFilters(current, incoming);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Arrays: union with dedup
|
|
75
|
+
if (Array.isArray(current) && Array.isArray(incoming)) {
|
|
76
|
+
return [...new Set([...current, ...incoming])];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Numbers: most permissive = largest
|
|
80
|
+
if (typeof current === "number" && typeof incoming === "number") {
|
|
81
|
+
return Math.max(current, incoming);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Booleans: most permissive = OR
|
|
85
|
+
if (typeof current === "boolean" && typeof incoming === "boolean") {
|
|
86
|
+
return current || incoming;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Dates: most permissive = latest
|
|
90
|
+
if (current instanceof Date && incoming instanceof Date) {
|
|
91
|
+
return new Date(Math.max(current.getTime(), incoming.getTime()));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Plain objects: recurse field-by-field
|
|
95
|
+
if (
|
|
96
|
+
typeof current === "object" &&
|
|
97
|
+
typeof incoming === "object" &&
|
|
98
|
+
current !== null &&
|
|
99
|
+
incoming !== null &&
|
|
100
|
+
!Array.isArray(current) &&
|
|
101
|
+
!Array.isArray(incoming) &&
|
|
102
|
+
!(current instanceof Date) &&
|
|
103
|
+
!(incoming instanceof Date)
|
|
104
|
+
) {
|
|
105
|
+
const result: Record<string, any> = { ...current };
|
|
106
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
107
|
+
result[k] = k in current ? mergeValue(k, current[k], v) : v;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Type mismatch or primitive: incoming wins
|
|
113
|
+
return incoming;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Merge two outputs intelligently based on the key
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* mergeOutputs(
|
|
121
|
+
* { filter: { _id: true }, data: { _id: 1 } },
|
|
122
|
+
* { filter: { name: true }, data: { name: "John" } }
|
|
123
|
+
* )
|
|
124
|
+
* // → { filter: { _id: true, name: true }, data: { _id: 1, name: "John" } }
|
|
125
|
+
*/
|
|
126
|
+
export function mergeOutputs(current: any, incoming: any): any {
|
|
127
|
+
if (!current) return incoming;
|
|
128
|
+
if (!incoming) return current;
|
|
129
|
+
|
|
130
|
+
const result = { ...current };
|
|
131
|
+
|
|
132
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
133
|
+
if (!(key in result)) {
|
|
134
|
+
// New key, add it
|
|
135
|
+
result[key] = value;
|
|
136
|
+
} else {
|
|
137
|
+
// Existing key, merge intelligently
|
|
138
|
+
result[key] = mergeValue(key, result[key], value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-grant aggregation primitives.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator runs each matching grant's rules independently, then folds
|
|
5
|
+
* the per-grant outputs through these functions to produce a single CanResult.
|
|
6
|
+
*
|
|
7
|
+
* Two axes:
|
|
8
|
+
* - constraints: AND intra-grant, OR cross-grant. Used for DB pushdown
|
|
9
|
+
* (`output.constraints` becomes a Mongo expression for `find()`).
|
|
10
|
+
* - filter spec: union cross-grant ("most permissive grant wins"). A grant
|
|
11
|
+
* with no filter exposes all fields and short-circuits the union.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Intra-grant: AND-merge constraint contributions from multiple match rules
|
|
15
|
+
* within the same grant.
|
|
16
|
+
*
|
|
17
|
+
* - 0 contributions → undefined (no match rule fired in this grant)
|
|
18
|
+
* - All `{}` (= "any") → `{}` (no restriction)
|
|
19
|
+
* - 1 concrete entry → that entry
|
|
20
|
+
* - N concrete entries → `{ $and: [...] }`
|
|
21
|
+
*/
|
|
22
|
+
export declare function combineGrantConstraints(parts: ReadonlyArray<Record<string, unknown>>): Record<string, unknown> | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Cross-grant: OR-merge per-grant constraints with "any wins" semantics.
|
|
25
|
+
*
|
|
26
|
+
* - 0 entries → undefined (no match rule active anywhere)
|
|
27
|
+
* - any `undefined` entry → undefined (a grant without constraint trumps;
|
|
28
|
+
* pushdown stays unrestricted)
|
|
29
|
+
* - any `{}` entry → `{}` ("any" — grants exist but impose no restriction)
|
|
30
|
+
* - 1 entry → that entry as-is
|
|
31
|
+
* - N entries → `{ $or: [...] }`
|
|
32
|
+
*/
|
|
33
|
+
export declare function aggregateConstraints(entries: ReadonlyArray<Record<string, unknown> | undefined>): Record<string, unknown> | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Project a source object through a field whitelist.
|
|
36
|
+
* Non-object sources pass through unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export declare function projectFields(source: unknown, fields: Record<string, boolean>): unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Cross-grant filter union state.
|
|
41
|
+
*
|
|
42
|
+
* - `undefined` : no filter rule has fired (initial state)
|
|
43
|
+
* - `null` : at least one grant exposed all fields (union saturates;
|
|
44
|
+
* any further contributions are ignored)
|
|
45
|
+
* - `Record` : union of allowed fields across grants so far
|
|
46
|
+
*/
|
|
47
|
+
export type FilterUnion = Record<string, boolean> | null | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Merge a filter contribution into the running union.
|
|
50
|
+
* `null` contribution means "this grant has no filter" (all fields allowed)
|
|
51
|
+
* and saturates the union. Once saturated, stays saturated.
|
|
52
|
+
*/
|
|
53
|
+
export declare function mergeFilterSpec(current: FilterUnion, contribution: Record<string, boolean> | null): FilterUnion;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve final `data` from the filter union state and a reference source.
|
|
56
|
+
*
|
|
57
|
+
* - union is `null` → reference source as-is (most permissive grant won)
|
|
58
|
+
* - union is `Record` → project the source through the union
|
|
59
|
+
* - union is `undefined` → fall back (no filter rule fired anywhere)
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveFilteredData(union: FilterUnion, referenceSource: unknown, fallback: unknown): unknown;
|
|
62
|
+
//# sourceMappingURL=aggregation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregation.d.ts","sourceRoot":"","sources":["../aggregation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC5C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAMrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,GAC1D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAOrC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAUT;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;AAErE;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAC3C,WAAW,CAIb;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,EAClB,eAAe,EAAE,OAAO,EACxB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAMT"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-grant aggregation primitives.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator runs each matching grant's rules independently, then folds
|
|
5
|
+
* the per-grant outputs through these functions to produce a single CanResult.
|
|
6
|
+
*
|
|
7
|
+
* Two axes:
|
|
8
|
+
* - constraints: AND intra-grant, OR cross-grant. Used for DB pushdown
|
|
9
|
+
* (`output.constraints` becomes a Mongo expression for `find()`).
|
|
10
|
+
* - filter spec: union cross-grant ("most permissive grant wins"). A grant
|
|
11
|
+
* with no filter exposes all fields and short-circuits the union.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Intra-grant: AND-merge constraint contributions from multiple match rules
|
|
15
|
+
* within the same grant.
|
|
16
|
+
*
|
|
17
|
+
* - 0 contributions → undefined (no match rule fired in this grant)
|
|
18
|
+
* - All `{}` (= "any") → `{}` (no restriction)
|
|
19
|
+
* - 1 concrete entry → that entry
|
|
20
|
+
* - N concrete entries → `{ $and: [...] }`
|
|
21
|
+
*/
|
|
22
|
+
export function combineGrantConstraints(parts) {
|
|
23
|
+
if (parts.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
const nonEmpty = parts.filter((p) => Object.keys(p).length > 0);
|
|
26
|
+
if (nonEmpty.length === 0)
|
|
27
|
+
return {};
|
|
28
|
+
if (nonEmpty.length === 1)
|
|
29
|
+
return nonEmpty[0];
|
|
30
|
+
return { $and: [...nonEmpty] };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Cross-grant: OR-merge per-grant constraints with "any wins" semantics.
|
|
34
|
+
*
|
|
35
|
+
* - 0 entries → undefined (no match rule active anywhere)
|
|
36
|
+
* - any `undefined` entry → undefined (a grant without constraint trumps;
|
|
37
|
+
* pushdown stays unrestricted)
|
|
38
|
+
* - any `{}` entry → `{}` ("any" — grants exist but impose no restriction)
|
|
39
|
+
* - 1 entry → that entry as-is
|
|
40
|
+
* - N entries → `{ $or: [...] }`
|
|
41
|
+
*/
|
|
42
|
+
export function aggregateConstraints(entries) {
|
|
43
|
+
if (entries.length === 0)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (entries.some((e) => e === undefined))
|
|
46
|
+
return undefined;
|
|
47
|
+
const concrete = entries;
|
|
48
|
+
if (concrete.some((e) => Object.keys(e).length === 0))
|
|
49
|
+
return {};
|
|
50
|
+
if (concrete.length === 1)
|
|
51
|
+
return concrete[0];
|
|
52
|
+
return { $or: [...concrete] };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Project a source object through a field whitelist.
|
|
56
|
+
* Non-object sources pass through unchanged.
|
|
57
|
+
*/
|
|
58
|
+
export function projectFields(source, fields) {
|
|
59
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
60
|
+
return source;
|
|
61
|
+
}
|
|
62
|
+
const out = {};
|
|
63
|
+
const src = source;
|
|
64
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
65
|
+
if (v === true && k in src)
|
|
66
|
+
out[k] = src[k];
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Merge a filter contribution into the running union.
|
|
72
|
+
* `null` contribution means "this grant has no filter" (all fields allowed)
|
|
73
|
+
* and saturates the union. Once saturated, stays saturated.
|
|
74
|
+
*/
|
|
75
|
+
export function mergeFilterSpec(current, contribution) {
|
|
76
|
+
if (current === null)
|
|
77
|
+
return null;
|
|
78
|
+
if (contribution === null)
|
|
79
|
+
return null;
|
|
80
|
+
return { ...(current ?? {}), ...contribution };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve final `data` from the filter union state and a reference source.
|
|
84
|
+
*
|
|
85
|
+
* - union is `null` → reference source as-is (most permissive grant won)
|
|
86
|
+
* - union is `Record` → project the source through the union
|
|
87
|
+
* - union is `undefined` → fall back (no filter rule fired anywhere)
|
|
88
|
+
*/
|
|
89
|
+
export function resolveFilteredData(union, referenceSource, fallback) {
|
|
90
|
+
if (union === null)
|
|
91
|
+
return referenceSource;
|
|
92
|
+
if (union !== undefined && referenceSource !== undefined) {
|
|
93
|
+
return projectFields(referenceSource, union);
|
|
94
|
+
}
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=aggregation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregation.js","sourceRoot":"","sources":["../aggregation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAA6C;IAE7C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC9C,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAA2D;IAE3D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3D,MAAM,QAAQ,GAAG,OAAiD,CAAC;IACnE,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC9C,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAe,EACf,MAA+B;IAE/B,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,MAAiC,CAAC;IAC9C,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,GAAG;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAYD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAoB,EACpB,YAA4C;IAE5C,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAG,YAAY,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAkB,EAClB,eAAwB,EACxB,QAAiB;IAEjB,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,eAAe,CAAC;IAC3C,IAAI,KAAK,KAAK,SAAS,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QACzD,OAAO,aAAa,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtering utilities for applying field-level permissions
|
|
3
|
+
*/
|
|
4
|
+
import type { FilterSpec } from "./merging.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Apply a filter specification to an object
|
|
7
|
+
*
|
|
8
|
+
* @param obj The object to filter
|
|
9
|
+
* @param filterSpec The filter specification (field: true/false)
|
|
10
|
+
* @returns The filtered object
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* applyFilter(
|
|
14
|
+
* { _id: 1, name: "John", email: "john@example.com", password: "secret" },
|
|
15
|
+
* { _id: true, name: true, email: true }
|
|
16
|
+
* )
|
|
17
|
+
* // → { _id: 1, name: "John", email: "john@example.com" }
|
|
18
|
+
*
|
|
19
|
+
* // Nested filtering
|
|
20
|
+
* applyFilter(
|
|
21
|
+
* { user: { name: "John", password: "secret" } },
|
|
22
|
+
* { user: { name: true } }
|
|
23
|
+
* )
|
|
24
|
+
* // → { user: { name: "John" } }
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyFilter(obj: any, filterSpec: FilterSpec): any;
|
|
27
|
+
/**
|
|
28
|
+
* Pick specific fields from an object
|
|
29
|
+
*
|
|
30
|
+
* @param obj The source object
|
|
31
|
+
* @param fields The fields to pick
|
|
32
|
+
* @returns A new object with only the specified fields
|
|
33
|
+
*/
|
|
34
|
+
export declare function pickFields<T extends Record<string, any>>(obj: T, fields: Record<string, boolean>): Partial<T>;
|
|
35
|
+
//# sourceMappingURL=filtering.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtering.d.ts","sourceRoot":"","sources":["../../core/filtering.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,UAAU,GAAG,GAAG,CA4BjE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACtD,GAAG,EAAE,CAAC,EACN,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,CAAC,CAAC,CAEZ"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filtering utilities for applying field-level permissions
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Apply a filter specification to an object
|
|
6
|
+
*
|
|
7
|
+
* @param obj The object to filter
|
|
8
|
+
* @param filterSpec The filter specification (field: true/false)
|
|
9
|
+
* @returns The filtered object
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* applyFilter(
|
|
13
|
+
* { _id: 1, name: "John", email: "john@example.com", password: "secret" },
|
|
14
|
+
* { _id: true, name: true, email: true }
|
|
15
|
+
* )
|
|
16
|
+
* // → { _id: 1, name: "John", email: "john@example.com" }
|
|
17
|
+
*
|
|
18
|
+
* // Nested filtering
|
|
19
|
+
* applyFilter(
|
|
20
|
+
* { user: { name: "John", password: "secret" } },
|
|
21
|
+
* { user: { name: true } }
|
|
22
|
+
* )
|
|
23
|
+
* // → { user: { name: "John" } }
|
|
24
|
+
*/
|
|
25
|
+
export function applyFilter(obj, filterSpec) {
|
|
26
|
+
if (!obj || typeof obj !== "object") {
|
|
27
|
+
return obj;
|
|
28
|
+
}
|
|
29
|
+
// Handle arrays
|
|
30
|
+
if (Array.isArray(obj)) {
|
|
31
|
+
return obj.map((item) => applyFilter(item, filterSpec));
|
|
32
|
+
}
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, spec] of Object.entries(filterSpec)) {
|
|
35
|
+
if (!(key in obj))
|
|
36
|
+
continue;
|
|
37
|
+
if (spec === true) {
|
|
38
|
+
// Include field as-is
|
|
39
|
+
result[key] = obj[key];
|
|
40
|
+
}
|
|
41
|
+
else if (spec === false) {
|
|
42
|
+
// Exclude field
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
else if (typeof spec === "object") {
|
|
46
|
+
// Nested filter
|
|
47
|
+
result[key] = applyFilter(obj[key], spec);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Pick specific fields from an object
|
|
54
|
+
*
|
|
55
|
+
* @param obj The source object
|
|
56
|
+
* @param fields The fields to pick
|
|
57
|
+
* @returns A new object with only the specified fields
|
|
58
|
+
*/
|
|
59
|
+
export function pickFields(obj, fields) {
|
|
60
|
+
return applyFilter(obj, fields);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=filtering.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtering.js","sourceRoot":"","sources":["../../core/filtering.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,WAAW,CAAC,GAAQ,EAAE,UAAsB;IAC1D,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,OAAO,GAAG,CAAC;IACb,CAAC;IAED,gBAAgB;IAChB,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,MAAM,GAAQ,EAAE,CAAC;IAEvB,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;YAAE,SAAS;QAE5B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,sBAAsB;YACtB,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;YAC1B,gBAAgB;YAChB,SAAS;QACX,CAAC;aAAM,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACpC,gBAAgB;YAChB,MAAM,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,GAAM,EACN,MAA+B;IAE/B,OAAO,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAClC,CAAC"}
|