@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,378 @@
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
+ import { mergeWhere, translateWhere, } from "./aventara-where.translator.js";
8
+ import { allOf, andOf, FALSE, notOf, nullSafe, orOf, ownOf, TRUE, } from "./scope.resolver.js";
9
+ /** The type `can` compares `field` of `resource` by. */
10
+ const typeOf = (policies, resource, field) => ownOf(ownOf(policies.schema, resource)?.types, field);
11
+ const DECIMAL = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
12
+ /**
13
+ * A decimal's value as `{ sign, digits, point }`: `0.<digits> × 10^point`, with no
14
+ * leading or trailing zero in `digits` (zero is `{ sign: "", digits: "", point: 0 }`).
15
+ * `undefined` for text that is no decimal.
16
+ */
17
+ export const decimalParts = (text) => {
18
+ const match = DECIMAL.exec(text);
19
+ if (match === null) {
20
+ return undefined;
21
+ }
22
+ // Indexed, not destructured: destructuring a match costs the type checker its
23
+ // iterator types (about 1,000 instantiations, the type budget's whole margin).
24
+ const sign = match[1] ?? "";
25
+ const whole = match[2] ?? "";
26
+ const fraction = match[3] ?? "";
27
+ const exponent = match[4] ?? "0";
28
+ const shift = Number(exponent);
29
+ if (!Number.isSafeInteger(shift)) {
30
+ return undefined;
31
+ }
32
+ let digits = whole + fraction;
33
+ let point = whole.length + shift;
34
+ const leading = digits.length - digits.replace(/^0+/, "").length;
35
+ digits = digits.slice(leading).replace(/0+$/, "");
36
+ point -= leading;
37
+ return digits === ""
38
+ ? { sign: "", digits: "", point: 0 }
39
+ : { sign: sign === "-" ? "-" : "", digits, point };
40
+ };
41
+ /**
42
+ * An ISO-8601 date, or date and time with its offset (never a local time), to the
43
+ * millisecond at most.
44
+ */
45
+ const INSTANT = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|([+-])(\d{2}):(\d{2})))?$/;
46
+ /**
47
+ * The epoch milliseconds of an ISO-8601 string that reads back as written: no
48
+ * February 30th, no hour 24, no offset past 23:59, nothing past the millisecond.
49
+ */
50
+ const instantOfText = (text) => {
51
+ const match = INSTANT.exec(text);
52
+ if (match === null) {
53
+ return undefined;
54
+ }
55
+ const part = (index) => Number(match[index] ?? "0");
56
+ const year = part(1);
57
+ const month = part(2);
58
+ const day = part(3);
59
+ const hour = part(4);
60
+ const minute = part(5);
61
+ const second = part(6);
62
+ const millisecond = Number((match[7] ?? "").padEnd(3, "0"));
63
+ const local = Date.UTC(year, month - 1, day, hour, minute, second, millisecond);
64
+ const read = new Date(local);
65
+ if (read.getUTCFullYear() !== year ||
66
+ read.getUTCMonth() !== month - 1 ||
67
+ read.getUTCDate() !== day ||
68
+ read.getUTCHours() !== hour ||
69
+ read.getUTCMinutes() !== minute ||
70
+ read.getUTCSeconds() !== second ||
71
+ part(10) > 23 ||
72
+ part(11) > 59) {
73
+ return undefined;
74
+ }
75
+ const offset = (part(10) * 60 + part(11)) * 60_000;
76
+ return match[9] === "-" ? local + offset : local - offset;
77
+ };
78
+ /** An instant's epoch milliseconds, from a valid `Date` or an ISO-8601 string that reads back as written. */
79
+ export const instantOf = (value) => {
80
+ if (value instanceof Date) {
81
+ const time = value.getTime();
82
+ return Number.isNaN(time) ? undefined : time;
83
+ }
84
+ return typeof value === "string" ? instantOfText(value) : undefined;
85
+ };
86
+ /** A decimal object's text: core's `AvDecimal`, an ORM's decimal, anything whose `toString()` is decimal digits. */
87
+ const decimalText = (value) => typeof value === "string"
88
+ ? value
89
+ : typeof value === "number"
90
+ ? Number.isFinite(value)
91
+ ? String(value)
92
+ : undefined
93
+ : typeof value === "object" &&
94
+ value !== null &&
95
+ !(value instanceof Date) &&
96
+ !Array.isArray(value)
97
+ ? String(value)
98
+ : undefined;
99
+ /**
100
+ * The key a value is compared by, for a field of `type`: equal keys, equal values.
101
+ * `undefined` when the value is not one of the type's (a type mismatch).
102
+ */
103
+ const comparisonKey = (value, type) => {
104
+ switch (type) {
105
+ case "decimal": {
106
+ const text = decimalText(value);
107
+ const parts = text === undefined ? undefined : decimalParts(text);
108
+ return parts === undefined
109
+ ? undefined
110
+ : `${parts.sign}${parts.digits}@${parts.point}`;
111
+ }
112
+ case "datetime":
113
+ return instantOf(value);
114
+ case "bigint":
115
+ return typeof value === "bigint"
116
+ ? value
117
+ : typeof value === "number" && Number.isSafeInteger(value)
118
+ ? BigInt(value)
119
+ : typeof value === "string" && /^-?\d+$/.test(value)
120
+ ? BigInt(value)
121
+ : undefined;
122
+ default:
123
+ return typeof value === "string" ||
124
+ typeof value === "boolean" ||
125
+ (typeof value === "number" && Number.isFinite(value))
126
+ ? value
127
+ : undefined;
128
+ }
129
+ };
130
+ /**
131
+ * One comparison of a row's value with a rule's, three-valued: a value of another
132
+ * type than the field's, or than the rule's, is `"unknown"`.
133
+ */
134
+ const compare = (rowValue, cmp, value, type) => {
135
+ if (rowValue === null) {
136
+ // A comparison on NULL is false, except `equals: null` (IS NULL); a list never
137
+ // holds it for `can`.
138
+ return cmp === "eq" && value === null;
139
+ }
140
+ const actual = comparisonKey(rowValue, type);
141
+ const equal = (literal) => {
142
+ const expected = comparisonKey(literal, type);
143
+ return actual === undefined ||
144
+ expected === undefined ||
145
+ typeof actual !== typeof expected
146
+ ? "unknown"
147
+ : actual === expected;
148
+ };
149
+ if (cmp === "eq" || cmp === "ne") {
150
+ if (value === null) {
151
+ return cmp === "ne";
152
+ }
153
+ const truth = equal(value);
154
+ return truth === "unknown" ? truth : cmp === "eq" ? truth : !truth;
155
+ }
156
+ let unknown = false;
157
+ for (const literal of value) {
158
+ if (literal === null) {
159
+ continue;
160
+ }
161
+ const truth = equal(literal);
162
+ if (truth === true) {
163
+ return cmp === "in";
164
+ }
165
+ unknown ||= truth === "unknown";
166
+ }
167
+ return unknown ? "unknown" : cmp === "notIn";
168
+ };
169
+ const UNKNOWN = { k: "unknown" };
170
+ /**
171
+ * A conjunction, three-valued. A part that is unknown makes the whole unknown unless
172
+ * another part is false: an unknown never waits on an `exists` question, and the
173
+ * caller fails closed on it.
174
+ */
175
+ const and3 = (k, args) => args.some((arg) => arg.k === "false")
176
+ ? FALSE
177
+ : args.some((arg) => arg.k === "unknown")
178
+ ? UNKNOWN
179
+ : (k === "and" ? andOf : allOf)(args);
180
+ const or3 = (args) => args.some((arg) => arg.k === "true")
181
+ ? TRUE
182
+ : args.some((arg) => arg.k === "unknown")
183
+ ? UNKNOWN
184
+ : orOf(args);
185
+ /** A row's own field: an inherited one is not on the row. */
186
+ const fieldOf = (row, field) => Object.hasOwn(row, field) ? row[field] : undefined;
187
+ /**
188
+ * Evaluates what the row carries. The answer is `TRUE`, `FALSE`, `UNKNOWN` (a value
189
+ * of another type than its field's), or the residual condition: the part the row
190
+ * could not decide, with the decided parts folded away. A relation member on the row
191
+ * is ignored: every relation step stays whole in the residual, for `exists`. Each comparison is made by its
192
+ * field's type (`types`): a decimal by its digits, a datetime by its instant, a
193
+ * bigint exactly.
194
+ */
195
+ export const evaluateOnRow = (policies, resource, node, row) => {
196
+ switch (node.k) {
197
+ case "true":
198
+ case "false":
199
+ return node;
200
+ case "and":
201
+ case "all":
202
+ return and3(node.k, node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)));
203
+ case "or":
204
+ return or3(node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)));
205
+ case "not": {
206
+ const arg = evaluateOnRow(policies, resource, node.arg, row);
207
+ return arg.k === "unknown" ? arg : notOf(arg);
208
+ }
209
+ case "cmp": {
210
+ const value = fieldOf(row, node.field);
211
+ if (value === undefined) {
212
+ return node;
213
+ }
214
+ const truth = compare(value, node.cmp, node.value, typeOf(policies, resource, node.field));
215
+ return truth === "unknown" ? UNKNOWN : truth ? TRUE : FALSE;
216
+ }
217
+ case "rel":
218
+ case "some":
219
+ case "every":
220
+ case "none":
221
+ // A relation is never read off the row: whoever built the row could have put
222
+ // anything there. It is decided through the row's identifier or references
223
+ // and `exists`, on what the database holds.
224
+ return node;
225
+ }
226
+ };
227
+ const ask = async (context, resource, where) => {
228
+ if (context.exists === undefined) {
229
+ return "unknown";
230
+ }
231
+ const answer = (await context.exists(resource, where)) === true;
232
+ context.lookups.push({ resource, where, answer });
233
+ return answer;
234
+ };
235
+ /** An `exists` question's `where`: the row's own keys and the residual, null-safe. */
236
+ const combine = (policies, resource, where, residual) => {
237
+ const condition = translateWhere(nullSafe(policies, resource, residual));
238
+ return mergeWhere([where, condition]) ?? { AND: [where, condition] };
239
+ };
240
+ /** A row's value as an `exists` question's `where` states it: a `Date` as its ISO instant, a bigint as itself (exact). */
241
+ const whereValue = (value) => value instanceof Date ? value.toISOString() : value;
242
+ /** Asks about an existing row, by its first identifier the row carries in full. */
243
+ const askByIdentifier = async (context, resource, residual) => {
244
+ const identifiers = ownOf(context.policies.schema, resource)?.identifiers ?? [];
245
+ const identifier = identifiers.find((fields) => fields.every((field) => fieldOf(context.row, field) !== undefined &&
246
+ fieldOf(context.row, field) !== null));
247
+ if (identifier === undefined) {
248
+ return "unknown";
249
+ }
250
+ const where = Object.fromEntries(identifier.map((field) => [
251
+ field,
252
+ { equals: whereValue(fieldOf(context.row, field)) },
253
+ ]));
254
+ return ask(context, resource, combine(context.policies, resource, where, residual));
255
+ };
256
+ /**
257
+ * The reference values a to-one step's relation takes from the row: `undefined` when
258
+ * this end holds no references, or the row does not carry them all.
259
+ */
260
+ const carriedReferences = (context, resource, node) => {
261
+ const relation = ownOf(ownOf(context.policies.schema, resource)?.relations, node.relation);
262
+ if (relation?.referenceFields === undefined ||
263
+ relation.references === undefined) {
264
+ return undefined;
265
+ }
266
+ const values = relation.referenceFields.map((field) => fieldOf(context.row, field));
267
+ return values.some((value) => value === undefined)
268
+ ? undefined
269
+ : { references: relation.references, values };
270
+ };
271
+ /** Whether a residual holds, outside any relation, a to-one step the row's references decide. */
272
+ const stepsByReference = (context, resource, node) => {
273
+ switch (node.k) {
274
+ case "and":
275
+ case "all":
276
+ case "or":
277
+ return node.args.some((arg) => stepsByReference(context, resource, arg));
278
+ case "not":
279
+ return stepsByReference(context, resource, node.arg);
280
+ case "rel":
281
+ return carriedReferences(context, resource, node) !== undefined;
282
+ default:
283
+ return false;
284
+ }
285
+ };
286
+ /**
287
+ * Decides a residual through `exists`. A to-one step whose references the row carries
288
+ * is asked on the row they name, with the row's own values: for a created row, and for
289
+ * an updated one too, since a caller may ask before the write, when the database still
290
+ * holds the old reference. Whatever else an existing row leaves is asked about the row
291
+ * itself, by its identifier (in one question when no step goes by reference); for a
292
+ * created row it is unknown.
293
+ */
294
+ const askOn = async (context, resource, node, kind) => {
295
+ if (kind === "existing" && !stepsByReference(context, resource, node)) {
296
+ return node.k === "true"
297
+ ? true
298
+ : node.k === "false"
299
+ ? false
300
+ : askByIdentifier(context, resource, node);
301
+ }
302
+ switch (node.k) {
303
+ case "true":
304
+ return true;
305
+ case "false":
306
+ return false;
307
+ case "and":
308
+ case "all": {
309
+ let unknown = false;
310
+ for (const arg of node.args) {
311
+ const answer = await askOn(context, resource, arg, kind);
312
+ if (answer === false) {
313
+ return false;
314
+ }
315
+ unknown ||= answer === "unknown";
316
+ }
317
+ return unknown ? "unknown" : true;
318
+ }
319
+ case "or": {
320
+ let unknown = false;
321
+ for (const arg of node.args) {
322
+ const answer = await askOn(context, resource, arg, kind);
323
+ if (answer === true) {
324
+ return true;
325
+ }
326
+ unknown ||= answer === "unknown";
327
+ }
328
+ return unknown ? "unknown" : false;
329
+ }
330
+ case "not": {
331
+ const answer = await askOn(context, resource, node.arg, kind);
332
+ return answer === "unknown" ? "unknown" : !answer;
333
+ }
334
+ case "rel": {
335
+ const carried = carriedReferences(context, resource, node);
336
+ if (carried === undefined) {
337
+ return kind === "existing"
338
+ ? askByIdentifier(context, resource, node)
339
+ : "unknown";
340
+ }
341
+ if (carried.values.some((value) => value === null)) {
342
+ return false;
343
+ }
344
+ const where = Object.fromEntries(carried.references.map((field, index) => [
345
+ field,
346
+ { equals: whereValue(carried.values[index]) },
347
+ ]));
348
+ return ask(context, node.target, combine(context.policies, node.target, where, node.where));
349
+ }
350
+ default:
351
+ return kind === "existing"
352
+ ? askByIdentifier(context, resource, node)
353
+ : "unknown";
354
+ }
355
+ };
356
+ /**
357
+ * Decides a folded condition on a row: in memory first, then through `exists` for
358
+ * what the row does not carry. `"unknown"` when neither can decide.
359
+ */
360
+ export const decideOnRow = async (args) => {
361
+ const residual = evaluateOnRow(args.policies, args.resource, args.condition, args.row);
362
+ if (residual.k === "true") {
363
+ return true;
364
+ }
365
+ if (residual.k === "false") {
366
+ return false;
367
+ }
368
+ if (residual.k === "unknown") {
369
+ return "unknown";
370
+ }
371
+ const context = {
372
+ policies: args.policies,
373
+ row: args.row,
374
+ exists: args.exists,
375
+ lookups: args.lookups,
376
+ };
377
+ return askOn(context, args.resource, residual, args.kind);
378
+ };
@@ -0,0 +1,2 @@
1
+ /** `prefix` and 256 random bits from the runtime's CSPRNG, in base64url. */
2
+ export declare const randomSecret: (prefix: string) => string;
@@ -0,0 +1,15 @@
1
+ // The secrets Access issues (registry keys, kiosk device secrets, beacons): a
2
+ // prefix that names what the secret is, then 256 random bits from the runtime's
3
+ // CSPRNG in base64url (43 characters, no padding).
4
+ const base64url = (bytes) => {
5
+ let binary = "";
6
+ for (const byte of bytes) {
7
+ binary += String.fromCharCode(byte);
8
+ }
9
+ return btoa(binary)
10
+ .replace(/\+/g, "-")
11
+ .replace(/\//g, "_")
12
+ .replace(/=+$/, "");
13
+ };
14
+ /** `prefix` and 256 random bits from the runtime's CSPRNG, in base64url. */
15
+ export const randomSecret = (prefix) => `${prefix}${base64url(crypto.getRandomValues(new Uint8Array(32)))}`;
@@ -0,0 +1,3 @@
1
+ import type { AccessPolicies } from "./policies-contract.js";
2
+ /** Warns once per field governed `"read"` that is part of one of its Resource's identifiers. */
3
+ export declare const warnReadGovernedUnique: (policies: AccessPolicies) => void;
@@ -0,0 +1,21 @@
1
+ // The warning ACCESS_READ_GOVERNED_UNIQUE: a field governed for reading only
2
+ // (`"read"`) that is one of its Resource's identifiers. Anyone who may write the row
3
+ // writes the field, and a value another row holds fails on the duplicate: the
4
+ // refusal tells the writer that value exists, which reading it would not. The
5
+ // warning changes nothing the engine decides.
6
+ import { emitAccessWarning } from "./access-warning.emitter.js";
7
+ import { ownOf } from "./scope.resolver.js";
8
+ const WARNING_CODE = "ACCESS_READ_GOVERNED_UNIQUE";
9
+ /** Warns once per field governed `"read"` that is part of one of its Resource's identifiers. */
10
+ export const warnReadGovernedUnique = (policies) => {
11
+ for (const [resource, modes] of Object.entries(policies.governModes ?? {})) {
12
+ const identifiers = ownOf(policies.schema, resource)?.identifiers ?? [];
13
+ for (const [field, mode] of Object.entries(modes)) {
14
+ if (mode !== "read" ||
15
+ !identifiers.some((identifier) => identifier.includes(field))) {
16
+ continue;
17
+ }
18
+ emitAccessWarning(WARNING_CODE, policies.namespace, `${resource}.${field} is governed for reading only ("read") and is unique (one of ${resource}'s identifiers): a create or update that sends a value another row holds fails on the duplicate, which tells the writer that value exists. Govern it as "both", so only a grant that names it writes it, or accept that writers learn which values exist.`);
19
+ }
20
+ }
21
+ };
@@ -0,0 +1,16 @@
1
+ import type { AccessPolicies } from "./policies-contract.js";
2
+ /**
3
+ * Better Auth's tables `createAccess` keeps closed and a `resources` key would open,
4
+ * by Resource: no grant on one is taken until that key opens it, so a warning about
5
+ * one names the key.
6
+ */
7
+ export type ClosedTables = ReadonlyMap<string, {
8
+ readonly model: string;
9
+ readonly key: string;
10
+ }>;
11
+ /**
12
+ * Emits `policies`' warnings under a framework that holds references to the
13
+ * Resources in `held`, each once per process and namespace; a target in `closed`
14
+ * is named with the `resources` key that opens it.
15
+ */
16
+ export declare const warnReferenceReads: (policies: AccessPolicies, held: ReadonlySet<string>, closed?: ClosedTables) => void;
@@ -0,0 +1,158 @@
1
+ // A check of the rules against the framework that hosts them: a role that may
2
+ // create or update a row holding a reference (a to-one relation's reference fields)
3
+ // to a Resource no read grant reaches for that role. Aventara holds a reference a
4
+ // write sends to the rows the caller may read, so such a write is refused at run
5
+ // time (A4002) whatever the write grant says. It holds only references to the
6
+ // Resources its Core Contract shows, and the app's root exemptions decide those,
7
+ // which the rules cannot see. So the check runs when the framework hosts the
8
+ // engine: core calls `engine.hosted({ held })` once it is built, `held` being the
9
+ // Resources it holds references to, and only those are warned about. Compiling the
10
+ // rules (`.contract()`, `avaccess push`) and building their engine warn about
11
+ // nothing, and a service that does not run on Aventara (the generated client, the
12
+ // standalone host) holds no reference and is never told.
13
+ //
14
+ // It warns and never refuses: the engine answers exactly as before. The warning is
15
+ // Node.js's (`process.emitWarning`, code ACCESS_REFERENCE_UNREADABLE, silenced
16
+ // with `--disable-warning=ACCESS_REFERENCE_UNREADABLE`), else `console.warn`;
17
+ // each one is emitted once per process and namespace.
18
+ //
19
+ // "Reaches" errs towards silence, since the rules cannot say which roles one
20
+ // caller holds together. A read grant on the target (or on `*`) reaches a role when
21
+ // it names that role, `anyone`, or `signedIn` (for any role but `anyone`). A grant
22
+ // that names the target itself also reaches a `signedIn` writer whatever role it
23
+ // names (the caller may hold it), and reaches a role held on rows of one Resource
24
+ // when it names another role held on that same Resource. A held target the
25
+ // rules' schema does not have is reached by no grant (`*` does not reach it, and a
26
+ // grant cannot name it), so a write to it is warned about for every writer. Not
27
+ // warned about: a reference the grant's `write.except` locks (its relation or a
28
+ // reference field; the write is refused for the field itself), a target the
29
+ // framework does not hold, a target the rules read as foreign (the engine lists it
30
+ // in `unheld`, so the framework holds no reference to it; the write grant decides
31
+ // which row may be linked, foreign-references.check.ts), and a writing Resource it
32
+ // does not serve (`held` is its Core Contract's Resources, and no write reaches one
33
+ // outside it).
34
+ //
35
+ // Under `createAccess` (or its plugin) a target may be one of Better Auth's tables
36
+ // that stays closed until a `resources` key opens it (`User` until
37
+ // `resources: { users: true }`): no grant on it is taken before that, and the
38
+ // built-in deny refuses a `*` read grant there too, so no read grant reaches it
39
+ // and the warning names the key.
40
+ import { emitAccessWarning, listed } from "./access-warning.emitter.js";
41
+ import { ownOf } from "./scope.resolver.js";
42
+ const WARNING_CODE = "ACCESS_REFERENCE_UNREADABLE";
43
+ /** The Resource a row role is held on, or `undefined` for `signedIn`, `anyone` and a global role. */
44
+ const heldOn = (policies, role) => {
45
+ const definition = ownOf(policies.roles, role);
46
+ return definition !== undefined && "on" in definition
47
+ ? definition.on
48
+ : undefined;
49
+ };
50
+ const reaches = (policies, read, writer, target) => read.subjects.some((subject) => {
51
+ if (subject === writer ||
52
+ subject === "anyone" ||
53
+ (subject === "signedIn" && writer !== "anyone")) {
54
+ return true;
55
+ }
56
+ if (read.resource !== target) {
57
+ return false;
58
+ }
59
+ if (writer === "signedIn") {
60
+ return true;
61
+ }
62
+ const on = heldOn(policies, writer);
63
+ return on !== undefined && heldOn(policies, subject) === on;
64
+ });
65
+ /**
66
+ * The warnings for `policies` hosted by a framework that holds references to the
67
+ * Resources in `held`, one per writing role and target Resource, each naming every
68
+ * reference that role may write to it.
69
+ */
70
+ const referenceReadWarnings = (policies, held, closed) => {
71
+ const reads = policies.grants.filter((grant) => grant.actions.includes("read"));
72
+ const found = new Map();
73
+ for (const grant of policies.grants) {
74
+ if (!grant.actions.some((a) => a === "create" || a === "update")) {
75
+ continue;
76
+ }
77
+ const locked = new Set(grant.write?.except ?? []);
78
+ const resources = grant.resource === "*" ? Object.keys(policies.schema) : [grant.resource];
79
+ for (const resource of resources) {
80
+ const facts = ownOf(policies.schema, resource);
81
+ // A Resource the framework does not serve is never written through it
82
+ // (its Core Contract has no such Resource, nor a relation to it).
83
+ if (facts === undefined ||
84
+ facts.foreign !== undefined ||
85
+ !held.has(resource)) {
86
+ continue;
87
+ }
88
+ for (const [relation, fact] of Object.entries(facts.relations)) {
89
+ const fields = fact.referenceFields;
90
+ const target = ownOf(policies.schema, fact.target);
91
+ if (fact.cardinality !== "one" ||
92
+ fields === undefined ||
93
+ locked.has(relation) ||
94
+ fields.some((field) => locked.has(field))) {
95
+ continue;
96
+ }
97
+ // A foreign target is never held: the engine lists it in `unheld`.
98
+ if (!held.has(fact.target) || target?.foreign !== undefined) {
99
+ continue;
100
+ }
101
+ // A target the schema does not have takes no grant: `*` does not
102
+ // reach it (`rulesFor`), and a grant cannot name it.
103
+ // A Better Auth table createAccess keeps closed takes no read either: the
104
+ // built-in deny refuses every action on it, `*` grants included.
105
+ const own = target !== undefined;
106
+ const readsOfTarget = own && closed?.has(fact.target) !== true
107
+ ? reads.filter((read) => read.resource === fact.target || read.resource === "*")
108
+ : [];
109
+ for (const writer of grant.subjects) {
110
+ if (readsOfTarget.some((read) => reaches(policies, read, writer, fact.target))) {
111
+ continue;
112
+ }
113
+ const key = `${writer}\u0000${fact.target}`;
114
+ const entry = found.get(key) ?? {
115
+ writer,
116
+ target: fact.target,
117
+ own,
118
+ references: [],
119
+ };
120
+ for (const field of fields) {
121
+ const name = `${resource}.${field}`;
122
+ if (!entry.references.includes(name)) {
123
+ entry.references.push(name);
124
+ }
125
+ }
126
+ found.set(key, entry);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ return [...found.values()].map(({ writer, target, own, references }) => {
132
+ const one = references.length === 1;
133
+ const what = `${writer} may write ${listed(references)}, ${one ? "a reference" : "references"} to Resource "${target}"`;
134
+ const hold = "Aventara holds a reference a write sends to the rows the caller may read, so these writes are refused (A4002).";
135
+ const out = "the reference out of the write (`write: { except: [...] }`).";
136
+ if (!own) {
137
+ return `${what}, which the rules' schema does not have: no read grant in these rules reaches it, and none can. ${hold} Leave ${out}`;
138
+ }
139
+ const to = writer === "signedIn" || writer === "anyone"
140
+ ? writer
141
+ : `${writer} (or to signedIn)`;
142
+ const table = closed?.get(target);
143
+ const opening = table === undefined
144
+ ? ""
145
+ : ` (Better Auth's \`${table.model}\` table, closed until \`resources: { ${table.key}: true }\` in createAccess opens it)`;
146
+ return `${what}, but no read grant on "${target}" reaches ${writer}. ${hold} Grant read on "${target}" to ${to}${opening}, or leave ${out}`;
147
+ });
148
+ };
149
+ /**
150
+ * Emits `policies`' warnings under a framework that holds references to the
151
+ * Resources in `held`, each once per process and namespace; a target in `closed`
152
+ * is named with the `resources` key that opens it.
153
+ */
154
+ export const warnReferenceReads = (policies, held, closed) => {
155
+ for (const message of referenceReadWarnings(policies, held, closed)) {
156
+ emitAccessWarning(WARNING_CODE, policies.namespace, message);
157
+ }
158
+ };