@aventara/access 0.0.0-stage → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +877 -0
  2. package/LICENSE +91 -0
  3. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  4. package/README.md +2536 -2
  5. package/dist/access/access-options.d.ts +74 -0
  6. package/dist/access/access-options.js +5 -0
  7. package/dist/access/access.error.d.ts +35 -0
  8. package/dist/access/access.error.js +11 -0
  9. package/dist/access/access.factory.d.ts +275 -0
  10. package/dist/access/access.factory.js +440 -0
  11. package/dist/access/better-auth.hardening.d.ts +120 -0
  12. package/dist/access/better-auth.hardening.js +420 -0
  13. package/dist/access/better-auth.loader.d.ts +67 -0
  14. package/dist/access/better-auth.loader.js +65 -0
  15. package/dist/access/better-auth.range.d.ts +9 -0
  16. package/dist/access/better-auth.range.js +28 -0
  17. package/dist/access/better-auth.require.d.ts +90 -0
  18. package/dist/access/better-auth.require.js +6 -0
  19. package/dist/access/better-auth.require.node.d.ts +7 -0
  20. package/dist/access/better-auth.require.node.js +52 -0
  21. package/dist/access/claims.mapper.d.ts +79 -0
  22. package/dist/access/claims.mapper.js +152 -0
  23. package/dist/access/key-set.cache.d.ts +10 -0
  24. package/dist/access/key-set.cache.js +63 -0
  25. package/dist/access/policies-block.checker.d.ts +64 -0
  26. package/dist/access/policies-block.checker.js +138 -0
  27. package/dist/access/principal.subject.d.ts +22 -0
  28. package/dist/access/principal.subject.js +89 -0
  29. package/dist/access/profile.policy.d.ts +74 -0
  30. package/dist/access/profile.policy.js +110 -0
  31. package/dist/access/request.state.d.ts +89 -0
  32. package/dist/access/request.state.js +266 -0
  33. package/dist/access/session.hooks.d.ts +114 -0
  34. package/dist/access/session.hooks.js +258 -0
  35. package/dist/access/token.verifier.d.ts +35 -0
  36. package/dist/access/token.verifier.js +69 -0
  37. package/dist/access-schema.d.ts +48 -0
  38. package/dist/access-schema.js +5 -0
  39. package/dist/access-warning.emitter.d.ts +4 -0
  40. package/dist/access-warning.emitter.js +24 -0
  41. package/dist/access.builder.d.ts +340 -0
  42. package/dist/access.builder.js +75 -0
  43. package/dist/access.engine.d.ts +155 -0
  44. package/dist/access.engine.js +477 -0
  45. package/dist/audit/audit-event.d.ts +135 -0
  46. package/dist/audit/audit-event.js +8 -0
  47. package/dist/audit/audit.log.d.ts +122 -0
  48. package/dist/audit/audit.log.js +345 -0
  49. package/dist/audit/audit.schema.d.ts +143 -0
  50. package/dist/audit/audit.schema.js +50 -0
  51. package/dist/auth/index.d.ts +12 -0
  52. package/dist/auth/index.js +2 -0
  53. package/dist/aventara-where.translator.d.ts +19 -0
  54. package/dist/aventara-where.translator.js +87 -0
  55. package/dist/beacon/beacon.check.d.ts +59 -0
  56. package/dist/beacon/beacon.check.js +128 -0
  57. package/dist/beacon/beacon.schema.d.ts +41 -0
  58. package/dist/beacon/beacon.schema.js +48 -0
  59. package/dist/beacon/beacon.store.d.ts +29 -0
  60. package/dist/beacon/beacon.store.js +78 -0
  61. package/dist/better-auth/access.client-plugin.d.ts +75 -0
  62. package/dist/better-auth/access.client-plugin.js +133 -0
  63. package/dist/better-auth/access.plugin.d.ts +164 -0
  64. package/dist/better-auth/access.plugin.js +348 -0
  65. package/dist/better-auth/access.provider.d.ts +80 -0
  66. package/dist/better-auth/access.provider.js +100 -0
  67. package/dist/better-auth/index.d.ts +7 -0
  68. package/dist/better-auth/index.js +4 -0
  69. package/dist/better-auth/organization.bridge.d.ts +66 -0
  70. package/dist/better-auth/organization.bridge.js +144 -0
  71. package/dist/canonical-json.writer.d.ts +12 -0
  72. package/dist/canonical-json.writer.js +30 -0
  73. package/dist/cli/avaccess.bin.d.ts +2 -0
  74. package/dist/cli/avaccess.bin.js +26 -0
  75. package/dist/cli/avaccess.program.d.ts +15 -0
  76. package/dist/cli/avaccess.program.js +166 -0
  77. package/dist/cli/beacon-pins.file.d.ts +14 -0
  78. package/dist/cli/beacon-pins.file.js +62 -0
  79. package/dist/cli/beacon.loader.d.ts +20 -0
  80. package/dist/cli/beacon.loader.js +71 -0
  81. package/dist/cli/cli.context.d.ts +49 -0
  82. package/dist/cli/cli.context.js +66 -0
  83. package/dist/cli/diff.command.d.ts +2 -0
  84. package/dist/cli/diff.command.js +66 -0
  85. package/dist/cli/generate.command.d.ts +2 -0
  86. package/dist/cli/generate.command.js +82 -0
  87. package/dist/cli/keys.command.d.ts +2 -0
  88. package/dist/cli/keys.command.js +96 -0
  89. package/dist/cli/lock.file.d.ts +31 -0
  90. package/dist/cli/lock.file.js +181 -0
  91. package/dist/cli/module.loader.d.ts +13 -0
  92. package/dist/cli/module.loader.js +63 -0
  93. package/dist/cli/policies.diff.d.ts +16 -0
  94. package/dist/cli/policies.diff.js +101 -0
  95. package/dist/cli/pull.command.d.ts +2 -0
  96. package/dist/cli/pull.command.js +234 -0
  97. package/dist/cli/push.command.d.ts +2 -0
  98. package/dist/cli/push.command.js +59 -0
  99. package/dist/cli/registry.client.d.ts +63 -0
  100. package/dist/cli/registry.client.js +287 -0
  101. package/dist/cli/schema.command.d.ts +2 -0
  102. package/dist/cli/schema.command.js +60 -0
  103. package/dist/cli/serve.command.d.ts +6 -0
  104. package/dist/cli/serve.command.js +200 -0
  105. package/dist/cli/status.command.d.ts +2 -0
  106. package/dist/cli/status.command.js +31 -0
  107. package/dist/cli/translator.flag.d.ts +6 -0
  108. package/dist/cli/translator.flag.js +24 -0
  109. package/dist/cli/verify.command.d.ts +2 -0
  110. package/dist/cli/verify.command.js +40 -0
  111. package/dist/client/access-client.factory.d.ts +92 -0
  112. package/dist/client/access-client.factory.js +269 -0
  113. package/dist/client/access-verbs.schema.d.ts +66 -0
  114. package/dist/client/access-verbs.schema.js +159 -0
  115. package/dist/client/verb-rate.limiter.d.ts +116 -0
  116. package/dist/client/verb-rate.limiter.js +358 -0
  117. package/dist/foreign/foreign-facts.d.ts +30 -0
  118. package/dist/foreign/foreign-facts.js +75 -0
  119. package/dist/foreign/foreign.merger.d.ts +10 -0
  120. package/dist/foreign/foreign.merger.js +52 -0
  121. package/dist/foreign-references.check.d.ts +6 -0
  122. package/dist/foreign-references.check.js +131 -0
  123. package/dist/generator/client.emitter.d.ts +28 -0
  124. package/dist/generator/client.emitter.js +411 -0
  125. package/dist/generator/copied-modules.reader.d.ts +14 -0
  126. package/dist/generator/copied-modules.reader.js +80 -0
  127. package/dist/generator/namespace.facts.d.ts +28 -0
  128. package/dist/generator/namespace.facts.js +32 -0
  129. package/dist/generator/orm.translator.d.ts +48 -0
  130. package/dist/generator/orm.translator.js +29 -0
  131. package/dist/generator/prisma7/prisma-schema.reader.d.ts +9 -0
  132. package/dist/generator/prisma7/prisma-schema.reader.js +394 -0
  133. package/dist/generator/prisma7/prisma-where.translator.d.ts +73 -0
  134. package/dist/generator/prisma7/prisma-where.translator.js +138 -0
  135. package/dist/generator/prisma7/prisma7.translator.d.ts +2 -0
  136. package/dist/generator/prisma7/prisma7.translator.js +87 -0
  137. package/dist/generator/schema.emitter.d.ts +14 -0
  138. package/dist/generator/schema.emitter.js +23 -0
  139. package/dist/generator/tsconfig.reader.d.ts +44 -0
  140. package/dist/generator/tsconfig.reader.js +110 -0
  141. package/dist/generator/typescript-literal.writer.d.ts +5 -0
  142. package/dist/generator/typescript-literal.writer.js +42 -0
  143. package/dist/guardrails/guardrails-contract.d.ts +45 -0
  144. package/dist/guardrails/guardrails-contract.js +12 -0
  145. package/dist/guardrails/guardrails.builder.d.ts +48 -0
  146. package/dist/guardrails/guardrails.builder.js +32 -0
  147. package/dist/guardrails/guardrails.compiler.d.ts +10 -0
  148. package/dist/guardrails/guardrails.compiler.js +254 -0
  149. package/dist/guardrails/guardrails.composer.d.ts +7 -0
  150. package/dist/guardrails/guardrails.composer.js +95 -0
  151. package/dist/guardrails/guardrails.hash.d.ts +10 -0
  152. package/dist/guardrails/guardrails.hash.js +38 -0
  153. package/dist/index.d.ts +16 -0
  154. package/dist/index.js +6 -0
  155. package/dist/kiosk/kiosk-verbs.schema.d.ts +19 -0
  156. package/dist/kiosk/kiosk-verbs.schema.js +61 -0
  157. package/dist/kiosk/kiosk.plugin.d.ts +217 -0
  158. package/dist/kiosk/kiosk.plugin.js +877 -0
  159. package/dist/kiosk/kiosk.schema.d.ts +176 -0
  160. package/dist/kiosk/kiosk.schema.js +101 -0
  161. package/dist/kiosk/kiosk.store.d.ts +171 -0
  162. package/dist/kiosk/kiosk.store.js +478 -0
  163. package/dist/kiosk/lan-range.policy.d.ts +31 -0
  164. package/dist/kiosk/lan-range.policy.js +113 -0
  165. package/dist/kiosk/pin.policy.d.ts +9 -0
  166. package/dist/kiosk/pin.policy.js +143 -0
  167. package/dist/kiosk/session-use.marker.d.ts +4 -0
  168. package/dist/kiosk/session-use.marker.js +16 -0
  169. package/dist/lock/lock-contract.d.ts +38 -0
  170. package/dist/lock/lock-contract.js +67 -0
  171. package/dist/lock/lock.verifier.d.ts +27 -0
  172. package/dist/lock/lock.verifier.js +62 -0
  173. package/dist/mcp/agent-scope.gate.d.ts +13 -0
  174. package/dist/mcp/agent-scope.gate.js +75 -0
  175. package/dist/mcp/agent.grant.d.ts +40 -0
  176. package/dist/mcp/agent.grant.js +92 -0
  177. package/dist/mcp/mcp-options.d.ts +60 -0
  178. package/dist/mcp/mcp-options.js +72 -0
  179. package/dist/oauth/access-token.verifier.d.ts +66 -0
  180. package/dist/oauth/access-token.verifier.js +201 -0
  181. package/dist/oauth/oauth-provider.hardening.d.ts +62 -0
  182. package/dist/oauth/oauth-provider.hardening.js +261 -0
  183. package/dist/passwords/argon2id.hasher.d.ts +97 -0
  184. package/dist/passwords/argon2id.hasher.js +256 -0
  185. package/dist/passwords/node-rs-argon2.adapter.d.ts +25 -0
  186. package/dist/passwords/node-rs-argon2.adapter.js +42 -0
  187. package/dist/passwords/passwords.options.d.ts +79 -0
  188. package/dist/passwords/passwords.options.js +212 -0
  189. package/dist/policies-contract.d.ts +219 -0
  190. package/dist/policies-contract.js +8 -0
  191. package/dist/policies.compiler.d.ts +38 -0
  192. package/dist/policies.compiler.js +1433 -0
  193. package/dist/policies.decider.d.ts +103 -0
  194. package/dist/policies.decider.js +390 -0
  195. package/dist/policies.hash.d.ts +10 -0
  196. package/dist/policies.hash.js +54 -0
  197. package/dist/predicate.evaluator.d.ts +62 -0
  198. package/dist/predicate.evaluator.js +378 -0
  199. package/dist/random-secret.generator.d.ts +2 -0
  200. package/dist/random-secret.generator.js +15 -0
  201. package/dist/read-governed-unique.check.d.ts +3 -0
  202. package/dist/read-governed-unique.check.js +21 -0
  203. package/dist/reference-reads.check.d.ts +16 -0
  204. package/dist/reference-reads.check.js +158 -0
  205. package/dist/registry/access.registry.d.ts +173 -0
  206. package/dist/registry/access.registry.js +865 -0
  207. package/dist/registry/beacon.ledger.d.ts +30 -0
  208. package/dist/registry/beacon.ledger.js +93 -0
  209. package/dist/registry/ownership.check.d.ts +54 -0
  210. package/dist/registry/ownership.check.js +221 -0
  211. package/dist/registry/registry.endpoints.d.ts +50 -0
  212. package/dist/registry/registry.endpoints.js +193 -0
  213. package/dist/registry/registry.key.d.ts +7 -0
  214. package/dist/registry/registry.key.js +13 -0
  215. package/dist/registry/registry.options.d.ts +13 -0
  216. package/dist/registry/registry.options.js +28 -0
  217. package/dist/registry/registry.refusal.d.ts +6 -0
  218. package/dist/registry/registry.refusal.js +11 -0
  219. package/dist/registry/registry.schema.d.ts +238 -0
  220. package/dist/registry/registry.schema.js +127 -0
  221. package/dist/resources/auth-tables.catalog.d.ts +96 -0
  222. package/dist/resources/auth-tables.catalog.js +110 -0
  223. package/dist/resources/auth-tables.guard.d.ts +106 -0
  224. package/dist/resources/auth-tables.guard.js +462 -0
  225. package/dist/scope.resolver.d.ts +144 -0
  226. package/dist/scope.resolver.js +529 -0
  227. package/dist/sha256.digest.d.ts +2 -0
  228. package/dist/sha256.digest.js +81 -0
  229. package/dist/standalone/access.server.d.ts +41 -0
  230. package/dist/standalone/access.server.js +301 -0
  231. package/dist/standalone/index.d.ts +1 -0
  232. package/dist/standalone/index.js +1 -0
  233. package/package.json +102 -3
  234. package/skill/SKILL.md +170 -0
  235. package/skill/reference/callers.md +99 -0
  236. package/skill/reference/errors.md +89 -0
  237. package/skill/reference/recipes.md +281 -0
  238. package/skill/reference/rules.md +153 -0
  239. package/skill/reference/setup.md +334 -0
  240. package/src/aventara-where.translator.ts +116 -0
  241. package/src/beacon/beacon.check.ts +204 -0
  242. package/src/canonical-json.writer.ts +51 -0
  243. package/src/generator/prisma7/prisma-where.translator.ts +243 -0
  244. package/src/policies-contract.ts +215 -0
  245. package/src/policies.decider.ts +662 -0
  246. package/src/predicate.evaluator.ts +574 -0
  247. package/src/scope.resolver.ts +819 -0
  248. package/src/sha256.digest.ts +85 -0
