@nmnmcc/ability 0.2.0 → 0.3.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 (83) hide show
  1. package/README.md +322 -469
  2. package/dist/Authorizer.d.ts +216 -0
  3. package/dist/Authorizer.d.ts.map +1 -0
  4. package/dist/Authorizer.js +55 -0
  5. package/dist/Authorizer.js.map +1 -0
  6. package/dist/Condition.d.ts +536 -0
  7. package/dist/Condition.d.ts.map +1 -0
  8. package/dist/Condition.js +141 -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 +148 -0
  15. package/dist/Domain.d.ts.map +1 -0
  16. package/dist/Domain.js +34 -0
  17. package/dist/Domain.js.map +1 -0
  18. package/dist/HttpApi.d.ts +129 -0
  19. package/dist/HttpApi.d.ts.map +1 -0
  20. package/dist/HttpApi.js +95 -0
  21. package/dist/HttpApi.js.map +1 -0
  22. package/dist/Policy.d.ts +429 -0
  23. package/dist/Policy.d.ts.map +1 -0
  24. package/dist/Policy.js +162 -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 +213 -0
  31. package/dist/Resource.d.ts.map +1 -0
  32. package/dist/Resource.js +71 -0
  33. package/dist/Resource.js.map +1 -0
  34. package/dist/Rule.d.ts +189 -0
  35. package/dist/Rule.d.ts.map +1 -0
  36. package/dist/Rule.js +50 -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/dist/internal/authorizer.d.ts +28 -0
  42. package/dist/internal/authorizer.d.ts.map +1 -0
  43. package/dist/internal/authorizer.js +153 -0
  44. package/dist/internal/authorizer.js.map +1 -0
  45. package/dist/internal/condition.d.ts +11 -0
  46. package/dist/internal/condition.d.ts.map +1 -0
  47. package/dist/internal/condition.js +393 -0
  48. package/dist/internal/condition.js.map +1 -0
  49. package/dist/internal/domain.js +34 -0
  50. package/dist/internal/domain.js.map +1 -0
  51. package/dist/internal/httpApi.js +41 -0
  52. package/dist/internal/httpApi.js.map +1 -0
  53. package/dist/internal/policy.js +524 -0
  54. package/dist/internal/policy.js.map +1 -0
  55. package/dist/internal/resource.js +66 -0
  56. package/dist/internal/resource.js.map +1 -0
  57. package/dist/internal/rule.js +131 -0
  58. package/dist/internal/rule.js.map +1 -0
  59. package/package.json +61 -72
  60. package/src/Authorizer.ts +330 -0
  61. package/src/Condition.ts +706 -0
  62. package/src/Decision.ts +117 -0
  63. package/src/Domain.ts +189 -0
  64. package/src/HttpApi.ts +148 -0
  65. package/src/Policy.ts +575 -0
  66. package/src/Principal.ts +41 -0
  67. package/src/Resource.ts +290 -0
  68. package/src/Rule.ts +265 -0
  69. package/src/index.ts +57 -0
  70. package/src/internal/authorizer.ts +271 -0
  71. package/src/internal/condition.ts +607 -0
  72. package/src/internal/domain.ts +56 -0
  73. package/src/internal/httpApi.ts +84 -0
  74. package/src/internal/policy.ts +815 -0
  75. package/src/internal/resource.ts +98 -0
  76. package/src/internal/rule.ts +203 -0
  77. package/dist/Ability.d.ts +0 -491
  78. package/dist/Ability.js +0 -207
  79. package/dist/AbilityExtra.d.ts +0 -79
  80. package/dist/AbilityExtra.js +0 -187
  81. package/dist/internal/ability.d.ts +0 -90
  82. package/dist/internal/ability.js +0 -550
  83. 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,189 @@
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 type * as Schema from "effect/Schema"
18
+ import * as internal from "./internal/domain.ts"
19
+ import type * as PrincipalSchema from "./Principal.ts"
20
+ import type * as Resource from "./Resource.ts"
21
+
22
+ /**
23
+ * @stability stable
24
+ * @category type IDs
25
+ * @since 0.3.0
26
+ */
27
+ export const TypeId: "~@nmnmcc/ability/Domain" = internal.TypeId
28
+
29
+ /**
30
+ * @stability stable
31
+ * @category type IDs
32
+ * @since 0.3.0
33
+ */
34
+ export type TypeId = typeof TypeId
35
+
36
+ /**
37
+ * A domain. `P` is the type of the principal, `Resources` is the union of the
38
+ * resources, and `Meta` is the type of the extra data that a rule may keep.
39
+ *
40
+ * @stability stable
41
+ * @category models
42
+ * @since 0.3.0
43
+ */
44
+ export interface Domain<
45
+ P extends object,
46
+ Resources extends Resource.Any,
47
+ Meta,
48
+ > {
49
+ readonly [TypeId]: TypeId
50
+ readonly principal: PrincipalSchema.Any
51
+ readonly resources: ReadonlyMap<string, Resource.Descriptor>
52
+ readonly meta: Schema.Top | undefined
53
+ readonly "~Domain"?: {
54
+ readonly Principal: P
55
+ readonly Resources: Resources
56
+ readonly Meta: Meta
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Any domain.
62
+ *
63
+ * @stability stable
64
+ * @category models
65
+ * @since 0.3.0
66
+ */
67
+ export type Any = Domain<any, any, any>
68
+
69
+ /**
70
+ * The type of the principal of a domain.
71
+ *
72
+ * @stability stable
73
+ * @category utility types
74
+ * @since 0.3.0
75
+ */
76
+ export type Principal<D> = D extends Domain<infer P, any, any> ? P : never
77
+
78
+ /**
79
+ * The union of the resources of a domain.
80
+ *
81
+ * @stability stable
82
+ * @category utility types
83
+ * @since 0.3.0
84
+ */
85
+ export type Resources<D> = D extends Domain<any, infer R, any> ? R : never
86
+
87
+ /**
88
+ * The type of the extra data that a rule of the domain may keep.
89
+ *
90
+ * @stability stable
91
+ * @category utility types
92
+ * @since 0.3.0
93
+ */
94
+ export type Meta<D> = D extends Domain<any, any, infer M> ? M : never
95
+
96
+ /**
97
+ * The kind names of a domain.
98
+ *
99
+ * @stability stable
100
+ * @category utility types
101
+ * @since 0.3.0
102
+ */
103
+ export type Kind<D> = Resource.Kind<Resources<D>>
104
+
105
+ /**
106
+ * The resource of a domain with the kind `K`.
107
+ *
108
+ * @stability stable
109
+ * @category utility types
110
+ * @since 0.3.0
111
+ */
112
+ export type ResourceOf<D, K extends string> =
113
+ Resources<D> extends infer R
114
+ ? R extends Resource.Resource<K, any, any, any>
115
+ ? R
116
+ : never
117
+ : never
118
+
119
+ /**
120
+ * The actions of the resource with the kind `K`.
121
+ *
122
+ * @stability stable
123
+ * @category utility types
124
+ * @since 0.3.0
125
+ */
126
+ export type ActionOf<D, K extends string> = Resource.Action<ResourceOf<D, K>>
127
+
128
+ /**
129
+ * All the actions of all the resources of a domain.
130
+ *
131
+ * @stability stable
132
+ * @category utility types
133
+ * @since 0.3.0
134
+ */
135
+ export type AnyAction<D> = Resource.Action<Resources<D>>
136
+
137
+ /**
138
+ * The data type of the resource with the kind `K`.
139
+ *
140
+ * @stability stable
141
+ * @category utility types
142
+ * @since 0.3.0
143
+ */
144
+ export type DataOf<D, K extends string> = Resource.Data<ResourceOf<D, K>>
145
+
146
+ /**
147
+ * The value type of the resource with the kind `K`.
148
+ *
149
+ * @stability stable
150
+ * @category utility types
151
+ * @since 0.3.0
152
+ */
153
+ export type ValueOf<D, K extends string> = Resource.Value<ResourceOf<D, K>>
154
+
155
+ /**
156
+ * The union of the value types of all the resources of a domain.
157
+ *
158
+ * @stability stable
159
+ * @category utility types
160
+ * @since 0.3.0
161
+ */
162
+ export type AnyValue<D> = Resource.Value<Resources<D>>
163
+
164
+ /**
165
+ * Makes a domain. Each resource must have its own kind name. Give `meta` when
166
+ * rules must keep extra data, such as the scope of a grant.
167
+ *
168
+ * @stability stable
169
+ * @category constructors
170
+ * @since 0.3.0
171
+ */
172
+ export const make: <
173
+ P extends PrincipalSchema.Any,
174
+ const Resources extends ReadonlyArray<Resource.Any>,
175
+ M extends Schema.Top = Schema.Never,
176
+ >(options: {
177
+ readonly principal: P
178
+ readonly resources: Resources
179
+ readonly meta?: M
180
+ }) => Domain<P["Type"], Resources[number], M["Type"]> = internal.make
181
+
182
+ /**
183
+ * Tests if a value is a domain.
184
+ *
185
+ * @stability stable
186
+ * @category guards
187
+ * @since 0.3.0
188
+ */
189
+ export const isDomain: (u: unknown) => u is Any = internal.isDomain
package/src/HttpApi.ts ADDED
@@ -0,0 +1,148 @@
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. An endpoint with no annotation is a
8
+ * defect, so a new endpoint is closed until you say what it needs. An empty
9
+ * list means the endpoint is open.
10
+ *
11
+ * ```ts
12
+ * class Authorization extends HttpApiMiddleware.Service<
13
+ * Authorization,
14
+ * { requires: Identity }
15
+ * >()("app/Authorization", { error: HttpApi.errors }) {}
16
+ *
17
+ * const endpoint = HttpApiEndpoint.patch("update", "/bots/:id", { ... })
18
+ * .annotate(HttpApi.Required, [
19
+ * HttpApi.requires(Auth, "update", "Bot", (params) => Bots.find(params.id)),
20
+ * ])
21
+ *
22
+ * const AuthorizationLive = HttpApi.layer(Authorization, Auth)
23
+ * ```
24
+ *
25
+ * This module is a separate entry point, `@nmnmcc/ability/HttpApi`, because
26
+ * `effect/http-api` is not stable yet.
27
+ *
28
+ * @stability unstable
29
+ * @since 0.3.0
30
+ */
31
+ import * as Context from "effect/Context"
32
+ import type * as Effect from "effect/Effect"
33
+ import type * as Layer from "effect/Layer"
34
+ import { HttpApiSchema } from "effect/http-api"
35
+ import * as Authorizer from "./Authorizer.ts"
36
+ import type * as Domain from "./Domain.ts"
37
+ import * as internal from "./internal/httpApi.ts"
38
+
39
+ /**
40
+ * One check that an endpoint needs: an action on a kind of resource. With
41
+ * `load`, the middleware loads the resource value from the path parameters
42
+ * and checks the action on that value. Without `load`, it checks the action
43
+ * on the kind.
44
+ *
45
+ * @stability unstable
46
+ * @category models
47
+ * @since 0.3.0
48
+ */
49
+ export interface Requirement {
50
+ readonly action: string
51
+ readonly kind: string
52
+ readonly load?: (
53
+ params: Readonly<Record<string, string | undefined>>,
54
+ ) => Effect.Effect<object, unknown, unknown>
55
+ }
56
+
57
+ /**
58
+ * The checks that an endpoint needs.
59
+ *
60
+ * @stability unstable
61
+ * @category annotations
62
+ * @since 0.3.0
63
+ */
64
+ export class Required extends Context.Service<
65
+ Required,
66
+ ReadonlyArray<Requirement>
67
+ >()("@nmnmcc/ability/HttpApi/Required") {}
68
+
69
+ /**
70
+ * The checks for every endpoint of a group with no {@link Required}
71
+ * annotation. `read` is for GET requests. `write` is for all other methods.
72
+ *
73
+ * @stability unstable
74
+ * @category annotations
75
+ * @since 0.3.0
76
+ */
77
+ export class GroupRequired extends Context.Service<
78
+ GroupRequired,
79
+ {
80
+ readonly read: ReadonlyArray<Requirement>
81
+ readonly write: ReadonlyArray<Requirement>
82
+ }
83
+ >()("@nmnmcc/ability/HttpApi/GroupRequired") {}
84
+
85
+ /**
86
+ * Makes a {@link Requirement} with the types of an authorizer. The action must
87
+ * be an action of the kind, and `load` must give a value of the kind.
88
+ *
89
+ * `load` may fail. Its errors must be errors of the middleware, and the
90
+ * services that it needs must be given to {@link layer}.
91
+ *
92
+ * @stability unstable
93
+ * @category constructors
94
+ * @since 0.3.0
95
+ */
96
+ export const requires: <
97
+ A extends { readonly domain: Domain.Any },
98
+ const K extends Domain.Kind<A["domain"]>,
99
+ >(
100
+ authorizer: A,
101
+ action: Domain.ActionOf<A["domain"], K>,
102
+ kind: K,
103
+ load?: (
104
+ params: Readonly<Record<string, string | undefined>>,
105
+ ) => Effect.Effect<Domain.ValueOf<A["domain"], K>, unknown, unknown>,
106
+ ) => Requirement = internal.requires
107
+
108
+ /**
109
+ * The errors of the middleware, with HTTP status codes: 403 for
110
+ * `Forbidden`, 503 for `PolicyUnavailable`, and 409 for `Pending`.
111
+ *
112
+ * @stability unstable
113
+ * @category errors
114
+ * @since 0.3.0
115
+ */
116
+ export const errors = [
117
+ Authorizer.Forbidden.pipe(HttpApiSchema.status(403)),
118
+ Authorizer.PolicyUnavailable.pipe(HttpApiSchema.status(503)),
119
+ Authorizer.Pending.pipe(HttpApiSchema.status(409)),
120
+ ] as const
121
+
122
+ /**
123
+ * Makes the layer of an authorization middleware. The middleware reads the
124
+ * {@link Required} and {@link GroupRequired} annotations and checks each
125
+ * requirement with the authorizer.
126
+ *
127
+ * The layer takes the authorizer when it is built. The middleware must
128
+ * require the services of the authorizer, such as the identity of the
129
+ * caller. `R` is the type of the services that the `load` functions need. The
130
+ * layer takes them when it is built too.
131
+ *
132
+ * @stability unstable
133
+ * @category layers
134
+ * @since 0.3.0
135
+ */
136
+ export const layer: <
137
+ Self,
138
+ Id extends string,
139
+ A extends Authorizer.AnyServiceClass,
140
+ R = never,
141
+ >(
142
+ middleware: Context.ServiceClass<Self, Id, any>,
143
+ authorizer: A,
144
+ ) => Layer.Layer<
145
+ Self,
146
+ never,
147
+ (A extends Context.Key<infer I, any> ? I : never) | R
148
+ > = internal.layer as never