@aventara/access 0.0.0-stage → 0.1.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/CHANGELOG.md +831 -0
  2. package/LICENSE +91 -0
  3. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  4. package/README.md +2439 -2
  5. package/dist/access/access-options.d.ts +66 -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 +273 -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 +12 -0
  28. package/dist/access/principal.subject.js +60 -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 +196 -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 +2 -0
  40. package/dist/access-warning.emitter.js +20 -0
  41. package/dist/access.builder.d.ts +299 -0
  42. package/dist/access.builder.js +66 -0
  43. package/dist/access.engine.d.ts +142 -0
  44. package/dist/access.engine.js +470 -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 +78 -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 +83 -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 +12 -0
  122. package/dist/foreign-references.check.js +134 -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 +72 -0
  134. package/dist/generator/prisma7/prisma-where.translator.js +136 -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 +11 -0
  174. package/dist/mcp/agent-scope.gate.js +75 -0
  175. package/dist/mcp/agent.grant.d.ts +36 -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 +192 -0
  190. package/dist/policies-contract.js +8 -0
  191. package/dist/policies.compiler.d.ts +36 -0
  192. package/dist/policies.compiler.js +1358 -0
  193. package/dist/policies.decider.d.ts +103 -0
  194. package/dist/policies.decider.js +382 -0
  195. package/dist/policies.hash.d.ts +10 -0
  196. package/dist/policies.hash.js +53 -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/reference-reads.check.d.ts +12 -0
  202. package/dist/reference-reads.check.js +148 -0
  203. package/dist/registry/access.registry.d.ts +173 -0
  204. package/dist/registry/access.registry.js +865 -0
  205. package/dist/registry/beacon.ledger.d.ts +30 -0
  206. package/dist/registry/beacon.ledger.js +93 -0
  207. package/dist/registry/ownership.check.d.ts +54 -0
  208. package/dist/registry/ownership.check.js +215 -0
  209. package/dist/registry/registry.endpoints.d.ts +50 -0
  210. package/dist/registry/registry.endpoints.js +193 -0
  211. package/dist/registry/registry.key.d.ts +7 -0
  212. package/dist/registry/registry.key.js +13 -0
  213. package/dist/registry/registry.options.d.ts +13 -0
  214. package/dist/registry/registry.options.js +28 -0
  215. package/dist/registry/registry.refusal.d.ts +6 -0
  216. package/dist/registry/registry.refusal.js +11 -0
  217. package/dist/registry/registry.schema.d.ts +238 -0
  218. package/dist/registry/registry.schema.js +127 -0
  219. package/dist/resources/auth-tables.catalog.d.ts +91 -0
  220. package/dist/resources/auth-tables.catalog.js +102 -0
  221. package/dist/resources/auth-tables.guard.d.ts +95 -0
  222. package/dist/resources/auth-tables.guard.js +405 -0
  223. package/dist/scope.resolver.d.ts +138 -0
  224. package/dist/scope.resolver.js +512 -0
  225. package/dist/sha256.digest.d.ts +2 -0
  226. package/dist/sha256.digest.js +81 -0
  227. package/dist/standalone/access.server.d.ts +41 -0
  228. package/dist/standalone/access.server.js +301 -0
  229. package/dist/standalone/index.d.ts +1 -0
  230. package/dist/standalone/index.js +1 -0
  231. package/package.json +101 -3
  232. package/src/aventara-where.translator.ts +116 -0
  233. package/src/beacon/beacon.check.ts +204 -0
  234. package/src/canonical-json.writer.ts +51 -0
  235. package/src/generator/prisma7/prisma-where.translator.ts +241 -0
  236. package/src/policies-contract.ts +184 -0
  237. package/src/policies.decider.ts +650 -0
  238. package/src/predicate.evaluator.ts +574 -0
  239. package/src/scope.resolver.ts +791 -0
  240. package/src/sha256.digest.ts +85 -0
