@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
@@ -7,8 +7,11 @@
7
7
  */
8
8
  import * as Arr from "effect/Array"
9
9
  import * as Data from "effect/Data"
10
+ import * as MutableHashMap from "effect/MutableHashMap"
11
+ import * as Option from "effect/Option"
10
12
  import * as Condition from "../Condition.ts"
11
13
  import type * as Domain from "../Domain.ts"
14
+ import type * as Policy from "../Policy.ts"
12
15
  import type * as Resource from "../Resource.ts"
13
16
  import * as Catalog from "./catalog.ts"
14
17
  import * as Nodes from "./nodes.ts"
@@ -33,17 +36,42 @@ export interface Shape {
33
36
  readonly domain?: Domain.Any
34
37
  }
35
38
 
36
- /** The class of every rule value. */
37
- export class Rule extends Data.Class<Shape> {
38
- *[Symbol.iterator](): Generator<this, this, unknown> {
39
+ /**
40
+ * The class of every rule value. The types are those of `Policy.Rule`: `D` is
41
+ * the domain, `I` is what the tests of the condition read, and `X` is the
42
+ * mark of the condition. With no types, as in the nodes of a policy, a rule
43
+ * fits no domain, its condition fits no other rule, and it may have a
44
+ * predicate.
45
+ */
46
+ export class Rule<D = never, I = never, X = unknown> extends Data.Class<
47
+ Policy.RuleShape<I, X>
48
+ > {
49
+ /** Only in the types. */
50
+ declare readonly "~Opaque": () => X
51
+ /** Only in the types. */
52
+ declare readonly "~Domain": (domain: D) => D
53
+ // A private field makes the type of a rule its own, as for a policy: a
54
+ // spread of a rule, or an object made by hand, is not a rule. `Equal` does
55
+ // not read it. It comes last, so its `;` stays: the generator method below
56
+ // needs one after a field. Node.js takes the `declare` fields out of the
57
+ // code, and a `;` after one of them would go with it.
58
+ readonly #rule = true;
59
+
60
+ *[Symbol.iterator](): Generator<Rule<D, I, X>, Rule<D, I, X>, unknown> {
39
61
  yield this
40
62
  return this
41
63
  }
64
+
65
+ /** Tests if a value is a rule, by its private field. */
66
+ static is(u: unknown): u is Rule {
67
+ return typeof u === "object" && u !== null && #rule in u
68
+ }
42
69
  }
43
70
 
44
71
  /** Makes a rule from its data. It does not check the data against a domain. */
45
72
  export const make = (data: Shape): Rule =>
46
73
  // A missing option is left out, so two rules with the same data are equal.
74
+ // The data has no types here, as the code below works with no types.
47
75
  new Rule({
48
76
  _tag: data._tag,
49
77
  actions: [...data.actions],
@@ -54,7 +82,7 @@ export const make = (data: Shape): Rule =>
54
82
  ...(data.id === undefined ? {} : { id: data.id }),
55
83
  ...(data.meta === undefined ? {} : { meta: data.meta }),
56
84
  ...(data.domain === undefined ? {} : { domain: data.domain }),
57
- })
85
+ } as never)
58
86
 
59
87
  /**
60
88
  * Makes a rule from its data, as `Policy.rule` does. A condition with a
@@ -71,7 +99,7 @@ export const makePlain = (data: Shape): Rule => {
71
99
  }
72
100
 
73
101
  /** Tests if a value is a rule. */
74
- export const isRule = (u: unknown): u is Rule => u instanceof Rule
102
+ export const isRule = (u: unknown): u is Rule => Rule.is(u)
75
103
 
76
104
  const toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>
77
105
  Array.isArray(value) ? value : [value as A]
@@ -132,6 +160,25 @@ export const matchesField = (pattern: string, field: string): boolean =>
132
160
  export const matchesInside = (pattern: string, field: string): boolean =>
133
161
  Patterns.matchesBelow(Patterns.parseBound(pattern), field.split("."))
134
162
 
163
+ /**
164
+ * Makes the test "a field pattern matches some field of the catalog" for one
165
+ * walk, such as one decision. A policy may name one pattern very many times,
166
+ * so the test keeps each answer by the text of the pattern, and the memo goes
167
+ * away when the walk ends. The binding puts a limit on the work of the
168
+ * patterns of a policy on each kind, so one walk does at most that work.
169
+ */
170
+ export const fitsOf = (
171
+ catalog: Catalog.Catalog,
172
+ ): ((pattern: string) => boolean) => {
173
+ const memo = MutableHashMap.empty<string, boolean>()
174
+ return (pattern) =>
175
+ Option.getOrElse(MutableHashMap.get(memo, pattern), () => {
176
+ const found = Patterns.fitsCatalog(Patterns.parseBound(pattern), catalog)
177
+ MutableHashMap.set(memo, pattern, found)
178
+ return found
179
+ })
180
+ }
181
+
135
182
  /**
136
183
  * Tests the parts of a rule that do not need the resource value: the action,
137
184
  * the kind and the field.
@@ -152,6 +199,8 @@ export const matchesTarget = (
152
199
  readonly kind: string
153
200
  readonly field?: string | undefined
154
201
  readonly descriptor?: Resource.Descriptor | undefined
202
+ /** The test of {@link fitsOf} for the catalog of the descriptor. */
203
+ readonly fits?: ((pattern: string) => boolean) | undefined
155
204
  },
156
205
  ): boolean => {
157
206
  if (!rule.actions.includes("*") && !rule.actions.includes(target.action)) {
@@ -173,12 +222,7 @@ export const matchesTarget = (
173
222
  return (
174
223
  rule._tag === "Allow" &&
175
224
  (descriptor === undefined ||
176
- Arr.some(rule.fields, (pattern) =>
177
- Patterns.fitsCatalog(
178
- Patterns.parseBound(pattern),
179
- descriptor.catalog,
180
- ),
181
- ))
225
+ Arr.some(rule.fields, target.fits ?? fitsOf(descriptor.catalog)))
182
226
  )
183
227
  }
184
228
  const field = target.field