@nmnmcc/ability 2.0.1 → 2.0.4
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 +25 -17
- package/dist/Condition.d.ts +26 -16
- package/dist/Condition.d.ts.map +1 -1
- package/dist/Condition.js +2 -1
- package/dist/Condition.js.map +1 -1
- package/dist/Limits.d.ts +3 -1
- package/dist/Limits.d.ts.map +1 -1
- package/dist/Limits.js +3 -1
- package/dist/Limits.js.map +1 -1
- package/dist/Policy.d.ts +6 -8
- package/dist/Policy.d.ts.map +1 -1
- package/dist/Policy.js +4 -2
- package/dist/Policy.js.map +1 -1
- package/dist/internal/algebra.js +23 -13
- package/dist/internal/algebra.js.map +1 -1
- package/dist/internal/analysis.js +4 -2
- package/dist/internal/analysis.js.map +1 -1
- package/dist/internal/authorizer.js +13 -7
- package/dist/internal/authorizer.js.map +1 -1
- package/dist/internal/bind.js +53 -27
- package/dist/internal/bind.js.map +1 -1
- package/dist/internal/catalog.d.ts +9 -0
- package/dist/internal/catalog.d.ts.map +1 -1
- package/dist/internal/catalog.js +37 -22
- package/dist/internal/catalog.js.map +1 -1
- package/dist/internal/decide.js +2 -1
- package/dist/internal/decide.js.map +1 -1
- package/dist/internal/place.js +19 -0
- package/dist/internal/place.js.map +1 -0
- package/dist/internal/policy-json.js +39 -39
- package/dist/internal/policy-json.js.map +1 -1
- package/dist/internal/rule.d.ts +20 -22
- package/dist/internal/rule.d.ts.map +1 -1
- package/dist/internal/rule.js +32 -4
- package/dist/internal/rule.js.map +1 -1
- package/package.json +1 -1
- package/src/Condition.ts +64 -50
- package/src/Limits.ts +3 -1
- package/src/Policy.ts +5 -9
- package/src/internal/algebra.ts +30 -13
- package/src/internal/analysis.ts +5 -0
- package/src/internal/authorizer.ts +17 -7
- package/src/internal/bind.ts +85 -23
- package/src/internal/catalog.ts +78 -30
- package/src/internal/decide.ts +3 -0
- package/src/internal/place.ts +32 -0
- package/src/internal/policy-json.ts +49 -29
- package/src/internal/rule.ts +55 -11
package/dist/internal/rule.js
CHANGED
|
@@ -5,6 +5,8 @@ import { builder as builder$1 } from "../Condition.js";
|
|
|
5
5
|
import { fitsCatalog, matches, matchesBelow, parseBound } from "./patterns.js";
|
|
6
6
|
import * as Data from "effect/Data";
|
|
7
7
|
import * as Arr from "effect/Array";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as MutableHashMap from "effect/MutableHashMap";
|
|
8
10
|
// src/internal/rule.ts
|
|
9
11
|
/**
|
|
10
12
|
* The values of rules, with no types. The types are in `Policy.ts`.
|
|
@@ -13,12 +15,23 @@ import * as Arr from "effect/Array";
|
|
|
13
15
|
* works in a generator: it gives itself, so `yield* _.allow(...)` gives the
|
|
14
16
|
* rule to `Policy.define`.
|
|
15
17
|
*/
|
|
16
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The class of every rule value. The types are those of `Policy.Rule`: `D` is
|
|
20
|
+
* the domain, `I` is what the tests of the condition read, and `X` is the
|
|
21
|
+
* mark of the condition. With no types, as in the nodes of a policy, a rule
|
|
22
|
+
* fits no domain, its condition fits no other rule, and it may have a
|
|
23
|
+
* predicate.
|
|
24
|
+
*/
|
|
17
25
|
var Rule = class extends Data.Class {
|
|
26
|
+
#rule = true;
|
|
18
27
|
*[Symbol.iterator]() {
|
|
19
28
|
yield this;
|
|
20
29
|
return this;
|
|
21
30
|
}
|
|
31
|
+
/** Tests if a value is a rule, by its private field. */
|
|
32
|
+
static is(u) {
|
|
33
|
+
return typeof u === "object" && u !== null && #rule in u;
|
|
34
|
+
}
|
|
22
35
|
};
|
|
23
36
|
/** Makes a rule from its data. It does not check the data against a domain. */
|
|
24
37
|
const make = (data) => new Rule({
|
|
@@ -42,7 +55,7 @@ const makePlain = (data) => {
|
|
|
42
55
|
return make(data);
|
|
43
56
|
};
|
|
44
57
|
/** Tests if a value is a rule. */
|
|
45
|
-
const isRule = (u) => u
|
|
58
|
+
const isRule = (u) => Rule.is(u);
|
|
46
59
|
const toArray = (value) => Array.isArray(value) ? value : [value];
|
|
47
60
|
const kindName = (target) => isResource(target) ? descriptor(target).kind : target;
|
|
48
61
|
const fromOptions = (tag, domain) => (action, resource, options) => make({
|
|
@@ -73,6 +86,21 @@ const matchesField = (pattern, field) => matches(parseBound(pattern), field.spli
|
|
|
73
86
|
*/
|
|
74
87
|
const matchesInside = (pattern, field) => matchesBelow(parseBound(pattern), field.split("."));
|
|
75
88
|
/**
|
|
89
|
+
* Makes the test "a field pattern matches some field of the catalog" for one
|
|
90
|
+
* walk, such as one decision. A policy may name one pattern very many times,
|
|
91
|
+
* so the test keeps each answer by the text of the pattern, and the memo goes
|
|
92
|
+
* away when the walk ends. The binding puts a limit on the work of the
|
|
93
|
+
* patterns of a policy on each kind, so one walk does at most that work.
|
|
94
|
+
*/
|
|
95
|
+
const fitsOf = (catalog) => {
|
|
96
|
+
const memo = MutableHashMap.empty();
|
|
97
|
+
return (pattern) => Option.getOrElse(MutableHashMap.get(memo, pattern), () => {
|
|
98
|
+
const found = fitsCatalog(parseBound(pattern), catalog);
|
|
99
|
+
MutableHashMap.set(memo, pattern, found);
|
|
100
|
+
return found;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
76
104
|
* Tests the parts of a rule that do not need the resource value: the action,
|
|
77
105
|
* the kind and the field.
|
|
78
106
|
*
|
|
@@ -92,12 +120,12 @@ const matchesTarget = (rule, target) => {
|
|
|
92
120
|
if (rule.fields.length === 0) return false;
|
|
93
121
|
if (target.field === void 0) {
|
|
94
122
|
const descriptor = target.descriptor;
|
|
95
|
-
return rule._tag === "Allow" && (descriptor === void 0 || Arr.some(rule.fields,
|
|
123
|
+
return rule._tag === "Allow" && (descriptor === void 0 || Arr.some(rule.fields, target.fits ?? fitsOf(descriptor.catalog)));
|
|
96
124
|
}
|
|
97
125
|
const field = target.field;
|
|
98
126
|
const open = rule._tag === "Deny" && target.descriptor !== void 0 && (lookup(target.descriptor.catalog, field)?.open ?? false);
|
|
99
127
|
return rule.fields.some((pattern) => matchesField(pattern, field) || open && matchesInside(pattern, field));
|
|
100
128
|
};
|
|
101
|
-
export { Rule, builder, isRule, make, makePlain, matchesField, matchesInside, matchesTarget };
|
|
129
|
+
export { Rule, builder, fitsOf, isRule, make, makePlain, matchesField, matchesInside, matchesTarget };
|
|
102
130
|
|
|
103
131
|
//# sourceMappingURL=rule.js.map
|
|
@@ -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 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/** The class of every rule value. */\nexport class Rule extends Data.Class<Shape> {\n\t*[Symbol.iterator](): Generator<this, this, unknown> {\n\t\tyield this\n\t\treturn this\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\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})\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 => u instanceof Rule\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 * 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},\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, (pattern) =>\n\t\t\t\t\tPatterns.fitsCatalog(\n\t\t\t\t\t\tPatterns.parseBound(pattern),\n\t\t\t\t\t\tdescriptor.catalog,\n\t\t\t\t\t),\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)?.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","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 MutableHashMap from \"effect/MutableHashMap\"\nimport * as Option from \"effect/Option\"\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\tconst memo = MutableHashMap.empty<string, boolean>()\n\treturn (pattern) =>\n\t\tOption.getOrElse(MutableHashMap.get(memo, pattern), () => {\n\t\t\tconst found = Patterns.fitsCatalog(Patterns.parseBound(pattern), catalog)\n\t\t\tMutableHashMap.set(memo, pattern, found)\n\t\t\treturn found\n\t\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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"}
|
package/package.json
CHANGED
package/src/Condition.ts
CHANGED
|
@@ -224,32 +224,24 @@ export interface Opaque<out E = never, out R = never> {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
* The error type of the predicates of a mark `X`. A mark that
|
|
228
|
-
*
|
|
227
|
+
* The error type of the predicates of a mark `X`. A mark that is not an
|
|
228
|
+
* {@link Opaque}, as `unknown` or `object`, may have any error.
|
|
229
229
|
*
|
|
230
230
|
* @stability stable
|
|
231
231
|
* @category utility types
|
|
232
232
|
* @since 2.0.0
|
|
233
233
|
*/
|
|
234
|
-
export type ErrorOf<X> =
|
|
235
|
-
? unknown
|
|
236
|
-
: X extends Opaque<infer E, infer _R>
|
|
237
|
-
? E
|
|
238
|
-
: never
|
|
234
|
+
export type ErrorOf<X> = X extends Opaque<infer E, infer _R> ? E : unknown
|
|
239
235
|
|
|
240
236
|
/**
|
|
241
|
-
* The service type of the predicates of a mark `X`. A mark that
|
|
242
|
-
*
|
|
237
|
+
* The service type of the predicates of a mark `X`. A mark that is not an
|
|
238
|
+
* {@link Opaque}, as `unknown` or `object`, may need any service.
|
|
243
239
|
*
|
|
244
240
|
* @stability stable
|
|
245
241
|
* @category utility types
|
|
246
242
|
* @since 2.0.0
|
|
247
243
|
*/
|
|
248
|
-
export type ServicesOf<X> =
|
|
249
|
-
? unknown
|
|
250
|
-
: X extends Opaque<infer _E, infer R>
|
|
251
|
-
? R
|
|
252
|
-
: never
|
|
244
|
+
export type ServicesOf<X> = X extends Opaque<infer _E, infer R> ? R : unknown
|
|
253
245
|
|
|
254
246
|
/**
|
|
255
247
|
* The mark `X` with no services.
|
|
@@ -258,11 +250,8 @@ export type ServicesOf<X> = unknown extends X
|
|
|
258
250
|
* @category utility types
|
|
259
251
|
* @since 2.0.0
|
|
260
252
|
*/
|
|
261
|
-
export type Provided<X> =
|
|
262
|
-
? Opaque<unknown>
|
|
263
|
-
: X extends Opaque<infer E, infer _R>
|
|
264
|
-
? Opaque<E>
|
|
265
|
-
: never
|
|
253
|
+
export type Provided<X> =
|
|
254
|
+
X extends Opaque<infer E, infer _R> ? Opaque<E> : Opaque<unknown>
|
|
266
255
|
|
|
267
256
|
/**
|
|
268
257
|
* What the predicates of a condition read: a resource value of the type `V`
|
|
@@ -366,14 +355,15 @@ type FieldPathInternal<
|
|
|
366
355
|
> = [Depth] extends [never]
|
|
367
356
|
? never
|
|
368
357
|
: {
|
|
369
|
-
|
|
370
|
-
|
|
358
|
+
// A record with number keys has number keys, as `counts.1`.
|
|
359
|
+
[K in keyof T & (string | number)]-?: PathsOf<
|
|
360
|
+
`${K}`,
|
|
371
361
|
NonNullable<T[K]>,
|
|
372
362
|
Depth,
|
|
373
363
|
Arrays,
|
|
374
364
|
Parents
|
|
375
365
|
>
|
|
376
|
-
}[keyof T & string]
|
|
366
|
+
}[keyof T & (string | number)]
|
|
377
367
|
|
|
378
368
|
/**
|
|
379
369
|
* The paths of one field `K` with the values `V`. Each kind of value in `V`
|
|
@@ -387,21 +377,30 @@ type PathsOf<
|
|
|
387
377
|
Arrays extends boolean,
|
|
388
378
|
Parents extends boolean,
|
|
389
379
|
> =
|
|
390
|
-
// A field that is only ever `null` or missing is a field too
|
|
391
|
-
|
|
380
|
+
// A field that is only ever `null` or missing is a field too, and so is a
|
|
381
|
+
// field that may hold any value, as one of `Schema.Unknown` may.
|
|
382
|
+
| ([V] extends [never] ? K : {} extends V ? K : never)
|
|
392
383
|
| ([Extract<V, Leaf | Whole>] extends [never] ? never : K)
|
|
393
384
|
| ([Extract<V, ReadonlyArray<unknown>>] extends [never]
|
|
394
385
|
? never
|
|
395
386
|
: Arrays extends true
|
|
396
387
|
? K
|
|
397
388
|
: never)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
389
|
+
// A condition reads a field below `K` only when `K` holds plain data: a
|
|
390
|
+
// list, a whole value such as a date, or a function may have a getter or a
|
|
391
|
+
// method with the name, so the binding does not take such a read. A rule
|
|
392
|
+
// or a request names a field and does not read it, so it may.
|
|
393
|
+
| (Arrays extends true
|
|
394
|
+
? ObjectPaths<
|
|
395
|
+
K,
|
|
396
|
+
Exclude<V, Leaf | Whole | Function | ReadonlyArray<unknown>>,
|
|
397
|
+
Depth,
|
|
398
|
+
Arrays,
|
|
399
|
+
Parents
|
|
400
|
+
>
|
|
401
|
+
: [Extract<V, Whole | Function | ReadonlyArray<unknown>>] extends [never]
|
|
402
|
+
? ObjectPaths<K, Exclude<V, Leaf>, Depth, Arrays, Parents>
|
|
403
|
+
: never)
|
|
405
404
|
|
|
406
405
|
/** The paths of a field `K` that holds the objects `O`, and of the fields below. */
|
|
407
406
|
type ObjectPaths<
|
|
@@ -419,7 +418,9 @@ type ObjectPaths<
|
|
|
419
418
|
: Arrays extends true
|
|
420
419
|
? string extends keyof O
|
|
421
420
|
? K
|
|
422
|
-
:
|
|
421
|
+
: number extends keyof O
|
|
422
|
+
? K
|
|
423
|
+
: never
|
|
423
424
|
: never)
|
|
424
425
|
| `${K}.${FieldPathInternal<O, PreviousDepth[Depth], Arrays, Parents>}`
|
|
425
426
|
|
|
@@ -427,9 +428,10 @@ type ObjectPaths<
|
|
|
427
428
|
* The paths of the fields of `T` that hold one value, with dots for nested
|
|
428
429
|
* fields. Arrays are left out. A map, a set, a date, a URL, a byte array, an
|
|
429
430
|
* `Option` and the other value types of Effect, such as `BigDecimal` and
|
|
430
|
-
* `Duration`, are one field, and a path does not go into them.
|
|
431
|
-
*
|
|
432
|
-
*
|
|
431
|
+
* `Duration`, are one field, and a path does not go into them. A path does
|
|
432
|
+
* not go into a field that may hold one of them or a list either. The type
|
|
433
|
+
* goes down four levels, so type checking stays fast. For a union of types,
|
|
434
|
+
* only the common fields are paths.
|
|
433
435
|
*
|
|
434
436
|
* @stability stable
|
|
435
437
|
* @category utility types
|
|
@@ -491,25 +493,36 @@ type Normalize<A> = Exclude<A, undefined> | (undefined extends A ? null : never)
|
|
|
491
493
|
|
|
492
494
|
/**
|
|
493
495
|
* The type of the field of `T` at `Path`. A field that may be missing may also
|
|
494
|
-
* be `null`.
|
|
496
|
+
* be `null`. Each type of a union gives its own value: a field below a value
|
|
497
|
+
* that is not an object, as below a string, is `null`, as a read of it is.
|
|
495
498
|
*
|
|
496
499
|
* @stability stable
|
|
497
500
|
* @category utility types
|
|
498
501
|
* @since 0.3.0
|
|
499
502
|
*/
|
|
500
|
-
export type PathValue<
|
|
501
|
-
T,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
503
|
+
export type PathValue<T, Path extends string> = T extends unknown
|
|
504
|
+
? PathValueIn<T, Path>
|
|
505
|
+
: never
|
|
506
|
+
|
|
507
|
+
/** The type of the field at `Path` of `T`, which is not a union. */
|
|
508
|
+
type PathValueIn<T, Path extends string> = T extends object
|
|
509
|
+
? Path extends `${infer Head}.${infer Rest}`
|
|
510
|
+
? PathValue<ChildOf<T, Head>, Rest>
|
|
511
|
+
: Normalize<ChildOf<T, Path>>
|
|
512
|
+
: null
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The type of the part `Name` of `T`. A part of a path is text, so the part
|
|
516
|
+
* `"1"` names the number key `1` too, as of a record with number keys.
|
|
517
|
+
*/
|
|
518
|
+
type ChildOf<T, Name extends string> = Name extends keyof T
|
|
519
|
+
? T[Name]
|
|
520
|
+
: Name extends `${infer N extends number}`
|
|
521
|
+
? N extends keyof T
|
|
522
|
+
? T[N]
|
|
523
|
+
: number extends keyof T
|
|
524
|
+
? T[number]
|
|
525
|
+
: never
|
|
513
526
|
: never
|
|
514
527
|
|
|
515
528
|
/**
|
|
@@ -858,7 +871,8 @@ type ReadsInput<I> =
|
|
|
858
871
|
/**
|
|
859
872
|
* Tests a condition on a value. A predicate may fail with its error. When the
|
|
860
873
|
* condition has a predicate, the input must have the value and the principal
|
|
861
|
-
* that the predicate reads.
|
|
874
|
+
* that the predicate reads. The test runs on the simple form of the condition
|
|
875
|
+
* with the principal in it, so it reads only the fields that a residual reads.
|
|
862
876
|
*
|
|
863
877
|
* @stability stable
|
|
864
878
|
* @category evaluation
|
package/src/Limits.ts
CHANGED
|
@@ -97,7 +97,9 @@ export const catalogFields: number = Limits.catalogFields
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* The most steps of one search for a value, as in `Policy.coverage`. Past
|
|
100
|
-
* it, the answer is `Unknown`.
|
|
100
|
+
* it, the answer is `Unknown`. It is also the most steps of the checks of
|
|
101
|
+
* the field patterns of one policy on every kind of their rules. Past it, the
|
|
102
|
+
* policy does not load.
|
|
101
103
|
*
|
|
102
104
|
* @stability stable
|
|
103
105
|
* @category constants
|
package/src/Policy.ts
CHANGED
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
import type * as Context from "effect/Context"
|
|
38
38
|
import type * as Data from "effect/Data"
|
|
39
39
|
import type * as Effect from "effect/Effect"
|
|
40
|
-
import type * as Pipeable from "effect/Pipeable"
|
|
41
40
|
import type * as Schema from "effect/Schema"
|
|
42
41
|
import type * as Condition from "./Condition.ts"
|
|
43
42
|
import * as Analysis from "./internal/analysis.ts"
|
|
@@ -301,12 +300,7 @@ export interface Explanation {
|
|
|
301
300
|
* @category models
|
|
302
301
|
* @since 0.3.0
|
|
303
302
|
*/
|
|
304
|
-
export
|
|
305
|
-
extends RuleShape<I, X>, Pipeable.Pipeable {
|
|
306
|
-
readonly "~Opaque": () => X
|
|
307
|
-
readonly "~Domain": (domain: D) => D
|
|
308
|
-
[Symbol.iterator](): Generator<Rule<D, I, X>, Rule<D, I, X>, unknown>
|
|
309
|
-
}
|
|
303
|
+
export type Rule<D = any, I = unknown, X = never> = RuleData.Rule<D, I, X>
|
|
310
304
|
|
|
311
305
|
/**
|
|
312
306
|
* The data of a rule.
|
|
@@ -778,8 +772,10 @@ export type ResidualReads<D, K> = Condition.Reads<
|
|
|
778
772
|
/**
|
|
779
773
|
* Gives the condition on resource values for which the policy allows a
|
|
780
774
|
* request. It has no principal fields and no `Under` nodes, so you can give it
|
|
781
|
-
* to `Condition.fold`. It is
|
|
782
|
-
*
|
|
775
|
+
* to `Condition.fold`. It is made smaller, so it is often `True` or `False`
|
|
776
|
+
* when the answer does not depend on the value, but it does no search: a
|
|
777
|
+
* condition that is true for every value may stay a test. To know if the
|
|
778
|
+
* policy allows every value, or some value, use {@link coverage}.
|
|
783
779
|
*
|
|
784
780
|
* With a scope (`within`), the condition is the scope and the residual
|
|
785
781
|
* together, so it is false for every value outside the scope. Give the policy
|
package/src/internal/algebra.ts
CHANGED
|
@@ -259,8 +259,8 @@ const testUnder = (
|
|
|
259
259
|
})
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
/** Tests a condition with no predicate on a value. */
|
|
263
|
-
|
|
262
|
+
/** Tests a simple condition with no predicate on a value. */
|
|
263
|
+
const testSync = (node: Node, input: Input): boolean => {
|
|
264
264
|
switch (node._tag) {
|
|
265
265
|
case "True":
|
|
266
266
|
return true
|
|
@@ -283,11 +283,11 @@ export const evaluateSync = (node: Node, input: Input): boolean => {
|
|
|
283
283
|
case "Under":
|
|
284
284
|
return testUnder(node.prefix, input)
|
|
285
285
|
case "And":
|
|
286
|
-
return Arr.every(node.conditions, (child) =>
|
|
286
|
+
return Arr.every(node.conditions, (child) => testSync(child, input))
|
|
287
287
|
case "Or":
|
|
288
|
-
return Arr.some(node.conditions, (child) =>
|
|
288
|
+
return Arr.some(node.conditions, (child) => testSync(child, input))
|
|
289
289
|
case "Not":
|
|
290
|
-
return !
|
|
290
|
+
return !testSync(node.condition, input)
|
|
291
291
|
case "Predicate":
|
|
292
292
|
// The types stop this. A predicate needs `evaluate`.
|
|
293
293
|
throw new Error(
|
|
@@ -297,38 +297,55 @@ export const evaluateSync = (node: Node, input: Input): boolean => {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
/**
|
|
300
|
-
* Tests a condition on a value. The children of `And` and `Or` run in
|
|
301
|
-
* and they stop at the first child that gives the answer.
|
|
300
|
+
* Tests a simple condition on a value. The children of `And` and `Or` run in
|
|
301
|
+
* order, and they stop at the first child that gives the answer.
|
|
302
302
|
*/
|
|
303
|
-
|
|
303
|
+
const test = (
|
|
304
304
|
node: Node,
|
|
305
305
|
input: Input,
|
|
306
306
|
): Effect.Effect<boolean, unknown, unknown> => {
|
|
307
307
|
if (!node.opaque) {
|
|
308
|
-
return Effect.sync(() =>
|
|
308
|
+
return Effect.sync(() => testSync(node, input))
|
|
309
309
|
}
|
|
310
310
|
switch (node._tag) {
|
|
311
311
|
case "Predicate":
|
|
312
312
|
return Nodes.runOf(node)(input.value, input.principal)
|
|
313
313
|
case "Not":
|
|
314
|
-
return Effect.map(
|
|
314
|
+
return Effect.map(test(node.condition, input), (value) => !value)
|
|
315
315
|
case "And":
|
|
316
316
|
return Effect.map(
|
|
317
317
|
Effect.findFirst(node.conditions, (child) =>
|
|
318
|
-
Effect.map(
|
|
318
|
+
Effect.map(test(child, input), (value) => !value),
|
|
319
319
|
),
|
|
320
320
|
Option.isNone,
|
|
321
321
|
)
|
|
322
322
|
case "Or":
|
|
323
323
|
return Effect.map(
|
|
324
|
-
Effect.findFirst(node.conditions, (child) =>
|
|
324
|
+
Effect.findFirst(node.conditions, (child) => test(child, input)),
|
|
325
325
|
Option.isSome,
|
|
326
326
|
)
|
|
327
327
|
default:
|
|
328
|
-
return Effect.sync(() =>
|
|
328
|
+
return Effect.sync(() => testSync(node, input))
|
|
329
329
|
}
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Tests a condition with no predicate on a value. The test runs on the
|
|
334
|
+
* simple form of the condition with the principal in it, as a residual does.
|
|
335
|
+
* So a test reads only the fields that the residual reads: a read of a field
|
|
336
|
+
* that is not plain data is a defect, and a field that the answer does not
|
|
337
|
+
* need, as in `x AND False`, is not read.
|
|
338
|
+
*/
|
|
339
|
+
export const evaluateSync = (node: Node, input: Input): boolean =>
|
|
340
|
+
testSync(partial(node, input), input)
|
|
341
|
+
|
|
342
|
+
/** Tests a condition on a value, as {@link evaluateSync} does. */
|
|
343
|
+
export const evaluate = (
|
|
344
|
+
node: Node,
|
|
345
|
+
input: Input,
|
|
346
|
+
): Effect.Effect<boolean, unknown, unknown> =>
|
|
347
|
+
Effect.suspend(() => test(partial(node, input), input))
|
|
348
|
+
|
|
332
349
|
// The principal and the path
|
|
333
350
|
|
|
334
351
|
/**
|
package/src/internal/analysis.ts
CHANGED
|
@@ -85,6 +85,8 @@ interface ResidualContext {
|
|
|
85
85
|
}
|
|
86
86
|
/** The resource of the kind, for the fields that a rule may grant. */
|
|
87
87
|
readonly descriptor: Resource.Descriptor
|
|
88
|
+
/** The test of `RuleData.fitsOf` for the catalog, for this request. */
|
|
89
|
+
readonly fits: (pattern: string) => boolean
|
|
88
90
|
/**
|
|
89
91
|
* The condition of each rule, with the principal in it. They are made
|
|
90
92
|
* first, from a flat list of the rules, so the walk does not go down the
|
|
@@ -241,6 +243,7 @@ interface PredicateContext {
|
|
|
241
243
|
readonly kind: string
|
|
242
244
|
readonly field: string | undefined
|
|
243
245
|
readonly descriptor: Resource.Descriptor
|
|
246
|
+
readonly fits: (pattern: string) => boolean
|
|
244
247
|
}
|
|
245
248
|
|
|
246
249
|
/** Tests if a rule that the request may use has a predicate. */
|
|
@@ -345,6 +348,7 @@ const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
|
345
348
|
field: request.field,
|
|
346
349
|
input,
|
|
347
350
|
descriptor,
|
|
351
|
+
fits: RuleData.fitsOf(descriptor.catalog),
|
|
348
352
|
}
|
|
349
353
|
const context: ResidualContext = {
|
|
350
354
|
...target,
|
|
@@ -378,6 +382,7 @@ const scopedResiduals = (policy: Any, request: ScopedRequest): Scoped => {
|
|
|
378
382
|
path: descriptor.path,
|
|
379
383
|
},
|
|
380
384
|
descriptor,
|
|
385
|
+
fits: RuleData.fitsOf(descriptor.catalog),
|
|
381
386
|
trace: undefined,
|
|
382
387
|
},
|
|
383
388
|
[],
|
|
@@ -93,19 +93,29 @@ type AnyTarget = {
|
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* A cache key for a principal. Two keys are equal when they have the same
|
|
96
|
-
* text, so two principals with the same key share one policy.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
96
|
+
* text, so two principals with the same key share one policy. A key from the
|
|
97
|
+
* JSON form also needs equal decoded principals: a Schema may make data when
|
|
98
|
+
* it decodes, as `expired` from a time, so one text may give two principals.
|
|
99
|
+
* The key also keeps the principal, so the cache can load its policy. A plain
|
|
100
|
+
* `Data` value can not do all this, so the key gives its own equality with
|
|
101
|
+
* `Equal.Equal`.
|
|
99
102
|
*/
|
|
100
103
|
class PrincipalKey implements Equal.Equal {
|
|
101
104
|
readonly principal: object
|
|
102
105
|
readonly text: string
|
|
103
|
-
|
|
106
|
+
/** The key is the JSON form, so the decoded principals must be equal too. */
|
|
107
|
+
readonly exact: boolean
|
|
108
|
+
constructor(principal: object, text: string, exact: boolean) {
|
|
104
109
|
this.principal = principal
|
|
105
110
|
this.text = text
|
|
111
|
+
this.exact = exact
|
|
106
112
|
}
|
|
107
113
|
[Equal.symbol](that: unknown): boolean {
|
|
108
|
-
return
|
|
114
|
+
return (
|
|
115
|
+
that instanceof PrincipalKey &&
|
|
116
|
+
that.text === this.text &&
|
|
117
|
+
(!this.exact || Equal.equals(that.principal, this.principal))
|
|
118
|
+
)
|
|
109
119
|
}
|
|
110
120
|
[Hash.symbol](): number {
|
|
111
121
|
return Hash.string(this.text)
|
|
@@ -160,7 +170,7 @@ const jsonKey = (
|
|
|
160
170
|
onFailure: () => undefined,
|
|
161
171
|
onSuccess: (decoded) =>
|
|
162
172
|
Equal.equals(decoded, principal)
|
|
163
|
-
? new PrincipalKey(decoded as object, text)
|
|
173
|
+
? new PrincipalKey(decoded as object, text, true)
|
|
164
174
|
: undefined,
|
|
165
175
|
})
|
|
166
176
|
},
|
|
@@ -226,7 +236,7 @@ export const makeLayer = (
|
|
|
226
236
|
const text = custom(principal)
|
|
227
237
|
return text === undefined
|
|
228
238
|
? undefined
|
|
229
|
-
: new PrincipalKey(canonical?.principal ?? principal, text)
|
|
239
|
+
: new PrincipalKey(canonical?.principal ?? principal, text, false)
|
|
230
240
|
}
|
|
231
241
|
return Layer.effect(
|
|
232
242
|
service,
|