@nmnmcc/ability 2.0.1 → 2.0.4

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 (48) hide show
  1. package/README.md +25 -17
  2. package/dist/Condition.d.ts +26 -16
  3. package/dist/Condition.d.ts.map +1 -1
  4. package/dist/Condition.js +2 -1
  5. package/dist/Condition.js.map +1 -1
  6. package/dist/Limits.d.ts +3 -1
  7. package/dist/Limits.d.ts.map +1 -1
  8. package/dist/Limits.js +3 -1
  9. package/dist/Limits.js.map +1 -1
  10. package/dist/Policy.d.ts +6 -8
  11. package/dist/Policy.d.ts.map +1 -1
  12. package/dist/Policy.js +4 -2
  13. package/dist/Policy.js.map +1 -1
  14. package/dist/internal/algebra.js +23 -13
  15. package/dist/internal/algebra.js.map +1 -1
  16. package/dist/internal/analysis.js +4 -2
  17. package/dist/internal/analysis.js.map +1 -1
  18. package/dist/internal/authorizer.js +13 -7
  19. package/dist/internal/authorizer.js.map +1 -1
  20. package/dist/internal/bind.js +53 -27
  21. package/dist/internal/bind.js.map +1 -1
  22. package/dist/internal/catalog.d.ts +9 -0
  23. package/dist/internal/catalog.d.ts.map +1 -1
  24. package/dist/internal/catalog.js +37 -22
  25. package/dist/internal/catalog.js.map +1 -1
  26. package/dist/internal/decide.js +2 -1
  27. package/dist/internal/decide.js.map +1 -1
  28. package/dist/internal/place.js +19 -0
  29. package/dist/internal/place.js.map +1 -0
  30. package/dist/internal/policy-json.js +39 -39
  31. package/dist/internal/policy-json.js.map +1 -1
  32. package/dist/internal/rule.d.ts +20 -22
  33. package/dist/internal/rule.d.ts.map +1 -1
  34. package/dist/internal/rule.js +32 -4
  35. package/dist/internal/rule.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/Condition.ts +64 -50
  38. package/src/Limits.ts +3 -1
  39. package/src/Policy.ts +5 -9
  40. package/src/internal/algebra.ts +30 -13
  41. package/src/internal/analysis.ts +5 -0
  42. package/src/internal/authorizer.ts +17 -7
  43. package/src/internal/bind.ts +85 -23
  44. package/src/internal/catalog.ts +78 -30
  45. package/src/internal/decide.ts +3 -0
  46. package/src/internal/place.ts +32 -0
  47. package/src/internal/policy-json.ts +49 -29
  48. package/src/internal/rule.ts +55 -11
