@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +747 -0
  3. package/aggregation.ts +114 -0
  4. package/core/filtering.ts +70 -0
  5. package/core/matching.ts +82 -0
  6. package/core/merging.ts +143 -0
  7. package/dist/aggregation.d.ts +62 -0
  8. package/dist/aggregation.d.ts.map +1 -0
  9. package/dist/aggregation.js +97 -0
  10. package/dist/aggregation.js.map +1 -0
  11. package/dist/core/filtering.d.ts +35 -0
  12. package/dist/core/filtering.d.ts.map +1 -0
  13. package/dist/core/filtering.js +62 -0
  14. package/dist/core/filtering.js.map +1 -0
  15. package/dist/core/matching.d.ts +31 -0
  16. package/dist/core/matching.d.ts.map +1 -0
  17. package/dist/core/matching.js +75 -0
  18. package/dist/core/matching.js.map +1 -0
  19. package/dist/core/merging.d.ts +29 -0
  20. package/dist/core/merging.d.ts.map +1 -0
  21. package/dist/core/merging.js +124 -0
  22. package/dist/core/merging.js.map +1 -0
  23. package/dist/indirect-aggregation.d.ts +41 -0
  24. package/dist/indirect-aggregation.d.ts.map +1 -0
  25. package/dist/indirect-aggregation.js +185 -0
  26. package/dist/indirect-aggregation.js.map +1 -0
  27. package/dist/indirect-resource.d.ts +126 -0
  28. package/dist/indirect-resource.d.ts.map +1 -0
  29. package/dist/indirect-resource.js +109 -0
  30. package/dist/indirect-resource.js.map +1 -0
  31. package/dist/mod.d.ts +25 -0
  32. package/dist/mod.d.ts.map +1 -0
  33. package/dist/mod.js +25 -0
  34. package/dist/mod.js.map +1 -0
  35. package/dist/mongo-query.d.ts +38 -0
  36. package/dist/mongo-query.d.ts.map +1 -0
  37. package/dist/mongo-query.js +88 -0
  38. package/dist/mongo-query.js.map +1 -0
  39. package/dist/permission.d.ts +57 -0
  40. package/dist/permission.d.ts.map +1 -0
  41. package/dist/permission.js +60 -0
  42. package/dist/permission.js.map +1 -0
  43. package/dist/resource.d.ts +48 -0
  44. package/dist/resource.d.ts.map +1 -0
  45. package/dist/resource.js +298 -0
  46. package/dist/resource.js.map +1 -0
  47. package/dist/rules.d.ts +106 -0
  48. package/dist/rules.d.ts.map +1 -0
  49. package/dist/rules.js +183 -0
  50. package/dist/rules.js.map +1 -0
  51. package/dist/sift/core.d.ts +104 -0
  52. package/dist/sift/core.d.ts.map +1 -0
  53. package/dist/sift/core.js +248 -0
  54. package/dist/sift/core.js.map +1 -0
  55. package/dist/sift/index.d.ts +10 -0
  56. package/dist/sift/index.d.ts.map +1 -0
  57. package/dist/sift/index.js +18 -0
  58. package/dist/sift/index.js.map +1 -0
  59. package/dist/sift/operations.d.ts +87 -0
  60. package/dist/sift/operations.d.ts.map +1 -0
  61. package/dist/sift/operations.js +257 -0
  62. package/dist/sift/operations.js.map +1 -0
  63. package/dist/sift/utils.d.ts +12 -0
  64. package/dist/sift/utils.d.ts.map +1 -0
  65. package/dist/sift/utils.js +80 -0
  66. package/dist/sift/utils.js.map +1 -0
  67. package/dist/system.d.ts +113 -0
  68. package/dist/system.d.ts.map +1 -0
  69. package/dist/system.js +712 -0
  70. package/dist/system.js.map +1 -0
  71. package/dist/target.d.ts +18 -0
  72. package/dist/target.d.ts.map +1 -0
  73. package/dist/target.js +41 -0
  74. package/dist/target.js.map +1 -0
  75. package/dist/types.d.ts +345 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +10 -0
  78. package/dist/types.js.map +1 -0
  79. package/indirect-aggregation.ts +216 -0
  80. package/indirect-resource.ts +205 -0
  81. package/mod.ts +81 -0
  82. package/mongo-query.ts +94 -0
  83. package/package.json +58 -0
  84. package/permission.ts +88 -0
  85. package/resource.ts +352 -0
  86. package/rules.ts +241 -0
  87. package/sift/MIT-LICENSE.txt +20 -0
  88. package/sift/core.ts +551 -0
  89. package/sift/index.ts +62 -0
  90. package/sift/operations.ts +449 -0
  91. package/sift/utils.ts +96 -0
  92. package/system.ts +974 -0
  93. package/target.ts +84 -0
  94. 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
+ }
@@ -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
+ }
@@ -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"}