@nmnmcc/ability 0.2.0 → 1.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 (58) hide show
  1. package/README.md +462 -454
  2. package/dist/Authorizer.d.ts +241 -0
  3. package/dist/Authorizer.d.ts.map +1 -0
  4. package/dist/Authorizer.js +340 -0
  5. package/dist/Authorizer.js.map +1 -0
  6. package/dist/Condition.d.ts +915 -0
  7. package/dist/Condition.d.ts.map +1 -0
  8. package/dist/Condition.js +1123 -0
  9. package/dist/Condition.js.map +1 -0
  10. package/dist/Decision.d.ts +131 -0
  11. package/dist/Decision.d.ts.map +1 -0
  12. package/dist/Decision.js +83 -0
  13. package/dist/Decision.js.map +1 -0
  14. package/dist/Domain.d.ts +158 -0
  15. package/dist/Domain.d.ts.map +1 -0
  16. package/dist/Domain.js +76 -0
  17. package/dist/Domain.js.map +1 -0
  18. package/dist/HttpApi.d.ts +173 -0
  19. package/dist/HttpApi.d.ts.map +1 -0
  20. package/dist/HttpApi.js +185 -0
  21. package/dist/HttpApi.js.map +1 -0
  22. package/dist/Policy.d.ts +687 -0
  23. package/dist/Policy.d.ts.map +1 -0
  24. package/dist/Policy.js +1339 -0
  25. package/dist/Policy.js.map +1 -0
  26. package/dist/Principal.d.ts +35 -0
  27. package/dist/Principal.d.ts.map +1 -0
  28. package/dist/Principal.js +37 -0
  29. package/dist/Principal.js.map +1 -0
  30. package/dist/Resource.d.ts +364 -0
  31. package/dist/Resource.d.ts.map +1 -0
  32. package/dist/Resource.js +844 -0
  33. package/dist/Resource.js.map +1 -0
  34. package/dist/Rule.d.ts +275 -0
  35. package/dist/Rule.d.ts.map +1 -0
  36. package/dist/Rule.js +257 -0
  37. package/dist/Rule.js.map +1 -0
  38. package/dist/_virtual/_rolldown/runtime.js +1 -2
  39. package/dist/index.d.ts +9 -3
  40. package/dist/index.js +9 -3
  41. package/package.json +61 -72
  42. package/src/Authorizer.ts +894 -0
  43. package/src/Condition.ts +2997 -0
  44. package/src/Decision.ts +117 -0
  45. package/src/Domain.ts +246 -0
  46. package/src/HttpApi.ts +348 -0
  47. package/src/Policy.ts +3315 -0
  48. package/src/Principal.ts +41 -0
  49. package/src/Resource.ts +1805 -0
  50. package/src/Rule.ts +683 -0
  51. package/src/index.ts +57 -0
  52. package/dist/Ability.d.ts +0 -491
  53. package/dist/Ability.js +0 -207
  54. package/dist/AbilityExtra.d.ts +0 -79
  55. package/dist/AbilityExtra.js +0 -187
  56. package/dist/internal/ability.d.ts +0 -90
  57. package/dist/internal/ability.js +0 -550
  58. package/dist/internal/serializable.js +0 -22
