@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,106 @@
1
+ import type { AccessFields, AccessFieldsQuestion } from "../policies.decider.js";
2
+ import type { AccessDenyRule, AccessPolicies } from "../policies-contract.js";
3
+ import type { ClosedTables } from "../reference-reads.check.js";
4
+ import { type AuthResourceKey, type AuthTable, type AuthTableNaming } from "./auth-tables.catalog.js";
5
+ /**
6
+ * The `resources` block: which of Better Auth's tables are Resources of the app's
7
+ * rules. Every other table of Better Auth's stays closed.
8
+ */
9
+ export type AccessResourcesOptions = {
10
+ /**
11
+ * Better Auth's `user`, `session` and `account` tables: read as the rules grant,
12
+ * secret columns excepted; through Aventara, a user's `role` is the one field a
13
+ * write may set.
14
+ */
15
+ readonly users?: boolean;
16
+ /**
17
+ * The organization plugin's `member` table (and its `organization`, read only):
18
+ * read as the rules grant; a membership may be created, removed, and its `role`
19
+ * changed. Needs `organization()` in `betterAuth.plugins`.
20
+ */
21
+ readonly members?: boolean;
22
+ /**
23
+ * Which Resource of the rules' schema is each of Better Auth's tables, by Better
24
+ * Auth model name (`{ user: "User", session: "AuthSession", jwks: null, … }`),
25
+ * `null` for a table the schema does not have (not "closed": a table the schema
26
+ * has is named, and closed or opened by its key). Each named Resource has every
27
+ * column of its table, by Better Auth's `fieldName`. Required, naming every table
28
+ * Better Auth is built with (its plugins' and Access's own included), when Better
29
+ * Auth does not run on its Prisma adapter and the rules have a schema: only the
30
+ * Prisma adapter ties a table to a model by name. Refused with the Prisma adapter.
31
+ */
32
+ readonly authTables?: {
33
+ readonly [model: string]: string | null;
34
+ };
35
+ };
36
+ /** The `resources` block, checked. */
37
+ export type ResourcesChoice = {
38
+ readonly [K in AuthResourceKey]: boolean;
39
+ } & {
40
+ readonly authTables: {
41
+ readonly [model: string]: string | null;
42
+ } | undefined;
43
+ };
44
+ /** The fields a write may set on one opened table, by action. */
45
+ type WritableSets = {
46
+ readonly create: ReadonlySet<string>;
47
+ readonly update: ReadonlySet<string>;
48
+ };
49
+ /** The built-in rules over Better Auth's tables, as the engine runs them. */
50
+ export type AuthTablesGuard = {
51
+ /** Denies added to the rules': closed tables, and the writes an opened one never takes. */
52
+ readonly denies: readonly AccessDenyRule[];
53
+ /** The secret columns, by Resource: never in an answer of `fieldsOf`. */
54
+ readonly sealed: ReadonlyMap<string, ReadonlySet<string>>;
55
+ /** The fields a write may set, by opened Resource. */
56
+ readonly writable: ReadonlyMap<string, WritableSets>;
57
+ /** The closed tables a `resources` key opens, by Resource: Better Auth's model name and that key. */
58
+ readonly closed: ClosedTables;
59
+ /** The fields governed for writing, by opened Resource: a grant that names one sets it. */
60
+ readonly governed: ReadonlyMap<string, readonly string[]>;
61
+ };
62
+ /** The `resources` block, checked: `users` and `members` `true` or `false`, `authTables` a map of names or `null`. */
63
+ export declare const checkResourcesOptions: (value: unknown) => ResourcesChoice;
64
+ /**
65
+ * How the rules' schema names Better Auth's tables, checked when the instance is
66
+ * built: with Better Auth on its Prisma adapter, by the adapter's own delegates
67
+ * (and no `authTables`); else by `authTables`, which must name every table
68
+ * (`models`) and nothing else. A rules' schema with no Resource needs neither.
69
+ */
70
+ export declare const authTableNaming: (input: {
71
+ readonly prisma: {
72
+ readonly usePlural: boolean;
73
+ } | undefined;
74
+ readonly authTables: ResourcesChoice["authTables"];
75
+ readonly models: readonly string[];
76
+ readonly schemaEmpty: boolean;
77
+ }) => AuthTableNaming;
78
+ /**
79
+ * The built-in rules for one set of rules: `resources` as the app gave it, Better
80
+ * Auth's tables as it is built (`authTablesOf`), and the rules' file. Throws
81
+ * `ACCESS_RESOURCE_MISSING` when a key's tables are missing, and
82
+ * `ACCESS_OPTIONS_INVALID` for a grant the built-in rules would leave without effect.
83
+ */
84
+ export declare const guardAuthTables: (input: {
85
+ readonly resources: { readonly [K in AuthResourceKey]: boolean; };
86
+ readonly tables: ReadonlyMap<string, AuthTable>;
87
+ readonly naming: AuthTableNaming;
88
+ readonly policies: AccessPolicies;
89
+ }) => AuthTablesGuard;
90
+ /**
91
+ * The rules' file with the built-in rules in it: their denies beside the file's, and
92
+ * each field they govern for writing governed so (a field the rules govern for
93
+ * reading only is then governed for both; one they govern for writing or both stays).
94
+ */
95
+ export declare const withBuiltInRules: (policies: AccessPolicies, guard: AuthTablesGuard) => AccessPolicies;
96
+ /**
97
+ * `fieldsOf`'s answer with the built-in rules applied: a secret column is never
98
+ * listed, and a write lists only the fields its action may set on an opened table.
99
+ * `"all"` becomes the list it stands for (every field that is not governed) when
100
+ * something is taken out of it.
101
+ */
102
+ export declare const guardedFields: (guard: AuthTablesGuard, question: AccessFieldsQuestion, answer: AccessFields, resource: {
103
+ readonly fields: readonly string[];
104
+ readonly governed: readonly string[];
105
+ }) => AccessFields;
106
+ export {};
@@ -0,0 +1,462 @@
1
+ // The built-in rules Access adds over Better Auth's tables, from the `resources` block
2
+ // and the tables Better Auth is built with. The engine runs them beside the app's
3
+ // rules, and they only narrow:
4
+ //
5
+ // - a table no `resources` key opens is closed: a deny on every action;
6
+ // - an opened table takes the app's grants for reading, and only the writes its
7
+ // key allows (a deny on the other actions; the fields a write may set);
8
+ // - a secret column (a password hash, a session token, a signing key) is never in
9
+ // an answer of `fieldsOf`, whatever the grants say: naming it is refused.
10
+ //
11
+ // At startup (the first read of `access.policy`, which is the framework's
12
+ // construction), a key whose tables are missing (from Better Auth, or from the
13
+ // rules' schema) is refused, and so is a grant that could never apply: one on a
14
+ // closed table, or one naming a write the table never takes.
15
+ import { AccessError } from "../access/access.error.js";
16
+ import { ACTIONS } from "../policies.compiler.js";
17
+ import { ownOf } from "../scope.resolver.js";
18
+ import { KEY_TABLES, OPENED_TABLES, SECRET_COLUMNS, } from "./auth-tables.catalog.js";
19
+ /** The prefix of the built-in deny ids, so `explain` and the audit log tell them from the rules'. */
20
+ const BUILT_IN_ID = "better-auth:";
21
+ const KEYS = ["users", "members"];
22
+ const refuse = (message) => {
23
+ throw new AccessError("ACCESS_OPTIONS_INVALID", message);
24
+ };
25
+ const missing = (message) => {
26
+ throw new AccessError("ACCESS_RESOURCE_MISSING", message);
27
+ };
28
+ /** The `resources` block, checked: `users` and `members` `true` or `false`, `authTables` a map of names or `null`. */
29
+ export const checkResourcesOptions = (value) => {
30
+ if (value === undefined) {
31
+ return { users: false, members: false, authTables: undefined };
32
+ }
33
+ const shape = "`resources` is `{ users?: boolean, members?: boolean, authTables?: { [model]: Resource | null } }`: which of Better Auth's tables are Resources of the rules";
34
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
35
+ return refuse(`${shape}.`);
36
+ }
37
+ const block = value;
38
+ for (const [key, on] of Object.entries(block)) {
39
+ if (key === "authTables") {
40
+ if (typeof on !== "object" || on === null || Array.isArray(on)) {
41
+ refuse(`${shape}; \`resources.authTables\` names each table's Resource, or \`null\`.`);
42
+ }
43
+ for (const [model, resource] of Object.entries(on)) {
44
+ if (resource !== null && typeof resource !== "string") {
45
+ refuse(`${shape}; \`resources.authTables.${model}\` is a Resource's name or \`null\`.`);
46
+ }
47
+ }
48
+ continue;
49
+ }
50
+ if (!KEYS.includes(key)) {
51
+ refuse(`${shape}; it does not take \`${key}\`.`);
52
+ }
53
+ if (typeof on !== "boolean") {
54
+ refuse(`${shape}; \`resources.${key}\` was ${JSON.stringify(on)}.`);
55
+ }
56
+ }
57
+ return {
58
+ users: block.users === true,
59
+ members: block.members === true,
60
+ authTables: block.authTables,
61
+ };
62
+ };
63
+ /**
64
+ * How the rules' schema names Better Auth's tables, checked when the instance is
65
+ * built: with Better Auth on its Prisma adapter, by the adapter's own delegates
66
+ * (and no `authTables`); else by `authTables`, which must name every table
67
+ * (`models`) and nothing else. A rules' schema with no Resource needs neither.
68
+ */
69
+ export const authTableNaming = (input) => {
70
+ const { prisma, authTables, models, schemaEmpty } = input;
71
+ if (prisma !== undefined) {
72
+ if (authTables !== undefined) {
73
+ refuse("`resources.authTables` is for Better Auth on another adapter than Prisma's: on Prisma's, Access finds each table by the Prisma model its adapter reads. Remove it.");
74
+ }
75
+ return { kind: "prisma", usePlural: prisma.usePlural };
76
+ }
77
+ if (schemaEmpty && authTables === undefined) {
78
+ return { kind: "map", map: {} };
79
+ }
80
+ const map = authTables ?? {};
81
+ const unnamed = [];
82
+ for (const model of models) {
83
+ if (!Object.hasOwn(map, model)) {
84
+ unnamed.push(model);
85
+ }
86
+ }
87
+ if (unnamed.length > 0) {
88
+ refuse(`Better Auth does not run on its Prisma adapter here, so nothing ties its tables to the rules' Resources by name (a Prisma schema can call any table anything, with \`@@map\`), and a table Access does not know would be an ordinary Resource, open to every grant, its secret columns included. Name the Resource each table is in \`resources.authTables\`, or \`null\` for a table the rules' schema does not have; it lacks ${unnamed.map((model) => `\`${model}\``).join(", ")}.`);
89
+ }
90
+ for (const model of Object.keys(map)) {
91
+ if (!models.includes(model)) {
92
+ refuse(`\`resources.authTables\` names \`${model}\`, which is no table of Better Auth's as it is built (its tables: ${models.join(", ")}).`);
93
+ }
94
+ }
95
+ return { kind: "map", map };
96
+ };
97
+ /** One Better Auth field's name in the app's schema, when the Resource has it. */
98
+ const fieldOf = (table, facts, field) => {
99
+ const name = table.fields.get(field) ?? field;
100
+ return facts.fields.includes(name) ? name : undefined;
101
+ };
102
+ /**
103
+ * The fields a write may set: Better Auth's fields the catalog lists, the row's
104
+ * identifier for a create, and each relation whose reference fields are all among
105
+ * them (writing the relation writes those fields).
106
+ */
107
+ const writableSet = (table, facts, fields, withIdentifier) => {
108
+ const set = new Set();
109
+ for (const field of fields ?? []) {
110
+ const name = fieldOf(table, facts, field);
111
+ if (name !== undefined) {
112
+ set.add(name);
113
+ }
114
+ }
115
+ if (fields !== undefined && withIdentifier) {
116
+ for (const field of facts.identifiers[0] ?? []) {
117
+ set.add(field);
118
+ }
119
+ }
120
+ for (const [relation, fact] of Object.entries(facts.relations)) {
121
+ if (fact.referenceFields?.every((field) => set.has(field))) {
122
+ set.add(relation);
123
+ }
124
+ }
125
+ return set;
126
+ };
127
+ const actionList = (actions) => actions.length === 1
128
+ ? (actions[0] ?? "")
129
+ : `${actions.slice(0, -1).join(", ")} and ${actions.at(-1)}`;
130
+ /** Better Auth's columns of a table, as the app's schema names them (`fieldName`). */
131
+ const columnsOf = (table) => {
132
+ const columns = [];
133
+ for (const name of table?.fields.values() ?? []) {
134
+ columns.push(name);
135
+ }
136
+ return columns;
137
+ };
138
+ /** Whether a Resource has every one of `columns` (and there is at least one). */
139
+ const hasAll = (facts, columns) => {
140
+ for (const column of columns) {
141
+ if (!facts.fields.includes(column)) {
142
+ return false;
143
+ }
144
+ }
145
+ return columns.length > 0;
146
+ };
147
+ /**
148
+ * `resources.authTables`, held to the rules' schema: a table's Resource exists, is
149
+ * no other table's, and has every column of the table (by its `fieldName`); a table
150
+ * the map says the schema does not have (`null`) is not, column for column, a
151
+ * Resource the map leaves unnamed. A map that says otherwise than the schema would
152
+ * leave a table of Better Auth's an ordinary Resource.
153
+ */
154
+ const checkAuthTablesMap = (map, tables, policies) => {
155
+ const named = new Map();
156
+ for (const [model, resource] of Object.entries(map)) {
157
+ if (resource === null) {
158
+ continue;
159
+ }
160
+ const facts = ownOf(policies.schema, resource);
161
+ if (facts === undefined) {
162
+ refuse(`\`resources.authTables\` puts Better Auth's \`${model}\` table in ${resource}, and the rules' schema has no ${resource}.`);
163
+ }
164
+ const other = named.get(resource);
165
+ if (other !== undefined) {
166
+ refuse(`\`resources.authTables\` puts Better Auth's \`${other}\` and \`${model}\` tables both in ${resource}.`);
167
+ }
168
+ named.set(resource, model);
169
+ const lacking = [];
170
+ for (const column of columnsOf(tables.get(model))) {
171
+ if (!facts?.fields.includes(column)) {
172
+ lacking.push(column);
173
+ }
174
+ }
175
+ if (lacking.length > 0) {
176
+ refuse(`\`resources.authTables\` puts Better Auth's \`${model}\` table in ${resource}, which lacks its columns ${lacking.join(", ")}: ${resource} is not that table, or names its columns otherwise than Better Auth's \`fieldName\`s say. Name the table's Resource, and its columns as Better Auth names them.`);
177
+ }
178
+ }
179
+ for (const [model, resource] of Object.entries(map)) {
180
+ if (resource !== null) {
181
+ continue;
182
+ }
183
+ const table = tables.get(model);
184
+ const columns = columnsOf(table);
185
+ const secrets = [];
186
+ for (const field of SECRET_COLUMNS[model] ?? []) {
187
+ secrets.push(table?.fields.get(field) ?? field);
188
+ }
189
+ for (const [other, facts] of Object.entries(policies.schema)) {
190
+ if (named.has(other)) {
191
+ continue;
192
+ }
193
+ const held = [];
194
+ let secret = false;
195
+ for (const column of columns) {
196
+ if (facts.fields.includes(column)) {
197
+ held.push(column);
198
+ secret ||= secrets.includes(column);
199
+ }
200
+ }
201
+ // Every column of the table, or one of its secrets with another of its
202
+ // columns beside it (a model that keeps only some): it may be the table.
203
+ if (hasAll(facts, columns) || (secret && held.length > 1)) {
204
+ refuse(`\`resources.authTables\` says the rules' schema does not have Better Auth's \`${model}\` table, and ${other} has columns of it (${held.join(", ")}), ${secret ? "a secret among them" : "every one"}. If ${other} is that table, name it: \`${model}: "${other}"\`; if it is your own, it cannot keep Better Auth's secret beside its columns.`);
205
+ }
206
+ }
207
+ }
208
+ };
209
+ /**
210
+ * On Prisma's adapter, refuses a table the adapter reads through a delegate no
211
+ * Resource answers to, while the schema has one that differs from it only in case
212
+ * or a plural `s`: the schema and the adapter disagree (an adapter without
213
+ * `usePlural` on a plural schema, say), and the table the adapter really reads is
214
+ * not certain to be closed.
215
+ */
216
+ const refuseDisagreement = (tables, usePlural, policies) => {
217
+ for (const table of tables.values()) {
218
+ if (table.resource !== undefined) {
219
+ continue;
220
+ }
221
+ const delegate = (usePlural ? `${table.name}s` : table.name).toLowerCase();
222
+ for (const resource of Object.keys(policies.schema)) {
223
+ const lowered = resource.toLowerCase();
224
+ if (lowered === delegate ||
225
+ lowered === `${delegate}s` ||
226
+ `${lowered}s` === delegate) {
227
+ refuse(`Better Auth's Prisma adapter reads its \`${table.model}\` table as \`prisma.${usePlural ? `${table.name}s` : table.name}\`, and the rules' schema has no such model but has ${resource}: the schema and the adapter disagree (\`usePlural\`?). Make them agree, so Access knows which Resource is the table.`);
228
+ }
229
+ }
230
+ }
231
+ };
232
+ /** Whether a Resource's name is the ownership beacon's table's: `AventaraBeacon`, `_aventara_beacon`, their plurals, in any case. */
233
+ const isBeaconName = (resource) => /^aventarabeacons?$/.test(resource.replace(/_/g, "").toLowerCase());
234
+ /**
235
+ * Closes every Resource that is the ownership beacon's table, whoever's it is: the
236
+ * Access server's (Better Auth's `aventaraBeacon`, found as its tables are) and one
237
+ * that only looks like it by name (a service's own `_aventara_beacon` in the rules'
238
+ * schema). Each is denied every action and its `value` sealed, whatever grants
239
+ * name it (`"*"` included): any deny wins. A grant on the Access server's table is
240
+ * refused as on any closed Better Auth table. A beacon is read by the startup check
241
+ * through the service's own connection, never through a grant.
242
+ */
243
+ const closeBeaconTables = (policies, tables, denies, sealed) => {
244
+ const found = tables.get(BEACON_TABLE_MODEL)?.resource;
245
+ for (const [resource, facts] of Object.entries(policies.schema)) {
246
+ if (facts.foreign !== undefined ||
247
+ (resource !== found && !isBeaconName(resource))) {
248
+ continue;
249
+ }
250
+ if (!denies.some((deny) => deny.id === `${BUILT_IN_ID}${resource}`)) {
251
+ denies.push({
252
+ id: `${BUILT_IN_ID}${resource}`,
253
+ actions: "*",
254
+ resource,
255
+ });
256
+ }
257
+ const secrets = new Set(sealed.get(resource) ?? []);
258
+ const value = resource === found
259
+ ? (tables.get(BEACON_TABLE_MODEL)?.fields.get("value") ?? "value")
260
+ : "value";
261
+ if (facts.fields.includes(value)) {
262
+ secrets.add(value);
263
+ }
264
+ if (secrets.size > 0) {
265
+ sealed.set(resource, secrets);
266
+ }
267
+ }
268
+ };
269
+ /** The Access server's beacon table, by Better Auth model name. */
270
+ const BEACON_TABLE_MODEL = "aventaraBeacon";
271
+ /**
272
+ * The built-in rules for one set of rules: `resources` as the app gave it, Better
273
+ * Auth's tables as it is built (`authTablesOf`), and the rules' file. Throws
274
+ * `ACCESS_RESOURCE_MISSING` when a key's tables are missing, and
275
+ * `ACCESS_OPTIONS_INVALID` for a grant the built-in rules would leave without effect.
276
+ */
277
+ export const guardAuthTables = (input) => {
278
+ const { resources, tables, naming, policies } = input;
279
+ if (naming.kind === "map") {
280
+ checkAuthTablesMap(naming.map, tables, policies);
281
+ }
282
+ else {
283
+ refuseDisagreement(tables, naming.usePlural, policies);
284
+ }
285
+ for (const key of KEYS) {
286
+ if (!resources[key]) {
287
+ continue;
288
+ }
289
+ for (const model of KEY_TABLES[key]) {
290
+ const table = tables.get(model);
291
+ const plugin = OPENED_TABLES[model]?.plugin;
292
+ if (table === undefined) {
293
+ missing(`\`resources.${key}\` opens Better Auth's \`${model}\` table, and Better Auth has none${plugin === undefined ? "" : `: add ${plugin} to betterAuth.plugins`}.`);
294
+ continue;
295
+ }
296
+ const facts = table.resource === undefined
297
+ ? undefined
298
+ : ownOf(policies.schema, table.resource);
299
+ if (facts === undefined) {
300
+ missing(naming.kind === "prisma"
301
+ ? `\`resources.${key}\` opens Better Auth's \`${model}\` table, and the rules' schema has no model Better Auth's Prisma adapter reads it through (\`prisma.${table.name}${naming.usePlural ? "s" : ""}\`). Run Better Auth's \`npx auth generate\`, then regenerate the model the rules are built from.`
302
+ : `\`resources.${key}\` opens Better Auth's \`${model}\` table, and \`resources.authTables\` puts it in no Resource of the rules' schema. Name its Resource there.`);
303
+ }
304
+ if (facts?.foreign !== undefined) {
305
+ refuse(`\`resources.${key}\` opens Better Auth's \`${model}\` table as the Resource ${table.resource}, which the rules read as namespace "${facts.foreign}"'s (foreign). Open it in the service that owns it.`);
306
+ }
307
+ }
308
+ }
309
+ const denies = [];
310
+ const sealed = new Map();
311
+ const writable = new Map();
312
+ const closed = new Map();
313
+ const governed = new Map();
314
+ for (const table of tables.values()) {
315
+ const { resource } = table;
316
+ const facts = resource === undefined ? undefined : ownOf(policies.schema, resource);
317
+ if (resource === undefined ||
318
+ facts === undefined ||
319
+ facts.foreign !== undefined) {
320
+ continue;
321
+ }
322
+ const opened = OPENED_TABLES[table.model];
323
+ const open = opened !== undefined && resources[opened.key];
324
+ const secrets = [];
325
+ for (const field of SECRET_COLUMNS[table.model] ?? []) {
326
+ const name = fieldOf(table, facts, field);
327
+ if (name !== undefined) {
328
+ secrets.push(name);
329
+ }
330
+ else if (open) {
331
+ // An opened table's secret must be found to be sealed: one the schema
332
+ // names otherwise (a Prisma `@map`) would be readable.
333
+ refuse(`Better Auth's \`${table.model}\` table keeps a secret in \`${field}\`, and ${resource} has no field \`${table.fields.get(field) ?? field}\`: Access could not keep it sealed. Name the field as Better Auth's \`fieldName\` for it says, or leave the table closed.`);
334
+ }
335
+ }
336
+ if (secrets.length > 0) {
337
+ sealed.set(resource, new Set(secrets));
338
+ }
339
+ const grants = policies.grants.filter((grant) => grant.resource === resource);
340
+ if (opened === undefined || !open) {
341
+ denies.push({
342
+ id: `${BUILT_IN_ID}${resource}`,
343
+ actions: "*",
344
+ resource,
345
+ });
346
+ if (opened !== undefined) {
347
+ closed.set(resource, { model: table.model, key: opened.key });
348
+ }
349
+ const [grant] = grants;
350
+ if (grant !== undefined) {
351
+ refuse(opened === undefined
352
+ ? `the rules grant ${actionList(grant.actions)} on ${resource} (grant "${grant.id}"), Better Auth's \`${table.model}\` table, which Access keeps closed: no grant opens it. Remove the grant.`
353
+ : `the rules grant ${actionList(grant.actions)} on ${resource} (grant "${grant.id}"), Better Auth's \`${table.model}\` table, which is closed until \`resources: { ${opened.key}: true }\` opens it as a Resource of the rules.`);
354
+ }
355
+ continue;
356
+ }
357
+ const { writes } = opened;
358
+ const refused = ACTIONS.filter((action) => (action === "create" && writes.create === undefined) ||
359
+ (action === "update" && writes.update === undefined) ||
360
+ (action === "delete" && !writes.delete));
361
+ if (refused.length > 0) {
362
+ denies.push({
363
+ id: `${BUILT_IN_ID}${resource}/${refused.join(",")}`,
364
+ actions: refused,
365
+ resource,
366
+ });
367
+ }
368
+ for (const grant of grants) {
369
+ const never = grant.actions.filter((action) => refused.includes(action));
370
+ if (never.length > 0) {
371
+ refuse(`the rules grant ${actionList(never)} on ${resource} (grant "${grant.id}"), Better Auth's \`${table.model}\` table, which Access never writes that way: a write through Aventara skips Better Auth's hooks and caches. Use Better Auth's own API for it, and remove ${actionList(never)} from the grant.`);
372
+ }
373
+ }
374
+ writable.set(resource, {
375
+ create: writableSet(table, facts, writes.create, true),
376
+ update: writableSet(table, facts, writes.update, false),
377
+ });
378
+ const writeGoverned = (opened.governed ?? []).flatMap((field) => {
379
+ const name = fieldOf(table, facts, field);
380
+ return name === undefined ? [] : [name];
381
+ });
382
+ if (writeGoverned.length > 0) {
383
+ governed.set(resource, writeGoverned);
384
+ }
385
+ }
386
+ closeBeaconTables(policies, tables, denies, sealed);
387
+ return { denies, sealed, writable, closed, governed };
388
+ };
389
+ /**
390
+ * The rules' file with the built-in rules in it: their denies beside the file's, and
391
+ * each field they govern for writing governed so (a field the rules govern for
392
+ * reading only is then governed for both; one they govern for writing or both stays).
393
+ */
394
+ export const withBuiltInRules = (policies, guard) => {
395
+ if (guard.denies.length === 0 && guard.governed.size === 0) {
396
+ return policies;
397
+ }
398
+ const govern = {
399
+ ...policies.govern,
400
+ };
401
+ const governModes = { ...policies.governModes };
402
+ for (const [resource, fields] of guard.governed) {
403
+ const listed = ownOf(policies.govern, resource) ?? [];
404
+ const modes = Object.hasOwn(governModes, resource)
405
+ ? { ...governModes[resource] }
406
+ : {};
407
+ const added = [];
408
+ for (const field of fields) {
409
+ if (!listed.includes(field)) {
410
+ added.push(field);
411
+ modes[field] = "write";
412
+ }
413
+ else if (Object.hasOwn(modes, field) && modes[field] === "read") {
414
+ // Governed for reading by the rules, for writing by Access: for both.
415
+ delete modes[field];
416
+ }
417
+ }
418
+ govern[resource] = [...listed, ...added];
419
+ if (Object.keys(modes).length > 0) {
420
+ governModes[resource] = modes;
421
+ }
422
+ else {
423
+ delete governModes[resource];
424
+ }
425
+ }
426
+ const { governModes: _given, ...rest } = policies;
427
+ return {
428
+ ...rest,
429
+ govern,
430
+ ...(Object.keys(governModes).length > 0 ? { governModes } : {}),
431
+ denies: [...policies.denies, ...guard.denies],
432
+ };
433
+ };
434
+ /**
435
+ * `fieldsOf`'s answer with the built-in rules applied: a secret column is never
436
+ * listed, and a write lists only the fields its action may set on an opened table.
437
+ * `"all"` becomes the list it stands for (every field that is not governed) when
438
+ * something is taken out of it.
439
+ */
440
+ export const guardedFields = (guard, question, answer, resource) => {
441
+ const secrets = guard.sealed.get(question.resource);
442
+ const sets = question.access === "write"
443
+ ? guard.writable.get(question.resource)
444
+ : undefined;
445
+ const writes = sets === undefined || question.access !== "write"
446
+ ? undefined
447
+ : question.action === "create"
448
+ ? sets.create
449
+ : question.action === "update"
450
+ ? sets.update
451
+ : new Set([...sets.create].filter((field) => sets.update.has(field)));
452
+ if (secrets === undefined && writes === undefined) {
453
+ return answer;
454
+ }
455
+ const keeps = (field) => !(secrets?.has(field) ?? false) && (writes?.has(field) ?? true);
456
+ return {
457
+ always: answer.always === "all"
458
+ ? resource.fields.filter((field) => !resource.governed.includes(field) && keeps(field))
459
+ : answer.always.filter(keeps),
460
+ perRow: Object.fromEntries(Object.entries(answer.perRow).filter(([field]) => keeps(field))),
461
+ };
462
+ };