@@ -1 +1 @@
1
- {"version":3,"file":"authorizer.js","names":["Policies.provideContext","Decide.decideAny","ResourceData.kindOf","DomainData.descriptorOf","Decide.fitsKind","Analysis.decideKind","Decision.reason","Analysis.coverageAny","permittedFields","Decide.permittedFields"],"sources":["../../src/internal/authorizer.ts"],"sourcesContent":["/**\n * The values of authorizers: the errors, the service class and the layer.\n * The types are in `Authorizer.ts`.\n */\n// The service erases the error type and the service type of predicates,\n// principals and deciders to `unknown`. The public types keep them, so the\n// language service check for `unknown` in an Effect is off here.\n// @effect-diagnostics anyUnknownInErrorContext:off\nimport * as Cache from \"effect/Cache\"\nimport * as Context from \"effect/Context\"\nimport type * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport * as Equal from \"effect/Equal\"\nimport * as Hash from \"effect/Hash\"\nimport * as Layer from \"effect/Layer\"\nimport * as Result from \"effect/Result\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Authorizer from \"../Authorizer.ts\"\nimport type * as Condition from \"../Condition.ts\"\nimport * as Decision from \"../Decision.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Policy from \"../Policy.ts\"\nimport * as Analysis from \"./analysis.ts\"\nimport * as Decide from \"./decide.ts\"\nimport * as DomainData from \"./domain.ts\"\nimport * as Policies from \"./policy.ts\"\nimport * as ResourceData from \"./resource.ts\"\n\ntype AnyServiceClass = Authorizer.AnyServiceClass\n\n/** The error when a policy says no. */\nexport class Forbidden extends Schema.TaggedError<Forbidden>(\n\t\"@nmnmcc/ability/Authorizer/Forbidden\",\n)(\"Forbidden\", {\n\taction: Schema.String,\n\tkind: Schema.String,\n\tfield: Schema.optionalKey(Schema.String),\n\treason: Schema.optionalKey(Schema.String),\n}) {}\n\n/** The error when the policy of the principal can not be loaded. */\nexport class PolicyUnavailable extends Schema.TaggedError<PolicyUnavailable>(\n\t\"@nmnmcc/ability/Authorizer/PolicyUnavailable\",\n)(\"PolicyUnavailable\", { cause: Schema.Defect() }) {}\n\n/** The error when a decider must wait for an answer. */\nexport class Pending extends Schema.TaggedError<Pending>(\n\t\"@nmnmcc/ability/Authorizer/Pending\",\n)(\"Pending\", {\n\tticket: Schema.String,\n\treason: Schema.optionalKey(Schema.String),\n}) {}\n\n/** Makes an authorizer service class for a domain. */\nexport const Service =\n\t() =>\n\t(\n\t\tid: string,\n\t\toptions: {\n\t\t\treadonly domain: Domain.Any\n\t\t\treadonly principal: Effect.Effect<any, any, any>\n\t\t},\n\t\t// The class has no name, and its type uses types of Effect that a\n\t\t// declaration file can not name. So this function gives a `Context.Key`.\n\t\t// The public module gives the full type.\n\t): Context.Key<any, any> => {\n\t\tconst Base = Context.Service<any, any>()(id)\n\t\treturn class extends Base {\n\t\t\tstatic readonly domain = options.domain\n\t\t\tstatic readonly principal = options.principal\n\t\t\tstatic readonly decide = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.decide(target))\n\t\t\tstatic readonly check = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.check(target))\n\t\t\tstatic readonly guard = (target: AnyTarget, options: unknown) =>\n\t\t\t\tthis.use((service: any) => service.guard(target, options))\n\t\t\tstatic readonly filter = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.filter(target))\n\t\t\tstatic readonly coverage = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.coverage(target))\n\t\t\tstatic readonly permittedFields = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.permittedFields(target))\n\t\t}\n\t}\n\ntype AnyTarget = {\n\treadonly action: string\n\treadonly resource?: object\n\treadonly kind?: string\n\treadonly field?: string\n\treadonly within?: unknown\n}\n\n/**\n * A cache key for a principal. Two keys are equal when they have the same\n * text, so two principals with the same key share one policy. The key also\n * keeps the principal, so the cache can load its policy. A plain `Data` value\n * can not do both, so the key gives its own equality with `Equal.Equal`.\n */\nclass PrincipalKey implements Equal.Equal {\n\treadonly principal: object\n\treadonly text: string\n\tconstructor(principal: object, text: string) {\n\t\tthis.principal = principal\n\t\tthis.text = text\n\t}\n\t[Equal.symbol](that: unknown): boolean {\n\t\treturn that instanceof PrincipalKey && that.text === this.text\n\t}\n\t[Hash.symbol](): number {\n\t\treturn Hash.string(this.text)\n\t}\n}\n\n/**\n * Makes the cache key of a principal from its JSON form. The key keeps the\n * principal that the Schema decodes from that form, so the loader sees only\n * the data that the key shows. A principal gets no key when it does not\n * encode, or when the decoded value is not `Equal` to it. Two classes, or a\n * string and a number, may have one JSON form, as in a union, and `Equal`\n * tells them apart. So two principals with one key are equal, and the cache\n * can not give one the policy of the other.\n */\n/**\n * Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON\n * form, as a struct with a symbol key has none.\n */\nconst jsonCodecOf = (\n\tschema: Schema.Top,\n): Schema.Codec<unknown, unknown> | undefined => {\n\ttry {\n\t\treturn Schema.toCodecJson(schema) as unknown as Schema.Codec<\n\t\t\tunknown,\n\t\t\tunknown\n\t\t>\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst jsonKey = (\n\tschema: Schema.Top,\n): ((principal: object) => PrincipalKey | undefined) => {\n\tconst codec = jsonCodecOf(schema)\n\t// A principal with no JSON form is never kept.\n\tif (codec === undefined) {\n\t\treturn () => undefined\n\t}\n\tconst encode = Schema.encodeUnknownResult(codec)\n\tconst decode = Schema.decodeUnknownResult(codec)\n\tconst keyOf = (principal: object): PrincipalKey | undefined =>\n\t\tResult.match(encode(principal), {\n\t\t\tonFailure: () => undefined,\n\t\t\tonSuccess: (json) => {\n\t\t\t\t// The key is the text, so the check decodes the text. The encoded\n\t\t\t\t// value may hold more than its text shows, as an object of a\n\t\t\t\t// `Schema.Any` field does: `JSON.stringify` leaves out a map.\n\t\t\t\tconst text = JSON.stringify(json)\n\t\t\t\treturn Result.match(decode(JSON.parse(text)), {\n\t\t\t\t\tonFailure: () => undefined,\n\t\t\t\t\tonSuccess: (decoded) =>\n\t\t\t\t\t\tEqual.equals(decoded, principal)\n\t\t\t\t\t\t\t? new PrincipalKey(decoded as object, text)\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t})\n\t\t\t},\n\t\t})\n\treturn (principal) => {\n\t\t// A very deep principal may fill the stack of the encoder or of\n\t\t// `Equal`. Such a principal is not kept.\n\t\ttry {\n\t\t\treturn keyOf(principal)\n\t\t} catch {\n\t\t\treturn undefined\n\t\t}\n\t}\n}\n\nconst forbidden = (\n\ttarget: AnyTarget,\n\tkind: string,\n\treason: string | undefined,\n): Forbidden =>\n\tForbidden.make({\n\t\taction: target.action,\n\t\tkind,\n\t\t...(target.field === undefined ? {} : { field: target.field }),\n\t\t...(reason === undefined ? {} : { reason }),\n\t})\n\n/** Makes the layer of an authorizer. */\nexport const makeLayer = (\n\tservice: AnyServiceClass,\n\toptions: {\n\t\treadonly policy: (\n\t\t\tprincipal: object,\n\t\t) => Effect.Effect<Policy.Any, unknown, unknown>\n\t\treadonly decider?: (\n\t\t\trequest: object,\n\t\t) => Effect.Effect<Decision.Decision, unknown, unknown>\n\t\treadonly cache?:\n\t\t\t| {\n\t\t\t\t\treadonly capacity?: number\n\t\t\t\t\treadonly timeToLive?: Duration.Input\n\t\t\t\t\treadonly key?: (principal: object) => string | undefined\n\t\t\t }\n\t\t\t| false\n\t},\n) => {\n\tconst domain: Domain.Any = service.domain\n\t// The JSON form of a principal, and the copy that the Schema decodes from\n\t// it. The loader always gets the copy, with a cache or not, so a cache can\n\t// not change an answer: a form may lose data that `Equal` does not see,\n\t// as `-0` becomes `0`. A principal with no such form gives `undefined`.\n\tconst canonicalOf = jsonKey(domain.principal)\n\tconst custom = options.cache === false ? undefined : options.cache?.key\n\t// It gives `undefined` when the principal has no key. Such a principal does\n\t// not use the cache.\n\tconst keyOf = (\n\t\tprincipal: object,\n\t\tcanonical: PrincipalKey | undefined,\n\t): PrincipalKey | undefined => {\n\t\tif (custom === undefined) {\n\t\t\treturn canonical\n\t\t}\n\t\tconst text = custom(principal)\n\t\treturn text === undefined\n\t\t\t? undefined\n\t\t\t: new PrincipalKey(canonical?.principal ?? principal, text)\n\t}\n\treturn Layer.effect(\n\t\tservice,\n\t\tEffect.gen(function* () {\n\t\t\t// The services of the policy loader and of the predicates come from the\n\t\t\t// layer, so a check does not need them.\n\t\t\tconst services = yield* Effect.context<never>()\n\n\t\t\t// The predicates of the policy get the services of the layer, so the\n\t\t\t// policy and its residual conditions need no services.\n\t\t\t// A policy of another domain may have the same types, but its resources\n\t\t\t// may have other paths. So it fails closed, as a policy that can not be\n\t\t\t// loaded.\n\t\t\tconst load = (principal: object) =>\n\t\t\t\toptions.policy(principal).pipe(\n\t\t\t\t\tEffect.provideContext(services),\n\t\t\t\t\tEffect.filterOrFail(\n\t\t\t\t\t\t(value) => value.domain === domain,\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\tnew globalThis.Error(\n\t\t\t\t\t\t\t\t\"The loaded policy is not a policy of this domain\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.mapError((cause) => PolicyUnavailable.make({ cause })),\n\t\t\t\t\tEffect.map((value) =>\n\t\t\t\t\t\tPolicies.provideContext(value, services as never),\n\t\t\t\t\t),\n\t\t\t\t)\n\n\t\t\tconst cache =\n\t\t\t\toptions.cache === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: yield* Cache.make({\n\t\t\t\t\t\t\tcapacity: options.cache?.capacity ?? 1000,\n\t\t\t\t\t\t\ttimeToLive: options.cache?.timeToLive ?? \"30 seconds\",\n\t\t\t\t\t\t\tlookup: (key: PrincipalKey) => load(key.principal),\n\t\t\t\t\t\t})\n\n\t\t\tconst policyFor = (principal: object) => {\n\t\t\t\tconst canonical = canonicalOf(principal)\n\t\t\t\tconst key =\n\t\t\t\t\tcache === undefined ? undefined : keyOf(principal, canonical)\n\t\t\t\tif (cache === undefined || key === undefined) {\n\t\t\t\t\treturn load(canonical?.principal ?? principal)\n\t\t\t\t}\n\t\t\t\t// A failed lookup is kept in the cache. Remove it, so the next check\n\t\t\t\t// tries again.\n\t\t\t\treturn Cache.get(cache, key).pipe(\n\t\t\t\t\tEffect.tapError(() => Cache.invalidate(cache, key)),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst withServices = <A, E>(\n\t\t\t\teffect: Effect.Effect<A, E, unknown>,\n\t\t\t): Effect.Effect<A, E, unknown> => Effect.provideContext(effect, services)\n\n\t\t\tconst current = Effect.gen(function* () {\n\t\t\t\tconst principal = yield* service.principal\n\t\t\t\tconst value = yield* policyFor(principal)\n\t\t\t\treturn { principal, policy: value }\n\t\t\t})\n\n\t\t\t// The principal and its policy, loaded one time. A check that asks\n\t\t\t// the policy two times, such as `guard`, uses one snapshot for both,\n\t\t\t// so the two answers come from one principal.\n\t\t\ttype Snapshot = Effect.Success<typeof current>\n\n\t\t\tconst decideWith = Effect.fnUntraced(function* (\n\t\t\t\t{ principal, policy: value }: Snapshot,\n\t\t\t\ttarget: AnyTarget,\n\t\t\t) {\n\t\t\t\tconst decision: Decision.Decision = yield* withServices(\n\t\t\t\t\tDecide.decideAny(value, { ...target, principal } as never),\n\t\t\t\t)\n\t\t\t\treturn decision\n\t\t\t})\n\n\t\t\tconst decide = (target: AnyTarget) =>\n\t\t\t\tEffect.flatMap(current, (snapshot) => decideWith(snapshot, target))\n\n\t\t\tconst checkWith = Effect.fnUntraced(function* (\n\t\t\t\t{ principal, policy: value }: Snapshot,\n\t\t\t\ttarget: AnyTarget,\n\t\t\t) {\n\t\t\t\tconst kind =\n\t\t\t\t\ttarget.resource === undefined\n\t\t\t\t\t\t? (target.kind ?? \"\")\n\t\t\t\t\t\t: (ResourceData.kindOf(target.resource) ?? \"\")\n\t\t\t\t// A request that does not fit its kind gets a \"no\" at once. The\n\t\t\t\t// decider must not say yes to an action that the kind does not have.\n\t\t\t\tconst descriptor = DomainData.descriptorOf(domain, kind)\n\t\t\t\tif (descriptor === undefined || !Decide.fitsKind(descriptor, target)) {\n\t\t\t\t\treturn yield* forbidden(target, kind, undefined)\n\t\t\t\t}\n\t\t\t\tconst fromPolicy: Decision.Decision =\n\t\t\t\t\ttarget.resource === undefined\n\t\t\t\t\t\t? Analysis.decideKind(value, { ...target, principal } as never)\n\t\t\t\t\t\t: yield* withServices(\n\t\t\t\t\t\t\t\tDecide.decideAny(value, { ...target, principal } as never),\n\t\t\t\t\t\t\t)\n\t\t\t\t// When no rule gives an answer, the decider gives it.\n\t\t\t\tconst decision: Decision.Decision =\n\t\t\t\t\tfromPolicy._tag === \"NotApplicable\" && options.decider !== undefined\n\t\t\t\t\t\t? yield* options.decider({ ...target, principal })\n\t\t\t\t\t\t: fromPolicy\n\t\t\t\tif (decision._tag !== \"Allow\") {\n\t\t\t\t\treturn yield* forbidden(target, kind, Decision.reason(decision))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tconst check = (target: AnyTarget) =>\n\t\t\t\tEffect.flatMap(current, (snapshot) => checkWith(snapshot, target))\n\n\t\t\tconst guard = Effect.fnUntraced(function* (\n\t\t\t\ttarget: AnyTarget,\n\t\t\t\tguardOptions: {\n\t\t\t\t\treadonly read?: string\n\t\t\t\t\treadonly hide: () => unknown\n\t\t\t\t},\n\t\t\t) {\n\t\t\t\tconst snapshot = yield* current\n\t\t\t\tconst read = guardOptions.read ?? \"read\"\n\t\t\t\tconst { field: _field, ...rest } = target\n\t\t\t\tconst visible: Decision.Decision = yield* decideWith(snapshot, {\n\t\t\t\t\t...rest,\n\t\t\t\t\taction: read,\n\t\t\t\t})\n\t\t\t\tif (visible._tag !== \"Allow\") {\n\t\t\t\t\treturn yield* Effect.fail(guardOptions.hide())\n\t\t\t\t}\n\t\t\t\tif (target.action !== read || target.field !== undefined) {\n\t\t\t\t\tyield* checkWith(snapshot, target)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// The filter is the condition of the coverage: the scope for `All`,\n\t\t\t// `False` for `None`, and else the residual condition in the scope.\n\t\t\t// The residual throws an error when it is too large, so the filter\n\t\t\t// fails.\n\t\t\tconst filter = (\n\t\t\t\ttarget: AnyTarget,\n\t\t\t): Effect.Effect<Condition.Ast, unknown, unknown> =>\n\t\t\t\tEffect.map(\n\t\t\t\t\tcurrent,\n\t\t\t\t\t({ principal, policy: value }): Condition.Ast =>\n\t\t\t\t\t\tAnalysis.coverageAny(value, { ...target, principal } as never)\n\t\t\t\t\t\t\t.condition as Condition.Ast,\n\t\t\t\t)\n\n\t\t\tconst coverage = (\n\t\t\t\ttarget: AnyTarget,\n\t\t\t): Effect.Effect<Analysis.AnyCoverage, unknown, unknown> =>\n\t\t\t\tEffect.map(current, ({ principal, policy: value }) =>\n\t\t\t\t\tAnalysis.coverageAny(value, { ...target, principal } as never),\n\t\t\t\t)\n\n\t\t\tconst permittedFields = Effect.fnUntraced(function* (target: AnyTarget) {\n\t\t\t\tconst { principal, policy: value } = yield* current\n\t\t\t\treturn yield* withServices(\n\t\t\t\t\tDecide.permittedFields(value, { ...target, principal } as never),\n\t\t\t\t)\n\t\t\t})\n\n\t\t\tconst invalidate = (principal?: object) => {\n\t\t\t\tif (cache === undefined) {\n\t\t\t\t\treturn Effect.void\n\t\t\t\t}\n\t\t\t\tif (principal === undefined) {\n\t\t\t\t\treturn Cache.invalidateAll(cache)\n\t\t\t\t}\n\t\t\t\tconst key = keyOf(principal, canonicalOf(principal))\n\t\t\t\treturn key === undefined ? Effect.void : Cache.invalidate(cache, key)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tdomain,\n\t\t\t\tprincipal: service.principal,\n\t\t\t\tpolicy: Effect.map(current, ({ policy: value }) => value),\n\t\t\t\tdecide,\n\t\t\t\tcheck,\n\t\t\t\tguard,\n\t\t\t\tfilter,\n\t\t\t\tcoverage,\n\t\t\t\tpermittedFields,\n\t\t\t\tinvalidate,\n\t\t\t} as never\n\t\t}),\n\t)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"authorizer.js","names":["Policies.provideContext","Decide.decideAny","ResourceData.kindOf","DomainData.descriptorOf","Decide.fitsKind","Analysis.decideKind","Decision.reason","Analysis.coverageAny","permittedFields","Decide.permittedFields"],"sources":["../../src/internal/authorizer.ts"],"sourcesContent":["/**\n * The values of authorizers: the errors, the service class and the layer.\n * The types are in `Authorizer.ts`.\n */\n// The service erases the error type and the service type of predicates,\n// principals and deciders to `unknown`. The public types keep them, so the\n// language service check for `unknown` in an Effect is off here.\n// @effect-diagnostics anyUnknownInErrorContext:off\nimport * as Cache from \"effect/Cache\"\nimport * as Context from \"effect/Context\"\nimport type * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport * as Equal from \"effect/Equal\"\nimport * as Hash from \"effect/Hash\"\nimport * as Layer from \"effect/Layer\"\nimport * as Result from \"effect/Result\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Authorizer from \"../Authorizer.ts\"\nimport type * as Condition from \"../Condition.ts\"\nimport * as Decision from \"../Decision.ts\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Policy from \"../Policy.ts\"\nimport * as Analysis from \"./analysis.ts\"\nimport * as Decide from \"./decide.ts\"\nimport * as DomainData from \"./domain.ts\"\nimport * as Policies from \"./policy.ts\"\nimport * as ResourceData from \"./resource.ts\"\n\ntype AnyServiceClass = Authorizer.AnyServiceClass\n\n/** The error when a policy says no. */\nexport class Forbidden extends Schema.TaggedError<Forbidden>(\n\t\"@nmnmcc/ability/Authorizer/Forbidden\",\n)(\"Forbidden\", {\n\taction: Schema.String,\n\tkind: Schema.String,\n\tfield: Schema.optionalKey(Schema.String),\n\treason: Schema.optionalKey(Schema.String),\n}) {}\n\n/** The error when the policy of the principal can not be loaded. */\nexport class PolicyUnavailable extends Schema.TaggedError<PolicyUnavailable>(\n\t\"@nmnmcc/ability/Authorizer/PolicyUnavailable\",\n)(\"PolicyUnavailable\", { cause: Schema.Defect() }) {}\n\n/** The error when a decider must wait for an answer. */\nexport class Pending extends Schema.TaggedError<Pending>(\n\t\"@nmnmcc/ability/Authorizer/Pending\",\n)(\"Pending\", {\n\tticket: Schema.String,\n\treason: Schema.optionalKey(Schema.String),\n}) {}\n\n/** Makes an authorizer service class for a domain. */\nexport const Service =\n\t() =>\n\t(\n\t\tid: string,\n\t\toptions: {\n\t\t\treadonly domain: Domain.Any\n\t\t\treadonly principal: Effect.Effect<any, any, any>\n\t\t},\n\t\t// The class has no name, and its type uses types of Effect that a\n\t\t// declaration file can not name. So this function gives a `Context.Key`.\n\t\t// The public module gives the full type.\n\t): Context.Key<any, any> => {\n\t\tconst Base = Context.Service<any, any>()(id)\n\t\treturn class extends Base {\n\t\t\tstatic readonly domain = options.domain\n\t\t\tstatic readonly principal = options.principal\n\t\t\tstatic readonly decide = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.decide(target))\n\t\t\tstatic readonly check = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.check(target))\n\t\t\tstatic readonly guard = (target: AnyTarget, options: unknown) =>\n\t\t\t\tthis.use((service: any) => service.guard(target, options))\n\t\t\tstatic readonly filter = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.filter(target))\n\t\t\tstatic readonly coverage = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.coverage(target))\n\t\t\tstatic readonly permittedFields = (target: AnyTarget) =>\n\t\t\t\tthis.use((service: any) => service.permittedFields(target))\n\t\t}\n\t}\n\ntype AnyTarget = {\n\treadonly action: string\n\treadonly resource?: object\n\treadonly kind?: string\n\treadonly field?: string\n\treadonly within?: unknown\n}\n\n/**\n * A cache key for a principal. Two keys are equal when they have the same\n * text, so two principals with the same key share one policy. A key from the\n * JSON form also needs equal decoded principals: a Schema may make data when\n * it decodes, as `expired` from a time, so one text may give two principals.\n * The key also keeps the principal, so the cache can load its policy. A plain\n * `Data` value can not do all this, so the key gives its own equality with\n * `Equal.Equal`.\n */\nclass PrincipalKey implements Equal.Equal {\n\treadonly principal: object\n\treadonly text: string\n\t/** The key is the JSON form, so the decoded principals must be equal too. */\n\treadonly exact: boolean\n\tconstructor(principal: object, text: string, exact: boolean) {\n\t\tthis.principal = principal\n\t\tthis.text = text\n\t\tthis.exact = exact\n\t}\n\t[Equal.symbol](that: unknown): boolean {\n\t\treturn (\n\t\t\tthat instanceof PrincipalKey &&\n\t\t\tthat.text === this.text &&\n\t\t\t(!this.exact || Equal.equals(that.principal, this.principal))\n\t\t)\n\t}\n\t[Hash.symbol](): number {\n\t\treturn Hash.string(this.text)\n\t}\n}\n\n/**\n * Makes the cache key of a principal from its JSON form. The key keeps the\n * principal that the Schema decodes from that form, so the loader sees only\n * the data that the key shows. A principal gets no key when it does not\n * encode, or when the decoded value is not `Equal` to it. Two classes, or a\n * string and a number, may have one JSON form, as in a union, and `Equal`\n * tells them apart. So two principals with one key are equal, and the cache\n * can not give one the policy of the other.\n */\n/**\n * Gives the JSON codec of a Schema, or `undefined` when the Schema has no JSON\n * form, as a struct with a symbol key has none.\n */\nconst jsonCodecOf = (\n\tschema: Schema.Top,\n): Schema.Codec<unknown, unknown> | undefined => {\n\ttry {\n\t\treturn Schema.toCodecJson(schema) as unknown as Schema.Codec<\n\t\t\tunknown,\n\t\t\tunknown\n\t\t>\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst jsonKey = (\n\tschema: Schema.Top,\n): ((principal: object) => PrincipalKey | undefined) => {\n\tconst codec = jsonCodecOf(schema)\n\t// A principal with no JSON form is never kept.\n\tif (codec === undefined) {\n\t\treturn () => undefined\n\t}\n\tconst encode = Schema.encodeUnknownResult(codec)\n\tconst decode = Schema.decodeUnknownResult(codec)\n\tconst keyOf = (principal: object): PrincipalKey | undefined =>\n\t\tResult.match(encode(principal), {\n\t\t\tonFailure: () => undefined,\n\t\t\tonSuccess: (json) => {\n\t\t\t\t// The key is the text, so the check decodes the text. The encoded\n\t\t\t\t// value may hold more than its text shows, as an object of a\n\t\t\t\t// `Schema.Any` field does: `JSON.stringify` leaves out a map.\n\t\t\t\tconst text = JSON.stringify(json)\n\t\t\t\treturn Result.match(decode(JSON.parse(text)), {\n\t\t\t\t\tonFailure: () => undefined,\n\t\t\t\t\tonSuccess: (decoded) =>\n\t\t\t\t\t\tEqual.equals(decoded, principal)\n\t\t\t\t\t\t\t? new PrincipalKey(decoded as object, text, true)\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t})\n\t\t\t},\n\t\t})\n\treturn (principal) => {\n\t\t// A very deep principal may fill the stack of the encoder or of\n\t\t// `Equal`. Such a principal is not kept.\n\t\ttry {\n\t\t\treturn keyOf(principal)\n\t\t} catch {\n\t\t\treturn undefined\n\t\t}\n\t}\n}\n\nconst forbidden = (\n\ttarget: AnyTarget,\n\tkind: string,\n\treason: string | undefined,\n): Forbidden =>\n\tForbidden.make({\n\t\taction: target.action,\n\t\tkind,\n\t\t...(target.field === undefined ? {} : { field: target.field }),\n\t\t...(reason === undefined ? {} : { reason }),\n\t})\n\n/** Makes the layer of an authorizer. */\nexport const makeLayer = (\n\tservice: AnyServiceClass,\n\toptions: {\n\t\treadonly policy: (\n\t\t\tprincipal: object,\n\t\t) => Effect.Effect<Policy.Any, unknown, unknown>\n\t\treadonly decider?: (\n\t\t\trequest: object,\n\t\t) => Effect.Effect<Decision.Decision, unknown, unknown>\n\t\treadonly cache?:\n\t\t\t| {\n\t\t\t\t\treadonly capacity?: number\n\t\t\t\t\treadonly timeToLive?: Duration.Input\n\t\t\t\t\treadonly key?: (principal: object) => string | undefined\n\t\t\t }\n\t\t\t| false\n\t},\n) => {\n\tconst domain: Domain.Any = service.domain\n\t// The JSON form of a principal, and the copy that the Schema decodes from\n\t// it. The loader always gets the copy, with a cache or not, so a cache can\n\t// not change an answer: a form may lose data that `Equal` does not see,\n\t// as `-0` becomes `0`. A principal with no such form gives `undefined`.\n\tconst canonicalOf = jsonKey(domain.principal)\n\tconst custom = options.cache === false ? undefined : options.cache?.key\n\t// It gives `undefined` when the principal has no key. Such a principal does\n\t// not use the cache.\n\tconst keyOf = (\n\t\tprincipal: object,\n\t\tcanonical: PrincipalKey | undefined,\n\t): PrincipalKey | undefined => {\n\t\tif (custom === undefined) {\n\t\t\treturn canonical\n\t\t}\n\t\tconst text = custom(principal)\n\t\treturn text === undefined\n\t\t\t? undefined\n\t\t\t: new PrincipalKey(canonical?.principal ?? principal, text, false)\n\t}\n\treturn Layer.effect(\n\t\tservice,\n\t\tEffect.gen(function* () {\n\t\t\t// The services of the policy loader and of the predicates come from the\n\t\t\t// layer, so a check does not need them.\n\t\t\tconst services = yield* Effect.context<never>()\n\n\t\t\t// The predicates of the policy get the services of the layer, so the\n\t\t\t// policy and its residual conditions need no services.\n\t\t\t// A policy of another domain may have the same types, but its resources\n\t\t\t// may have other paths. So it fails closed, as a policy that can not be\n\t\t\t// loaded.\n\t\t\tconst load = (principal: object) =>\n\t\t\t\toptions.policy(principal).pipe(\n\t\t\t\t\tEffect.provideContext(services),\n\t\t\t\t\tEffect.filterOrFail(\n\t\t\t\t\t\t(value) => value.domain === domain,\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\tnew globalThis.Error(\n\t\t\t\t\t\t\t\t\"The loaded policy is not a policy of this domain\",\n\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t\tEffect.mapError((cause) => PolicyUnavailable.make({ cause })),\n\t\t\t\t\tEffect.map((value) =>\n\t\t\t\t\t\tPolicies.provideContext(value, services as never),\n\t\t\t\t\t),\n\t\t\t\t)\n\n\t\t\tconst cache =\n\t\t\t\toptions.cache === false\n\t\t\t\t\t? undefined\n\t\t\t\t\t: yield* Cache.make({\n\t\t\t\t\t\t\tcapacity: options.cache?.capacity ?? 1000,\n\t\t\t\t\t\t\ttimeToLive: options.cache?.timeToLive ?? \"30 seconds\",\n\t\t\t\t\t\t\tlookup: (key: PrincipalKey) => load(key.principal),\n\t\t\t\t\t\t})\n\n\t\t\tconst policyFor = (principal: object) => {\n\t\t\t\tconst canonical = canonicalOf(principal)\n\t\t\t\tconst key =\n\t\t\t\t\tcache === undefined ? undefined : keyOf(principal, canonical)\n\t\t\t\tif (cache === undefined || key === undefined) {\n\t\t\t\t\treturn load(canonical?.principal ?? principal)\n\t\t\t\t}\n\t\t\t\t// A failed lookup is kept in the cache. Remove it, so the next check\n\t\t\t\t// tries again.\n\t\t\t\treturn Cache.get(cache, key).pipe(\n\t\t\t\t\tEffect.tapError(() => Cache.invalidate(cache, key)),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst withServices = <A, E>(\n\t\t\t\teffect: Effect.Effect<A, E, unknown>,\n\t\t\t): Effect.Effect<A, E, unknown> => Effect.provideContext(effect, services)\n\n\t\t\tconst current = Effect.gen(function* () {\n\t\t\t\tconst principal = yield* service.principal\n\t\t\t\tconst value = yield* policyFor(principal)\n\t\t\t\treturn { principal, policy: value }\n\t\t\t})\n\n\t\t\t// The principal and its policy, loaded one time. A check that asks\n\t\t\t// the policy two times, such as `guard`, uses one snapshot for both,\n\t\t\t// so the two answers come from one principal.\n\t\t\ttype Snapshot = Effect.Success<typeof current>\n\n\t\t\tconst decideWith = Effect.fnUntraced(function* (\n\t\t\t\t{ principal, policy: value }: Snapshot,\n\t\t\t\ttarget: AnyTarget,\n\t\t\t) {\n\t\t\t\tconst decision: Decision.Decision = yield* withServices(\n\t\t\t\t\tDecide.decideAny(value, { ...target, principal } as never),\n\t\t\t\t)\n\t\t\t\treturn decision\n\t\t\t})\n\n\t\t\tconst decide = (target: AnyTarget) =>\n\t\t\t\tEffect.flatMap(current, (snapshot) => decideWith(snapshot, target))\n\n\t\t\tconst checkWith = Effect.fnUntraced(function* (\n\t\t\t\t{ principal, policy: value }: Snapshot,\n\t\t\t\ttarget: AnyTarget,\n\t\t\t) {\n\t\t\t\tconst kind =\n\t\t\t\t\ttarget.resource === undefined\n\t\t\t\t\t\t? (target.kind ?? \"\")\n\t\t\t\t\t\t: (ResourceData.kindOf(target.resource) ?? \"\")\n\t\t\t\t// A request that does not fit its kind gets a \"no\" at once. The\n\t\t\t\t// decider must not say yes to an action that the kind does not have.\n\t\t\t\tconst descriptor = DomainData.descriptorOf(domain, kind)\n\t\t\t\tif (descriptor === undefined || !Decide.fitsKind(descriptor, target)) {\n\t\t\t\t\treturn yield* forbidden(target, kind, undefined)\n\t\t\t\t}\n\t\t\t\tconst fromPolicy: Decision.Decision =\n\t\t\t\t\ttarget.resource === undefined\n\t\t\t\t\t\t? Analysis.decideKind(value, { ...target, principal } as never)\n\t\t\t\t\t\t: yield* withServices(\n\t\t\t\t\t\t\t\tDecide.decideAny(value, { ...target, principal } as never),\n\t\t\t\t\t\t\t)\n\t\t\t\t// When no rule gives an answer, the decider gives it.\n\t\t\t\tconst decision: Decision.Decision =\n\t\t\t\t\tfromPolicy._tag === \"NotApplicable\" && options.decider !== undefined\n\t\t\t\t\t\t? yield* options.decider({ ...target, principal })\n\t\t\t\t\t\t: fromPolicy\n\t\t\t\tif (decision._tag !== \"Allow\") {\n\t\t\t\t\treturn yield* forbidden(target, kind, Decision.reason(decision))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tconst check = (target: AnyTarget) =>\n\t\t\t\tEffect.flatMap(current, (snapshot) => checkWith(snapshot, target))\n\n\t\t\tconst guard = Effect.fnUntraced(function* (\n\t\t\t\ttarget: AnyTarget,\n\t\t\t\tguardOptions: {\n\t\t\t\t\treadonly read?: string\n\t\t\t\t\treadonly hide: () => unknown\n\t\t\t\t},\n\t\t\t) {\n\t\t\t\tconst snapshot = yield* current\n\t\t\t\tconst read = guardOptions.read ?? \"read\"\n\t\t\t\tconst { field: _field, ...rest } = target\n\t\t\t\tconst visible: Decision.Decision = yield* decideWith(snapshot, {\n\t\t\t\t\t...rest,\n\t\t\t\t\taction: read,\n\t\t\t\t})\n\t\t\t\tif (visible._tag !== \"Allow\") {\n\t\t\t\t\treturn yield* Effect.fail(guardOptions.hide())\n\t\t\t\t}\n\t\t\t\tif (target.action !== read || target.field !== undefined) {\n\t\t\t\t\tyield* checkWith(snapshot, target)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// The filter is the condition of the coverage: the scope for `All`,\n\t\t\t// `False` for `None`, and else the residual condition in the scope.\n\t\t\t// The residual throws an error when it is too large, so the filter\n\t\t\t// fails.\n\t\t\tconst filter = (\n\t\t\t\ttarget: AnyTarget,\n\t\t\t): Effect.Effect<Condition.Ast, unknown, unknown> =>\n\t\t\t\tEffect.map(\n\t\t\t\t\tcurrent,\n\t\t\t\t\t({ principal, policy: value }): Condition.Ast =>\n\t\t\t\t\t\tAnalysis.coverageAny(value, { ...target, principal } as never)\n\t\t\t\t\t\t\t.condition as Condition.Ast,\n\t\t\t\t)\n\n\t\t\tconst coverage = (\n\t\t\t\ttarget: AnyTarget,\n\t\t\t): Effect.Effect<Analysis.AnyCoverage, unknown, unknown> =>\n\t\t\t\tEffect.map(current, ({ principal, policy: value }) =>\n\t\t\t\t\tAnalysis.coverageAny(value, { ...target, principal } as never),\n\t\t\t\t)\n\n\t\t\tconst permittedFields = Effect.fnUntraced(function* (target: AnyTarget) {\n\t\t\t\tconst { principal, policy: value } = yield* current\n\t\t\t\treturn yield* withServices(\n\t\t\t\t\tDecide.permittedFields(value, { ...target, principal } as never),\n\t\t\t\t)\n\t\t\t})\n\n\t\t\tconst invalidate = (principal?: object) => {\n\t\t\t\tif (cache === undefined) {\n\t\t\t\t\treturn Effect.void\n\t\t\t\t}\n\t\t\t\tif (principal === undefined) {\n\t\t\t\t\treturn Cache.invalidateAll(cache)\n\t\t\t\t}\n\t\t\t\tconst key = keyOf(principal, canonicalOf(principal))\n\t\t\t\treturn key === undefined ? Effect.void : Cache.invalidate(cache, key)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tdomain,\n\t\t\t\tprincipal: service.principal,\n\t\t\t\tpolicy: Effect.map(current, ({ policy: value }) => value),\n\t\t\t\tdecide,\n\t\t\t\tcheck,\n\t\t\t\tguard,\n\t\t\t\tfilter,\n\t\t\t\tcoverage,\n\t\t\t\tpermittedFields,\n\t\t\t\tinvalidate,\n\t\t\t} as never\n\t\t}),\n\t)\n}\n"],"mappings":"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"}
@@ -1,6 +1,8 @@
1
+ import { searchSteps } from "./limits.js";
1
2
  import { isNode } from "./nodes.js";
2
3
  import { accepts, anyKinds, lookup } from "./catalog.js";
3
4
  import { fitsCatalog, parse } from "./patterns.js";
5
+ import { at, pathOf } from "./place.js";
4
6
  import { withSuggestion } from "./suggest.js";
5
7
  import * as Result from "effect/Result";
6
8
  import * as Arr from "effect/Array";
@@ -21,6 +23,10 @@ const makeMemo = () => ({
21
23
  issues: {
22
24
  count: 0,
23
25
  noted: false
26
+ },
27
+ work: {
28
+ steps: 0,
29
+ noted: false
24
30
  }
25
31
  });
26
32
  /**
@@ -74,6 +80,22 @@ const fieldIssue = (context, field, at) => {
74
80
  }];
75
81
  };
76
82
  /**
83
+ * The issues of a field that a condition reads: a field that is not a field
84
+ * of every resource of the rule, or a field that is not plain data in one of
85
+ * them. A read of a value that is not plain data, as a getter of a class, is
86
+ * a defect in a decision. A residual may leave the read out, so the residual
87
+ * would say "yes" where the decision fails. So the binding does not take
88
+ * such a read: see `Catalog.Field.plain`.
89
+ */
90
+ const readIssues = (context, field, at) => {
91
+ const exists = fieldIssue(context, field, at);
92
+ if (exists.length > 0) return exists;
93
+ return Arr.some(context.named, (descriptor) => !(lookup(descriptor.catalog, field)?.plain ?? true)) ? [{
94
+ path: at,
95
+ issue: `A condition may not read the field "${field}". A value above it may be an object that does not have it as a data field, as a class with a getter, a date or a list. Use a predicate for it.`
96
+ }] : [];
97
+ };
98
+ /**
77
99
  * The issues of a field pattern: a pattern that does not parse, or that
78
100
  * matches no field of the resources of the rule, as a pattern with a mistake
79
101
  * in a name does.
@@ -83,17 +105,29 @@ const patternIssues = (memo, known, pattern, at) => Result.match(parse(pattern),
83
105
  path: at,
84
106
  issue
85
107
  }],
86
- onSuccess: (parsed) => known.length === 0 || Arr.some(known, (descriptor) => {
87
- const key = `${descriptor.kind}\u0000${pattern}`;
88
- return Option.getOrElse(MutableHashMap.get(memo.fits, key), () => {
89
- const fits = fitsCatalog(parsed, descriptor.catalog);
90
- MutableHashMap.set(memo.fits, key, fits);
91
- return fits;
108
+ onSuccess: (parsed) => {
109
+ const fits = known.length === 0 || Arr.reduce(known, false, (before, descriptor) => {
110
+ const key = `${descriptor.kind}\u0000${pattern}`;
111
+ return Option.getOrElse(MutableHashMap.get(memo.fits, key), () => {
112
+ memo.work.steps += descriptor.catalog.fields.length * Math.max(parsed.length, 1);
113
+ if (memo.work.steps > 1e6) return true;
114
+ const found = fitsCatalog(parsed, descriptor.catalog);
115
+ MutableHashMap.set(memo.fits, key, found);
116
+ return found;
117
+ }) || before;
92
118
  });
93
- }) ? [] : [{
94
- path: at,
95
- issue: `The field pattern "${pattern}" matches no field of the resources of the rule`
96
- }]
119
+ if (memo.work.steps > 1e6 && !memo.work.noted) {
120
+ memo.work.noted = true;
121
+ return [{
122
+ path: at,
123
+ issue: `The field patterns of the policy take more than ${String(searchSteps)} steps to check. The most is ${String(searchSteps)}.`
124
+ }];
125
+ }
126
+ return fits ? [] : [{
127
+ path: at,
128
+ issue: `The field pattern "${pattern}" matches no field of the resources of the rule`
129
+ }];
130
+ }
97
131
  });
98
132
  /** The fields of the resources that a test of the rule may read. */
99
133
  const fieldsAt = (context, field) => Arr.flatMap(context.applicable, (descriptor) => {
@@ -219,14 +253,14 @@ const conditionIssues = (context, node, at, seen) => {
219
253
  const here = (issues) => under(context.memo, at, issues);
220
254
  switch (node._tag) {
221
255
  case "Compare": {
222
- const exists = fieldIssue(context, node.field, ["field"]);
256
+ const exists = readIssues(context, node.field, ["field"]);
223
257
  return here(exists.length > 0 ? exists : operandIssues(context, node.field, node.op !== "eq" && node.op !== "ne", node.value, ["value"]));
224
258
  }
225
259
  case "In": {
226
- const exists = fieldIssue(context, node.field, ["field"]);
260
+ const exists = readIssues(context, node.field, ["field"]);
227
261
  return here(exists.length > 0 ? exists : Arr.flatMap(node.values, (operand, index) => operandIssues(context, node.field, false, operand, ["values", index])));
228
262
  }
229
- case "Exists": return here(fieldIssue(context, node.field, ["field"]));
263
+ case "Exists": return here(readIssues(context, node.field, ["field"]));
230
264
  case "Under": return here(underIssues(context, node.prefix, []));
231
265
  case "And":
232
266
  case "Or": return Arr.flatMap(node.conditions, (child, index) => conditionIssues(context, child, () => [
@@ -282,29 +316,21 @@ const ruleIssues = (domain, rule, at, memo = makeMemo()) => {
282
316
  };
283
317
  /** Checks each rule of a policy tree against a domain. */
284
318
  const treeIssues = (domain, root) => {
285
- const rulesOf = (node, path, level) => {
319
+ const rulesOf = (node, place, level) => {
286
320
  if (level > 256) return [{
287
- path,
321
+ path: pathOf(place),
288
322
  issue: `A policy tree has more than ${String(256)} levels. The most is ${String(256)}.`
289
323
  }];
290
324
  switch (node._tag) {
291
- case "Rules": return Arr.map(node.rules, (rule, index) => [rule, [
292
- ...path,
293
- "rules",
294
- index
295
- ]]);
325
+ case "Rules": return Arr.map(node.rules, (rule, index) => [rule, at(place, "rules", index)]);
296
326
  case "All":
297
327
  case "Any":
298
- case "FirstOf": return Arr.flatMap(node.policies, (child, index) => rulesOf(child, [
299
- ...path,
300
- "policies",
301
- index
302
- ], level + 1));
303
- case "Ceiling": return [...rulesOf(node.cap, [...path, "cap"], level + 1), ...rulesOf(node.policy, [...path, "policy"], level + 1)];
328
+ case "FirstOf": return Arr.flatMap(node.policies, (child, index) => rulesOf(child, at(place, "policies", index), level + 1));
329
+ case "Ceiling": return [...rulesOf(node.cap, at(place, "cap"), level + 1), ...rulesOf(node.policy, at(place, "policy"), level + 1)];
304
330
  }
305
331
  };
306
332
  const memo = makeMemo();
307
- return Arr.flatMap(rulesOf(root, [], 1), (found) => Array.isArray(found) ? ruleIssues(domain, found[0], found[1], memo) : [found]);
333
+ return Arr.flatMap(rulesOf(root, void 0, 1), (found) => Array.isArray(found) ? ruleIssues(domain, found[0], pathOf(found[1]), memo) : [found]);
308
334
  };
309
335
  export { ruleIssues, treeIssues };
310
336
 
@@ -1 +1 @@
1
- {"version":3,"file":"bind.js","names":["Catalog.accepts","Catalog.lookup","Patterns.parse","Patterns.fitsCatalog","Catalog.anyKinds","Nodes.isNode"],"sources":["../../src/internal/bind.ts"],"sourcesContent":["/**\n * The binding of rules to a domain. It is the one check of a rule, for a rule\n * from the builder, from `Policy.rule`, and from JSON alike. It reads the\n * catalogs of the resources and of the principal, and it gives one issue for\n * each problem, with the path of the problem and, for a name with a mistake,\n * the name that it may mean.\n */\nimport * as Arr from \"effect/Array\"\nimport * as HashMap from \"effect/HashMap\"\nimport * as MutableHashMap from \"effect/MutableHashMap\"\nimport * as Option from \"effect/Option\"\nimport * as Result from \"effect/Result\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Resource from \"../Resource.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Limits from \"./limits.ts\"\nimport * as Nodes from \"./nodes.ts\"\nimport * as Patterns from \"./patterns.ts\"\nimport { withSuggestion } from \"./suggest.ts\"\n\n/** One problem of a rule or a policy, with its path. */\nexport interface Issue {\n\treadonly path: ReadonlyArray<PropertyKey>\n\treadonly issue: string\n}\n\n/** The data of a rule that the binding reads. */\nexport interface RuleData {\n\treadonly actions: ReadonlyArray<string>\n\treadonly resources: ReadonlyArray<string>\n\treadonly when?: unknown\n\treadonly fields?: ReadonlyArray<string>\n}\n\n/**\n * The memo of one binding, for all its rules. A policy may name one wrong\n * field or one pattern in very many places, so the work on each is done one\n * time. It goes away when the binding ends.\n */\ninterface Memo {\n\t/** The suggestion for each name, by the place where it was looked for. */\n\treadonly suggestions: MutableHashMap.MutableHashMap<string, string>\n\t/** If a pattern matches a field of a kind, by the kind and the pattern. */\n\treadonly fits: MutableHashMap.MutableHashMap<string, boolean>\n\t/** The count of the issues of conditions so far. It belongs to one binding. */\n\treadonly issues: { count: number; noted: boolean }\n}\n\nconst makeMemo = (): Memo => ({\n\tsuggestions: MutableHashMap.empty(),\n\tfits: MutableHashMap.empty(),\n\tissues: { count: 0, noted: false },\n})\n\n/**\n * The most names that one binding makes a suggestion for. A suggestion\n * compares the name with every name of a catalog, so a policy with very many\n * wrong names would take a long time.\n */\nconst maxSuggestions = 32\n\n/** Adds a suggestion to a message, with the memo of the binding. */\nconst suggestIn = (\n\tmemo: Memo,\n\tmessage: string,\n\tname: string,\n\tscope: string,\n\tnames: () => ReadonlyArray<string>,\n): string => {\n\tconst key = `${scope}\\u0000${name}`\n\tconst known = MutableHashMap.get(memo.suggestions, key)\n\tif (Option.isSome(known)) {\n\t\treturn known.value\n\t}\n\tif (MutableHashMap.size(memo.suggestions) >= maxSuggestions) {\n\t\treturn message\n\t}\n\tconst made = withSuggestion(message, name, names())\n\tMutableHashMap.set(memo.suggestions, key, made)\n\treturn made\n}\n\n/** The kinds that a rule may run on, from the view of its checks. */\ninterface Context {\n\treadonly domain: Domain.Any\n\treadonly memo: Memo\n\t/** The rule is for every resource (`\"*\"`). */\n\treadonly all: boolean\n\t/** The resources that the rule names and that the domain has. */\n\treadonly named: ReadonlyArray<Resource.Descriptor>\n\t/** The named resources that have one of the actions of the rule. */\n\treadonly applicable: ReadonlyArray<Resource.Descriptor>\n\t/**\n\t * The resources whose paths a path test may read: every resource with one\n\t * of the actions, for a rule for every resource.\n\t */\n\treadonly paths: ReadonlyArray<Resource.Descriptor>\n}\n\ntype Operand = Nodes.Operand\n\n/**\n * Tests if a field may hold a value of the type of a fixed value. A test with\n * `null` is a test that the field is missing, so `null` always fits. A number\n * fits any field of numbers: a test of an integer field with `1.5` or\n * `Infinity` has a meaning, as `lt` shows.\n */\nconst fitsLiteral = (kinds: Catalog.Kinds, value: unknown): boolean =>\n\tvalue === null ||\n\t(typeof value === \"number\" && kinds.number !== \"none\") ||\n\tCatalog.accepts(kinds, value)\n\n/**\n * Tests if two fields may hold one scalar. A principal field that is `null`\n * or that holds an object is a missing value, and a test with a missing value\n * is false, so only scalars count.\n */\nconst meet = (left: Catalog.Kinds, right: Catalog.Kinds): boolean =>\n\t(left.string && right.string) ||\n\t(left.boolean && right.boolean) ||\n\t(left.number !== \"none\" && right.number !== \"none\") ||\n\tArr.some(left.literals, (value) => Catalog.accepts(right, value)) ||\n\tArr.some(right.literals, (value) => Catalog.accepts(left, value))\n\nconst hasStrings = (kinds: Catalog.Kinds): boolean =>\n\tkinds.string || Arr.some(kinds.literals, (value) => typeof value === \"string\")\n\nconst hasNumbers = (kinds: Catalog.Kinds): boolean =>\n\tkinds.number !== \"none\" ||\n\tArr.some(kinds.literals, (value) => typeof value === \"number\")\n\n/** Tests if a field may hold a value with an order: a number or a string. */\nconst isOrdered = (kinds: Catalog.Kinds): boolean =>\n\thasStrings(kinds) || hasNumbers(kinds)\n\n/** Tests if two fields may hold values of one ordered type. */\nconst orderedMeet = (left: Catalog.Kinds, right: Catalog.Kinds): boolean =>\n\t(hasStrings(left) && hasStrings(right)) ||\n\t(hasNumbers(left) && hasNumbers(right))\n\n/** The texts of the fields of a catalog that have only names, for suggestions. */\nconst namesOf = (catalog: Catalog.Catalog): ReadonlyArray<string> =>\n\tArr.map(\n\t\tArr.filter(catalog.fields, (field) =>\n\t\t\tArr.every(field.path, (segment) => typeof segment === \"string\"),\n\t\t),\n\t\t(field) => field.text,\n\t)\n\n/** The issue of a field that is not a field of every resource of the rule. */\nconst fieldIssue = (\n\tcontext: Context,\n\tfield: string,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> => {\n\tconst message = `The field \"${field}\" is not a field of every resource of the rule`\n\tif (context.all || context.named.length === 0) {\n\t\treturn [{ path: at, issue: message }]\n\t}\n\tconst without = Arr.filter(\n\t\tcontext.named,\n\t\t(descriptor) => Catalog.lookup(descriptor.catalog, field) === undefined,\n\t)\n\treturn without.length === 0\n\t\t? []\n\t\t: [\n\t\t\t\t{\n\t\t\t\t\tpath: at,\n\t\t\t\t\tissue: suggestIn(context.memo, message, field, without[0]!.kind, () =>\n\t\t\t\t\t\tnamesOf(without[0]!.catalog),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t]\n}\n\n/**\n * The issues of a field pattern: a pattern that does not parse, or that\n * matches no field of the resources of the rule, as a pattern with a mistake\n * in a name does.\n */\nconst patternIssues = (\n\tmemo: Memo,\n\tknown: ReadonlyArray<Resource.Descriptor>,\n\tpattern: string,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> =>\n\tResult.match(Patterns.parse(pattern), {\n\t\tonFailure: (issue) => [{ path: at, issue }],\n\t\tonSuccess: (parsed) =>\n\t\t\t// A rule with no known resource has an issue for that already.\n\t\t\tknown.length === 0 ||\n\t\t\tArr.some(known, (descriptor) => {\n\t\t\t\tconst key = `${descriptor.kind}\\u0000${pattern}`\n\t\t\t\treturn Option.getOrElse(MutableHashMap.get(memo.fits, key), () => {\n\t\t\t\t\tconst fits = Patterns.fitsCatalog(parsed, descriptor.catalog)\n\t\t\t\t\tMutableHashMap.set(memo.fits, key, fits)\n\t\t\t\t\treturn fits\n\t\t\t\t})\n\t\t\t})\n\t\t\t\t? []\n\t\t\t\t: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\t\tissue: `The field pattern \"${pattern}\" matches no field of the resources of the rule`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t})\n\n/** The fields of the resources that a test of the rule may read. */\nconst fieldsAt = (\n\tcontext: Context,\n\tfield: string,\n): ReadonlyArray<Catalog.Field> =>\n\tArr.flatMap(context.applicable, (descriptor) => {\n\t\tconst found = Catalog.lookup(descriptor.catalog, field)\n\t\treturn found === undefined ? [] : [found]\n\t})\n\n/** The field of the principal that an operand names, or the issue when there is none. */\nconst principalField = (\n\tcontext: Context,\n\tref: string,\n\tat: ReadonlyArray<PropertyKey>,\n): Catalog.Field | Issue => {\n\tconst found = Catalog.lookup(context.domain.principalCatalog, ref)\n\treturn found !== undefined\n\t\t? found\n\t\t: {\n\t\t\t\tpath: at,\n\t\t\t\tissue: suggestIn(\n\t\t\t\t\tcontext.memo,\n\t\t\t\t\t`The principal has no field \"${ref}\"`,\n\t\t\t\t\tref,\n\t\t\t\t\t\"~principal\",\n\t\t\t\t\t() => namesOf(context.domain.principalCatalog),\n\t\t\t\t),\n\t\t\t}\n}\n\nconst isIssue = (value: Catalog.Field | Issue): value is Issue =>\n\t\"issue\" in value\n\n/** The issues of one operand of a test of a field. */\nconst operandIssues = (\n\tcontext: Context,\n\tfield: string,\n\tordered: boolean,\n\toperand: Operand,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> => {\n\tconst fields = fieldsAt(context, field)\n\tif (operand._tag === \"Literal\") {\n\t\tconst value = operand.value\n\t\tif (ordered && typeof value !== \"string\" && typeof value !== \"number\") {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tpath: at,\n\t\t\t\t\tissue: `The field \"${field}\" has an ordered test with a value that has no order`,\n\t\t\t\t},\n\t\t\t]\n\t\t}\n\t\treturn [\n\t\t\t...(Arr.some(fields, (found) => !fitsLiteral(found.kinds, value))\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\t\tissue: `The field \"${field}\" can not hold the value ${JSON.stringify(value)}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: []),\n\t\t\t...(ordered && Arr.some(fields, (found) => !isOrdered(found.kinds))\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\t\tissue: `The field \"${field}\" has an ordered test with a value that has no order`,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: []),\n\t\t]\n\t}\n\tconst principal = principalField(context, operand.path, at)\n\tif (isIssue(principal)) {\n\t\treturn [principal]\n\t}\n\treturn [\n\t\t...(Arr.some(fields, (found) => !meet(found.kinds, principal.kinds))\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\tissue: `The field \"${field}\" can not hold a value of the principal field \"${operand.path}\"`,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []),\n\t\t...(ordered &&\n\t\tArr.some(fields, (found) => !orderedMeet(found.kinds, principal.kinds))\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\tissue: `The field \"${field}\" has an ordered test with a value that has no order`,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []),\n\t]\n}\n\n/**\n * Tests if the path of a resource may start with a prefix: it is as long as\n * the prefix, and each fixed string of the prefix is the string of the path\n * at its place, when the path has a string there.\n */\nconst mayStartWith = (\n\tdescriptor: Resource.Descriptor,\n\tstrings: ReadonlyArray<string | null>,\n): boolean =>\n\t(descriptor.path?.length ?? 0) >= strings.length &&\n\tArr.every(strings, (string, index) => {\n\t\tconst segment = descriptor.path?.[index]\n\t\treturn string === null || typeof segment !== \"string\" || segment === string\n\t})\n\n/** The kinds of the part of the path of a resource at a place. */\nconst pathPartKinds = (\n\tdescriptor: Resource.Descriptor,\n\tindex: number,\n): Catalog.Kinds | string | undefined => {\n\tconst segment = descriptor.path?.[index]\n\treturn segment === undefined || typeof segment === \"string\"\n\t\t? segment\n\t\t: (Catalog.lookup(descriptor.catalog, segment.field)?.kinds ??\n\t\t\t\tCatalog.anyKinds)\n}\n\n/** The issues of the prefix of a path test. */\nconst underIssues = (\n\tcontext: Context,\n\tprefix: ReadonlyArray<Operand>,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> => {\n\tconst strings = Arr.map(prefix, (operand) =>\n\t\toperand._tag === \"Literal\" && typeof operand.value === \"string\"\n\t\t\t? operand.value\n\t\t\t: null,\n\t)\n\t// The test can match only a path that starts with the fixed strings of\n\t// the prefix, so the other resources are left out.\n\tconst candidates = Arr.filter(context.paths, (descriptor) =>\n\t\tmayStartWith(descriptor, strings),\n\t)\n\treturn Arr.flatMap(prefix, (operand, index): ReadonlyArray<Issue> => {\n\t\tconst place = [...at, \"prefix\", index]\n\t\tif (operand._tag === \"Literal\") {\n\t\t\treturn Arr.some(candidates, (descriptor) => {\n\t\t\t\tconst kinds = pathPartKinds(descriptor, index)\n\t\t\t\treturn typeof kinds === \"object\" && !fitsLiteral(kinds, operand.value)\n\t\t\t})\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: place,\n\t\t\t\t\t\t\tissue: `The path part ${String(index)} can not hold the value ${JSON.stringify(operand.value)}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: []\n\t\t}\n\t\tconst principal = principalField(context, operand.path, place)\n\t\tif (isIssue(principal)) {\n\t\t\treturn [principal]\n\t\t}\n\t\treturn Arr.some(candidates, (descriptor) => {\n\t\t\tconst kinds = pathPartKinds(descriptor, index)\n\t\t\treturn kinds === undefined\n\t\t\t\t? true\n\t\t\t\t: typeof kinds === \"string\"\n\t\t\t\t\t? !Catalog.accepts(principal.kinds, kinds)\n\t\t\t\t\t: !meet(kinds, principal.kinds)\n\t\t})\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: place,\n\t\t\t\t\t\tissue: `The principal field \"${operand.path}\" can not hold a part of the path of the resource`,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []\n\t})\n}\n\nconst notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({\n\tpath: at,\n\tissue: \"The condition is not a whole condition tree\",\n})\n\n/**\n * The most issues that the walk of the conditions of one binding gives. A\n * policy may have very many wrong tests, and each issue keeps its path.\n */\nconst maxIssues = 100\n\n/** A path that the walk makes only when it needs it, for an issue. */\ntype LazyPath = () => ReadonlyArray<PropertyKey>\n\n/**\n * Puts the path of a node before the paths of its issues. A walk keeps no\n * path for a node with no issue, so a deep and wide condition costs little.\n */\nconst under = (\n\tmemo: Memo,\n\tat: LazyPath,\n\tissues: ReadonlyArray<Issue>,\n): ReadonlyArray<Issue> => {\n\tif (issues.length === 0) {\n\t\treturn issues\n\t}\n\tconst room = maxIssues - memo.issues.count\n\tmemo.issues.count += issues.length\n\tconst kept = Arr.map(Arr.take(issues, Math.max(room, 0)), (issue) => ({\n\t\tpath: [...at(), ...issue.path],\n\t\tissue: issue.issue,\n\t}))\n\tconst first = memo.issues.count > maxIssues && !memo.issues.noted\n\tmemo.issues.noted ||= first\n\treturn first\n\t\t? [\n\t\t\t\t...kept,\n\t\t\t\t{\n\t\t\t\t\tpath: [],\n\t\t\t\t\tissue: `The policy has more problems. The first ${String(maxIssues)} are here.`,\n\t\t\t\t},\n\t\t\t]\n\t\t: kept\n}\n\n/**\n * The issues of a condition. The constructors of the nodes checked the shape\n * of each node, so this checks the names and the types against the domain. A\n * node that the condition uses in many places is checked one time. The nodes\n * it has seen are in a native `Set`, because it must find a node by the node\n * itself: `Equal` compares nodes by structure.\n */\nconst conditionIssues = (\n\tcontext: Context,\n\tnode: Nodes.Node,\n\tat: LazyPath,\n\tseen: Set<Nodes.Node>,\n): ReadonlyArray<Issue> => {\n\tif (seen.has(node)) {\n\t\treturn []\n\t}\n\tseen.add(node)\n\tconst here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)\n\tswitch (node._tag) {\n\t\tcase \"Compare\": {\n\t\t\tconst exists = fieldIssue(context, node.field, [\"field\"])\n\t\t\treturn here(\n\t\t\t\texists.length > 0\n\t\t\t\t\t? exists\n\t\t\t\t\t: operandIssues(\n\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\tnode.field,\n\t\t\t\t\t\t\tnode.op !== \"eq\" && node.op !== \"ne\",\n\t\t\t\t\t\t\tnode.value,\n\t\t\t\t\t\t\t[\"value\"],\n\t\t\t\t\t\t),\n\t\t\t)\n\t\t}\n\t\tcase \"In\": {\n\t\t\tconst exists = fieldIssue(context, node.field, [\"field\"])\n\t\t\treturn here(\n\t\t\t\texists.length > 0\n\t\t\t\t\t? exists\n\t\t\t\t\t: Arr.flatMap(node.values, (operand, index) =>\n\t\t\t\t\t\t\toperandIssues(context, node.field, false, operand, [\n\t\t\t\t\t\t\t\t\"values\",\n\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t),\n\t\t\t)\n\t\t}\n\t\tcase \"Exists\":\n\t\t\treturn here(fieldIssue(context, node.field, [\"field\"]))\n\t\tcase \"Under\":\n\t\t\treturn here(underIssues(context, node.prefix, []))\n\t\tcase \"And\":\n\t\tcase \"Or\":\n\t\t\treturn Arr.flatMap(node.conditions, (child, index) =>\n\t\t\t\tconditionIssues(\n\t\t\t\t\tcontext,\n\t\t\t\t\tchild,\n\t\t\t\t\t() => [...at(), \"conditions\", index],\n\t\t\t\t\tseen,\n\t\t\t\t),\n\t\t\t)\n\t\tcase \"Not\":\n\t\t\treturn conditionIssues(\n\t\t\t\tcontext,\n\t\t\t\tnode.condition,\n\t\t\t\t() => [...at(), \"condition\"],\n\t\t\t\tseen,\n\t\t\t)\n\t\tdefault:\n\t\t\treturn []\n\t}\n}\n\nconst hasAction = (\n\tdescriptor: Resource.Descriptor,\n\tactions: ReadonlyArray<string>,\n): boolean =>\n\tactions.includes(\"*\") ||\n\tArr.some(actions, (action) => descriptor.actions.includes(action))\n\n/**\n * Checks one rule against a domain. It gives one issue for each problem. `at`\n * is the path of the rule, for the paths of the issues.\n */\nexport const ruleIssues = (\n\tdomain: Domain.Any,\n\trule: RuleData,\n\tat: ReadonlyArray<PropertyKey>,\n\tmemo: Memo = makeMemo(),\n): ReadonlyArray<Issue> => {\n\tconst all = rule.resources.includes(\"*\")\n\t// Each kind is checked one time, also when the list names it again.\n\tconst names = Arr.dedupe(Arr.filter(rule.resources, (kind) => kind !== \"*\"))\n\tconst kinds = Array.from(HashMap.keys(domain.resources))\n\tconst named = Arr.flatMap(names, (kind) =>\n\t\tOption.toArray(HashMap.get(domain.resources, kind)),\n\t)\n\tconst known = all ? Array.from(HashMap.values(domain.resources)) : named\n\t// The rule never runs on a kind without one of its actions, so the types\n\t// of its tests are checked only on the kinds with one.\n\tconst applicable = Arr.filter(named, (descriptor) =>\n\t\thasAction(descriptor, rule.actions),\n\t)\n\tconst context: Context = {\n\t\tdomain,\n\t\tmemo,\n\t\tall,\n\t\tnamed,\n\t\tapplicable,\n\t\tpaths: all\n\t\t\t? Arr.filter(known, (descriptor) => hasAction(descriptor, rule.actions))\n\t\t\t: applicable,\n\t}\n\tconst actions = Arr.dedupe(\n\t\tArr.flatMap(known, (descriptor) => descriptor.actions),\n\t)\n\treturn [\n\t\t...Arr.map(\n\t\t\tArr.filter(names, (kind) => !HashMap.has(domain.resources, kind)),\n\t\t\t(kind): Issue => ({\n\t\t\t\tpath: [...at, \"resources\"],\n\t\t\t\tissue: suggestIn(\n\t\t\t\t\tmemo,\n\t\t\t\t\t`There is no resource with the kind \"${kind}\"`,\n\t\t\t\t\tkind,\n\t\t\t\t\t\"~kinds\",\n\t\t\t\t\t() => kinds,\n\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t\t...Arr.map(\n\t\t\tArr.filter(\n\t\t\t\trule.actions,\n\t\t\t\t(action) => action !== \"*\" && !actions.includes(action),\n\t\t\t),\n\t\t\t(action): Issue => ({\n\t\t\t\tpath: [...at, \"actions\"],\n\t\t\t\tissue: suggestIn(\n\t\t\t\t\tmemo,\n\t\t\t\t\t`The action \"${action}\" is not an action of these resources`,\n\t\t\t\t\taction,\n\t\t\t\t\t`~actions\\u0000${names.join(\",\")}`,\n\t\t\t\t\t() => actions,\n\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t\t...Arr.flatMap(rule.fields ?? [], (field, index) =>\n\t\t\tfield.includes(\"*\")\n\t\t\t\t? patternIssues(memo, known, field, [...at, \"fields\", index])\n\t\t\t\t: fieldIssue(context, field, [...at, \"fields\", index]),\n\t\t),\n\t\t...(rule.when === undefined\n\t\t\t? []\n\t\t\t: Nodes.isNode(rule.when)\n\t\t\t\t? conditionIssues(context, rule.when, () => [...at, \"when\"], new Set())\n\t\t\t\t: [notWhole([...at, \"when\"])]),\n\t]\n}\n\n/** The JSON form of a policy tree, or a policy tree, as the binding reads it. */\ntype TreeNode =\n\t| { readonly _tag: \"Rules\"; readonly rules: ReadonlyArray<RuleData> }\n\t| {\n\t\t\treadonly _tag: \"All\" | \"Any\" | \"FirstOf\"\n\t\t\treadonly policies: ReadonlyArray<TreeNode>\n\t }\n\t| {\n\t\t\treadonly _tag: \"Ceiling\"\n\t\t\treadonly cap: TreeNode\n\t\t\treadonly policy: TreeNode\n\t }\n\n/** Checks each rule of a policy tree against a domain. */\nexport const treeIssues = (\n\tdomain: Domain.Any,\n\troot: TreeNode,\n): ReadonlyArray<Issue> => {\n\t// The rules come out of the tree first, and each one is checked after.\n\t// A check of a condition goes down its levels, so a check inside the walk\n\t// of the tree would go down the levels of both.\n\tconst rulesOf = (\n\t\tnode: TreeNode,\n\t\tpath: ReadonlyArray<PropertyKey>,\n\t\tlevel: number,\n\t): ReadonlyArray<Issue | readonly [RuleData, ReadonlyArray<PropertyKey>]> => {\n\t\tif (level > Limits.policyDepth) {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tpath,\n\t\t\t\t\tissue: `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,\n\t\t\t\t},\n\t\t\t]\n\t\t}\n\t\tswitch (node._tag) {\n\t\t\tcase \"Rules\":\n\t\t\t\treturn Arr.map(\n\t\t\t\t\tnode.rules,\n\t\t\t\t\t(rule, index) => [rule, [...path, \"rules\", index]] as const,\n\t\t\t\t)\n\t\t\tcase \"All\":\n\t\t\tcase \"Any\":\n\t\t\tcase \"FirstOf\":\n\t\t\t\treturn Arr.flatMap(node.policies, (child, index) =>\n\t\t\t\t\trulesOf(child, [...path, \"policies\", index], level + 1),\n\t\t\t\t)\n\t\t\tcase \"Ceiling\":\n\t\t\t\treturn [\n\t\t\t\t\t...rulesOf(node.cap, [...path, \"cap\"], level + 1),\n\t\t\t\t\t...rulesOf(node.policy, [...path, \"policy\"], level + 1),\n\t\t\t\t]\n\t\t}\n\t}\n\tconst memo = makeMemo()\n\treturn Arr.flatMap(rulesOf(root, [], 1), (found) =>\n\t\tArray.isArray(found)\n\t\t\t? ruleIssues(domain, found[0], found[1], memo)\n\t\t\t: [found as Issue],\n\t)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bind.js","names":["Catalog.accepts","Catalog.lookup","Patterns.parse","Patterns.fitsCatalog","Limits.searchSteps","Catalog.anyKinds","Nodes.isNode","Place.pathOf","Place.at"],"sources":["../../src/internal/bind.ts"],"sourcesContent":["/**\n * The binding of rules to a domain. It is the one check of a rule, for a rule\n * from the builder, from `Policy.rule`, and from JSON alike. It reads the\n * catalogs of the resources and of the principal, and it gives one issue for\n * each problem, with the path of the problem and, for a name with a mistake,\n * the name that it may mean.\n */\nimport * as Arr from \"effect/Array\"\nimport * as HashMap from \"effect/HashMap\"\nimport * as MutableHashMap from \"effect/MutableHashMap\"\nimport * as Option from \"effect/Option\"\nimport * as Result from \"effect/Result\"\nimport type * as Domain from \"../Domain.ts\"\nimport type * as Resource from \"../Resource.ts\"\nimport * as Catalog from \"./catalog.ts\"\nimport * as Limits from \"./limits.ts\"\nimport * as Nodes from \"./nodes.ts\"\nimport * as Patterns from \"./patterns.ts\"\nimport * as Place from \"./place.ts\"\nimport { withSuggestion } from \"./suggest.ts\"\n\n/** One problem of a rule or a policy, with its path. */\nexport interface Issue {\n\treadonly path: ReadonlyArray<PropertyKey>\n\treadonly issue: string\n}\n\n/** The data of a rule that the binding reads. */\nexport interface RuleData {\n\treadonly actions: ReadonlyArray<string>\n\treadonly resources: ReadonlyArray<string>\n\treadonly when?: unknown\n\treadonly fields?: ReadonlyArray<string>\n}\n\n/**\n * The memo of one binding, for all its rules. A policy may name one wrong\n * field or one pattern in very many places, so the work on each is done one\n * time. It goes away when the binding ends.\n */\ninterface Memo {\n\t/** The suggestion for each name, by the place where it was looked for. */\n\treadonly suggestions: MutableHashMap.MutableHashMap<string, string>\n\t/** If a pattern matches a field of a kind, by the kind and the pattern. */\n\treadonly fits: MutableHashMap.MutableHashMap<string, boolean>\n\t/** The count of the issues of conditions so far. It belongs to one binding. */\n\treadonly issues: { count: number; noted: boolean }\n\t/** The steps of the checks of patterns so far. It belongs to one binding. */\n\treadonly work: { steps: number; noted: boolean }\n}\n\nconst makeMemo = (): Memo => ({\n\tsuggestions: MutableHashMap.empty(),\n\tfits: MutableHashMap.empty(),\n\tissues: { count: 0, noted: false },\n\twork: { steps: 0, noted: false },\n})\n\n/**\n * The most names that one binding makes a suggestion for. A suggestion\n * compares the name with every name of a catalog, so a policy with very many\n * wrong names would take a long time.\n */\nconst maxSuggestions = 32\n\n/** Adds a suggestion to a message, with the memo of the binding. */\nconst suggestIn = (\n\tmemo: Memo,\n\tmessage: string,\n\tname: string,\n\tscope: string,\n\tnames: () => ReadonlyArray<string>,\n): string => {\n\tconst key = `${scope}\\u0000${name}`\n\tconst known = MutableHashMap.get(memo.suggestions, key)\n\tif (Option.isSome(known)) {\n\t\treturn known.value\n\t}\n\tif (MutableHashMap.size(memo.suggestions) >= maxSuggestions) {\n\t\treturn message\n\t}\n\tconst made = withSuggestion(message, name, names())\n\tMutableHashMap.set(memo.suggestions, key, made)\n\treturn made\n}\n\n/** The kinds that a rule may run on, from the view of its checks. */\ninterface Context {\n\treadonly domain: Domain.Any\n\treadonly memo: Memo\n\t/** The rule is for every resource (`\"*\"`). */\n\treadonly all: boolean\n\t/** The resources that the rule names and that the domain has. */\n\treadonly named: ReadonlyArray<Resource.Descriptor>\n\t/** The named resources that have one of the actions of the rule. */\n\treadonly applicable: ReadonlyArray<Resource.Descriptor>\n\t/**\n\t * The resources whose paths a path test may read: every resource with one\n\t * of the actions, for a rule for every resource.\n\t */\n\treadonly paths: ReadonlyArray<Resource.Descriptor>\n}\n\ntype Operand = Nodes.Operand\n\n/**\n * Tests if a field may hold a value of the type of a fixed value. A test with\n * `null` is a test that the field is missing, so `null` always fits. A number\n * fits any field of numbers: a test of an integer field with `1.5` or\n * `Infinity` has a meaning, as `lt` shows.\n */\nconst fitsLiteral = (kinds: Catalog.Kinds, value: unknown): boolean =>\n\tvalue === null ||\n\t(typeof value === \"number\" && kinds.number !== \"none\") ||\n\tCatalog.accepts(kinds, value)\n\n/**\n * Tests if two fields may hold one scalar. A principal field that is `null`\n * or that holds an object is a missing value, and a test with a missing value\n * is false, so only scalars count.\n */\nconst meet = (left: Catalog.Kinds, right: Catalog.Kinds): boolean =>\n\t(left.string && right.string) ||\n\t(left.boolean && right.boolean) ||\n\t(left.number !== \"none\" && right.number !== \"none\") ||\n\tArr.some(left.literals, (value) => Catalog.accepts(right, value)) ||\n\tArr.some(right.literals, (value) => Catalog.accepts(left, value))\n\nconst hasStrings = (kinds: Catalog.Kinds): boolean =>\n\tkinds.string || Arr.some(kinds.literals, (value) => typeof value === \"string\")\n\nconst hasNumbers = (kinds: Catalog.Kinds): boolean =>\n\tkinds.number !== \"none\" ||\n\tArr.some(kinds.literals, (value) => typeof value === \"number\")\n\n/** Tests if a field may hold a value with an order: a number or a string. */\nconst isOrdered = (kinds: Catalog.Kinds): boolean =>\n\thasStrings(kinds) || hasNumbers(kinds)\n\n/** Tests if two fields may hold values of one ordered type. */\nconst orderedMeet = (left: Catalog.Kinds, right: Catalog.Kinds): boolean =>\n\t(hasStrings(left) && hasStrings(right)) ||\n\t(hasNumbers(left) && hasNumbers(right))\n\n/** The texts of the fields of a catalog that have only names, for suggestions. */\nconst namesOf = (catalog: Catalog.Catalog): ReadonlyArray<string> =>\n\tArr.map(\n\t\tArr.filter(catalog.fields, (field) =>\n\t\t\tArr.every(field.path, (segment) => typeof segment === \"string\"),\n\t\t),\n\t\t(field) => field.text,\n\t)\n\n/** The issue of a field that is not a field of every resource of the rule. */\nconst fieldIssue = (\n\tcontext: Context,\n\tfield: string,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> => {\n\tconst message = `The field \"${field}\" is not a field of every resource of the rule`\n\tif (context.all || context.named.length === 0) {\n\t\treturn [{ path: at, issue: message }]\n\t}\n\tconst without = Arr.filter(\n\t\tcontext.named,\n\t\t(descriptor) => Catalog.lookup(descriptor.catalog, field) === undefined,\n\t)\n\treturn without.length === 0\n\t\t? []\n\t\t: [\n\t\t\t\t{\n\t\t\t\t\tpath: at,\n\t\t\t\t\tissue: suggestIn(context.memo, message, field, without[0]!.kind, () =>\n\t\t\t\t\t\tnamesOf(without[0]!.catalog),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t]\n}\n\n/**\n * The issues of a field that a condition reads: a field that is not a field\n * of every resource of the rule, or a field that is not plain data in one of\n * them. A read of a value that is not plain data, as a getter of a class, is\n * a defect in a decision. A residual may leave the read out, so the residual\n * would say \"yes\" where the decision fails. So the binding does not take\n * such a read: see `Catalog.Field.plain`.\n */\nconst readIssues = (\n\tcontext: Context,\n\tfield: string,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> => {\n\tconst exists = fieldIssue(context, field, at)\n\tif (exists.length > 0) {\n\t\treturn exists\n\t}\n\treturn Arr.some(\n\t\tcontext.named,\n\t\t(descriptor) => !(Catalog.lookup(descriptor.catalog, field)?.plain ?? true),\n\t)\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tpath: at,\n\t\t\t\t\tissue: `A condition may not read the field \"${field}\". A value above it may be an object that does not have it as a data field, as a class with a getter, a date or a list. Use a predicate for it.`,\n\t\t\t\t},\n\t\t\t]\n\t\t: []\n}\n\n/**\n * The issues of a field pattern: a pattern that does not parse, or that\n * matches no field of the resources of the rule, as a pattern with a mistake\n * in a name does.\n */\nconst patternIssues = (\n\tmemo: Memo,\n\tknown: ReadonlyArray<Resource.Descriptor>,\n\tpattern: string,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> =>\n\tResult.match(Patterns.parse(pattern), {\n\t\tonFailure: (issue) => [{ path: at, issue }],\n\t\tonSuccess: (parsed) => {\n\t\t\t// A rule with no known resource has an issue for that already. The\n\t\t\t// pattern is checked on every kind, not only up to the first that it\n\t\t\t// fits: a decision on a kind does this check, so the limit of the\n\t\t\t// work below is a limit of the work of a decision too.\n\t\t\tconst fits =\n\t\t\t\tknown.length === 0 ||\n\t\t\t\tArr.reduce(known, false, (before, descriptor) => {\n\t\t\t\t\tconst key = `${descriptor.kind}\\u0000${pattern}`\n\t\t\t\t\treturn (\n\t\t\t\t\t\tOption.getOrElse(MutableHashMap.get(memo.fits, key), () => {\n\t\t\t\t\t\t\t// Each check of a pattern reads the fields of a catalog. The\n\t\t\t\t\t\t\t// work of one binding has a limit, so very many patterns stop\n\t\t\t\t\t\t\t// with one issue, and the policy does not load.\n\t\t\t\t\t\t\tmemo.work.steps +=\n\t\t\t\t\t\t\t\tdescriptor.catalog.fields.length * Math.max(parsed.length, 1)\n\t\t\t\t\t\t\tif (memo.work.steps > Limits.searchSteps) {\n\t\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst found = Patterns.fitsCatalog(parsed, descriptor.catalog)\n\t\t\t\t\t\t\tMutableHashMap.set(memo.fits, key, found)\n\t\t\t\t\t\t\treturn found\n\t\t\t\t\t\t}) || before\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\tif (memo.work.steps > Limits.searchSteps && !memo.work.noted) {\n\t\t\t\tmemo.work.noted = true\n\t\t\t\treturn [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\tissue: `The field patterns of the policy take more than ${String(Limits.searchSteps)} steps to check. The most is ${String(Limits.searchSteps)}.`,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t}\n\t\t\treturn fits\n\t\t\t\t? []\n\t\t\t\t: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\t\tissue: `The field pattern \"${pattern}\" matches no field of the resources of the rule`,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t},\n\t})\n\n/** The fields of the resources that a test of the rule may read. */\nconst fieldsAt = (\n\tcontext: Context,\n\tfield: string,\n): ReadonlyArray<Catalog.Field> =>\n\tArr.flatMap(context.applicable, (descriptor) => {\n\t\tconst found = Catalog.lookup(descriptor.catalog, field)\n\t\treturn found === undefined ? [] : [found]\n\t})\n\n/** The field of the principal that an operand names, or the issue when there is none. */\nconst principalField = (\n\tcontext: Context,\n\tref: string,\n\tat: ReadonlyArray<PropertyKey>,\n): Catalog.Field | Issue => {\n\tconst found = Catalog.lookup(context.domain.principalCatalog, ref)\n\treturn found !== undefined\n\t\t? found\n\t\t: {\n\t\t\t\tpath: at,\n\t\t\t\tissue: suggestIn(\n\t\t\t\t\tcontext.memo,\n\t\t\t\t\t`The principal has no field \"${ref}\"`,\n\t\t\t\t\tref,\n\t\t\t\t\t\"~principal\",\n\t\t\t\t\t() => namesOf(context.domain.principalCatalog),\n\t\t\t\t),\n\t\t\t}\n}\n\nconst isIssue = (value: Catalog.Field | Issue): value is Issue =>\n\t\"issue\" in value\n\n/** The issues of one operand of a test of a field. */\nconst operandIssues = (\n\tcontext: Context,\n\tfield: string,\n\tordered: boolean,\n\toperand: Operand,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> => {\n\tconst fields = fieldsAt(context, field)\n\tif (operand._tag === \"Literal\") {\n\t\tconst value = operand.value\n\t\tif (ordered && typeof value !== \"string\" && typeof value !== \"number\") {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tpath: at,\n\t\t\t\t\tissue: `The field \"${field}\" has an ordered test with a value that has no order`,\n\t\t\t\t},\n\t\t\t]\n\t\t}\n\t\treturn [\n\t\t\t...(Arr.some(fields, (found) => !fitsLiteral(found.kinds, value))\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\t\tissue: `The field \"${field}\" can not hold the value ${JSON.stringify(value)}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: []),\n\t\t\t...(ordered && Arr.some(fields, (found) => !isOrdered(found.kinds))\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\t\tissue: `The field \"${field}\" has an ordered test with a value that has no order`,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: []),\n\t\t]\n\t}\n\tconst principal = principalField(context, operand.path, at)\n\tif (isIssue(principal)) {\n\t\treturn [principal]\n\t}\n\treturn [\n\t\t...(Arr.some(fields, (found) => !meet(found.kinds, principal.kinds))\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\tissue: `The field \"${field}\" can not hold a value of the principal field \"${operand.path}\"`,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []),\n\t\t...(ordered &&\n\t\tArr.some(fields, (found) => !orderedMeet(found.kinds, principal.kinds))\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: at,\n\t\t\t\t\t\tissue: `The field \"${field}\" has an ordered test with a value that has no order`,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []),\n\t]\n}\n\n/**\n * Tests if the path of a resource may start with a prefix: it is as long as\n * the prefix, and each fixed string of the prefix is the string of the path\n * at its place, when the path has a string there.\n */\nconst mayStartWith = (\n\tdescriptor: Resource.Descriptor,\n\tstrings: ReadonlyArray<string | null>,\n): boolean =>\n\t(descriptor.path?.length ?? 0) >= strings.length &&\n\tArr.every(strings, (string, index) => {\n\t\tconst segment = descriptor.path?.[index]\n\t\treturn string === null || typeof segment !== \"string\" || segment === string\n\t})\n\n/** The kinds of the part of the path of a resource at a place. */\nconst pathPartKinds = (\n\tdescriptor: Resource.Descriptor,\n\tindex: number,\n): Catalog.Kinds | string | undefined => {\n\tconst segment = descriptor.path?.[index]\n\treturn segment === undefined || typeof segment === \"string\"\n\t\t? segment\n\t\t: (Catalog.lookup(descriptor.catalog, segment.field)?.kinds ??\n\t\t\t\tCatalog.anyKinds)\n}\n\n/** The issues of the prefix of a path test. */\nconst underIssues = (\n\tcontext: Context,\n\tprefix: ReadonlyArray<Operand>,\n\tat: ReadonlyArray<PropertyKey>,\n): ReadonlyArray<Issue> => {\n\tconst strings = Arr.map(prefix, (operand) =>\n\t\toperand._tag === \"Literal\" && typeof operand.value === \"string\"\n\t\t\t? operand.value\n\t\t\t: null,\n\t)\n\t// The test can match only a path that starts with the fixed strings of\n\t// the prefix, so the other resources are left out.\n\tconst candidates = Arr.filter(context.paths, (descriptor) =>\n\t\tmayStartWith(descriptor, strings),\n\t)\n\treturn Arr.flatMap(prefix, (operand, index): ReadonlyArray<Issue> => {\n\t\tconst place = [...at, \"prefix\", index]\n\t\tif (operand._tag === \"Literal\") {\n\t\t\treturn Arr.some(candidates, (descriptor) => {\n\t\t\t\tconst kinds = pathPartKinds(descriptor, index)\n\t\t\t\treturn typeof kinds === \"object\" && !fitsLiteral(kinds, operand.value)\n\t\t\t})\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tpath: place,\n\t\t\t\t\t\t\tissue: `The path part ${String(index)} can not hold the value ${JSON.stringify(operand.value)}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: []\n\t\t}\n\t\tconst principal = principalField(context, operand.path, place)\n\t\tif (isIssue(principal)) {\n\t\t\treturn [principal]\n\t\t}\n\t\treturn Arr.some(candidates, (descriptor) => {\n\t\t\tconst kinds = pathPartKinds(descriptor, index)\n\t\t\treturn kinds === undefined\n\t\t\t\t? true\n\t\t\t\t: typeof kinds === \"string\"\n\t\t\t\t\t? !Catalog.accepts(principal.kinds, kinds)\n\t\t\t\t\t: !meet(kinds, principal.kinds)\n\t\t})\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: place,\n\t\t\t\t\t\tissue: `The principal field \"${operand.path}\" can not hold a part of the path of the resource`,\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []\n\t})\n}\n\nconst notWhole = (at: ReadonlyArray<PropertyKey>): Issue => ({\n\tpath: at,\n\tissue: \"The condition is not a whole condition tree\",\n})\n\n/**\n * The most issues that the walk of the conditions of one binding gives. A\n * policy may have very many wrong tests, and each issue keeps its path.\n */\nconst maxIssues = 100\n\n/** A path that the walk makes only when it needs it, for an issue. */\ntype LazyPath = () => ReadonlyArray<PropertyKey>\n\n/**\n * Puts the path of a node before the paths of its issues. A walk keeps no\n * path for a node with no issue, so a deep and wide condition costs little.\n */\nconst under = (\n\tmemo: Memo,\n\tat: LazyPath,\n\tissues: ReadonlyArray<Issue>,\n): ReadonlyArray<Issue> => {\n\tif (issues.length === 0) {\n\t\treturn issues\n\t}\n\tconst room = maxIssues - memo.issues.count\n\tmemo.issues.count += issues.length\n\tconst kept = Arr.map(Arr.take(issues, Math.max(room, 0)), (issue) => ({\n\t\tpath: [...at(), ...issue.path],\n\t\tissue: issue.issue,\n\t}))\n\tconst first = memo.issues.count > maxIssues && !memo.issues.noted\n\tmemo.issues.noted ||= first\n\treturn first\n\t\t? [\n\t\t\t\t...kept,\n\t\t\t\t{\n\t\t\t\t\tpath: [],\n\t\t\t\t\tissue: `The policy has more problems. The first ${String(maxIssues)} are here.`,\n\t\t\t\t},\n\t\t\t]\n\t\t: kept\n}\n\n/**\n * The issues of a condition. The constructors of the nodes checked the shape\n * of each node, so this checks the names and the types against the domain. A\n * node that the condition uses in many places is checked one time. The nodes\n * it has seen are in a native `Set`, because it must find a node by the node\n * itself: `Equal` compares nodes by structure.\n */\nconst conditionIssues = (\n\tcontext: Context,\n\tnode: Nodes.Node,\n\tat: LazyPath,\n\tseen: Set<Nodes.Node>,\n): ReadonlyArray<Issue> => {\n\tif (seen.has(node)) {\n\t\treturn []\n\t}\n\tseen.add(node)\n\tconst here = (issues: ReadonlyArray<Issue>) => under(context.memo, at, issues)\n\tswitch (node._tag) {\n\t\tcase \"Compare\": {\n\t\t\tconst exists = readIssues(context, node.field, [\"field\"])\n\t\t\treturn here(\n\t\t\t\texists.length > 0\n\t\t\t\t\t? exists\n\t\t\t\t\t: operandIssues(\n\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\tnode.field,\n\t\t\t\t\t\t\tnode.op !== \"eq\" && node.op !== \"ne\",\n\t\t\t\t\t\t\tnode.value,\n\t\t\t\t\t\t\t[\"value\"],\n\t\t\t\t\t\t),\n\t\t\t)\n\t\t}\n\t\tcase \"In\": {\n\t\t\tconst exists = readIssues(context, node.field, [\"field\"])\n\t\t\treturn here(\n\t\t\t\texists.length > 0\n\t\t\t\t\t? exists\n\t\t\t\t\t: Arr.flatMap(node.values, (operand, index) =>\n\t\t\t\t\t\t\toperandIssues(context, node.field, false, operand, [\n\t\t\t\t\t\t\t\t\"values\",\n\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t),\n\t\t\t)\n\t\t}\n\t\tcase \"Exists\":\n\t\t\treturn here(readIssues(context, node.field, [\"field\"]))\n\t\tcase \"Under\":\n\t\t\treturn here(underIssues(context, node.prefix, []))\n\t\tcase \"And\":\n\t\tcase \"Or\":\n\t\t\treturn Arr.flatMap(node.conditions, (child, index) =>\n\t\t\t\tconditionIssues(\n\t\t\t\t\tcontext,\n\t\t\t\t\tchild,\n\t\t\t\t\t() => [...at(), \"conditions\", index],\n\t\t\t\t\tseen,\n\t\t\t\t),\n\t\t\t)\n\t\tcase \"Not\":\n\t\t\treturn conditionIssues(\n\t\t\t\tcontext,\n\t\t\t\tnode.condition,\n\t\t\t\t() => [...at(), \"condition\"],\n\t\t\t\tseen,\n\t\t\t)\n\t\tdefault:\n\t\t\treturn []\n\t}\n}\n\nconst hasAction = (\n\tdescriptor: Resource.Descriptor,\n\tactions: ReadonlyArray<string>,\n): boolean =>\n\tactions.includes(\"*\") ||\n\tArr.some(actions, (action) => descriptor.actions.includes(action))\n\n/**\n * Checks one rule against a domain. It gives one issue for each problem. `at`\n * is the path of the rule, for the paths of the issues.\n */\nexport const ruleIssues = (\n\tdomain: Domain.Any,\n\trule: RuleData,\n\tat: ReadonlyArray<PropertyKey>,\n\tmemo: Memo = makeMemo(),\n): ReadonlyArray<Issue> => {\n\tconst all = rule.resources.includes(\"*\")\n\t// Each kind is checked one time, also when the list names it again.\n\tconst names = Arr.dedupe(Arr.filter(rule.resources, (kind) => kind !== \"*\"))\n\tconst kinds = Array.from(HashMap.keys(domain.resources))\n\tconst named = Arr.flatMap(names, (kind) =>\n\t\tOption.toArray(HashMap.get(domain.resources, kind)),\n\t)\n\tconst known = all ? Array.from(HashMap.values(domain.resources)) : named\n\t// The rule never runs on a kind without one of its actions, so the types\n\t// of its tests are checked only on the kinds with one.\n\tconst applicable = Arr.filter(named, (descriptor) =>\n\t\thasAction(descriptor, rule.actions),\n\t)\n\tconst context: Context = {\n\t\tdomain,\n\t\tmemo,\n\t\tall,\n\t\tnamed,\n\t\tapplicable,\n\t\tpaths: all\n\t\t\t? Arr.filter(known, (descriptor) => hasAction(descriptor, rule.actions))\n\t\t\t: applicable,\n\t}\n\tconst actions = Arr.dedupe(\n\t\tArr.flatMap(known, (descriptor) => descriptor.actions),\n\t)\n\treturn [\n\t\t...Arr.map(\n\t\t\tArr.filter(names, (kind) => !HashMap.has(domain.resources, kind)),\n\t\t\t(kind): Issue => ({\n\t\t\t\tpath: [...at, \"resources\"],\n\t\t\t\tissue: suggestIn(\n\t\t\t\t\tmemo,\n\t\t\t\t\t`There is no resource with the kind \"${kind}\"`,\n\t\t\t\t\tkind,\n\t\t\t\t\t\"~kinds\",\n\t\t\t\t\t() => kinds,\n\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t\t...Arr.map(\n\t\t\tArr.filter(\n\t\t\t\trule.actions,\n\t\t\t\t(action) => action !== \"*\" && !actions.includes(action),\n\t\t\t),\n\t\t\t(action): Issue => ({\n\t\t\t\tpath: [...at, \"actions\"],\n\t\t\t\tissue: suggestIn(\n\t\t\t\t\tmemo,\n\t\t\t\t\t`The action \"${action}\" is not an action of these resources`,\n\t\t\t\t\taction,\n\t\t\t\t\t`~actions\\u0000${names.join(\",\")}`,\n\t\t\t\t\t() => actions,\n\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t\t...Arr.flatMap(rule.fields ?? [], (field, index) =>\n\t\t\tfield.includes(\"*\")\n\t\t\t\t? patternIssues(memo, known, field, [...at, \"fields\", index])\n\t\t\t\t: fieldIssue(context, field, [...at, \"fields\", index]),\n\t\t),\n\t\t...(rule.when === undefined\n\t\t\t? []\n\t\t\t: Nodes.isNode(rule.when)\n\t\t\t\t? conditionIssues(context, rule.when, () => [...at, \"when\"], new Set())\n\t\t\t\t: [notWhole([...at, \"when\"])]),\n\t]\n}\n\n/** The JSON form of a policy tree, or a policy tree, as the binding reads it. */\ntype TreeNode =\n\t| { readonly _tag: \"Rules\"; readonly rules: ReadonlyArray<RuleData> }\n\t| {\n\t\t\treadonly _tag: \"All\" | \"Any\" | \"FirstOf\"\n\t\t\treadonly policies: ReadonlyArray<TreeNode>\n\t }\n\t| {\n\t\t\treadonly _tag: \"Ceiling\"\n\t\t\treadonly cap: TreeNode\n\t\t\treadonly policy: TreeNode\n\t }\n\n/** Checks each rule of a policy tree against a domain. */\nexport const treeIssues = (\n\tdomain: Domain.Any,\n\troot: TreeNode,\n): ReadonlyArray<Issue> => {\n\t// The rules come out of the tree first, and each one is checked after.\n\t// A check of a condition goes down its levels, so a check inside the walk\n\t// of the tree would go down the levels of both.\n\t// The place of each rule shares the path of its parent, and the full path\n\t// is made only when its rule is checked. A deep tree with very many rules\n\t// would need very much memory for a full copy of the path of each rule.\n\tconst rulesOf = (\n\t\tnode: TreeNode,\n\t\tplace: Place.Place | undefined,\n\t\tlevel: number,\n\t): ReadonlyArray<Issue | readonly [RuleData, Place.Place]> => {\n\t\tif (level > Limits.policyDepth) {\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tpath: Place.pathOf(place),\n\t\t\t\t\tissue: `A policy tree has more than ${String(Limits.policyDepth)} levels. The most is ${String(Limits.policyDepth)}.`,\n\t\t\t\t},\n\t\t\t]\n\t\t}\n\t\tswitch (node._tag) {\n\t\t\tcase \"Rules\":\n\t\t\t\treturn Arr.map(\n\t\t\t\t\tnode.rules,\n\t\t\t\t\t(rule, index) => [rule, Place.at(place, \"rules\", index)] as const,\n\t\t\t\t)\n\t\t\tcase \"All\":\n\t\t\tcase \"Any\":\n\t\t\tcase \"FirstOf\":\n\t\t\t\treturn Arr.flatMap(node.policies, (child, index) =>\n\t\t\t\t\trulesOf(child, Place.at(place, \"policies\", index), level + 1),\n\t\t\t\t)\n\t\t\tcase \"Ceiling\":\n\t\t\t\treturn [\n\t\t\t\t\t...rulesOf(node.cap, Place.at(place, \"cap\"), level + 1),\n\t\t\t\t\t...rulesOf(node.policy, Place.at(place, \"policy\"), level + 1),\n\t\t\t\t]\n\t\t}\n\t}\n\tconst memo = makeMemo()\n\treturn Arr.flatMap(rulesOf(root, undefined, 1), (found) =>\n\t\tArray.isArray(found)\n\t\t\t? ruleIssues(domain, found[0], Place.pathOf(found[1]), memo)\n\t\t\t: [found as Issue],\n\t)\n}\n"],"mappings":"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"}
@@ -35,6 +35,15 @@ export interface Field {
35
35
  readonly listed: boolean;
36
36
  /** The field may hold parts that the Schema does not name, as a record may. */
37
37
  readonly open: boolean;
38
+ /**
39
+ * A read of the field finds only plain data. It is `false` when a value
40
+ * above the field may be an object that does not have it as a data field:
41
+ * a class without the field, which may have a getter of that name, a
42
+ * value such as a date or a list, or any object, as of `Schema.Unknown`.
43
+ * A decode or a `make` copies a struct or a record into a plain object,
44
+ * so a struct without the field gives `null` for it.
45
+ */
46
+ readonly plain: boolean;
38
47
  }
39
48
  /** A place in the tree of a catalog, with the places below it. */
40
49
  export interface Branch {
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","names":[],"sources":["../../src/internal/catalog.ts"],"mappings":";;;iBAsBiB;;WAEP;WACA;;WAEA;;WAEA;;WAEA;;WAEA,UAAU;;;iBA4EH;WACP;;WAEA,YAAY,cAAc,UAAU;;;YAIlC,mBAAmB;;iBAGd;WACP,MAAM,cAAc;;WAEpB;WACA,OAAO;;;;;WAKP;;WAEA;;;iBAIO;;WAEP,OAAO;;WAEP,UAAU,gCAAgC;WAC1C,OAAO,QAAQ,gBAAgB;;WAE/B,KAAK;WACL,WAAW;;;iBAIJ;;WAEP,QAAQ,cAAc;WACtB,MAAM"}
1
+ {"version":3,"file":"catalog.d.ts","names":[],"sources":["../../src/internal/catalog.ts"],"mappings":";;;iBAsBiB;;WAEP;WACA;;WAEA;;WAEA;;WAEA;;WAEA,UAAU;;;iBA4EH;WACP;;WAEA,YAAY,cAAc,UAAU;;;YAIlC,mBAAmB;;iBAGd;WACP,MAAM,cAAc;;WAEpB;WACA,OAAO;;;;;WAKP;;WAEA;;;;;;;;;WASA;;;iBAIO;;WAEP,OAAO;;WAEP,UAAU,gCAAgC;WAC1C,OAAO,QAAQ,gBAAgB;;WAE/B,KAAK;WACL,WAAW;;;iBAIJ;;WAEP,QAAQ,cAAc;WACtB,MAAM"}
@@ -105,8 +105,8 @@ const classStruct = (ast) => {
105
105
  return ast.annotations?.["~constructor"] !== void 0 && ast.typeParameters.length === 1 && inner?._tag === "Objects" ? inner : void 0;
106
106
  };
107
107
  /**
108
- * Gives the Schemas that a value at one place may fit: it opens unions,
109
- * `Schema.suspend` and classes. The open suspend nodes are in a native `Set`,
108
+ * Gives the Schemas that a value at one place may fit: it opens unions and
109
+ * `Schema.suspend`. A class stays a class: see {@link structOf}. The open suspend nodes are in a native `Set`,
110
110
  * because the walk must find a node by the node itself: `Equal` compares
111
111
  * Schema nodes by structure, and a recursive Schema has no end. A suspend
112
112
  * node that gives itself again, with no step on the path, gives nothing.
@@ -115,16 +115,14 @@ const alternatives = (ast, open) => {
115
115
  switch (ast._tag) {
116
116
  case "Union": return Arr.flatMap(ast.types, (type) => alternatives(type, open));
117
117
  case "Suspend": return open.has(ast) ? [] : alternatives(ast.thunk(), /* @__PURE__ */ new Set([...open, ast]));
118
- case "Declaration": {
119
- const inner = classStruct(ast);
120
- return inner === void 0 ? [ast] : alternatives(inner, open);
121
- }
122
118
  default: return [ast];
123
119
  }
124
120
  };
121
+ /** The struct of a Schema: a struct, or the struct of a class. */
122
+ const structOf = (ast) => ast._tag === "Objects" ? ast : ast._tag === "Declaration" ? classStruct(ast) : void 0;
125
123
  const hasCheck = (checks, id) => Arr.some(checks, (check) => check.annotations?.representation?.id === id || check._tag === "FilterGroup" && hasCheck(check.checks, id));
126
124
  /** A struct with named fields, or with the keys of a record. */
127
- const isStruct = (ast) => ast._tag === "Objects" && (ast.propertySignatures.length > 0 || ast.indexSignatures.length > 0);
125
+ const isStruct = (ast) => ast !== void 0 && (ast.propertySignatures.length > 0 || ast.indexSignatures.length > 0);
128
126
  /** The kinds of the values of a Schema that is not a struct. */
129
127
  const kindsOf = (ast) => {
130
128
  switch (ast._tag) {
@@ -176,12 +174,25 @@ const declarationId = (ast) => {
176
174
  return hasProperty(representation, "id") && typeof representation.id === "string" ? representation.id : void 0;
177
175
  };
178
176
  const textOf = (path) => Arr.map(path, (segment) => typeof segment === "string" ? segment : "*").join(".");
179
- /** Gives the property of a struct with a name, if it has one. */
180
- const propertyOf = (struct, name) => Option.getOrUndefined(Arr.findFirst(struct.propertySignatures, (signature) => signature.name === name));
177
+ /**
178
+ * The name of a property as a part of a path. A key of an object is a string,
179
+ * so a number key, as from `Schema.Record(Schema.Literal(1), ...)`, has the
180
+ * name `"1"`. A symbol key is not a field.
181
+ */
182
+ const nameOf = (signature) => typeof signature.name === "symbol" ? void 0 : String(signature.name);
183
+ /**
184
+ * The properties of a struct by name. A walk makes it one time for each
185
+ * struct of a place, so a struct with very many fields does not take a
186
+ * search for each name.
187
+ */
188
+ const propertiesOf = (struct) => HashMap.fromIterable(Arr.flatMap(struct.propertySignatures, (signature) => {
189
+ const name = nameOf(signature);
190
+ return name === void 0 ? [] : [[name, signature]];
191
+ }));
181
192
  /** The names of the fields of the structs, in the order that they first come. */
182
193
  const namesOf = (structs) => Arr.dedupe(Arr.flatMap(structs, (struct) => Arr.flatMap(struct.propertySignatures, (signature) => {
183
- const name = signature.name;
184
- if (typeof name !== "string") return [];
194
+ const name = nameOf(signature);
195
+ if (name === void 0) return [];
185
196
  if (name.includes(".")) throw new Error(`A field name may not have a dot: "${name}"`);
186
197
  if (name === "") throw new Error("A field name may not be empty");
187
198
  return [name];
@@ -192,9 +203,10 @@ const namesOf = (structs) => Arr.dedupe(Arr.flatMap(structs, (struct) => Arr.fla
192
203
  * very many fields before the walk is done.
193
204
  */
194
205
  const branchOf = (place, count) => {
195
- const structs = Arr.filter(place.schemas, isStruct);
196
- const values = Arr.filter(place.schemas, (schema) => !isStruct(schema));
197
- const valueKinds = Arr.reduce(values, noKinds, (kinds, schema) => joinKinds(kinds, kindsOf(schema)));
206
+ const structs = Arr.filter(Arr.map(place.schemas, structOf), isStruct);
207
+ const classes = Arr.filter(Arr.map(Arr.filter(place.schemas, (schema) => schema._tag === "Declaration"), structOf), isStruct);
208
+ const values = Arr.filter(place.schemas, (schema) => !isStruct(structOf(schema)));
209
+ const valueKinds = Arr.reduce(values, noKinds, (kinds, schema) => joinKinds(kinds, kindsOf(structOf(schema) ?? schema)));
198
210
  const kinds = place.top ? anyKinds : joinKinds(joinKinds(valueKinds, structs.length > 0 ? objectKinds : noKinds), place.missing ? nullKinds : noKinds);
199
211
  const records = Arr.filter(structs, (struct) => struct.indexSignatures.length > 0);
200
212
  const last = place.path.length === 5;
@@ -203,7 +215,8 @@ const branchOf = (place, count) => {
203
215
  text: textOf(place.path),
204
216
  kinds,
205
217
  listed: values.length > 0 || records.length > 0 || last && structs.length > 0,
206
- open: records.length > 0 || structs.length > 0 || valueKinds.object || place.top
218
+ open: records.length > 0 || structs.length > 0 || valueKinds.object || place.top,
219
+ plain: place.plain
207
220
  };
208
221
  if (field !== void 0) {
209
222
  count.size += 1;
@@ -216,16 +229,16 @@ const branchOf = (place, count) => {
216
229
  key: void 0,
217
230
  keyBranch: void 0
218
231
  };
232
+ const structProperties = Arr.map(structs, propertiesOf);
233
+ const classProperties = Arr.map(classes, propertiesOf);
219
234
  const children = Arr.map(namesOf(structs), (name) => {
220
- const properties = Arr.flatMap(structs, (struct) => {
221
- const property = propertyOf(struct, name);
222
- return property === void 0 ? [] : [property];
223
- });
235
+ const properties = Arr.flatMap(structProperties, (named) => Option.toArray(HashMap.get(named, name)));
224
236
  const below = {
225
237
  path: [...place.path, name],
226
238
  schemas: Arr.flatMap(properties, (property) => alternatives(property.type, /* @__PURE__ */ new Set())),
227
239
  missing: place.missing || values.length > 0 || Arr.some(properties, (property) => SchemaAST.isOptional(property.type)),
228
- top: place.top || valueKinds.object || Arr.some(structs, (struct) => propertyOf(struct, name) === void 0)
240
+ top: place.top || valueKinds.object || properties.length < structs.length,
241
+ plain: place.plain && !valueKinds.object && Arr.every(classProperties, (named) => HashMap.has(named, name))
229
242
  };
230
243
  return [name, branchOf(below, count)];
231
244
  });
@@ -251,7 +264,8 @@ const branchOf = (place, count) => {
251
264
  path: [...place.path, key],
252
265
  schemas: Arr.flatMap(signatures, (signature) => alternatives(signature.type, /* @__PURE__ */ new Set())),
253
266
  missing: true,
254
- top: place.top || valueKinds.object || records.length < structs.length
267
+ top: place.top || valueKinds.object || records.length < structs.length,
268
+ plain: place.plain && !valueKinds.object && Arr.every(classes, (struct) => struct.indexSignatures.length > 0)
255
269
  }, count)
256
270
  };
257
271
  };
@@ -269,7 +283,8 @@ const catalogOf = (ast) => {
269
283
  path: [],
270
284
  schemas: alternatives(ast, /* @__PURE__ */ new Set()),
271
285
  missing: false,
272
- top: false
286
+ top: false,
287
+ plain: true
273
288
  }, { size: 0 });
274
289
  return {
275
290
  fields: fieldsOf(root),