@@ -0,0 +1,791 @@
1
+ // The principal fold: substitute the principal into every rule, fold what
2
+ // the principal alone decides (claims, origin, `signedIn`), expand roles, and keep only
3
+ // row conditions. Grants combine with OR, denies with AND NOT. What leaves this module
4
+ // holds literals only.
5
+
6
+ import type {
7
+ AccessAction,
8
+ AccessComparison,
9
+ AccessDenyRule,
10
+ AccessGrantRule,
11
+ AccessLiteral,
12
+ AccessOperand,
13
+ AccessOrigin,
14
+ AccessPolicies,
15
+ AccessPredicate,
16
+ AccessPrincipalCondition,
17
+ } from "./policies-contract.js";
18
+
19
+ /**
20
+ * A row condition after the principal fold. `and` is a condition the author wrote
21
+ * as one object (the translator may write it back as one); `all` is a conjunction
22
+ * the engine builds (grant parts, a scope's grants and denies), always written as
23
+ * `AND`.
24
+ */
25
+ export type Resolved =
26
+ | { readonly k: "true" }
27
+ | { readonly k: "false" }
28
+ | { readonly k: "and" | "all" | "or"; readonly args: readonly Resolved[] }
29
+ | { readonly k: "not"; readonly arg: Resolved }
30
+ | {
31
+ readonly k: "cmp";
32
+ readonly field: string;
33
+ readonly cmp: AccessComparison;
34
+ readonly value: AccessLiteral | readonly AccessLiteral[];
35
+ }
36
+ | {
37
+ readonly k: "rel";
38
+ readonly relation: string;
39
+ readonly target: string;
40
+ readonly where: Resolved;
41
+ }
42
+ | {
43
+ readonly k: "some" | "every" | "none";
44
+ readonly relation: string;
45
+ readonly target: string;
46
+ readonly where: Resolved;
47
+ };
48
+
49
+ export const TRUE: Resolved = { k: "true" };
50
+ export const FALSE: Resolved = { k: "false" };
51
+
52
+ const junction = (k: "and" | "all", args: readonly Resolved[]): Resolved => {
53
+ const kept: Resolved[] = [];
54
+ for (const arg of args) {
55
+ if (arg.k === "false") {
56
+ return FALSE;
57
+ }
58
+ if (arg.k !== "true") {
59
+ kept.push(arg);
60
+ }
61
+ }
62
+ if (kept.length === 0) {
63
+ return TRUE;
64
+ }
65
+ return kept.length === 1 ? (kept[0] as Resolved) : { k, args: kept };
66
+ };
67
+
68
+ /** A conjunction the author wrote as one object. */
69
+ export const andOf = (args: readonly Resolved[]): Resolved =>
70
+ junction("and", args);
71
+ /** A conjunction the engine builds. */
72
+ export const allOf = (args: readonly Resolved[]): Resolved =>
73
+ junction("all", args);
74
+
75
+ export const orOf = (args: readonly Resolved[]): Resolved => {
76
+ const kept: Resolved[] = [];
77
+ for (const arg of args) {
78
+ if (arg.k === "true") {
79
+ return TRUE;
80
+ }
81
+ if (arg.k !== "false") {
82
+ kept.push(arg);
83
+ }
84
+ }
85
+ if (kept.length === 0) {
86
+ return FALSE;
87
+ }
88
+ return kept.length === 1 ? (kept[0] as Resolved) : { k: "or", args: kept };
89
+ };
90
+
91
+ export const notOf = (arg: Resolved): Resolved =>
92
+ arg.k === "true" ? FALSE : arg.k === "false" ? TRUE : { k: "not", arg };
93
+
94
+ type Schema = AccessPolicies["schema"];
95
+ type Facts = Schema[string];
96
+
97
+ /**
98
+ * A record's own member: `undefined` for a key the record only inherits
99
+ * (`constructor`, `toString`, `__proto__`), so no name in a file, a row or a principal
100
+ * ever reads `Object.prototype`. One signature per record of the file, not a generic
101
+ * one: each call would instantiate it, and the lookups are many.
102
+ */
103
+ export function ownOf(
104
+ record: Schema | undefined,
105
+ key: string,
106
+ ): Facts | undefined;
107
+ export function ownOf(
108
+ record: AccessPolicies["roles"] | undefined,
109
+ key: string,
110
+ ): AccessPolicies["roles"][string] | undefined;
111
+ export function ownOf(
112
+ record: Facts["relations"] | undefined,
113
+ key: string,
114
+ ): Facts["relations"][string] | undefined;
115
+ export function ownOf(
116
+ record: Facts["types"] | undefined,
117
+ key: string,
118
+ ): NonNullable<Facts["types"]>[string] | undefined;
119
+ export function ownOf(
120
+ record: AccessPolicies["claims"] | undefined,
121
+ key: string,
122
+ ): AccessPolicies["claims"][string] | undefined;
123
+ export function ownOf(
124
+ record: { readonly [key: string]: readonly string[] } | undefined,
125
+ key: string,
126
+ ): readonly string[] | undefined;
127
+ export function ownOf(
128
+ record: { readonly [key: string]: unknown } | undefined,
129
+ key: string,
130
+ ): unknown {
131
+ return record !== undefined && Object.hasOwn(record, key)
132
+ ? record[key]
133
+ : undefined;
134
+ }
135
+
136
+ /** A plain object: made by `{}` or JSON, not an array, a `Date`, a `Map` or a class instance. */
137
+ export const isPlainObject = (
138
+ value: unknown,
139
+ ): value is { readonly [key: string]: unknown } => {
140
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
141
+ return false;
142
+ }
143
+ const prototype: unknown = Object.getPrototypeOf(value);
144
+ return prototype === Object.prototype || prototype === null;
145
+ };
146
+
147
+ /** What every question states about who asks. */
148
+ export type PrincipalFacts = {
149
+ /** The principal: an object, or `null` (anonymous). */
150
+ readonly principal: { readonly [key: string]: unknown } | null;
151
+ readonly origin: AccessOrigin;
152
+ };
153
+
154
+ /**
155
+ * Reads the principal off a question: any object, an instance of a class included
156
+ * (an entity), is a principal; an array, a function or a primitive is anonymous.
157
+ */
158
+ export const principalFactsOf = (
159
+ principal: unknown,
160
+ origin: AccessOrigin,
161
+ ): PrincipalFacts => ({
162
+ principal:
163
+ typeof principal === "object" &&
164
+ principal !== null &&
165
+ !Array.isArray(principal)
166
+ ? (principal as { readonly [key: string]: unknown })
167
+ : null,
168
+ origin,
169
+ });
170
+
171
+ /** The value at `path`, through own members only; `undefined` where one is missing. */
172
+ const readPath = (from: unknown, path: readonly string[]): unknown => {
173
+ let value: unknown = from;
174
+ for (const key of path) {
175
+ if (
176
+ typeof value !== "object" ||
177
+ value === null ||
178
+ !Object.hasOwn(value, key)
179
+ ) {
180
+ return undefined;
181
+ }
182
+ value = (value as { readonly [key: string]: unknown })[key];
183
+ }
184
+ return value;
185
+ };
186
+
187
+ /**
188
+ * A `me` value the principal does not have: anonymous, a missing or null property,
189
+ * an empty string (no identity is `""`), a number JSON cannot carry, or anything but
190
+ * a string, number or boolean.
191
+ */
192
+ class MissingPrincipalValue extends Error {}
193
+
194
+ const literalOf = (
195
+ operand: AccessOperand,
196
+ facts: PrincipalFacts,
197
+ ): AccessLiteral => {
198
+ if (operand === null || typeof operand !== "object") {
199
+ return operand;
200
+ }
201
+ const value =
202
+ facts.principal === null
203
+ ? undefined
204
+ : readPath(facts.principal, operand.principal);
205
+ if (
206
+ value === "" ||
207
+ (typeof value === "number" && !Number.isFinite(value)) ||
208
+ (typeof value !== "string" &&
209
+ typeof value !== "number" &&
210
+ typeof value !== "boolean")
211
+ ) {
212
+ throw new MissingPrincipalValue();
213
+ }
214
+ return value;
215
+ };
216
+
217
+ /** A condition on the principal, three-valued: a claim it lacks is `"unknown"`. */
218
+ const principalTruth = (
219
+ condition: AccessPrincipalCondition,
220
+ facts: PrincipalFacts,
221
+ ): boolean | "unknown" => {
222
+ switch (condition.op) {
223
+ case "and": {
224
+ let unknown = false;
225
+ for (const arg of condition.args) {
226
+ const truth = principalTruth(arg, facts);
227
+ if (truth === false) {
228
+ return false;
229
+ }
230
+ unknown ||= truth === "unknown";
231
+ }
232
+ return unknown ? "unknown" : true;
233
+ }
234
+ case "origin":
235
+ return facts.origin === condition.value;
236
+ case "cmp": {
237
+ if (facts.principal === null) {
238
+ // The anonymous caller has no token, so none can be stale: no flag is set on
239
+ // it (a ban does not close an `anyone` grant), and any other claim is unknown.
240
+ return condition.value === true ? false : "unknown";
241
+ }
242
+ const value = readPath(facts.principal, ["claims", ...condition.claim]);
243
+ if (value === undefined || value === null) {
244
+ return "unknown";
245
+ }
246
+ return typeof value === typeof condition.value
247
+ ? value === condition.value
248
+ : "unknown";
249
+ }
250
+ }
251
+ };
252
+
253
+ /**
254
+ * Folds a condition on the principal alone (claims, origin) to a boolean. A claim
255
+ * the principal lacks, or holds as another type, fails closed: a grant's global role
256
+ * is not held, and a deny's `if` holds, whatever value it compares with. Access mints
257
+ * every declared boolean claim, `false` when unset, so a token it issued lacks none;
258
+ * a foreign or stale token without `banned` is denied by `banned: true`. The
259
+ * anonymous caller is the one exception: a comparison with `true` never holds for it.
260
+ */
261
+ export const foldPrincipalCondition = (
262
+ condition: AccessPrincipalCondition,
263
+ facts: PrincipalFacts,
264
+ rule: "grant" | "deny",
265
+ ): boolean => {
266
+ const truth = principalTruth(condition, facts);
267
+ return truth === "unknown" ? rule === "deny" : truth;
268
+ };
269
+
270
+ /**
271
+ * How a rule reads a `me` value the principal lacks. A grant drops: it never
272
+ * matches, whatever `NOT` surrounds the value. A deny matches as a whole, whatever
273
+ * `not` or `NOT` surrounds the value. Both fail closed.
274
+ */
275
+ export type MissingMode = "drop-rule" | "match-rule";
276
+
277
+ const resolveIn = (
278
+ policies: AccessPolicies,
279
+ predicate: AccessPredicate,
280
+ facts: PrincipalFacts,
281
+ mode: MissingMode,
282
+ ): Resolved => {
283
+ switch (predicate.op) {
284
+ case "true":
285
+ return TRUE;
286
+ case "false":
287
+ return FALSE;
288
+ case "and":
289
+ return andOf(
290
+ predicate.args.map((arg) => resolveIn(policies, arg, facts, mode)),
291
+ );
292
+ case "or":
293
+ return orOf(
294
+ predicate.args.map((arg) => resolveIn(policies, arg, facts, mode)),
295
+ );
296
+ case "not":
297
+ return notOf(resolveIn(policies, predicate.arg, facts, mode));
298
+ case "cmp": {
299
+ const value = Array.isArray(predicate.value)
300
+ ? (predicate.value as readonly AccessOperand[]).map((item) =>
301
+ literalOf(item, facts),
302
+ )
303
+ : literalOf(predicate.value as AccessOperand, facts);
304
+ return { k: "cmp", field: predicate.field, cmp: predicate.cmp, value };
305
+ }
306
+ case "rel": {
307
+ const where = resolveIn(policies, predicate.where, facts, mode);
308
+ return where.k === "false"
309
+ ? FALSE
310
+ : {
311
+ k: "rel",
312
+ relation: predicate.relation,
313
+ target: predicate.target,
314
+ where,
315
+ };
316
+ }
317
+ case "some":
318
+ case "every":
319
+ case "none": {
320
+ const where = resolveIn(policies, predicate.where, facts, mode);
321
+ if (predicate.op === "some" && where.k === "false") {
322
+ return FALSE;
323
+ }
324
+ if (predicate.op === "none" && where.k === "false") {
325
+ return TRUE;
326
+ }
327
+ if (predicate.op === "every" && where.k === "true") {
328
+ return TRUE;
329
+ }
330
+ return {
331
+ k: predicate.op,
332
+ relation: predicate.relation,
333
+ target: predicate.target,
334
+ where,
335
+ };
336
+ }
337
+ case "role": {
338
+ const role = ownOf(policies.roles, predicate.role);
339
+ if (role === undefined || !("on" in role)) {
340
+ return FALSE;
341
+ }
342
+ return resolveIn(policies, role.where, facts, mode);
343
+ }
344
+ }
345
+ };
346
+
347
+ /**
348
+ * Substitutes the principal into a row condition. A `me` value the principal lacks
349
+ * makes the whole rule `false` for a grant (`drop-rule`) and `true` for a deny
350
+ * (`match-rule`).
351
+ */
352
+ export const resolvePredicate = (
353
+ policies: AccessPolicies,
354
+ predicate: AccessPredicate,
355
+ facts: PrincipalFacts,
356
+ mode: MissingMode,
357
+ ): Resolved => {
358
+ try {
359
+ return resolveIn(policies, predicate, facts, mode);
360
+ } catch (error) {
361
+ if (error instanceof MissingPrincipalValue) {
362
+ return mode === "drop-rule" ? FALSE : TRUE;
363
+ }
364
+ throw error;
365
+ }
366
+ };
367
+
368
+ /** Whether a rule names this action on this Resource. */
369
+ const covers = (
370
+ resource: string,
371
+ action: AccessAction,
372
+ rule: AccessGrantRule | AccessDenyRule,
373
+ ): boolean =>
374
+ (rule.resource === resource || rule.resource === "*") &&
375
+ (rule.actions === "*" || rule.actions.includes(action));
376
+
377
+ /**
378
+ * The grants and denies a question about `action` on `resource` reads. A grant on
379
+ * `"*"` reaches the file's own Resources only: never a foreign one, and never one
380
+ * its schema does not list. A deny on `"*"` reaches every Resource.
381
+ */
382
+ export const rulesFor = (
383
+ policies: AccessPolicies,
384
+ resource: string,
385
+ action: AccessAction,
386
+ ) => {
387
+ const facts = ownOf(policies.schema, resource);
388
+ const own = facts !== undefined && facts.foreign === undefined;
389
+ return {
390
+ grants: policies.grants.filter(
391
+ (grant) =>
392
+ (own || grant.resource !== "*") && covers(resource, action, grant),
393
+ ),
394
+ denies: policies.denies.filter((deny) => covers(resource, action, deny)),
395
+ };
396
+ };
397
+
398
+ /** What a subject contributes to a grant, folded: `true`, `false` or a row condition. */
399
+ const subjectCondition = (
400
+ policies: AccessPolicies,
401
+ grant: AccessGrantRule,
402
+ subject: string,
403
+ facts: PrincipalFacts,
404
+ ): Resolved => {
405
+ if (subject === "anyone") {
406
+ return TRUE;
407
+ }
408
+ if (subject === "signedIn") {
409
+ return facts.principal === null ? FALSE : TRUE;
410
+ }
411
+ const role = ownOf(policies.roles, subject);
412
+ if (role === undefined) {
413
+ return FALSE;
414
+ }
415
+ if ("global" in role) {
416
+ return foldPrincipalCondition(role.global, facts, "grant") ? TRUE : FALSE;
417
+ }
418
+ const held: AccessPredicate = { op: "role", role: subject };
419
+ const predicate: AccessPredicate =
420
+ grant.in === undefined
421
+ ? held
422
+ : { op: "rel", relation: grant.in, target: role.on, where: held };
423
+ return resolvePredicate(policies, predicate, facts, "drop-rule");
424
+ };
425
+
426
+ /** One grant, for one of its subjects, folded for the principal. */
427
+ export type FoldedGrant = {
428
+ readonly grant: AccessGrantRule;
429
+ readonly subject: string;
430
+ readonly condition: Resolved;
431
+ };
432
+
433
+ /**
434
+ * Each grant's condition per subject: its `where`, then the subject's (its row
435
+ * role, through `in`), then, with `after`, the `after` condition. A grant that reads
436
+ * a `me` value the principal lacks drops.
437
+ */
438
+ export const foldGrants = (
439
+ policies: AccessPolicies,
440
+ grants: readonly AccessGrantRule[],
441
+ facts: PrincipalFacts,
442
+ withAfter = false,
443
+ ): FoldedGrant[] =>
444
+ grants.flatMap((grant) =>
445
+ grant.subjects.map((subject) => {
446
+ const parts: Resolved[] = [];
447
+ if (grant.where !== undefined) {
448
+ parts.push(resolvePredicate(policies, grant.where, facts, "drop-rule"));
449
+ }
450
+ parts.push(subjectCondition(policies, grant, subject, facts));
451
+ if (withAfter && grant.after !== undefined) {
452
+ parts.push(resolvePredicate(policies, grant.after, facts, "drop-rule"));
453
+ }
454
+ return { grant, subject, condition: allOf(parts) };
455
+ }),
456
+ );
457
+
458
+ /** One deny, folded for the principal. */
459
+ export type FoldedDeny = {
460
+ readonly deny: AccessDenyRule;
461
+ readonly condition: Resolved;
462
+ };
463
+
464
+ /**
465
+ * Each deny's row condition: `false` when its `if` does not hold for this principal,
466
+ * `true` when its `where` reads a `me` value the principal lacks.
467
+ */
468
+ export const foldDenies = (
469
+ policies: AccessPolicies,
470
+ denies: readonly AccessDenyRule[],
471
+ facts: PrincipalFacts,
472
+ ): FoldedDeny[] =>
473
+ denies.map((deny) => {
474
+ if (
475
+ deny.if !== undefined &&
476
+ !foldPrincipalCondition(deny.if, facts, "deny")
477
+ ) {
478
+ return { deny, condition: FALSE };
479
+ }
480
+ return {
481
+ deny,
482
+ condition:
483
+ deny.where === undefined
484
+ ? TRUE
485
+ : resolvePredicate(policies, deny.where, facts, "match-rule"),
486
+ };
487
+ });
488
+
489
+ /** The rows a principal may `action`: OR of the grants, AND NOT each deny. */
490
+ export const scopeCondition = (
491
+ grants: readonly FoldedGrant[],
492
+ denies: readonly FoldedDeny[],
493
+ ): Resolved => {
494
+ const allowed = orOf(grants.map((grant) => grant.condition));
495
+ if (allowed.k === "false") {
496
+ return FALSE;
497
+ }
498
+ return allOf([allowed, ...denies.map((deny) => notOf(deny.condition))]);
499
+ };
500
+
501
+ // ---------------------------------------------------------------------------
502
+ // Null-safe form: the condition as SQL must read it to keep the rows `can` keeps
503
+ // ---------------------------------------------------------------------------
504
+
505
+ type Comparison = Extract<Resolved, { readonly k: "cmp" }>;
506
+ type Step = Extract<Resolved, { readonly k: "rel" }>;
507
+ type Quantified = Extract<Resolved, { readonly k: "some" | "every" | "none" }>;
508
+
509
+ /** Whether the schema entry lists `field` as nullable. A file without `nullable` has none. */
510
+ const nullableIn = (
511
+ policies: AccessPolicies,
512
+ resource: string,
513
+ field: string,
514
+ ): boolean =>
515
+ ownOf(policies.schema, resource)?.nullable?.includes(field) === true;
516
+
517
+ /**
518
+ * Whether a to-one step can find no row: `"nullable"` when one of its reference
519
+ * fields is nullable, `"required"` when it holds references and none is, and
520
+ * `"unstated"` for the end that holds no references (the other end's step).
521
+ */
522
+ const stepKind = (
523
+ policies: AccessPolicies,
524
+ resource: string,
525
+ node: Step,
526
+ ): "nullable" | "required" | "unstated" => {
527
+ const fields = ownOf(
528
+ ownOf(policies.schema, resource)?.relations,
529
+ node.relation,
530
+ )?.referenceFields;
531
+ if (fields === undefined) {
532
+ return "unstated";
533
+ }
534
+ return fields.some((field) => nullableIn(policies, resource, field))
535
+ ? "nullable"
536
+ : "required";
537
+ };
538
+
539
+ /**
540
+ * Whether SQL can read a comparison as unknown where `can` reads it as false: a
541
+ * nullable field compared with a value (SQL's `NULL = v` is unknown; `f = NULL` is
542
+ * written `IS NULL` and never is), or an `in` list that holds `null`.
543
+ */
544
+ const unknownIn = (
545
+ policies: AccessPolicies,
546
+ resource: string,
547
+ node: Comparison,
548
+ ): boolean =>
549
+ Array.isArray(node.value)
550
+ ? (node.value as readonly AccessLiteral[]).includes(null) ||
551
+ nullableIn(policies, resource, node.field)
552
+ : node.value !== null && nullableIn(policies, resource, node.field);
553
+
554
+ /** Whether SQL reads `node` exactly as `can` does: no unknown comparison, no step that can find no row. */
555
+ const known = (
556
+ policies: AccessPolicies,
557
+ resource: string,
558
+ node: Resolved,
559
+ ): boolean => {
560
+ switch (node.k) {
561
+ case "true":
562
+ case "false":
563
+ return true;
564
+ case "cmp":
565
+ return !unknownIn(policies, resource, node);
566
+ case "and":
567
+ case "all":
568
+ case "or":
569
+ return node.args.every((arg) => known(policies, resource, arg));
570
+ case "not":
571
+ return known(policies, resource, node.arg);
572
+ case "rel":
573
+ return (
574
+ stepKind(policies, resource, node) !== "nullable" &&
575
+ known(policies, node.target, node.where)
576
+ );
577
+ case "some":
578
+ case "every":
579
+ case "none":
580
+ return known(policies, node.target, node.where);
581
+ }
582
+ };
583
+
584
+ /**
585
+ * A comparison with the `null`s taken out of its `in` list: `can` never matches a
586
+ * NULL field through a list, and SQL's `IN (NULL, …)` is unknown for every row. A
587
+ * list left empty: `in` matches no row; `notIn` every row whose field is not NULL,
588
+ * which on a required field is every row.
589
+ */
590
+ const listWithoutNull = (
591
+ policies: AccessPolicies,
592
+ resource: string,
593
+ node: Comparison,
594
+ ): Resolved => {
595
+ if (!Array.isArray(node.value)) {
596
+ return node;
597
+ }
598
+ const value: AccessLiteral[] = [];
599
+ for (const item of node.value as readonly AccessLiteral[]) {
600
+ if (item !== null) {
601
+ value.push(item);
602
+ }
603
+ }
604
+ if (value.length > 0) {
605
+ return { k: "cmp", field: node.field, cmp: node.cmp, value };
606
+ }
607
+ if (node.cmp === "in") {
608
+ return FALSE;
609
+ }
610
+ return nullableIn(policies, resource, node.field)
611
+ ? { k: "cmp", field: node.field, cmp: "ne", value: null }
612
+ : TRUE;
613
+ };
614
+
615
+ const isNull = (field: string): Resolved => ({
616
+ k: "cmp",
617
+ field,
618
+ cmp: "eq",
619
+ value: null,
620
+ });
621
+
622
+ /** The same to-one step, around another condition. */
623
+ const step = (node: Step, where: Resolved): Resolved => ({
624
+ k: "rel",
625
+ relation: node.relation,
626
+ target: node.target,
627
+ where,
628
+ });
629
+
630
+ /** A quantifier over the same to-many relation. */
631
+ const quantified = (
632
+ k: Quantified["k"],
633
+ node: Quantified,
634
+ where: Resolved,
635
+ ): Resolved => ({ k, relation: node.relation, target: node.target, where });
636
+
637
+ /** One rewrite applied to each condition of a list. */
638
+ const each = (
639
+ rewrite: (
640
+ policies: AccessPolicies,
641
+ resource: string,
642
+ node: Resolved,
643
+ ) => Resolved,
644
+ policies: AccessPolicies,
645
+ resource: string,
646
+ nodes: readonly Resolved[],
647
+ ): Resolved[] => {
648
+ const out: Resolved[] = [];
649
+ for (const node of nodes) {
650
+ out.push(rewrite(policies, resource, node));
651
+ }
652
+ return out;
653
+ };
654
+
655
+ /** The condition, true on exactly the rows where `can` reads it as true. */
656
+ const holds = (
657
+ policies: AccessPolicies,
658
+ resource: string,
659
+ node: Resolved,
660
+ ): Resolved => {
661
+ if (known(policies, resource, node)) {
662
+ return node;
663
+ }
664
+ switch (node.k) {
665
+ case "true":
666
+ case "false":
667
+ return node;
668
+ case "cmp":
669
+ return listWithoutNull(policies, resource, node);
670
+ case "and":
671
+ return andOf(each(holds, policies, resource, node.args));
672
+ case "all":
673
+ return allOf(each(holds, policies, resource, node.args));
674
+ case "or":
675
+ return orOf(each(holds, policies, resource, node.args));
676
+ case "not":
677
+ return fails(policies, resource, node.arg);
678
+ case "rel":
679
+ // A step that finds no row is false, in SQL as in `can`.
680
+ return step(node, holds(policies, node.target, node.where));
681
+ case "some":
682
+ case "none":
683
+ // `EXISTS` and `NOT EXISTS`: true or false around the rows they count.
684
+ return quantified(node.k, node, holds(policies, node.target, node.where));
685
+ case "every":
686
+ // An ORM may write `every` as `NOT EXISTS (… WHERE NOT condition)`, where an
687
+ // unknown condition passes: ask that no related row fails it instead.
688
+ return quantified("none", node, fails(policies, node.target, node.where));
689
+ }
690
+ };
691
+
692
+ /** The condition's negation, true on exactly the rows where `can` reads it as false. */
693
+ const fails = (
694
+ policies: AccessPolicies,
695
+ resource: string,
696
+ node: Resolved,
697
+ ): Resolved => {
698
+ if (known(policies, resource, node)) {
699
+ return notOf(node);
700
+ }
701
+ switch (node.k) {
702
+ case "true":
703
+ case "false":
704
+ return notOf(node);
705
+ case "cmp": {
706
+ const negated = notOf(listWithoutNull(policies, resource, node));
707
+ return nullableIn(policies, resource, node.field)
708
+ ? orOf([negated, isNull(node.field)])
709
+ : negated;
710
+ }
711
+ case "and":
712
+ case "all": {
713
+ // Comparisons only: `NOT (a AND b) OR a IS NULL OR b IS NULL`.
714
+ if (node.args.every((arg) => arg.k === "cmp")) {
715
+ const comparisons = node.args as readonly Comparison[];
716
+ const guards: Resolved[] = [];
717
+ const guarded: string[] = [];
718
+ for (const arg of comparisons) {
719
+ if (
720
+ arg.value !== null &&
721
+ nullableIn(policies, resource, arg.field) &&
722
+ !guarded.includes(arg.field)
723
+ ) {
724
+ guarded.push(arg.field);
725
+ guards.push(isNull(arg.field));
726
+ }
727
+ }
728
+ const lists: Resolved[] = [];
729
+ for (const arg of comparisons) {
730
+ lists.push(listWithoutNull(policies, resource, arg));
731
+ }
732
+ const strict = (node.k === "and" ? andOf : allOf)(lists);
733
+ return orOf([notOf(strict), ...guards]);
734
+ }
735
+ return orOf(each(fails, policies, resource, node.args));
736
+ }
737
+ case "or":
738
+ return allOf(each(fails, policies, resource, node.args));
739
+ case "not":
740
+ return holds(policies, resource, node.arg);
741
+ case "rel": {
742
+ const failing = step(node, fails(policies, node.target, node.where));
743
+ switch (stepKind(policies, resource, node)) {
744
+ case "required":
745
+ // The row is always there: the step fails where its condition does.
746
+ return failing;
747
+ case "nullable": {
748
+ // No row (a NULL reference) fails it too, said on this row's own fields.
749
+ const references =
750
+ ownOf(ownOf(policies.schema, resource)?.relations, node.relation)
751
+ ?.referenceFields ?? [];
752
+ const guards: Resolved[] = [];
753
+ for (const field of references) {
754
+ guards.push(isNull(field));
755
+ }
756
+ guards.push(failing);
757
+ return orOf(guards);
758
+ }
759
+ case "unstated":
760
+ // No reference fields here to say "no row" with: the `NOT` of the step
761
+ // covers the missing row, the failing condition the row that is there.
762
+ return orOf([
763
+ notOf(step(node, holds(policies, node.target, node.where))),
764
+ failing,
765
+ ]);
766
+ }
767
+ return failing;
768
+ }
769
+ case "some":
770
+ case "none":
771
+ return notOf(
772
+ quantified(node.k, node, holds(policies, node.target, node.where)),
773
+ );
774
+ case "every":
775
+ return quantified("some", node, fails(policies, node.target, node.where));
776
+ }
777
+ };
778
+
779
+ /**
780
+ * A folded condition on `resource` in the form a database reads as `can` does.
781
+ * `can` reads a comparison on a NULL field as false, so its `NOT` keeps the row; SQL
782
+ * reads it as unknown, and its `NOT` drops the row. Each negated comparison on a
783
+ * nullable field (the schema entry's `nullable`) gets its own guard, `NOT (f = v) OR
784
+ * f IS NULL`, and a negated to-one step whose reference is nullable gets `OR fk IS
785
+ * NULL`. A condition with neither is returned as it is.
786
+ */
787
+ export const nullSafe = (
788
+ policies: AccessPolicies,
789
+ resource: string,
790
+ node: Resolved,
791
+ ): Resolved => holds(policies, resource, node);