@nmnmcc/ability 0.3.0 → 1.0.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 (66) hide show
  1. package/README.md +199 -31
  2. package/dist/Authorizer.d.ts +64 -39
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +291 -6
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +444 -65
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +1007 -25
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Domain.d.ts +27 -17
  11. package/dist/Domain.d.ts.map +1 -1
  12. package/dist/Domain.js +52 -10
  13. package/dist/Domain.js.map +1 -1
  14. package/dist/HttpApi.d.ts +84 -40
  15. package/dist/HttpApi.d.ts.map +1 -1
  16. package/dist/HttpApi.js +103 -13
  17. package/dist/HttpApi.js.map +1 -1
  18. package/dist/Policy.d.ts +314 -56
  19. package/dist/Policy.d.ts.map +1 -1
  20. package/dist/Policy.js +1215 -38
  21. package/dist/Policy.js.map +1 -1
  22. package/dist/Principal.js +2 -2
  23. package/dist/Principal.js.map +1 -1
  24. package/dist/Resource.d.ts +171 -20
  25. package/dist/Resource.d.ts.map +1 -1
  26. package/dist/Resource.js +788 -15
  27. package/dist/Resource.js.map +1 -1
  28. package/dist/Rule.d.ts +115 -29
  29. package/dist/Rule.d.ts.map +1 -1
  30. package/dist/Rule.js +219 -12
  31. package/dist/Rule.js.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +2 -2
  34. package/src/Authorizer.ts +625 -61
  35. package/src/Condition.ts +2403 -112
  36. package/src/Domain.ts +80 -23
  37. package/src/HttpApi.ts +225 -25
  38. package/src/Policy.ts +2836 -96
  39. package/src/Principal.ts +2 -2
  40. package/src/Resource.ts +1544 -29
  41. package/src/Rule.ts +463 -45
  42. package/dist/internal/authorizer.d.ts +0 -28
  43. package/dist/internal/authorizer.d.ts.map +0 -1
  44. package/dist/internal/authorizer.js +0 -153
  45. package/dist/internal/authorizer.js.map +0 -1
  46. package/dist/internal/condition.d.ts +0 -11
  47. package/dist/internal/condition.d.ts.map +0 -1
  48. package/dist/internal/condition.js +0 -393
  49. package/dist/internal/condition.js.map +0 -1
  50. package/dist/internal/domain.js +0 -34
  51. package/dist/internal/domain.js.map +0 -1
  52. package/dist/internal/httpApi.js +0 -41
  53. package/dist/internal/httpApi.js.map +0 -1
  54. package/dist/internal/policy.js +0 -524
  55. package/dist/internal/policy.js.map +0 -1
  56. package/dist/internal/resource.js +0 -66
  57. package/dist/internal/resource.js.map +0 -1
  58. package/dist/internal/rule.js +0 -131
  59. package/dist/internal/rule.js.map +0 -1
  60. package/src/internal/authorizer.ts +0 -271
  61. package/src/internal/condition.ts +0 -607
  62. package/src/internal/domain.ts +0 -56
  63. package/src/internal/httpApi.ts +0 -84
  64. package/src/internal/policy.ts +0 -815
  65. package/src/internal/resource.ts +0 -98
  66. package/src/internal/rule.ts +0 -203
package/dist/Policy.js CHANGED
@@ -1,29 +1,167 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { TypeId as TypeId$1, all as all$1, any as any$1, ceiling as ceiling$1, decide as decide$1, decideSync as decideSync$1, define as define$1, empty as empty$1, explain as explain$1, firstOf as firstOf$1, isPolicy as isPolicy$1, permittedFields as permittedFields$1, residual as residual$1, rules as rules$1, schema as schema$1 } from "./internal/policy.js";
2
+ import { domainOf, fieldPaths, hasGetter, hasPath, isPlainField, kindOf } from "./Resource.js";
3
+ import { always as always$1, and, andAll, builder, evaluate, fields, hasPredicate, isAst, never as never$1, not, orAll, partial, provideContext as provideContext$1, satisfiable, schema as schema$1, simplifier, simplify, toJsonValue } from "./Condition.js";
4
+ import { Allow, Deny, NotApplicable } from "./Decision.js";
5
+ import { descriptorOf } from "./Domain.js";
6
+ import { builder as builder$1, domainOf as domainOf$1, isRule, make, matchesInside, matchesTarget } from "./Rule.js";
7
+ import * as Arr from "effect/Array";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Option from "effect/Option";
10
+ import * as Schema from "effect/Schema";
11
+ import * as Data from "effect/Data";
12
+ import { dual } from "effect/Function";
13
+ import * as HashMap from "effect/HashMap";
14
+ import * as HashSet from "effect/HashSet";
15
+ import { hasProperty } from "effect/Predicate";
16
+ import * as SchemaIssue from "effect/SchemaIssue";
17
+ import * as SchemaTransformation from "effect/SchemaTransformation";
18
+ import * as Chunk from "effect/Chunk";
19
+ import * as Ref from "effect/Ref";
3
20
  // src/Policy.ts
4
21
  var Policy_exports = /* @__PURE__ */ __exportAll({
5
- TypeId: () => TypeId,
6
22
  all: () => all,
7
23
  any: () => any,
8
24
  ceiling: () => ceiling,
25
+ coverage: () => coverage,
9
26
  decide: () => decide,
10
27
  decideSync: () => decideSync,
11
28
  define: () => define,
12
29
  empty: () => empty,
13
30
  explain: () => explain,
14
31
  firstOf: () => firstOf,
32
+ fitsKind: () => fitsKind,
15
33
  isPolicy: () => isPolicy,
16
34
  permittedFields: () => permittedFields,
35
+ provideContext: () => provideContext,
17
36
  residual: () => residual,
37
+ residuals: () => residuals,
18
38
  rules: () => rules,
19
- schema: () => schema
39
+ schema: () => schema,
40
+ scopeHasValue: () => scopeHasValue,
41
+ scopeIsEmpty: () => scopeIsEmpty
20
42
  });
43
+ const nodes = Data.taggedEnum();
21
44
  /**
45
+ * The most nodes that the JSON form of a policy may have. A tree may use one
46
+ * child in many places, and the JSON form writes it out each time, so a small
47
+ * tree may have a very large JSON form.
48
+ */
49
+ const maxEncodedNodes = 1e5;
50
+ /**
51
+ * The number of nodes of the JSON form of a condition. A shared child counts
52
+ * one time for each place, as the JSON form writes it out each time.
53
+ */
54
+ const conditionSize = (ast, memo) => remember(memo, ast, () => {
55
+ switch (ast._tag) {
56
+ case "And":
57
+ case "Or": return Arr.reduce(ast.conditions, 1, (size, child) => size + conditionSize(child, memo));
58
+ case "Not": return 1 + conditionSize(ast.condition, memo);
59
+ default: return 1;
60
+ }
61
+ });
62
+ /**
63
+ * The number of nodes of the JSON form of a tree, with the conditions of its
64
+ * rules.
65
+ */
66
+ const encodedSize = (node, memo = /* @__PURE__ */ new Map(), conditions = /* @__PURE__ */ new Map()) => remember(memo, node, () => {
67
+ switch (node._tag) {
68
+ case "Rules": return Arr.reduce(node.rules, 1, (size, value) => size + 1 + (value.when === void 0 ? 0 : conditionSize(value.when, conditions)));
69
+ case "All":
70
+ case "Any":
71
+ case "FirstOf": return Arr.reduce(node.policies, 1, (size, child) => size + encodedSize(child, memo, conditions));
72
+ case "Ceiling": return 1 + encodedSize(node.cap, memo, conditions) + encodedSize(node.policy, memo, conditions);
73
+ }
74
+ });
75
+ /** Gives the JSON form of a tree, when it is not too large. */
76
+ const encodeTree = (node) => {
77
+ if (encodedSize(node) > maxEncodedNodes) throw new globalThis.Error(tooLargeMessage);
78
+ return encodeNode(node);
79
+ };
80
+ const tooLargeMessage = `The JSON form of this policy would have more than ${String(maxEncodedNodes)} nodes`;
81
+ const encodeNode = (node) => {
82
+ switch (node._tag) {
83
+ case "Rules": return {
84
+ _tag: "Rules",
85
+ algorithm: node.algorithm,
86
+ rules: node.rules.map(({ domain: _domain, ...value }) => ({ ...value }))
87
+ };
88
+ case "All":
89
+ case "Any":
90
+ case "FirstOf": return {
91
+ _tag: node._tag,
92
+ policies: node.policies.map(encodeNode)
93
+ };
94
+ case "Ceiling": return {
95
+ _tag: "Ceiling",
96
+ cap: encodeNode(node.cap),
97
+ policy: encodeNode(node.policy)
98
+ };
99
+ }
100
+ };
101
+ /**
102
+ * The tree of a policy as a value that `JSON.stringify` keeps with no loss.
103
+ * See `Condition.toJsonValue`.
104
+ */
105
+ const jsonNode = (node, meta) => {
106
+ const json = (child) => jsonNode(child, meta);
107
+ switch (node._tag) {
108
+ case "Rules": return {
109
+ ...node,
110
+ rules: Arr.map(node.rules, (value) => ({
111
+ ...value,
112
+ ...value.when === void 0 ? {} : { when: toJsonValue(value.when) },
113
+ ...value.meta === void 0 || meta === void 0 ? {} : { meta: Schema.encodeUnknownSync(meta)(value.meta) }
114
+ }))
115
+ };
116
+ case "All":
117
+ case "Any":
118
+ case "FirstOf": return {
119
+ ...node,
120
+ policies: Arr.map(node.policies, json)
121
+ };
122
+ case "Ceiling": return {
123
+ ...node,
124
+ cap: json(node.cap),
125
+ policy: json(node.policy)
126
+ };
127
+ }
128
+ };
129
+ /**
130
+ * The class of every policy value. Two policies of one domain with the same
131
+ * tree are equal. As JSON, a policy is its tree.
132
+ */
133
+ var PolicyData = class extends Data.Class {
134
+ toJSON() {
135
+ return jsonNode(encodeTree(this.node), this.domain.meta);
136
+ }
137
+ };
138
+ /** The node of a policy, with no types, for the code below. */
139
+ const nodeOf = (policy) => policy.node;
140
+ const fromNode = (domain, node) => new PolicyData({
141
+ domain,
142
+ node
143
+ });
144
+ /**
145
+ * Makes a policy from a list of rules.
146
+ *
22
147
  * @stability stable
23
- * @category type IDs
148
+ * @category constructors
24
149
  * @since 0.3.0
25
150
  */
