@nmnmcc/ability 1.0.1 → 2.0.0

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 (127) hide show
  1. package/README.md +112 -69
  2. package/dist/Authorizer.d.ts +43 -47
  3. package/dist/Authorizer.d.ts.map +1 -1
  4. package/dist/Authorizer.js +5 -290
  5. package/dist/Authorizer.js.map +1 -1
  6. package/dist/Condition.d.ts +164 -332
  7. package/dist/Condition.d.ts.map +1 -1
  8. package/dist/Condition.js +75 -890
  9. package/dist/Condition.js.map +1 -1
  10. package/dist/Decision.d.ts +13 -13
  11. package/dist/Decision.d.ts.map +1 -1
  12. package/dist/Decision.js.map +1 -1
  13. package/dist/Domain.d.ts +10 -21
  14. package/dist/Domain.d.ts.map +1 -1
  15. package/dist/Domain.js +4 -62
  16. package/dist/Domain.js.map +1 -1
  17. package/dist/HttpApi.d.ts +31 -7
  18. package/dist/HttpApi.d.ts.map +1 -1
  19. package/dist/HttpApi.js +25 -8
  20. package/dist/HttpApi.js.map +1 -1
  21. package/dist/Limits.d.ts +114 -0
  22. package/dist/Limits.d.ts.map +1 -0
  23. package/dist/Limits.js +132 -0
  24. package/dist/Limits.js.map +1 -0
  25. package/dist/Policy.d.ts +271 -269
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +68 -1177
  28. package/dist/Policy.js.map +1 -1
  29. package/dist/Resource.d.ts +4 -177
  30. package/dist/Resource.d.ts.map +1 -1
  31. package/dist/Resource.js +4 -803
  32. package/dist/Resource.js.map +1 -1
  33. package/dist/index.d.ts +3 -4
  34. package/dist/index.js +4 -5
  35. package/dist/internal/algebra.js +267 -0
  36. package/dist/internal/algebra.js.map +1 -0
  37. package/dist/internal/analysis.js +245 -0
  38. package/dist/internal/analysis.js.map +1 -0
  39. package/dist/internal/authorizer.d.ts +30 -0
  40. package/dist/internal/authorizer.d.ts.map +1 -0
  41. package/dist/internal/authorizer.js +221 -0
  42. package/dist/internal/authorizer.js.map +1 -0
  43. package/dist/internal/bind.js +286 -0
  44. package/dist/internal/bind.js.map +1 -0
  45. package/dist/internal/catalog.d.ts +56 -0
  46. package/dist/internal/catalog.d.ts.map +1 -0
  47. package/dist/internal/catalog.js +292 -0
  48. package/dist/internal/catalog.js.map +1 -0
  49. package/dist/internal/decide.js +166 -0
  50. package/dist/internal/decide.js.map +1 -0
  51. package/dist/internal/domain.js +32 -0
  52. package/dist/internal/domain.js.map +1 -0
  53. package/dist/internal/json.js +134 -0
  54. package/dist/internal/json.js.map +1 -0
  55. package/dist/internal/limits.d.ts +14 -0
  56. package/dist/internal/limits.d.ts.map +1 -0
  57. package/dist/internal/limits.js +43 -0
  58. package/dist/internal/limits.js.map +1 -0
  59. package/dist/internal/nodes.d.ts +121 -0
  60. package/dist/internal/nodes.d.ts.map +1 -0
  61. package/dist/internal/nodes.js +277 -0
  62. package/dist/internal/nodes.js.map +1 -0
  63. package/dist/internal/patterns.js +110 -0
  64. package/dist/internal/patterns.js.map +1 -0
  65. package/dist/internal/policy-json.js +255 -0
  66. package/dist/internal/policy-json.js.map +1 -0
  67. package/dist/internal/policy-nodes.d.ts +61 -0
  68. package/dist/internal/policy-nodes.d.ts.map +1 -0
  69. package/dist/internal/policy-nodes.js +146 -0
  70. package/dist/internal/policy-nodes.js.map +1 -0
  71. package/dist/internal/policy.d.ts +42 -0
  72. package/dist/internal/policy.d.ts.map +1 -0
  73. package/dist/internal/policy.js +124 -0
  74. package/dist/internal/policy.js.map +1 -0
  75. package/dist/internal/read.js +39 -0
  76. package/dist/internal/read.js.map +1 -0
  77. package/dist/internal/resource.js +115 -0
  78. package/dist/internal/resource.js.map +1 -0
  79. package/dist/internal/rule.d.ts +26 -0
  80. package/dist/internal/rule.d.ts.map +1 -0
  81. package/dist/internal/rule.js +93 -0
  82. package/dist/internal/rule.js.map +1 -0
  83. package/dist/internal/search.d.ts +22 -0
  84. package/dist/internal/search.d.ts.map +1 -0
  85. package/dist/internal/search.js +274 -0
  86. package/dist/internal/search.js.map +1 -0
  87. package/dist/internal/suggest.js +46 -0
  88. package/dist/internal/suggest.js.map +1 -0
  89. package/package.json +3 -4
  90. package/src/Authorizer.ts +67 -583
  91. package/src/Condition.ts +339 -2095
  92. package/src/Decision.ts +6 -5
  93. package/src/Domain.ts +12 -69
  94. package/src/HttpApi.ts +72 -10
  95. package/src/Limits.ts +132 -0
  96. package/src/Policy.ts +386 -2772
  97. package/src/Resource.ts +7 -1548
  98. package/src/index.ts +3 -9
  99. package/src/internal/algebra.ts +482 -0
  100. package/src/internal/analysis.ts +478 -0
  101. package/src/internal/authorizer.ts +418 -0
  102. package/src/internal/bind.ts +596 -0
  103. package/src/internal/catalog.ts +534 -0
  104. package/src/internal/decide.ts +402 -0
  105. package/src/internal/domain.ts +66 -0
  106. package/src/internal/json.ts +260 -0
  107. package/src/internal/limits.ts +78 -0
  108. package/src/internal/nodes.ts +404 -0
  109. package/src/internal/patterns.ts +211 -0
  110. package/src/internal/policy-json.ts +484 -0
  111. package/src/internal/policy-nodes.ts +207 -0
  112. package/src/internal/policy.ts +264 -0
  113. package/src/internal/read.ts +55 -0
  114. package/src/internal/resource.ts +180 -0
  115. package/src/internal/rule.ts +181 -0
  116. package/src/internal/search.ts +541 -0
  117. package/src/internal/suggest.ts +104 -0
  118. package/dist/Principal.d.ts +0 -35
  119. package/dist/Principal.d.ts.map +0 -1
  120. package/dist/Principal.js +0 -37
  121. package/dist/Principal.js.map +0 -1
  122. package/dist/Rule.d.ts +0 -275
  123. package/dist/Rule.d.ts.map +0 -1
  124. package/dist/Rule.js +0 -257
  125. package/dist/Rule.js.map +0 -1
  126. package/src/Principal.ts +0 -41
  127. package/src/Rule.ts +0 -683
