@nmnmcc/ability 2.0.0 → 2.0.2

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.
@@ -1,3 +1,4 @@
1
+ import { isNode } from "./nodes.js";
1
2
  import { lookup } from "./catalog.js";
2
3
  import { descriptor, isResource } from "./resource.js";
3
4
  import { builder as builder$1 } from "../Condition.js";
@@ -31,6 +32,15 @@ const make = (data) => new Rule({
31
32
  ...data.meta === void 0 ? {} : { meta: data.meta },
32
33
  ...data.domain === void 0 ? {} : { domain: data.domain }
33
34
  });
35
+ /**
36
+ * Makes a rule from its data, as `Policy.rule` does. A condition with a
37
+ * predicate is a defect: the types of the predicate tie it to the kinds of
38
+ * the rule of a builder, and the binding can not check it.
39
+ */
40
+ const makePlain = (data) => {
41
+ if (data.when !== void 0 && isNode(data.when) && data.when.opaque) throw new Error("Policy.rule takes no condition with a predicate. Make the rule with Policy.builder.");
42
+ return make(data);
43
+ };
34
44
  /** Tests if a value is a rule. */
35
45
  const isRule = (u) => u instanceof Rule;
36
46
  const toArray = (value) => Array.isArray(value) ? value : [value];
@@ -88,6 +98,6 @@ const matchesTarget = (rule, target) => {
88
98
  const open = rule._tag === "Deny" && target.descriptor !== void 0 && (lookup(target.descriptor.catalog, field)?.open ?? false);
89
99
  return rule.fields.some((pattern) => matchesField(pattern, field) || open && matchesInside(pattern, field));
90
100
  };
91
- export { Rule, builder, isRule, make, matchesField, matchesInside, matchesTarget };
101
+ export { Rule, builder, isRule, make, makePlain, matchesField, matchesInside, matchesTarget };
92
102
 
