@nmnmcc/ability 0.3.0 → 1.0.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 (66) hide show
  1. package/README.md +199 -31
  2. package/dist/Authorizer.d.ts +64 -39
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +291 -6
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +444 -65
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +1007 -25
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Domain.d.ts +27 -17
  11. package/dist/Domain.d.ts.map +1 -1
  12. package/dist/Domain.js +52 -10
  13. package/dist/Domain.js.map +1 -1
  14. package/dist/HttpApi.d.ts +84 -40
  15. package/dist/HttpApi.d.ts.map +1 -1
  16. package/dist/HttpApi.js +103 -13
  17. package/dist/HttpApi.js.map +1 -1
  18. package/dist/Policy.d.ts +314 -56
  19. package/dist/Policy.d.ts.map +1 -1
  20. package/dist/Policy.js +1215 -38
  21. package/dist/Policy.js.map +1 -1
  22. package/dist/Principal.js +2 -2
  23. package/dist/Principal.js.map +1 -1
  24. package/dist/Resource.d.ts +171 -20
  25. package/dist/Resource.d.ts.map +1 -1
  26. package/dist/Resource.js +788 -15
  27. package/dist/Resource.js.map +1 -1
  28. package/dist/Rule.d.ts +115 -29
  29. package/dist/Rule.d.ts.map +1 -1
  30. package/dist/Rule.js +219 -12
  31. package/dist/Rule.js.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +2 -2
  34. package/src/Authorizer.ts +625 -61
  35. package/src/Condition.ts +2403 -112
  36. package/src/Domain.ts +80 -23
  37. package/src/HttpApi.ts +225 -25
  38. package/src/Policy.ts +2836 -96
  39. package/src/Principal.ts +2 -2
  40. package/src/Resource.ts +1544 -29
  41. package/src/Rule.ts +463 -45
  42. package/dist/internal/authorizer.d.ts +0 -28
  43. package/dist/internal/authorizer.d.ts.map +0 -1
  44. package/dist/internal/authorizer.js +0 -153
  45. package/dist/internal/authorizer.js.map +0 -1
  46. package/dist/internal/condition.d.ts +0 -11
  47. package/dist/internal/condition.d.ts.map +0 -1
  48. package/dist/internal/condition.js +0 -393
  49. package/dist/internal/condition.js.map +0 -1
  50. package/dist/internal/domain.js +0 -34
  51. package/dist/internal/domain.js.map +0 -1
  52. package/dist/internal/httpApi.js +0 -41
  53. package/dist/internal/httpApi.js.map +0 -1
  54. package/dist/internal/policy.js +0 -524
  55. package/dist/internal/policy.js.map +0 -1
  56. package/dist/internal/resource.js +0 -66
  57. package/dist/internal/resource.js.map +0 -1
  58. package/dist/internal/rule.js +0 -131
  59. package/dist/internal/rule.js.map +0 -1
  60. package/src/internal/authorizer.ts +0 -271
  61. package/src/internal/condition.ts +0 -607
  62. package/src/internal/domain.ts +0 -56
  63. package/src/internal/httpApi.ts +0 -84
  64. package/src/internal/policy.ts +0 -815
  65. package/src/internal/resource.ts +0 -98
  66. package/src/internal/rule.ts +0 -203
package/src/Rule.ts CHANGED
@@ -19,49 +19,58 @@
19
19
  * @stability stable
20
20
  * @since 0.3.0
21
21
  */
22
- import type * as Equal from "effect/Equal"
23
- import type * as Inspectable from "effect/Inspectable"
22
+ import * as Arr from "effect/Array"
23
+ import * as Data from "effect/Data"
24
+ import * as HashSet from "effect/HashSet"
24
25
  import type * as Pipeable from "effect/Pipeable"
25
- import type * as Condition from "./Condition.ts"
26
+ import * as Condition from "./Condition.ts"
26
27
  import type * as Domain from "./Domain.ts"
27
- import * as internal from "./internal/rule.ts"
28
- import type * as Resource from "./Resource.ts"
29
-
30
- /**
31
- * @stability stable
32
- * @category type IDs
33
- * @since 0.3.0
34
- */
35
- export const TypeId: "~@nmnmcc/ability/Rule" = internal.TypeId
28
+ import * as Resource from "./Resource.ts"
36
29
 
