@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
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The JSON form of a condition. The decoder is a plain walk with two limits:
3
- * it goes at most `conditionDepth` levels down, and it reads at most
4
- * `jsonNodes` objects and array items, with a shared object counted for each
5
- * place. So no input can fill the stack or take a very long time. It makes
2
+ * The JSON form of a condition. The decoder is a walk of the trampoline, so a
3
+ * deep input does not fill the stack. It reads at most `jsonNodes` objects
4
+ * and array items, with a shared object counted for each place, so no input
5
+ * can take a very long time. It makes
6
6
  * each node with its constructor, and it reads only the own data fields of
7
7
  * the input, as a condition reads a value.
8
8
  */
@@ -12,7 +12,9 @@ import * as Result from "effect/Result"
12
12
  import type * as SchemaIssue from "effect/SchemaIssue"
13
13
  import * as Limits from "./limits.ts"
14
14
  import * as Nodes from "./nodes.ts"
15
+ import * as Place from "./place.ts"
15
16
  import { readPart } from "./read.ts"
17
+ import * as Trampoline from "./trampoline.ts"
16
18
 
17
19
  type Node = Nodes.Node
18
20
 
@@ -27,84 +29,123 @@ export class Problem extends Data.TaggedError("Problem")<{
27
29
  readonly issue?: SchemaIssue.Issue | undefined
28
30
  }> {}
29
31
 
30
- /** The count of the objects and items that one decode has read. It belongs to one decode. */
32
+ /**
33
+ * The count of the objects and items that one decode has read, with the
34
+ * limits of the decode. It belongs to one decode.
35
+ */
31
36
  export interface Budget {
32
37
  used: number
38
+ readonly limits: Limits.Limits
33
39
  }
34
40
 
35
41
  export const problem = (path: ReadonlyArray<PropertyKey>, message: string) =>
36
42
  new Problem({ path, message })
37
43
 
44
+ /**
45
+ * The problem at a place of the input. A decoder keeps the place of each
46
+ * part, which shares the path of its parent, and makes the full path only for
47
+ * a problem: see `Place`.
48
+ */
49
+ export const problemAt = (at: Place.Place | undefined, message: string) =>
50
+ problem(Place.pathOf(at), message)
51
+
38
52
  /** Counts `count` more objects or items, and stops past the limit. */