93
103
  //# sourceMappingURL=rule.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule.js","names":["ResourceData.isResource","ResourceData.descriptor","Condition.builder","Patterns.matches","Patterns.parseBound","Patterns.matchesBelow","Patterns.fitsCatalog","Catalog.lookup"],"sources":["../../src/internal/rule.ts"],"sourcesContent":["/**\n * The values of rules, with no types. The types are in `Policy.ts`.\n *\n * Rules do not change, and two rules with the same data are equal. A rule\n * works in a generator: it gives itself, so `yield* _.allow(...)` gives the\n * rule to `Policy.define`.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as Condition from \"../Condition.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Resource from \"../Resource.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as ResourceData from \"./resource.ts\"\nimport * as Patterns from \"./patterns.ts\"\n\n/** The data of a rule. */\nexport interface Shape {\n\treadonly _tag: \"Allow\" | \"Deny\"\n\treadonly actions: ReadonlyArray<string>\n\treadonly resources: ReadonlyArray<string>\n\treadonly when?: Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n\t/**\n\t * The domain of the builder that made the rule. Two domains may have the\n\t * same types but other paths, so the types can not tell them apart. It is\n\t * not in the JSON form of a rule.\n\t */\n\treadonly domain?: Domain.Any\n}\n\n/** The class of every rule value. */\nexport class Rule extends Data.Class<Shape> {\n\t*[Symbol.iterator](): Generator<this, this, unknown> {\n\t\tyield this\n\t\treturn this\n\t}\n}\n\n/** Makes a rule from its data. It does not check the data against a domain. */\nexport const make = (data: Shape): Rule =>\n\t// A missing option is left out, so two rules with the same data are equal.\n\tnew Rule({\n\t\t_tag: data._tag,\n\t\tactions: [...data.actions],\n\t\tresources: [...data.resources],\n\t\t...(data.when === undefined ? {} : { when: data.when }),\n\t\t...(data.fields === undefined ? {} : { fields: [...data.fields] }),\n\t\t...(data.reason === undefined ? {} : { reason: data.reason }),\n\t\t...(data.id === undefined ? {} : { id: data.id }),\n\t\t...(data.meta === undefined ? {} : { meta: data.meta }),\n\t\t...(data.domain === undefined ? {} : { domain: data.domain }),\n\t})\n\n/** Tests if a value is a rule. */\nexport const isRule = (u: unknown): u is Rule => u instanceof Rule\n\nconst toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>\n\tArray.isArray(value) ? value : [value as A]\n\nconst kindName = (target: unknown): string =>\n\tResourceData.isResource(target)\n\t\t? ResourceData.descriptor(target).kind\n\t\t: (target as string)\n\n/** The options of a rule from a builder, with no types. */\ninterface Options {\n\treadonly when?: (builder: never) => Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n}\n\nconst fromOptions =\n\t(tag: \"Allow\" | \"Deny\", domain: Domain.Any) =>\n\t(\n\t\taction: string | ReadonlyArray<string>,\n\t\tresource: unknown,\n\t\toptions?: Options,\n\t): Rule =>\n\t\tmake({\n\t\t\t_tag: tag,\n\t\t\tactions: toArray(action),\n\t\t\tresources: Arr.map(toArray(resource), kindName),\n\t\t\t...(options?.when === undefined\n\t\t\t\t? {}\n\t\t\t\t: { when: options.when(Condition.builder() as never) }),\n\t\t\t...(options?.fields === undefined ? {} : { fields: options.fields }),\n\t\t\t...(options?.reason === undefined ? {} : { reason: options.reason }),\n\t\t\t...(options?.id === undefined ? {} : { id: options.id }),\n\t\t\t...(options?.meta === undefined ? {} : { meta: options.meta }),\n\t\t\tdomain,\n\t\t})\n\n/** Makes the rule builder of a domain. */\nexport const builder = (domain: Domain.Any) => ({\n\tallow: fromOptions(\"Allow\", domain),\n\tdeny: fromOptions(\"Deny\", domain),\n})\n\n/**\n * Tests if a field name matches a field name or a field pattern of a rule. A\n * pattern matches the fields below a field that it matches too, so `settings`\n * matches `settings.model`.\n */\nexport const matchesField = (pattern: string, field: string): boolean =>\n\tPatterns.matches(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests if a field pattern may match a field inside `field`, such as\n * `settings.secret` inside `settings`.\n */\nexport const matchesInside = (pattern: string, field: string): boolean =>\n\tPatterns.matchesBelow(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests the parts of a rule that do not need the resource value: the action,\n * the kind and the field.\n *\n * A rule with an empty list of fields covers no request. A rule with fields\n * covers a request with no field only when it is an allow rule. So \"may\n * update some fields\" is a yes. A deny rule for some fields does not stop a\n * request for the whole value. With the `descriptor` of the kind, such an\n * allow rule covers the request only when one of its patterns matches a field\n * of the kind, and a deny rule for a part of a field that holds parts covers\n * a request for the whole field. A grant of fields that the kind does not have is not a grant of\n * some fields.\n */\nexport const matchesTarget = (\n\trule: Shape,\n\ttarget: {\n\t\treadonly action: string\n\t\treadonly kind: string\n\t\treadonly field?: string | undefined\n\t\treadonly descriptor?: Resource.Descriptor | undefined\n\t},\n): boolean => {\n\tif (!rule.actions.includes(\"*\") && !rule.actions.includes(target.action)) {\n\t\treturn false\n\t}\n\tif (!rule.resources.includes(\"*\") && !rule.resources.includes(target.kind)) {\n\t\treturn false\n\t}\n\tif (rule.fields === undefined) {\n\t\treturn true\n\t}\n\t// A rule for no fields is a rule for nothing. A grant with its last field\n\t// taken away must not cover the whole value.\n\tif (rule.fields.length === 0) {\n\t\treturn false\n\t}\n\tif (target.field === undefined) {\n\t\tconst descriptor = target.descriptor\n\t\treturn (\n\t\t\trule._tag === \"Allow\" &&\n\t\t\t(descriptor === undefined ||\n\t\t\t\tArr.some(rule.fields, (pattern) =>\n\t\t\t\t\tPatterns.fitsCatalog(\n\t\t\t\t\t\tPatterns.parseBound(pattern),\n\t\t\t\t\t\tdescriptor.catalog,\n\t\t\t\t\t),\n\t\t\t\t))\n\t\t)\n\t}\n\tconst field = target.field\n\t// A field that holds parts, as a record does, shows its parts too. So a\n\t// deny rule for a part inside it, as `data.secret` inside `data`, also\n\t// covers a request for the whole field.\n\tconst open =\n\t\trule._tag === \"Deny\" &&\n\t\ttarget.descriptor !== undefined &&\n\t\t(Catalog.lookup(target.descriptor.catalog, field)?.open ?? false)\n\treturn rule.fields.some(\n\t\t(pattern) =>\n\t\t\tmatchesField(pattern, field) || (open && matchesInside(pattern, field)),\n\t)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rule.js","names":["Nodes.isNode","ResourceData.isResource","ResourceData.descriptor","Condition.builder","Patterns.matches","Patterns.parseBound","Patterns.matchesBelow","Patterns.fitsCatalog","Catalog.lookup"],"sources":["../../src/internal/rule.ts"],"sourcesContent":["/**\n * The values of rules, with no types. The types are in `Policy.ts`.\n *\n * Rules do not change, and two rules with the same data are equal. A rule\n * works in a generator: it gives itself, so `yield* _.allow(...)` gives the\n * rule to `Policy.define`.\n */\nimport * as Arr from \"effect/Array\"\nimport * as Data from \"effect/Data\"\nimport * as Condition from \"../Condition.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Resource from \"../Resource.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Nodes from \"./nodes.ts\"\nimport * as ResourceData from \"./resource.ts\"\nimport * as Patterns from \"./patterns.ts\"\n\n/** The data of a rule. */\nexport interface Shape {\n\treadonly _tag: \"Allow\" | \"Deny\"\n\treadonly actions: ReadonlyArray<string>\n\treadonly resources: ReadonlyArray<string>\n\treadonly when?: Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n\t/**\n\t * The domain of the builder that made the rule. Two domains may have the\n\t * same types but other paths, so the types can not tell them apart. It is\n\t * not in the JSON form of a rule.\n\t */\n\treadonly domain?: Domain.Any\n}\n\n/** The class of every rule value. */\nexport class Rule extends Data.Class<Shape> {\n\t*[Symbol.iterator](): Generator<this, this, unknown> {\n\t\tyield this\n\t\treturn this\n\t}\n}\n\n/** Makes a rule from its data. It does not check the data against a domain. */\nexport const make = (data: Shape): Rule =>\n\t// A missing option is left out, so two rules with the same data are equal.\n\tnew Rule({\n\t\t_tag: data._tag,\n\t\tactions: [...data.actions],\n\t\tresources: [...data.resources],\n\t\t...(data.when === undefined ? {} : { when: data.when }),\n\t\t...(data.fields === undefined ? {} : { fields: [...data.fields] }),\n\t\t...(data.reason === undefined ? {} : { reason: data.reason }),\n\t\t...(data.id === undefined ? {} : { id: data.id }),\n\t\t...(data.meta === undefined ? {} : { meta: data.meta }),\n\t\t...(data.domain === undefined ? {} : { domain: data.domain }),\n\t})\n\n/**\n * Makes a rule from its data, as `Policy.rule` does. A condition with a\n * predicate is a defect: the types of the predicate tie it to the kinds of\n * the rule of a builder, and the binding can not check it.\n */\nexport const makePlain = (data: Shape): Rule => {\n\tif (data.when !== undefined && Nodes.isNode(data.when) && data.when.opaque) {\n\t\tthrow new Error(\n\t\t\t\"Policy.rule takes no condition with a predicate. Make the rule with Policy.builder.\",\n\t\t)\n\t}\n\treturn make(data)\n}\n\n/** Tests if a value is a rule. */\nexport const isRule = (u: unknown): u is Rule => u instanceof Rule\n\nconst toArray = <A>(value: A | ReadonlyArray<A>): ReadonlyArray<A> =>\n\tArray.isArray(value) ? value : [value as A]\n\nconst kindName = (target: unknown): string =>\n\tResourceData.isResource(target)\n\t\t? ResourceData.descriptor(target).kind\n\t\t: (target as string)\n\n/** The options of a rule from a builder, with no types. */\ninterface Options {\n\treadonly when?: (builder: never) => Condition.Ast\n\treadonly fields?: ReadonlyArray<string>\n\treadonly reason?: string\n\treadonly id?: string\n\treadonly meta?: unknown\n}\n\nconst fromOptions =\n\t(tag: \"Allow\" | \"Deny\", domain: Domain.Any) =>\n\t(\n\t\taction: string | ReadonlyArray<string>,\n\t\tresource: unknown,\n\t\toptions?: Options,\n\t): Rule =>\n\t\tmake({\n\t\t\t_tag: tag,\n\t\t\tactions: toArray(action),\n\t\t\tresources: Arr.map(toArray(resource), kindName),\n\t\t\t...(options?.when === undefined\n\t\t\t\t? {}\n\t\t\t\t: { when: options.when(Condition.builder() as never) }),\n\t\t\t...(options?.fields === undefined ? {} : { fields: options.fields }),\n\t\t\t...(options?.reason === undefined ? {} : { reason: options.reason }),\n\t\t\t...(options?.id === undefined ? {} : { id: options.id }),\n\t\t\t...(options?.meta === undefined ? {} : { meta: options.meta }),\n\t\t\tdomain,\n\t\t})\n\n/** Makes the rule builder of a domain. */\nexport const builder = (domain: Domain.Any) => ({\n\tallow: fromOptions(\"Allow\", domain),\n\tdeny: fromOptions(\"Deny\", domain),\n})\n\n/**\n * Tests if a field name matches a field name or a field pattern of a rule. A\n * pattern matches the fields below a field that it matches too, so `settings`\n * matches `settings.model`.\n */\nexport const matchesField = (pattern: string, field: string): boolean =>\n\tPatterns.matches(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests if a field pattern may match a field inside `field`, such as\n * `settings.secret` inside `settings`.\n */\nexport const matchesInside = (pattern: string, field: string): boolean =>\n\tPatterns.matchesBelow(Patterns.parseBound(pattern), field.split(\".\"))\n\n/**\n * Tests the parts of a rule that do not need the resource value: the action,\n * the kind and the field.\n *\n * A rule with an empty list of fields covers no request. A rule with fields\n * covers a request with no field only when it is an allow rule. So \"may\n * update some fields\" is a yes. A deny rule for some fields does not stop a\n * request for the whole value. With the `descriptor` of the kind, such an\n * allow rule covers the request only when one of its patterns matches a field\n * of the kind, and a deny rule for a part of a field that holds parts covers\n * a request for the whole field. A grant of fields that the kind does not have is not a grant of\n * some fields.\n */\nexport const matchesTarget = (\n\trule: Shape,\n\ttarget: {\n\t\treadonly action: string\n\t\treadonly kind: string\n\t\treadonly field?: string | undefined\n\t\treadonly descriptor?: Resource.Descriptor | undefined\n\t},\n): boolean => {\n\tif (!rule.actions.includes(\"*\") && !rule.actions.includes(target.action)) {\n\t\treturn false\n\t}\n\tif (!rule.resources.includes(\"*\") && !rule.resources.includes(target.kind)) {\n\t\treturn false\n\t}\n\tif (rule.fields === undefined) {\n\t\treturn true\n\t}\n\t// A rule for no fields is a rule for nothing. A grant with its last field\n\t// taken away must not cover the whole value.\n\tif (rule.fields.length === 0) {\n\t\treturn false\n\t}\n\tif (target.field === undefined) {\n\t\tconst descriptor = target.descriptor\n\t\treturn (\n\t\t\trule._tag === \"Allow\" &&\n\t\t\t(descriptor === undefined ||\n\t\t\t\tArr.some(rule.fields, (pattern) =>\n\t\t\t\t\tPatterns.fitsCatalog(\n\t\t\t\t\t\tPatterns.parseBound(pattern),\n\t\t\t\t\t\tdescriptor.catalog,\n\t\t\t\t\t),\n\t\t\t\t))\n\t\t)\n\t}\n\tconst field = target.field\n\t// A field that holds parts, as a record does, shows its parts too. So a\n\t// deny rule for a part inside it, as `data.secret` inside `data`, also\n\t// covers a request for the whole field.\n\tconst open =\n\t\trule._tag === \"Deny\" &&\n\t\ttarget.descriptor !== undefined &&\n\t\t(Catalog.lookup(target.descriptor.catalog, field)?.open ?? false)\n\treturn rule.fields.some(\n\t\t(pattern) =>\n\t\t\tmatchesField(pattern, field) || (open && matchesInside(pattern, field)),\n\t)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmnmcc/ability",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Typed, composable authorization policies for Effect programs.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/Condition.ts CHANGED