26
- const TypeId = TypeId$1;
151
+ const rules = (domain, values, options) => {
152
+ const node = nodes.Rules({
153
+ algorithm: options?.algorithm ?? "denyOverrides",
154
+ rules: Array.from(values)
155
+ });
156
+ if (!Arr.every(node.rules, isRule)) throw new globalThis.Error("A value in the list is not a rule");
157
+ if (Arr.some(node.rules, (value) => {
158
+ const origin = domainOf$1(value);
159
+ return origin !== void 0 && origin !== domain;
160
+ })) throw new globalThis.Error("A rule comes from the builder of another domain");
161
+ const issues = validate(domain, encodeNode(node));
162
+ if (issues.length > 0) throw new globalThis.Error(`A rule does not fit the domain: ${issues.map((issue) => issue.issue).join("; ")}`);
163
+ return fromNode(domain, node);
164
+ };
27
165
  /**
28
166
  * Makes a policy from a generator function that gives rules. It is pure and it
29
167
  * gives its result at once.
@@ -31,9 +169,9 @@ const TypeId = TypeId$1;
31
169
  * @example
32
170
  * ```ts
33
171
  * const policy = Policy.define(App)(function* (_) {
34
- * yield* _.allow("read", Bot)
35
- * yield* _.allow(["update", "delete"], Bot, {
36
- * when: ($) => $.eq("ownerUserId", $.principal("userId")),
172
+ * yield* _.allow("read", Document)
173
+ * yield* _.allow(["update", "delete"], Document, {
174
+ * when: ($) => $.eq("ownerId", $.principal("id")),
37
175
  * })
38
176
  * })
39
177
  * ```
@@ -42,23 +180,50 @@ const TypeId = TypeId$1;
42
180
  * @category constructors
43
181
  * @since 0.3.0
44
182
  */
45
- const define = define$1;
183
+ const define = (domain, options) => (body) => {
184
+ const iterator = body(builder$1(domain));
185
+ const collected = Arr.unfold(iterator.next(), (state) => state.done === true ? Option.none() : Option.some([state.value, iterator.next(state.value)]));
186
+ return rules(domain, collected, options);
187
+ };
46
188
  /**
47
- * Makes a policy from a list of rules.
189
+ * A policy with no rules. It is not applicable to any request.
48
190
  *
49
191
  * @stability stable
50
192
  * @category constructors
51
193
  * @since 0.3.0
52
194
  */
53
- const rules = rules$1;
195
+ const empty = (domain) => rules(domain, []);
196
+ const sameDomain = (policies) => {
197
+ if (!Arr.every(policies, isPolicy)) throw new globalThis.Error("A value in the list is not a policy");
198
+ const domain = policies[0].domain;
199
+ if (!Arr.every(policies, (policy) => policy.domain === domain)) throw new globalThis.Error("Policies from two domains can not be put together");
200
+ return domain;
201
+ };
54
202
  /**
55
- * A policy with no rules. It is not applicable to any request.
56
- *
57
- * @stability stable
58
- * @category constructors
59
- * @since 0.3.0
203
+ * The most levels in a policy tree. A walk over a tree calls itself for each
204
+ * level, so a much deeper tree could fill the stack and stop a check.
205
+ */
206
+ const maxDepth = 256;
207
+ /** The number of levels of a policy tree. */
208
+ const heightOf = (node, memo = /* @__PURE__ */ new Map()) => remember(memo, node, () => {
209
+ const height = (child) => heightOf(child, memo);
210
+ switch (node._tag) {
211
+ case "Rules": return 1;
212
+ case "All":
213
+ case "Any":
214
+ case "FirstOf": return 1 + Arr.reduce(node.policies, 0, (top, child) => Math.max(top, height(child)));
215
+ case "Ceiling": return 1 + Math.max(height(node.cap), height(node.policy));
216
+ }
217
+ });
218
+ /**
219
+ * Makes a policy from a new node. Its children come from policies, so each
220
+ * has at most `maxDepth` levels, and the walk of `heightOf` is safe.
60
221
  */
61
- const empty = empty$1;
222
+ const fromCombined = (domain, node) => {
223
+ if (heightOf(node) > maxDepth) throw new globalThis.Error(`A policy tree may have at most ${maxDepth} levels`);
224
+ return fromNode(domain, node);
225
+ };
226
+ const combine = (tag) => (policies) => fromCombined(sameDomain(policies), nodes[tag]({ policies: policies.map(nodeOf) }));
62
227
  /**
63
228
  * Every policy must allow, and one deny is enough to deny. With some policies
64
229
  * not applicable and none that deny, the result is not applicable.
@@ -67,7 +232,7 @@ const empty = empty$1;
67
232
  * @category combinators
68
233
  * @since 0.3.0
69
234
  */
70
- const all = all$1;
235
+ const all = combine("All");
71
236
  /**
72
237
  * One allow is enough. Without an allow, one deny gives a deny.
73
238
  *
@@ -75,7 +240,7 @@ const all = all$1;
75
240
  * @category combinators
76
241
  * @since 0.3.0
77
242
  */
78
- const any = any$1;
243
+ const any = combine("Any");
79
244
  /**
80
245
  * The first policy that allows or denies gives the answer.
81
246
  *
@@ -83,7 +248,7 @@ const any = any$1;
83
248
  * @category combinators
84
249
  * @since 0.3.0
85
250
  */
86
- const firstOf = firstOf$1;
251
+ const firstOf = combine("FirstOf");
87
252
  /**
88
253
  * Puts a limit on a policy. A deny from `cap` always wins. An allow from `cap`
89
254
  * does nothing.
@@ -92,7 +257,170 @@ const firstOf = firstOf$1;
92
257
  * @category combinators
93
258
  * @since 0.3.0
94
259
  */
