@nmnmcc/ability 2.0.6 → 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 (89) hide show
  1. package/README.md +56 -32
  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 +118 -18
  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 +101 -74
  22. package/dist/internal/algebra.js.map +1 -1
  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 +16 -11
  27. package/dist/internal/authorizer.js.map +1 -1
  28. package/dist/internal/bind.js +119 -80
  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 +153 -78
  33. package/dist/internal/catalog.js.map +1 -1
  34. package/dist/internal/decide.js +25 -29
  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 +7 -0
  45. package/dist/internal/nodes.d.ts.map +1 -1
  46. package/dist/internal/nodes.js +86 -47
  47. package/dist/internal/nodes.js.map +1 -1
  48. package/dist/internal/patterns.js +50 -15
  49. package/dist/internal/patterns.js.map +1 -1
  50. package/dist/internal/policy-json.js +169 -127
  51. package/dist/internal/policy-json.js.map +1 -1
  52. package/dist/internal/policy-nodes.d.ts.map +1 -1
  53. package/dist/internal/policy-nodes.js +17 -14
  54. package/dist/internal/policy-nodes.js.map +1 -1
  55. package/dist/internal/policy.d.ts.map +1 -1
  56. package/dist/internal/policy.js +38 -22
  57. package/dist/internal/policy.js.map +1 -1
  58. package/dist/internal/resource.js +1 -1
  59. package/dist/internal/rule.d.ts.map +1 -1
  60. package/dist/internal/rule.js +6 -5
  61. package/dist/internal/rule.js.map +1 -1
  62. package/dist/internal/search.d.ts.map +1 -1
  63. package/dist/internal/search.js +106 -92
  64. package/dist/internal/search.js.map +1 -1
  65. package/dist/internal/trampoline.js +89 -0
  66. package/dist/internal/trampoline.js.map +1 -0
  67. package/package.json +1 -1
  68. package/src/Authorizer.ts +30 -20
  69. package/src/Condition.ts +308 -83
  70. package/src/Domain.ts +9 -0
  71. package/src/Limits.ts +69 -29
  72. package/src/Policy.ts +131 -15
  73. package/src/internal/algebra.ts +218 -135
  74. package/src/internal/analysis.ts +154 -122
  75. package/src/internal/authorizer.ts +34 -16
  76. package/src/internal/bind.ts +279 -148
  77. package/src/internal/catalog.ts +307 -133
  78. package/src/internal/decide.ts +58 -49
  79. package/src/internal/domain.ts +39 -3
  80. package/src/internal/json.ts +212 -117
  81. package/src/internal/limits.ts +101 -31
  82. package/src/internal/nodes.ts +101 -59
  83. package/src/internal/patterns.ts +72 -26
  84. package/src/internal/policy-json.ts +266 -197
  85. package/src/internal/policy-nodes.ts +33 -24
  86. package/src/internal/policy.ts +63 -23
  87. package/src/internal/rule.ts +16 -3
  88. package/src/internal/search.ts +186 -136
  89. package/src/internal/trampoline.ts +194 -0