@@ -224,32 +224,24 @@ export interface Opaque<out E = never, out R = never> {
224
224
  }
225
225
 
226
226
  /**
227
- * The error type of the predicates of a mark `X`. A mark that may be
228
- * anything, as `unknown`, may have any error.
227
+ * The error type of the predicates of a mark `X`. A mark that is not an
228
+ * {@link Opaque}, as `unknown` or `object`, may have any error.
229
229
  *
230
230
  * @stability stable
231
231
  * @category utility types
232
232
  * @since 2.0.0
233
233
  */
234
- export type ErrorOf<X> = unknown extends X
235
- ? unknown
236
- : X extends Opaque<infer E, infer _R>
237
- ? E
238
- : never
234
+ export type ErrorOf<X> = X extends Opaque<infer E, infer _R> ? E : unknown
239
235
 
240
236
  /**
241
- * The service type of the predicates of a mark `X`. A mark that may be
242
- * anything, as `unknown`, may need any service.
237
+ * The service type of the predicates of a mark `X`. A mark that is not an
238
+ * {@link Opaque}, as `unknown` or `object`, may need any service.
243
239
  *
244
240
  * @stability stable
245
241
  * @category utility types
246
242
  * @since 2.0.0
247
243
  */
248
- export type ServicesOf<X> = unknown extends X
249
- ? unknown
250
- : X extends Opaque<infer _E, infer R>
251
- ? R
252
- : never
244
+ export type ServicesOf<X> = X extends Opaque<infer _E, infer R> ? R : unknown
253
245
 