95
- const ceiling = ceiling$1;
260
+ const ceiling = dual(2, (policy, cap) => fromCombined(sameDomain([policy, cap]), nodes.Ceiling({
261
+ cap: nodeOf(cap),
262
+ policy: nodeOf(policy)
263
+ })));
264
+ /**
265
+ * Gives the answer for a key from a memo, or works it out and keeps it. One
266
+ * node may be a child of many nodes, so each walk over a tree keeps its work
267
+ * on a node in a memo that the walk makes for itself. No memo lives longer
268
+ * than one call.
269
+ *
270
+ * The memo is a native `Map`, because it must find a node by the node itself.
271
+ * An Effect `HashMap` finds a key by `Equal`, and the nodes are Effect data,
272
+ * so it would hash the whole structure of a node, and a shared child again for
273
+ * each path to it.
274
+ */
275
+ const remember = (memo, key, work) => {
276
+ if (memo.has(key)) return memo.get(key);
277
+ const result = work();
278
+ memo.set(key, result);
279
+ return result;
280
+ };
281
+ const provideNode = (node, context, memo = {
282
+ done: /* @__PURE__ */ new Map(),
283
+ predicates: /* @__PURE__ */ new Map()
284
+ }) => !nodeHasPredicate(node, memo.predicates) ? node : remember(memo.done, node, () => provideOnce(node, context, memo));
285
+ const provideOnce = (node, context, memo) => {
286
+ switch (node._tag) {
287
+ case "Rules": return nodes.Rules({
288
+ algorithm: node.algorithm,
289
+ rules: node.rules.map((value) => value.when === void 0 || !hasPredicate(value.when) ? value : make({
290
+ ...value,
291
+ when: provideContext$1(value.when, context)
292
+ }))
293
+ });
294
+ case "All":
295
+ case "Any":
296
+ case "FirstOf": return nodes[node._tag]({ policies: Arr.map(node.policies, (child) => provideNode(child, context, memo)) });
297
+ case "Ceiling": return nodes.Ceiling({
298
+ cap: provideNode(node.cap, context, memo),
299
+ policy: provideNode(node.policy, context, memo)
300
+ });
301
+ }
302
+ };
303
+ /**
304
+ * Gives the services in `context` to every predicate of a policy, so the
305
+ * policy needs no services when it runs.
306
+ *
307
+ * @stability stable
308
+ * @category services
309
+ * @since 1.0.0
310
+ */
311
+ const provideContext = dual(2, (policy, context) => fromNode(policy.domain, provideNode(nodeOf(policy), context)));
312
+ const ruleApplies = (value, context, path) => {
313
+ if (!matchesTarget(value, context)) return Effect.succeed(false);
314
+ const test = value.when === void 0 ? Effect.succeed(true) : evaluate(value.when, context.input);
315
+ const trace = context.trace;
316
+ return trace === void 0 ? test : Effect.tap(test, (applies) => Ref.update(trace, Chunk.append({
317
+ path,
318
+ rule: value,
319
+ applies
320
+ })));
321
+ };
322
+ const fromRule = (value) => value._tag === "Allow" ? Allow({ rule: value }) : value.reason === void 0 ? Deny({ rule: value }) : Deny({
323
+ rule: value,
324
+ reason: value.reason
325
+ });
326
+ /** Gives the first rule that applies, in order. */
327
+ const firstApplying = (candidates, context, path) => Effect.map(Effect.findFirst(candidates, ([value, index]) => ruleApplies(value, context, [...path, index])), Option.map(([value]) => value));
328
+ /** Gives the first result that is found, and tests `next` only when none is. */
329
+ const orElse = (first, next) => Effect.filterOrElse(first, Option.isSome, next);
330
+ const decideRules = (node, context, path) => {
331
+ const indexed = Arr.map(node.rules, (value, index) => [value, index]);
332
+ const denies = Arr.filter(indexed, ([value]) => value._tag === "Deny");
333
+ const allows = Arr.filter(indexed, ([value]) => value._tag === "Allow");
334
+ const first = (candidates) => firstApplying(candidates, context, path);
335
+ const found = (() => {
336
+ switch (node.algorithm) {
337
+ case "denyOverrides": return orElse(first(denies), () => first(allows));
338
+ case "permitOverrides": return orElse(first(allows), () => first(denies));
339
+ case "firstApplicable": return first(indexed);
340
+ case "lastApplicable": return first(Arr.reverse(indexed));
341
+ }
342
+ })();
343
+ return Effect.map(found, Option.match({
344
+ onNone: () => NotApplicable,
345
+ onSome: fromRule
346
+ }));
347
+ };
348
+ const decideNode = (node, context, path) => {
349
+ if (usesPredicate(node, context)) return decideOnce(node, context, path);
350
+ const cached = context.decisions.get(node);
351
+ return cached !== void 0 ? Effect.succeed(cached) : Effect.tap(decideOnce(node, context, path), (decision) => Effect.sync(() => context.decisions.set(node, decision)));
352
+ };
353
+ /**
354
+ * Decides the children in order. After each child, `step` stops with a
355
+ * decision, or gives the state for the next child. After the last child,
356
+ * `end` gives the decision. A child after a stop is not decided, so its
357
+ * predicates do not run.
358
+ */
359
+ const walkChildren = (children, context, path, start, step, end) => {
360
+ const go = (index, state) => index === children.length ? Effect.succeed(end(state)) : Effect.flatMap(decideNode(children[index], context, [...path, index]), (decision) => {
361
+ const next = step(state, decision);
362
+ return next._tag === "Stop" ? Effect.succeed(next.decision) : go(index + 1, next.state);
363
+ });
364
+ return go(0, start);
365
+ };
366
+ const stop = (decision) => ({
367
+ _tag: "Stop",
368
+ decision
369
+ });
370
+ const next = (state) => ({
371
+ _tag: "Next",
372
+ state
373
+ });
374
+ const decideOnce = (node, context, path) => {
375
+ switch (node._tag) {
376
+ case "Rules": return decideRules(node, context, path);
377
+ case "All": return walkChildren(node.policies, context, path, {
378
+ firstAllow: Option.none(),
379
+ allAllow: node.policies.length > 0
380
+ }, (state, decision) => decision._tag === "Deny" ? stop(decision) : next({
381
+ firstAllow: Option.orElse(state.firstAllow, () => decision._tag === "Allow" ? Option.some(decision) : Option.none()),
382
+ allAllow: state.allAllow && decision._tag === "Allow"
383
+ }), (state) => state.allAllow ? Option.getOrElse(state.firstAllow, () => NotApplicable) : NotApplicable);
384
+ case "Any": return walkChildren(node.policies, context, path, Option.none(), (firstDeny, decision) => decision._tag === "Allow" ? stop(decision) : next(Option.orElse(firstDeny, () => decision._tag === "Deny" ? Option.some(decision) : Option.none())), (firstDeny) => Option.getOrElse(firstDeny, () => NotApplicable));
385
+ case "FirstOf": return walkChildren(node.policies, context, path, void 0, (_, decision) => decision._tag === "NotApplicable" ? next(void 0) : stop(decision), () => NotApplicable);
386
+ case "Ceiling": return Effect.filterOrElse(decideNode(node.cap, context, [...path, 0]), (capped) => capped._tag === "Deny", () => decideNode(node.policy, context, [...path, 1]));
387
+ }
388
+ };
389
+ /**
390
+ * Tests if a request fits its kind: the action is an action of the kind, and
391
+ * the field, if there is one, is a field of the kind. The types check this,
392
+ * but a value with a union type, or a value from outside, may get past them.
393
+ * A rule for every action (`"*"`) must not cover an action that the kind does
394
+ * not have, so such a request is not applicable.
395
+ *
396
+ * @stability stable
397
+ * @category predicates
398
+ * @since 1.0.0
399
+ */
400
+ const fitsKind = (descriptor, request) => descriptor.actions.includes(request.action) && (request.field === void 0 || descriptor.fieldPaths.includes(request.field) || hasPath(descriptor.schema, request.field) || hasGetter(descriptor.schema, request.field));
401
+ const makeContext = (domain, request, trace) => {
402
+ const kind = kindOf(request.resource);
403
+ const descriptor = kind === void 0 ? void 0 : descriptorOf(domain, kind);
404
+ if (kind === void 0 || descriptor === void 0 || !fitsKind(descriptor, request)) return;
405
+ return {
406
+ action: request.action,
407
+ kind,
408
+ field: request.field,
409
+ input: {
410
+ value: request.resource,
411
+ principal: request.principal,
412
+ path: descriptor.path
413
+ },
414
+ descriptor,
415
+ trace,
416
+ predicates: /* @__PURE__ */ new Map(),
417
+ decisions: /* @__PURE__ */ new Map()
418
+ };
419
+ };
420
+ const decideAny = (policy, request) => Effect.suspend(() => {
421
+ const context = makeContext(policy.domain, request, void 0);
422
+ return context === void 0 ? Effect.succeed(NotApplicable) : decideNode(nodeOf(policy), context, []);
423
+ });
96
424
  /**
97
425
  * Gives the decision of a policy for a request.
98
426
  *
@@ -100,7 +428,7 @@ const ceiling = ceiling$1;
100
428
  * @category evaluation
101
429
  * @since 0.3.0
102
430
  */
103
- const decide = decide$1;
431
+ const decide = dual(2, decideAny);
104
432
  /**
105
433
  * Gives the decision of a policy with no predicates, at once.
106
434
  *
@@ -108,47 +436,883 @@ const decide = decide$1;
108
436
  * @category evaluation
109
437
  * @since 0.3.0
110
438
  */
111
- const decideSync = decideSync$1;
439
+ const decideSync = dual(2, (policy, request) => Effect.runSync(decideAny(policy, request)));
112
440
  /**
113
441
  * Gives the decision of a policy and the rules that were tested to make it.
114
442
  *
443
+ * One node may be a child of many nodes. A node with no predicate that the
444
+ * request may use gives the same decision each time, so its rules are tested,
445
+ * and are in the steps, only the first time. So a small tree with many shared
446
+ * nodes gives a short list of steps.
447
+ *
115
448
  * @stability stable
116
449
  * @category evaluation
117
450
  * @since 0.3.0
118
451
  */
