@nmnmcc/ability 2.1.2 → 2.1.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 (43) hide show
  1. package/dist/Authorizer.d.ts +11 -11
  2. package/dist/Authorizer.d.ts.map +1 -1
  3. package/dist/Authorizer.js.map +1 -1
  4. package/dist/Condition.d.ts +1 -1
  5. package/dist/HttpApi.js +1 -1
  6. package/dist/Policy.d.ts +16 -6
  7. package/dist/Policy.d.ts.map +1 -1
  8. package/dist/Policy.js.map +1 -1
  9. package/dist/internal/algebra.js +8 -5
  10. package/dist/internal/algebra.js.map +1 -1
  11. package/dist/internal/authorizer.d.ts.map +1 -1
  12. package/dist/internal/authorizer.js +35 -9
  13. package/dist/internal/authorizer.js.map +1 -1
  14. package/dist/internal/bind.js +37 -27
  15. package/dist/internal/bind.js.map +1 -1
  16. package/dist/internal/catalog.d.ts +0 -31
  17. package/dist/internal/catalog.d.ts.map +1 -1
  18. package/dist/internal/catalog.js +39 -17
  19. package/dist/internal/catalog.js.map +1 -1
  20. package/dist/internal/decide.js +1 -1
  21. package/dist/internal/decide.js.map +1 -1
  22. package/dist/internal/domain.js +8 -5
  23. package/dist/internal/domain.js.map +1 -1
  24. package/dist/internal/patterns.js +1 -1
  25. package/dist/internal/policy-json.js +2 -2
  26. package/dist/internal/policy-json.js.map +1 -1
  27. package/dist/internal/policy-nodes.js +21 -12
  28. package/dist/internal/policy-nodes.js.map +1 -1
  29. package/dist/internal/policy.js +1 -1
  30. package/dist/internal/search.js +39 -27
  31. package/dist/internal/search.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/Authorizer.ts +10 -10
  34. package/src/Policy.ts +16 -5
  35. package/src/internal/algebra.ts +12 -8
  36. package/src/internal/authorizer.ts +77 -34
  37. package/src/internal/bind.ts +43 -36
  38. package/src/internal/catalog.ts +69 -40
  39. package/src/internal/decide.ts +5 -1
  40. package/src/internal/domain.ts +6 -11
  41. package/src/internal/policy-json.ts +24 -8
  42. package/src/internal/policy-nodes.ts +19 -13
  43. package/src/internal/search.ts +31 -21