254
246
  /**
255
247
  * The mark `X` with no services.
@@ -258,11 +250,8 @@ export type ServicesOf<X> = unknown extends X
258
250
  * @category utility types
259
251
  * @since 2.0.0
260
252
  */
261
- export type Provided<X> = unknown extends X
262
- ? Opaque<unknown>
263
- : X extends Opaque<infer E, infer _R>
264
- ? Opaque<E>
265
- : never
253
+ export type Provided<X> =
254
+ X extends Opaque<infer E, infer _R> ? Opaque<E> : Opaque<unknown>
266
255
 
267
256
  /**
268
257
  * What the predicates of a condition read: a resource value of the type `V`
@@ -366,14 +355,15 @@ type FieldPathInternal<
366
355
  > = [Depth] extends [never]
367
356
  ? never
368
357
  : {
369
- [K in keyof T & string]-?: PathsOf<
370
- K,
358
+ // A record with number keys has number keys, as `counts.1`.
359
+ [K in keyof T & (string | number)]-?: PathsOf<
360
+ `${K}`,
371
361
  NonNullable<T[K]>,
372
362
  Depth,
373
363
  Arrays,
374
364
  Parents
375
365
  >
376
- }[keyof T & string]
366
+ }[keyof T & (string | number)]
377
367
 
378
368
  /**
379
369
  * The paths of one field `K` with the values `V`. Each kind of value in `V`
@@ -387,8 +377,9 @@ type PathsOf<
387
377
  Arrays extends boolean,
388
378
  Parents extends boolean,
389
379
  > =
390
- // A field that is only ever `null` or missing is a field too.
391
- | ([V] extends [never] ? K : never)
380
+ // A field that is only ever `null` or missing is a field too, and so is a
381
+ // field that may hold any value, as one of `Schema.Unknown` may.
382
+ | ([V] extends [never] ? K : {} extends V ? K : never)
392
383
  | ([Extract<V, Leaf | Whole>] extends [never] ? never : K)
393
384
  | ([Extract<V, ReadonlyArray<unknown>>] extends [never]
394
385
  ? never
@@ -419,7 +410,9 @@ type ObjectPaths<
419
410
  : Arrays extends true
420
411
  ? string extends keyof O
421
412
  ? K
422
- : never
413
+ : number extends keyof O
414
+ ? K
415
+ : never
423
416
  : never)
424
417
  | `${K}.${FieldPathInternal<O, PreviousDepth[Depth], Arrays, Parents>}`
425
418
 
package/src/Policy.ts CHANGED
@@ -533,14 +533,19 @@ export const builder = <D extends Domain.Any>(domain: D): Builder<D> =>
533
533
  * against a domain: a policy does that when the rule goes into it. Use
534
534
  * {@link builder} for rules with types, or {@link schema} for rules from JSON.
535
535
  *
536
+ * The condition may not have a predicate. A policy checks the fields of a
537
+ * condition against the resources of its rule, but it can not check what
538
+ * the code of a predicate reads. A rule from the builder keeps that in its
539
+ * types; this one could give a predicate of one kind to another kind.
540
+ *
536
541
  * @stability stable
537
542
  * @category constructors
538
543
  * @since 2.0.0
539
544
  */