37
30
  /**
31
+ * A rule. `E`, `R` and `O` come from its condition. See
32
+ * `Condition.Condition`. `D` is the domain of the rule. A predicate reads the
33
+ * values and the principal of its domain, so a rule may go only into a policy
34
+ * of its own domain. A rule from {@link make} has the domain `any`. `I` is
35
+ * what the predicates of its condition read. See `Condition.Reads`.
36
+ *
38
37
  * @stability stable
39
- * @category type IDs
38
+ * @category models
40
39
  * @since 0.3.0
41
40
  */
42
- export type TypeId = typeof TypeId
41
+ export interface Rule<E = never, R = never, O = never, D = any, I = unknown>
42
+ extends Shape<E, R, O, I>, Pipeable.Pipeable {
43
+ readonly "~Rule": Condition.Variance<E, R, O>
44
+ readonly "~Domain": (domain: D) => D
45
+ [Symbol.iterator](): Generator<
46
+ Rule<E, R, O, D, I>,
47
+ Rule<E, R, O, D, I>,
48
+ unknown
49
+ >
50
+ }
43
51
 
44
52
  /**
45
- * A rule. `E`, `R` and `O` come from its condition. See
46
- * `Condition.Condition`.
53
+ * The data of a rule.
47
54
  *
48
55
  * @stability stable
49
56
  * @category models
50
- * @since 0.3.0
57
+ * @since 1.0.0
51
58
  */