@@ -175,12 +175,6 @@ export interface Branch {
175
175
 
176
176
  /** The catalog of the fields of a Schema. */
177
177
  export interface Catalog {
178
- /**
179
- * The fields that a list shows, a field before the fields below it. For a
180
- * recursive Schema, the list goes into each recursive place one time on
181
- * each path, so it is finite.
182
- */
183
- readonly fields: ReadonlyArray<Field>
184
178
  /** The places, by id. The root is the place with the id 0. */
185
179
  readonly branches: ReadonlyArray<Branch>
186
180
  /**
@@ -272,13 +266,30 @@ const structOf = (ast: SchemaAST.AST): SchemaAST.Objects | undefined =>
272
266
  const hasCheck = (
273
267
  checks: ReadonlyArray<SchemaAST.Check<never>>,
274
268
  id: string,
275
- ): boolean =>
276
- Arr.some(
277
- checks,
278
- (check) =>
279
- check.annotations?.representation?.id === id ||
280
- (check._tag === "FilterGroup" && hasCheck(check.checks, id)),
281
- )
269
+ ): boolean => hasCheckIn({ checks, id })
270
+
271
+ /**
272
+ * The walk of {@link hasCheck}. A group of checks may hold a group, very many
273
+ * levels down, as `.and(...)` many times makes, so it is a walk of the
274
+ * trampoline for an input of an unknown depth.
275
+ */
276
+ const hasCheckIn = Trampoline.adaptive<
277
+ {
278
+ readonly checks: ReadonlyArray<SchemaAST.Check<never>>
279
+ readonly id: string
280
+ },
281
+ boolean
282
+ >(
283
+ (R, child) =>
284
+ ({ checks, id }, depth) =>
285
+ R.some(checks, (check) =>
286
+ check.annotations?.representation?.id === id
287
+ ? R.succeed(true)
288
+ : check._tag === "FilterGroup"
289
+ ? child({ checks: check.checks, id }, depth + 1)
290
+ : R.succeed(false),
291
+ ),
292
+ )
282
293
 
283
294
  /** A struct with named fields, or with the keys of a record. */
284
295
  const isStruct = (
@@ -287,6 +298,13 @@ const isStruct = (
287
298
  ast !== undefined &&
288
299
  (ast.propertySignatures.length > 0 || ast.indexSignatures.length > 0)
289
300
 
301
+ /** Tests if a Schema holds only values that are not objects, as `bigint`. */
302
+ const isPrimitive = (ast: SchemaAST.AST): boolean =>
303
+ ast._tag === "BigInt" ||
304
+ ast._tag === "Symbol" ||
305
+ ast._tag === "UniqueSymbol" ||
306
+ (ast._tag === "Literal" && typeof ast.literal === "bigint")
307
+
290
308
  /** The kinds of the values of a Schema that is not a struct. */
291
309
  const kindsOf = (ast: SchemaAST.AST): Kinds => {
292
310
  switch (ast._tag) {
@@ -464,6 +482,15 @@ const placeOf = (place: Place): Made => {
464
482
  const valueKinds = Arr.reduce(values, noKinds, (kinds, schema) =>
465
483
  joinKinds(kinds, kindsOf(structOf(schema) ?? schema)),
466
484
  )
485
+ // A value of a branch may be an object, with parts that the Schema does
486
+ // not name. A `bigint` or a `symbol` is not a scalar that a test takes,
487
+ // so its kinds say `object`, but it has no parts: a read below it gives
488
+ // `null`, as below a string.
489
+ const objects = Arr.some(
490
+ values,
491
+ (schema) =>
492
+ !isPrimitive(schema) && kindsOf(structOf(schema) ?? schema).object,
493
+ )
467
494
  const kinds = place.top
468
495
  ? anyKinds
469
496
  : joinKinds(
@@ -485,8 +512,7 @@ const placeOf = (place: Place): Made => {
485
512
  // decision may get a value made by hand. A value that may be any
486
513
  // object, as one of `Schema.Unknown` may, may have the name with any
487
514
  // value too.
488
- const top =
489
- place.top || valueKinds.object || properties.length < structs.length
515
+ const top = place.top || objects || properties.length < structs.length
490
516
  const below: Place = {
491
517
  schemas: Arr.flatMap(properties, (property) =>
492
518
  alternatives(property.type, new Set()),
@@ -505,8 +531,7 @@ const placeOf = (place: Place): Made => {
505
531
  return [name, below] as const
506
532
  })
507
533
  const signatures = Arr.flatMap(records, (struct) => struct.indexSignatures)
508
- const keyTop =
509
- place.top || valueKinds.object || records.length < structs.length
534
+ const keyTop = place.top || objects || records.length < structs.length
510
535
  return {
511
536
  branch: {
512
537
  kinds,
@@ -514,11 +539,7 @@ const placeOf = (place: Place): Made => {
514
539
  structs: structs.length > 0,
515
540
  // A field that is a scalar in one branch of a union and a struct in
516
541
  // another holds parts too.
517
- open:
518
- records.length > 0 ||
519
- structs.length > 0 ||
520
- valueKinds.object ||
521
- place.top,
542
+ open: records.length > 0 || structs.length > 0 || objects || place.top,
522
543
  plain: place.plain,
523
544
  key:
524
545
  signatures.length === 0
@@ -634,13 +655,18 @@ const fieldAt = (
634
655
  * The fields that a list shows, a field before the fields below it, down to
635
656
  * `parts` parts. A path goes into a place that is on it already one time
636
657
  * only, so the list of a recursive Schema is finite. The walk keeps a stack
637
- * of the places to visit, so a deep Schema does not fill the stack. The
638
- * stack belongs to this walk.
658
+ * of the places to visit, so a deep Schema does not fill the stack. A Schema
659
+ * that shares one part in many places may have very many fields, so the
660
+ * walk counts the fields that it makes, and it stops at the first one past
661
+ * `most`. The stack and the count belong to this walk.
639
662
  */
640
663
  const listOf = (
641
664
  branches: ReadonlyArray<Branch>,
642
665
  parts: number,
666
+ most: number,
667
+ thing: string,
643
668
  ): ReadonlyArray<Field> => {
669
+ const count = { fields: 0 }
644
670
  interface Visit {
645
671
  readonly branch: Branch
646
672
  readonly path: ReadonlyArray<Segment>
@@ -691,13 +717,14 @@ const listOf = (
691
717
  visit.path.length === 0
692
718
  ? []
693
719
  : [fieldAt(visit.branch, visit.path, parts)]
694
- if (field.length > 0 && rest.length + 1 > Limits.ceilings.catalogFields) {
720
+ count.fields += field.length
721
+ if (count.fields > most) {
695
722
  throw Limits.exceeded(
696
- "A Schema",
723
+ thing,
697
724
  "catalogFields",
698
725
  "fields",
699
- rest.length + 1,
700
- Limits.ceilings.catalogFields,
726
+ count.fields,
727
+ most,
701
728
  )
702
729
  }
703
730
  return Option.some([field, rest] as const)
@@ -706,22 +733,25 @@ const listOf = (
706
733
  }
707
734
 
708
735
  /**
709
- * The fields that a list shows for paths of at most `parts` parts, as for a
710
- * domain with that limit. See `Catalog.fields`.
736
+ * The fields that a list shows, a field before the fields below it, for
737
+ * paths of at most `parts` parts. For a recursive Schema, the list goes into
738
+ * each recursive place one time on each path, so it is finite. A list with
739
+ * more than `most` fields is an error, as for a domain with these limits.
740
+ * Each call makes the list again: a domain checks its size one time, and a
741
+ * list of permitted fields reads it.
711
742
  */
712
743
  export const fieldsOf = (
713
744
  catalog: Catalog,
714
- parts: number,
715
- ): ReadonlyArray<Field> =>
716
- parts === Limits.fieldPathParts
717
- ? catalog.fields
718
- : listOf(catalog.branches, parts)
745
+ parts: number = Limits.fieldPathParts,
746
+ most: number = Limits.ceilings.catalogFields,
747
+ thing: string = "A Schema",
748
+ ): ReadonlyArray<Field> => listOf(catalog.branches, parts, most, thing)
719
749
 
720
750
  /**
721
- * Makes the catalog of a Schema, with the list of its fields for the default
722
- * limit of the parts of a path. It throws when a field name has a dot or is
723
- * empty, and when the Schema has more places or fields than the ceiling of
724
- * `catalogFields`. A domain checks the size of the list with its own limit.
751
+ * Makes the catalog of a Schema. It throws when a field name has a dot or is
752
+ * empty, and when the Schema has more places than the ceiling of
753
+ * `catalogFields`. A domain checks the size of the list of the fields with
754
+ * its own limit: see {@link fieldsOf}.
725
755
  */
726
756
  export const catalogOf = (ast: SchemaAST.AST): Catalog => {
727
757
  const branches = branchesOf({
@@ -731,7 +761,6 @@ export const catalogOf = (ast: SchemaAST.AST): Catalog => {
731
761
  plain: true,
732
762
  })
733
763
  return {
734
- fields: listOf(branches, Limits.fieldPathParts),
735
764
  branches,
736
765
  edges: Arr.reduce(
737
766
  branches,
@@ -390,7 +390,11 @@ export const permittedFields = (
390
390
  const listed = Arr.filter(
391
391
  // The list for the limit of the parts of a path of the domain, as the
392
392
  // decision of each field reads it.
393
- Catalog.fieldsOf(descriptor.catalog, policy.domain.limits.fieldPathParts),
393
+ Catalog.fieldsOf(
394
+ descriptor.catalog,
395
+ policy.domain.limits.fieldPathParts,
396
+ policy.domain.limits.catalogFields,
397
+ ),
394
398
  (field) =>
395
399
  field.listed &&
396
400
  Arr.every(field.path, (segment) => typeof segment === "string"),
@@ -21,22 +21,17 @@ class DomainData extends Data.Class<{
21
21
  readonly limits: Limits.Limits
22
22
  }> {}
23
23
 
24
- /** Stops a catalog with more fields in its list than the domain takes. */
24
+ /**
25
+ * Stops a catalog with more fields in its list than the domain takes. The
26
+ * list stops at the first field past the limit, so a Schema with very many
27
+ * fields does not take very much memory first.
28
+ */
25
29
  const checkCatalog = (
26
30
  catalog: Catalog.Catalog,
27
31
  limits: Limits.Limits,
28
32
  name: string,
29
33
  ): void => {
30
- const fields = Catalog.fieldsOf(catalog, limits.fieldPathParts).length
31
- if (fields > limits.catalogFields) {
32
- throw Limits.exceeded(
33
- name,
34
- "catalogFields",
35
- "fields",
36
- fields,
37
- limits.catalogFields,
38
- )
39
- }
34
+ Catalog.fieldsOf(catalog, limits.fieldPathParts, limits.catalogFields, name)
40
35
  }
41
36
 
42
37
  /**
@@ -287,14 +287,30 @@ const decodeMeta = (
287
287
  (found) =>
288
288
  found === undefined
289
289
  ? Effect.void
290
- : Effect.mapError(
291
- Schema.decodeUnknownEffect(metaCodec(found.schema))(found.value),
292
- (error) =>
293
- new Json.Problem({
294
- path: Place.pathOf(at),
295
- message: error.message,
296
- issue: error.issue,
297
- }),
290
+ : Schema.decodeUnknownEffect(metaCodec(found.schema))(found.value).pipe(
291
+ Effect.mapError(
292
+ (error) =>
293
+ new Json.Problem({
294
+ path: Place.pathOf(at),
295
+ message: error.message,
296
+ issue: error.issue,
297
+ }),
298
+ ),
299
+ // The decoder of a Schema in Effect calls itself for each level of
300
+ // the value, so a deep value that the budget takes, as 5,000 levels
301
+ // of a recursive `meta` Schema, fills its stack. That is a defect
302
+ // of the decoder, not of the policy, so it is a problem of the
303
+ // input here, and the policy does not load.
304
+ Effect.catchDefect((defect) =>
305
+ defect instanceof RangeError
306
+ ? Effect.fail(
307
+ Json.problemAt(
308
+ at,
309
+ "The meta of the rule has too many levels for the Schema of meta",
310
+ ),
311
+ )
312
+ : Effect.die(defect),
313
+ ),
298
314
  ),
299
315
  )
300
316
  }
@@ -196,29 +196,35 @@ export const isNode = (u: unknown): u is Node => u instanceof Base
196
196
  * into the condition of each rule takes the rules from this list first, so it
197
197
  * does not go down the levels of the tree and of a condition at once.
198
198
  */
199
- export const rulesOf = (root: Node): ReadonlyArray<Rule.Rule> =>
199
+ export const rulesOf = (root: Node): ReadonlyArray<Rule.Rule> => {
200
+ // The rules go into one list, in order. A walk that joined the lists of
201
+ // the children at each node would copy the list of a deep tree at each
202
+ // level. The list belongs to this walk.
203
+ const rules: Array<Rule.Rule> = []
200
204
  // The walk takes the steps of a trampoline, so a deep tree does not fill
201
205
  // the stack.
202
206
  Trampoline.run(root.depth, (R) => {
203
- const go = (node: Node): Trampoline.Step<ReadonlyArray<Rule.Rule>> => {
207
+ const children = (nodes: ReadonlyArray<Node>) =>
208
+ R.map(
209
+ R.forEach(nodes, (child) => R.suspend(() => go(child))),
210
+ () => undefined,
211
+ )
212
+ const go = (node: Node): Trampoline.Step<undefined> => {
204
213
  switch (node._tag) {
205
214
  case "Rules":
206
- return R.succeed(node.rules)
215
+ Arr.forEach(node.rules, (rule) => {
216
+ rules.push(rule)
217
+ })
218
+ return R.succeed(undefined)
207
219
  case "All":
208
220
  case "Any":
209
221
  case "FirstOf":
210
- return R.map(
211
- R.forEach(node.policies, (child) => R.suspend(() => go(child))),
212
- Arr.flatten,
213
- )
222
+ return children(node.policies)
214
223
  case "Ceiling":
215
- return R.map(
216
- R.forEach([node.cap, node.policy], (child) =>
217
- R.suspend(() => go(child)),
218
- ),
219
- Arr.flatten,
220
- )
224
+ return children([node.cap, node.policy])
221
225
  }
222
226
  }
223
227
  return go(root)
224
228
  })
229
+ return rules
230
+ }
@@ -317,28 +317,35 @@ const fix = (root: Node, field: string, value: unknown, search: Search): Node =>
317
317
  })
318
318
 
319
319
  /** The fields of the tests of a condition, once for each test. */
320
- const testedFields = (root: Node): ReadonlyArray<string> =>
320
+ const testedFields = (root: Node): ReadonlyArray<string> => {
321
+ // The fields go into one list, in order. A walk that joined the lists of
322
+ // the children would copy the list of a deep condition at each level. The
323
+ // list belongs to this walk.
324
+ const fields: Array<string> = []
321
325
  Trampoline.run(root.depth, (R) => {
322
- const go = (node: Node): Trampoline.Step<ReadonlyArray<string>> => {
326
+ const go = (node: Node): Trampoline.Step<undefined> => {
323
327
  switch (node._tag) {
324
328
  case "Compare":
325
329
  case "In":
326
330
  case "Exists":
327
- return R.succeed([node.field])
331
+ fields.push(node.field)
332
+ return R.succeed(undefined)
328
333
  case "And":
329
334
  case "Or":
330
335
  return R.map(
331
336
  R.forEach(node.conditions, (child) => R.suspend(() => go(child))),
332
- Arr.flatten,
337
+ () => undefined,
333
338
  )
334
339
  case "Not":
335
340
  return R.suspend(() => go(node.condition))
336
341
  default:
337
- return R.succeed([])
342
+ return R.succeed(undefined)
338
343
  }
339
344
  }
340
345
  return go(root)
341
346
  })
347
+ return fields
348
+ }
342
349
 
343
350
  /**
344
351
  * Picks the next field to split on. A field comes before the fields below it,
@@ -486,20 +493,29 @@ const exampleOf = (
486
493
  * belongs to one search.
487
494
  */
488
495
  const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
489
- type Pair = readonly [string, ReadonlyArray<Nodes.Scalar>]
496
+ // The constants go into one map as the walk finds them. A walk that
497
+ // joined the lists of the children would copy the list of a deep
498
+ // condition at each level. The map belongs to one search.
499
+ const all = new Map<string, Set<Nodes.Scalar>>()
500
+ const add = (field: string, values: ReadonlyArray<Nodes.Scalar>): void => {
501
+ const known = all.get(field) ?? new Set<Nodes.Scalar>()
502
+ Arr.forEach(values, (value) => known.add(value))
503
+ all.set(field, known)
504
+ }
490
505
  // The walk takes the steps of a trampoline, so a deep condition does not
491
506
  // fill the stack.
492
- const pairs = Trampoline.run(node.depth, (R) => {
493
- const go = (current: Node): Trampoline.Step<ReadonlyArray<Pair>> => {
507
+ Trampoline.run(node.depth, (R) => {
508
+ const go = (current: Node): Trampoline.Step<undefined> => {
494
509
  switch (current._tag) {
495
510
  case "Compare":
496
- return R.succeed([[current.field, [literalOf(current.value)]]])
511
+ add(current.field, [literalOf(current.value)])
512
+ return R.succeed(undefined)
497
513
  case "In":
498
- return R.succeed([
499
- [current.field, Arr.map(current.values, literalOf)],
500
- ])
514
+ add(current.field, Arr.map(current.values, literalOf))
515
+ return R.succeed(undefined)
501
516
  case "Exists":
502
- return R.succeed([[current.field, []]])
517
+ add(current.field, [])
518
+ return R.succeed(undefined)
503
519
  case "Under":
504
520
  throw new Error(
505
521
  "Condition.satisfiable needs a condition with no Under nodes. Call Condition.partial first.",
@@ -510,22 +526,16 @@ const constantsOf = (node: Node): Map<string, Set<Nodes.Scalar>> => {
510
526
  R.forEach(current.conditions, (child) =>
511
527
  R.suspend(() => go(child)),
512
528
  ),
513
- Arr.flatten,
529
+ () => undefined,
514
530
  )
515
531
  case "Not":
516
532
  return R.suspend(() => go(current.condition))
517
533
  default:
518
- return R.succeed([])
534
+ return R.succeed(undefined)
519
535
  }
520
536
  }
521
537
  return go(node)
522
538
  })
523
- const all = new Map<string, Set<Nodes.Scalar>>()
524
- Arr.forEach(pairs, ([field, values]) => {
525
- const known = all.get(field) ?? new Set<Nodes.Scalar>()
526
- Arr.forEach(values, (value) => known.add(value))
527
- all.set(field, known)
528
- })
529
539
  return all
530
540
  }
531
541