@nmnmcc/ability 2.0.7 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -29
- package/dist/Authorizer.d.ts +21 -12
- package/dist/Authorizer.d.ts.map +1 -1
- package/dist/Authorizer.js.map +1 -1
- package/dist/Condition.d.ts +121 -19
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +32 -21
- package/dist/Condition.js.map +1 -1
- package/dist/Domain.d.ts +9 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +5 -0
- package/dist/Domain.js.map +1 -1
- package/dist/Limits.d.ts +44 -13
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +52 -22
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +102 -18
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +7 -6
- package/dist/Policy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/internal/algebra.js +2 -2
- package/dist/internal/analysis.js +81 -75
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.d.ts.map +1 -1
- package/dist/internal/authorizer.js +11 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +86 -64
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +33 -10
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +170 -77
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +27 -31
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/domain.js +19 -5
- package/dist/internal/domain.js.map +1 -1
- package/dist/internal/json.js +120 -76
- package/dist/internal/json.js.map +1 -1
- package/dist/internal/limits.d.ts +28 -0
- package/dist/internal/limits.d.ts.map +1 -1
- package/dist/internal/limits.js +75 -16
- package/dist/internal/limits.js.map +1 -1
- package/dist/internal/nodes.d.ts.map +1 -1
- package/dist/internal/nodes.js +53 -44
- package/dist/internal/nodes.js.map +1 -1
- package/dist/internal/patterns.js +50 -15
- package/dist/internal/patterns.js.map +1 -1
- package/dist/internal/policy-json.js +169 -127
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/policy-nodes.d.ts.map +1 -1
- package/dist/internal/policy-nodes.js +16 -13
- package/dist/internal/policy-nodes.js.map +1 -1
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +38 -22
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/resource.js +1 -1
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +6 -5
- package/dist/internal/rule.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +106 -92
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/trampoline.js +19 -5
- package/dist/internal/trampoline.js.map +1 -1
- package/package.json +1 -1
- package/src/Authorizer.ts +30 -20
- package/src/Condition.ts +317 -86
- package/src/Domain.ts +9 -0
- package/src/Limits.ts +69 -29
- package/src/Policy.ts +143 -21
- package/src/internal/analysis.ts +154 -124
- package/src/internal/authorizer.ts +29 -14
- package/src/internal/bind.ts +193 -136
- package/src/internal/catalog.ts +320 -118
- package/src/internal/decide.ts +61 -50
- package/src/internal/domain.ts +39 -3
- package/src/internal/json.ts +212 -117
- package/src/internal/limits.ts +101 -31
- package/src/internal/nodes.ts +65 -47
- package/src/internal/patterns.ts +79 -25
- package/src/internal/policy-json.ts +266 -197
- package/src/internal/policy-nodes.ts +31 -23
- package/src/internal/policy.ts +63 -23
- package/src/internal/rule.ts +16 -3
- package/src/internal/search.ts +186 -136
- package/src/internal/trampoline.ts +51 -4
package/dist/internal/policy.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { exceeded } from "./limits.js";
|
|
2
|
+
import { run } from "./trampoline.js";
|
|
1
3
|
import { toNode } from "./nodes.js";
|
|
2
4
|
import { provideContext as provideContext$1 } from "../Condition.js";
|
|
3
5
|
import { treeIssues } from "./bind.js";
|
|
4
6
|
import { builder, make } from "./rule.js";
|
|
5
7
|
import { Ceiling, Rules, policy_nodes_exports, rulesOf } from "./policy-nodes.js";
|
|
6
8
|
import { decode, encode, syncMeta, toJson } from "./policy-json.js";
|
|
9
|
+
import * as Arr from "effect/Array";
|
|
7
10
|
import * as Effect from "effect/Effect";
|
|
8
11
|
import * as Schema from "effect/Schema";
|
|
9
12
|
import * as Data from "effect/Data";
|
|
10
|
-
import * as Arr from "effect/Array";
|
|
11
13
|
import { dual } from "effect/Function";
|
|
12
14
|
import * as SchemaIssue from "effect/SchemaIssue";
|
|
13
15
|
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
@@ -29,10 +31,19 @@ var PolicyData = class extends Data.Class {
|
|
|
29
31
|
};
|
|
30
32
|
/** The node of a policy, with no types, for the code below. */
|
|
31
33
|
const nodeOf = (policy) => policy.node;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Makes a policy of a domain from its tree. The constructors of the nodes
|
|
36
|
+
* check the ceiling of `policyNodes`, since a node does not know its domain,
|
|
37
|
+
* so this checks the limit of the domain.
|
|
38
|
+
*/
|
|
39
|
+
const fromNode = (domain, node) => {
|
|
40
|
+
const most = domain.limits.policyNodes;
|
|
41
|
+
if (node.size > most) throw exceeded("A policy tree", "policyNodes", "nodes", node.size, most);
|
|
42
|
+
return new PolicyData({
|
|
43
|
+
domain,
|
|
44
|
+
node
|
|
45
|
+
});
|
|
46
|
+
};
|
|
36
47
|
/**
|
|
37
48
|
* The error when a rule does not fit its domain. It has one issue for each
|
|
38
49
|
* problem, with its path.
|
|
@@ -92,23 +103,28 @@ const provideNode = (root, context) => {
|
|
|
92
103
|
...value,
|
|
93
104
|
when: provideContext$1(value.when, context)
|
|
94
105
|
})]));
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
return run(root.depth, (R) => {
|
|
107
|
+
const provide = (node) => {
|
|
108
|
+
if (!node.opaque) return R.succeed(node);
|
|
109
|
+
switch (node._tag) {
|
|
110
|
+
case "Rules": return R.succeed(new Rules({
|
|
111
|
+
algorithm: node.algorithm,
|
|
112
|
+
rules: Arr.map(node.rules, (value) => provided.get(value) ?? value)
|
|
113
|
+
}));
|
|
114
|
+
case "All":
|
|
115
|
+
case "Any":
|
|
116
|
+
case "FirstOf": {
|
|
117
|
+
const tag = node._tag;
|
|
118
|
+
return R.map(R.forEach(node.policies, (child) => R.suspend(() => provide(child))), (policies) => new policy_nodes_exports[tag](policies));
|
|
119
|
+
}
|
|
120
|
+
case "Ceiling": return R.flatMap(R.suspend(() => provide(node.cap)), (cap) => R.map(R.suspend(() => provide(node.policy)), (policy) => new Ceiling({
|
|
121
|
+
cap,
|
|
122
|
+
policy
|
|
123
|
+
})));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
return provide(root);
|
|
127
|
+
});
|
|
112
128
|
};
|
|
113
129
|
/** Gives the services in `context` to every predicate of a policy. */
|
|
114
130
|
const provideContext = dual(2, (policy, context) => fromNode(policy.domain, provideNode(nodeOf(policy), context)));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","names":["PolicyJson.toJson","PolicyJson.syncMeta","PolicyNodes.Rules","Bind.treeIssues","RuleData.builder","PolicyNodes","PolicyNodes.Ceiling","PolicyNodes.rulesOf","Nodes.toNode","RuleData.make","Condition.provideContext","PolicyJson.decode","PolicyJson.encode"],"sources":["../../src/internal/policy.ts"],"sourcesContent":["/**\n * The values of policies: the class of a policy, the constructors, the\n * combinators and the Schema. The types are in `Policy.ts`.\n */\nimport type * as Context from \"effect/Context\"\nimport * as Data from \"effect/Data\"\nimport * as Arr from \"effect/Array\"\nimport * as Effect from \"effect/Effect\"\nimport { dual } from \"effect/Function\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as SchemaIssue from \"effect/SchemaIssue\"\nimport * as SchemaTransformation from \"effect/SchemaTransformation\"\nimport * as Condition from \"../Condition.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Policy from \"../Policy.ts\"\nimport * as Bind from \"./bind.ts\"\nimport * as Nodes from \"./nodes.ts\"\nimport * as PolicyJson from \"./policy-json.ts\"\nimport * as PolicyNodes from \"./policy-nodes.ts\"\nimport * as RuleData from \"./rule.ts\"\n\ntype Any = Policy.Any\ntype Node = Policy.Node\ntype Options = Policy.Options\n\n/**\n * The class of every policy value. Two policies of one domain with the same\n * tree are equal. As JSON, a policy is its tree.\n */\nexport class PolicyData<\n\tD extends Domain.Any = any,\n\tX = never,\n> extends Data.Class<{\n\treadonly domain: D\n\treadonly node: Node\n}> {\n\t// A private field makes the type of a policy its own: a spread of a\n\t// policy, or an object made by hand, is not a policy. `Equal` does not\n\t// read it.\n\treadonly #policy = true\n\t/** Only in the types. */\n\tdeclare readonly \"~Opaque\": () => X\n\t/**\n\t * Only in the types. A policy reads the values and the principal of its\n\t * domain, so a policy of one domain is not a policy of a wider domain.\n\t */\n\tdeclare readonly \"~Domain\": (domain: D) => D\n\n\ttoJSON(): unknown {\n\t\treturn PolicyJson.toJson(this.node, PolicyJson.syncMeta(this.domain.meta))\n\t}\n\n\t/** Tests if a value is a policy, by its private field. */\n\tstatic is(u: unknown): u is Any {\n\t\treturn typeof u === \"object\" && u !== null && #policy in u\n\t}\n}\n\n/** The node of a policy, with no types, for the code below. */\nexport const nodeOf = (policy: Any): PolicyNodes.Node => policy.node as never\n\nexport const fromNode = (domain: Domain.Any, node: PolicyNodes.Node): any =>\n\tnew PolicyData({ domain, node })\n\n/**\n * The error when a rule does not fit its domain. It has one issue for each\n * problem, with its path.\n */\nexport class PolicyError extends Schema.TaggedError<PolicyError>(\n\t\"@nmnmcc/ability/Policy/PolicyError\",\n)(\"PolicyError\", {\n\tissues: Schema.Array(\n\t\tSchema.Struct({\n\t\t\tpath: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),\n\t\t\tmessage: Schema.String,\n\t\t}),\n\t),\n\tmessage: Schema.String,\n}) {}\n\n/** Makes a policy from a list of rules, and binds them to the domain. */\nconst fromRules = (\n\tdomain: Domain.Any,\n\tvalues: Iterable<unknown>,\n\toptions?: Options,\n): Any => {\n\t// The constructor takes only rules, and it stops a list past the limits.\n\tconst node = new PolicyNodes.Rules({\n\t\talgorithm: options?.algorithm ?? \"denyOverrides\",\n\t\trules: Array.from(values) as ReadonlyArray<RuleData.Rule>,\n\t})\n\t// A rule from a builder of another domain is a mistake in the code, so it\n\t// is a defect. Two domains may have the same types, so the types do not\n\t// find it.\n\tif (\n\t\tArr.some(node.rules, (value) => {\n\t\t\tconst origin = value.domain\n\t\t\treturn origin !== undefined && origin !== domain\n\t\t})\n\t) {\n\t\tthrow new globalThis.Error(\n\t\t\t\"A rule comes from the builder of another domain\",\n\t\t)\n\t}\n\t// Every rule is bound to the domain here, also a rule from `Policy.rule`.\n\tconst issues = Bind.treeIssues(domain, node)\n\tif (issues.length > 0) {\n\t\tthrow PolicyError.make({\n\t\t\tissues: Arr.map(issues, (issue) => ({\n\t\t\t\tpath: Arr.map(issue.path, (part) =>\n\t\t\t\t\ttypeof part === \"number\" ? part : String(part),\n\t\t\t\t),\n\t\t\t\tmessage: issue.issue,\n\t\t\t})),\n\t\t\tmessage: `A rule does not fit the domain: ${issues.map((issue) => issue.issue).join(\"; \")}`,\n\t\t})\n\t}\n\treturn fromNode(domain, node)\n}\n\n/** Makes a policy from a generator function that gives rules. */\nexport const define =\n\t(domain: Domain.Any, options?: Options) =>\n\t(body: (builder: unknown) => Generator<unknown, unknown, unknown>): Any => {\n\t\tconst iterator = body(RuleData.builder(domain))\n\t\t// Each rule goes back into the generator, as `yield*` gives it.\n\t\tconst collected = Arr.unfold(iterator.next(), (state) =>\n\t\t\tstate.done === true\n\t\t\t\t? Option.none()\n\t\t\t\t: Option.some([state.value, iterator.next(state.value)] as const),\n\t\t)\n\t\treturn fromRules(domain, collected, options) as never\n\t}\n\nconst sameDomain = (policies: ReadonlyArray<Any>): Domain.Any => {\n\t// A copy of a policy made with a spread may keep the type fields of another\n\t// policy. Only a policy value goes into a tree.\n\tif (!Arr.every(policies, isPolicy)) {\n\t\tthrow new globalThis.Error(\"A value in the list is not a policy\")\n\t}\n\tconst domain = policies[0]!.domain\n\tif (!Arr.every(policies, (policy) => policy.domain === domain)) {\n\t\tthrow new globalThis.Error(\n\t\t\t\"Policies from two domains can not be put together\",\n\t\t)\n\t}\n\treturn domain\n}\n\n// The constructors of the nodes stop a tree past `policyDepth` or\n// `policyNodes`.\nexport const combine =\n\t(tag: \"All\" | \"Any\" | \"FirstOf\") =>\n\t(policies: ReadonlyArray<Any>): any =>\n\t\tfromNode(\n\t\t\tsameDomain(policies),\n\t\t\tnew PolicyNodes[tag](Arr.map(policies, nodeOf)),\n\t\t)\n\n/** Puts a limit on a policy. A deny from `cap` always wins. */\nexport const ceiling = dual(2, (policy: Any, cap: Any): any =>\n\tfromNode(\n\t\tsameDomain([policy, cap]),\n\t\tnew PolicyNodes.Ceiling({ cap: nodeOf(cap), policy: nodeOf(policy) }),\n\t),\n)\n\n/**\n * Gives the services to the predicates of a tree. A node with none stays. The\n * conditions get the services first, from a flat list of the rules, so the\n * walk does not go down the levels of the tree and of a condition at once.\n */\nconst provideNode = (\n\troot: PolicyNodes.Node,\n\tcontext: Context.Context<never>,\n): PolicyNodes.Node => {\n\t// A native map, because it must find a rule by the rule itself: two rules\n\t// with the same data are `Equal`.\n\tconst provided = new Map<RuleData.Rule, RuleData.Rule>(\n\t\tArr.map(PolicyNodes.rulesOf(root), (value) => [\n\t\t\tvalue,\n\t\t\tvalue.when === undefined || !Nodes.toNode(value.when).opaque\n\t\t\t\t? value\n\t\t\t\t: RuleData.make({\n\t\t\t\t\t\t...value,\n\t\t\t\t\t\twhen: Condition.provideContext(\n\t\t\t\t\t\t\tvalue.when as Condition.Any,\n\t\t\t\t\t\t\tcontext as never,\n\t\t\t\t\t\t),\n\t\t\t\t\t}),\n\t\t]),\n\t)\n\tconst provide = (node: PolicyNodes.Node): PolicyNodes.Node => {\n\t\tif (!node.opaque) {\n\t\t\treturn node\n\t\t}\n\t\tswitch (node._tag) {\n\t\t\tcase \"Rules\":\n\t\t\t\treturn new PolicyNodes.Rules({\n\t\t\t\t\talgorithm: node.algorithm,\n\t\t\t\t\trules: Arr.map(node.rules, (value) => provided.get(value) ?? value),\n\t\t\t\t})\n\t\t\tcase \"All\":\n\t\t\tcase \"Any\":\n\t\t\tcase \"FirstOf\":\n\t\t\t\treturn new PolicyNodes[node._tag](Arr.map(node.policies, provide))\n\t\t\tcase \"Ceiling\":\n\t\t\t\treturn new PolicyNodes.Ceiling({\n\t\t\t\t\tcap: provide(node.cap),\n\t\t\t\t\tpolicy: provide(node.policy),\n\t\t\t\t})\n\t\t}\n\t}\n\treturn provide(root)\n}\n\n/** Gives the services in `context` to every predicate of a policy. */\nexport const provideContext = dual(\n\t2,\n\t(policy: Any, context: Context.Context<never>): any =>\n\t\tfromNode(policy.domain, provideNode(nodeOf(policy), context)),\n)\n\n/** Tests if a value is a policy. */\nexport const isPolicy = (u: unknown): u is Any => PolicyData.is(u)\n\n/** The Schema of the policies of a domain with no predicates. */\nexport const schema = (domain: Domain.Any): Schema.Top =>\n\tSchema.Unknown.pipe(\n\t\tSchema.decodeTo(\n\t\t\t// A policy with a predicate is not a plain policy, and it can not\n\t\t\t// become JSON.\n\t\t\tSchema.declare(\n\t\t\t\t(u): u is Any =>\n\t\t\t\t\tisPolicy(u) && u.domain === domain && !nodeOf(u).opaque,\n\t\t\t).check(\n\t\t\t\tSchema.makeFilter((policy) => Bind.treeIssues(domain, nodeOf(policy))),\n\t\t\t),\n\t\t\tSchemaTransformation.transformEffect({\n\t\t\t\tdecode: (input: unknown, options) =>\n\t\t\t\t\tPolicyJson.decode(input, domain).pipe(\n\t\t\t\t\t\tEffect.map((node) => fromNode(domain, node)),\n\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t(problem) =>\n\t\t\t\t\t\t\t\tnew SchemaIssue.Pointer(\n\t\t\t\t\t\t\t\t\tproblem.path,\n\t\t\t\t\t\t\t\t\tproblem.issue ??\n\t\t\t\t\t\t\t\t\t\tnew SchemaIssue.InvalidValue(\n\t\t\t\t\t\t\t\t\t\t\t{ message: problem.message },\n\t\t\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\t\t\toptions,\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\tencode: (policy: Any) =>\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\tPolicyJson.encode(nodeOf(policy), domain.meta),\n\t\t\t\t\t\t(error) => error.issue,\n\t\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t) as never\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"policy.js","names":["PolicyJson.toJson","PolicyJson.syncMeta","Limits.exceeded","PolicyNodes.Rules","Bind.treeIssues","RuleData.builder","PolicyNodes","PolicyNodes.Ceiling","PolicyNodes.rulesOf","Nodes.toNode","RuleData.make","Condition.provideContext","Trampoline.run","PolicyJson.decode","PolicyJson.encode"],"sources":["../../src/internal/policy.ts"],"sourcesContent":["/**\n * The values of policies: the class of a policy, the constructors, the\n * combinators and the Schema. The types are in `Policy.ts`.\n */\nimport type * as Context from \"effect/Context\"\nimport * as Data from \"effect/Data\"\nimport * as Arr from \"effect/Array\"\nimport * as Effect from \"effect/Effect\"\nimport { dual } from \"effect/Function\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as SchemaIssue from \"effect/SchemaIssue\"\nimport * as SchemaTransformation from \"effect/SchemaTransformation\"\nimport * as Condition from \"../Condition.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Policy from \"../Policy.ts\"\nimport * as Bind from \"./bind.ts\"\nimport * as Limits from \"./limits.ts\"\nimport * as Nodes from \"./nodes.ts\"\nimport * as PolicyJson from \"./policy-json.ts\"\nimport * as PolicyNodes from \"./policy-nodes.ts\"\nimport * as RuleData from \"./rule.ts\"\nimport * as Trampoline from \"./trampoline.ts\"\n\ntype Any = Policy.Any\ntype Node = Policy.Node\ntype Options = Policy.Options\n\n/**\n * The class of every policy value. Two policies of one domain with the same\n * tree are equal. As JSON, a policy is its tree.\n */\nexport class PolicyData<\n\tD extends Domain.Any = any,\n\tX = never,\n> extends Data.Class<{\n\treadonly domain: D\n\treadonly node: Node\n}> {\n\t// A private field makes the type of a policy its own: a spread of a\n\t// policy, or an object made by hand, is not a policy. `Equal` does not\n\t// read it.\n\treadonly #policy = true\n\t/** Only in the types. */\n\tdeclare readonly \"~Opaque\": () => X\n\t/**\n\t * Only in the types. A policy reads the values and the principal of its\n\t * domain, so a policy of one domain is not a policy of a wider domain.\n\t */\n\tdeclare readonly \"~Domain\": (domain: D) => D\n\n\ttoJSON(): unknown {\n\t\treturn PolicyJson.toJson(this.node, PolicyJson.syncMeta(this.domain.meta))\n\t}\n\n\t/** Tests if a value is a policy, by its private field. */\n\tstatic is(u: unknown): u is Any {\n\t\treturn typeof u === \"object\" && u !== null && #policy in u\n\t}\n}\n\n/** The node of a policy, with no types, for the code below. */\nexport const nodeOf = (policy: Any): PolicyNodes.Node => policy.node as never\n\n/**\n * Makes a policy of a domain from its tree. The constructors of the nodes\n * check the ceiling of `policyNodes`, since a node does not know its domain,\n * so this checks the limit of the domain.\n */\nexport const fromNode = (domain: Domain.Any, node: PolicyNodes.Node): any => {\n\tconst most = domain.limits.policyNodes\n\tif (node.size > most) {\n\t\tthrow Limits.exceeded(\n\t\t\t\"A policy tree\",\n\t\t\t\"policyNodes\",\n\t\t\t\"nodes\",\n\t\t\tnode.size,\n\t\t\tmost,\n\t\t)\n\t}\n\treturn new PolicyData({ domain, node })\n}\n\n/**\n * The error when a rule does not fit its domain. It has one issue for each\n * problem, with its path.\n */\nexport class PolicyError extends Schema.TaggedError<PolicyError>(\n\t\"@nmnmcc/ability/Policy/PolicyError\",\n)(\"PolicyError\", {\n\tissues: Schema.Array(\n\t\tSchema.Struct({\n\t\t\tpath: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),\n\t\t\tmessage: Schema.String,\n\t\t}),\n\t),\n\tmessage: Schema.String,\n}) {}\n\n/** Makes a policy from a list of rules, and binds them to the domain. */\nconst fromRules = (\n\tdomain: Domain.Any,\n\tvalues: Iterable<unknown>,\n\toptions?: Options,\n): Any => {\n\t// The constructor takes only rules, and it stops a list past the limits.\n\tconst node = new PolicyNodes.Rules({\n\t\talgorithm: options?.algorithm ?? \"denyOverrides\",\n\t\trules: Array.from(values) as ReadonlyArray<RuleData.Rule>,\n\t})\n\t// A rule from a builder of another domain is a mistake in the code, so it\n\t// is a defect. Two domains may have the same types, so the types do not\n\t// find it.\n\tif (\n\t\tArr.some(node.rules, (value) => {\n\t\t\tconst origin = value.domain\n\t\t\treturn origin !== undefined && origin !== domain\n\t\t})\n\t) {\n\t\tthrow new globalThis.Error(\n\t\t\t\"A rule comes from the builder of another domain\",\n\t\t)\n\t}\n\t// Every rule is bound to the domain here, also a rule from `Policy.rule`.\n\tconst issues = Bind.treeIssues(domain, node)\n\tif (issues.length > 0) {\n\t\tthrow PolicyError.make({\n\t\t\tissues: Arr.map(issues, (issue) => ({\n\t\t\t\tpath: Arr.map(issue.path, (part) =>\n\t\t\t\t\ttypeof part === \"number\" ? part : String(part),\n\t\t\t\t),\n\t\t\t\tmessage: issue.issue,\n\t\t\t})),\n\t\t\tmessage: `A rule does not fit the domain: ${issues.map((issue) => issue.issue).join(\"; \")}`,\n\t\t})\n\t}\n\treturn fromNode(domain, node)\n}\n\n/** Makes a policy from a generator function that gives rules. */\nexport const define =\n\t(domain: Domain.Any, options?: Options) =>\n\t(body: (builder: unknown) => Generator<unknown, unknown, unknown>): Any => {\n\t\tconst iterator = body(RuleData.builder(domain))\n\t\t// Each rule goes back into the generator, as `yield*` gives it.\n\t\tconst collected = Arr.unfold(iterator.next(), (state) =>\n\t\t\tstate.done === true\n\t\t\t\t? Option.none()\n\t\t\t\t: Option.some([state.value, iterator.next(state.value)] as const),\n\t\t)\n\t\treturn fromRules(domain, collected, options) as never\n\t}\n\nconst sameDomain = (policies: ReadonlyArray<Any>): Domain.Any => {\n\t// A copy of a policy made with a spread may keep the type fields of another\n\t// policy. Only a policy value goes into a tree.\n\tif (!Arr.every(policies, isPolicy)) {\n\t\tthrow new globalThis.Error(\"A value in the list is not a policy\")\n\t}\n\tconst domain = policies[0]!.domain\n\tif (!Arr.every(policies, (policy) => policy.domain === domain)) {\n\t\tthrow new globalThis.Error(\n\t\t\t\"Policies from two domains can not be put together\",\n\t\t)\n\t}\n\treturn domain\n}\n\n// The constructors of the nodes stop a tree past `policyDepth` or\n// `policyNodes`.\nexport const combine =\n\t(tag: \"All\" | \"Any\" | \"FirstOf\") =>\n\t(policies: ReadonlyArray<Any>): any =>\n\t\tfromNode(\n\t\t\tsameDomain(policies),\n\t\t\tnew PolicyNodes[tag](Arr.map(policies, nodeOf)),\n\t\t)\n\n/** Puts a limit on a policy. A deny from `cap` always wins. */\nexport const ceiling = dual(2, (policy: Any, cap: Any): any =>\n\tfromNode(\n\t\tsameDomain([policy, cap]),\n\t\tnew PolicyNodes.Ceiling({ cap: nodeOf(cap), policy: nodeOf(policy) }),\n\t),\n)\n\n/**\n * Gives the services to the predicates of a tree. A node with none stays. The\n * conditions get the services first, from a flat list of the rules, so the\n * walk does not go down the levels of the tree and of a condition at once.\n */\nconst provideNode = (\n\troot: PolicyNodes.Node,\n\tcontext: Context.Context<never>,\n): PolicyNodes.Node => {\n\t// A native map, because it must find a rule by the rule itself: two rules\n\t// with the same data are `Equal`.\n\tconst provided = new Map<RuleData.Rule, RuleData.Rule>(\n\t\tArr.map(PolicyNodes.rulesOf(root), (value) => [\n\t\t\tvalue,\n\t\t\tvalue.when === undefined || !Nodes.toNode(value.when).opaque\n\t\t\t\t? value\n\t\t\t\t: RuleData.make({\n\t\t\t\t\t\t...value,\n\t\t\t\t\t\twhen: Condition.provideContext(\n\t\t\t\t\t\t\tvalue.when as Condition.Any,\n\t\t\t\t\t\t\tcontext as never,\n\t\t\t\t\t\t),\n\t\t\t\t\t}),\n\t\t]),\n\t)\n\t// The walk of the tree takes the steps of a trampoline, so a deep tree\n\t// does not fill the stack.\n\treturn Trampoline.run(root.depth, (R) => {\n\t\tconst provide = (\n\t\t\tnode: PolicyNodes.Node,\n\t\t): Trampoline.Step<PolicyNodes.Node> => {\n\t\t\tif (!node.opaque) {\n\t\t\t\treturn R.succeed(node)\n\t\t\t}\n\t\t\tswitch (node._tag) {\n\t\t\t\tcase \"Rules\":\n\t\t\t\t\treturn R.succeed(\n\t\t\t\t\t\tnew PolicyNodes.Rules({\n\t\t\t\t\t\t\talgorithm: node.algorithm,\n\t\t\t\t\t\t\trules: Arr.map(\n\t\t\t\t\t\t\t\tnode.rules,\n\t\t\t\t\t\t\t\t(value) => provided.get(value) ?? value,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\tcase \"All\":\n\t\t\t\tcase \"Any\":\n\t\t\t\tcase \"FirstOf\": {\n\t\t\t\t\tconst tag = node._tag\n\t\t\t\t\treturn R.map(\n\t\t\t\t\t\tR.forEach(node.policies, (child) =>\n\t\t\t\t\t\t\tR.suspend(() => provide(child)),\n\t\t\t\t\t\t),\n\t\t\t\t\t\t(policies) => new PolicyNodes[tag](policies),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tcase \"Ceiling\":\n\t\t\t\t\treturn R.flatMap(\n\t\t\t\t\t\tR.suspend(() => provide(node.cap)),\n\t\t\t\t\t\t(cap) =>\n\t\t\t\t\t\t\tR.map(\n\t\t\t\t\t\t\t\tR.suspend(() => provide(node.policy)),\n\t\t\t\t\t\t\t\t(policy) => new PolicyNodes.Ceiling({ cap, policy }),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\treturn provide(root)\n\t})\n}\n\n/** Gives the services in `context` to every predicate of a policy. */\nexport const provideContext = dual(\n\t2,\n\t(policy: Any, context: Context.Context<never>): any =>\n\t\tfromNode(policy.domain, provideNode(nodeOf(policy), context)),\n)\n\n/** Tests if a value is a policy. */\nexport const isPolicy = (u: unknown): u is Any => PolicyData.is(u)\n\n/** The Schema of the policies of a domain with no predicates. */\nexport const schema = (domain: Domain.Any): Schema.Top =>\n\tSchema.Unknown.pipe(\n\t\tSchema.decodeTo(\n\t\t\t// A policy with a predicate is not a plain policy, and it can not\n\t\t\t// become JSON.\n\t\t\tSchema.declare(\n\t\t\t\t(u): u is Any =>\n\t\t\t\t\tisPolicy(u) && u.domain === domain && !nodeOf(u).opaque,\n\t\t\t).check(\n\t\t\t\tSchema.makeFilter((policy) => Bind.treeIssues(domain, nodeOf(policy))),\n\t\t\t),\n\t\t\tSchemaTransformation.transformEffect({\n\t\t\t\tdecode: (input: unknown, options) =>\n\t\t\t\t\tPolicyJson.decode(input, domain).pipe(\n\t\t\t\t\t\tEffect.map((node) => fromNode(domain, node)),\n\t\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\t\t(problem) =>\n\t\t\t\t\t\t\t\tnew SchemaIssue.Pointer(\n\t\t\t\t\t\t\t\t\tproblem.path,\n\t\t\t\t\t\t\t\t\tproblem.issue ??\n\t\t\t\t\t\t\t\t\t\tnew SchemaIssue.InvalidValue(\n\t\t\t\t\t\t\t\t\t\t\t{ message: problem.message },\n\t\t\t\t\t\t\t\t\t\t\tinput,\n\t\t\t\t\t\t\t\t\t\t\toptions,\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\tencode: (policy: Any) =>\n\t\t\t\t\tEffect.mapError(\n\t\t\t\t\t\tPolicyJson.encode(nodeOf(policy), domain.meta),\n\t\t\t\t\t\t(error) => error.issue,\n\t\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t) as never\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { catalogOf } from "./catalog.js";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
2
3
|
import * as Schema from "effect/Schema";
|
|
3
4
|
import { Class } from "effect/Data";
|
|
4
|
-
import * as Arr from "effect/Array";
|
|
5
5
|
import { hasProperty } from "effect/Predicate";
|
|
6
6
|
// src/internal/resource.ts
|
|
7
7
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule.d.ts","names":["Policy.RuleShape"],"sources":["../../src/internal/rule.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"rule.d.ts","names":["Policy.RuleShape"],"sources":["../../src/internal/rule.ts"],"mappings":";;;;;;;;;;;;;qBA4Ca,KAAK,WAAW,WAAW,qBAAqB,KAAK,MACjEA,UAAiB,GAAG;;;4BAGc;;uBAEL,QAAQ,MAAM;GAQzC,OAAO,aAAa,UAAU,KAAK,GAAG,GAAG,IAAI,KAAK,GAAG,GAAG;;SAMnD,GAAG,aAAa,KAAK"}
|
package/dist/internal/rule.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { fieldPathParts } from "./limits.js";
|
|
1
2
|
import { isNode } from "./nodes.js";
|
|
2
3
|
import { lookup } from "./catalog.js";
|
|
3
4
|
import { descriptor, isResource } from "./resource.js";
|
|
4
5
|
import { builder as builder$1 } from "../Condition.js";
|
|
5
6
|
import { fitsCatalog, matches, matchesBelow, parseBound } from "./patterns.js";
|
|
6
|
-
import * as Data from "effect/Data";
|
|
7
7
|
import * as Arr from "effect/Array";
|
|
8
|
+
import * as Data from "effect/Data";
|
|
8
9
|
// src/internal/rule.ts
|
|
9
10
|
/**
|
|
10
11
|
* The values of rules, with no types. The types are in `Policy.ts`.
|
|
@@ -90,12 +91,12 @@ const matchesInside = (pattern, field) => matchesBelow(parseBound(pattern), fiel
|
|
|
90
91
|
* away when the walk ends. The binding puts a limit on the work of the
|
|
91
92
|
* patterns of a policy on each kind, so one walk does at most that work.
|
|
92
93
|
*/
|
|
93
|
-
const fitsOf = (catalog) => {
|
|
94
|
+
const fitsOf = (catalog, parts) => {
|
|
94
95
|
const memo = /* @__PURE__ */ new Map();
|
|
95
96
|
return (pattern) => {
|
|
96
97
|
const known = memo.get(pattern);
|
|
97
98
|
if (known !== void 0) return known;
|
|
98
|
-
const found = fitsCatalog(parseBound(pattern), catalog);
|
|
99
|
+
const found = fitsCatalog(parseBound(pattern), catalog, parts);
|
|
99
100
|
memo.set(pattern, found);
|
|
100
101
|
return found;
|
|
101
102
|
};
|
|
@@ -120,10 +121,10 @@ const matchesTarget = (rule, target) => {
|
|
|
120
121
|
if (rule.fields.length === 0) return false;
|
|
121
122
|
if (target.field === void 0) {
|
|
122
123
|
const descriptor = target.descriptor;
|
|
123
|
-
return rule._tag === "Allow" && (descriptor === void 0 || Arr.some(rule.fields, target.fits ?? fitsOf(descriptor.catalog)));
|
|
124
|
+
return rule._tag === "Allow" && (descriptor === void 0 || Arr.some(rule.fields, target.fits ?? fitsOf(descriptor.catalog, target.parts ?? fieldPathParts)));
|
|
124
125
|
}
|
|
125
126
|
const field = target.field;
|
|
126
|
-
const open = rule._tag === "Deny" && target.descriptor !== void 0 && (lookup(target.descriptor.catalog, field)?.open ?? false);
|
|
127
|
+
const open = rule._tag === "Deny" && target.descriptor !== void 0 && (lookup(target.descriptor.catalog, field, target.parts)?.open ?? false);
|
|
127
128
|
return rule.fields.some((pattern) => matchesField(pattern, field) || open && matchesInside(pattern, field));
|
|
128
129
|
};
|
|
129
130
|
export { Rule, builder, fitsOf, isRule, make, makePlain, matchesField, matchesInside, matchesTarget };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule.js","names":["Nodes.isNode","ResourceData.isResource","ResourceData.descriptor","Condition.builder","Patterns.matches","Patterns.parseBound","Patterns.matchesBelow","Patterns.fitsCatalog","Catalog.lookup"],"sources":["../../src/internal/rule.ts"],"sourcesContent":["/**\n * The values of rules, with no types. The types are in `Policy.ts`.\n *\n * Rules do not change, and two rules with the same data are equal. A rule\n * works in a generator: it gives itself, so `yield* _.allow(...)` gives the\n * rule to `Policy.define`.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as Condition from \"../Condition.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Policy from \"../Policy.ts\"\nimport type * as Resource from \"../Resource.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Nodes from \"./nodes.ts\"\nimport * as ResourceData from \"./resource.ts\"\nimport * as Patterns from \"./patterns.ts\"\n\n/** The data of a rule. */\nexport interface Shape {\n\treadonly _tag: \"Allow\" | \"Deny\"\n\treadonly actions: ReadonlyArray<string>\n\treadonly resources: ReadonlyArray<string>\n\treadonly when?: Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n\t/**\n\t * The domain of the builder that made the rule. Two domains may have the\n\t * same types but other paths, so the types can not tell them apart. It is\n\t * not in the JSON form of a rule.\n\t */\n\treadonly domain?: Domain.Any\n}\n\n/**\n * The class of every rule value. The types are those of `Policy.Rule`: `D` is\n * the domain, `I` is what the tests of the condition read, and `X` is the\n * mark of the condition. With no types, as in the nodes of a policy, a rule\n * fits no domain, its condition fits no other rule, and it may have a\n * predicate.\n */\nexport class Rule<D = never, I = never, X = unknown> extends Data.Class<\n\tPolicy.RuleShape<I, X>\n> {\n\t/** Only in the types. */\n\tdeclare readonly \"~Opaque\": () => X\n\t/** Only in the types. */\n\tdeclare readonly \"~Domain\": (domain: D) => D\n\t// A private field makes the type of a rule its own, as for a policy: a\n\t// spread of a rule, or an object made by hand, is not a rule. `Equal` does\n\t// not read it. It comes last, so its `;` stays: the generator method below\n\t// needs one after a field. Node.js takes the `declare` fields out of the\n\t// code, and a `;` after one of them would go with it.\n\treadonly #rule = true;\n\n\t*[Symbol.iterator](): Generator<Rule<D, I, X>, Rule<D, I, X>, unknown> {\n\t\tyield this\n\t\treturn this\n\t}\n\n\t/** Tests if a value is a rule, by its private field. */\n\tstatic is(u: unknown): u is Rule {\n\t\treturn typeof u === \"object\" && u !== null && #rule in u\n\t}\n}\n\n/** Makes a rule from its data. It does not check the data against a domain. */\nexport const make = (data: Shape): Rule =>\n\t// A missing option is left out, so two rules with the same data are equal.\n\t// The data has no types here, as the code below works with no types.\n\tnew Rule({\n\t\t_tag: data._tag,\n\t\tactions: [...data.actions],\n\t\tresources: [...data.resources],\n\t\t...(data.when === undefined ? {} : { when: data.when }),\n\t\t...(data.fields === undefined ? {} : { fields: [...data.fields] }),\n\t\t...(data.reason === undefined ? {} : { reason: data.reason }),\n\t\t...(data.id === undefined ? {} : { id: data.id }),\n\t\t...(data.meta === undefined ? {} : { meta: data.meta }),\n\t\t...(data.domain === undefined ? {} : { domain: data.domain }),\n\t} as never)\n\n/**\n * Makes a rule from its data, as `Policy.rule` does. A condition with a\n * predicate is a defect: the types of the predicate tie it to the kinds of\n * the rule of a builder, and the binding can not check it.\n */\nexport const makePlain = (data: Shape): Rule => {\n\tif (data.when !== undefined && Nodes.isNode(data.when) && data.when.opaque) {\n\t\tthrow new Error(\n\t\t\t\"Policy.rule takes no condition with a predicate. Make the rule with Policy.builder.\",\n\t\t)\n\t}\n\treturn make(data)\n}\n\n/** Tests if a value is a rule. */\nexport const isRule = (u: unknown): u is Rule => Rule.is(u)\n\nconst toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>\n\tArray.isArray(value) ? value : [value as A]\n\nconst kindName = (target: unknown): string =>\n\tResourceData.isResource(target)\n\t\t? ResourceData.descriptor(target).kind\n\t\t: (target as string)\n\n/** The options of a rule from a builder, with no types. */\ninterface Options {\n\treadonly when?: (builder: never) => Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n}\n\nconst fromOptions =\n\t(tag: \"Allow\" | \"Deny\", domain: Domain.Any) =>\n\t(\n\t\taction: string | ReadonlyArray<string>,\n\t\tresource: unknown,\n\t\toptions?: Options,\n\t): Rule =>\n\t\tmake({\n\t\t\t_tag: tag,\n\t\t\tactions: toArray(action),\n\t\t\tresources: Arr.map(toArray(resource), kindName),\n\t\t\t...(options?.when === undefined\n\t\t\t\t? {}\n\t\t\t\t: { when: options.when(Condition.builder() as never) }),\n\t\t\t...(options?.fields === undefined ? {} : { fields: options.fields }),\n\t\t\t...(options?.reason === undefined ? {} : { reason: options.reason }),\n\t\t\t...(options?.id === undefined ? {} : { id: options.id }),\n\t\t\t...(options?.meta === undefined ? {} : { meta: options.meta }),\n\t\t\tdomain,\n\t\t})\n\n/** Makes the rule builder of a domain. */\nexport const builder = (domain: Domain.Any) => ({\n\tallow: fromOptions(\"Allow\", domain),\n\tdeny: fromOptions(\"Deny\", domain),\n})\n\n/**\n * Tests if a field name matches a field name or a field pattern of a rule. A\n * pattern matches the fields below a field that it matches too, so `settings`\n * matches `settings.model`.\n */\nexport const matchesField = (pattern: string, field: string): boolean =>\n\tPatterns.matches(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests if a field pattern may match a field inside `field`, such as\n * `settings.secret` inside `settings`.\n */\nexport const matchesInside = (pattern: string, field: string): boolean =>\n\tPatterns.matchesBelow(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Makes the test \"a field pattern matches some field of the catalog\" for one\n * walk, such as one decision. A policy may name one pattern very many times,\n * so the test keeps each answer by the text of the pattern, and the memo goes\n * away when the walk ends. The binding puts a limit on the work of the\n * patterns of a policy on each kind, so one walk does at most that work.\n */\nexport const fitsOf = (\n\tcatalog: Catalog.Catalog,\n): ((pattern: string) => boolean) => {\n\t// A native `Map`: the hash of a string in Effect has no random seed, and a\n\t// policy could hold very many patterns with one hash. The engine hashes a\n\t// string with a random seed.\n\tconst memo = new Map<string, boolean>()\n\treturn (pattern) => {\n\t\tconst known = memo.get(pattern)\n\t\tif (known !== undefined) {\n\t\t\treturn known\n\t\t}\n\t\tconst found = Patterns.fitsCatalog(Patterns.parseBound(pattern), catalog)\n\t\tmemo.set(pattern, found)\n\t\treturn found\n\t}\n}\n\n/**\n * Tests the parts of a rule that do not need the resource value: the action,\n * the kind and the field.\n *\n * A rule with an empty list of fields covers no request. A rule with fields\n * covers a request with no field only when it is an allow rule. So \"may\n * update some fields\" is a yes. A deny rule for some fields does not stop a\n * request for the whole value. With the `descriptor` of the kind, such an\n * allow rule covers the request only when one of its patterns matches a field\n * of the kind, and a deny rule for a part of a field that holds parts covers\n * a request for the whole field. A grant of fields that the kind does not have is not a grant of\n * some fields.\n */\nexport const matchesTarget = (\n\trule: Shape,\n\ttarget: {\n\t\treadonly action: string\n\t\treadonly kind: string\n\t\treadonly field?: string | undefined\n\t\treadonly descriptor?: Resource.Descriptor | undefined\n\t\t/** The test of {@link fitsOf} for the catalog of the descriptor. */\n\t\treadonly fits?: ((pattern: string) => boolean) | undefined\n\t},\n): boolean => {\n\tif (!rule.actions.includes(\"*\") && !rule.actions.includes(target.action)) {\n\t\treturn false\n\t}\n\tif (!rule.resources.includes(\"*\") && !rule.resources.includes(target.kind)) {\n\t\treturn false\n\t}\n\tif (rule.fields === undefined) {\n\t\treturn true\n\t}\n\t// A rule for no fields is a rule for nothing. A grant with its last field\n\t// taken away must not cover the whole value.\n\tif (rule.fields.length === 0) {\n\t\treturn false\n\t}\n\tif (target.field === undefined) {\n\t\tconst descriptor = target.descriptor\n\t\treturn (\n\t\t\trule._tag === \"Allow\" &&\n\t\t\t(descriptor === undefined ||\n\t\t\t\tArr.some(rule.fields, target.fits ?? fitsOf(descriptor.catalog)))\n\t\t)\n\t}\n\tconst field = target.field\n\t// A field that holds parts, as a record does, shows its parts too. So a\n\t// deny rule for a part inside it, as `data.secret` inside `data`, also\n\t// covers a request for the whole field.\n\tconst open =\n\t\trule._tag === \"Deny\" &&\n\t\ttarget.descriptor !== undefined &&\n\t\t(Catalog.lookup(target.descriptor.catalog, field)?.open ?? false)\n\treturn rule.fields.some(\n\t\t(pattern) =>\n\t\t\tmatchesField(pattern, field) || (open && matchesInside(pattern, field)),\n\t)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rule.js","names":["Nodes.isNode","ResourceData.isResource","ResourceData.descriptor","Condition.builder","Patterns.matches","Patterns.parseBound","Patterns.matchesBelow","Patterns.fitsCatalog","Limits.fieldPathParts","Catalog.lookup"],"sources":["../../src/internal/rule.ts"],"sourcesContent":["/**\n * The values of rules, with no types. The types are in `Policy.ts`.\n *\n * Rules do not change, and two rules with the same data are equal. A rule\n * works in a generator: it gives itself, so `yield* _.allow(...)` gives the\n * rule to `Policy.define`.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as Condition from \"../Condition.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Policy from \"../Policy.ts\"\nimport type * as Resource from \"../Resource.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Limits from \"./limits.ts\"\nimport * as Nodes from \"./nodes.ts\"\nimport * as ResourceData from \"./resource.ts\"\nimport * as Patterns from \"./patterns.ts\"\n\n/** The data of a rule. */\nexport interface Shape {\n\treadonly _tag: \"Allow\" | \"Deny\"\n\treadonly actions: ReadonlyArray<string>\n\treadonly resources: ReadonlyArray<string>\n\treadonly when?: Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n\t/**\n\t * The domain of the builder that made the rule. Two domains may have the\n\t * same types but other paths, so the types can not tell them apart. It is\n\t * not in the JSON form of a rule.\n\t */\n\treadonly domain?: Domain.Any\n}\n\n/**\n * The class of every rule value. The types are those of `Policy.Rule`: `D` is\n * the domain, `I` is what the tests of the condition read, and `X` is the\n * mark of the condition. With no types, as in the nodes of a policy, a rule\n * fits no domain, its condition fits no other rule, and it may have a\n * predicate.\n */\nexport class Rule<D = never, I = never, X = unknown> extends Data.Class<\n\tPolicy.RuleShape<I, X>\n> {\n\t/** Only in the types. */\n\tdeclare readonly \"~Opaque\": () => X\n\t/** Only in the types. */\n\tdeclare readonly \"~Domain\": (domain: D) => D\n\t// A private field makes the type of a rule its own, as for a policy: a\n\t// spread of a rule, or an object made by hand, is not a rule. `Equal` does\n\t// not read it. It comes last, so its `;` stays: the generator method below\n\t// needs one after a field. Node.js takes the `declare` fields out of the\n\t// code, and a `;` after one of them would go with it.\n\treadonly #rule = true;\n\n\t*[Symbol.iterator](): Generator<Rule<D, I, X>, Rule<D, I, X>, unknown> {\n\t\tyield this\n\t\treturn this\n\t}\n\n\t/** Tests if a value is a rule, by its private field. */\n\tstatic is(u: unknown): u is Rule {\n\t\treturn typeof u === \"object\" && u !== null && #rule in u\n\t}\n}\n\n/** Makes a rule from its data. It does not check the data against a domain. */\nexport const make = (data: Shape): Rule =>\n\t// A missing option is left out, so two rules with the same data are equal.\n\t// The data has no types here, as the code below works with no types.\n\tnew Rule({\n\t\t_tag: data._tag,\n\t\tactions: [...data.actions],\n\t\tresources: [...data.resources],\n\t\t...(data.when === undefined ? {} : { when: data.when }),\n\t\t...(data.fields === undefined ? {} : { fields: [...data.fields] }),\n\t\t...(data.reason === undefined ? {} : { reason: data.reason }),\n\t\t...(data.id === undefined ? {} : { id: data.id }),\n\t\t...(data.meta === undefined ? {} : { meta: data.meta }),\n\t\t...(data.domain === undefined ? {} : { domain: data.domain }),\n\t} as never)\n\n/**\n * Makes a rule from its data, as `Policy.rule` does. A condition with a\n * predicate is a defect: the types of the predicate tie it to the kinds of\n * the rule of a builder, and the binding can not check it.\n */\nexport const makePlain = (data: Shape): Rule => {\n\tif (data.when !== undefined && Nodes.isNode(data.when) && data.when.opaque) {\n\t\tthrow new Error(\n\t\t\t\"Policy.rule takes no condition with a predicate. Make the rule with Policy.builder.\",\n\t\t)\n\t}\n\treturn make(data)\n}\n\n/** Tests if a value is a rule. */\nexport const isRule = (u: unknown): u is Rule => Rule.is(u)\n\nconst toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>\n\tArray.isArray(value) ? value : [value as A]\n\nconst kindName = (target: unknown): string =>\n\tResourceData.isResource(target)\n\t\t? ResourceData.descriptor(target).kind\n\t\t: (target as string)\n\n/** The options of a rule from a builder, with no types. */\ninterface Options {\n\treadonly when?: (builder: never) => Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n}\n\nconst fromOptions =\n\t(tag: \"Allow\" | \"Deny\", domain: Domain.Any) =>\n\t(\n\t\taction: string | ReadonlyArray<string>,\n\t\tresource: unknown,\n\t\toptions?: Options,\n\t): Rule =>\n\t\tmake({\n\t\t\t_tag: tag,\n\t\t\tactions: toArray(action),\n\t\t\tresources: Arr.map(toArray(resource), kindName),\n\t\t\t...(options?.when === undefined\n\t\t\t\t? {}\n\t\t\t\t: { when: options.when(Condition.builder() as never) }),\n\t\t\t...(options?.fields === undefined ? {} : { fields: options.fields }),\n\t\t\t...(options?.reason === undefined ? {} : { reason: options.reason }),\n\t\t\t...(options?.id === undefined ? {} : { id: options.id }),\n\t\t\t...(options?.meta === undefined ? {} : { meta: options.meta }),\n\t\t\tdomain,\n\t\t})\n\n/** Makes the rule builder of a domain. */\nexport const builder = (domain: Domain.Any) => ({\n\tallow: fromOptions(\"Allow\", domain),\n\tdeny: fromOptions(\"Deny\", domain),\n})\n\n/**\n * Tests if a field name matches a field name or a field pattern of a rule. A\n * pattern matches the fields below a field that it matches too, so `settings`\n * matches `settings.model`.\n */\nexport const matchesField = (pattern: string, field: string): boolean =>\n\tPatterns.matches(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests if a field pattern may match a field inside `field`, such as\n * `settings.secret` inside `settings`.\n */\nexport const matchesInside = (pattern: string, field: string): boolean =>\n\tPatterns.matchesBelow(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Makes the test \"a field pattern matches some field of the catalog\" for one\n * walk, such as one decision. A policy may name one pattern very many times,\n * so the test keeps each answer by the text of the pattern, and the memo goes\n * away when the walk ends. The binding puts a limit on the work of the\n * patterns of a policy on each kind, so one walk does at most that work.\n */\nexport const fitsOf = (\n\tcatalog: Catalog.Catalog,\n\tparts: number,\n): ((pattern: string) => boolean) => {\n\t// A native `Map`: the hash of a string in Effect has no random seed, and a\n\t// policy could hold very many patterns with one hash. The engine hashes a\n\t// string with a random seed.\n\tconst memo = new Map<string, boolean>()\n\treturn (pattern) => {\n\t\tconst known = memo.get(pattern)\n\t\tif (known !== undefined) {\n\t\t\treturn known\n\t\t}\n\t\tconst found = Patterns.fitsCatalog(\n\t\t\tPatterns.parseBound(pattern),\n\t\t\tcatalog,\n\t\t\tparts,\n\t\t)\n\t\tmemo.set(pattern, found)\n\t\treturn found\n\t}\n}\n\n/**\n * Tests the parts of a rule that do not need the resource value: the action,\n * the kind and the field.\n *\n * A rule with an empty list of fields covers no request. A rule with fields\n * covers a request with no field only when it is an allow rule. So \"may\n * update some fields\" is a yes. A deny rule for some fields does not stop a\n * request for the whole value. With the `descriptor` of the kind, such an\n * allow rule covers the request only when one of its patterns matches a field\n * of the kind, and a deny rule for a part of a field that holds parts covers\n * a request for the whole field. A grant of fields that the kind does not have is not a grant of\n * some fields.\n */\nexport const matchesTarget = (\n\trule: Shape,\n\ttarget: {\n\t\treadonly action: string\n\t\treadonly kind: string\n\t\treadonly field?: string | undefined\n\t\treadonly descriptor?: Resource.Descriptor | undefined\n\t\t/** The test of {@link fitsOf} for the catalog of the descriptor. */\n\t\treadonly fits?: ((pattern: string) => boolean) | undefined\n\t\t/** The most parts of a field path in the domain. */\n\t\treadonly parts?: number | undefined\n\t},\n): boolean => {\n\tif (!rule.actions.includes(\"*\") && !rule.actions.includes(target.action)) {\n\t\treturn false\n\t}\n\tif (!rule.resources.includes(\"*\") && !rule.resources.includes(target.kind)) {\n\t\treturn false\n\t}\n\tif (rule.fields === undefined) {\n\t\treturn true\n\t}\n\t// A rule for no fields is a rule for nothing. A grant with its last field\n\t// taken away must not cover the whole value.\n\tif (rule.fields.length === 0) {\n\t\treturn false\n\t}\n\tif (target.field === undefined) {\n\t\tconst descriptor = target.descriptor\n\t\treturn (\n\t\t\trule._tag === \"Allow\" &&\n\t\t\t(descriptor === undefined ||\n\t\t\t\tArr.some(\n\t\t\t\t\trule.fields,\n\t\t\t\t\ttarget.fits ??\n\t\t\t\t\t\tfitsOf(descriptor.catalog, target.parts ?? Limits.fieldPathParts),\n\t\t\t\t))\n\t\t)\n\t}\n\tconst field = target.field\n\t// A field that holds parts, as a record does, shows its parts too. So a\n\t// deny rule for a part inside it, as `data.secret` inside `data`, also\n\t// covers a request for the whole field.\n\tconst open =\n\t\trule._tag === \"Deny\" &&\n\t\ttarget.descriptor !== undefined &&\n\t\t(Catalog.lookup(target.descriptor.catalog, field, target.parts)?.open ??\n\t\t\tfalse)\n\treturn rule.fields.some(\n\t\t(pattern) =>\n\t\t\tmatchesField(pattern, field) || (open && matchesInside(pattern, field)),\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","names":[],"sources":["../../src/internal/search.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"search.d.ts","names":[],"sources":["../../src/internal/search.ts"],"mappings":";;;;;;;;iBA4BiB;YACN;;;YAIC,iBAAiB,KAAK;EACjC;aAAwB,SAAS;;EACjC;EACA;aAAoB"}
|
package/dist/internal/search.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { searchSteps } from "./limits.js";
|
|
1
|
+
import { searchFields, searchSteps } from "./limits.js";
|
|
2
|
+
import { run } from "./trampoline.js";
|
|
2
3
|
import { always, never } from "./nodes.js";
|
|
3
4
|
import { and, keyOf, not, or, simplify, testCompare } from "./algebra.js";
|
|
4
5
|
import { anyKinds, lookup } from "./catalog.js";
|
|
5
|
-
import * as Data from "effect/Data";
|
|
6
6
|
import * as Arr from "effect/Array";
|
|
7
|
+
import * as Data from "effect/Data";
|
|
7
8
|
import { isObject } from "effect/Predicate";
|
|
8
9
|
import * as Option from "effect/Option";
|
|
9
10
|
// src/internal/search.ts
|
|
@@ -125,33 +126,39 @@ const testFixed = (node, value, search) => {
|
|
|
125
126
|
}
|
|
126
127
|
};
|
|
127
128
|
/** Puts one value into a field, and into the fields below it. */
|
|
128
|
-
const fix = (
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
const fix = (root, field, value, search) => run(root.depth, (R) => {
|
|
130
|
+
const go = (node) => {
|
|
131
|
+
const children = (nodes) => R.forEach(nodes, (child) => R.suspend(() => go(child)));
|
|
132
|
+
switch (node._tag) {
|
|
133
|
+
case "Compare":
|
|
134
|
+
case "In":
|
|
135
|
+
case "Exists": {
|
|
136
|
+
const known = fixedValue(node.field, field, value);
|
|
137
|
+
return R.succeed(known === free ? node : testFixed(node, known, search) ? always : never);
|
|
138
|
+
}
|
|
139
|
+
case "And": return R.map(children(node.conditions), and);
|
|
140
|
+
case "Or": return R.map(children(node.conditions), or);
|
|
141
|
+
case "Not": return R.map(R.suspend(() => go(node.condition)), not);
|
|
142
|
+
default: return R.succeed(node);
|
|
136
143
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
default: return node;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
144
|
+
};
|
|
145
|
+
return go(root);
|
|
146
|
+
});
|
|
143
147
|
/** The fields of the tests of a condition, once for each test. */
|
|
144
|
-
const testedFields = (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
148
|
+
const testedFields = (root) => run(root.depth, (R) => {
|
|
149
|
+
const go = (node) => {
|
|
150
|
+
switch (node._tag) {
|
|
151
|
+
case "Compare":
|
|
152
|
+
case "In":
|
|
153
|
+
case "Exists": return R.succeed([node.field]);
|
|
154
|
+
case "And":
|
|
155
|
+
case "Or": return R.map(R.forEach(node.conditions, (child) => R.suspend(() => go(child))), Arr.flatten);
|
|
156
|
+
case "Not": return R.suspend(() => go(node.condition));
|
|
157
|
+
default: return R.succeed([]);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return go(root);
|
|
161
|
+
});
|
|
155
162
|
/**
|
|
156
163
|
* Picks the next field to split on. A field comes before the fields below it,
|
|
157
164
|
* so a value of a field can fix them. Then a field in more tests comes first,
|
|
@@ -169,58 +176,62 @@ const nextField = (node) => {
|
|
|
169
176
|
* one field at a time, and tries one value from each cell of the field. Each
|
|
170
177
|
* step reads the whole condition, so the budget counts its size.
|
|
171
178
|
*/
|
|
172
|
-
const searchNode = (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
const searchNode = (search) => (R) => {
|
|
180
|
+
const go = (node, state, assignment) => {
|
|
181
|
+
if (node._tag === "True") return R.succeed({
|
|
182
|
+
assignment: Option.some(assignment),
|
|
183
|
+
state
|
|
184
|
+
});
|
|
185
|
+
if (node._tag === "False") return R.succeed({
|
|
186
|
+
assignment: Option.none(),
|
|
187
|
+
state
|
|
188
|
+
});
|
|
189
|
+
const key = keyOf(node);
|
|
190
|
+
if (search.failed.has(key)) return R.succeed({
|
|
191
|
+
assignment: Option.none(),
|
|
192
|
+
state
|
|
193
|
+
});
|
|
194
|
+
if (state.steps + node.size > search.budget) throw new OutOfBudget();
|
|
195
|
+
const field = nextField(node);
|
|
196
|
+
return R.map(R.reduce(search.cells(field), {
|
|
197
|
+
assignment: Option.none(),
|
|
198
|
+
state: {
|
|
199
|
+
...state,
|
|
200
|
+
steps: state.steps + node.size
|
|
201
|
+
}
|
|
202
|
+
}, (before, cell) => {
|
|
203
|
+
if (Option.isSome(before.assignment)) return R.succeed(before);
|
|
204
|
+
const steps = before.state.steps + node.size;
|
|
205
|
+
if (steps > search.budget) throw new OutOfBudget();
|
|
206
|
+
return R.suspend(() => go(fix(node, field, cell, search), {
|
|
207
|
+
...before.state,
|
|
208
|
+
steps
|
|
209
|
+
}, [...assignment, [field, cell]]));
|
|
210
|
+
}), (result) => {
|
|
211
|
+
if (Option.isNone(result.assignment)) search.failed.add(key);
|
|
212
|
+
return result;
|
|
213
|
+
});
|
|
185
214
|
};
|
|
186
|
-
|
|
187
|
-
const field = nextField(node);
|
|
188
|
-
const result = Arr.reduce(search.cells(field), {
|
|
189
|
-
assignment: Option.none(),
|
|
190
|
-
state: {
|
|
191
|
-
...state,
|
|
192
|
-
steps: state.steps + node.size
|
|
193
|
-
}
|
|
194
|
-
}, (before, cell) => {
|
|
195
|
-
if (Option.isSome(before.assignment)) return before;
|
|
196
|
-
const steps = before.state.steps + node.size;
|
|
197
|
-
if (steps > search.budget) throw new OutOfBudget();
|
|
198
|
-
return searchNode(fix(node, field, cell, search), search, {
|
|
199
|
-
...before.state,
|
|
200
|
-
steps
|
|
201
|
-
}, [...assignment, [field, cell]]);
|
|
202
|
-
});
|
|
203
|
-
if (Option.isNone(result.assignment)) search.failed.add(key);
|
|
204
|
-
return result;
|
|
215
|
+
return go;
|
|
205
216
|
};
|
|
206
|
-
/**
|
|
207
|
-
* Puts a value at a path in an object, and gives a new object. A value with
|
|
208
|
-
* parts (`opaque`) becomes an empty object, and it keeps an object that is
|
|
209
|
-
* there.
|
|
210
|
-
*/
|
|
211
217
|
const setPath = (root, names, value) => {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
};
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
const last = names[names.length - 1];
|
|
219
|
+
if (last === void 0) return root;
|
|
220
|
+
const own = (base, name) => Object.hasOwn(base, name) ? base[name] : void 0;
|
|
221
|
+
const bases = Arr.scan(names.slice(0, -1), root, (base, name) => {
|
|
222
|
+
const inner = own(base, name);
|
|
223
|
+
return isObject(inner) && inner !== opaque ? inner : {};
|
|
224
|
+
});
|
|
225
|
+
const target = bases[bases.length - 1];
|
|
226
|
+
const inner = own(target, last);
|
|
227
|
+
const placed = value === opaque && isObject(inner) ? target : {
|
|
228
|
+
...target,
|
|
229
|
+
[last]: value === opaque ? {} : value
|
|
223
230
|
};
|
|
231
|
+
return Arr.reduceRight(names.slice(0, -1), placed, (below, name, index) => ({
|
|
232
|
+
...bases[index],
|
|
233
|
+
[name]: below
|
|
234
|
+
}));
|
|
224
235
|
};
|
|
225
236
|
const exampleOf = (assignment) => Arr.reduce(assignment, {}, (root, [field, value]) => setPath(root, field.split("."), value));
|
|
226
237
|
/**
|
|
@@ -229,20 +240,23 @@ const exampleOf = (assignment) => Arr.reduce(assignment, {}, (root, [field, valu
|
|
|
229
240
|
* belongs to one search.
|
|
230
241
|
*/
|
|
231
242
|
const constantsOf = (node) => {
|
|
232
|
-
const pairs = (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
243
|
+
const pairs = run(node.depth, (R) => {
|
|
244
|
+
const go = (current) => {
|
|
245
|
+
switch (current._tag) {
|
|
246
|
+
case "Compare": return R.succeed([[current.field, [literalOf(current.value)]]]);
|
|
247
|
+
case "In": return R.succeed([[current.field, Arr.map(current.values, literalOf)]]);
|
|
248
|
+
case "Exists": return R.succeed([[current.field, []]]);
|
|
249
|
+
case "Under": throw new Error("Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.");
|
|
250
|
+
case "And":
|
|
251
|
+
case "Or": return R.map(R.forEach(current.conditions, (child) => R.suspend(() => go(child))), Arr.flatten);
|
|
252
|
+
case "Not": return R.suspend(() => go(current.condition));
|
|
253
|
+
default: return R.succeed([]);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
return go(node);
|
|
257
|
+
});
|
|
244
258
|
const all = /* @__PURE__ */ new Map();
|
|
245
|
-
Arr.forEach(pairs
|
|
259
|
+
Arr.forEach(pairs, ([field, values]) => {
|
|
246
260
|
const known = all.get(field) ?? /* @__PURE__ */ new Set();
|
|
247
261
|
Arr.forEach(values, (value) => known.add(value));
|
|
248
262
|
all.set(field, known);
|
|
@@ -250,16 +264,16 @@ const constantsOf = (node) => {
|
|
|
250
264
|
return all;
|
|
251
265
|
};
|
|
252
266
|
/** Gives the kinds of each field of a catalog. A field that it does not have may hold any value. */
|
|
253
|
-
const kindsIn = (catalog) => (field) => (catalog === void 0 ? void 0 : lookup(catalog, field))?.kinds ?? anyKinds;
|
|
267
|
+
const kindsIn = (catalog, parts) => (field) => (catalog === void 0 ? void 0 : lookup(catalog, field, parts))?.kinds ?? anyKinds;
|
|
254
268
|
/**
|
|
255
269
|
* Searches for values that make a condition true. `kindsOf` gives the kinds
|
|
256
270
|
* of values of each field. The condition must have no principal fields and no
|
|
257
271
|
* `Under` nodes.
|
|
258
272
|
*/
|
|
259
|
-
const satisfiable = (node, kindsOf, budget = searchSteps) => {
|
|
273
|
+
const satisfiable = (node, kindsOf, budget = searchSteps, fields = searchFields) => {
|
|
260
274
|
if (node.opaque) return satisfiability.Unknown({ reason: "predicate" });
|
|
261
275
|
const constants = constantsOf(node);
|
|
262
|
-
if (constants.size >
|
|
276
|
+
if (constants.size > fields) return satisfiability.Unknown({ reason: "budget" });
|
|
263
277
|
const cells = /* @__PURE__ */ new Map();
|
|
264
278
|
const search = {
|
|
265
279
|
members: /* @__PURE__ */ new Map(),
|
|
@@ -276,7 +290,7 @@ const satisfiable = (node, kindsOf, budget = searchSteps) => {
|
|
|
276
290
|
budget
|
|
277
291
|
};
|
|
278
292
|
try {
|
|
279
|
-
const found = searchNode(simplify(node),
|
|
293
|
+
const found = run(constants.size, (R) => searchNode(search)(R)(simplify(node), { steps: 0 }, []));
|
|
280
294
|
return Option.match(found.assignment, {
|
|
281
295
|
onNone: () => satisfiability.Unsatisfiable(),
|
|
282
296
|
onSome: (assignment) => satisfiability.Satisfiable({ example: exampleOf(assignment) })
|