@nmnmcc/ability 2.0.7 → 2.1.1

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 (86) hide show
  1. package/README.md +50 -27
  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 +116 -16
  6. package/dist/Condition.d.ts.map +1 -1
  7. package/dist/Condition.js +27 -18
  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 +95 -12
  18. package/dist/Policy.d.ts.map +1 -1
  19. package/dist/Policy.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/internal/algebra.js +2 -2
  22. package/dist/internal/analysis.js +81 -75
  23. package/dist/internal/analysis.js.map +1 -1
  24. package/dist/internal/authorizer.d.ts.map +1 -1
  25. package/dist/internal/authorizer.js +11 -7
  26. package/dist/internal/authorizer.js.map +1 -1
  27. package/dist/internal/bind.js +86 -64
  28. package/dist/internal/bind.js.map +1 -1
  29. package/dist/internal/catalog.d.ts +33 -10
  30. package/dist/internal/catalog.d.ts.map +1 -1
  31. package/dist/internal/catalog.js +147 -72
  32. package/dist/internal/catalog.js.map +1 -1
  33. package/dist/internal/decide.js +25 -29
  34. package/dist/internal/decide.js.map +1 -1
  35. package/dist/internal/domain.js +19 -5
  36. package/dist/internal/domain.js.map +1 -1
  37. package/dist/internal/json.js +120 -76
  38. package/dist/internal/json.js.map +1 -1
  39. package/dist/internal/limits.d.ts +28 -0
  40. package/dist/internal/limits.d.ts.map +1 -1
  41. package/dist/internal/limits.js +75 -16
  42. package/dist/internal/limits.js.map +1 -1
  43. package/dist/internal/nodes.d.ts.map +1 -1
  44. package/dist/internal/nodes.js +53 -44
  45. package/dist/internal/nodes.js.map +1 -1
  46. package/dist/internal/patterns.js +50 -15
  47. package/dist/internal/patterns.js.map +1 -1
  48. package/dist/internal/policy-json.js +169 -127
  49. package/dist/internal/policy-json.js.map +1 -1
  50. package/dist/internal/policy-nodes.d.ts.map +1 -1
  51. package/dist/internal/policy-nodes.js +16 -13
  52. package/dist/internal/policy-nodes.js.map +1 -1
  53. package/dist/internal/policy.d.ts.map +1 -1
  54. package/dist/internal/policy.js +38 -22
  55. package/dist/internal/policy.js.map +1 -1
  56. package/dist/internal/resource.js +1 -1
  57. package/dist/internal/rule.d.ts.map +1 -1
  58. package/dist/internal/rule.js +6 -5
  59. package/dist/internal/rule.js.map +1 -1
  60. package/dist/internal/search.d.ts.map +1 -1
  61. package/dist/internal/search.js +106 -92
  62. package/dist/internal/search.js.map +1 -1
  63. package/dist/internal/trampoline.js +19 -5
  64. package/dist/internal/trampoline.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/Authorizer.ts +30 -20
  67. package/src/Condition.ts +308 -83
  68. package/src/Domain.ts +9 -0
  69. package/src/Limits.ts +69 -29
  70. package/src/Policy.ts +131 -15
  71. package/src/internal/analysis.ts +154 -124
  72. package/src/internal/authorizer.ts +29 -14
  73. package/src/internal/bind.ts +193 -136
  74. package/src/internal/catalog.ts +295 -112
  75. package/src/internal/decide.ts +58 -49
  76. package/src/internal/domain.ts +39 -3
  77. package/src/internal/json.ts +212 -117
  78. package/src/internal/limits.ts +101 -31
  79. package/src/internal/nodes.ts +65 -47
  80. package/src/internal/patterns.ts +72 -26
  81. package/src/internal/policy-json.ts +266 -197
  82. package/src/internal/policy-nodes.ts +31 -23
  83. package/src/internal/policy.ts +63 -23
  84. package/src/internal/rule.ts +16 -3
  85. package/src/internal/search.ts +186 -136
  86. 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,64 @@ 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 the places of the match: each pair
205
+ * comes one time, a level after a level, so the search is short, and it
206
+ * keeps a queue, so a deep Schema does not fill the stack. The set of the
207
+ * pairs is native, with keys that are numbers, and the set and the queue
208
+ * belong to this call.
209
+ */
193
210
  export const fitsCatalog = (
194
211
  pattern: Pattern,
195
212
  catalog: Catalog.Catalog,
213
+ parts: number = Limits.fieldPathParts,
196
214
  ): 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)
212
- )
215
+ interface Visit {
216
+ readonly branch: Catalog.Branch
217
+ readonly states: States
218
+ readonly depth: number
219
+ }
220
+ const seen = new Set<string>()
221
+ const next = (visit: Visit): ReadonlyArray<Visit> => {
222
+ if (visit.states.length === 0 || visit.depth === parts) {
223
+ return []
224
+ }
225
+ const steps: ReadonlyArray<readonly [Catalog.Segment, number]> = [
226
+ ...visit.branch.children,
227
+ ...(visit.branch.key === undefined || visit.branch.keyBranch === undefined
228
+ ? []
229
+ : [[visit.branch.key, visit.branch.keyBranch] as const]),
230
+ ]
231
+ return Arr.flatMap(steps, ([segment, id]) => {
232
+ const states = step(pattern, visit.states, segment)
233
+ const key = `${String(id)}|${states.join(",")}`
234
+ if (states.length === 0 || seen.has(key)) {
235
+ return []
236
+ }
237
+ seen.add(key)
238
+ return [{ branch: catalog.branches[id]!, states, depth: visit.depth + 1 }]
239
+ })
213
240
  }
214
- return below(catalog.root, start(pattern))
241
+ const queue: Array<Visit> = [
242
+ { branch: catalog.root, states: start(pattern), depth: 0 },
243
+ ]
244
+ // The walk stops at the first field that the pattern matches.
245
+ const found = (visit: Visit) =>
246
+ visit.depth > 0 && isDone(pattern, visit.states)
247
+ return Arr.some(
248
+ Arr.unfold(0, (index) => {
249
+ const visit = queue[index]
250
+ if (visit === undefined) {
251
+ return Option.none()
252
+ }
253
+ if (found(visit)) {
254
+ return Option.some([true, queue.length] as const)
255
+ }
256
+ queue.push(...next(visit))
257
+ return Option.some([false, index + 1] as const)
258
+ }),
259
+ (match) => match,
260
+ )
215
261
  }