@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.
Files changed (48) hide show
  1. package/README.md +25 -17
  2. package/dist/Condition.d.ts +26 -16
  3. package/dist/Condition.d.ts.map +1 -1
  4. package/dist/Condition.js +2 -1
  5. package/dist/Condition.js.map +1 -1
  6. package/dist/Limits.d.ts +3 -1
  7. package/dist/Limits.d.ts.map +1 -1
  8. package/dist/Limits.js +3 -1
  9. package/dist/Limits.js.map +1 -1
  10. package/dist/Policy.d.ts +6 -8
  11. package/dist/Policy.d.ts.map +1 -1
  12. package/dist/Policy.js +4 -2
  13. package/dist/Policy.js.map +1 -1
  14. package/dist/internal/algebra.js +23 -13
  15. package/dist/internal/algebra.js.map +1 -1
  16. package/dist/internal/analysis.js +4 -2
  17. package/dist/internal/analysis.js.map +1 -1
  18. package/dist/internal/authorizer.js +13 -7
  19. package/dist/internal/authorizer.js.map +1 -1
  20. package/dist/internal/bind.js +53 -27
  21. package/dist/internal/bind.js.map +1 -1
  22. package/dist/internal/catalog.d.ts +9 -0
  23. package/dist/internal/catalog.d.ts.map +1 -1
  24. package/dist/internal/catalog.js +37 -22
  25. package/dist/internal/catalog.js.map +1 -1
  26. package/dist/internal/decide.js +2 -1
  27. package/dist/internal/decide.js.map +1 -1
  28. package/dist/internal/place.js +19 -0
  29. package/dist/internal/place.js.map +1 -0
  30. package/dist/internal/policy-json.js +39 -39
  31. package/dist/internal/policy-json.js.map +1 -1
  32. package/dist/internal/rule.d.ts +20 -22
  33. package/dist/internal/rule.d.ts.map +1 -1
  34. package/dist/internal/rule.js +32 -4
  35. package/dist/internal/rule.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/Condition.ts +64 -50
  38. package/src/Limits.ts +3 -1
  39. package/src/Policy.ts +5 -9
  40. package/src/internal/algebra.ts +30 -13
  41. package/src/internal/analysis.ts +5 -0
  42. package/src/internal/authorizer.ts +17 -7
  43. package/src/internal/bind.ts +85 -23
  44. package/src/internal/catalog.ts +78 -30
  45. package/src/internal/decide.ts +3 -0
  46. package/src/internal/place.ts +32 -0
  47. package/src/internal/policy-json.ts +49 -29
  48. package/src/internal/rule.ts +55 -11
@@ -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
- /** The class of every rule value. */
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 instanceof Rule;
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, (pattern) => fitsCatalog(parseBound(pattern), descriptor.catalog)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmnmcc/ability",
3
- "version": "2.0.1",
3
+ "version": "2.0.4",
4
4
  "description": "Typed, composable authorization policies for Effect programs.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
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 may be
228
- * anything, as `unknown`, may have any error.
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> = unknown extends 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 may be
242
- * anything, as `unknown`, may need any service.
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> = unknown extends 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> = unknown extends 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
- [K in keyof T & string]-?: PathsOf<
370
- K,
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
- | ([V] extends [never] ? K : never)
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
- | ObjectPaths<
399
- K,
400
- Exclude<V, Leaf | Whole | Function | ReadonlyArray<unknown>>,
401
- Depth,
402
- Arrays,
403
- Parents
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
- : never
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. The type goes down four levels, so type
431
- * checking stays fast. For a union of types, only the common fields are
432
- * paths.
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
- Path extends string,
503
- > = Path extends `${infer Head}.${infer Rest}`
504
- ? Head extends keyof T
505
- ? undefined extends T[Head]
506
- ? PathValue<NonNullable<T[Head]>, Rest> | null
507
- : null extends T[Head]
508
- ? PathValue<NonNullable<T[Head]>, Rest> | null
509
- : PathValue<T[Head], Rest>
510
- : never
511
- : Path extends keyof T
512
- ? Normalize<T[Path]>
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 interface Rule<D = any, I = unknown, X = never>
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 `True` when the policy allows every value of the
782
- * kind, and `False` when it allows none.
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
@@ -259,8 +259,8 @@ const testUnder = (
259
259
  })
260
260
  }
261
261
 
262
- /** Tests a condition with no predicate on a value. */
263
- export const evaluateSync = (node: Node, input: Input): boolean => {
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) => evaluateSync(child, input))
286
+ return Arr.every(node.conditions, (child) => testSync(child, input))
287
287
  case "Or":
288
- return Arr.some(node.conditions, (child) => evaluateSync(child, input))
288
+ return Arr.some(node.conditions, (child) => testSync(child, input))
289
289
  case "Not":
290
- return !evaluateSync(node.condition, input)
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 order,
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
- export const evaluate = (
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(() => evaluateSync(node, input))
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(evaluate(node.condition, input), (value) => !value)
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(evaluate(child, input), (value) => !value),
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) => evaluate(child, input)),
324
+ Effect.findFirst(node.conditions, (child) => test(child, input)),
325
325
  Option.isSome,
326
326
  )
327
327
  default:
328
- return Effect.sync(() => evaluateSync(node, input))
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
  /**
@@ -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. The key also
97
- * keeps the principal, so the cache can load its policy. A plain `Data` value
98
- * can not do both, so the key gives its own equality with `Equal.Equal`.
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
- constructor(principal: object, text: string) {
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 that instanceof PrincipalKey && that.text === this.text
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,