52
- export interface Rule<E = never, R = never, O = never>
53
- extends Equal.Equal, Inspectable.Inspectable, Pipeable.Pipeable {
54
- readonly [TypeId]: TypeId
59
+ export interface Shape<E = never, R = never, O = never, I = unknown> {
55
60
  readonly _tag: "Allow" | "Deny"
56
61
  readonly actions: ReadonlyArray<string>
57
62
  readonly resources: ReadonlyArray<string>
58
- readonly when?: Condition.Ast
63
+ readonly when?: Condition.Condition<E, R, O, I>
59
64
  readonly fields?: ReadonlyArray<string>
60
65
  readonly reason?: string
61
66
  readonly id?: string
62
67
  readonly meta?: unknown
63
- readonly "~Rule"?: Condition.Variance<E, R, O>
64
- [Symbol.iterator](): Generator<Rule<E, R, O>, Rule<E, R, O>, unknown>
68
+ /**
69
+ * The domain of the builder that made the rule. Two domains may have the
70
+ * same types but other paths, so the types can not tell them apart. It is
71
+ * not in the JSON form of a rule.
72
+ */
73
+ readonly domain?: Domain.Any
65
74
  }
66
75
 
67
76
  /**
@@ -71,7 +80,9 @@ export interface Rule<E = never, R = never, O = never>
71
80
  * @category models
72
81
  * @since 0.3.0
73
82
  */
74
- export type Any = Rule<any, any, any>
83
+ // Every rule fits `never` as what it reads, and a condition taken from such a
84
+ // rule fits no rule that reads a value or a principal.
85
+ export type Any = Rule<any, any, any, any, never>
75
86
 
76
87
  /**
77
88
  * The error type of a rule.
@@ -143,6 +154,42 @@ export type ActionFor<D, K> =
143
154
  ? Domain.AnyAction<D>
144
155
  : Domain.ActionOf<D, Extract<K, string>>)
145
156
 
157
+ /**
158
+ * The targets in the resource argument of a rule: the one target, or the
159
+ * targets of a list.
160
+ *
161
+ * @stability stable
162
+ * @category utility types
163
+ * @since 1.0.0
164
+ */
165
+ export type TargetOf<Res> = Res extends ReadonlyArray<infer X> ? X : Res
166
+
167
+ /**
168
+ * The actions that a rule may use for its resource argument. A list of
169
+ * resources may use an action of any of them. One target with a union type
170
+ * may use only an action of every kind in the union, because its value may be
171
+ * any one of them. When the argument may be a list or one target, the rule
172
+ * may use only an action that each of these shapes allows.
173
+ *
174
+ * @stability stable
175
+ * @category utility types
176
+ * @since 1.0.0
177
+ */
178
+ export type ActionForTargets<D, Res> = (
179
+ Res extends ReadonlyArray<infer X>
180
+ ? (action: ActionFor<D, KindOf<X>>) => void
181
+ : (action: ActionForTarget<D, Res>) => void
182
+ ) extends (action: infer A) => void
183
+ ? A
184
+ : never
185
+
186
+ /** The actions for one target that is not a list. */
187
+ type ActionForTarget<D, Res> =
188
+ | "*"
189
+ | ("*" extends KindOf<Res>
190
+ ? Domain.AnyAction<D>
191
+ : Domain.CommonAction<D, Extract<KindOf<Res>, string>>)
192
+
146
193
  /**
147
194
  * The data type of the kinds `K`. For `"*"` it has no fields.
148
195
  *
@@ -173,7 +220,22 @@ export type ValueFor<D, K> = "*" extends K
173
220
  * @since 0.3.0
174
221
  */
175
222
  export type FieldFor<D, K> =
176
- Condition.FieldPath<DataFor<D, K>> | `${string}*${string}`
223
+ Condition.FieldSelector<DataFor<D, K>> | `${string}*${string}`
224
+
225
+ /**
226
+ * A condition that a rule for the kinds `K` may have. Its predicates may read
227
+ * only the values of these kinds and the principal of the domain.
228
+ *
229
+ * @stability stable
230
+ * @category models
231
+ * @since 1.0.0
232
+ */
233
+ export type ConditionFor<D, K> = Condition.Condition<
234
+ any,
235
+ any,
236
+ any,
237
+ Condition.Reads<ValueFor<D, K>, Domain.Principal<D>>
238
+ >
177
239
 
178
240
  /**
179
241
  * The options of a rule.
@@ -190,7 +252,7 @@ export interface Options<D, K, C extends Condition.Any> {
190
252
  ValueFor<D, K>,
191
253
  Domain.Principal<D>
192
254
  >,
193
- ) => C
255
+ ) => C & Condition.Checked<C>
194
256
  readonly fields?: ReadonlyArray<FieldFor<D, K>>
195
257
  readonly reason?: string
196
258
  readonly id?: string
@@ -208,33 +270,130 @@ type SingleOrArray<A> = A | ReadonlyArray<A>
208
270
  */
209
271
  export interface Builder<D> {
210
272
  readonly allow: <
211
- const T extends Target<D>,
212
- const A extends ActionFor<D, KindOf<T>>,
213
- const C extends Condition.Any = Condition.Condition,
273
+ const Res extends SingleOrArray<Target<D>>,
274
+ const A extends ActionForTargets<D, Res>,
275
+ const C extends ConditionFor<D, KindOf<TargetOf<Res>>> =
276
+ Condition.Condition,
214
277
  >(
215
278
  action: SingleOrArray<A>,
216
- resource: SingleOrArray<T>,
217
- options?: Options<D, KindOf<T>, C>,
218
- ) => Rule<Condition.Error<C>, Condition.Services<C>, Condition.OpaqueOf<C>>
279
+ resource: Res,
280
+ options?: Options<D, KindOf<TargetOf<Res>>, C>,
281
+ ) => Rule<
282
+ Condition.Error<C>,
283
+ Condition.Services<C>,
284
+ Condition.OpaqueOf<C>,
285
+ D,
286
+ Condition.ReadsOf<C>
287
+ >
219
288
  readonly deny: <
220
- const T extends Target<D>,
221
- const A extends ActionFor<D, KindOf<T>>,
222
- const C extends Condition.Any = Condition.Condition,
289
+ const Res extends SingleOrArray<Target<D>>,
290
+ const A extends ActionForTargets<D, Res>,
291
+ const C extends ConditionFor<D, KindOf<TargetOf<Res>>> =
292
+ Condition.Condition,
223
293
  >(
224
294
  action: SingleOrArray<A>,
225
- resource: SingleOrArray<T>,
226
- options?: Options<D, KindOf<T>, C>,
227
- ) => Rule<Condition.Error<C>, Condition.Services<C>, Condition.OpaqueOf<C>>
295
+ resource: Res,
296
+ options?: Options<D, KindOf<TargetOf<Res>>, C>,
297
+ ) => Rule<
298
+ Condition.Error<C>,
299
+ Condition.Services<C>,
300
+ Condition.OpaqueOf<C>,
301
+ D,
302
+ Condition.ReadsOf<C>
303
+ >
228
304
  }
229
305
 