@@ -0,0 +1,894 @@
1
+ /**
2
+ * An authorizer is an Effect service that makes decisions for the principal
3
+ * of the current request.
4
+ *
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`:
8
+ *
9
+ * ```ts
10
+ * class Auth extends Authorizer.Service<Auth, { requires: Identity }>()(
11
+ * "app/Auth",
12
+ * App,
13
+ * ) {}
14
+ * ```
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.
19
+ *
20
+ * Then use the service in a handler:
21
+ *
22
+ * ```ts
23
+ * yield* Auth.check({ action: "update", resource: bot })
24
+ * ```
25
+ *
26
+ * A "no" answer goes into the error channel as {@link Forbidden}. When the
27
+ * policy can not be loaded, the check fails with {@link PolicyUnavailable}.
28
+ * So the check fails closed: an error is never a "yes".
29
+ *
30
+ * When no rule gives an answer, the authorizer asks the decider, if there is
31
+ * one. A decider may answer at once, or fail with {@link Pending} when a person
32
+ * or a Bot must answer later.
33
+ *
34
+ * @stability stable
35
+ * @since 0.3.0
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"
44
+ 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"
57
+
58
+ /**
59
+ * The error when a policy says no.
60
+ *
61
+ * @stability stable
62
+ * @category errors
63
+ * @since 0.3.0
64
+ */
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
+ }) {}
73
+
74
+ /**
75
+ * The error when the policy of the principal can not be loaded. The cause is
76
+ * kept, but it is not shown to the client.
77
+ *
78
+ * @stability stable
79
+ * @category errors
80
+ * @since 0.3.0
81
+ */
82
+ export class PolicyUnavailable extends Schema.TaggedError<PolicyUnavailable>(
83
+ "@nmnmcc/ability/Authorizer/PolicyUnavailable",
84
+ )("PolicyUnavailable", { cause: Schema.Defect() }) {}
85
+
86
+ /**
87
+ * The error when a decider must wait for an answer. `ticket` names the
88
+ * request that waits, so the client can come back for the answer.
89
+ *
90
+ * @stability stable
91
+ * @category errors
92
+ * @since 0.3.0
93
+ */
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
+ }) {}
100
+
101
+ /**
102
+ * The services and the errors of the principal and the decider of an
103
+ * authorizer.
104
+ *
105
+ * @stability stable
106
+ * @category models
107
+ * @since 0.3.0
108
+ */
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
116
+
117
+ /**
118
+ * What a check is about: an act on a resource value, or an act on a kind of
119
+ * resource. A check on a kind passes only when the policy allows the act on
120
+ * every value of the kind. With a scope (`within`), it passes when the policy
121
+ * allows the act on every value in the scope, and the scope is not empty.
122
+ *
123
+ * @stability stable
124
+ * @category models
125
+ * @since 0.3.0
126
+ */
127
+ export type Target<D> = Policy.Target<D> | Policy.KindTarget<D>
128
+
129
+ /**
130
+ * A request to the decider.
131
+ *
132
+ * @stability stable
133
+ * @category models
134
+ * @since 0.3.0
135
+ */
136
+ export type DeciderRequest<D> = Policy.Request<D> | Policy.KindRequest<D>
137
+
138
+ /**
139
+ * The options of a guard.
140
+ *
141
+ * @stability stable
142
+ * @category models
143
+ * @since 0.3.0
144
+ */
145
+ export type GuardOptions<D, E2, K extends string = Domain.Kind<D>> = {
146
+ /** The error when the principal may not see the resource. */
147
+ readonly hide: () => E2
148
+ } & ("read" extends Domain.CommonAction<D, K>
149
+ ? {
150
+ /**
151
+ * The action that lets the principal see the resource. It must be an
152
+ * action of every kind in `K`. The default is `"read"`.
153
+ */
154
+ readonly read?: Domain.CommonAction<D, K>
155
+ }
156
+ : {
157
+ /**
158
+ * The action that lets the principal see the resource. It must be an
159
+ * action of every kind in `K`. Some kind in `K` has no `"read"`
160
+ * action, so you must give it.
161
+ */
162
+ readonly read: Domain.CommonAction<D, K>
163
+ })
164
+
165
+ /**
166
+ * The service.
167
+ *
168
+ * @stability stable
169
+ * @category models
170
+ * @since 0.3.0
171
+ */
172
+ export interface Authorizer<D extends Domain.Any, E, R> {
173
+ readonly domain: D
174
+ /** The principal of the current request. */
175
+ readonly principal: Effect.Effect<Domain.Principal<D>, E, R>
176
+ /**
177
+ * The policy of the principal of the current request. Its predicates have
178
+ * the services of the layer. It may have predicates, so it is opaque.
179
+ */
180
+ readonly policy: Effect.Effect<
181
+ Policy.Policy<D, E, never, Condition.Opaque>,
182
+ PolicyUnavailable | E,
183
+ R
184
+ >
185
+ /** Gives the decision of the policy. It does not ask the decider. */
186
+ readonly decide: (
187
+ target: Policy.Target<D>,
188
+ ) => Effect.Effect<Decision.Decision, PolicyUnavailable | E, R>
189
+ /** Succeeds when the principal may do the act, and fails when it may not. */
190
+ readonly check: (
191
+ target: Target<D>,
192
+ ) => Effect.Effect<void, Forbidden | Pending | PolicyUnavailable | E, R>
193
+ /**
194
+ * Checks the read action first, and fails with the error of `hide` when the
195
+ * principal may not see the resource. Then it checks the act. Use it to
196
+ * answer "not found" to a principal that may not know that a resource
197
+ * exists.
198
+ */
199
+ readonly guard: <const T extends Policy.Target<D>, E2>(
200
+ target: T,
201
+ options: GuardOptions<D, E2, T["resource"]["_tag"]>,
202
+ ) => Effect.Effect<void, E2 | Forbidden | Pending | PolicyUnavailable | E, R>
203
+ /**
204
+ * Gives the condition on resource values that the principal may act on,
205
+ * inside the scope of the target when it has one. Change it into a query
206
+ * with `Condition.fold`. It is `False` when the principal may act on no
207
+ * value, so the list is empty: it is not an error.
208
+ */
209
+ readonly filter: <const T extends Policy.KindTarget<D>>(
210
+ target: T,
211
+ ) => Effect.Effect<
212
+ Condition.Condition<
213
+ E,
214
+ never,
215
+ Condition.Opaque,
216
+ Policy.ResidualReads<D, T["kind"]>
217
+ >,
218
+ PolicyUnavailable | E,
219
+ R
220
+ >
221
+ /**
222
+ * Tells if the principal may act on all, none or some of the values in
223
+ * the scope of the target. See `Policy.coverage`. Use it to show a button
224
+ * only when the answer is not `None`.
225
+ */
226
+ readonly coverage: <const T extends Policy.KindTarget<D>>(
227
+ target: T,
228
+ ) => Effect.Effect<
229
+ Policy.Coverage<
230
+ E,
231
+ never,
232
+ Condition.Opaque,
233
+ Policy.ResidualReads<D, T["kind"]>
234
+ >,
235
+ PolicyUnavailable | E,
236
+ R
237
+ >
238
+ /** Gives the fields of a value that the principal may use. */
239
+ readonly permittedFields: (
240
+ target: Policy.FieldlessTarget<D>,
241
+ ) => Effect.Effect<ReadonlyArray<string>, PolicyUnavailable | E, R>
242
+ /**
243
+ * Removes the cached policy of a principal, or of every principal. Call it
244
+ * after a change to the rules or roles of a principal.
245
+ */
246
+ readonly invalidate: (principal?: Domain.Principal<D>) => Effect.Effect<void>
247
+ }
248
+
249
+ /**
250
+ * The class made by {@link Service}. The static functions use the service
251
+ * from the context.
252
+ *
253
+ * @stability stable
254
+ * @category models
255
+ * @since 0.3.0
256
+ */
257
+ export interface ServiceClass<
258
+ Self,
259
+ Id extends string,
260
+ D extends Domain.Any,
261
+ E,
262
+ R,
263
+ > extends Context.ServiceClass<Self, Id, Authorizer<D, E, R>> {
264
+ readonly domain: D
265
+ readonly decide: (
266
+ target: Policy.Target<D>,
267
+ ) => Effect.Effect<Decision.Decision, PolicyUnavailable | E, R | Self>
268
+ readonly check: (
269
+ target: Target<D>,
270
+ ) => Effect.Effect<
271
+ void,
272
+ Forbidden | Pending | PolicyUnavailable | E,
273
+ R | Self
274
+ >
275
+ readonly guard: <const T extends Policy.Target<D>, E2>(
276
+ target: T,
277
+ options: GuardOptions<D, E2, T["resource"]["_tag"]>,
278
+ ) => Effect.Effect<
279
+ void,
280
+ E2 | Forbidden | Pending | PolicyUnavailable | E,
281
+ R | Self
282
+ >
283
+ readonly filter: <const T extends Policy.KindTarget<D>>(
284
+ target: T,
285
+ ) => Effect.Effect<
286
+ Condition.Condition<
287
+ E,
288
+ never,
289
+ Condition.Opaque,
290
+ Policy.ResidualReads<D, T["kind"]>
291
+ >,
292
+ PolicyUnavailable | E,
293
+ R | Self
294
+ >
295
+ readonly coverage: <const T extends Policy.KindTarget<D>>(
296
+ target: T,
297
+ ) => Effect.Effect<
298
+ Policy.Coverage<
299
+ E,
300
+ never,
301
+ Condition.Opaque,
302
+ Policy.ResidualReads<D, T["kind"]>
303
+ >,
304
+ PolicyUnavailable | E,
305
+ R | Self
306
+ >
307
+ readonly permittedFields: (
308
+ target: Policy.FieldlessTarget<D>,
309
+ ) => Effect.Effect<ReadonlyArray<string>, PolicyUnavailable | E, R | Self>
310
+ }
311
+
312
+ /**
313
+ * Any service class made by {@link Service}.
314
+ *
315
+ * @stability stable
316
+ * @category models
317
+ * @since 0.3.0
318
+ */
319
+ export type AnyServiceClass = Context.Key<any, any> & {
320
+ readonly domain: Domain.Any
321
+ }
322
+
323
+ /**
324
+ * Makes an authorizer service class for a domain.
325
+ *
326
+ * @stability stable
327
+ * @category constructors
328
+ * @since 0.3.0
329
+ */
330
+ export const Service: <Self, C extends Config = {}>() => <
331
+ const Id extends string,
332
+ D extends Domain.Any,
333
+ >(
334
+ 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
+ }
363
+
364
+ /**
365
+ * The options of {@link layer}.
366
+ *
367
+ * @stability stable
368
+ * @category models
369
+ * @since 0.3.0
370
+ */
371
+ 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
+ /**
375
+ * Loads the policy of a principal. A failure becomes
376
+ * {@link PolicyUnavailable}. The services of the predicates of the policy
377
+ * are taken when the layer is built.
378
+ */
379
+ readonly policy: (
380
+ principal: Domain.Principal<D>,
381
+ ) => Effect.Effect<Policy.Policy<D, E, RP, any>, EL, RL>
382
+ /**
383
+ * Answers when no rule gives an answer. Without a decider, such a request
384
+ * fails with {@link Forbidden}.
385
+ */
386
+ readonly decider?: (
387
+ request: DeciderRequest<D>,
388
+ ) => Effect.Effect<Decision.Decision, Pending | E, R>
389
+ /**
390
+ * How the policies are kept. The default keeps 1000 policies for 30 seconds.
391
+ * Give `false` to load the policy for each check.
392
+ *
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.
398
+ */
399
+ readonly cache?:
400
+ | {
401
+ readonly capacity?: number
402
+ readonly timeToLive?: Duration.Input
403
+ }
404
+ | false
405
+ }
406
+
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
+ /**
876
+ * Makes the layer of an authorizer.
877
+ *
878
+ * @stability stable
879
+ * @category layers
880
+ * @since 0.3.0
881
+ */
882
+ export const layer: <
883
+ Self,
884
+ Id extends string,
885
+ D extends Domain.Any,
886
+ E,
887
+ R,
888
+ EL = never,
889
+ RL = never,
890
+ RP = never,
891
+ >(
892
+ service: ServiceClass<Self, Id, D, E, R>,
893
+ options: Options<D, NoInfer<E>, NoInfer<R>, EL, RL, RP>,
894
+ ) => Layer.Layer<Self, never, RL | RP> = makeLayer as never