540
- export const rule = <I = unknown, X = never>(
541
- data: RuleShape<I, X>,
545
+ export const rule: <I = unknown>(
546
+ data: RuleShape<I, never>,
542
547
  // The types come from the data only, not from where the rule goes.
543
- ): Rule<any, NoInfer<I>, NoInfer<X>> => RuleData.make(data as never) as never
548
+ ) => Rule<any, NoInfer<I>> = RuleData.makePlain as never
544
549
 
545
550
  /**
546
551
  * The options of a list of rules.
@@ -773,8 +778,10 @@ export type ResidualReads<D, K> = Condition.Reads<
773
778
  /**
774
779
  * Gives the condition on resource values for which the policy allows a
775
780
  * request. It has no principal fields and no `Under` nodes, so you can give it
776
- * to `Condition.fold`. It is `True` when the policy allows every value of the
777
- * kind, and `False` when it allows none.
781
+ * to `Condition.fold`. It is made smaller, so it is often `True` or `False`
782
+ * when the answer does not depend on the value, but it does no search: a
783
+ * condition that is true for every value may stay a test. To know if the
784
+ * policy allows every value, or some value, use {@link coverage}.
778
785
  *
779
786
  * With a scope (`within`), the condition is the scope and the residual
780
787
  * together, so it is false for every value outside the scope. Give the policy
@@ -93,19 +93,29 @@ type AnyTarget = {
93
93
 
94
94
  /**
95
95
  * A cache key for a principal. Two keys are equal when they have the same
96
- * text, so two principals with the same key share one policy. The key also
97
- * keeps the principal, so the cache can load its policy. A plain `Data` value
98
- * can not do both, so the key gives its own equality with `Equal.Equal`.
96
+ * text, so two principals with the same key share one policy. A key from the
97
+ * JSON form also needs equal decoded principals: a Schema may make data when
98
+ * it decodes, as `expired` from a time, so one text may give two principals.
99
+ * The key also keeps the principal, so the cache can load its policy. A plain
100
+ * `Data` value can not do all this, so the key gives its own equality with
101
+ * `Equal.Equal`.
99
102
  */