119
- const explain = explain$1;
452
+ const explain = dual(2, (policy, request) => Effect.gen(function* () {
453
+ const trace = yield* Ref.make(Chunk.empty());
454
+ const context = makeContext(policy.domain, request, trace);
455
+ if (context === void 0) return {
456
+ decision: NotApplicable,
457
+ steps: []
458
+ };
459
+ return {
460
+ decision: yield* decideNode(nodeOf(policy), context, []),
461
+ steps: Chunk.toReadonlyArray(yield* Ref.get(trace))
462
+ };
463
+ }));
464
+ /**
465
+ * Tests if a deny rule of the request names a part inside `field`.
466
+ */
467
+ const deniesInside = (node, target, field, seen = /* @__PURE__ */ new Set()) => {
468
+ if (seen.has(node)) return false;
469
+ seen.add(node);
470
+ switch (node._tag) {
471
+ case "Rules": return node.rules.some((value) => value._tag === "Deny" && value.fields !== void 0 && value.fields.some((pattern) => matchesInside(pattern, field)) && (value.actions.includes("*") || value.actions.includes(target.action)) && (value.resources.includes("*") || value.resources.includes(target.kind)));
472
+ case "All":
473
+ case "Any":
474
+ case "FirstOf": return node.policies.some((child) => deniesInside(child, target, field, seen));
475
+ case "Ceiling": return deniesInside(node.cap, target, field, seen) || deniesInside(node.policy, target, field, seen);
476
+ }
477
+ };
478
+ /**
479
+ * Gives the fields of a resource value that the policy lets a request use. It
480
+ * tests each field of the resource Schema, with dots for nested fields.
481
+ *
482
+ * A field that holds more than one value, such as a record, is left out when a
483
+ * deny rule names a part of it. The list can not name that part, and the whole
484
+ * field would show it.
485
+ *
486
+ * @stability stable
487
+ * @category evaluation
488
+ * @since 0.3.0
489
+ */
490
+ const permittedFields = dual(2, (policy, request) => {
491
+ const kind = kindOf(request.resource);
492
+ const descriptor = kind === void 0 ? void 0 : descriptorOf(policy.domain, kind);
493
+ if (descriptor === void 0) return Effect.succeed([]);
494
+ return Effect.filter(descriptor.fieldPaths, (field) => Effect.map(decideAny(policy, {
495
+ ...request,
496
+ field
497
+ }), (result) => result._tag === "Allow" && !(descriptor.openPaths.includes(field) && deniesInside(nodeOf(policy), {
498
+ ...request,
499
+ kind: descriptor.kind
500
+ }, field))));
501
+ });
502
+ const always = always$1;
503
+ const never = never$1;
504
+ /**
505
+ * The most nodes that a residual condition may have. A policy tree may use a
506
+ * condition of a child two times, in its allow condition and in its deny
507
+ * condition, so a small policy may give a very large condition. Past this
508
+ * size, `residual` throws an error.
509
+ */
510
+ const maxResidualSize = 1e4;
511
+ /**
512
+ * The most levels that a residual condition may have. The rules of an ordered
513
+ * algorithm, such as `firstApplicable`, give one level for each run of rules
514
+ * with one tag. A walk over a condition calls itself for each level, so a much
515
+ * deeper condition could fill the stack. Past this depth, `residual` throws an
516
+ * error.
517
+ */
518
+ const maxResidualDepth = 500;
519
+ /**
520
+ * A residual condition is too large. See {@link maxResidualSize} and
521
+ * {@link maxResidualDepth}.
522
+ */
523
+ var TooLarge = class {};
524
+ const makeBuild = () => {
525
+ const simplify = simplifier();
526
+ const sizes = /* @__PURE__ */ new Map();
527
+ /** Gives the number of nodes in a condition, as JSON would have them. */
528
+ const sizeOf = (ast) => remember(sizes, ast, () => {
529
+ switch (ast._tag) {
530
+ case "And":
531
+ case "Or": return Arr.reduce(ast.conditions, 1, (size, child) => size + sizeOf(child));
532
+ case "Not": return 1 + sizeOf(ast.condition);
533
+ default: return 1;
534
+ }
535
+ });
536
+ const depths = /* @__PURE__ */ new Map();
537
+ /**
538
+ * Gives the number of levels in a condition. Each child went through
539
+ * `limit`, so this walk is not deeper than `maxResidualDepth`.
540
+ */
541
+ const depthOf = (ast) => remember(depths, ast, () => {
542
+ switch (ast._tag) {
543
+ case "And":
544
+ case "Or": return 1 + Arr.reduce(ast.conditions, 0, (top, child) => Math.max(top, depthOf(child)));
545
+ case "Not": return 1 + depthOf(ast.condition);
546
+ default: return 1;
547
+ }
548
+ });
549
+ const limit = (ast) => {
550
+ if (sizeOf(ast) > maxResidualSize || depthOf(ast) > maxResidualDepth) throw new TooLarge();
551
+ return ast;
552
+ };
553
+ return {
554
+ and: (values) => limit(simplify(andAll(values))),
555
+ or: (values) => limit(simplify(orAll(values))),
556
+ not: (value) => limit(simplify(not(value)))
557
+ };
558
+ };
559
+ const ruleCondition = (value, context) => !matchesTarget(value, context) ? never : value.when === void 0 ? always : partial(value.when, context.input);
560
+ /**
561
+ * Gives the conditions for "the first rule that applies is an allow rule" and
562
+ * "the first rule that applies is a deny rule".
563
+ */
564
+ const firstApplicableResidual = (values, context) => {
565
+ const { and, not, or } = context.build;
566
+ const runs = Arr.isReadonlyArrayNonEmpty(values) ? Arr.groupWith(values, (a, b) => a._tag === b._tag) : [];
567
+ return Arr.reduceRight(runs, {
568
+ allow: never,
569
+ deny: never
570
+ }, (rest, run) => {
571
+ const applies = or(Arr.map(run, (value) => ruleCondition(value, context)));
572
+ return run[0]._tag === "Allow" ? {
573
+ allow: or([applies, rest.allow]),
574
+ deny: and([not(applies), rest.deny])
575
+ } : {
576
+ allow: and([not(applies), rest.allow]),
577
+ deny: or([applies, rest.deny])
578
+ };
579
+ });
580
+ };
581
+ const residualNode = (node, context) => remember(context.pairs, node, () => residualOnce(node, context));
582
+ const residualOnce = (node, context) => {
583
+ const { and, not, or } = context.build;
584
+ switch (node._tag) {
585
+ case "Rules": switch (node.algorithm) {
586
+ case "denyOverrides": {
587
+ const deny = or(node.rules.filter((value) => value._tag === "Deny").map((value) => ruleCondition(value, context)));
588
+ return {
589
+ allow: and([not(deny), or(node.rules.filter((value) => value._tag === "Allow").map((value) => ruleCondition(value, context)))]),
590
+ deny
591
+ };
592
+ }
593
+ case "permitOverrides": {
594
+ const allow = or(node.rules.filter((value) => value._tag === "Allow").map((value) => ruleCondition(value, context)));
595
+ return {
596
+ allow,
597
+ deny: and([not(allow), or(node.rules.filter((value) => value._tag === "Deny").map((value) => ruleCondition(value, context)))])
598
+ };
599
+ }
600
+ case "firstApplicable": return firstApplicableResidual(node.rules, context);
601
+ case "lastApplicable": return firstApplicableResidual(node.rules.slice().reverse(), context);
602
+ }
603
+ case "All": {
604
+ if (node.policies.length === 0) return {
605
+ allow: never,
606
+ deny: never
607
+ };
608
+ const pairs = node.policies.map((child) => residualNode(child, context));
609
+ const deny = or(pairs.map((pair) => pair.deny));
610
+ return {
611
+ allow: and([...pairs.map((pair) => pair.allow), not(deny)]),
612
+ deny
613
+ };
614
+ }
615
+ case "Any": {
616
+ const pairs = node.policies.map((child) => residualNode(child, context));
617
+ const allow = or(pairs.map((pair) => pair.allow));
618
+ return {
619
+ allow,
620
+ deny: and([not(allow), or(pairs.map((pair) => pair.deny))])
621
+ };
622
+ }
623
+ case "FirstOf": return Arr.reduceRight(node.policies, {
624
+ allow: never,
625
+ deny: never
626
+ }, (rest, child) => {
627
+ const pair = residualNode(child, context);
628
+ return {
629
+ allow: or([pair.allow, and([not(pair.deny), rest.allow])]),
630
+ deny: or([pair.deny, and([not(pair.allow), rest.deny])])
631
+ };
632
+ });
633
+ case "Ceiling": {
634
+ const cap = residualNode(node.cap, context);
635
+ const inner = residualNode(node.policy, context);
636
+ return {
637
+ allow: and([not(cap.deny), inner.allow]),
638
+ deny: or([cap.deny, inner.deny])
639
+ };
640
+ }
641
+ }
642
+ };
643
+ /** The name of the predicate of a residual condition that runs `decide`. */
644
+ const policyPredicate = "policy";
645
+ /** Tests if a rule of a policy tree has a predicate. */
646
+ const nodeHasPredicate = (node, memo = /* @__PURE__ */ new Map()) => remember(memo, node, () => {
647
+ const test = (child) => nodeHasPredicate(child, memo);
648
+ switch (node._tag) {
649
+ case "Rules": return Arr.some(node.rules, (value) => value.when !== void 0 && hasPredicate(value.when));
650
+ case "All":
651
+ case "Any":
652
+ case "FirstOf": return Arr.some(node.policies, test);
653
+ case "Ceiling": return test(node.cap) || test(node.policy);
654
+ }
655
+ });
656
+ const usesPredicate = (node, context) => remember(context.predicates, node, () => usesPredicateOnce(node, context));
657
+ const usesPredicateOnce = (node, context) => {
658
+ switch (node._tag) {
659
+ case "Rules": return node.rules.some((value) => value.when !== void 0 && hasPredicate(value.when) && matchesTarget(value, context));
660
+ case "All":
661
+ case "Any":
662
+ case "FirstOf": return node.policies.some((child) => usesPredicate(child, context));
663
+ case "Ceiling": return usesPredicate(node.cap, context) || usesPredicate(node.policy, context);
664
+ }
665
+ };
666
+ const isScopeValue = (operand) => operand._tag === "Principal" || operand.value === null || typeof operand.value === "string" || typeof operand.value === "number" || typeof operand.value === "boolean";
667
+ /**
668
+ * Gives the problem of a scope that is not plain data, or `undefined` when it
669
+ * is plain data. A predicate, or a value that a condition can not hold, such
670
+ * as `undefined` from a route parameter, is a defect: such a scope could make
671
+ * a check pass on the wrong values. A shared node is tested one time.
672
+ */
673
+ const scopeProblem = (ast, memo = /* @__PURE__ */ new Map()) => remember(memo, ast, () => {
674
+ switch (ast._tag) {
675
+ case "Predicate": return "can not have a predicate";
676
+ case "Compare": return isScopeValue(ast.value) ? null : `has a value for "${ast.field}" that is not a scalar`;
677
+ case "In": return Arr.every(ast.values, isScopeValue) ? null : `has a value for "${ast.field}" that is not a scalar`;
678
+ case "Under": return Arr.every(ast.prefix, isScopeValue) ? null : "has a path part that is not a scalar";
679
+ case "And":
680
+ case "Or": return Option.getOrNull(Arr.findFirst(Arr.map(ast.conditions, (child) => scopeProblem(child, memo)), (problem) => problem !== void 0)) ?? null;
681
+ case "Not": return scopeProblem(ast.condition, memo) ?? null;
682
+ default: return null;
683
+ }
684
+ }) ?? void 0;
685
+ /**
686
+ * Gives the condition of a scope, with the principal and the path in it.
687
+ * With no scope, it is `True`.
688
+ */
689
+ /**
690
+ * The fields that a scope reads, before it is made smaller. A smaller scope
691
+ * may leave out a read that fails, such as a getter.
692
+ */
693
+ const scopeFields = (within) => within === void 0 ? [] : fields(within(builder()));
694
+ const scopeOf = (within, input) => {
695
+ if (within === void 0) return always;
696
+ const ast = within(builder());
697
+ const problem = scopeProblem(ast);
698
+ if (problem !== void 0) throw new globalThis.Error(`A scope ${problem}`);
699
+ return partial(ast, input);
700
+ };
701
+ const residualsAny = (policy, request) => scopedResiduals(policy, request);
702
+ const scopedResiduals = (policy, request) => {
703
+ const descriptor = descriptorOf(policy.domain, request.kind);
704
+ if (descriptor === void 0 || !fitsKind(descriptor, request)) return {
705
+ scope: never,
706
+ allow: never,
707
+ deny: never,
708
+ schema: void 0
709
+ };
710
+ if (scopeReadsGetter(request, descriptor)) throw new globalThis.Error("The scope of this request reads a getter, so it has no residual condition");
711
+ const input = {
712
+ principal: request.principal,
713
+ path: descriptor.path
714
+ };
715
+ const scope = scopeOf(request.within, input);
716
+ const build = makeBuild();
717
+ const and = build.and;
718
+ const inScope = (pair) => ({
719
+ scope,
720
+ allow: and([scope, pair.allow]),
721
+ deny: and([scope, pair.deny]),
722
+ schema: descriptor.schema
723
+ });
724
+ if (scope._tag === "False") return inScope({
725
+ allow: never,
726
+ deny: never
727
+ });
728
+ const context = {
729
+ action: request.action,
730
+ kind: request.kind,
731
+ field: request.field,
732
+ input,
733
+ descriptor,
734
+ pairs: /* @__PURE__ */ new Map(),
735
+ predicates: /* @__PURE__ */ new Map(),
736
+ build
737
+ };
738
+ if (!usesPredicate(nodeOf(policy), context)) {
739
+ if (readsGetter(policy, request, descriptor)) throw new globalThis.Error("A rule of this request reads a getter, so it has no residual condition");
740
+ try {
741
+ return inScope(residualNode(nodeOf(policy), context));
742
+ } catch (error) {
743
+ if (error instanceof TooLarge) throw new globalThis.Error(`The residual condition of this request would have more than ${String(maxResidualSize)} nodes or ${String(maxResidualDepth)} levels`);
744
+ throw error;
745
+ }
746
+ }
747
+ const decision = (value) => decideNode(policy.node, {
748
+ action: request.action,
749
+ kind: request.kind,
750
+ field: request.field,
751
+ input: {
752
+ value,
753
+ principal: request.principal,
754
+ path: descriptor.path
755
+ },
756
+ descriptor,
757
+ trace: void 0,
758
+ predicates: /* @__PURE__ */ new Map(),
759
+ decisions: /* @__PURE__ */ new Map()
760
+ }, []);
761
+ const test = (tag) => builder().predicate(policyPredicate, (value) => kindOf(value) === request.kind ? Effect.map(decision(value), (result) => result._tag === tag) : Effect.succeed(false));
762
+ return inScope({
763
+ allow: test("Allow"),
764
+ deny: test("Deny")
765
+ });
766
+ };
120
767
  /**
121
768
  * Gives the condition on resource values for which the policy allows a
122
769
  * request. It has no principal fields and no `Under` nodes, so you can give it
123
770
  * to `Condition.fold`. It is `True` when the policy allows every value of the
124
771
  * kind, and `False` when it allows none.
125
772
  *
126
- * Predicates stay in the condition. They take the principal with them.
773
+ * With a scope (`within`), the condition is the scope and the residual
774
+ * together, so it is false for every value outside the scope. Give the policy
775
+ * first to use a scope. The condition comes from its parts, with no search. To
776
+ * know if it allows all of the scope or none of it, use {@link coverage}.
777
+ *
778
+ * When a rule that the request may use has a predicate, the condition is one
779
+ * predicate with the name `"policy"`. It runs the whole policy for each value,
780
+ * so its answers and its failures are the same as those of {@link decide}. It
781
+ * takes the principal with it. Give it resource values with their `_tag`. It
782
+ * is false for a value of another kind.
783
+ *
784
+ * A policy tree may use the condition of a child two times, so a small policy
785
+ * may give a very large condition. When the condition would have more than
786
+ * 10000 nodes or 500 levels, this function throws an error. In an
787
+ * `Authorizer`, the error is
788
+ * a defect, so the check fails.
127
789
  *
128
790
  * @stability stable
129
791
  * @category evaluation
130
792
  * @since 0.3.0
131
793
  */
