@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.
Files changed (87) hide show
  1. package/README.md +52 -29
  2. package/dist/Authorizer.d.ts +21 -12
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js.map +1 -1
  5. package/dist/Condition.d.ts +121 -19
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +32 -21
  8. package/dist/Condition.js.map +1 -1
  9. package/dist/Domain.d.ts +9 -0
  10. package/dist/Domain.d.ts.map +1 -1
  11. package/dist/Domain.js +5 -0
  12. package/dist/Domain.js.map +1 -1
  13. package/dist/Limits.d.ts +44 -13
  14. package/dist/Limits.d.ts.map +1 -1
  15. package/dist/Limits.js +52 -22
  16. package/dist/Limits.js.map +1 -1
  17. package/dist/Policy.d.ts +102 -18
  18. package/dist/Policy.d.ts.map +1 -1
  19. package/dist/Policy.js +7 -6
  20. package/dist/Policy.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/internal/algebra.js +2 -2
  23. package/dist/internal/analysis.js +81 -75
  24. package/dist/internal/analysis.js.map +1 -1
  25. package/dist/internal/authorizer.d.ts.map +1 -1
  26. package/dist/internal/authorizer.js +11 -7
  27. package/dist/internal/authorizer.js.map +1 -1
  28. package/dist/internal/bind.js +86 -64
  29. package/dist/internal/bind.js.map +1 -1
  30. package/dist/internal/catalog.d.ts +33 -10
  31. package/dist/internal/catalog.d.ts.map +1 -1
  32. package/dist/internal/catalog.js +170 -77
  33. package/dist/internal/catalog.js.map +1 -1
  34. package/dist/internal/decide.js +27 -31
  35. package/dist/internal/decide.js.map +1 -1
  36. package/dist/internal/domain.js +19 -5
  37. package/dist/internal/domain.js.map +1 -1
  38. package/dist/internal/json.js +120 -76
  39. package/dist/internal/json.js.map +1 -1
  40. package/dist/internal/limits.d.ts +28 -0
  41. package/dist/internal/limits.d.ts.map +1 -1
  42. package/dist/internal/limits.js +75 -16
  43. package/dist/internal/limits.js.map +1 -1
  44. package/dist/internal/nodes.d.ts.map +1 -1
  45. package/dist/internal/nodes.js +53 -44
  46. package/dist/internal/nodes.js.map +1 -1
  47. package/dist/internal/patterns.js +50 -15
  48. package/dist/internal/patterns.js.map +1 -1
  49. package/dist/internal/policy-json.js +169 -127
  50. package/dist/internal/policy-json.js.map +1 -1
  51. package/dist/internal/policy-nodes.d.ts.map +1 -1
  52. package/dist/internal/policy-nodes.js +16 -13
  53. package/dist/internal/policy-nodes.js.map +1 -1
  54. package/dist/internal/policy.d.ts.map +1 -1
  55. package/dist/internal/policy.js +38 -22
  56. package/dist/internal/policy.js.map +1 -1
  57. package/dist/internal/resource.js +1 -1
  58. package/dist/internal/rule.d.ts.map +1 -1
  59. package/dist/internal/rule.js +6 -5
  60. package/dist/internal/rule.js.map +1 -1
  61. package/dist/internal/search.d.ts.map +1 -1
  62. package/dist/internal/search.js +106 -92
  63. package/dist/internal/search.js.map +1 -1
  64. package/dist/internal/trampoline.js +19 -5
  65. package/dist/internal/trampoline.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/Authorizer.ts +30 -20
  68. package/src/Condition.ts +317 -86
  69. package/src/Domain.ts +9 -0
  70. package/src/Limits.ts +69 -29
  71. package/src/Policy.ts +143 -21
  72. package/src/internal/analysis.ts +154 -124
  73. package/src/internal/authorizer.ts +29 -14
  74. package/src/internal/bind.ts +193 -136
  75. package/src/internal/catalog.ts +320 -118
  76. package/src/internal/decide.ts +61 -50
  77. package/src/internal/domain.ts +39 -3
  78. package/src/internal/json.ts +212 -117
  79. package/src/internal/limits.ts +101 -31
  80. package/src/internal/nodes.ts +65 -47
  81. package/src/internal/patterns.ts +79 -25
  82. package/src/internal/policy-json.ts +266 -197
  83. package/src/internal/policy-nodes.ts +31 -23
  84. package/src/internal/policy.ts +63 -23
  85. package/src/internal/rule.ts +16 -3
  86. package/src/internal/search.ts +186 -136
  87. package/src/internal/trampoline.ts +51 -4
