@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,9 @@
1
+ import type { AccessSchema } from "../../access-schema.js";
2
+ /** What the reader made of a schema: the `AccessSchema`, and what it left out and why. */
3
+ type PrismaSchemaReading = {
4
+ readonly schema: AccessSchema;
5
+ readonly skipped: readonly string[];
6
+ };
7
+ /** Reads the Prisma schema at `path` (a `.prisma` file or a folder of them) as an `AccessSchema`. */
8
+ export declare const readPrismaSchema: (path: string) => Promise<PrismaSchemaReading>;
9
+ export {};
@@ -0,0 +1,394 @@
1
+ // A Prisma 7 schema (`schema.prisma`, or a folder of `.prisma` files) read as the
2
+ // `AccessSchema` a service authors its rules against with `definePolicies`. It reads
3
+ // what the Aventara framework's Prisma 7 discovery reads for the same facts, so an
4
+ // Aventara app's discovered model and this reader agree on every Resource: each
5
+ // model's scalar and relation fields, enums, the reference pairs a relation holds,
6
+ // its inverse, and the field tuples that identify a row.
7
+ //
8
+ // It is a bounded reader, not a parser: Prisma's schema language puts each field and
9
+ // each block attribute on one line of its own (a declaration split over lines is a
10
+ // Prisma validation error), so it reads line by line. It locates tokens and never
11
+ // evaluates them: `@default(...)`, `@map`, `@db.*`, `@@index` and `@@schema` are
12
+ // read past. What it cannot model (an `Unsupported(...)` column, a composite `type`,
13
+ // a `view`, an `@ignore`d field or model) is left out and reported, never guessed.
14
+ import { readdir, readFile } from "node:fs/promises";
15
+ import { resolve } from "node:path";
16
+ import { BEACON_TABLE } from "../../beacon/beacon.store.js";
17
+ /** Prisma's built-in scalars, as Access (and the framework) name them. */
18
+ const SCALARS = {
19
+ String: "string",
20
+ Boolean: "boolean",
21
+ Int: "int",
22
+ Float: "float",
23
+ BigInt: "bigint",
24
+ Decimal: "decimal",
25
+ DateTime: "datetime",
26
+ Bytes: "bytes",
27
+ Json: "json",
28
+ };
29
+ /** A line without its comment: `//` outside a string literal ends the line. */
30
+ const withoutComment = (line) => {
31
+ let quoted = false;
32
+ for (let index = 0; index < line.length; index += 1) {
33
+ const char = line[index];
34
+ if (char === "\\" && quoted) {
35
+ index += 1;
36
+ }
37
+ else if (char === '"') {
38
+ quoted = !quoted;
39
+ }
40
+ else if (!quoted && char === "/" && line[index + 1] === "/") {
41
+ return line.slice(0, index);
42
+ }
43
+ }
44
+ return line;
45
+ };
46
+ /** The `@name(args)` and `@@name(args)` attributes of a line, in order, with their raw arguments. */
47
+ const attributesOf = (text) => {
48
+ const attributes = [];
49
+ let index = 0;
50
+ while (index < text.length) {
51
+ if (text[index] !== "@") {
52
+ index += 1;
53
+ continue;
54
+ }
55
+ const start = index;
56
+ while (text[index] === "@") {
57
+ index += 1;
58
+ }
59
+ const nameMatch = /^[A-Za-z_][\w.]*/.exec(text.slice(index));
60
+ const name = `${text.slice(start, index)}${nameMatch?.[0] ?? ""}`;
61
+ index += nameMatch?.[0].length ?? 0;
62
+ let args;
63
+ if (text[index] === "(") {
64
+ let depth = 0;
65
+ let quoted = false;
66
+ const open = index;
67
+ for (; index < text.length; index += 1) {
68
+ const char = text[index];
69
+ if (quoted) {
70
+ if (char === "\\") {
71
+ index += 1;
72
+ }
73
+ else if (char === '"') {
74
+ quoted = false;
75
+ }
76
+ }
77
+ else if (char === '"') {
78
+ quoted = true;
79
+ }
80
+ else if (char === "(") {
81
+ depth += 1;
82
+ }
83
+ else if (char === ")") {
84
+ depth -= 1;
85
+ if (depth === 0) {
86
+ index += 1;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ args = text.slice(open + 1, index - 1);
92
+ }
93
+ attributes.push({ name, args });
94
+ }
95
+ return attributes;
96
+ };
97
+ /** An attribute's arguments split at the commas outside brackets, parentheses and strings. */
98
+ const argumentsOf = (args) => {
99
+ const parts = [];
100
+ let depth = 0;
101
+ let quoted = false;
102
+ let current = "";
103
+ for (let index = 0; index < args.length; index += 1) {
104
+ const char = args[index] ?? "";
105
+ if (quoted) {
106
+ current += char;
107
+ if (char === "\\") {
108
+ current += args[index + 1] ?? "";
109
+ index += 1;
110
+ }
111
+ else if (char === '"') {
112
+ quoted = false;
113
+ }
114
+ continue;
115
+ }
116
+ if (char === '"') {
117
+ quoted = true;
118
+ }
119
+ else if (char === "(" || char === "[") {
120
+ depth += 1;
121
+ }
122
+ else if (char === ")" || char === "]") {
123
+ depth -= 1;
124
+ }
125
+ else if (char === "," && depth === 0) {
126
+ parts.push(current.trim());
127
+ current = "";
128
+ continue;
129
+ }
130
+ current += char;
131
+ }
132
+ if (current.trim() !== "") {
133
+ parts.push(current.trim());
134
+ }
135
+ return parts;
136
+ };
137
+ /** The named argument `name:` of an attribute, or its first positional one when `name` is `undefined`. */
138
+ const argumentOf = (args, name) => {
139
+ for (const part of argumentsOf(args ?? "")) {
140
+ const named = /^([A-Za-z_]\w*)\s*:\s*([\s\S]*)$/.exec(part);
141
+ if (name === undefined ? named === null : named?.[1] === name) {
142
+ return name === undefined ? part : named?.[2]?.trim();
143
+ }
144
+ }
145
+ return undefined;
146
+ };
147
+ /** The field names of a `[a, b(sort: Desc)]` list. */
148
+ const fieldListOf = (list) => {
149
+ const inner = /^\[([\s\S]*)\]$/.exec(list?.trim() ?? "")?.[1];
150
+ if (inner === undefined) {
151
+ return undefined;
152
+ }
153
+ return argumentsOf(inner)
154
+ .map((member) => /^[A-Za-z_]\w*/.exec(member)?.[0] ?? "")
155
+ .filter((member) => member !== "");
156
+ };
157
+ /** A string literal's value: `"Mentorship"` → `Mentorship`. */
158
+ const stringOf = (literal) => /^"((?:[^"\\]|\\.)*)"$/.exec(literal?.trim() ?? "")?.[1];
159
+ const FIELD = /^([A-Za-z_]\w*)\s+([A-Za-z_]\w*(?:\([^)]*\))?)(\[\])?(\?)?(.*)$/;
160
+ /** The `.prisma` files at `path`: the file itself, or every one under the folder, by path. */
161
+ const schemaFiles = async (path) => {
162
+ if (path.endsWith(".prisma")) {
163
+ return [path];
164
+ }
165
+ const files = [];
166
+ const walk = async (folder) => {
167
+ const entries = await readdir(folder, { withFileTypes: true });
168
+ for (const entry of entries) {
169
+ const full = resolve(folder, entry.name);
170
+ if (entry.isDirectory()) {
171
+ await walk(full);
172
+ }
173
+ else if (entry.isFile() && entry.name.endsWith(".prisma")) {
174
+ files.push(full);
175
+ }
176
+ }
177
+ };
178
+ await walk(path);
179
+ return files.sort();
180
+ };
181
+ /** Reads the schema's blocks: models with their lines, enums with their values. */
182
+ const blocksOf = (sources, skipped) => {
183
+ const models = [];
184
+ const enums = new Map();
185
+ const composites = new Set();
186
+ for (const { file, text } of sources) {
187
+ let block;
188
+ for (const raw of text.split(/\r?\n/)) {
189
+ const line = withoutComment(raw).trim();
190
+ if (line === "") {
191
+ continue;
192
+ }
193
+ if (block === undefined) {
194
+ const opened = /^(\w+)\s+([A-Za-z_]\w*)\s*\{$/.exec(line);
195
+ if (opened !== null) {
196
+ block = { kind: opened[1] ?? "", name: opened[2] ?? "", lines: [] };
197
+ }
198
+ continue;
199
+ }
200
+ if (line === "}") {
201
+ if (block.kind === "model") {
202
+ const fields = [];
203
+ const blockAttributes = [];
204
+ for (const member of block.lines) {
205
+ if (member.startsWith("@@")) {
206
+ blockAttributes.push(...attributesOf(member));
207
+ continue;
208
+ }
209
+ const field = FIELD.exec(member);
210
+ if (field === null) {
211
+ continue;
212
+ }
213
+ fields.push({
214
+ name: field[1] ?? "",
215
+ type: field[2] ?? "",
216
+ list: field[3] !== undefined,
217
+ optional: field[4] !== undefined,
218
+ attributes: attributesOf(field[5] ?? ""),
219
+ });
220
+ }
221
+ models.push({ name: block.name, file, fields, blockAttributes });
222
+ }
223
+ else if (block.kind === "enum") {
224
+ enums.set(block.name, block.lines
225
+ .filter((member) => !member.startsWith("@@"))
226
+ .map((member) => /^[A-Za-z_]\w*/.exec(member)?.[0] ?? "")
227
+ .filter((member) => member !== ""));
228
+ }
229
+ else if (block.kind === "type") {
230
+ composites.add(block.name);
231
+ }
232
+ else if (block.kind === "view") {
233
+ skipped.push(`view ${block.name}: a view is not a Resource Access reads; left out`);
234
+ }
235
+ block = undefined;
236
+ continue;
237
+ }
238
+ block.lines.push(line);
239
+ }
240
+ }
241
+ return { models, enums, composites };
242
+ };
243
+ const has = (attributes, name) => attributes.some((attribute) => attribute.name === name);
244
+ /** The relation's name, from `@relation("Name")` or `@relation(name: "Name")`. */
245
+ const relationNameOf = (field) => {
246
+ const relation = field.attributes.find((attribute) => attribute.name === "@relation");
247
+ if (relation === undefined) {
248
+ return null;
249
+ }
250
+ return (stringOf(argumentOf(relation.args, "name")) ??
251
+ stringOf(argumentOf(relation.args, undefined)) ??
252
+ null);
253
+ };
254
+ /** Whether a model is the ownership beacon's table: named, or mapped to, `_aventara_beacon`. */
255
+ const isBeaconTable = (model) => BEACON_NAMES.includes(model.name) ||
256
+ model.blockAttributes.some((attribute) => attribute.name === "@@map" &&
257
+ BEACON_NAMES.includes(stringOf(argumentOf(attribute.args, undefined) ??
258
+ argumentOf(attribute.args, "name")) ?? ""));
259
+ /** The beacon table's names: a service's `_aventara_beacon`, the Access server's `aventaraBeacon` (Prisma model `AventaraBeacon`). */
260
+ const BEACON_NAMES = [
261
+ BEACON_TABLE,
262
+ "aventaraBeacon",
263
+ "AventaraBeacon",
264
+ ];
265
+ /** Reads the Prisma schema at `path` (a `.prisma` file or a folder of them) as an `AccessSchema`. */
266
+ export const readPrismaSchema = async (path) => {
267
+ const sources = await Promise.all((await schemaFiles(path)).map(async (file) => ({
268
+ file,
269
+ text: await readFile(file, "utf8"),
270
+ })));
271
+ const skipped = [];
272
+ const { models: declared, enums, composites } = blocksOf(sources, skipped);
273
+ const models = declared.filter((model) => {
274
+ if (has(model.blockAttributes, "@@ignore")) {
275
+ skipped.push(`model ${model.name}: @@ignore; left out`);
276
+ return false;
277
+ }
278
+ if (isBeaconTable(model)) {
279
+ skipped.push(`model ${model.name}: the ownership beacon's table, which no rule reads; left out`);
280
+ return false;
281
+ }
282
+ return true;
283
+ });
284
+ const byName = new Map(models.map((model) => [model.name, model]));
285
+ const ignoredModels = new Set(declared.filter((model) => !byName.has(model.name)).map((m) => m.name));
286
+ const resources = {};
287
+ for (const model of models) {
288
+ const fields = {};
289
+ const columns = new Map();
290
+ for (const field of model.fields) {
291
+ const where = `${model.name}.${field.name}`;
292
+ if (has(field.attributes, "@ignore")) {
293
+ skipped.push(`${where}: @ignore; left out`);
294
+ continue;
295
+ }
296
+ const scalar = SCALARS[field.type];
297
+ if (scalar !== undefined || enums.has(field.type)) {
298
+ fields[field.name] = {
299
+ kind: "scalar",
300
+ type: scalar !== undefined ? { scalar } : { enum: field.type },
301
+ nullable: field.optional,
302
+ list: field.list,
303
+ };
304
+ if (!field.list) {
305
+ columns.set(field.name, field);
306
+ }
307
+ continue;
308
+ }
309
+ const target = byName.get(field.type);
310
+ if (target === undefined) {
311
+ skipped.push(`${where}: ${field.type.startsWith("Unsupported(")
312
+ ? "an Unsupported column"
313
+ : composites.has(field.type)
314
+ ? `the composite type ${field.type}`
315
+ : ignoredModels.has(field.type)
316
+ ? `a relation to the ignored model ${field.type}`
317
+ : `the type ${field.type}, which the schema does not declare`}; left out`);
318
+ continue;
319
+ }
320
+ const name = relationNameOf(field);
321
+ const partners = target.fields.filter((candidate) => candidate.type === model.name &&
322
+ relationNameOf(candidate) === name &&
323
+ !(target.name === model.name && candidate.name === field.name));
324
+ const relation = field.attributes.find((attribute) => attribute.name === "@relation");
325
+ const referenceFields = fieldListOf(argumentOf(relation?.args, "fields"));
326
+ const references = fieldListOf(argumentOf(relation?.args, "references"));
327
+ const cardinality = field.list ? "many" : "one";
328
+ const fact = {
329
+ kind: "relation",
330
+ target: target.name,
331
+ cardinality,
332
+ nullable: cardinality === "one" && field.optional,
333
+ ...(partners.length === 1 && partners[0] !== undefined
334
+ ? { inverse: partners[0].name }
335
+ : {}),
336
+ ...(referenceFields !== undefined &&
337
+ referenceFields.length > 0 &&
338
+ references !== undefined &&
339
+ references.length > 0
340
+ ? {
341
+ referenceFields: referenceFields,
342
+ references: references,
343
+ }
344
+ : {}),
345
+ };
346
+ fields[field.name] = fact;
347
+ }
348
+ // Identifiers, as discovery lists them: each field's `@id` then `@unique`, in
349
+ // field order, then `@@id`, then each `@@unique`; a tuple once; none over a
350
+ // field that is not a column here, and none over a nullable one.
351
+ const criteria = [];
352
+ for (const field of model.fields) {
353
+ if (has(field.attributes, "@id")) {
354
+ criteria.push([field.name]);
355
+ }
356
+ if (has(field.attributes, "@unique")) {
357
+ criteria.push([field.name]);
358
+ }
359
+ }
360
+ for (const name of ["@@id", "@@unique"]) {
361
+ for (const attribute of model.blockAttributes) {
362
+ if (attribute.name !== name) {
363
+ continue;
364
+ }
365
+ const members = fieldListOf(argumentOf(attribute.args, "fields")) ??
366
+ fieldListOf(argumentOf(attribute.args, undefined));
367
+ if (members !== undefined) {
368
+ criteria.push(members);
369
+ }
370
+ }
371
+ }
372
+ const seen = new Set();
373
+ const identifiers = [];
374
+ for (const members of criteria) {
375
+ const key = JSON.stringify(members);
376
+ if (seen.has(key)) {
377
+ continue;
378
+ }
379
+ seen.add(key);
380
+ if (members.length > 0 &&
381
+ members.every((member) => columns.get(member)?.optional === false)) {
382
+ identifiers.push(members);
383
+ }
384
+ }
385
+ resources[model.name] = { fields, identifiers };
386
+ }
387
+ return {
388
+ schema: {
389
+ enums: Object.fromEntries([...enums].map(([name, values]) => [name, { values }])),
390
+ resources,
391
+ },
392
+ skipped,
393
+ };
394
+ };
@@ -0,0 +1,72 @@
1
+ import { type AccessWhere } from "../../aventara-where.translator.js";
2
+ import type { AccessFields } from "../../policies.decider.js";
3
+ import type { AccessPolicies } from "../../policies-contract.js";
4
+ /** A Prisma `where`: plain data any `<Model>WhereInput` accepts. */
5
+ export type PrismaWhere = {
6
+ [key: string]: unknown;
7
+ };
8
+ /** A Prisma `select` of scalar fields. */
9
+ export type PrismaSelect = {
10
+ [field: string]: true;
11
+ };
12
+ /** What `prismaExists` needs of a model's delegate on the Prisma client. */
13
+ export type PrismaFindFirst = {
14
+ findFirst(args: {
15
+ where: PrismaWhere;
16
+ select: PrismaSelect;
17
+ }): PromiseLike<unknown>;
18
+ };
19
+ /** Aventara's `where` on a Resource of `policies`, as Prisma's `where` on its model. */
20
+ export declare const toPrismaWhere: (policies: AccessPolicies, resource: string, where: AccessWhere) => PrismaWhere;
21
+ /**
22
+ * A scope (`scopeOf`'s answer) as Prisma's `where`: every row (`{}`), no row
23
+ * (`{ OR: [] }`, which Prisma matches with nothing), or the translated condition.
24
+ */
25
+ export declare const toPrismaScope: (policies: AccessPolicies, resource: string, scope: boolean | AccessWhere) => PrismaWhere;
26
+ /**
27
+ * A `fieldsOf` read answer as Prisma's `select`: the Resource's scalar fields a read
28
+ * shows on every row. A governed field is selected only when the answer opens it on
29
+ * every row; a field it opens on some rows only is left out, since a `select` cannot
30
+ * depend on the row (ask `fieldsOf` for those). Relations are never selected: a
31
+ * related row would not be scoped.
32
+ */
33
+ export declare const toPrismaSelect: (policies: AccessPolicies, resource: string, fields: AccessFields) => PrismaSelect;
34
+ /** The property of the Prisma client that holds a model's delegate: its name, first letter lowered. */
35
+ export declare const prismaDelegateOf: (resource: string) => string;
36
+ /**
37
+ * Core's `exists`, through the service's Prisma client: whether a row of `resource`
38
+ * meets `where`, unscoped. It selects the row's first identifier and nothing else.
39
+ */
40
+ export declare const existsThroughPrisma: (client: object, policies: AccessPolicies, resource: string, where: AccessWhere) => Promise<boolean>;
41
+ /**
42
+ * The Prisma client the ownership beacon is read and written through: a model over
43
+ * the `_aventara_beacon` table, keyed by the namespace,
44
+ * `model AventaraBeacon { id String @id namespace String @unique value String createdAt DateTime @default(now()) @@map("_aventara_beacon") }`.
45
+ */
46
+ export type PrismaBeaconClient = {
47
+ readonly aventaraBeacon: {
48
+ findUnique(args: {
49
+ where: {
50
+ namespace: string;
51
+ };
52
+ select: {
53
+ value: true;
54
+ };
55
+ }): PromiseLike<unknown>;
56
+ create(args: {
57
+ data: {
58
+ namespace: string;
59
+ value: string;
60
+ };
61
+ }): PromiseLike<unknown>;
62
+ };
63
+ };
64
+ /**
65
+ * The beacon store over the service's Prisma client: `read` a namespace's row in
66
+ * `_aventara_beacon`, `create` one. What `avaccess push --beacon` and `pull --beacon`
67
+ * load, and what the client's `checkBeacons` reads through.
68
+ */
69
+ export declare const beaconThroughPrisma: (client: PrismaBeaconClient) => {
70
+ read(namespace: string): Promise<string | null>;
71
+ create(namespace: string, value: string): Promise<void>;
72
+ };
@@ -0,0 +1,136 @@
1
+ // The Prisma 7 translator: the answers the decisions give, in Prisma's words. A scope
2
+ // (`scopeOf`'s `where`, the shape the engine hands Aventara) becomes a Prisma
3
+ // `where`, a `fieldsOf` read answer becomes a Prisma `select`, and an `exists`
4
+ // question is asked through the service's own Prisma client. `avaccess generate
5
+ // --orm prisma7` copies this module into the generated client beside the decisions;
6
+ // like them, it imports nothing but them, and no Prisma module: what it builds is
7
+ // plain data that Prisma's own input types accept.
8
+ //
9
+ // Each step mirrors how the framework's Prisma 7 adapter writes Aventara's `where`:
10
+ // boolean nodes (`AND`, `OR`, `NOT`) and the scalar operators (`equals`, `not`,
11
+ // `in`, `notIn`) keep their spelling; a to-one relation's condition is wrapped in
12
+ // Prisma's `is`, because Prisma reads an empty unwrapped one as no condition at all;
13
+ // a to-many relation's `some`, `every` and `none` keep theirs.
14
+ import { defineMember, } from "../../aventara-where.translator.js";
15
+ const isWhereNode = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16
+ /** A record's own member, never one it inherits. */
17
+ const own = (record, key) => record !== undefined && Object.hasOwn(record, key) ? record[key] : undefined;
18
+ /**
19
+ * A scalar field's operators, with each `in` list copied (Prisma's lists are
20
+ * mutable). A bigint past a safe integer, which the file writes as its digits, is
21
+ * handed to Prisma as a `bigint`.
22
+ */
23
+ const operatorsOf = (value, bigint) => {
24
+ const operandOf = (operand) => bigint && typeof operand === "string" ? BigInt(operand) : operand;
25
+ return isWhereNode(value)
26
+ ? Object.fromEntries(Object.entries(value).map(([operator, operand]) => [
27
+ operator,
28
+ Array.isArray(operand) ? operand.map(operandOf) : operandOf(operand),
29
+ ]))
30
+ : value;
31
+ };
32
+ /** Aventara's `where` on a Resource of `policies`, as Prisma's `where` on its model. */
33
+ export const toPrismaWhere = (policies, resource, where) => {
34
+ const facts = own(policies.schema, resource);
35
+ const relations = facts?.relations ?? {};
36
+ const translated = {};
37
+ for (const [key, value] of Object.entries(where)) {
38
+ if (key === "AND" || key === "OR") {
39
+ defineMember(translated, key, value.map((node) => toPrismaWhere(policies, resource, node)));
40
+ continue;
41
+ }
42
+ if (key === "NOT") {
43
+ defineMember(translated, key, toPrismaWhere(policies, resource, value));
44
+ continue;
45
+ }
46
+ const relation = own(relations, key);
47
+ if (relation === undefined || !isWhereNode(value)) {
48
+ defineMember(translated, key, operatorsOf(value, own(facts?.types, key) === "bigint"));
49
+ continue;
50
+ }
51
+ if (relation.cardinality === "one") {
52
+ defineMember(translated, key, {
53
+ is: toPrismaWhere(policies, relation.target, value),
54
+ });
55
+ continue;
56
+ }
57
+ defineMember(translated, key, Object.fromEntries(Object.entries(value).map(([quantifier, node]) => [
58
+ quantifier,
59
+ toPrismaWhere(policies, relation.target, node),
60
+ ])));
61
+ }
62
+ return translated;
63
+ };
64
+ /**
65
+ * A scope (`scopeOf`'s answer) as Prisma's `where`: every row (`{}`), no row
66
+ * (`{ OR: [] }`, which Prisma matches with nothing), or the translated condition.
67
+ */
68
+ export const toPrismaScope = (policies, resource, scope) => scope === true
69
+ ? {}
70
+ : scope === false
71
+ ? { OR: [] }
72
+ : toPrismaWhere(policies, resource, scope);
73
+ /**
74
+ * A `fieldsOf` read answer as Prisma's `select`: the Resource's scalar fields a read
75
+ * shows on every row. A governed field is selected only when the answer opens it on
76
+ * every row; a field it opens on some rows only is left out, since a `select` cannot
77
+ * depend on the row (ask `fieldsOf` for those). Relations are never selected: a
78
+ * related row would not be scoped.
79
+ */
80
+ export const toPrismaSelect = (policies, resource, fields) => {
81
+ const facts = own(policies.schema, resource);
82
+ const governed = own(policies.govern, resource) ?? [];
83
+ const scalars = (facts?.fields ?? []).filter((field) => own(facts?.relations, field) === undefined);
84
+ const select = {};
85
+ for (const field of scalars) {
86
+ const opened = own(fields.perRow, field);
87
+ const everyRow = opened !== undefined && Object.keys(opened).length === 0;
88
+ const always = !governed.includes(field) &&
89
+ (fields.always === "all" || fields.always.includes(field));
90
+ if (always || everyRow) {
91
+ select[field] = true;
92
+ }
93
+ }
94
+ return select;
95
+ };
96
+ /** The property of the Prisma client that holds a model's delegate: its name, first letter lowered. */
97
+ export const prismaDelegateOf = (resource) => `${resource.charAt(0).toLowerCase()}${resource.slice(1)}`;
98
+ /**
99
+ * Core's `exists`, through the service's Prisma client: whether a row of `resource`
100
+ * meets `where`, unscoped. It selects the row's first identifier and nothing else.
101
+ */
102
+ export const existsThroughPrisma = async (client, policies, resource, where) => {
103
+ const delegateName = prismaDelegateOf(resource);
104
+ const delegate = client[delegateName];
105
+ if (delegate === undefined || typeof delegate.findFirst !== "function") {
106
+ throw new Error(`Aventara Access: the rules ask whether a ${resource} row exists, and the Prisma client has no \`${delegateName}\`: add the model ${resource} to the Prisma schema this service queries`);
107
+ }
108
+ const facts = own(policies.schema, resource);
109
+ const identifier = facts?.identifiers[0] ?? facts?.fields.slice(0, 1) ?? [];
110
+ const select = {};
111
+ for (const field of identifier) {
112
+ select[field] = true;
113
+ }
114
+ const found = await delegate.findFirst({
115
+ where: toPrismaWhere(policies, resource, where),
116
+ select,
117
+ });
118
+ return found !== null && found !== undefined;
119
+ };
120
+ /**
121
+ * The beacon store over the service's Prisma client: `read` a namespace's row in
122
+ * `_aventara_beacon`, `create` one. What `avaccess push --beacon` and `pull --beacon`
123
+ * load, and what the client's `checkBeacons` reads through.
124
+ */
125
+ export const beaconThroughPrisma = (client) => ({
126
+ read: async (namespace) => {
127
+ const row = (await client.aventaraBeacon.findUnique({
128
+ where: { namespace },
129
+ select: { value: true },
130
+ }));
131
+ return typeof row?.value === "string" ? row.value : null;
132
+ },
133
+ create: async (namespace, value) => {
134
+ await client.aventaraBeacon.create({ data: { namespace, value } });
135
+ },
136
+ });
@@ -0,0 +1,2 @@
1
+ import type { OrmTranslator } from "../orm.translator.js";
2
+ export declare const prisma7Translator: OrmTranslator;