@@ -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
@@ -124,37 +128,46 @@ const checkOperands = (values: unknown): ReadonlyArray<Operand> => {
124
128
 
125
129
  interface Facts {
126
130
  readonly size: number
131
+ readonly weight: number
127
132
  readonly depth: number
128
133
  readonly opaque: boolean
129
134
  }
130
135
 
131
- const leaf: Facts = { size: 1, depth: 1, opaque: false }
136
+ const leaf: Facts = { size: 1, weight: 1, depth: 1, opaque: false }
132
137
 
133
- /** The facts of a node with children. It stops a node past the limits. */
134
- const factsOf = (children: ReadonlyArray<Node>): Facts => {
135
- const size = Arr.reduce(children, 1, (total, child) => total + child.size)
136
- if (size > Limits.conditionNodes) {
138
+ /**
139
+ * The facts of a test with values, as `In` and `Under`. Each value counts in
140
+ * the weight, since a walk that reads the test reads its values too. It stops
141
+ * a test past the limits.
142
+ */
143
+ const valuesFacts = (values: ReadonlyArray<Operand>): Facts =>
144
+ checkWeight({ ...leaf, weight: 1 + values.length })
145
+
146
+ const checkWeight = (facts: Facts): Facts => {
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) {
137
150
  throw Limits.exceeded(
138
151
  "A condition",
139
152
  "conditionNodes",
140
- "nodes",
141
- size,
142
- Limits.conditionNodes,
153
+ "nodes and values",
154
+ facts.weight,
155
+ Limits.ceilings.conditionNodes,
143
156
  )
144
157
  }
158
+ return facts
159
+ }
160
+
161
+ /** The facts of a node with children. It stops a node past the limits. */
162
+ const factsOf = (children: ReadonlyArray<Node>): Facts => {
163
+ const size = Arr.reduce(children, 1, (total, child) => total + child.size)
164
+ const weight = Arr.reduce(children, 1, (total, child) => total + child.weight)
165
+ checkWeight({ ...leaf, weight })
145
166
  const depth =
146
167
  1 + Arr.reduce(children, 0, (top, child) => Math.max(top, child.depth))
147
- if (depth > Limits.conditionDepth) {
148
- throw Limits.exceeded(
149
- "A condition",
150
- "conditionDepth",
151
- "levels",
152
- depth,
153
- Limits.conditionDepth,
154
- )
155
- }
156
168
  return {
157
169
  size,
170
+ weight,
158
171
  depth,
159
172
  opaque: Arr.some(children, (child) => child.opaque),
160
173
  }
@@ -183,6 +196,15 @@ abstract class Base extends Data.Class {
183
196
  return this.#facts.size
184
197
  }
185
198
 
199
+ /**
200
+ * The number of nodes and of the values of their `In` and `Under` tests,
201
+ * with a shared child counted for each place. A walk that reads the tests
202
+ * does this much work, so the limits of the nodes count it.
203
+ */
204
+ get weight(): number {
205
+ return this.#facts.weight
206
+ }
207
+
186
208
  /** The number of levels. */
187
209
  get depth(): number {
188
210
  return this.#facts.depth
@@ -244,10 +266,8 @@ export class In extends Base {
244
266
  readonly field: string
245
267
  readonly values: ReadonlyArray<Operand>
246
268
  }) {
247
- super(
248
- { field: checkPath(props.field), values: checkOperands(props.values) },
249
- leaf,
250
- )
269
+ const values = checkOperands(props.values)
270
+ super({ field: checkPath(props.field), values }, valuesFacts(values))
251
271
  }
252
272
  }
253
273
 
@@ -265,7 +285,8 @@ export class Under extends Base {
265
285
  readonly _tag = "Under"
266
286
  declare readonly prefix: ReadonlyArray<Operand>
267
287
  constructor(props: { readonly prefix: ReadonlyArray<Operand> }) {
268
- super({ prefix: checkOperands(props.prefix) }, leaf)
288
+ const prefix = checkOperands(props.prefix)
289
+ super({ prefix }, valuesFacts(prefix))
269
290
  }
270
291
  }
271
292
 
@@ -333,7 +354,7 @@ export class Predicate extends Base {
333
354
  }
334
355
  super(
335
356
  { name: props.name, run: props.run },
336
- { size: 1, depth: 1, opaque: true },
357
+ { size: 1, weight: 1, depth: 1, opaque: true },
337
358
  )
338
359
  }
339
360
  }
@@ -358,40 +379,61 @@ const operandJson = (operand: Operand): unknown =>
358
379
  }
359
380
  : { _tag: "Principal", path: operand.path }
360
381
 
361
- /** Gives the JSON form of a condition with no predicate. */
362
- export const toJson = (node: Node): unknown => {
363
- switch (node._tag) {
364
- case "True":
365
- case "False":
366
- return { _tag: node._tag }
367
- case "Compare":
368
- return {
369
- _tag: "Compare",
370
- op: node.op,
371
- field: node.field,
372
- value: operandJson(node.value),
373
- }
374
- case "In":
375
- return {
376
- _tag: "In",
377
- field: node.field,
378
- 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
+ )
379
433
  }
380
- case "Exists":
381
- return { _tag: "Exists", field: node.field }
382
- case "Under":
383
- return { _tag: "Under", prefix: Arr.map(node.prefix, operandJson) }
384
- case "And":
385
- case "Or":
386
- return { _tag: node._tag, conditions: Arr.map(node.conditions, toJson) }
387
- case "Not":
388
- return { _tag: "Not", condition: toJson(node.condition) }
389
- case "Predicate":
390
- throw new Error(
391
- `The predicate "${node.name}" can not become JSON. A condition with a predicate has no JSON form.`,
392
- )
393
- }
394
- }
434
+ }
435
+ return go(root)
436
+ })
395
437
 
396
438
  /** Tests if a value is a node. Only a constructor of a node makes one. */
397
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
  }