@@ -23,6 +23,7 @@ import type * as Effect from "effect/Effect"
23
23
  import { hasProperty } from "effect/Predicate"
24
24
  import * as Schema from "effect/Schema"
25
25
  import * as Limits from "./limits.ts"
26
+ import * as Trampoline from "./trampoline.ts"
26
27
 
27
28
  /** A value that a condition can compare. */
28
29
  export type Scalar = string | number | boolean | null
@@ -83,14 +84,17 @@ const checkPath = (path: unknown): string => {
83
84
  if (typeof path !== "string") {
84
85
  throw new Error("A field path must be a string")
85
86
  }
86
- const parts = path.split(".", Limits.fieldPathParts + 1).length
87
- if (parts > Limits.fieldPathParts) {
87
+ // A node does not know its domain, so it checks the ceiling. The binding
88
+ // to a domain checks the limit of the domain.
89
+ const most = Limits.ceilings.fieldPathParts
90
+ const parts = path.split(".", most + 1).length
91
+ if (parts > most) {
88
92
  throw Limits.exceeded(
89
93
  "A field path",
90
94
  "fieldPathParts",
91
95
  "parts",
92
96
  parts,
93
- Limits.fieldPathParts,
97
+ most,
94
98
  )
95
99
  }
96
100
  return path
@@ -140,13 +144,15 @@ const valuesFacts = (values: ReadonlyArray<Operand>): Facts =>
140
144
  checkWeight({ ...leaf, weight: 1 + values.length })
141
145
 
142
146
  const checkWeight = (facts: Facts): Facts => {
143
- if (facts.weight > Limits.conditionNodes) {
147
+ // A node does not know its domain, so it checks the ceiling. The binding
148
+ // to a domain, and a decoder, check their limits.
149
+ if (facts.weight > Limits.ceilings.conditionNodes) {
144
150
  throw Limits.exceeded(
145
151
  "A condition",
146
152
  "conditionNodes",
147
153
  "nodes and values",
148
154
  facts.weight,
149
- Limits.conditionNodes,
155
+ Limits.ceilings.conditionNodes,
150
156
  )
151
157
  }
152
158
  return facts
@@ -159,15 +165,6 @@ const factsOf = (children: ReadonlyArray<Node>): Facts => {
159
165
  checkWeight({ ...leaf, weight })
160
166
  const depth =
161
167
  1 + Arr.reduce(children, 0, (top, child) => Math.max(top, child.depth))
162
- if (depth > Limits.conditionDepth) {
163
- throw Limits.exceeded(
164
- "A condition",
165
- "conditionDepth",
166
- "levels",
167
- depth,
168
- Limits.conditionDepth,
169
- )
170
- }
171
168
  return {
172
169
  size,
173
170
  weight,
@@ -382,40 +379,61 @@ const operandJson = (operand: Operand): unknown =>
382
379
  }
383
380
  : { _tag: "Principal", path: operand.path }
384
381
 
385
- /** Gives the JSON form of a condition with no predicate. */
386
- export const toJson = (node: Node): unknown => {
387
- switch (node._tag) {
388
- case "True":
389
- case "False":
390
- return { _tag: node._tag }
391
- case "Compare":
392
- return {
393
- _tag: "Compare",
394
- op: node.op,
395
- field: node.field,
396
- value: operandJson(node.value),
397
- }
398
- case "In":
399
- return {
400
- _tag: "In",
401
- field: node.field,
402
- values: Arr.map(node.values, operandJson),
382
+ /**
383
+ * Gives the JSON form of a condition with no predicate. The walk takes the
384
+ * steps of a trampoline, so a deep condition does not fill the stack.
385
+ */
386
+ export const toJson = (root: Node): unknown =>
387
+ Trampoline.run(root.depth, (R) => {
388
+ const go = (node: Node): Trampoline.Step<unknown> => {
389
+ switch (node._tag) {
390
+ case "True":
391
+ case "False":
392
+ return R.succeed({ _tag: node._tag })
393
+ case "Compare":
394
+ return R.succeed({
395
+ _tag: "Compare",
396
+ op: node.op,
397
+ field: node.field,
398
+ value: operandJson(node.value),
399
+ })
400
+ case "In":
401
+ return R.succeed({
402
+ _tag: "In",
403
+ field: node.field,
404
+ values: Arr.map(node.values, operandJson),
405
+ })
406
+ case "Exists":
407
+ return R.succeed({ _tag: "Exists", field: node.field })
408
+ case "Under":
409
+ return R.succeed({
410
+ _tag: "Under",
411
+ prefix: Arr.map(node.prefix, operandJson),
412
+ })
413
+ case "And":
414
+ case "Or": {
415
+ const tag = node._tag
416
+ return R.map(
417
+ R.forEach(node.conditions, (child) => R.suspend(() => go(child))),
418
+ (conditions) => ({ _tag: tag, conditions }),
419
+ )
420
+ }
421
+ case "Not":
422
+ return R.map(
423
+ R.suspend(() => go(node.condition)),
424
+ (condition) => ({
425
+ _tag: "Not",
426
+ condition,
427
+ }),
428
+ )
429
+ case "Predicate":
430
+ throw new Error(
431
+ `The predicate "${node.name}" can not become JSON. A condition with a predicate has no JSON form.`,
432
+ )
403
433
  }
404
- case "Exists":
405
- return { _tag: "Exists", field: node.field }
406
- case "Under":
407
- return { _tag: "Under", prefix: Arr.map(node.prefix, operandJson) }
408
- case "And":
409
- case "Or":
410
- return { _tag: node._tag, conditions: Arr.map(node.conditions, toJson) }
411
- case "Not":
412
- return { _tag: "Not", condition: toJson(node.condition) }
413
- case "Predicate":
414
- throw new Error(
415
- `The predicate "${node.name}" can not become JSON. A condition with a predicate has no JSON form.`,
416
- )
417
- }
418
- }
434
+ }
435
+ return go(root)
436
+ })
419
437
 
420
438
  /** Tests if a value is a node. Only a constructor of a node makes one. */
421
439
  export const isNode = (u: unknown): u is Node => u instanceof Base
@@ -14,6 +14,7 @@
14
14
  * number of names times the number of parts.
15
15
  */
16
16
  import * as Arr from "effect/Array"
17
+ import * as Option from "effect/Option"
17
18
  import * as Result from "effect/Result"
18
19
  import * as Catalog from "./catalog.ts"
19
20
  import * as Limits from "./limits.ts"
@@ -39,13 +40,17 @@ const partOf = (text: string): Part =>
39
40
 
40
41
  /**
41
42
  * Reads a pattern. It fails with the problem when the pattern has `**`
42
- * inside a part, or has more parts than `patternParts`.
43
+ * inside a part, or has more parts than `parts`, the `patternParts` of a
44
+ * domain.
43
45
  */
44
- export const parse = (text: string): Result.Result<Pattern, string> => {
45
- const texts = text.split(".", Limits.patternParts + 1)
46
- if (texts.length > Limits.patternParts) {
46
+ export const parse = (
47
+ text: string,
48
+ parts: number = Limits.patternParts,
49
+ ): Result.Result<Pattern, string> => {
50
+ const texts = text.split(".", parts + 1)
51
+ if (texts.length > parts) {
47
52
  return Result.fail(
48
- `The field pattern "${text}" has more than ${String(Limits.patternParts)} parts. The most is ${String(Limits.patternParts)}.`,
53
+ `The field pattern "${text}" has more than ${String(parts)} parts. The most is ${String(parts)}.`,
49
54
  )
50
55
  }
51
56
  if (Arr.some(texts, (part) => part !== "**" && part.includes("**"))) {
@@ -63,11 +68,15 @@ export const parse = (text: string): Result.Result<Pattern, string> => {
63
68
  }
64
69
 
65
70
  /**
66
- * Reads a pattern of a bound rule. The binding of the rule took the pattern,
67
- * so a problem here is a defect.
71
+ * Reads a pattern of a bound rule. The binding of the rule took the pattern
72
+ * with the limits of its domain, so this reads it up to the ceiling, and a
73
+ * problem here is a defect.
68
74
  */
69
75
  export const parseBound = (text: string): Pattern =>
70
- Result.getOrThrowWith(parse(text), (problem) => new Error(problem))
76
+ Result.getOrThrowWith(
77
+ parse(text, Limits.ceilings.patternParts),
78
+ (problem) => new Error(problem),
79
+ )
71
80
 
72
81
  /** Tests if a name matches the pieces of a part with stars. */
73
82
  const globMatches = (pieces: ReadonlyArray<string>, name: string): boolean => {
@@ -189,27 +198,72 @@ export const matchesBelow = (
189
198
  (state) => state < pattern.length,
190
199
  )
191
200
 
192
- /** Tests if a pattern matches some field of a catalog. */
201
+ /**
202
+ * Tests if a pattern matches some field of a catalog, with at most `parts`
203
+ * parts. A recursive Schema has no end of fields, so the search goes over the
204
+ * pairs of a place of the catalog and one place of the match. There are at
205
+ * most the places of the catalog times the parts of the pattern of them, so
206
+ * the work is the steps of the catalog times the parts of the pattern, as the
207
+ * binding counts it. Each pair comes one time, a level after a level, so the
208
+ * search finds the shortest field first, and it keeps a queue, so a deep
209
+ * Schema does not fill the stack. The set of the pairs is native, and the set
210
+ * and the queue belong to this call.
211
+ */
193
212
  export const fitsCatalog = (
194
213
  pattern: Pattern,
195
214
  catalog: Catalog.Catalog,
215
+ parts: number = Limits.fieldPathParts,
196
216
  ): boolean => {
197
- const below = (branch: Catalog.Branch, states: States): boolean =>
198
- states.length > 0 &&
199
- (Arr.some(branch.children, ([name, child]) => visit(child, name, states)) ||
200
- (branch.key !== undefined &&
201
- branch.keyBranch !== undefined &&
202
- visit(branch.keyBranch, branch.key, states)))
203
- const visit = (
204
- branch: Catalog.Branch,
205
- segment: Catalog.Segment,
206
- before: States,
207
- ): boolean => {
208
- const states = step(pattern, before, segment)
209
- return (
210
- (branch.field !== undefined && isDone(pattern, states)) ||
211
- below(branch, states)
217
+ interface Visit {
218
+ readonly branch: Catalog.Branch
219
+ readonly state: number
220
+ readonly depth: number
221
+ }
222
+ const seen = new Set<string>()
223
+ const visits = (
224
+ id: number,
225
+ states: States,
226
+ depth: number,
227
+ ): ReadonlyArray<Visit> =>
228
+ Arr.flatMap(states, (state) => {
229
+ const key = `${String(id)}|${String(state)}`
230
+ if (seen.has(key)) {
231
+ return []
232
+ }
233
+ seen.add(key)
234
+ return [{ branch: catalog.branches[id]!, state, depth }]
235
+ })
236
+ const next = (visit: Visit): ReadonlyArray<Visit> => {
237
+ if (visit.depth === parts) {
238
+ return []
239
+ }
240
+ const branch = visit.branch
241
+ const steps: ReadonlyArray<readonly [Catalog.Segment, number]> =
242
+ branch.key === undefined || branch.keyBranch === undefined
243
+ ? branch.children
244
+ : [...branch.children, [branch.key, branch.keyBranch] as const]
245
+ return Arr.flatMap(steps, ([segment, id]) =>
246
+ visits(id, step(pattern, [visit.state], segment), visit.depth + 1),
212
247
  )
213
248
  }
214
- return below(catalog.root, start(pattern))
249
+ const queue: Array<Visit> = [...visits(catalog.root.id, start(pattern), 0)]
250
+ // The walk stops at the first field that the pattern matches. The visits
251
+ // go into the queue one at a time: a spread into a call would pass one
252
+ // argument for each, past the most arguments of a call.
253
+ return Arr.some(
254
+ Arr.unfold(0, (index) => {
255
+ const visit = queue[index]
256
+ if (visit === undefined) {
257
+ return Option.none()
258
+ }
259
+ if (visit.depth > 0 && visit.state === pattern.length) {
260
+ return Option.some([true, queue.length] as const)
261
+ }
262
+ Arr.forEach(next(visit), (found) => {
263
+ queue.push(found)
264
+ })
265
+ return Option.some([false, index + 1] as const)
266
+ }),
267
+ (match) => match,
268
+ )
215
269
  }