package/src/Authorizer.ts CHANGED
@@ -3,19 +3,19 @@
3
3
  * of the current request.
4
4
  *
5
5
  * You make the service class, in the same way as `Context.Service`. Give the
6
- * services that the principal and the decider need as `requires`, and their
7
- * errors as `error`:
6
+ * domain, and the Effect that gives the principal of the current request. The
7
+ * error type and the service type of the checks come from that Effect:
8
8
  *
9
9
  * ```ts
10
- * class Auth extends Authorizer.Service<Auth, { requires: Identity }>()(
11
- * "app/Auth",
12
- * App,
13
- * ) {}
10
+ * class Auth extends Authorizer.Service<Auth>()("app/Auth", {
11
+ * domain: App,
12
+ * principal: Identity,
13
+ * }) {}
14
14
  * ```
15
15
  *
16
- * Then give a layer with {@link layer}. It says where the principal comes
17
- * from and how to load its policy. The policy of each principal is loaded
18
- * once and kept in a cache for a short time.
16
+ * Then give a layer with {@link layer}. It says how to load the policy of a
17
+ * principal. The policy of each principal is loaded once and kept in a cache
18
+ * for a short time.
19
19
  *
20
20
  * Then use the service in a handler:
21
21
  *
@@ -34,26 +34,15 @@
34
34
  * @stability stable
35
35
  * @since 0.3.0
36
36
  */
37
- // The service erases the error type and the service type of predicates,
38
- // principals and deciders to `unknown`. The public types keep them, so the
39
- // language service check for `unknown` in an Effect is off here.
40
- // @effect-diagnostics anyUnknownInErrorContext:off
41
- import * as Arr from "effect/Array"
42
- import * as Cache from "effect/Cache"
43
- import * as Context from "effect/Context"
37
+ import type * as Context from "effect/Context"
44
38
  import type * as Duration from "effect/Duration"
45
- import * as Effect from "effect/Effect"
46
- import * as Equal from "effect/Equal"
47
- import * as Hash from "effect/Hash"
48
- import * as Layer from "effect/Layer"
49
- import * as MutableHashMap from "effect/MutableHashMap"
50
- import * as Option from "effect/Option"
51
- import * as Schema from "effect/Schema"
52
- import * as Condition from "./Condition.ts"
53
- import * as Decision from "./Decision.ts"
54
- import * as Domain from "./Domain.ts"
55
- import * as Policy from "./Policy.ts"
56
- import * as Resource from "./Resource.ts"
39
+ import type * as Effect from "effect/Effect"
40
+ import type * as Layer from "effect/Layer"
41
+ import type * as Condition from "./Condition.ts"
42
+ import type * as Decision from "./Decision.ts"
43
+ import type * as Domain from "./Domain.ts"
44
+ import * as Internal from "./internal/authorizer.ts"
45
+ import type * as Policy from "./Policy.ts"
57
46
 