306
+ /**
307
+ * The class of every rule value. A rule works in a generator: it gives
308
+ * itself, so `yield* _.allow(...)` gives the rule to `Policy.define`.
309
+ */
310
+ class RuleData extends Data.Class<Shape<unknown, unknown, unknown, any>> {
311
+ *[Symbol.iterator](): Generator<this, this, unknown> {
312
+ yield this
313
+ return this
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Makes a rule from its data. It does not check the data against a domain.
319
+ * Use {@link builder} for rules with types, or `Policy.schema` for rules from
320
+ * JSON.
321
+ *
322
+ * @stability stable
323
+ * @category constructors
324
+ * @since 1.0.0
325
+ */
326
+ export const make = <E = never, R = never, O = never, I = unknown>(
327
+ data: Shape<E, R, O, I>,
328
+ ): Rule<E, R, O, any, I> => {
329
+ // A missing option is left out, so two rules with the same data are equal.
330
+ const shape: Shape<E, R, O, I> = {
331
+ _tag: data._tag,
332
+ actions: [...data.actions],
333
+ resources: [...data.resources],
334
+ ...(data.when === undefined ? {} : { when: data.when }),
335
+ ...(data.fields === undefined ? {} : { fields: [...data.fields] }),
336
+ ...(data.reason === undefined ? {} : { reason: data.reason }),
337
+ ...(data.id === undefined ? {} : { id: data.id }),
338
+ ...(data.meta === undefined ? {} : { meta: data.meta }),
339
+ ...(data.domain === undefined ? {} : { domain: data.domain }),
340
+ }
341
+ return new RuleData(shape) as never
342
+ }
343
+
344
+ const toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>
345
+ Array.isArray(value) ? value : [value as A]
346
+
347
+ const kindName = (target: unknown): string =>
348
+ Resource.isResource(target)
349
+ ? Resource.descriptor(target).kind
350
+ : (target as string)
351
+
352
+ /**
353
+ * Gives the domain of the builder that made a rule. A rule from {@link make}
354
+ * has none, unless its data gives one.
355
+ *
356
+ * @stability stable
357
+ * @category getters
358
+ * @since 1.0.0
359
+ */
360
+ export const domainOf = (rule: Any): Domain.Any | undefined => rule.domain
361
+
362
+ const makeFromOptions =
363
+ (tag: "Allow" | "Deny", domain: Domain.Any) =>
364
+ (
365
+ action: string | ReadonlyArray<string>,
366
+ resource: unknown,
367
+ options?: {
368
+ readonly when?: (builder: never) => Condition.Any
369
+ readonly fields?: ReadonlyArray<string>
370
+ readonly reason?: string
371
+ readonly id?: string
372
+ readonly meta?: unknown
373
+ },
374
+ ): Any =>
375
+ make({
376
+ _tag: tag,
377
+ actions: toArray(action),
378
+ resources: Arr.map(toArray(resource), kindName),
379
+ ...(options?.when === undefined
380
+ ? {}
381
+ : { when: options.when(Condition.builder() as never) }),
382
+ ...(options?.fields === undefined ? {} : { fields: options.fields }),
383
+ ...(options?.reason === undefined ? {} : { reason: options.reason }),
384
+ ...(options?.id === undefined ? {} : { id: options.id }),
385
+ ...(options?.meta === undefined ? {} : { meta: options.meta }),
386
+ domain,
387
+ })
388
+
230
389
  /**
231
390
  * Makes a rule builder for a domain.
232
391
  *
233
392
  * @example
234
393
  * ```ts
235
394
  * const _ = Rule.builder(App)
236
- * const rule = _.allow("read", Bot, {
237
- * when: ($) => $.eq("ownerUserId", $.principal("userId")),
395
+ * const rule = _.allow("read", Document, {
396
+ * when: ($) => $.eq("ownerId", $.principal("id")),
238
397
  * })
239
398
  * ```
240
399
  *
@@ -242,8 +401,11 @@ export interface Builder<D> {
242
401
  * @category constructors
243
402
  * @since 0.3.0
244
403
  */
245
- export const builder: <D extends Domain.Any>(domain: D) => Builder<D> =
246
- internal.builder
404
+ export const builder = <D extends Domain.Any>(domain: D): Builder<D> =>
405
+ ({
406
+ allow: makeFromOptions("Allow", domain),
407
+ deny: makeFromOptions("Deny", domain),
408
+ }) as never
247
409
 
248
410
  /**
249
411
  * Tests if a value is a rule.
@@ -252,7 +414,131 @@ export const builder: <D extends Domain.Any>(domain: D) => Builder<D> =
252
414
  * @category guards
253
415
  * @since 0.3.0
254
416
  */
255
- export const isRule: (u: unknown) => u is Any = internal.isRule
417
+ export const isRule = (u: unknown): u is Any => u instanceof RuleData
418
+
419
+ /**
420
+ * A part of a field pattern: `"**"`, `"*"`, or one plain Unicode code point.
421
+ */
422
+ type Token = string
423
+
424
+ /**
425
+ * Gives the parts of a pattern. It reads the pattern by Unicode code points,
426
+ * as `run` reads the field. One star is `*`. Two or more stars next to each
427
+ * other match what `**` matches, so they are one part, `**`. So two star parts
428
+ * are never next to each other.
429
+ */
430
+ const tokensOf = (pattern: string): ReadonlyArray<Token> =>
431
+ Arr.map(normalize(pattern).match(/\*+|[^*]/gu) ?? [], (part) =>
432
+ part.startsWith("*") ? (part.length === 1 ? "*" : "**") : part,
433
+ )
434
+
435
+ /**
436
+ * Puts each run of whole `**` parts together into one part. `a.**.**` matches
437
+ * what `a.**` matches, and with one part the rule of {@link skips} for no
438
+ * parts covers the whole run.
439
+ */
440
+ const normalize = (pattern: string): string =>
441
+ Arr.dedupeAdjacentWith(
442
+ pattern.split("."),
443
+ (left, right) => isGlobstar(left) && isGlobstar(right),
444
+ ).join(".")
445
+
446
+ /** Tests if a part of a pattern is only stars, two or more of them. */
447
+ const isGlobstar = (part: string): boolean => /^\*{2,}$/u.test(part)
448
+
449
+ const isStar = (token: Token | undefined): boolean =>
450
+ token === "*" || token === "**"
451
+
452
+ type States = HashSet.HashSet<number>
453
+
454
+ /**
455
+ * Gives the places that a match may reach from `state` with no input. A star
456
+ * may match nothing. A `**` that is a whole part may also match no parts, so
457
+ * the dot next to it may be left out too: `**.secret` and `settings.**.secret`
458
+ * match `secret` and `settings.secret`, and `settings.**` matches `settings`.
459
+ */
460
+ const skips = (
461
+ tokens: ReadonlyArray<Token>,
462
+ state: number,
463
+ ): ReadonlyArray<number> => {
464
+ const token = tokens[state]
465
+ const startsPart = state === 0 || tokens[state - 1] === "."
466
+ return [
467
+ ...(isStar(token) ? [state + 1] : []),
468
+ ...(token === "**" && startsPart && tokens[state + 1] === "."
469
+ ? [state + 2]
470
+ : []),
471
+ ...(token === "." &&
472
+ tokens[state + 1] === "**" &&
473
+ state + 2 === tokens.length &&
474
+ state > 0
475
+ ? [state + 2]
476
+ : []),
477
+ ]
478
+ }
479
+
480
+ /**
481
+ * Adds the places that a match may reach with no input. See {@link skips}.
482
+ * Each step reads only the places that the step before found, so each place
483
+ * is read one time.
484
+ */
485
+ const close = (
486
+ tokens: ReadonlyArray<Token>,
487
+ states: States,
488
+ found: States = states,
489
+ ): States => {
490
+ const after = HashSet.filter(
491
+ HashSet.fromIterable(
492
+ Arr.flatMap(Array.from(found), (state) => skips(tokens, state)),
493
+ ),
494
+ (state) => !HashSet.has(states, state),
495
+ )
496
+ return HashSet.size(after) === 0
497
+ ? states
498
+ : close(tokens, HashSet.union(states, after), after)
499
+ }
500
+
501
+ /**
502
+ * Gives the places in the pattern after one more character of the input. A
503
+ * star that takes the character stays at its place. It has taken part of a
504
+ * name, so it may not match no parts any more: only the place after it is
505
+ * added, with no other skips from {@link skips}.
506
+ */
507
+ const step = (
508
+ tokens: ReadonlyArray<Token>,
509
+ states: States,
510
+ char: string,
511
+ ): States => {
512
+ const list = Array.from(states)
513
+ const stars = Arr.filter(list, (state) => {
514
+ const token = tokens[state]
515
+ return token === "**" || (token === "*" && char !== ".")
516
+ })
517
+ const moved = Arr.flatMap(list, (state) =>
518
+ !isStar(tokens[state]) && tokens[state] === char ? [state + 1] : [],
519
+ )
520
+ return HashSet.union(
521
+ HashSet.fromIterable(stars),
522
+ close(
523
+ tokens,
524
+ HashSet.fromIterable([...moved, ...Arr.map(stars, (state) => state + 1)]),
525
+ ),
526
+ )
527
+ }
528
+
529
+ /**
530
+ * Gives every place in the pattern that a match may have reached after
531
+ * `input`. It keeps a set of places, and it does not go back, so the time is
532
+ * at most the length of the input times the length of the pattern. A regular
533
+ * expression may take a very long time on a pattern with many stars.
534
+ */
535
+ const run = (tokens: ReadonlyArray<Token>, input: string): States =>
536
+ Arr.reduce(
537
+ Array.from(input),
538
+ close(tokens, HashSet.make(0)),
539
+ (states, char) =>
540
+ HashSet.size(states) === 0 ? states : step(tokens, states, char),
541
+ )
256
542
 
257
543
  /**
258
544
  * Tests if a field name matches a field name or a field pattern of a rule.
@@ -261,5 +547,137 @@ export const isRule: (u: unknown) => u is Any = internal.isRule
261
547
  * @category predicates
262
548
  * @since 0.3.0
263
549
  */
264
- export const matchesField: (pattern: string, field: string) => boolean =
265
- internal.matchesFieldPattern
550
+ export const matchesField = (pattern: string, field: string): boolean => {
551
+ if (!pattern.includes("*")) {
552
+ return pattern === field
553
+ }
554
+ const tokens = tokensOf(pattern)
555
+ return HashSet.has(run(tokens, field), tokens.length)
556
+ }
557
+
558
+ /**
559
+ * Tests if a field pattern may match a field inside `field`, such as
560
+ * `settings.secret` inside `settings`, or `settings.` for the empty key of a
561
+ * record.
562
+ *
563
+ * @stability stable
564
+ * @category predicates
565
+ * @since 1.0.0
566
+ */
567
+ export const matchesInside = (pattern: string, field: string): boolean =>
568
+ // The name of a part may be empty, as a key of a record may be. So a match
569
+ // that ends just after the dot names a part too.
570
+ HashSet.size(run(tokensOf(pattern), `${field}.`)) > 0
571
+
572
+ /**
573
+ * Tests if a field pattern may match a field of a kind: a field in its list,
574
+ * a path that the Schema has, such as a key of a record, or a getter. See
575
+ * `Resource.hasMatchingPath` and `Resource.hasMatchingGetter`.
576
+ */
577
+ const fitsSomeField = (
578
+ pattern: string,
579
+ descriptor: Resource.Descriptor,
580
+ ): boolean =>
581
+ Arr.some(descriptor.fieldPaths, (field) => matchesField(pattern, field)) ||
582
+ // A pattern with more parts than a field path may have fits no other
583
+ // field, and it is not cut into lists of parts.
584
+ (pattern.split(".", maxPatternParts + 1).length <= maxPatternParts &&
585
+ Arr.some(
586
+ partLists(pattern),
587
+ (parts) =>
588
+ Resource.hasMatchingPath(descriptor.schema, parts, matchesField) ||
589
+ Resource.hasMatchingGetter(descriptor.schema, parts, matchesField),
590
+ ))
591
+
592
+ /** The most parts of a pattern that the walk of the Schema takes. */
593
+ const maxPatternParts = 256
594
+
595
+ /**
596
+ * The lists of parts that a pattern may stand for. A part that is only stars
597
+ * is `**`. One `**` inside a part, as in `**value`, may take the dots of more
598
+ * parts, so such a part gives two lists: one with the part as it is, with
599
+ * `*` for `**`, and one with the part cut at the stars, as in `*`, `**`,
600
+ * `*value`. A part with two such runs stays as it is, with `*` for `**`, so
601
+ * it may miss a field but never fits one too many. The number of lists is
602
+ * kept small.
603
+ */
604
+ const partLists = (pattern: string): ReadonlyArray<ReadonlyArray<string>> =>
605
+ Arr.take(
606
+ Arr.reduce(
607
+ normalize(pattern).split("."),
608
+ [[]] as ReadonlyArray<ReadonlyArray<string>>,
609
+ (lists, part) => {
610
+ const choices: ReadonlyArray<ReadonlyArray<string>> = isGlobstar(part)
611
+ ? [["**"]]
612
+ : // Only one run of stars: with two, the cut would leave out the
613
+ // text between them, and the lists would fit more fields.
614
+ (part.match(/\*{2,}/gu) ?? []).length === 1
615
+ ? [
616
+ [part.replace(/\*{2,}/gu, "*")],
617
+ [
618
+ `${part.slice(0, part.indexOf("**"))}*`,
619
+ "**",
620
+ `*${part.slice(part.lastIndexOf("**") + 2)}`,
621
+ ],
622
+ ]
623
+ : [[part]]
624
+ return Arr.take(
625
+ Arr.flatMap(lists, (list) =>
626
+ Arr.map(choices, (choice) => [...list, ...choice]),
627
+ ),
628
+ 16,
629
+ )
630
+ },
631
+ ),
632
+ 16,
633
+ )
634
+
635
+ /**
636
+ * Tests the parts of a rule that do not need the resource value: the action,
637
+ * the kind and the field.
638
+ *
639
+ * A rule with an empty list of fields covers no request. A rule with fields
640
+ * covers a request with no field only when it is an allow rule. So "may update some fields" is a yes. A deny rule for some fields does
641
+ * not stop a request for the whole value. With the `descriptor` of the kind,
642
+ * such an allow rule covers the request only when one of its patterns may
643
+ * match a field of the kind. A grant of fields that the kind does not have is
644
+ * not a grant of some fields.
645
+ *
646
+ * @stability stable
647
+ * @category predicates
648
+ * @since 1.0.0
649
+ */
650
+ export const matchesTarget = (
651
+ rule: Any,
652
+ target: {
653
+ readonly action: string
654
+ readonly kind: string
655
+ readonly field?: string | undefined
656
+ readonly descriptor?: Resource.Descriptor | undefined
657
+ },
658
+ ): boolean => {
659
+ if (!rule.actions.includes("*") && !rule.actions.includes(target.action)) {
660
+ return false
661
+ }
662
+ if (!rule.resources.includes("*") && !rule.resources.includes(target.kind)) {
663
+ return false
664
+ }
665
+ if (rule.fields === undefined) {
666
+ return true
667
+ }
668
+ // A rule for no fields is a rule for nothing. A grant with its last field
669
+ // taken away must not cover the whole value.
670
+ if (rule.fields.length === 0) {
671
+ return false
672
+ }
673
+ if (target.field === undefined) {
674
+ const descriptor = target.descriptor
675
+ return (
676
+ rule._tag === "Allow" &&
677
+ (descriptor === undefined ||
678
+ Arr.some(rule.fields, (pattern) => fitsSomeField(pattern, descriptor)))
679
+ )
680
+ }
681
+ const field = target.field
682
+ return rule.fields.some((pattern) => matchesField(pattern, field))
683
+ }
@@ -1,28 +0,0 @@
1
- import "../Domain.js";
2
- import "../Decision.js";
3
- import "../Policy.js";
4
- import "../Authorizer.js";
5
- import "effect/Effect";
6
- import "effect/Layer";
7
- import * as Schema from "effect/Schema";
8
- declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
9
- readonly action: Schema.String;
10
- readonly kind: Schema.String;
11
- readonly field: Schema.optionalKey<Schema.String>;
12
- readonly reason: Schema.optionalKey<Schema.String>;
13
- }>, import("effect/Cause").YieldableError>;
14
- /** @internal */
15
- export declare class Forbidden extends Forbidden_base {}
16
- declare const PolicyUnavailable_base: Schema.Class<PolicyUnavailable, Schema.TaggedStruct<"PolicyUnavailable", {
17
- readonly cause: Schema.Defect;
18
- }>, import("effect/Cause").YieldableError>;
19
- /** @internal */
20
- export declare class PolicyUnavailable extends PolicyUnavailable_base {}
21
- declare const Pending_base: Schema.Class<Pending, Schema.TaggedStruct<"Pending", {
22
- readonly ticket: Schema.String;
23
- readonly reason: Schema.optionalKey<Schema.String>;
24
- }>, import("effect/Cause").YieldableError>;
25
- /** @internal */
26
- export declare class Pending extends Pending_base {}
27
- export {};
28
- //# sourceMappingURL=authorizer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"authorizer.d.ts","names":[],"sources":["../../src/internal/authorizer.ts"],"mappings":";;;;;;;;;;;;;;qBAqBa,kBAAkB;;;;;qBAUlB,0BAA0B;;;;;;qBAK1B,gBAAgB"}