132
- const residual = residual$1;
794
+ const residual = dual(2, (policy, request) => residualsAny(policy, request).allow);
133
795
  /**
134
- * Gives the fields of a resource value that the policy lets a request use. It
135
- * tests each field of the resource Schema, with dots for nested fields.
796
+ * Gives both residual conditions of a request: the values that the policy
797
+ * allows, and the values that it denies. A value in neither is not
798
+ * applicable. See {@link residual}.
136
799
  *
137
800
  * @stability stable
138
801
  * @category evaluation
139
- * @since 0.3.0
802
+ * @since 1.0.0
140
803
  */
141
- const permittedFields = permittedFields$1;
804
+ const residuals = dual(2, residualsAny);
805
+ const coverages = Data.taggedEnum();
142
806
  /**
143
- * The Schema of the policies of a domain with no predicates. Decoding checks
144
- * every kind, action, field and principal field against the domain, and the
145
- * `meta` of each rule against the `meta` Schema of the domain.
807
+ * Tests if a rule that the request may use, or its scope, reads a getter of
808
+ * a class, in any branch of a union.
809
+ */
810
+ const readsGetter = (policy, request, descriptor) => scopeReadsGetter(request, descriptor) || !readsOnly(nodeOf(policy), targetOf(request, descriptor), (field) => !readsGetterAt(descriptor.schema, field), /* @__PURE__ */ new Map());
811
+ /**
812
+ * Tests if a read of a field may read a getter of a class: at the field, or
813
+ * at a field on the way to it, in any branch of a union.
814
+ */
815
+ const readsGetterAt = (schema, field) => {
816
+ const parts = field.split(".");
817
+ return Arr.some(Arr.range(1, parts.length), (count) => hasGetter(schema, parts.slice(0, count).join(".")));
818
+ };
819
+ /**
820
+ * Tests if the scope of a request reads a getter of a class. A scope is made
821
+ * smaller in each residual, also next to a predicate that runs `decide`, so
822
+ * such a scope has no residual.
823
+ */
824
+ const scopeReadsGetter = (request, descriptor) => !Arr.every(scopeFields(request.within), (field) => !readsGetterAt(descriptor.schema, field));
825
+ /**
826
+ * Tests if a request has no residual: its scope reads a getter, or it has a
827
+ * plain residual and a rule that it may use reads a getter. A residual with
828
+ * a predicate runs `decide`, so the reads of its rules stay.
829
+ */
830
+ const hasNoResidual = (policy, request, descriptor) => scopeReadsGetter(request, descriptor) || !usesPredicate(nodeOf(policy), targetOf(request, descriptor)) && readsGetter(policy, request, descriptor);
831
+ /** The parts of a request that tell which rules it may use. */
832
+ const targetOf = (request, descriptor) => ({
833
+ action: request.action,
834
+ kind: request.kind,
835
+ field: request.field,
836
+ descriptor,
837
+ predicates: /* @__PURE__ */ new Map()
838
+ });
839
+ /**
840
+ * Tests if each field that a rule that the request may use reads passes
841
+ * `test`. It looks at the conditions before they are made smaller. A smaller
842
+ * condition may leave out a field, but a read of that field, such as a
843
+ * getter, may fail when `decide` runs. A shared node is visited one time.
844
+ */
845
+ const readsOnly = (node, target, test, memo) => remember(memo, node, () => {
846
+ switch (node._tag) {
847
+ case "Rules": return Arr.every(node.rules, (value) => value.when === void 0 || !matchesTarget(value, target) || Arr.every(fields(value.when), test));
848
+ case "All":
849
+ case "Any":
850
+ case "FirstOf": return Arr.every(node.policies, (child) => readsOnly(child, target, test, memo));
851
+ case "Ceiling": return readsOnly(node.cap, target, test, memo) && readsOnly(node.policy, target, test, memo);
852
+ }
853
+ });
854
+ const coverageAny = (policy, request) => {
855
+ const descriptor = descriptorOf(policy.domain, request.kind);
856
+ if (descriptor !== void 0 && hasNoResidual(policy, request, descriptor)) return coverages.Unknown({
857
+ condition: never,
858
+ reason: "schema"
859
+ });
860
+ const scoped = scopedResiduals(policy, request);
861
+ const allowed = scoped.allow;
862
+ if (descriptor !== void 0) {
863
+ const plain = (field) => isPlainField(descriptor.schema, field);
864
+ if (readsGetter(policy, request, descriptor) || !Arr.every(scopeFields(request.within), plain) || !readsOnly(nodeOf(policy), targetOf(request, descriptor), plain, /* @__PURE__ */ new Map())) return coverages.Unknown({
865
+ condition: allowed,
866
+ reason: "schema"
867
+ });
868
+ }
869
+ const options = { schema: scoped.schema };
870
+ const some = allowed._tag === "False" ? void 0 : satisfiable(allowed, options);
871
+ if (some === void 0 || some._tag === "Unsatisfiable") return coverages.None({ condition: never });
872
+ if (some._tag === "Unknown") return coverages.Unknown({
873
+ condition: allowed,
874
+ reason: some.reason
875
+ });
876
+ const refused = simplify(and(scoped.scope, not(allowed)));
877
+ const missing = refused._tag === "False" ? void 0 : satisfiable(refused, options);
878
+ if (missing === void 0 || missing._tag === "Unsatisfiable") return coverages.All({ condition: scoped.scope });
879
+ if (missing._tag === "Unknown") return coverages.Unknown({
880
+ condition: allowed,
881
+ reason: missing.reason
882
+ });
883
+ return coverages.Some({
884
+ condition: allowed,
885
+ allowed: some.example,
886
+ refused: missing.example
887
+ });
888
+ };
889
+ /**
890
+ * Tests if the scope of a request has a value of the Schema of its kind. The
891
+ * answer is `true` only when the search finds such a value. When the search
892
+ * can not say, the answer is `false`. So a "yes" from it is safe to act on,
893
+ * as for the question "may the decider answer for this scope?".
146
894
  *
147
895
  * @stability stable
148
- * @category schemas
149
- * @since 0.3.0
896
+ * @category evaluation
897
+ * @since 1.0.0
898
+ */
899
+ const scopeHasValue = (policy, request) => {
900
+ const scoped = scopedResiduals(policy, request);
901
+ return satisfiable(scoped.scope, { schema: scoped.schema })._tag === "Satisfiable";
902
+ };
903
+ /**
904
+ * Tests if the scope of a request has no value of the Schema of its kind. A
905
+ * scope with a mistake, such as two values for one field, is empty. With no
906
+ * scope, the answer is `false` when the kind has a value. When the search can
907
+ * not say, the answer is `false`.
908
+ *
909
+ * @stability stable
910
+ * @category evaluation
911
+ * @since 1.0.0
150
912
  */