58
47
  /**
59
48
  * The error when a policy says no.
@@ -62,14 +51,14 @@ import * as Resource from "./Resource.ts"
62
51
  * @category errors
63
52
  * @since 0.3.0
64
53
  */
65
- export class Forbidden extends Schema.TaggedError<Forbidden>(
66
- "@nmnmcc/ability/Authorizer/Forbidden",
67
- )("Forbidden", {
68
- action: Schema.String,
69
- kind: Schema.String,
70
- field: Schema.optionalKey(Schema.String),
71
- reason: Schema.optionalKey(Schema.String),
72
- }) {}
54
+ export const Forbidden: typeof Internal.Forbidden = Internal.Forbidden
55
+
56
+ /**
57
+ * @stability stable
58
+ * @category errors
59
+ * @since 0.3.0
60
+ */
61
+ export type Forbidden = Internal.Forbidden
73
62
 
74
63
  /**
75
64
  * The error when the policy of the principal can not be loaded. The cause is
@@ -79,9 +68,15 @@ export class Forbidden extends Schema.TaggedError<Forbidden>(
79
68
  * @category errors
80
69
  * @since 0.3.0
81
70
  */
82
- export class PolicyUnavailable extends Schema.TaggedError<PolicyUnavailable>(
83
- "@nmnmcc/ability/Authorizer/PolicyUnavailable",
84
- )("PolicyUnavailable", { cause: Schema.Defect() }) {}
71
+ export const PolicyUnavailable: typeof Internal.PolicyUnavailable =
72
+ Internal.PolicyUnavailable
73
+
74
+ /**
75
+ * @stability stable
76
+ * @category errors
77
+ * @since 0.3.0
78
+ */
79
+ export type PolicyUnavailable = Internal.PolicyUnavailable
85
80
 
86
81
  /**
87
82
  * The error when a decider must wait for an answer. `ticket` names the
@@ -91,28 +86,14 @@ export class PolicyUnavailable extends Schema.TaggedError<PolicyUnavailable>(
91
86
  * @category errors
92
87
  * @since 0.3.0
93
88
  */
94
- export class Pending extends Schema.TaggedError<Pending>(
95
- "@nmnmcc/ability/Authorizer/Pending",
96
- )("Pending", {
97
- ticket: Schema.String,
98
- reason: Schema.optionalKey(Schema.String),
99
- }) {}
89
+ export const Pending: typeof Internal.Pending = Internal.Pending
100
90
 
101
91
  /**
102
- * The services and the errors of the principal and the decider of an
103
- * authorizer.
104
- *
105
92
  * @stability stable
106
- * @category models
93
+ * @category errors
107
94
  * @since 0.3.0
108
95
  */
109
- export interface Config {
110
- readonly requires?: unknown
111
- readonly error?: unknown
112
- }
113
-
114
- type RequiresOf<C> = C extends { readonly requires: infer R } ? R : never
115
- type ErrorOf<C> = C extends { readonly error: infer E } ? E : never
96
+ export type Pending = Internal.Pending
116
97
 
117
98
  /**
118
99
  * What a check is about: an act on a resource value, or an act on a kind of
@@ -178,7 +159,7 @@ export interface Authorizer<D extends Domain.Any, E, R> {
178
159
  * the services of the layer. It may have predicates, so it is opaque.
179
160
  */
180
161
  readonly policy: Effect.Effect<
181
- Policy.Policy<D, E, never, Condition.Opaque>,
162
+ Policy.Policy<D, Condition.Opaque<E>>,
182
163
  PolicyUnavailable | E,
183
164
  R
184
165
  >
@@ -210,10 +191,8 @@ export interface Authorizer<D extends Domain.Any, E, R> {
210
191
  target: T,
211
192
  ) => Effect.Effect<
212
193
  Condition.Condition<
213
- E,
214
- never,
215
- Condition.Opaque,
216
- Policy.ResidualReads<D, T["kind"]>
194
+ Policy.ResidualReads<D, T["kind"]>,
195
+ Condition.Opaque<E>
217
196
  >,
218
197
  PolicyUnavailable | E,
219
198
  R