@@ -0,0 +1,574 @@
1
+ // In-memory evaluation of a folded condition against a row's own scalar fields, and
2
+ // the `exists` questions for the rest. A to-one step whose references the row carries
3
+ // is asked on the row they name, with the row's values; whatever else an existing row
4
+ // leaves is asked about the row itself, by its identifier. A relation member on the
5
+ // row is never read: core strips them, and any other caller could have forged one.
6
+ // What cannot be asked is unknown, and the caller fails closed on it.
7
+
8
+ import {
9
+ type AccessWhere,
10
+ mergeWhere,
11
+ translateWhere,
12
+ } from "./aventara-where.translator.js";
13
+ import type {
14
+ AccessComparison,
15
+ AccessLiteral,
16
+ AccessPolicies,
17
+ } from "./policies-contract.js";
18
+ import {
19
+ allOf,
20
+ andOf,
21
+ FALSE,
22
+ notOf,
23
+ nullSafe,
24
+ orOf,
25
+ ownOf,
26
+ type Resolved,
27
+ TRUE,
28
+ } from "./scope.resolver.js";
29
+
30
+ /**
31
+ * A row as core hands it to `can`: its scalar fields. A relation member on it is
32
+ * ignored; never put relation data a client sent on it.
33
+ */
34
+ export type AccessRow = { readonly [field: string]: unknown };
35
+
36
+ /** Core's `exists`: whether a row of `resource` meets `where`, unscoped. */
37
+ export type AccessExists = (
38
+ resource: string,
39
+ where: AccessWhere,
40
+ ) => Promise<boolean>;
41
+
42
+ /** One `exists` question the engine asked, and its answer. */
43
+ export type AccessLookup = {
44
+ readonly resource: string;
45
+ readonly where: AccessWhere;
46
+ readonly answer: boolean;
47
+ };
48
+
49
+ /** A three-valued answer: decided, or not decidable from what the engine has. */
50
+ export type AccessTruth = boolean | "unknown";
51
+
52
+ /** Whether the row is about to be created, or already exists (an after check). */
53
+ export type AccessRowKind = "new" | "existing";
54
+
55
+ /** A field's type when `can` compares it by more than its JavaScript value (`types`). */
56
+ type ComparedType = "bigint" | "datetime" | "decimal" | undefined;
57
+
58
+ /** The type `can` compares `field` of `resource` by. */
59
+ const typeOf = (
60
+ policies: AccessPolicies,
61
+ resource: string,
62
+ field: string,
63
+ ): ComparedType => ownOf(ownOf(policies.schema, resource)?.types, field);
64
+
65
+ const DECIMAL = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
66
+
67
+ /**
68
+ * A decimal's value as `{ sign, digits, point }`: `0.<digits> × 10^point`, with no
69
+ * leading or trailing zero in `digits` (zero is `{ sign: "", digits: "", point: 0 }`).
70
+ * `undefined` for text that is no decimal.
71
+ */
72
+ export const decimalParts = (
73
+ text: string,
74
+ ):
75
+ | { readonly sign: "" | "-"; readonly digits: string; readonly point: number }
76
+ | undefined => {
77
+ const match = DECIMAL.exec(text);
78
+ if (match === null) {
79
+ return undefined;
80
+ }
81
+ // Indexed, not destructured: destructuring a match costs the type checker its
82
+ // iterator types (about 1,000 instantiations, the type budget's whole margin).
83
+ const sign = match[1] ?? "";
84
+ const whole = match[2] ?? "";
85
+ const fraction = match[3] ?? "";
86
+ const exponent = match[4] ?? "0";
87
+ const shift = Number(exponent);
88
+ if (!Number.isSafeInteger(shift)) {
89
+ return undefined;
90
+ }
91
+ let digits = whole + fraction;
92
+ let point = whole.length + shift;
93
+ const leading = digits.length - digits.replace(/^0+/, "").length;
94
+ digits = digits.slice(leading).replace(/0+$/, "");
95
+ point -= leading;
96
+ return digits === ""
97
+ ? { sign: "", digits: "", point: 0 }
98
+ : { sign: sign === "-" ? "-" : "", digits, point };
99
+ };
100
+
101
+ /**
102
+ * An ISO-8601 date, or date and time with its offset (never a local time), to the
103
+ * millisecond at most.
104
+ */
105
+ const INSTANT =
106
+ /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|([+-])(\d{2}):(\d{2})))?$/;
107
+
108
+ /**
109
+ * The epoch milliseconds of an ISO-8601 string that reads back as written: no
110
+ * February 30th, no hour 24, no offset past 23:59, nothing past the millisecond.
111
+ */
112
+ const instantOfText = (text: string): number | undefined => {
113
+ const match = INSTANT.exec(text);
114
+ if (match === null) {
115
+ return undefined;
116
+ }
117
+ const part = (index: number): number => Number(match[index] ?? "0");
118
+ const year = part(1);
119
+ const month = part(2);
120
+ const day = part(3);
121
+ const hour = part(4);
122
+ const minute = part(5);
123
+ const second = part(6);
124
+ const millisecond = Number((match[7] ?? "").padEnd(3, "0"));
125
+ const local = Date.UTC(
126
+ year,
127
+ month - 1,
128
+ day,
129
+ hour,
130
+ minute,
131
+ second,
132
+ millisecond,
133
+ );
134
+ const read = new Date(local);
135
+ if (
136
+ read.getUTCFullYear() !== year ||
137
+ read.getUTCMonth() !== month - 1 ||
138
+ read.getUTCDate() !== day ||
139
+ read.getUTCHours() !== hour ||
140
+ read.getUTCMinutes() !== minute ||
141
+ read.getUTCSeconds() !== second ||
142
+ part(10) > 23 ||
143
+ part(11) > 59
144
+ ) {
145
+ return undefined;
146
+ }
147
+ const offset = (part(10) * 60 + part(11)) * 60_000;
148
+ return match[9] === "-" ? local + offset : local - offset;
149
+ };
150
+
151
+ /** An instant's epoch milliseconds, from a valid `Date` or an ISO-8601 string that reads back as written. */
152
+ export const instantOf = (value: unknown): number | undefined => {
153
+ if (value instanceof Date) {
154
+ const time = value.getTime();
155
+ return Number.isNaN(time) ? undefined : time;
156
+ }
157
+ return typeof value === "string" ? instantOfText(value) : undefined;
158
+ };
159
+
160
+ /** A decimal object's text: core's `AvDecimal`, an ORM's decimal, anything whose `toString()` is decimal digits. */
161
+ const decimalText = (value: unknown): string | undefined =>
162
+ typeof value === "string"
163
+ ? value
164
+ : typeof value === "number"
165
+ ? Number.isFinite(value)
166
+ ? String(value)
167
+ : undefined
168
+ : typeof value === "object" &&
169
+ value !== null &&
170
+ !(value instanceof Date) &&
171
+ !Array.isArray(value)
172
+ ? String(value)
173
+ : undefined;
174
+
175
+ /**
176
+ * The key a value is compared by, for a field of `type`: equal keys, equal values.
177
+ * `undefined` when the value is not one of the type's (a type mismatch).
178
+ */
179
+ const comparisonKey = (value: unknown, type: ComparedType): unknown => {
180
+ switch (type) {
181
+ case "decimal": {
182
+ const text = decimalText(value);
183
+ const parts = text === undefined ? undefined : decimalParts(text);
184
+ return parts === undefined
185
+ ? undefined
186
+ : `${parts.sign}${parts.digits}@${parts.point}`;
187
+ }
188
+ case "datetime":
189
+ return instantOf(value);
190
+ case "bigint":
191
+ return typeof value === "bigint"
192
+ ? value
193
+ : typeof value === "number" && Number.isSafeInteger(value)
194
+ ? BigInt(value)
195
+ : typeof value === "string" && /^-?\d+$/.test(value)
196
+ ? BigInt(value)
197
+ : undefined;
198
+ default:
199
+ return typeof value === "string" ||
200
+ typeof value === "boolean" ||
201
+ (typeof value === "number" && Number.isFinite(value))
202
+ ? value
203
+ : undefined;
204
+ }
205
+ };
206
+
207
+ /**
208
+ * One comparison of a row's value with a rule's, three-valued: a value of another
209
+ * type than the field's, or than the rule's, is `"unknown"`.
210
+ */
211
+ const compare = (
212
+ rowValue: unknown,
213
+ cmp: AccessComparison,
214
+ value: AccessLiteral | readonly AccessLiteral[],
215
+ type: ComparedType,
216
+ ): AccessTruth => {
217
+ if (rowValue === null) {
218
+ // A comparison on NULL is false, except `equals: null` (IS NULL); a list never
219
+ // holds it for `can`.
220
+ return cmp === "eq" && value === null;
221
+ }
222
+ const actual = comparisonKey(rowValue, type);
223
+ const equal = (literal: AccessLiteral): AccessTruth => {
224
+ const expected = comparisonKey(literal, type);
225
+ return actual === undefined ||
226
+ expected === undefined ||
227
+ typeof actual !== typeof expected
228
+ ? "unknown"
229
+ : actual === expected;
230
+ };
231
+ if (cmp === "eq" || cmp === "ne") {
232
+ if (value === null) {
233
+ return cmp === "ne";
234
+ }
235
+ const truth = equal(value as AccessLiteral);
236
+ return truth === "unknown" ? truth : cmp === "eq" ? truth : !truth;
237
+ }
238
+ let unknown = false;
239
+ for (const literal of value as readonly AccessLiteral[]) {
240
+ if (literal === null) {
241
+ continue;
242
+ }
243
+ const truth = equal(literal);
244
+ if (truth === true) {
245
+ return cmp === "in";
246
+ }
247
+ unknown ||= truth === "unknown";
248
+ }
249
+ return unknown ? "unknown" : cmp === "notIn";
250
+ };
251
+
252
+ /** A condition a row decided as neither true nor false: a value of the wrong type. */
253
+ export type Unknown = { readonly k: "unknown" };
254
+
255
+ const UNKNOWN: Unknown = { k: "unknown" };
256
+
257
+ /** What evaluating on a row gives: `TRUE`, `FALSE`, `UNKNOWN`, or the residual. */
258
+ export type Evaluated = Resolved | Unknown;
259
+
260
+ /**
261
+ * A conjunction, three-valued. A part that is unknown makes the whole unknown unless
262
+ * another part is false: an unknown never waits on an `exists` question, and the
263
+ * caller fails closed on it.
264
+ */
265
+ const and3 = (k: "and" | "all", args: readonly Evaluated[]): Evaluated =>
266
+ args.some((arg) => arg.k === "false")
267
+ ? FALSE
268
+ : args.some((arg) => arg.k === "unknown")
269
+ ? UNKNOWN
270
+ : (k === "and" ? andOf : allOf)(args as readonly Resolved[]);
271
+
272
+ const or3 = (args: readonly Evaluated[]): Evaluated =>
273
+ args.some((arg) => arg.k === "true")
274
+ ? TRUE
275
+ : args.some((arg) => arg.k === "unknown")
276
+ ? UNKNOWN
277
+ : orOf(args as readonly Resolved[]);
278
+
279
+ /** A row's own field: an inherited one is not on the row. */
280
+ const fieldOf = (row: AccessRow, field: string): unknown =>
281
+ Object.hasOwn(row, field) ? row[field] : undefined;
282
+
283
+ /**
284
+ * Evaluates what the row carries. The answer is `TRUE`, `FALSE`, `UNKNOWN` (a value
285
+ * of another type than its field's), or the residual condition: the part the row
286
+ * could not decide, with the decided parts folded away. A relation member on the row
287
+ * is ignored: every relation step stays whole in the residual, for `exists`. Each comparison is made by its
288
+ * field's type (`types`): a decimal by its digits, a datetime by its instant, a
289
+ * bigint exactly.
290
+ */
291
+ export const evaluateOnRow = (
292
+ policies: AccessPolicies,
293
+ resource: string,
294
+ node: Resolved,
295
+ row: AccessRow,
296
+ ): Evaluated => {
297
+ switch (node.k) {
298
+ case "true":
299
+ case "false":
300
+ return node;
301
+ case "and":
302
+ case "all":
303
+ return and3(
304
+ node.k,
305
+ node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)),
306
+ );
307
+ case "or":
308
+ return or3(
309
+ node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)),
310
+ );
311
+ case "not": {
312
+ const arg = evaluateOnRow(policies, resource, node.arg, row);
313
+ return arg.k === "unknown" ? arg : notOf(arg);
314
+ }
315
+ case "cmp": {
316
+ const value = fieldOf(row, node.field);
317
+ if (value === undefined) {
318
+ return node;
319
+ }
320
+ const truth = compare(
321
+ value,
322
+ node.cmp,
323
+ node.value,
324
+ typeOf(policies, resource, node.field),
325
+ );
326
+ return truth === "unknown" ? UNKNOWN : truth ? TRUE : FALSE;
327
+ }
328
+ case "rel":
329
+ case "some":
330
+ case "every":
331
+ case "none":
332
+ // A relation is never read off the row: whoever built the row could have put
333
+ // anything there. It is decided through the row's identifier or references
334
+ // and `exists`, on what the database holds.
335
+ return node;
336
+ }
337
+ };
338
+
339
+ type AskContext = {
340
+ readonly policies: AccessPolicies;
341
+ readonly row: AccessRow;
342
+ readonly exists: AccessExists | undefined;
343
+ readonly lookups: AccessLookup[];
344
+ };
345
+
346
+ const ask = async (
347
+ context: AskContext,
348
+ resource: string,
349
+ where: AccessWhere,
350
+ ): Promise<AccessTruth> => {
351
+ if (context.exists === undefined) {
352
+ return "unknown";
353
+ }
354
+ const answer = (await context.exists(resource, where)) === true;
355
+ context.lookups.push({ resource, where, answer });
356
+ return answer;
357
+ };
358
+
359
+ /** An `exists` question's `where`: the row's own keys and the residual, null-safe. */
360
+ const combine = (
361
+ policies: AccessPolicies,
362
+ resource: string,
363
+ where: AccessWhere,
364
+ residual: Resolved,
365
+ ): AccessWhere => {
366
+ const condition = translateWhere(nullSafe(policies, resource, residual));
367
+ return mergeWhere([where, condition]) ?? { AND: [where, condition] };
368
+ };
369
+
370
+ /** A row's value as an `exists` question's `where` states it: a `Date` as its ISO instant, a bigint as itself (exact). */
371
+ const whereValue = (value: unknown): unknown =>
372
+ value instanceof Date ? value.toISOString() : value;
373
+
374
+ /** Asks about an existing row, by its first identifier the row carries in full. */
375
+ const askByIdentifier = async (
376
+ context: AskContext,
377
+ resource: string,
378
+ residual: Resolved,
379
+ ): Promise<AccessTruth> => {
380
+ const identifiers =
381
+ ownOf(context.policies.schema, resource)?.identifiers ?? [];
382
+ const identifier = identifiers.find((fields) =>
383
+ fields.every(
384
+ (field) =>
385
+ fieldOf(context.row, field) !== undefined &&
386
+ fieldOf(context.row, field) !== null,
387
+ ),
388
+ );
389
+ if (identifier === undefined) {
390
+ return "unknown";
391
+ }
392
+ const where = Object.fromEntries(
393
+ identifier.map((field) => [
394
+ field,
395
+ { equals: whereValue(fieldOf(context.row, field)) },
396
+ ]),
397
+ );
398
+ return ask(
399
+ context,
400
+ resource,
401
+ combine(context.policies, resource, where, residual),
402
+ );
403
+ };
404
+
405
+ /**
406
+ * The reference values a to-one step's relation takes from the row: `undefined` when
407
+ * this end holds no references, or the row does not carry them all.
408
+ */
409
+ const carriedReferences = (
410
+ context: AskContext,
411
+ resource: string,
412
+ node: Extract<Resolved, { k: "rel" }>,
413
+ ):
414
+ | {
415
+ readonly references: readonly string[];
416
+ readonly values: readonly unknown[];
417
+ }
418
+ | undefined => {
419
+ const relation = ownOf(
420
+ ownOf(context.policies.schema, resource)?.relations,
421
+ node.relation,
422
+ );
423
+ if (
424
+ relation?.referenceFields === undefined ||
425
+ relation.references === undefined
426
+ ) {
427
+ return undefined;
428
+ }
429
+ const values = relation.referenceFields.map((field) =>
430
+ fieldOf(context.row, field),
431
+ );
432
+ return values.some((value) => value === undefined)
433
+ ? undefined
434
+ : { references: relation.references, values };
435
+ };
436
+
437
+ /** Whether a residual holds, outside any relation, a to-one step the row's references decide. */
438
+ const stepsByReference = (
439
+ context: AskContext,
440
+ resource: string,
441
+ node: Resolved,
442
+ ): boolean => {
443
+ switch (node.k) {
444
+ case "and":
445
+ case "all":
446
+ case "or":
447
+ return node.args.some((arg) => stepsByReference(context, resource, arg));
448
+ case "not":
449
+ return stepsByReference(context, resource, node.arg);
450
+ case "rel":
451
+ return carriedReferences(context, resource, node) !== undefined;
452
+ default:
453
+ return false;
454
+ }
455
+ };
456
+
457
+ /**
458
+ * Decides a residual through `exists`. A to-one step whose references the row carries
459
+ * is asked on the row they name, with the row's own values: for a created row, and for
460
+ * an updated one too, since a caller may ask before the write, when the database still
461
+ * holds the old reference. Whatever else an existing row leaves is asked about the row
462
+ * itself, by its identifier (in one question when no step goes by reference); for a
463
+ * created row it is unknown.
464
+ */
465
+ const askOn = async (
466
+ context: AskContext,
467
+ resource: string,
468
+ node: Resolved,
469
+ kind: AccessRowKind,
470
+ ): Promise<AccessTruth> => {
471
+ if (kind === "existing" && !stepsByReference(context, resource, node)) {
472
+ return node.k === "true"
473
+ ? true
474
+ : node.k === "false"
475
+ ? false
476
+ : askByIdentifier(context, resource, node);
477
+ }
478
+ switch (node.k) {
479
+ case "true":
480
+ return true;
481
+ case "false":
482
+ return false;
483
+ case "and":
484
+ case "all": {
485
+ let unknown = false;
486
+ for (const arg of node.args) {
487
+ const answer = await askOn(context, resource, arg, kind);
488
+ if (answer === false) {
489
+ return false;
490
+ }
491
+ unknown ||= answer === "unknown";
492
+ }
493
+ return unknown ? "unknown" : true;
494
+ }
495
+ case "or": {
496
+ let unknown = false;
497
+ for (const arg of node.args) {
498
+ const answer = await askOn(context, resource, arg, kind);
499
+ if (answer === true) {
500
+ return true;
501
+ }
502
+ unknown ||= answer === "unknown";
503
+ }
504
+ return unknown ? "unknown" : false;
505
+ }
506
+ case "not": {
507
+ const answer = await askOn(context, resource, node.arg, kind);
508
+ return answer === "unknown" ? "unknown" : !answer;
509
+ }
510
+ case "rel": {
511
+ const carried = carriedReferences(context, resource, node);
512
+ if (carried === undefined) {
513
+ return kind === "existing"
514
+ ? askByIdentifier(context, resource, node)
515
+ : "unknown";
516
+ }
517
+ if (carried.values.some((value) => value === null)) {
518
+ return false;
519
+ }
520
+ const where = Object.fromEntries(
521
+ carried.references.map((field, index) => [
522
+ field,
523
+ { equals: whereValue(carried.values[index]) },
524
+ ]),
525
+ );
526
+ return ask(
527
+ context,
528
+ node.target,
529
+ combine(context.policies, node.target, where, node.where),
530
+ );
531
+ }
532
+ default:
533
+ return kind === "existing"
534
+ ? askByIdentifier(context, resource, node)
535
+ : "unknown";
536
+ }
537
+ };
538
+
539
+ /**
540
+ * Decides a folded condition on a row: in memory first, then through `exists` for
541
+ * what the row does not carry. `"unknown"` when neither can decide.
542
+ */
543
+ export const decideOnRow = async (args: {
544
+ readonly policies: AccessPolicies;
545
+ readonly resource: string;
546
+ readonly condition: Resolved;
547
+ readonly row: AccessRow;
548
+ readonly kind: AccessRowKind;
549
+ readonly exists: AccessExists | undefined;
550
+ readonly lookups: AccessLookup[];
551
+ }): Promise<AccessTruth> => {
552
+ const residual = evaluateOnRow(
553
+ args.policies,
554
+ args.resource,
555
+ args.condition,
556
+ args.row,
557
+ );
558
+ if (residual.k === "true") {
559
+ return true;
560
+ }
561
+ if (residual.k === "false") {
562
+ return false;
563
+ }
564
+ if (residual.k === "unknown") {
565
+ return "unknown";
566
+ }
567
+ const context: AskContext = {
568
+ policies: args.policies,
569
+ row: args.row,
570
+ exists: args.exists,
571
+ lookups: args.lookups,
572
+ };
573
+ return askOn(context, args.resource, residual, args.kind);
574
+ };