39
53
  export const spend = (
40
54
  budget: Budget,
41
55
  count: number,
42
- path: ReadonlyArray<PropertyKey>,
56
+ at: Place.Place | undefined,
43
57
  ): void => {
44
58
  budget.used += count
45
- if (budget.used > Limits.jsonNodes) {
46
- throw problem(
47
- path,
48
- `The input has more than ${String(Limits.jsonNodes)} objects and items. The most is ${String(Limits.jsonNodes)}.`,
59
+ const most = budget.limits.jsonNodes
60
+ if (budget.used > most) {
61
+ throw problemAt(
62
+ at,
63
+ `The input has more than ${String(most)} objects and items. The most is ${String(most)}.`,
49
64
  )
50
65
  }
51
66
  }
52
67
 
53
- /** Reads a field of the input. A field that is not plain data is a problem. */
68
+ /**
69
+ * Reads a field of the input at the place `at`. A field that is not plain
70
+ * data is a problem.
71
+ */
54
72
  export const read = (
55
73
  input: unknown,
56
74
  name: string,
57
- path: ReadonlyArray<PropertyKey>,
75
+ at: Place.Place | undefined,
58
76
  ): unknown => {
59
77
  try {
60
78
  return readPart(input, name)
61
79
  } catch {
62
- throw problem([...path, name], `The field "${name}" is not plain data`)
80
+ throw problemAt(Place.at(at, name), `The field "${name}" is not plain data`)
63
81
  }
64
82
  }
65
83
 
66
84
  export const readString = (
67
85
  input: unknown,
68
86
  name: string,
69
- path: ReadonlyArray<PropertyKey>,
87
+ at: Place.Place | undefined,
70
88
  ): string => {
71
- const value = read(input, name, path)
89
+ const value = read(input, name, at)
72
90
  if (typeof value !== "string") {
73
- throw problem([...path, name], `The field "${name}" must be a string`)
91
+ throw problemAt(Place.at(at, name), `The field "${name}" must be a string`)
74
92
  }
75
93
  return value
76
94
  }
77
95
 
78
- /** Reads the items of an array. */
96
+ /** Reads the items of an array. The place of an item is `at`, `name`, index. */
79
97
  export const readList = (
80
98
  input: unknown,
81
99
  name: string,
82
- path: ReadonlyArray<PropertyKey>,
100
+ at: Place.Place | undefined,
83
101
  budget: Budget,
84
102
  ): ReadonlyArray<unknown> => {
85
- const value = read(input, name, path)
86
- const at = [...path, name]
103
+ const value = read(input, name, at)
104
+ const place = Place.at(at, name)
87
105
  if (!Array.isArray(value)) {
88
- throw problem(at, `The field "${name}" must be an array`)
106
+ throw problemAt(place, `The field "${name}" must be an array`)
89
107
  }
90
108
  const list: ReadonlyArray<unknown> = value
91
- spend(budget, list.length, at)
109
+ spend(budget, list.length, place)
92
110
  // `Array.makeBy` of Effect gives at least one item, so it can not make an
93
111
  // empty list. An array-like object with a length makes one with no read
94
112
  // of the items before the read rule.
95
113
  return Array.from({ length: list.length }, (_, index) =>
96
- read(list, String(index), at),
114
+ read(list, String(index), place),
97
115
  )
98
116
  }
99
117
 
118
+ /**
119
+ * Reads a field path, and stops one with more parts than the limit of the
120
+ * decode. A node checks the ceiling only, since it does not know its domain.
121
+ */
122
+ const readPath = (
123
+ input: unknown,
124
+ name: string,
125
+ at: Place.Place | undefined,
126
+ budget: Budget,
127
+ ): string => {
128
+ const path = readString(input, name, at)
129
+ const most = budget.limits.fieldPathParts
130
+ const parts = path.split(".", most + 1).length
131
+ if (parts > most) {
132
+ throw problemAt(
133
+ Place.at(at, name),
134
+ `A field path has more than ${String(most)} parts. The most is ${String(most)}.`,
135
+ )
136
+ }
137
+ return path
138
+ }
139
+
100
140
  const isJsonScalar = (value: unknown): value is Nodes.Scalar =>
101
141
  Nodes.isScalar(value) && (typeof value !== "number" || Number.isFinite(value))
102
142
 
103
143
  const decodeOperand = (
104
144
  input: unknown,
105
- path: ReadonlyArray<PropertyKey>,
145
+ at: Place.Place | undefined,
146
+ budget: Budget,
106
147
  ): Nodes.Operand => {
107
- const tag = read(input, "_tag", path)
148
+ const tag = read(input, "_tag", at)
108
149
  if (tag === "Literal") {
109
150
  // A missing value is not `null`: a test with `null` has its own
110
151
  // meaning, and a lost value must not become one.
@@ -113,22 +154,22 @@ const decodeOperand = (
113
154
  ? Object.getOwnPropertyDescriptor(input, "value")
114
155
  : undefined
115
156
  if (own === undefined || ("value" in own && own.value === undefined)) {
116
- throw problem([...path, "value"], "A fixed value is missing")
157
+ throw problemAt(Place.at(at, "value"), "A fixed value is missing")
117
158
  }
118
- const value = read(input, "value", path)
159
+ const value = read(input, "value", at)
119
160
  if (!isJsonScalar(value)) {
120
- throw problem(
121
- [...path, "value"],
161
+ throw problemAt(
162
+ Place.at(at, "value"),
122
163
  "A fixed value must be a string, a finite number, a boolean or null",
123
164
  )
124
165
  }
125
166
  return Nodes.literal(value)
126
167
  }
127
168
  if (tag === "Principal") {
128
- return Nodes.principalRef(readString(input, "path", path))
169
+ return Nodes.principalRef(readPath(input, "path", at, budget))
129
170
  }
130
- throw problem(
131
- [...path, "_tag"],
171
+ throw problemAt(
172
+ Place.at(at, "_tag"),
132
173
  'An operand must have the tag "Literal" or "Principal"',
133
174
  )
134
175
  }
@@ -136,96 +177,148 @@ const decodeOperand = (
136
177
  const decodeOperands = (
137
178
  input: unknown,
138
179
  name: string,
139
- path: ReadonlyArray<PropertyKey>,
180
+ at: Place.Place | undefined,
140
181
  budget: Budget,
141
182
  ): ReadonlyArray<Nodes.Operand> =>
142
- Arr.map(readList(input, name, path, budget), (operand, index) =>
143
- decodeOperand(operand, [...path, name, index]),
183
+ Arr.map(readList(input, name, at, budget), (operand, index) =>
184
+ decodeOperand(operand, Place.at(at, name, index), budget),
144
185
  )
145
186
 
146
- const decodeNode = (
147
- input: unknown,
148
- path: ReadonlyArray<PropertyKey>,
149
- depth: number,
150
- budget: Budget,
151
- ): Node => {
152
- if (depth > Limits.conditionDepth) {
153
- throw problem(
154
- path,
155
- `A condition has more than ${String(Limits.conditionDepth)} levels. The most is ${String(Limits.conditionDepth)}.`,
156
- )
157
- }
158
- if (typeof input !== "object" || input === null) {
159
- throw problem(path, "A condition node must be an object")
160
- }
161
- spend(budget, 1, path)
162
- const children = (name: string) =>
163
- Arr.map(readList(input, name, path, budget), (child, index) =>
164
- decodeNode(child, [...path, name, index], depth + 1, budget),
165
- )
166
- try {
167
- return makeNode(input, path, depth, budget, children)
168
- } catch (error) {
169
- // A node past a limit, such as a field path with too many parts, is a
170
- // problem at its place.
171
- if (error instanceof Limits.LimitExceeded) {
172
- throw problem(path, error.message)
173
- }
174
- throw error
175
- }
187
+ /** A part of the input to decode, with its place. */
188
+ interface Part {
189
+ readonly input: unknown
190
+ readonly at: Place.Place | undefined
176
191
  }
177
192
 
178
- const makeNode = (
179
- input: object,
180
- path: ReadonlyArray<PropertyKey>,
181
- depth: number,
182
- budget: Budget,
183
- children: (name: string) => ReadonlyArray<Node>,
184
- ): Node => {
185
- const tag = read(input, "_tag", path)
186
- switch (tag) {
187
- case "True":
188
- return Nodes.always
189
- case "False":
190
- return Nodes.never
191
- case "Compare": {
192
- const op = readString(input, "op", path)
193
- if (!Nodes.isCompareOperator(op)) {
194
- throw problem([...path, "op"], `"${op}" is not a compare operator`)
193
+ /**
194
+ * The decoder of a condition, with the budget of one decode. It is a walk of
195
+ * the trampoline for an input of unknown depth, so a deep input does not fill
196
+ * the stack.
197
+ */
198
+ const decoder = (budget: Budget): ((part: Part) => Node) => {
199
+ // The nodes of this condition that the decoder has read. A condition with
200
+ // more nodes than `conditionNodes` does not load, so the decoder stops at
201
+ // the first node past it, and it does not read the rest of a very large
202
+ // input. The count belongs to the decode of one condition.
203
+ const count = { nodes: 0 }
204
+ return Trampoline.adaptive<Part, Node>((R, child) => (part, depth) => {
205
+ const { input, at } = part
206
+ if (typeof input !== "object" || input === null) {
207
+ throw problemAt(at, "A condition node must be an object")
208
+ }
209
+ spend(budget, 1, at)
210
+ count.nodes += 1
211
+ if (count.nodes > budget.limits.conditionNodes) {
212
+ throw problemAt(
213
+ at,
214
+ `A condition has more than ${String(budget.limits.conditionNodes)} nodes and values. The most is ${String(budget.limits.conditionNodes)}.`,
215
+ )
216
+ }
217
+ // A node past a limit, such as a field path with too many parts, is a
218
+ // problem at its place. The constructor checks the ceilings, and the
219
+ // decoder checks the limits of the decode.
220
+ const make = (build: () => Node): Node => {
221
+ const node = (() => {
222
+ try {
223
+ return build()
224
+ } catch (error) {
225
+ if (error instanceof Limits.LimitExceeded) {
226
+ throw problemAt(at, error.message)
227
+ }
228
+ throw error
229
+ }
230
+ })()
231
+ const most = budget.limits.conditionNodes
232
+ if (node.weight > most) {
233
+ throw problemAt(
234
+ at,
235
+ `A condition has ${String(node.weight)} nodes and values. The most is ${String(most)}.`,
236
+ )
195
237
  }
196
- return new Nodes.Compare({
197
- op,
198
- field: readString(input, "field", path),
199
- value: decodeOperand(read(input, "value", path), [...path, "value"]),
200
- })
238
+ return node
201
239
  }
202
- case "In":
203
- return new Nodes.In({
204
- field: readString(input, "field", path),
205
- values: decodeOperands(input, "values", path, budget),
206
- })
207
- case "Exists":
208
- return new Nodes.Exists({ field: readString(input, "field", path) })
209
- case "Under":
210
- return new Nodes.Under({
211
- prefix: decodeOperands(input, "prefix", path, budget),
212
- })
213
- case "And":
214
- return new Nodes.And(children("conditions"))
215
- case "Or":
216
- return new Nodes.Or(children("conditions"))
217
- case "Not":
218
- return new Nodes.Not(
219
- decodeNode(
220
- read(input, "condition", path),
221
- [...path, "condition"],
222
- depth + 1,
223
- budget,
224
- ),
240
+ const children = (name: string) =>
241
+ R.forEach(readList(input, name, at, budget), (item, index) =>
242
+ child({ input: item, at: Place.at(at, name, index) }, depth + 1),
225
243
  )
226
- default:
227
- throw problem([...path, "_tag"], "The node has no known tag")
228
- }
244
+ const tag = read(input, "_tag", at)
245
+ switch (tag) {
246
+ case "True":
247
+ return R.succeed(Nodes.always)
248
+ case "False":
249
+ return R.succeed(Nodes.never)
250
+ case "Compare": {
251
+ const op = readString(input, "op", at)
252
+ if (!Nodes.isCompareOperator(op)) {
253
+ throw problemAt(
254
+ Place.at(at, "op"),
255
+ `"${op}" is not a compare operator`,
256
+ )
257
+ }
258
+ return R.succeed(
259
+ make(
260
+ () =>
261
+ new Nodes.Compare({
262
+ op,
263
+ field: readPath(input, "field", at, budget),
264
+ value: decodeOperand(
265
+ read(input, "value", at),
266
+ Place.at(at, "value"),
267
+ budget,
268
+ ),
269
+ }),
270
+ ),
271
+ )
272
+ }
273
+ case "In":
274
+ return R.succeed(
275
+ make(
276
+ () =>
277
+ new Nodes.In({
278
+ field: readPath(input, "field", at, budget),
279
+ values: decodeOperands(input, "values", at, budget),
280
+ }),
281
+ ),
282
+ )
283
+ case "Exists":
284
+ return R.succeed(
285
+ make(
286
+ () =>
287
+ new Nodes.Exists({ field: readPath(input, "field", at, budget) }),
288
+ ),
289
+ )
290
+ case "Under":
291
+ return R.succeed(
292
+ make(
293
+ () =>
294
+ new Nodes.Under({
295
+ prefix: decodeOperands(input, "prefix", at, budget),
296
+ }),
297
+ ),
298
+ )
299
+ case "And":
300
+ return R.map(children("conditions"), (conditions) =>
301
+ make(() => new Nodes.And(conditions)),
302
+ )
303
+ case "Or":
304
+ return R.map(children("conditions"), (conditions) =>
305
+ make(() => new Nodes.Or(conditions)),
306
+ )
307
+ case "Not":
308
+ return R.map(
309
+ child(
310
+ {
311
+ input: read(input, "condition", at),
312
+ at: Place.at(at, "condition"),
313
+ },
314
+ depth + 1,
315
+ ),
316
+ (condition) => make(() => new Nodes.Not(condition)),
317
+ )
318
+ default:
319
+ throw problemAt(Place.at(at, "_tag"), "The node has no known tag")
320
+ }
321
+ })
229
322
  }
230
323
 
231
324
  /**
@@ -234,14 +327,16 @@ const makeNode = (
234
327
  */
235
328
  export const decodeAt = (
236
329
  input: unknown,
237
- path: ReadonlyArray<PropertyKey>,
330
+ at: Place.Place | undefined,
238
331
  budget: Budget,
239
- ): Node => decodeNode(input, path, 1, budget)
332
+ ): Node => decoder(budget)({ input, at })
240
333
 
241
- /** Decodes the JSON form of a condition. */
334
+ /** Decodes the JSON form of a condition, with the default limits. */
242
335
  export const decode = (input: unknown): Result.Result<Node, Problem> => {
243
336
  try {
244
- return Result.succeed(decodeNode(input, [], 1, { used: 0 }))
337
+ return Result.succeed(
338
+ decoder({ used: 0, limits: Limits.defaults })({ input, at: undefined }),
339
+ )
245
340
  } catch (error) {
246
341
  if (error instanceof Problem) {
247
342
  return Result.fail(error)
@@ -1,50 +1,120 @@
1
1
  /**
2
2
  * The one table of limits. Each limit keeps a walk, a decoder or a search
3
- * bounded, so no input can fill the stack or take a very long time. The
4
- * values are the same in the types and at run time where both have one, as
5
- * for the parts of a field path.
3
+ * bounded, so no input can take a very long time or very much memory. A
4
+ * domain may set its own limits, up to the ceilings.
6
5
  */
7
6
  import * as Schema from "effect/Schema"
8
7
 
9
- /** The most parts of a field path, as in `settings.model`. */
10
- export const fieldPathParts = 5
11
-
12
- /** The most parts of a field pattern, as in `**.secret`. */
13
- export const patternParts = 16
8
+ /** The values of all the limits. */
9
+ export interface Limits {
10
+ /** The most parts of a field path, as in `settings.model`. */
11
+ readonly fieldPathParts: number
12
+ /** The most parts of a field pattern, as in `**.secret`. */
13
+ readonly patternParts: number
14
+ /**
15
+ * The most nodes of one condition. A child that a condition uses in many
16
+ * places is counted for each place.
17
+ */
18
+ readonly conditionNodes: number
19
+ /** The most nodes of one residual condition. */
20
+ readonly residualNodes: number
21
+ /**
22
+ * The most nodes of a policy tree, with its rules and the nodes of their
23
+ * conditions. A part that the tree uses in many places is counted for
24
+ * each place.
25
+ */
26
+ readonly policyNodes: number
27
+ /** The most objects and arrays in one input to a decoder. */
28
+ readonly jsonNodes: number
29
+ /** The most fields in the list of the fields of one Schema. */
30
+ readonly catalogFields: number
31
+ /** The most steps of one search for a value. */
32
+ readonly searchSteps: number
33
+ /** The most fields that one search for a value may test. */
34
+ readonly searchFields: number
35
+ }
14
36
 
15
37
  /**
16
- * The most nodes of one condition. A child that a condition uses in many
17
- * places is counted for each place.
38
+ * The limits of a domain that sets none. A walk does not fill the stack at
39
+ * any depth (see `Trampoline`), and the levels of a condition or of a policy
40
+ * tree are at most its nodes, so the limits of the nodes keep the time and
41
+ * the memory of a deep input small too. There is no limit of levels.
18
42
  */
19
- export const conditionNodes = 100_000
20
-
21
- /** The most levels of one condition. */
22
- export const conditionDepth = 500
43
+ export const defaults: Limits = {
44
+ fieldPathParts: 1000,
45
+ patternParts: 1000,
46
+ conditionNodes: 1_000_000,
47
+ residualNodes: 100_000,
48
+ policyNodes: 1_000_000,
49
+ jsonNodes: 10_000_000,
50
+ catalogFields: 100_000,
51
+ // A search is the work of one check of a kind, as in the Authorizer, so
52
+ // its steps are a limit of the time of one request.
53
+ searchSteps: 1_000_000,
54
+ searchFields: 1000,
55
+ }
23
56
 
24
- /** The most nodes of one residual condition. */
25
- export const residualNodes = 10_000
57
+ /**
58
+ * The most that a domain may set for each limit. The constructors of the
59
+ * nodes check these, since a node does not know its domain. The binding to a
60
+ * domain checks the limits of the domain.
61
+ */
62
+ export const ceilings: Limits = {
63
+ fieldPathParts: 10_000,
64
+ patternParts: 10_000,
65
+ conditionNodes: 100_000_000,
66
+ residualNodes: 100_000_000,
67
+ policyNodes: 100_000_000,
68
+ jsonNodes: 1_000_000_000,
69
+ catalogFields: 10_000_000,
70
+ searchSteps: 1_000_000_000,
71
+ searchFields: 100_000,
72
+ }
26
73
 
27
- /** The most levels of a policy tree. */
28
- export const policyDepth = 256
74
+ /** The Schema of one limit: a whole number from 1 up to its ceiling. */
75
+ const limit = (name: keyof Limits) =>
76
+ Schema.optionalKey(
77
+ Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: ceilings[name] })),
78
+ )
29
79
 
30
80
  /**
31
- * The most nodes of a policy tree, with its rules and the nodes of their
32
- * conditions. A part that the tree uses in many places is counted for each
33
- * place.
81
+ * The Schema of the limits that a domain sets. Each one is a whole number
82
+ * from 1 up to its ceiling, and a limit that the domain leaves out has its
83
+ * default.
34
84
  */
35
- export const policyNodes = 100_000
85
+ export const Options = Schema.Struct({
86
+ fieldPathParts: limit("fieldPathParts"),
87
+ patternParts: limit("patternParts"),
88
+ conditionNodes: limit("conditionNodes"),
89
+ residualNodes: limit("residualNodes"),
90
+ policyNodes: limit("policyNodes"),
91
+ jsonNodes: limit("jsonNodes"),
92
+ catalogFields: limit("catalogFields"),
93
+ searchSteps: limit("searchSteps"),
94
+ searchFields: limit("searchFields"),
95
+ })
36
96
 
37
- /** The most objects and arrays in one input to a decoder. */
38
- export const jsonNodes = 1_000_000
39
-
40
- /** The most fields in the catalog of one Schema. */
41
- export const catalogFields = 10_000
97
+ /**
98
+ * Makes the limits of a domain from the ones that it sets. A value that is
99
+ * not a whole number from 1 up to its ceiling is a mistake in the code, so
100
+ * it is a defect.
101
+ */
102
+ export const make = (options: unknown = {}): Limits => ({
103
+ ...defaults,
104
+ ...Schema.decodeUnknownSync(Options)(options),
105
+ })
42
106
 
43
- /** The most steps of one search for a value. */
44
- export const searchSteps = 1_000_000
107
+ // The default of each limit, by its name.
45
108
 
46
- /** The most fields that one search for a value may test. */
47
- export const searchFields = 256
109
+ export const fieldPathParts = defaults.fieldPathParts
110
+ export const patternParts = defaults.patternParts
111
+ export const conditionNodes = defaults.conditionNodes
112
+ export const residualNodes = defaults.residualNodes
113
+ export const policyNodes = defaults.policyNodes
114
+ export const jsonNodes = defaults.jsonNodes
115
+ export const catalogFields = defaults.catalogFields
116
+ export const searchSteps = defaults.searchSteps
117
+ export const searchFields = defaults.searchFields
48
118
 
49
119
  /**
50
120
  * The error when an input goes past a limit. It names the limit, the size of