@@ -226,12 +205,7 @@ export interface Authorizer<D extends Domain.Any, E, R> {
226
205
  readonly coverage: <const T extends Policy.KindTarget<D>>(
227
206
  target: T,
228
207
  ) => Effect.Effect<
229
- Policy.Coverage<
230
- E,
231
- never,
232
- Condition.Opaque,
233
- Policy.ResidualReads<D, T["kind"]>
234
- >,
208
+ Policy.Coverage<Policy.ResidualReads<D, T["kind"]>, Condition.Opaque<E>>,
235
209
  PolicyUnavailable | E,
236
210
  R
237
211
  >
@@ -262,6 +236,8 @@ export interface ServiceClass<
262
236
  R,
263
237
  > extends Context.ServiceClass<Self, Id, Authorizer<D, E, R>> {
264
238
  readonly domain: D
239
+ /** The Effect that gives the principal of the current request. */
240
+ readonly principal: Effect.Effect<Domain.Principal<D>, E, R>
265
241
  readonly decide: (
266
242
  target: Policy.Target<D>,
267
243
  ) => Effect.Effect<Decision.Decision, PolicyUnavailable | E, R | Self>
@@ -284,10 +260,8 @@ export interface ServiceClass<
284
260
  target: T,
285
261
  ) => Effect.Effect<
286
262
  Condition.Condition<
287
- E,
288
- never,
289
- Condition.Opaque,
290
- Policy.ResidualReads<D, T["kind"]>
263
+ Policy.ResidualReads<D, T["kind"]>,
264
+ Condition.Opaque<E>
291
265
  >,
292
266
  PolicyUnavailable | E,
293
267
  R | Self
@@ -295,12 +269,7 @@ export interface ServiceClass<
295
269
  readonly coverage: <const T extends Policy.KindTarget<D>>(
296
270
  target: T,
297
271
  ) => Effect.Effect<
298
- Policy.Coverage<
299
- E,
300
- never,
301
- Condition.Opaque,
302
- Policy.ResidualReads<D, T["kind"]>
303
- >,
272
+ Policy.Coverage<Policy.ResidualReads<D, T["kind"]>, Condition.Opaque<E>>,
304
273
  PolicyUnavailable | E,
305
274
  R | Self
306
275
  >
@@ -318,6 +287,7 @@ export interface ServiceClass<
318
287
  */
319
288
  export type AnyServiceClass = Context.Key<any, any> & {
320
289
  readonly domain: Domain.Any
290
+ readonly principal: Effect.Effect<any, any, any>
321
291
  }
322
292
 
323
293
  /**
@@ -327,39 +297,19 @@ export type AnyServiceClass = Context.Key<any, any> & {
327
297
  * @category constructors
328
298
  * @since 0.3.0
329
299
  */
330
- export const Service: <Self, C extends Config = {}>() => <
300
+ export const Service: <Self>() => <
331
301
  const Id extends string,
332
302
  D extends Domain.Any,
303
+ E = never,
304
+ R = never,
333
305
  >(
334
306
  id: Id,
335
- domain: D,
336
- ) => ServiceClass<Self, Id, D, ErrorOf<C>, RequiresOf<C>> = (() =>
337
- (id: string, domain: Domain.Any) => {
338
- const Base = Context.Service<any, any>()(id)
339
- return class extends Base {
340
- static readonly domain = domain
341
- static readonly decide = (target: AnyTarget) =>
342
- this.use((service: any) => service.decide(target))
343
- static readonly check = (target: AnyTarget) =>
344
- this.use((service: any) => service.check(target))
345
- static readonly guard = (target: AnyTarget, options: unknown) =>
346
- this.use((service: any) => service.guard(target, options))
347
- static readonly filter = (target: AnyTarget) =>
348
- this.use((service: any) => service.filter(target))
349
- static readonly coverage = (target: AnyTarget) =>
350
- this.use((service: any) => service.coverage(target))
351
- static readonly permittedFields = (target: AnyTarget) =>
352
- this.use((service: any) => service.permittedFields(target))
353
- }
354
- }) as never
355
-
356
- type AnyTarget = {
357
- readonly action: string
358
- readonly resource?: object
359
- readonly kind?: string
360
- readonly field?: string
361
- readonly within?: unknown
362
- }
307
+ options: {
308
+ readonly domain: D
309
+ /** Gives the principal of the current request. */
310
+ readonly principal: Effect.Effect<Domain.Principal<NoInfer<D>>, E, R>
311
+ },
312
+ ) => ServiceClass<Self, Id, D, E, R> = Internal.Service as never
363
313
 
364
314
  /**
365
315
  * The options of {@link layer}.
@@ -369,8 +319,6 @@ type AnyTarget = {
369
319
  * @since 0.3.0
370
320
  */
371
321
  export interface Options<D extends Domain.Any, E, R, EL, RL, RP> {
372
- /** Gives the principal of the current request. */
373
- readonly principal: Effect.Effect<Domain.Principal<D>, E, R>
374
322
  /**
375
323
  * Loads the policy of a principal. A failure becomes
376
324
  * {@link PolicyUnavailable}. The services of the predicates of the policy
@@ -378,7 +326,7 @@ export interface Options<D extends Domain.Any, E, R, EL, RL, RP> {
378
326
  */
379
327
  readonly policy: (
380
328
  principal: Domain.Principal<D>,
381
- ) => Effect.Effect<Policy.Policy<D, E, RP, any>, EL, RL>
329
+ ) => Effect.Effect<Policy.Policy<D, Condition.Opaque<E, RP>>, EL, RL>
382
330
  /**
383
331
  * Answers when no rule gives an answer. Without a decider, such a request
384
332
  * fails with {@link Forbidden}.
@@ -390,488 +338,24 @@ export interface Options<D extends Domain.Any, E, R, EL, RL, RP> {
390
338
  * How the policies are kept. The default keeps 1000 policies for 30 seconds.
391
339
  * Give `false` to load the policy for each check.
392
340
  *
393
- * Two principals with the same data get one policy from the cache. So the
394
- * loader must decide by the data of the principal only. When it tests that
395
- * an object of the principal is a given object, as `role === admin` does,
396
- * give `false`. A principal with objects in an array, a set or the keys of
397
- * a map is not kept, because such tests are usual there.
341
+ * The key of a principal is the text of its JSON form, from the Schema of
342
+ * the principal of the domain. A principal is kept only when the value that
343
+ * the Schema decodes from that form is `Equal` to it. The loader always
344
+ * gets that decoded copy, also with no cache, so two principals with one
345
+ * key get one policy, and the cache can not change an answer. So the loader
346
+ * must decide by the data of the principal, not by the identity of its
347
+ * objects. A principal with no JSON form is not kept, and the loader gets
348
+ * it as it is. Give `key` to make the key in another way.
398
349
  */