151
- const schema = schema$1;
913
+ const scopeIsEmpty = (policy, request) => {
914
+ const scoped = scopedResiduals(policy, request);
915
+ return satisfiable(scoped.scope, { schema: scoped.schema })._tag === "Unsatisfiable";
916
+ };
917
+ /**
918
+ * Tells how much of a scope the policy allows: all of it, none of it, or some
919
+ * of it. See {@link Coverage}. With no scope (`within`), the scope is every
920
+ * value of the kind.
921
+ *
922
+ * The answer comes from {@link Condition.satisfiable} with the Schema of the
923
+ * resource, so it is exact for the values of the Schema. When a condition
924
+ * tests a field that is not a plain scalar (see `Resource.isPlainField`),
925
+ * such as a field with a check other than a finite or integer check, the
926
+ * answer is `Unknown`, and a check on the kind says no.
927
+ *
928
+ * `Some` means that some value of the Schema is allowed. It does not mean that
929
+ * such a value is in your database. To know that, run `condition` as a query.
930
+ *
931
+ * @stability stable
932
+ * @category evaluation
933
+ * @since 1.0.0
934
+ */
935
+ const coverage = dual(2, coverageAny);
936
+ const AlgorithmSchema = Schema.Literals([
937
+ "denyOverrides",
938
+ "permitOverrides",
939
+ "firstApplicable",
940
+ "lastApplicable"
941
+ ]);
942
+ const encodedRuleSchema = (meta) => Schema.Struct({
943
+ _tag: Schema.Literals(["Allow", "Deny"]),
944
+ actions: Schema.Array(Schema.String),
945
+ resources: Schema.Array(Schema.String),
946
+ when: Schema.optionalKey(schema$1),
947
+ fields: Schema.String.pipe(Schema.Array, Schema.optionalKey),
948
+ reason: Schema.optionalKey(Schema.String),
949
+ id: Schema.optionalKey(Schema.String),
950
+ meta: Schema.optionalKey(meta ?? Schema.Never)
951
+ });
952
+ /**
953
+ * The most levels of objects and arrays in the JSON form of a policy. The
954
+ * Schema of a policy calls itself for each level, so a much deeper input
955
+ * could fill the stack before the checks of the policy run. A policy tree of
956
+ * `maxDepth` levels with deep conditions stays well below this.
957
+ */
958
+ const maxJsonDepth = 1e3;
959
+ /**
960
+ * The most objects and arrays in the JSON form of an input, with a shared one
961
+ * counted for each place. The decoder writes out a shared one each time, so
962
+ * an input with few objects may give a very large tree.
963
+ */
964
+ const maxInputNodes = 1e6;
965
+ /**
966
+ * Measures a value, or gives `undefined` when it is higher than `levels`. A
967
+ * shared object is measured one time. The walk stops at that height, so it
968
+ * does not fill the stack itself. The memo is a native `Map`, because it
969
+ * must find an object by the object itself.
970
+ */
971
+ const measure = (input, levels, memo) => {
972
+ if (typeof input !== "object" || input === null) return {
973
+ size: 0,
974
+ height: 0
975
+ };
976
+ const cached = memo.get(input);
977
+ if (cached !== void 0 && ("failed" in cached ? levels <= cached.failed : true)) return "failed" in cached || cached.height > levels ? void 0 : cached;
978
+ const result = levels === 0 || !isPlainData(input) ? void 0 : Arr.reduce(Object.values(input), {
979
+ size: 1,
980
+ height: 1
981
+ }, (total, child) => {
982
+ const measured = total === void 0 ? void 0 : measure(child, levels - 1, memo);
983
+ return total === void 0 || measured === void 0 ? void 0 : {
984
+ size: total.size + measured.size,
985
+ height: Math.max(total.height, 1 + measured.height)
986
+ };
987
+ });
988
+ memo.set(input, result ?? { failed: levels });
989
+ return result;
990
+ };
991
+ /**
992
+ * Tests if each own key of an object is a plain data key that a loop over the
993
+ * keys sees. The `length` of an array is the one key that is not seen. The
994
+ * decoder reads keys of the prototypes too, so a prototype other than the
995
+ * one of a plain object or an array may have only methods, as the prototype
996
+ * of a class has.
997
+ */
998
+ const isPlainData = (input) => Arr.every(Reflect.ownKeys(input), (key) => {
999
+ const property = Object.getOwnPropertyDescriptor(input, key);
1000
+ return property !== void 0 && "value" in property && (property.enumerable === true || Array.isArray(input) && key === "length");
1001
+ }) && hasOnlyMethods(Object.getPrototypeOf(input));
1002
+ /** Tests if the prototypes from `prototype` up have only methods. */
1003
+ const hasOnlyMethods = (prototype) => prototype === null || prototype === Object.prototype || prototype === Array.prototype || typeof prototype === "object" && Arr.every(Reflect.ownKeys(prototype), (key) => {
1004
+ const property = Object.getOwnPropertyDescriptor(prototype, key);
1005
+ return typeof key === "symbol" || key === "constructor" || property !== void 0 && "value" in property && typeof property.value === "function";
1006
+ }) && hasOnlyMethods(Object.getPrototypeOf(prototype));
1007
+ /** Tests if a value is low and small enough to decode. */
1008
+ const fitsLimits = (input) => {
1009
+ const measured = measure(input, maxJsonDepth, /* @__PURE__ */ new Map());
1010
+ return measured === void 0 ? `A policy may have at most ${String(maxJsonDepth)} levels of JSON` : measured.size > maxInputNodes ? `A policy may have at most ${String(maxInputNodes)} objects and arrays in JSON` : true;
1011
+ };
1012
+ const encodedNodeSchema = (meta) => {
1013
+ const self = Schema.Union([
1014
+ Schema.TaggedStruct("Rules", {
1015
+ algorithm: AlgorithmSchema,
1016
+ rules: Schema.Array(encodedRuleSchema(meta))
1017
+ }),
1018
+ Schema.TaggedStruct("All", { policies: Schema.Array(Schema.suspend(() => self)) }),
1019
+ Schema.TaggedStruct("Any", { policies: Schema.Array(Schema.suspend(() => self)) }),
1020
+ Schema.TaggedStruct("FirstOf", { policies: Schema.Array(Schema.suspend(() => self)) }),
1021
+ Schema.TaggedStruct("Ceiling", {
1022
+ cap: Schema.suspend(() => self),
1023
+ policy: Schema.suspend(() => self)
1024
+ })
1025
+ ]);
1026
+ return self;
1027
+ };
1028
+ const principalRefs = (ast) => Array.from(refsIn(ast, /* @__PURE__ */ new Map()));
1029
+ /** The principal fields of a node. A shared node is visited one time. */
1030
+ const refsIn = (node, memo) => remember(memo, node, () => {
1031
+ const refs = (operands) => HashSet.fromIterable(Arr.flatMap(operands, (operand) => operand._tag === "Principal" ? [operand.path] : []));
1032
+ switch (node._tag) {
1033
+ case "Compare": return refs([node.value]);
1034
+ case "In": return refs(node.values);
1035
+ case "Under": return refs(node.prefix);
1036
+ case "And":
1037
+ case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, refsIn(child, memo)));
1038
+ case "Not": return refsIn(node.condition, memo);
1039
+ default: return HashSet.empty();
1040
+ }
1041
+ });
1042
+ const literalTests = (node) => Arr.map(Array.from(literalTestsIn(node, /* @__PURE__ */ new Map())), (text) => {
1043
+ const [field, value] = JSON.parse(text);
1044
+ return [field, hasProperty(value, "number") ? Number(value.number) : value];
1045
+ });
1046
+ /**
1047
+ * The JSON text of a test. JSON has no `Infinity`, `-Infinity` or `NaN`, so
1048
+ * such a number goes in as its text.
1049
+ */
1050
+ const literalText = (field, value) => JSON.stringify([field, typeof value === "number" && !Number.isFinite(value) ? { number: String(value) } : value]);
1051
+ /**
1052
+ * The tests of fields with fixed values in a node, each as the JSON text of a
1053
+ * {@link LiteralTest}. A set of texts keeps each test one time, so a node
1054
+ * with many shared parts gives a small set. A shared node is visited one time.
1055
+ */
1056
+ const literalTestsIn = (node, memo) => remember(memo, node, () => {
1057
+ const tests = (field, operands) => HashSet.fromIterable(Arr.flatMap(operands, (operand) => operand._tag === "Literal" ? [literalText(field, operand.value)] : []));
1058
+ switch (node._tag) {
1059
+ case "Compare": return tests(node.field, [node.value]);
1060
+ case "In": return tests(node.field, node.values);
1061
+ case "And":
1062
+ case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, literalTestsIn(child, memo)));
1063
+ case "Not": return literalTestsIn(node.condition, memo);
1064
+ default: return HashSet.empty();
1065
+ }
1066
+ });
1067
+ const principalTests = (node) => Arr.map(Array.from(principalTestsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
1068
+ /**
1069
+ * The tests of fields with principal fields in a node, each as the JSON text
1070
+ * of a {@link PrincipalTest}. See {@link literalTestsIn}.
1071
+ */
1072
+ const principalTestsIn = (node, memo) => remember(memo, node, () => {
1073
+ const tests = (field, operands) => HashSet.fromIterable(Arr.flatMap(operands, (operand) => operand._tag === "Principal" ? [JSON.stringify([field, operand.path])] : []));
1074
+ switch (node._tag) {
1075
+ case "Compare": return tests(node.field, [node.value]);
1076
+ case "In": return tests(node.field, node.values);
1077
+ case "And":
1078
+ case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, principalTestsIn(child, memo)));
1079
+ case "Not": return principalTestsIn(node.condition, memo);
1080
+ default: return HashSet.empty();
1081
+ }
1082
+ });
1083
+ /**
1084
+ * Tests if a node has a fixed value that is not a {@link Condition.Scalar},
1085
+ * such as `undefined`. A shared node is visited one time.
1086
+ */
1087
+ const hasBadLiteralIn = (node, memo) => remember(memo, node, () => {
1088
+ const bad = (operands) => Arr.some(operands, (operand) => operand._tag === "Literal" && operand.value !== null && typeof operand.value !== "string" && typeof operand.value !== "number" && typeof operand.value !== "boolean");
1089
+ switch (node._tag) {
1090
+ case "Compare": return bad([node.value]);
1091
+ case "In": return bad(node.values);
1092
+ case "Under": return bad(node.prefix);
1093
+ case "And":
1094
+ case "Or": return Arr.some(node.conditions, (child) => hasBadLiteralIn(child, memo));
1095
+ case "Not": return hasBadLiteralIn(node.condition, memo);
1096
+ default: return false;
1097
+ }
1098
+ });
1099
+ /**
1100
+ * The principal fields in the prefixes of `Under` nodes, each as the JSON text
1101
+ * of the place in the prefix and the path of the field. See
1102
+ * {@link literalTestsIn}.
1103
+ */
1104
+ /**
1105
+ * The fixed values in the prefixes of `Under` nodes, each as the JSON text of
1106
+ * the place in the prefix, the value, and the fixed strings of the whole
1107
+ * prefix. A number that JSON can not hold is left out: it fits no field
1108
+ * that a path names. See {@link underRefsIn}.
1109
+ */
1110
+ const underLiteralsIn = (node, memo) => remember(memo, node, () => {
1111
+ switch (node._tag) {
1112
+ case "Under": {
1113
+ const strings = Arr.map(node.prefix, (operand) => operand._tag === "Literal" && typeof operand.value === "string" ? operand.value : null);
1114
+ return HashSet.fromIterable(Arr.flatMap(node.prefix, (operand, index) => operand._tag === "Literal" && (operand.value === null || typeof operand.value === "string" || typeof operand.value === "boolean" || Number.isFinite(operand.value)) ? [JSON.stringify([
1115
+ index,
1116
+ operand.value,
1117
+ strings
1118
+ ])] : []));
1119
+ }
1120
+ case "And":
1121
+ case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, underLiteralsIn(child, memo)));
1122
+ case "Not": return underLiteralsIn(node.condition, memo);
1123
+ default: return HashSet.empty();
1124
+ }
1125
+ });
1126
+ const underLiterals = (node) => Arr.map(Array.from(underLiteralsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
1127
+ const underRefsIn = (node, memo) => remember(memo, node, () => {
1128
+ switch (node._tag) {
1129
+ case "Under": {
1130
+ const strings = Arr.map(node.prefix, (operand) => operand._tag === "Literal" && typeof operand.value === "string" ? operand.value : null);
1131
+ return HashSet.fromIterable(Arr.flatMap(node.prefix, (operand, index) => operand._tag === "Principal" ? [JSON.stringify([
1132
+ index,
1133
+ operand.path,
1134
+ strings
1135
+ ])] : []));
1136
+ }
1137
+ case "And":
1138
+ case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, underRefsIn(child, memo)));
1139
+ case "Not": return underRefsIn(node.condition, memo);
1140
+ default: return HashSet.empty();
1141
+ }
1142
+ });
1143
+ const underRefs = (node) => Arr.map(Array.from(underRefsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
1144
+ /**
1145
+ * Tests if the path of a resource may start with a prefix: it is as long as
1146
+ * the prefix, and each fixed string of the prefix is the string of the path
1147
+ * at its place, when the path has a string there.
1148
+ */
1149
+ const mayStartWith = (descriptor, strings) => (descriptor.path?.length ?? 0) >= strings.length && Arr.every(strings, (string, index) => {
1150
+ const segment = descriptor.path?.[index];
1151
+ return string === null || typeof segment !== "string" || segment === string;
1152
+ });
1153
+ /**
1154
+ * Tests if a principal field may hold the part of a path at `index`: a string
1155
+ * of the path, or a value of the field that the path names there.
1156
+ */
1157
+ const fitsPathPart = (descriptor, index, principal) => {
1158
+ const segment = descriptor.path?.[index];
1159
+ return segment === void 0 ? false : typeof segment === "string" ? fitsDomain(principal, segment) : domainsMeet(domainOf(descriptor.schema, segment.field), principal);
1160
+ };
1161
+ /**
1162
+ * The fields of ordered tests (`lt`, `lte`, `gt`, `gte`) in a node, each as
1163
+ * the JSON text of the field and the operand. See {@link literalTestsIn}.
1164
+ */
1165
+ const orderedTestsIn = (node, memo) => remember(memo, node, () => {
1166
+ switch (node._tag) {
1167
+ case "Compare": return node.op === "eq" || node.op === "ne" ? HashSet.empty() : HashSet.make(JSON.stringify([node.field, node.value._tag === "Literal" ? {
1168
+ _tag: "Literal",
1169
+ type: typeof node.value.value
1170
+ } : node.value]));
1171
+ case "And":
1172
+ case "Or": return Arr.reduce(node.conditions, HashSet.empty(), (all, child) => HashSet.union(all, orderedTestsIn(child, memo)));
1173
+ case "Not": return orderedTestsIn(node.condition, memo);
1174
+ default: return HashSet.empty();
1175
+ }
1176
+ });
1177
+ const orderedTests = (node) => Arr.map(Array.from(orderedTestsIn(node, /* @__PURE__ */ new Map())), (text) => JSON.parse(text));
1178
+ /**
1179
+ * Tests if a field may hold a value with an order: a number or a string. An
1180
+ * ordered test is false for any other value.
1181
+ */
1182
+ const isOrdered = (domain) => domain.string || domain.number !== "none" || Arr.some(domain.literals, (value) => typeof value === "string" || typeof value === "number");
1183
+ /**
1184
+ * Tests if two fields may hold values of one ordered type: two strings or two
1185
+ * numbers. An ordered test of two values of other types is false.
1186
+ */
1187
+ const orderedMeet = (left, right) => {
1188
+ const strings = (domain) => domain.string || Arr.some(domain.literals, (value) => typeof value === "string");
1189
+ const numbers = (domain) => domain.number !== "none" || Arr.some(domain.literals, (value) => typeof value === "number");
1190
+ return strings(left) && strings(right) || numbers(left) && numbers(right);
1191
+ };
1192
+ /**
1193
+ * Tests if two fields may hold one value: a value of one type, or a single
1194
+ * value that the other field may hold.
1195
+ */
1196
+ const domainsMeet = (left, right) => left.string && right.string || left.boolean && right.boolean || left.number !== "none" && right.number !== "none" || Arr.some(left.literals, (value) => fitsDomain(right, value)) || Arr.some(right.literals, (value) => fitsDomain(left, value));
1197
+ /**
1198
+ * Tests if a field with this domain may hold a value of the type of `value`.
1199
+ * `null` always fits: a test with `null` is a test that the field is missing.
1200
+ */
1201
+ const fitsDomain = (domain, value) => value === null || domain.literals.includes(value) || typeof value === "string" && domain.string || typeof value === "boolean" && domain.boolean || typeof value === "number" && domain.number !== "none";
1202
+ /**
1203
+ * Checks the names in an encoded policy against the domain. It gives one
1204
+ * message for each problem.
1205
+ */
1206
+ const validate = (domain, root) => {
1207
+ const principalPaths = domain.principal.pipe(fieldPaths, HashSet.fromIterable);
1208
+ const ruleIssues = (value, at) => {
1209
+ const all = value.resources.includes("*");
1210
+ const named = Array.from(HashSet.fromIterable(Arr.filter(value.resources, (kind) => kind !== "*")));
1211
+ const descriptors = Arr.flatMap(named, (kind) => {
1212
+ const descriptor = descriptorOf(domain, kind);
1213
+ return descriptor === void 0 ? [] : [descriptor];
1214
+ });
1215
+ const known = all ? Array.from(HashMap.values(domain.resources)) : descriptors;
1216
+ const applicable = Arr.filter(descriptors, (descriptor) => value.actions.includes("*") || Arr.some(value.actions, (action) => descriptor.actions.includes(action)));
1217
+ const usedFields = [...Arr.filter(value.fields ?? [], (field) => !field.includes("*")), ...value.when === void 0 ? [] : fields(value.when)];
1218
+ return [
1219
+ ...Arr.map(Arr.filter(named, (kind) => !HashMap.has(domain.resources, kind)), (kind) => ({
1220
+ path: [...at, "resources"],
1221
+ issue: `There is no resource with the kind "${kind}"`
1222
+ })),
1223
+ ...Arr.map(Arr.filter(value.actions, (action) => action !== "*" && !Arr.some(known, (descriptor) => descriptor.actions.includes(action))), (action) => ({
1224
+ path: [...at, "actions"],
1225
+ issue: `The action "${action}" is not an action of these resources`
1226
+ })),
1227
+ ...Arr.map(Arr.filter(usedFields, (field) => all || descriptors.length === 0 || !Arr.every(descriptors, (descriptor) => descriptor.fieldPaths.includes(field) || hasPath(descriptor.schema, field) || hasGetter(descriptor.schema, field))), (field) => ({
1228
+ path: at,
1229
+ issue: `The field "${field}" is not a field of every resource of the rule`
1230
+ })),
1231
+ ...Arr.map(Arr.filter(value.when === void 0 ? [] : principalRefs(value.when), (ref) => !HashSet.has(principalPaths, ref) && !hasPath(domain.principal, ref) && !hasGetter(domain.principal, ref)), (ref) => ({
1232
+ path: [...at, "when"],
1233
+ issue: `The principal has no field "${ref}"`
1234
+ })),
1235
+ ...Arr.map(Arr.filter(value.when === void 0 ? [] : literalTests(value.when), ([field, literal]) => Arr.some(applicable, (descriptor) => !fitsDomain(domainOf(descriptor.schema, field), literal))), ([field, literal]) => ({
1236
+ path: [...at, "when"],
1237
+ issue: `The field "${field}" can not hold the value ${JSON.stringify(literal)}`
1238
+ })),
1239
+ ...Arr.map(Arr.filter(value.when === void 0 ? [] : principalTests(value.when), ([field, ref]) => Arr.some(applicable, (descriptor) => !domainsMeet(domainOf(descriptor.schema, field), domainOf(domain.principal, ref)))), ([field, ref]) => ({
1240
+ path: [...at, "when"],
1241
+ issue: `The field "${field}" can not hold a value of the principal field "${ref}"`
1242
+ })),
1243
+ ...value.when !== void 0 && !isAst(value.when) ? [{
1244
+ path: [...at, "when"],
1245
+ issue: "The condition is not a whole condition tree"
1246
+ }] : [],
1247
+ ...value.when !== void 0 && hasBadLiteralIn(value.when, /* @__PURE__ */ new Map()) ? [{
1248
+ path: [...at, "when"],
1249
+ issue: "A test has a fixed value that is not a string, a number, a boolean or null"
1250
+ }] : [],
1251
+ ...Arr.map(Arr.filter(value.when === void 0 ? [] : underRefs(value.when), ([index, ref, strings]) => Arr.some(Arr.filter(all ? Arr.filter(known, (descriptor) => descriptor.path?.[index] !== void 0 && (value.actions.includes("*") || Arr.some(value.actions, (action) => descriptor.actions.includes(action)))) : applicable, (descriptor) => mayStartWith(descriptor, strings)), (descriptor) => !fitsPathPart(descriptor, index, domainOf(domain.principal, ref)))), ([, ref]) => ({
1252
+ path: [...at, "when"],
1253
+ issue: `The principal field "${ref}" can not hold a part of the path of the resource`
1254
+ })),
1255
+ ...Arr.map(Arr.filter(value.when === void 0 ? [] : underLiterals(value.when), ([index, literal, strings]) => Arr.some(Arr.filter(all ? Arr.filter(known, (descriptor) => value.actions.includes("*") || Arr.some(value.actions, (action) => descriptor.actions.includes(action))) : applicable, (descriptor) => mayStartWith(descriptor, strings)), (descriptor) => {
1256
+ const segment = descriptor.path?.[index];
1257
+ return segment !== void 0 && typeof segment !== "string" && !fitsDomain(domainOf(descriptor.schema, segment.field), literal);
1258
+ })), ([index, literal]) => ({
1259
+ path: [...at, "when"],
1260
+ issue: `The path part ${String(index)} can not hold the value ${JSON.stringify(literal)}`
1261
+ })),
1262
+ ...Arr.map(Arr.filter(value.when === void 0 ? [] : orderedTests(value.when), ([field, operand]) => operand._tag === "Literal" && operand.type !== "string" && operand.type !== "number" || Arr.some(applicable, (descriptor) => {
1263
+ const fieldDomain = domainOf(descriptor.schema, field);
1264
+ return operand._tag === "Literal" ? !isOrdered(fieldDomain) : !orderedMeet(fieldDomain, domainOf(domain.principal, operand.path));
1265
+ })), ([field]) => ({
1266
+ path: [...at, "when"],
1267
+ issue: `The field "${field}" has an ordered test with a value that has no order`
1268
+ }))
1269
+ ];
1270
+ };
1271
+ const issuesOf = (node, path, level) => {
1272
+ if (level > maxDepth) return [{
1273
+ path,
1274
+ issue: `A policy tree may have at most ${maxDepth} levels`
1275
+ }];
1276
+ switch (node._tag) {
1277
+ case "Rules": return Arr.flatMap(node.rules, (value, index) => ruleIssues(value, [
1278
+ ...path,
1279
+ "rules",
1280
+ index
1281
+ ]));
1282
+ case "All":
1283
+ case "Any":
1284
+ case "FirstOf": return Arr.flatMap(node.policies, (child, index) => issuesOf(child, [
1285
+ ...path,
1286
+ "policies",
1287
+ index
1288
+ ], level + 1));
1289
+ case "Ceiling": return [...issuesOf(node.cap, [...path, "cap"], level + 1), ...issuesOf(node.policy, [...path, "policy"], level + 1)];
1290
+ }
1291
+ };
1292
+ return issuesOf(root, [], 1);
1293
+ };
1294
+ /**
1295
+ * Makes a policy tree from its JSON form. Each rule gets the domain of the
1296
+ * Schema, as a rule from the builder of the domain has it.
1297
+ */
1298
+ const decodeNode = (node, domain) => {
1299
+ switch (node._tag) {
1300
+ case "Rules": return nodes.Rules({
1301
+ algorithm: node.algorithm,
1302
+ rules: node.rules.map((value) => make({
1303
+ ...value,
1304
+ domain
1305
+ }))
1306
+ });
1307
+ case "All":
1308
+ case "Any":
1309
+ case "FirstOf": return nodes[node._tag]({ policies: node.policies.map((child) => decodeNode(child, domain)) });
1310
+ case "Ceiling": return nodes.Ceiling({
1311
+ cap: decodeNode(node.cap, domain),
1312
+ policy: decodeNode(node.policy, domain)
1313
+ });
1314
+ }
1315
+ };
152
1316
  /**
153
1317
  * Tests if a value is a policy.
154
1318
  *
@@ -156,7 +1320,20 @@ const schema = schema$1;
156
1320
  * @category guards
157
1321
  * @since 0.3.0
158
1322
  */
159
- const isPolicy = isPolicy$1;
160
- export { Policy_exports, TypeId, all, any, ceiling, decide, decideSync, define, empty, explain, firstOf, isPolicy, permittedFields, residual, rules, schema };
1323
+ const isPolicy = (u) => u instanceof PolicyData;
1324
+ /**
1325
+ * The Schema of the policies of a domain with no predicates. Decoding checks
1326
+ * every kind, action, field and principal field against the domain, and the
1327
+ * `meta` of each rule against the `meta` Schema of the domain.
1328
+ *
1329
+ * @stability stable
1330
+ * @category schemas
1331
+ * @since 0.3.0
1332
+ */
1333
+ const schema = (domain) => Schema.declare((input) => typeof input === "object" && input !== null).pipe(Schema.check(Schema.makeFilter(fitsLimits)), Schema.decodeTo(encodedNodeSchema(domain.meta), SchemaTransformation.passthrough()), Schema.check(Schema.makeFilter((node) => validate(domain, node))), Schema.decodeTo(Schema.declare((u) => isPolicy(u) && u.domain === domain && !nodeHasPredicate(u.node)), SchemaTransformation.transformEffect({
1334
+ decode: (node) => Effect.succeed(fromNode(domain, decodeNode(node, domain))),
1335
+ encode: (policy, options) => encodedSize(policy.node) > maxEncodedNodes ? Effect.fail(new SchemaIssue.InvalidValue({ message: tooLargeMessage }, policy, options)) : Effect.succeed(encodeNode(policy.node))
1336
+ })));
1337
+ export { Policy_exports, all, any, ceiling, coverage, decide, decideSync, define, empty, explain, firstOf, fitsKind, isPolicy, permittedFields, provideContext, residual, residuals, rules, schema, scopeHasValue, scopeIsEmpty };
161
1338
 
162
1339
  //# sourceMappingURL=Policy.js.map