@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,117 @@
1
+ /**
2
+ * A decision is the answer of a policy to a request:
3
+ * - `Allow`: a rule lets the principal do the act.
4
+ * - `Deny`: a rule stops the principal.
5
+ * - `NotApplicable`: no rule gives an answer.
6
+ *
7
+ * `NotApplicable` is not the same as `Deny`. A check treats it as a "no", but
8
+ * an `Authorizer` with a decider asks the decider first. See
9
+ * `Authorizer.Options.decider`.
10
+ *
11
+ * @stability stable
12
+ * @since 0.3.0
13
+ */
14
+ import * as Data from "effect/Data"
15
+ import type * as Rule from "./Rule.ts"
16
+
17
+ /**
18
+ * @stability stable
19
+ * @category models
20
+ * @since 0.3.0
21
+ */
22
+ export type Decision = Data.TaggedEnum<{
23
+ Allow: {
24
+ /** The rule that gave the answer, when a rule gave it. */
25
+ readonly rule?: Rule.Any
26
+ }
27
+ Deny: {
28
+ /** The rule that gave the answer, when a rule gave it. */
29
+ readonly rule?: Rule.Any
30
+ readonly reason?: string
31
+ }
32
+ NotApplicable: {}
33
+ }>
34
+
35
+ const {
36
+ $is,
37
+ $match,
38
+ Allow: allow,
39
+ Deny: deny,
40
+ NotApplicable: notApplicable,
41
+ } = Data.taggedEnum<Decision>()
42
+
43
+ /**
44
+ * @stability stable
45
+ * @category constructors
46
+ * @since 0.3.0
47
+ */
48
+ export const Allow: (args: { readonly rule?: Rule.Any }) => Decision = allow
49
+
50
+ /**
51
+ * @stability stable
52
+ * @category constructors
53
+ * @since 0.3.0
54
+ */
55
+ export const Deny: (args: {
56
+ readonly rule?: Rule.Any
57
+ readonly reason?: string
58
+ }) => Decision = deny
59
+
60
+ /**
61
+ * @stability stable
62
+ * @category constructors
63
+ * @since 0.3.0
64
+ */
65
+ export const NotApplicable: Decision = notApplicable()
66
+
67
+ /**
68
+ * @stability stable
69
+ * @category guards
70
+ * @since 0.3.0
71
+ */
72
+ export const isAllow: (
73
+ decision: Decision,
74
+ ) => decision is Extract<Decision, { readonly _tag: "Allow" }> = $is("Allow")
75
+
76
+ /**
77
+ * @stability stable
78
+ * @category guards
79
+ * @since 0.3.0
80
+ */
81
+ export const isDeny: (
82
+ decision: Decision,
83
+ ) => decision is Extract<Decision, { readonly _tag: "Deny" }> = $is("Deny")
84
+
85
+ /**
86
+ * @stability stable
87
+ * @category guards
88
+ * @since 0.3.0
89
+ */
90
+ export const isNotApplicable: (
91
+ decision: Decision,
92
+ ) => decision is Extract<Decision, { readonly _tag: "NotApplicable" }> =
93
+ $is("NotApplicable")
94
+
95
+ /**
96
+ * Matches each kind of decision.
97
+ *
98
+ * @stability stable
99
+ * @category pattern matching
100
+ * @since 0.3.0
101
+ */
102
+ export const match = $match
103
+
104
+ /**
105
+ * The reason of a decision: the reason of a `Deny`, or the reason of the rule
106
+ * that gave the answer.
107
+ *
108
+ * @stability stable
109
+ * @category getters
110
+ * @since 0.3.0
111
+ */
112
+ export const reason = (decision: Decision): string | undefined =>
113
+ decision._tag === "Deny"
114
+ ? (decision.reason ?? decision.rule?.reason)
115
+ : decision._tag === "Allow"
116
+ ? decision.rule?.reason
117
+ : undefined
package/src/Domain.ts ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * A domain puts together the principal and all the resources of an
3
+ * application. All other modules get their types from the domain. A rule may
4
+ * only use the actions of its resource. A condition may only use the fields of
5
+ * its resource and of the principal.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { Domain } from "@nmnmcc/ability"
10
+ *
11
+ * const App = Domain.make({ principal: Caller, resources: [Bot, Trigger] })
12
+ * ```
13
+ *
14
+ * @stability stable
15
+ * @since 0.3.0
16
+ */
17
+ import * as Arr from "effect/Array"
18
+ import * as Data from "effect/Data"
19
+ import * as HashMap from "effect/HashMap"
20
+ import * as Option from "effect/Option"
21
+ import type * as Schema from "effect/Schema"
22
+ import type * as PrincipalSchema from "./Principal.ts"
23
+ import * as Resource from "./Resource.ts"
24
+
25
+ /**
26
+ * A domain. `P` is the type of the principal, `Resources` is the union of the
27
+ * resources, and `Meta` is the type of the extra data that a rule may keep.
28
+ *
29
+ * @stability stable
30
+ * @category models
31
+ * @since 0.3.0
32
+ */
33
+ export interface Domain<
34
+ P extends object,
35
+ Resources extends Resource.Any,
36
+ Meta,
37
+ > {
38
+ readonly principal: PrincipalSchema.Any
39
+ /** The descriptor of each resource, by its kind name. */
40
+ readonly resources: HashMap.HashMap<string, Resource.Descriptor>
41
+ readonly meta: Schema.Top | undefined
42
+ readonly "~Domain"?: {
43
+ readonly Principal: P
44
+ readonly Resources: Resources
45
+ readonly Meta: Meta
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Any domain.
51
+ *
52
+ * @stability stable
53
+ * @category models
54
+ * @since 0.3.0
55
+ */
56
+ export type Any = Domain<any, any, any>
57
+
58
+ /**
59
+ * The type of the principal of a domain.
60
+ *
61
+ * @stability stable
62
+ * @category utility types
63
+ * @since 0.3.0
64
+ */
65
+ export type Principal<D> = D extends Domain<infer P, any, any> ? P : never
66
+
67
+ /**
68
+ * The union of the resources of a domain.
69
+ *
70
+ * @stability stable
71
+ * @category utility types
72
+ * @since 0.3.0
73
+ */
74
+ export type Resources<D> = D extends Domain<any, infer R, any> ? R : never
75
+
76
+ /**
77
+ * The type of the extra data that a rule of the domain may keep.
78
+ *
79
+ * @stability stable
80
+ * @category utility types
81
+ * @since 0.3.0
82
+ */
83
+ export type Meta<D> = D extends Domain<any, any, infer M> ? M : never
84
+
85
+ /**
86
+ * The kind names of a domain.
87
+ *
88
+ * @stability stable
89
+ * @category utility types
90
+ * @since 0.3.0
91
+ */
92
+ export type Kind<D> = Resource.Kind<Resources<D>>
93
+
94
+ /**
95
+ * The resource of a domain with the kind `K`.
96
+ *
97
+ * @stability stable
98
+ * @category utility types
99
+ * @since 0.3.0
100
+ */
101
+ export type ResourceOf<D, K extends string> =
102
+ Resources<D> extends infer R
103
+ ? R extends Resource.Resource<K, any, any, any>
104
+ ? R
105
+ : never
106
+ : never
107
+
108
+ /**
109
+ * The actions of the resource with the kind `K`.
110
+ *
111
+ * @stability stable
112
+ * @category utility types
113
+ * @since 0.3.0
114
+ */
115
+ export type ActionOf<D, K extends string> = Resource.Action<ResourceOf<D, K>>
116
+
117
+ /**
118
+ * The actions that every kind in `K` has. For one kind, they are its actions.
119
+ * For a union of kinds, an action of only some of them is left out, so a
120
+ * value of the union type can not get an action that its kind does not have.
121
+ *
122
+ * @stability stable
123
+ * @category utility types
124
+ * @since 1.0.0
125
+ */
126
+ export type CommonAction<D, K extends string> = (
127
+ K extends unknown ? (action: ActionOf<D, K>) => void : never
128
+ ) extends (action: infer A) => void
129
+ ? A
130
+ : never
131
+
132
+ /**
133
+ * All the actions of all the resources of a domain.
134
+ *
135
+ * @stability stable
136
+ * @category utility types
137
+ * @since 0.3.0
138
+ */
139
+ export type AnyAction<D> = Resource.Action<Resources<D>>
140
+
141
+ /**
142
+ * The data type of the resource with the kind `K`.
143
+ *
144
+ * @stability stable
145
+ * @category utility types
146
+ * @since 0.3.0
147
+ */
148
+ export type DataOf<D, K extends string> = Resource.Data<ResourceOf<D, K>>
149
+
150
+ /**
151
+ * The value type of the resource with the kind `K`.
152
+ *
153
+ * @stability stable
154
+ * @category utility types
155
+ * @since 0.3.0
156
+ */
157
+ export type ValueOf<D, K extends string> = Resource.Value<ResourceOf<D, K>>
158
+
159
+ /**
160
+ * The union of the value types of all the resources of a domain.
161
+ *
162
+ * @stability stable
163
+ * @category utility types
164
+ * @since 0.3.0
165
+ */
166
+ export type AnyValue<D> = Resource.Value<Resources<D>>
167
+
168
+ /**
169
+ * The class of every domain value. {@link isDomain} uses it to know a domain.
170
+ */
171
+ class DomainData extends Data.Class<{
172
+ readonly principal: PrincipalSchema.Any
173
+ readonly resources: HashMap.HashMap<string, Resource.Descriptor>
174
+ readonly meta: Schema.Top | undefined
175
+ }> {}
176
+
177
+ /**
178
+ * Makes a domain. Each resource must have its own kind name. Give `meta` when
179
+ * rules must keep extra data, such as the scope of a grant. The `meta` Schema
180
+ * may not need services, because `Policy.schema` gives a Schema that needs
181
+ * none.
182
+ *
183
+ * @stability stable
184
+ * @category constructors
185
+ * @since 0.3.0
186
+ */
187
+ export const make: <
188
+ P extends PrincipalSchema.Any,
189
+ const Resources extends ReadonlyArray<Resource.Any>,
190
+ M extends Schema.Codec<any, any, never, never> = Schema.Never,
191
+ >(options: {
192
+ readonly principal: P
193
+ readonly resources: Resources
194
+ readonly meta?: M
195
+ }) => Domain<P["Type"], Resources[number], M["Type"]> = (options) => {
196
+ const descriptors = Arr.map(options.resources, Resource.descriptor)
197
+ const kinds = Arr.map(descriptors, (descriptor) => descriptor.kind)
198
+ // Two resources with one kind name is a mistake in the code, not in the
199
+ // data, so it is a defect.
200
+ const twice = Arr.findFirst(
201
+ kinds,
202
+ (kind, index) => kinds.indexOf(kind) !== index,
203
+ )
204
+ if (Option.isSome(twice)) {
205
+ throw new Error(`Two resources have the kind name "${twice.value}"`)
206
+ }
207
+ if (kinds.includes("*")) {
208
+ throw new Error(`A resource may not have the kind name "*"`)
209
+ }
210
+ const resources = HashMap.fromIterable(
211
+ Arr.map(
212
+ descriptors,
213
+ (descriptor) => [descriptor.kind, descriptor] as const,
214
+ ),
215
+ )
216
+ // This checks the field names of the principal. See `Resource.fieldPaths`.
217
+ Resource.fieldPaths(options.principal)
218
+ return new DomainData({
219
+ principal: options.principal,
220
+ resources,
221
+ meta: options.meta,
222
+ })
223
+ }
224
+
225
+ /**
226
+ * Gives the descriptor of the resource with a kind name, or `undefined` when
227
+ * the domain has no such resource.
228
+ *
229
+ * @stability stable
230
+ * @category getters
231
+ * @since 1.0.0
232
+ */
233
+ export const descriptorOf = (
234
+ domain: Any,
235
+ kind: string,
236
+ ): Resource.Descriptor | undefined =>
237
+ Option.getOrUndefined(HashMap.get(domain.resources, kind))
238
+
239
+ /**
240
+ * Tests if a value is a domain.
241
+ *
242
+ * @stability stable
243
+ * @category guards
244
+ * @since 0.3.0
245
+ */
246
+ export const isDomain = (u: unknown): u is Any => u instanceof DomainData
package/src/HttpApi.ts ADDED
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Tools to check authorization in an `effect/http-api` server.
3
+ *
4
+ * Each endpoint says what it needs with the {@link Required} annotation. A
5
+ * group can give a default with {@link GroupRequired}: one list for GET
6
+ * requests and one list for all other methods. A middleware checks the
7
+ * requirements before the handler runs, in each group that uses it. An
8
+ * endpoint with no annotation is a defect, so a new endpoint is closed until
9
+ * you say what it needs. An empty list means the endpoint is open. An endpoint
10
+ * in a group without the middleware is not checked at all.
11
+ *
12
+ * ```ts
13
+ * class Authorization extends HttpApiMiddleware.Service<
14
+ * Authorization,
15
+ * { requires: Identity }
16
+ * >()("app/Authorization", { error: HttpApi.errors }) {}
17
+ *
18
+ * const endpoint = HttpApiEndpoint.patch("update", "/bots/:id", { ... })
19
+ * .annotate(HttpApi.Required, [
20
+ * HttpApi.requires(Auth, "update", "Bot", (params) => Bots.find(params.id)),
21
+ * ])
22
+ *
23
+ * const bots = HttpApiGroup.make("bots").add(endpoint).middleware(Authorization)
24
+ *
25
+ * const AuthorizationLive = HttpApi.layer(Authorization, Auth)
26
+ * ```
27
+ *
28
+ * This module is a separate entry point, `@nmnmcc/ability/HttpApi`, because
29
+ * `effect/http-api` is not stable yet.
30
+ *
31
+ * @stability unstable
32
+ * @since 0.3.0
33
+ */
34
+ // A `load` function of a requirement is kept in an annotation, so its error
35
+ // type and its service type are erased to `unknown`. The language service
36
+ // check for `unknown` in an Effect is off here.
37
+ // @effect-diagnostics anyUnknownInErrorContext:off
38
+ import * as Context from "effect/Context"
39
+ import * as Effect from "effect/Effect"
40
+ import * as Layer from "effect/Layer"
41
+ import * as Option from "effect/Option"
42
+ import * as Schema from "effect/Schema"
43
+ import * as SchemaTransformation from "effect/SchemaTransformation"
44
+ import { HttpRouter } from "effect/http"
45
+ import { HttpApiSchema } from "effect/http-api"
46
+ import * as Authorizer from "./Authorizer.ts"
47
+ import type * as Domain from "./Domain.ts"
48
+ import type * as Policy from "./Policy.ts"
49
+ import * as Resource from "./Resource.ts"
50
+
51
+ /**
52
+ * The path parameters of a request.
53
+ *
54
+ * @stability unstable
55
+ * @category models
56
+ * @since 1.0.0
57
+ */
58
+ export type Params = Readonly<Record<string, string | undefined>>
59
+
60
+ /**
61
+ * One check that an endpoint needs: an action on a kind of resource.
62
+ * - With `load`, the middleware loads the resource value from the path
63
+ * parameters and checks the action on that value.
64
+ * - With `within`, it makes a scope from the path parameters and checks the
65
+ * action on every value in the scope.
66
+ * - With `list`, the endpoint gives a list. The handler gets the list query
67
+ * with `Authorizer.filter`, so the middleware checks nothing, unless
68
+ * `nonEmpty` is true: then it fails when the principal may act on no value
69
+ * in the scope.
70
+ * - With none of them, it checks the action on every value of the kind.
71
+ *
72
+ * @stability unstable
73
+ * @category models
74
+ * @since 0.3.0
75
+ */
76
+ export interface Requirement {
77
+ readonly action: string
78
+ readonly kind: string
79
+ readonly load?: (params: Params) => Effect.Effect<object, unknown, unknown>
80
+ readonly within?: (params: Params) => unknown
81
+ readonly list?: { readonly nonEmpty: boolean }
82
+ }
83
+
84
+ /**
85
+ * The checks that an endpoint needs.
86
+ *
87
+ * @stability unstable
88
+ * @category annotations
89
+ * @since 0.3.0
90
+ */
91
+ export class Required extends Context.Service<
92
+ Required,
93
+ ReadonlyArray<Requirement>
94
+ >()("@nmnmcc/ability/HttpApi/Required") {}
95
+
96
+ /**
97
+ * The checks for every endpoint of a group with no {@link Required}
98
+ * annotation. `read` is for GET requests. `write` is for all other methods.
99
+ *
100
+ * @stability unstable
101
+ * @category annotations
102
+ * @since 0.3.0
103
+ */
104
+ export class GroupRequired extends Context.Service<
105
+ GroupRequired,
106
+ {
107
+ readonly read: ReadonlyArray<Requirement>
108
+ readonly write: ReadonlyArray<Requirement>
109
+ }
110
+ >()("@nmnmcc/ability/HttpApi/GroupRequired") {}
111
+
112
+ /**
113
+ * Makes a {@link Requirement} with the types of an authorizer. The action must
114
+ * be an action of the kind, also when the kind has a union type.
115
+ *
116
+ * The last argument is one of:
117
+ * - a `load` function, which must give a value of the kind. It may fail. Its
118
+ * errors must be errors of the middleware, and the services that it needs
119
+ * must be given to {@link layer}.
120
+ * - `{ within }`, a function from the path parameters to a scope. Use it for
121
+ * an act on a part of the kind, such as a new value with no id yet:
122
+ * `{ within: (params) => ($) => $.under("org", params.org ?? null) }`. A path
123
+ * parameter may be `undefined`, and a scope with `undefined` in it is a
124
+ * defect, so give a value for it.
125
+ *
126
+ * @stability unstable
127
+ * @category constructors
128
+ * @since 0.3.0
129
+ */
130
+ export const requires: <
131
+ A extends { readonly domain: Domain.Any },
132
+ const K extends Domain.Kind<A["domain"]>,
133
+ >(
134
+ authorizer: A,
135
+ action: Domain.CommonAction<A["domain"], K>,
136
+ kind: K,
137
+ how?:
138
+ | ((
139
+ params: Params,
140
+ ) => Effect.Effect<Domain.ValueOf<A["domain"], K>, unknown, unknown>)
141
+ | { readonly within: (params: Params) => Policy.Scope<A["domain"], K> },
142
+ ) => Requirement = (_authorizer, action, kind, how) =>
143
+ how === undefined
144
+ ? { action: action as string, kind }
145
+ : typeof how === "function"
146
+ ? { action: action as string, kind, load: how }
147
+ : { action: action as string, kind, within: how.within }
148
+
149
+ /**
150
+ * Makes a {@link Requirement} for an endpoint that gives a list. The middleware
151
+ * does not check that the principal may act on every value, because the list
152
+ * shows only the values that it may act on. The handler gets the list query
153
+ * with `Authorizer.filter`, with the same scope.
154
+ *
155
+ * When the principal may act on no value, the list is empty: that is not an
156
+ * error, so a client does not learn that hidden values are there. With
157
+ * `nonEmpty: true`, the middleware fails with `Forbidden` in that case. It
158
+ * also fails when the coverage is `Unknown`, because then it can not say that
159
+ * the list has a value.
160
+ *
161
+ * @stability unstable
162
+ * @category constructors
163
+ * @since 1.0.0
164
+ */
165
+ export const lists: <
166
+ A extends { readonly domain: Domain.Any },
167
+ const K extends Domain.Kind<A["domain"]>,
168
+ >(
169
+ authorizer: A,
170
+ action: Domain.CommonAction<A["domain"], K>,
171
+ kind: K,
172
+ options?: {
173
+ readonly within?: (params: Params) => Policy.Scope<A["domain"], K>
174
+ readonly nonEmpty?: boolean
175
+ },
176
+ ) => Requirement = (_authorizer, action, kind, options) => ({
177
+ action: action as string,
178
+ kind,
179
+ list: { nonEmpty: options?.nonEmpty ?? false },
180
+ ...(options?.within === undefined ? {} : { within: options.within }),
181
+ })
182
+
183
+ /**
184
+ * The Schema of a `PolicyUnavailable` response. It encodes only the `_tag`, so
185
+ * the cause does not go to the client.
186
+ */
187
+ const PolicyUnavailableResponse = Schema.TaggedStruct(
188
+ "PolicyUnavailable",
189
+ {},
190
+ ).pipe(
191
+ Schema.decodeTo(
192
+ Schema.declare(
193
+ (u): u is Authorizer.PolicyUnavailable =>
194
+ u instanceof Authorizer.PolicyUnavailable,
195
+ ),
196
+ SchemaTransformation.transform({
197
+ decode: () => Authorizer.PolicyUnavailable.make({ cause: undefined }),
198
+ encode: () => ({ _tag: "PolicyUnavailable" as const }),
199
+ }),
200
+ ),
201
+ )
202
+
203
+ /**
204
+ * The errors of the middleware, with HTTP status codes: 403 for
205
+ * `Forbidden`, 503 for `PolicyUnavailable`, and 409 for `Pending`.
206
+ *
207
+ * The body of a `PolicyUnavailable` response has only its `_tag`. The cause
208
+ * may have private details, such as an error from the database, so it stays
209
+ * on the server.
210
+ *
211
+ * @stability unstable
212
+ * @category errors
213
+ * @since 0.3.0
214
+ */
215
+ export const errors = [
216
+ Authorizer.Forbidden.pipe(HttpApiSchema.status(403)),
217
+ PolicyUnavailableResponse.pipe(HttpApiSchema.status(503)),
218
+ Authorizer.Pending.pipe(HttpApiSchema.status(409)),
219
+ ] as const
220
+
221
+ const makeLayer = (
222
+ middleware: Context.ServiceClass<any, string, any>,
223
+ authorizer: Authorizer.AnyServiceClass,
224
+ ) =>
225
+ Layer.effect(
226
+ middleware,
227
+ Effect.gen(function* () {
228
+ const service = yield* authorizer
229
+ const services = yield* Effect.context<never>()
230
+ return Effect.fnUntraced(function* (
231
+ httpEffect: Effect.Effect<unknown, unknown, unknown>,
232
+ options: {
233
+ readonly endpoint: {
234
+ readonly name: string
235
+ readonly method: string
236
+ readonly annotations: Context.Context<never>
237
+ }
238
+ readonly group: {
239
+ readonly identifier: string
240
+ readonly annotations: Context.Context<never>
241
+ }
242
+ },
243
+ ) {
244
+ const requirements = Option.orElse(
245
+ Context.getOption(options.endpoint.annotations, Required),
246
+ () =>
247
+ Option.map(
248
+ Context.getOption(options.group.annotations, GroupRequired),
249
+ (rules) =>
250
+ options.endpoint.method === "GET" ? rules.read : rules.write,
251
+ ),
252
+ )
253
+ if (Option.isNone(requirements)) {
254
+ return yield* Effect.die(
255
+ new Error(
256
+ `The endpoint ${options.group.identifier}.${options.endpoint.name} does not say what it needs. Give it the HttpApi.Required annotation.`,
257
+ ),
258
+ )
259
+ }
260
+ const endpoint = `${options.group.identifier}.${options.endpoint.name}`
261
+ // The requirements are checked in order. The first one that fails
262
+ // stops the request.
263
+ yield* Effect.forEach(
264
+ requirements.value,
265
+ ({ action, kind, load, within, list }) => {
266
+ if (load !== undefined) {
267
+ return HttpRouter.params.pipe(
268
+ Effect.flatMap((params) =>
269
+ Effect.provideContext(load(params), services),
270
+ ),
271
+ // The check is for the kind of the requirement. A value of
272
+ // another kind is a mistake in `load`, and the check must
273
+ // not pass with the rules of that other kind.
274
+ Effect.filterOrElse(
275
+ (value) => Resource.kindOf(value) === kind,
276
+ () =>
277
+ Effect.die(
278
+ new Error(
279
+ `The load function of ${endpoint} gave a value that is not a ${kind}`,
280
+ ),
281
+ ),
282
+ ),
283
+ Effect.flatMap((value) =>
284
+ service.check({ action, resource: value } as never),
285
+ ),
286
+ )
287
+ }
288
+ const target =
289
+ within === undefined
290
+ ? Effect.succeed({ action, kind } as never)
291
+ : Effect.map(
292
+ HttpRouter.params,
293
+ (params) =>
294
+ ({ action, kind, within: within(params) }) as never,
295
+ )
296
+ if (list === undefined) {
297
+ return Effect.flatMap(target, (value) => service.check(value))
298
+ }
299
+ // A list endpoint may give an empty list, unless it must not.
300
+ // Only `All` and `Some` say that the list has a value. `Unknown`
301
+ // is not a "yes".
302
+ return list.nonEmpty
303
+ ? target.pipe(
304
+ Effect.flatMap((value) => service.coverage(value)),
305
+ Effect.filterOrElse(
306
+ (covered) =>
307
+ (covered as { readonly _tag: string })._tag === "All" ||
308
+ (covered as { readonly _tag: string })._tag === "Some",
309
+ () =>
310
+ Effect.fail(Authorizer.Forbidden.make({ action, kind })),
311
+ ),
312
+ )
313
+ : Effect.void
314
+ },
315
+ { discard: true },
316
+ )
317
+ return yield* httpEffect
318
+ })
319
+ }),
320
+ )
321
+
322
+ /**
323
+ * Makes the layer of an authorization middleware. The middleware reads the
324
+ * {@link Required} and {@link GroupRequired} annotations and checks each
325
+ * requirement with the authorizer.
326
+ *
327
+ * The layer takes the authorizer when it is built. The middleware must
328
+ * require the services of the authorizer, such as the identity of the
329
+ * caller. `R` is the type of the services that the `load` functions need. The
330
+ * layer takes them when it is built too.
331
+ *
332
+ * @stability unstable
333
+ * @category layers
334
+ * @since 0.3.0
335
+ */
336
+ export const layer: <
337
+ Self,
338
+ Id extends string,
339
+ A extends Authorizer.AnyServiceClass,
340
+ R = never,
341
+ >(
342
+ middleware: Context.ServiceClass<Self, Id, any>,
343
+ authorizer: A,
344
+ ) => Layer.Layer<
345
+ Self,
346
+ never,
347
+ (A extends Context.Key<infer I, any> ? I : never) | R
348
+ > = makeLayer as never