399
350
  readonly cache?:
400
351
  | {
401
352
  readonly capacity?: number
402
353
  readonly timeToLive?: Duration.Input
354
+ readonly key?: (principal: Domain.Principal<D>) => string | undefined
403
355
  }
404
356
  | false
405
357
  }
406
358
 
407
- /**
408
- * A cache key for a principal. Two keys are equal when they have the same
409
- * text, so two principals with the same value share one policy. The key also
410
- * keeps the principal, so the cache can load its policy. A plain `Data` value
411
- * can not do both, so the key gives its own equality with `Equal.Equal`.
412
- */
413
- class PrincipalKey implements Equal.Equal {
414
- readonly principal: object
415
- readonly text: string
416
- constructor(principal: object, text: string) {
417
- this.principal = principal
418
- this.text = text
419
- }
420
- [Equal.symbol](that: unknown): boolean {
421
- return that instanceof PrincipalKey && that.text === this.text
422
- }
423
- [Hash.symbol](): number {
424
- return Hash.string(this.text)
425
- }
426
- }
427
-
428
- /**
429
- * Tests if an array has a plain field at each index below its length. With
430
- * one more own key for `length`, it then has no other own keys.
431
- */
432
- const hasEveryIndex = (value: Array<unknown>): boolean =>
433
- Arr.every(
434
- Array.from({ length: value.length }, (_, index) => index),
435
- (index) => isPlainField(value, String(index)),
436
- )
437
-
438
- /**
439
- * Tests if an own key of an object holds plain data that the text shows: it
440
- * can be seen in a loop over the keys, and it is not a getter.
441
- */
442
- const isPlainField = (value: object, key: string): boolean => {
443
- const descriptor = Object.getOwnPropertyDescriptor(value, key)
444
- return (
445
- descriptor !== undefined &&
446
- descriptor.enumerable === true &&
447
- "value" in descriptor
448
- )
449
- }
450
-
451
- const isObjectLike = (value: unknown): boolean =>
452
- (typeof value === "object" && value !== null) || typeof value === "function"
453
-
454
- /**
455
- * The number of each class that a key has seen. Each layer has its own, so
456
- * no state is kept for the whole program. A class is a function, and `Equal`
457
- * compares functions by the function itself.
458
- */
459
- type ClassIds = MutableHashMap.MutableHashMap<object, number>
460
-
461
- /** Gives a number that is only for this class, in this layer. */
462
- const classId = (classes: ClassIds, constructor: object): string => {
463
- const id = Option.getOrElse(MutableHashMap.get(classes, constructor), () => {
464
- const fresh = MutableHashMap.size(classes)
465
- MutableHashMap.set(classes, constructor, fresh)
466
- return fresh
467
- })
468
- return `#${String(id)}`
469
- }
470
-
471
- /** The most levels of objects in a principal that the cache takes. */
472
- const maxKeyDepth = 256
473
-
474
- /**
475
- * Gives a text that names a principal value. Two values get one text only when
476
- * they have the same types and the same data. So a string `"1"` and a bigint
477
- * `1n` get two texts. The text keeps the class of each object, by a number
478
- * that is its own, because two classes may have one name.
479
- *
480
- * It reads only the fields of plain objects and of `Schema.Class` values, and
481
- * only when each own key is a plain field that a loop over the keys can see.
482
- * An object of another class may keep its data where the text can not see it,
483
- * as a `URL` does. So it gives `undefined` for such an object, unless it is a
484
- * plain `Date`, `URL`, array, `Map` or `Set` with no extra own keys. It also
485
- * gives `undefined` for a function, a symbol, or a value with one object in two
486
- * places. The text can not show that two places hold the same object, and a
487
- * loader may test it, as `set.has(value)` does.
488
- * The private fields (`#name`) of a class can not be seen, so a principal
489
- * class must not keep its data in them.
490
- *
491
- * It also gives `undefined` for a value with more than {@link maxKeyDepth}
492
- * levels of objects. The walk calls itself for each level, so a deeper value
493
- * could fill the stack. Such a principal is not cached.
494
- */
495
- const keyText = (
496
- value: unknown,
497
- seen: Set<object>,
498
- classes: ClassIds,
499
- depth: number = 0,
500
- ): string | undefined => {
501
- const plain = (text: string): string => text
502
- switch (typeof value) {
503
- case "string":
504
- return plain(`s${JSON.stringify(value)}`)
505
- case "number":
506
- return plain(Object.is(value, -0) ? "n-0" : `n${String(value)}`)
507
- case "bigint":
508
- return plain(`b${String(value)}`)
509
- case "boolean":
510
- return plain(value ? "true" : "false")
511
- case "undefined":
512
- return plain("undefined")
513
- case "object":
514
- break
515
- default:
516
- return undefined
517
- }
518
- if (value === null) {
519
- return plain("null")
520
- }
521
- if (seen.has(value) || depth >= maxKeyDepth) {
522
- return undefined
523
- }
524
- seen.add(value)
525
- const list = (
526
- items: ReadonlyArray<unknown>,
527
- wrap: (texts: ReadonlyArray<string>) => string,
528
- ): string | undefined => {
529
- const texts = Arr.map(items, (item) =>
530
- keyText(item, seen, classes, depth + 1),
531
- )
532
- return Arr.every(texts, (text) => text !== undefined)
533
- ? wrap(texts as ReadonlyArray<string>)
534
- : undefined
535
- }
536
- const prototype: unknown = Object.getPrototypeOf(value)
537
- // The own keys of the value. A built-in value keeps its data inside, so it
538
- // may have no own keys, and the elements of an array are its own keys. An
539
- // extra own key is data that the text does not show.
540
- const own = Reflect.ownKeys(value)
541
- // The entries of a map or a set keep their order, because a loader may read
542
- // the first one.
543
- if (prototype === Date.prototype && own.length === 0) {
544
- return `d${String((value as Date).getTime())}`
545
- }
546
- if (prototype === URL.prototype && own.length === 0) {
547
- return `u${JSON.stringify((value as URL).href)}`
548
- }
549
- if (
550
- prototype === Array.prototype &&
551
- own.length === (value as Array<unknown>).length + 1 &&
552
- hasEveryIndex(value as Array<unknown>) &&
553
- // An array finds an object by the object itself, as in
554
- // `roles.includes(admin)`.
555
- !Arr.some(value as Array<unknown>, isObjectLike)
556
- ) {
557
- return list(value as Array<unknown>, (texts) => `[${texts.join(",")}]`)
558
- }
559
- if (
560
- prototype === Map.prototype &&
561
- own.length === 0 &&
562
- // A map finds an object key by the object itself, not by its data, as
563
- // in `map.get(role)`. The text can not show which object it is.
564
- !Arr.some(Array.from((value as Map<unknown, unknown>).keys()), isObjectLike)
565
- ) {
566
- // The keys and the values go in one list, so a value that is an object
567
- // does not go through the test of the elements of an array.
568
- return list(
569
- Arr.flatMap(Array.from(value as Map<unknown, unknown>), ([key, item]) => [
570
- key,
571
- item,
572
- ]),
573
- (texts) => `Map{${texts.join(",")}}`,
574
- )
575
- }
576
- if (
577
- prototype === Set.prototype &&
578
- own.length === 0 &&
579
- // A set finds an object by the object itself, as in `set.has(role)`.
580
- !Arr.some(Array.from(value as Set<unknown>), isObjectLike)
581
- ) {
582
- return list(
583
- Array.from(value as Set<unknown>),
584
- (texts) => `Set{${texts.join(",")}}`,
585
- )
586
- }
587
- if (
588
- (prototype === null ||
589
- prototype === Object.prototype ||
590
- // The prototype must be the prototype of the class itself. A value
591
- // made with `Object.create(user)` has `user` as its prototype, and
592
- // the data of `user` is not in the own keys. Such a prototype may
593
- // have its own `constructor` key, so the test compares the
594
- // prototype with the `prototype` of the class.
595
- (Schema.isSchema((prototype as object).constructor) &&
596
- (prototype as object).constructor.prototype === prototype)) &&
597
- Arr.every(own, (key) => typeof key === "string" && isPlainField(value, key))
598
- ) {
599
- // The keys keep their order, because a loader may read the first one.
600
- const keys = own as ReadonlyArray<string>
601
- const name =
602
- prototype === null
603
- ? ""
604
- : classId(classes, (prototype as object).constructor)
605
- return list(
606
- Arr.map(keys, (key) => (value as Record<string, unknown>)[key]),
607
- (texts) =>
608
- `${name}{${Arr.map(keys, (key, index) => `${JSON.stringify(key)}:${texts[index]!}`).join(",")}}`,
609
- )
610
- }
611
- return undefined
612
- }
613
-
614
- const forbidden = (
615
- target: AnyTarget,
616
- kind: string,
617
- reason: string | undefined,
618
- ): Forbidden =>
619
- Forbidden.make({
620
- action: target.action,
621
- kind,
622
- ...(target.field === undefined ? {} : { field: target.field }),
623
- ...(reason === undefined ? {} : { reason }),
624
- })
625
-
626
- const makeLayer = (
627
- service: AnyServiceClass,
628
- options: {
629
- readonly principal: Effect.Effect<object, unknown, unknown>
630
- readonly policy: (
631
- principal: object,
632
- ) => Effect.Effect<Policy.Any, unknown, unknown>
633
- readonly decider?: (
634
- request: object,
635
- ) => Effect.Effect<Decision.Decision, unknown, unknown>
636
- readonly cache?:
637
- | { readonly capacity?: number; readonly timeToLive?: Duration.Input }
638
- | false
639
- },
640
- ) => {
641
- const domain: Domain.Any = service.domain
642
- // It gives `undefined` when the principal has no key. Such a principal does
643
- // not use the cache.
644
- const classes: ClassIds = MutableHashMap.empty()
645
- const keyOf = (principal: object): PrincipalKey | undefined => {
646
- // One set of seen objects for the walk: the walk adds each object to it,
647
- // and the set goes away when the key is made.
648
- const text = keyText(principal, new Set(), classes)
649
- return text === undefined ? undefined : new PrincipalKey(principal, text)
650
- }
651
- return Layer.effect(
652
- service,
653
- Effect.gen(function* () {
654
- // The services of the policy loader and of the predicates come from the
655
- // layer, so a check does not need them.
656
- const services = yield* Effect.context<never>()
657
-
658
- // The predicates of the policy get the services of the layer, so the
659
- // policy and its residual conditions need no services.
660
- // A policy of another domain may have the same types, but its resources
661
- // may have other paths. So it fails closed, as a policy that can not be
662
- // loaded.
663
- const load = (principal: object) =>
664
- options.policy(principal).pipe(
665
- Effect.provideContext(services),
666
- Effect.filterOrFail(
667
- (value) => value.domain === domain,
668
- () =>
669
- new globalThis.Error(
670
- "The loaded policy is not a policy of this domain",
671
- ),
672
- ),
673
- Effect.mapError((cause) => PolicyUnavailable.make({ cause })),
674
- Effect.map((value) =>
675
- Policy.provideContext(value, services as never),
676
- ),
677
- )
678
-
679
- const cache =
680
- options.cache === false
681
- ? undefined
682
- : yield* Cache.make({
683
- capacity: options.cache?.capacity ?? 1000,
684
- timeToLive: options.cache?.timeToLive ?? "30 seconds",
685
- lookup: (key: PrincipalKey) => load(key.principal),
686
- })
687
-
688
- const policyFor = (principal: object) => {
689
- const key = cache === undefined ? undefined : keyOf(principal)
690
- if (cache === undefined || key === undefined) {
691
- return load(principal)
692
- }
693
- // A failed lookup is kept in the cache. Remove it, so the next check
694
- // tries again.
695
- return Cache.get(cache, key).pipe(
696
- Effect.tapError(() => Cache.invalidate(cache, key)),
697
- )
698
- }
699
-
700
- const withServices = <A, E>(
701
- effect: Effect.Effect<A, E, unknown>,
702
- ): Effect.Effect<A, E, unknown> => Effect.provideContext(effect, services)
703
-
704
- const current = Effect.gen(function* () {
705
- const principal = yield* options.principal
706
- const value = yield* policyFor(principal)
707
- return { principal, policy: value }
708
- })
709
-
710
- // The principal and its policy, loaded one time. A check that asks
711
- // the policy two times, such as `guard`, uses one snapshot for both,
712
- // so the two answers come from one principal.
713
- type Snapshot = Effect.Success<typeof current>
714
-
715
- const decideWith = Effect.fnUntraced(function* (
716
- { principal, policy: value }: Snapshot,
717
- target: AnyTarget,
718
- ) {
719
- const decision: Decision.Decision = yield* withServices(
720
- Policy.decide(value, { ...target, principal } as never),
721
- )
722
- return decision
723
- })
724
-
725
- const decide = (target: AnyTarget) =>
726
- Effect.flatMap(current, (snapshot) => decideWith(snapshot, target))
727
-
728
- const kindDecision = (
729
- value: Policy.Any,
730
- principal: object,
731
- target: AnyTarget,
732
- ): Decision.Decision => {
733
- const request = { ...target, principal } as never
734
- const covered = Policy.coverage(value, request)
735
- if (covered._tag === "All") {
736
- return Decision.Allow({})
737
- }
738
- // The decider may answer for the scope only when no value in it is
739
- // denied, and when no predicate may fail. Else some value may get a
740
- // "no" from the policy, so the answer for the scope is "no". An
741
- // unknown answer may hide a denied value, so it is a "no" too.
742
- // An empty scope never passes, so the decider is not asked for it.
743
- // A scope that the search can not say has a value may be empty, so
744
- // the decider is not asked for it either.
745
- if (
746
- covered._tag !== "Unknown" &&
747
- Policy.scopeHasValue(value, request)
748
- ) {
749
- const deny: Condition.Ast = Policy.residuals(value, request).deny
750
- const schema = Domain.descriptorOf(
751
- value.domain,
752
- target.kind ?? "",
753
- )?.schema
754
- if (
755
- deny._tag === "False" ||
756
- Condition.satisfiable(deny as Condition.Condition, { schema })
757
- ._tag === "Unsatisfiable"
758
- ) {
759
- return Decision.NotApplicable
760
- }
761
- }
762
- return Decision.Deny({})
763
- }
764
-
765
- const checkWith = Effect.fnUntraced(function* (
766
- { principal, policy: value }: Snapshot,
767
- target: AnyTarget,
768
- ) {
769
- const kind =
770
- target.resource === undefined
771
- ? (target.kind ?? "")
772
- : (Resource.kindOf(target.resource) ?? "")
773
- // A request that does not fit its kind gets a "no" at once. The
774
- // decider must not say yes to an action that the kind does not have.
775
- const descriptor = Domain.descriptorOf(domain, kind)
776
- if (descriptor === undefined || !Policy.fitsKind(descriptor, target)) {
777
- return yield* forbidden(target, kind, undefined)
778
- }
779
- const fromPolicy: Decision.Decision =
780
- target.resource === undefined
781
- ? kindDecision(value, principal, target)
782
- : yield* withServices(
783
- Policy.decide(value, { ...target, principal } as never),
784
- )
785
- // When no rule gives an answer, the decider gives it.
786
- const decision: Decision.Decision =
787
- fromPolicy._tag === "NotApplicable" && options.decider !== undefined
788
- ? yield* options.decider({ ...target, principal })
789
- : fromPolicy
790
- if (decision._tag !== "Allow") {
791
- return yield* forbidden(target, kind, Decision.reason(decision))
792
- }
793
- })
794
-
795
- const check = (target: AnyTarget) =>
796
- Effect.flatMap(current, (snapshot) => checkWith(snapshot, target))
797
-
798
- const guard = Effect.fnUntraced(function* (
799
- target: AnyTarget,
800
- guardOptions: {
801
- readonly read?: string
802
- readonly hide: () => unknown
803
- },
804
- ) {
805
- const snapshot = yield* current
806
- const read = guardOptions.read ?? "read"
807
- const { field: _field, ...rest } = target
808
- const visible: Decision.Decision = yield* decideWith(snapshot, {
809
- ...rest,
810
- action: read,
811
- })
812
- if (visible._tag !== "Allow") {
813
- return yield* Effect.fail(guardOptions.hide())
814
- }
815
- if (target.action !== read || target.field !== undefined) {
816
- yield* checkWith(snapshot, target)
817
- }
818
- })
819
-
820
- // The filter is the condition of the coverage. When the coverage is
821
- // not known, it is the residual condition in the scope, which throws
822
- // an error when there is none, so the filter fails.
823
- const filter = (
824
- target: AnyTarget,
825
- ): Effect.Effect<Condition.Ast, unknown, unknown> =>
826
- Effect.map(current, ({ principal, policy: value }): Condition.Ast => {
827
- const request = { ...target, principal } as never
828
- const covered = Policy.coverage(value, request)
829
- return covered._tag === "Unknown"
830
- ? Policy.residual(value, request)
831
- : (covered.condition as Condition.Ast)
832
- })
833
-
834
- const coverage = (
835
- target: AnyTarget,
836
- ): Effect.Effect<Policy.Coverage<any, any, any, any>, unknown, unknown> =>
837
- Effect.map(current, ({ principal, policy: value }) =>
838
- Policy.coverage(value, { ...target, principal } as never),
839
- )
840
-
841
- const permittedFields = Effect.fnUntraced(function* (target: AnyTarget) {
842
- const { principal, policy: value } = yield* current
843
- return yield* withServices(
844
- Policy.permittedFields(value, { ...target, principal } as never),
845
- )
846
- })
847
-
848
- const invalidate = (principal?: object) => {
849
- if (cache === undefined) {
850
- return Effect.void
851
- }
852
- if (principal === undefined) {
853
- return Cache.invalidateAll(cache)
854
- }
855
- const key = keyOf(principal)
856
- return key === undefined ? Effect.void : Cache.invalidate(cache, key)
857
- }
858
-
859
- return {
860
- domain,
861
- principal: options.principal,
862
- policy: Effect.map(current, ({ policy: value }) => value),
863
- decide,
864
- check,
865
- guard,
866
- filter,
867
- coverage,
868
- permittedFields,
869
- invalidate,
870
- } as never
871
- }),
872
- )
873
- }
874
-
875
359
  /**
876
360
  * Makes the layer of an authorizer.
877
361
  *
@@ -891,4 +375,4 @@ export const layer: <
891
375
  >(
892
376
  service: ServiceClass<Self, Id, D, E, R>,
893
377
  options: Options<D, NoInfer<E>, NoInfer<R>, EL, RL, RP>,
894
- ) => Layer.Layer<Self, never, RL | RP> = makeLayer as never
378
+ ) => Layer.Layer<Self, never, RL | RP> = Internal.makeLayer as never