100
103
  class PrincipalKey implements Equal.Equal {
101
104
  readonly principal: object
102
105
  readonly text: string
103
- constructor(principal: object, text: string) {
106
+ /** The key is the JSON form, so the decoded principals must be equal too. */
107
+ readonly exact: boolean
108
+ constructor(principal: object, text: string, exact: boolean) {
104
109
  this.principal = principal
105
110
  this.text = text
111
+ this.exact = exact
106
112
  }
107
113
  [Equal.symbol](that: unknown): boolean {
108
- return that instanceof PrincipalKey && that.text === this.text
114
+ return (
115
+ that instanceof PrincipalKey &&
116
+ that.text === this.text &&
117
+ (!this.exact || Equal.equals(that.principal, this.principal))
118
+ )
109
119
  }
110
120
  [Hash.symbol](): number {
111
121
  return Hash.string(this.text)
@@ -160,7 +170,7 @@ const jsonKey = (
160
170
  onFailure: () => undefined,
161
171
  onSuccess: (decoded) =>
162
172
  Equal.equals(decoded, principal)
163
- ? new PrincipalKey(decoded as object, text)
173
+ ? new PrincipalKey(decoded as object, text, true)
164
174
  : undefined,
165
175
  })
166
176
  },
@@ -226,7 +236,7 @@ export const makeLayer = (
226
236
  const text = custom(principal)
227
237
  return text === undefined
228
238
  ? undefined
229
- : new PrincipalKey(canonical?.principal ?? principal, text)
239
+ : new PrincipalKey(canonical?.principal ?? principal, text, false)
230
240
  }
231
241
  return Layer.effect(
232
242
  service,
@@ -42,11 +42,17 @@ interface Memo {
42
42
  readonly suggestions: MutableHashMap.MutableHashMap<string, string>
43
43
  /** If a pattern matches a field of a kind, by the kind and the pattern. */
44
44
  readonly fits: MutableHashMap.MutableHashMap<string, boolean>
45
+ /** The count of the issues of conditions so far. It belongs to one binding. */
46
+ readonly issues: { count: number; noted: boolean }
47
+ /** The steps of the checks of patterns so far. It belongs to one binding. */
48
+ readonly work: { steps: number; noted: boolean }
45
49
  }
46
50
 
47
51
  const makeMemo = (): Memo => ({
48
52
  suggestions: MutableHashMap.empty(),
49
53
  fits: MutableHashMap.empty(),
54
+ issues: { count: 0, noted: false },
55
+ work: { steps: 0, noted: false },
50
56
  })
51
57
 
52
58
  /**
@@ -183,24 +189,44 @@ const patternIssues = (
183
189
  ): ReadonlyArray<Issue> =>
184
190
  Result.match(Patterns.parse(pattern), {
185
191
  onFailure: (issue) => [{ path: at, issue }],
186
- onSuccess: (parsed) =>
192
+ onSuccess: (parsed) => {
187
193
  // A rule with no known resource has an issue for that already.
188
- known.length === 0 ||
189
- Arr.some(known, (descriptor) => {
190
- const key = `${descriptor.kind}\u0000${pattern}`
191
- return Option.getOrElse(MutableHashMap.get(memo.fits, key), () => {
192
- const fits = Patterns.fitsCatalog(parsed, descriptor.catalog)
193
- MutableHashMap.set(memo.fits, key, fits)
194
- return fits
194
+ const fits =
195
+ known.length === 0 ||
196
+ Arr.some(known, (descriptor) => {
197
+ const key = `${descriptor.kind}\u0000${pattern}`
198
+ return Option.getOrElse(MutableHashMap.get(memo.fits, key), () => {
199
+ // Each check of a pattern reads the fields of a catalog. The
200
+ // work of one binding has a limit, so very many patterns stop
201
+ // with one issue, and the policy does not load.
202
+ memo.work.steps +=
203
+ descriptor.catalog.fields.length * Math.max(parsed.length, 1)
204
+ if (memo.work.steps > Limits.searchSteps) {
205
+ return true
206
+ }
207
+ const found = Patterns.fitsCatalog(parsed, descriptor.catalog)
208
+ MutableHashMap.set(memo.fits, key, found)
209
+ return found
210
+ })
195
211
  })
196
- })
212
+ if (memo.work.steps > Limits.searchSteps && !memo.work.noted) {
213
+ memo.work.noted = true
214
+ return [
215
+ {
216
+ path: at,
217
+ issue: `The field patterns of the policy take more than ${String(Limits.searchSteps)} steps to check. The most is ${String(Limits.searchSteps)}.`,
218
+ },
219
+ ]
220
+ }
221
+ return fits
197
222
  ? []
198
223
  : [
199
224
  {
200
225
  path: at,
201
226
  issue: `The field pattern "${pattern}" matches no field of the resources of the rule`,
202
227
  },
203
- ],
228
+ ]
229
+ },
204
230
  })
205
231
 
206
232
  /** The fields of the resources that a test of the rule may read. */
@@ -385,6 +411,46 @@ const notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({
385
411
  issue: "The condition is not a whole condition tree",
386
412
  })
387
413
 
414
+ /**
415
+ * The most issues that the walk of the conditions of one binding gives. A
416
+ * policy may have very many wrong tests, and each issue keeps its path.
417
+ */
418
+ const maxIssues = 100
419
+
420
+ /** A path that the walk makes only when it needs it, for an issue. */
421
+ type LazyPath = () => ReadonlyArray<PropertyKey>
422
+
423
+ /**
424
+ * Puts the path of a node before the paths of its issues. A walk keeps no
425
+ * path for a node with no issue, so a deep and wide condition costs little.
426
+ */
427
+ const under = (
428
+ memo: Memo,
429
+ at: LazyPath,
430
+ issues: ReadonlyArray<Issue>,
431
+ ): ReadonlyArray<Issue> => {
432
+ if (issues.length === 0) {
433
+ return issues
434
+ }
435
+ const room = maxIssues - memo.issues.count
436
+ memo.issues.count += issues.length
437
+ const kept = Arr.map(Arr.take(issues, Math.max(room, 0)), (issue) => ({
438
+ path: [...at(), ...issue.path],
439
+ issue: issue.issue,
440
+ }))
441
+ const first = memo.issues.count > maxIssues && !memo.issues.noted
442
+ memo.issues.noted ||= first
443
+ return first
444
+ ? [
445
+ ...kept,
446
+ {
447
+ path: [],
448
+ issue: `The policy has more problems. The first ${String(maxIssues)} are here.`,
449
+ },
450
+ ]
451
+ : kept
452
+ }
453
+
388
454
  /**
389
455
  * The issues of a condition. The constructors of the nodes checked the shape
390
456
  * of each node, so this checks the names and the types against the domain. A
@@ -395,52 +461,61 @@ const notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({
395
461
  const conditionIssues = (
396
462
  context: Context,
397
463
  node: Nodes.Node,
398
- at: ReadonlyArray<PropertyKey>,
464
+ at: LazyPath,
399
465
  seen: Set<Nodes.Node>,
400
466
  ): ReadonlyArray<Issue> => {
401
467
  if (seen.has(node)) {
402
468
  return []
403
469
  }
404
470
  seen.add(node)
471
+ const here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)
405
472
  switch (node._tag) {
406
473
  case "Compare": {
407
- const exists = fieldIssue(context, node.field, [...at, "field"])
408
- return exists.length > 0
409
- ? exists
410
- : operandIssues(
411
- context,
412
- node.field,
413
- node.op !== "eq" && node.op !== "ne",
414
- node.value,
415
- [...at, "value"],
416
- )
474
+ const exists = fieldIssue(context, node.field, ["field"])
475
+ return here(
476
+ exists.length > 0
477
+ ? exists
478
+ : operandIssues(
479
+ context,
480
+ node.field,
481
+ node.op !== "eq" && node.op !== "ne",
482
+ node.value,
483
+ ["value"],
484
+ ),
485
+ )
417
486
  }
418
487
  case "In": {
419
- const exists = fieldIssue(context, node.field, [...at, "field"])
420
- return exists.length > 0
421
- ? exists
422
- : Arr.flatMap(node.values, (operand, index) =>
423
- operandIssues(context, node.field, false, operand, [
424
- ...at,
425
- "values",
426
- index,
427
- ]),
428
- )
488
+ const exists = fieldIssue(context, node.field, ["field"])
489
+ return here(
490
+ exists.length > 0
491
+ ? exists
492
+ : Arr.flatMap(node.values, (operand, index) =>
493
+ operandIssues(context, node.field, false, operand, [
494
+ "values",
495
+ index,
496
+ ]),
497
+ ),
498
+ )
429
499
  }
430
500
  case "Exists":
431
- return fieldIssue(context, node.field, [...at, "field"])
501
+ return here(fieldIssue(context, node.field, ["field"]))
432
502
  case "Under":
433
- return underIssues(context, node.prefix, at)
503
+ return here(underIssues(context, node.prefix, []))
434
504
  case "And":
435
505
  case "Or":
436
506
  return Arr.flatMap(node.conditions, (child, index) =>
437
- conditionIssues(context, child, [...at, "conditions", index], seen),
507
+ conditionIssues(
508
+ context,
509
+ child,
510
+ () => [...at(), "conditions", index],
511
+ seen,
512
+ ),
438
513
  )
439
514
  case "Not":
440
515
  return conditionIssues(
441
516
  context,
442
517
  node.condition,
443
- [...at, "condition"],
518
+ () => [...at(), "condition"],
444
519
  seen,
445
520
  )
446
521
  default:
@@ -529,7 +604,7 @@ export const ruleIssues = (
529
604
  ...(rule.when === undefined
530
605
  ? []
531
606
  : Nodes.isNode(rule.when)
532
- ? conditionIssues(context, rule.when, [...at, "when"], new Set())
607
+ ? conditionIssues(context, rule.when, () => [...at, "when"], new Set())
533
608
  : [notWhole([...at, "when"])]),
534
609
  ]
535
610
  }
@@ -16,6 +16,15 @@ import * as PolicyNodes from "./policy-nodes.ts"
16
16
 
17
17
  type Node = PolicyNodes.Node
18
18
 
19
+ /**
20
+ * The codec of the `meta` Schema of a domain to and from JSON. The JSON form
21
+ * of a policy must stay the same through `JSON.stringify` and `JSON.parse`, so
22
+ * a `Date` in `meta`, for one, becomes a string in it, and a string again
23
+ * when it is decoded.
24
+ */
25
+ const metaCodec = (meta: Schema.Top): Schema.Codec<unknown, unknown> =>
26
+ Schema.toCodecJson(meta) as unknown as Schema.Codec<unknown, unknown>
27
+
19
28
  /** Gives the JSON form of the `meta` of a rule, or `undefined` for none. */
20
29
  export type MetaJson = (rule: Rule.Rule) => unknown
21
30
 
@@ -29,9 +38,7 @@ export const syncMeta =
29
38
  (rule) =>
30
39
  rule.meta === undefined || meta === undefined
31
40
  ? undefined
32
- : Schema.encodeUnknownSync(meta as Schema.Codec<unknown, unknown>)(
33
- rule.meta,
34
- )
41
+ : Schema.encodeUnknownSync(metaCodec(meta))(rule.meta)
35
42
 
36
43
  /** The JSON form of the condition of each rule, by the rule. */
37
44
  type WhenJson = (rule: Rule.Rule) => unknown
@@ -118,9 +125,7 @@ export const encode = (
118
125
  ? []
119
126
  : yield* Effect.forEach(rules, (rule) =>
120
127
  Effect.map(
121
- Schema.encodeUnknownEffect(
122
- meta as Schema.Codec<unknown, unknown>,
123
- )(rule.meta),
128
+ Schema.encodeUnknownEffect(metaCodec(meta))(rule.meta),
124
129
  (json) => [rule, json] as const,
125
130
  ),
126
131
  )
@@ -264,16 +269,14 @@ const decodeMeta = (
264
269
  )
265
270
  }
266
271
  spendValue(value, [...path, "meta"], 1, budget)
267
- return { value }
272
+ return { value, schema: domain.meta }
268
273
  }),
