@nmnmcc/ability 1.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +276 -269
  26. package/dist/Policy.d.ts.map +1 -1
  27. package/dist/Policy.js +73 -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 +311 -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 +265 -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 +103 -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 +391 -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 +648 -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 +487 -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 +196 -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
@@ -0,0 +1,402 @@
1
+ /**
2
+ * The decision walk of a policy: `decide`, `explain` and `permittedFields`.
3
+ * The types are in `Policy.ts`.
4
+ */
5
+ // A policy keeps the error type and the service type of its predicates in the
6
+ // public types. The code below works on the nodes with no types, where they
7
+ // are `unknown`, so the language service check for `unknown` in an Effect is
8
+ // off here.
9
+ // @effect-diagnostics anyUnknownInErrorContext:off
10
+ import * as Arr from "effect/Array"
11
+ import * as Chunk from "effect/Chunk"
12
+ import * as Effect from "effect/Effect"
13
+ import * as Option from "effect/Option"
14
+ import * as Ref from "effect/Ref"
15
+ import * as Condition from "../Condition.ts"
16
+ import * as Decision from "../Decision.ts"
17
+ import type * as Domain from "../Domain.ts"
18
+ import type * as Policy from "../Policy.ts"
19
+ import type * as Resource from "../Resource.ts"
20
+ import * as Catalog from "./catalog.ts"
21
+ import * as DomainData from "./domain.ts"
22
+ import type * as PolicyNodes from "./policy-nodes.ts"
23
+ import { nodeOf } from "./policy.ts"
24
+ import * as ResourceData from "./resource.ts"
25
+ import * as RuleData from "./rule.ts"
26
+
27
+ type Any = Policy.Any
28
+ type AnyRequest = Policy.AnyRequest
29
+ type AnyRule = Policy.AnyRule
30
+ type Explanation = Policy.Explanation
31
+ type Step = Policy.Step
32
+
33
+ interface DecideContext {
34
+ readonly action: string
35
+ readonly kind: string
36
+ readonly field: string | undefined
37
+ readonly input: Condition.Input
38
+ /** The resource of the kind, for the fields that a rule may grant. */
39
+ readonly descriptor: Resource.Descriptor
40
+ /** The rules that were tested, for {@link explain}. */
41
+ readonly trace: Ref.Ref<Chunk.Chunk<Step>> | undefined
42
+ }
43
+
44
+ const ruleApplies = (
45
+ value: RuleData.Rule,
46
+ context: DecideContext,
47
+ path: ReadonlyArray<number>,
48
+ ): Effect.Effect<boolean, unknown, unknown> => {
49
+ if (!RuleData.matchesTarget(value, context)) {
50
+ return Effect.succeed(false)
51
+ }
52
+ const test =
53
+ value.when === undefined
54
+ ? Effect.succeed(true)
55
+ : // A rule of a node keeps no type of what it reads. The decision gives
56
+ // it the value and the principal of the request.
57
+ Condition.evaluate(value.when as Condition.Any, context.input)
58
+ const trace = context.trace
59
+ return trace === undefined
60
+ ? test
61
+ : Effect.tap(test, (applies) =>
62
+ // A chunk adds a step with no copy of the steps before.
63
+ Ref.update(trace, Chunk.append({ path, rule: typed(value), applies })),
64
+ )
65
+ }
66
+
67
+ /** A rule of a node, with the type of a rule of no domain. */
68
+ const typed = (value: RuleData.Rule): AnyRule => value as never
69
+
70
+ const fromRule = (value: RuleData.Rule): Decision.Decision =>
71
+ value._tag === "Allow"
72
+ ? Decision.Allow({ rule: typed(value) })
73
+ : value.reason === undefined
74
+ ? Decision.Deny({ rule: typed(value) })
75
+ : Decision.Deny({ rule: typed(value), reason: value.reason })
76
+
77
+ /** Gives the first rule that applies, in order. */
78
+ const firstApplying = (
79
+ candidates: ReadonlyArray<readonly [RuleData.Rule, number]>,
80
+ context: DecideContext,
81
+ path: ReadonlyArray<number>,
82
+ ): Effect.Effect<Option.Option<RuleData.Rule>, unknown, unknown> =>
83
+ Effect.map(
84
+ Effect.findFirst(candidates, ([value, index]) =>
85
+ ruleApplies(value, context, [...path, index]),
86
+ ),
87
+ Option.map(([value]) => value),
88
+ )
89
+
90
+ /** Gives the first result that is found, and tests `next` only when none is. */
91
+ const orElse = <A, E, R>(
92
+ first: Effect.Effect<Option.Option<A>, E, R>,
93
+ next: () => Effect.Effect<Option.Option<A>, E, R>,
94
+ ): Effect.Effect<Option.Option<A>, E, R> =>
95
+ Effect.filterOrElse(first, Option.isSome, next)
96
+
97
+ const decideRules = (
98
+ node: PolicyNodes.Rules,
99
+ context: DecideContext,
100
+ path: ReadonlyArray<number>,
101
+ ): Effect.Effect<Decision.Decision, unknown, unknown> => {
102
+ const indexed = Arr.map(node.rules, (value, index) => [value, index] as const)
103
+ const denies = Arr.filter(indexed, ([value]) => value._tag === "Deny")
104
+ const allows = Arr.filter(indexed, ([value]) => value._tag === "Allow")
105
+ const first = (candidates: ReadonlyArray<readonly [RuleData.Rule, number]>) =>
106
+ firstApplying(candidates, context, path)
107
+ const found = (() => {
108
+ switch (node.algorithm) {
109
+ case "denyOverrides":
110
+ return orElse(first(denies), () => first(allows))
111
+ case "permitOverrides":
112
+ return orElse(first(allows), () => first(denies))
113
+ case "firstApplicable":
114
+ return first(indexed)
115
+ case "lastApplicable":
116
+ return first(Arr.reverse(indexed))
117
+ }
118
+ })()
119
+ return Effect.map(
120
+ found,
121
+ Option.match({ onNone: () => Decision.NotApplicable, onSome: fromRule }),
122
+ )
123
+ }
124
+
125
+ /** The next step of a walk over children: stop with a decision, or go on. */
126
+ type WalkStep<S> =
127
+ | { readonly _tag: "Stop"; readonly decision: Decision.Decision }
128
+ | { readonly _tag: "Next"; readonly state: S }
129
+
130
+ /**
131
+ * Decides the children in order. After each child, `step` stops with a
132
+ * decision, or gives the state for the next child. After the last child,
133
+ * `end` gives the decision. A child after a stop is not decided, so its
134
+ * predicates do not run.
135
+ */
136
+ const walkChildren = <S>(
137
+ children: ReadonlyArray<PolicyNodes.Node>,
138
+ context: DecideContext,
139
+ path: ReadonlyArray<number>,
140
+ start: S,
141
+ step: (state: S, decision: Decision.Decision) => WalkStep<S>,
142
+ end: (state: S) => Decision.Decision,
143
+ ): Effect.Effect<Decision.Decision, unknown, unknown> => {
144
+ const go = (
145
+ index: number,
146
+ state: S,
147
+ ): Effect.Effect<Decision.Decision, unknown, unknown> =>
148
+ index === children.length
149
+ ? Effect.succeed(end(state))
150
+ : Effect.flatMap(
151
+ decideNode(children[index]!, context, [...path, index]),
152
+ (decision) => {
153
+ const next = step(state, decision)
154
+ return next._tag === "Stop"
155
+ ? Effect.succeed(next.decision)
156
+ : go(index + 1, next.state)
157
+ },
158
+ )
159
+ return go(0, start)
160
+ }
161
+
162
+ const stop = (decision: Decision.Decision): WalkStep<never> => ({
163
+ _tag: "Stop",
164
+ decision,
165
+ })
166
+
167
+ const next = <S>(state: S): WalkStep<S> => ({ _tag: "Next", state })
168
+
169
+ /**
170
+ * Decides a node. A shared child is decided again for each place, so its
171
+ * predicates run each time, as the policy says. The limits of the nodes keep
172
+ * this work small.
173
+ */
174
+ export const decideNode = (
175
+ node: PolicyNodes.Node,
176
+ context: DecideContext,
177
+ path: ReadonlyArray<number>,
178
+ ): Effect.Effect<Decision.Decision, unknown, unknown> => {
179
+ switch (node._tag) {
180
+ case "Rules":
181
+ return decideRules(node, context, path)
182
+ case "All":
183
+ // One deny is enough to deny. Every child must allow.
184
+ return walkChildren(
185
+ node.policies,
186
+ context,
187
+ path,
188
+ {
189
+ firstAllow: Option.none<Decision.Decision>(),
190
+ allAllow: node.policies.length > 0,
191
+ },
192
+ (state, decision) =>
193
+ decision._tag === "Deny"
194
+ ? stop(decision)
195
+ : next({
196
+ firstAllow: Option.orElse(state.firstAllow, () =>
197
+ decision._tag === "Allow"
198
+ ? Option.some(decision)
199
+ : Option.none(),
200
+ ),
201
+ allAllow: state.allAllow && decision._tag === "Allow",
202
+ }),
203
+ (state) =>
204
+ state.allAllow
205
+ ? Option.getOrElse(state.firstAllow, () => Decision.NotApplicable)
206
+ : Decision.NotApplicable,
207
+ )
208
+ case "Any":
209
+ // One allow is enough. Without one, the first deny denies.
210
+ return walkChildren(
211
+ node.policies,
212
+ context,
213
+ path,
214
+ Option.none<Decision.Decision>(),
215
+ (firstDeny, decision) =>
216
+ decision._tag === "Allow"
217
+ ? stop(decision)
218
+ : next(
219
+ Option.orElse(firstDeny, () =>
220
+ decision._tag === "Deny"
221
+ ? Option.some(decision)
222
+ : Option.none(),
223
+ ),
224
+ ),
225
+ (firstDeny) =>
226
+ Option.getOrElse(firstDeny, () => Decision.NotApplicable),
227
+ )
228
+ case "FirstOf":
229
+ return walkChildren(
230
+ node.policies,
231
+ context,
232
+ path,
233
+ undefined,
234
+ (_, decision) =>
235
+ decision._tag === "NotApplicable" ? next(undefined) : stop(decision),
236
+ () => Decision.NotApplicable,
237
+ )
238
+ case "Ceiling":
239
+ // A deny of the cap wins. Else the policy decides.
240
+ return Effect.filterOrElse(
241
+ decideNode(node.cap, context, [...path, 0]),
242
+ (capped) => capped._tag === "Deny",
243
+ () => decideNode(node.policy, context, [...path, 1]),
244
+ )
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Tests if a request fits its kind: the action is an action of the kind, and
250
+ * the field, if there is one, is a field of the kind.
251
+ */
252
+ export const fitsKind = (
253
+ descriptor: Resource.Descriptor,
254
+ request: { readonly action: string; readonly field?: string | undefined },
255
+ ): boolean =>
256
+ descriptor.actions.includes(request.action) &&
257
+ (request.field === undefined ||
258
+ (Catalog.lookup(descriptor.catalog, request.field)?.listed ?? false))
259
+
260
+ const makeContext = (
261
+ domain: Domain.Any,
262
+ request: AnyRequest,
263
+ trace: Ref.Ref<Chunk.Chunk<Step>> | undefined,
264
+ ): DecideContext | undefined => {
265
+ const kind = ResourceData.kindOf(request.resource)
266
+ const descriptor =
267
+ kind === undefined ? undefined : DomainData.descriptorOf(domain, kind)
268
+ if (
269
+ kind === undefined ||
270
+ descriptor === undefined ||
271
+ !fitsKind(descriptor, request)
272
+ ) {
273
+ return undefined
274
+ }
275
+ return {
276
+ action: request.action,
277
+ kind,
278
+ field: request.field,
279
+ input: {
280
+ value: request.resource,
281
+ principal: request.principal,
282
+ path: descriptor.path,
283
+ },
284
+ descriptor,
285
+ trace,
286
+ }
287
+ }
288
+
289
+ export const decideAny = (
290
+ policy: Any,
291
+ request: AnyRequest,
292
+ ): Effect.Effect<Decision.Decision, unknown, unknown> =>
293
+ // The context reads the resource value, so it is made for each run.
294
+ Effect.suspend(() => {
295
+ const context = makeContext(policy.domain, request, undefined)
296
+ return context === undefined
297
+ ? Effect.succeed(Decision.NotApplicable)
298
+ : decideNode(nodeOf(policy), context, [])
299
+ })
300
+
301
+ /** Gives the decision of a policy with no predicates, at once. */
302
+ export const decideSync = (
303
+ policy: Any,
304
+ request: AnyRequest,
305
+ ): Decision.Decision =>
306
+ Effect.runSync(
307
+ // The public type of `decideSync` takes only policies with no
308
+ // predicates, so this Effect has no error and needs no services.
309
+ // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off
310
+ decideAny(policy, request) as Effect.Effect<Decision.Decision>,
311
+ )
312
+
313
+ /** Gives the decision of a policy and the rules that were tested to make it. */
314
+ export const explain = (
315
+ policy: Any,
316
+ request: AnyRequest,
317
+ ): Effect.Effect<Explanation, unknown, unknown> =>
318
+ // Each run of the Effect needs its own trace, so the trace is made inside
319
+ // the Effect.
320
+ Effect.gen(function* () {
321
+ const trace = yield* Ref.make(Chunk.empty<Step>())
322
+ const context = makeContext(policy.domain, request, trace)
323
+ if (context === undefined) {
324
+ return { decision: Decision.NotApplicable, steps: [] }
325
+ }
326
+ const decision = yield* decideNode(nodeOf(policy), context, [])
327
+ return { decision, steps: Chunk.toReadonlyArray(yield* Ref.get(trace)) }
328
+ })
329
+
330
+ /**
331
+ * Tests if a deny rule of the request names a part inside `field`.
332
+ */
333
+ const deniesInside = (
334
+ node: PolicyNodes.Node,
335
+ target: { readonly action: string; readonly kind: string },
336
+ field: string,
337
+ ): boolean => {
338
+ switch (node._tag) {
339
+ case "Rules":
340
+ return node.rules.some(
341
+ (value) =>
342
+ value._tag === "Deny" &&
343
+ value.fields !== undefined &&
344
+ value.fields.some((pattern) =>
345
+ RuleData.matchesInside(pattern, field),
346
+ ) &&
347
+ (value.actions.includes("*") ||
348
+ value.actions.includes(target.action)) &&
349
+ (value.resources.includes("*") ||
350
+ value.resources.includes(target.kind)),
351
+ )
352
+ case "All":
353
+ case "Any":
354
+ case "FirstOf":
355
+ return node.policies.some((child) => deniesInside(child, target, field))
356
+ case "Ceiling":
357
+ return (
358
+ deniesInside(node.cap, target, field) ||
359
+ deniesInside(node.policy, target, field)
360
+ )
361
+ }
362
+ }
363
+
364
+ /** Gives the fields of a resource value that the policy lets a request use. */
365
+ export const permittedFields = (
366
+ policy: Any,
367
+ request: Omit<AnyRequest, "field">,
368
+ ): Effect.Effect<ReadonlyArray<string>, unknown, unknown> => {
369
+ const kind = ResourceData.kindOf(request.resource)
370
+ const descriptor: Resource.Descriptor | undefined =
371
+ kind === undefined
372
+ ? undefined
373
+ : DomainData.descriptorOf(policy.domain, kind)
374
+ if (descriptor === undefined) {
375
+ return Effect.succeed([])
376
+ }
377
+ // The fields with names only: a list can not name every key of a record.
378
+ const listed = Arr.filter(
379
+ descriptor.catalog.fields,
380
+ (field) =>
381
+ field.listed &&
382
+ Arr.every(field.path, (segment) => typeof segment === "string"),
383
+ )
384
+ return Effect.map(
385
+ Effect.filter(listed, (field) =>
386
+ Effect.map(
387
+ decideAny(policy, { ...request, field: field.text }),
388
+ (result) =>
389
+ result._tag === "Allow" &&
390
+ !(
391
+ field.open &&
392
+ deniesInside(
393
+ nodeOf(policy),
394
+ { ...request, kind: descriptor.kind },
395
+ field.text,
396
+ )
397
+ ),
398
+ ),
399
+ ),
400
+ Arr.map((field) => field.text),
401
+ )
402
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The values of domains. The types are in `Domain.ts`.
3
+ */
4
+ import * as Arr from "effect/Array"
5
+ import * as Data from "effect/Data"
6
+ import * as HashMap from "effect/HashMap"
7
+ import * as Option from "effect/Option"
8
+ import type * as Schema from "effect/Schema"
9
+ import type * as Domain from "../Domain.ts"
10
+ import type * as Resource from "../Resource.ts"
11
+ import * as Catalog from "./catalog.ts"
12
+ import * as ResourceData from "./resource.ts"
13
+
14
+ /** The class of every domain value. `isDomain` uses it to know a domain. */
15
+ class DomainData extends Data.Class<{
16
+ readonly principal: Schema.Top
17
+ readonly principalCatalog: Catalog.Catalog
18
+ readonly resources: HashMap.HashMap<string, Resource.Descriptor>
19
+ readonly meta: Schema.Top | undefined
20
+ }> {}
21
+
22
+ /** Makes a domain. Each resource must have its own kind name. */
23
+ export const make = (options: {
24
+ readonly principal: Schema.Top
25
+ readonly resources: ReadonlyArray<Resource.Any>
26
+ readonly meta?: Schema.Top
27
+ }): DomainData => {
28
+ const descriptors = Arr.map(options.resources, ResourceData.descriptor)
29
+ const kinds = Arr.map(descriptors, (descriptor) => descriptor.kind)
30
+ // Two resources with one kind name is a mistake in the code, not in the
31
+ // data, so it is a defect.
32
+ const twice = Arr.findFirst(
33
+ kinds,
34
+ (kind, index) => kinds.indexOf(kind) !== index,
35
+ )
36
+ if (Option.isSome(twice)) {
37
+ throw new Error(`Two resources have the kind name "${twice.value}"`)
38
+ }
39
+ if (kinds.includes("*")) {
40
+ throw new Error(`A resource may not have the kind name "*"`)
41
+ }
42
+ const resources = HashMap.fromIterable(
43
+ Arr.map(
44
+ descriptors,
45
+ (descriptor) => [descriptor.kind, descriptor] as const,
46
+ ),
47
+ )
48
+ // The catalog checks the field names of the principal, so a bad name is a
49
+ // defect when the domain is made.
50
+ return new DomainData({
51
+ principal: options.principal,
52
+ principalCatalog: Catalog.catalogOf(options.principal.ast),
53
+ resources,
54
+ meta: options.meta,
55
+ })
56
+ }
57
+
58
+ /** Gives the descriptor of the resource with a kind name. */
59
+ export const descriptorOf = (
60
+ domain: Domain.Any,
61
+ kind: string,
62
+ ): Resource.Descriptor | undefined =>
63
+ Option.getOrUndefined(HashMap.get(domain.resources, kind))
64
+
65
+ /** Tests if a value is a domain. */
66
+ export const isDomain = (u: unknown): u is Domain.Any => u instanceof DomainData