269
274
  ),
270
275
  (found) =>
271
276
  found === undefined
272
277
  ? Effect.void
273
278
  : Effect.mapError(
274
- Schema.decodeUnknownEffect(
275
- domain.meta as Schema.Codec<unknown, unknown>,
276
- )(found.value),
279
+ Schema.decodeUnknownEffect(metaCodec(found.schema))(found.value),
277
280
  (error) =>
278
281
  new Json.Problem({
279
282
  path: [...path, "meta"],
@@ -11,6 +11,7 @@ import * as Condition from "../Condition.ts"
11
11
  import type * as Domain from "../Domain.ts"
12
12
  import type * as Resource from "../Resource.ts"
13
13
  import * as Catalog from "./catalog.ts"
14
+ import * as Nodes from "./nodes.ts"
14
15
  import * as ResourceData from "./resource.ts"
15
16
  import * as Patterns from "./patterns.ts"
16
17
 
@@ -55,6 +56,20 @@ export const make = (data: Shape): Rule =>
55
56
  ...(data.domain === undefined ? {} : { domain: data.domain }),
56
57
  })
57
58
 
59
+ /**
60
+ * Makes a rule from its data, as `Policy.rule` does. A condition with a
61
+ * predicate is a defect: the types of the predicate tie it to the kinds of
62
+ * the rule of a builder, and the binding can not check it.
63
+ */
64
+ export const makePlain = (data: Shape): Rule => {
65
+ if (data.when !== undefined && Nodes.isNode(data.when) && data.when.opaque) {
66
+ throw new Error(
67
+ "Policy.rule takes no condition with a predicate. Make the rule with Policy.builder.",
68
+ )
69
+ }
70
+ return make(data)
71
+ }
72
+
58
73
  /** Tests if a value is a rule. */
59
74
  export const isRule = (u: unknown): u is Rule => u instanceof Rule
60
75