@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,212 @@
1
+ // The `passwords` block of `createAccess`, checked, and the password hook Better
2
+ // Auth is built with. Better Auth hashes with scrypt; Access hashes with argon2id
3
+ // three ways:
4
+ //
5
+ // - by default, when the app sets no `betterAuth.emailAndPassword.password`, at the
6
+ // `"owasp-minimum"` profile;
7
+ // - at the profile the app names: `passwords: { argon2id: "rfc9106-low-memory" }`;
8
+ // - not at all when the app set `betterAuth.emailAndPassword.password` itself: its
9
+ // hook passes through untouched, as everything in the `betterAuth` block does.
10
+ //
11
+ // Naming a profile and setting the hook too is refused, so one hasher is never in
12
+ // doubt. The hook goes in whether or not email and password sign-in is on: Better
13
+ // Auth's plugins (`admin`, `username`, `phone-number`, `email-otp`) hash and verify
14
+ // through it too.
15
+ //
16
+ // Better Auth's scrypt hashes verify only with `legacyScrypt: true`. A sign-in whose
17
+ // password matches a hash that is not at the current profile (a scrypt hash, or
18
+ // argon2id at another profile's parameters) rehashes it at the profile (an after
19
+ // hook on Better Auth's `/sign-in/*` routes), so each such account moves to the
20
+ // profile at its next sign-in.
21
+ import { AccessError } from "../access/access.error.js";
22
+ import { isRecord } from "../policies.compiler.js";
23
+ import { ARGON2ID_PROFILES, argon2idHasher, DEFAULT_ARGON2ID_PROFILE, isLegacyScryptHash, needsRehash, rehashPolicyOf, } from "./argon2id.hasher.js";
24
+ import { NODE_RS_ARGON2_VERSION } from "./node-rs-argon2.adapter.js";
25
+ /** Better Auth's default `emailAndPassword.maxPasswordLength`. */
26
+ const BETTER_AUTH_MAX_PASSWORD_LENGTH = 128;
27
+ const refuse = (message) => {
28
+ throw new AccessError("ACCESS_OPTIONS_INVALID", message);
29
+ };
30
+ const PROFILE_NAMES = Object.keys(ARGON2ID_PROFILES)
31
+ .map((name) => `"${name}"`)
32
+ .join(" or ");
33
+ /** The checked `passwords` block, `undefined` when it is absent. */
34
+ export const checkPasswordsOptions = (passwords) => {
35
+ if (passwords === undefined) {
36
+ return undefined;
37
+ }
38
+ const profile = isRecord(passwords) ? passwords.argon2id : undefined;
39
+ const legacy = isRecord(passwords) ? passwords.legacyScrypt : undefined;
40
+ if (!isRecord(passwords) ||
41
+ Object.keys(passwords).some((key) => key !== "argon2id" && key !== "legacyScrypt") ||
42
+ typeof profile !== "string" ||
43
+ !Object.hasOwn(ARGON2ID_PROFILES, profile) ||
44
+ (legacy !== undefined && typeof legacy !== "boolean")) {
45
+ return refuse(`\`passwords\` is \`{ argon2id: ${PROFILE_NAMES}, legacyScrypt?: boolean }\`; it was ${JSON.stringify(passwords)}.`);
46
+ }
47
+ return {
48
+ profile: profile,
49
+ legacyScrypt: legacy === true,
50
+ };
51
+ };
52
+ /**
53
+ * The after hook that moves a password to the current profile: on a sign-in that
54
+ * started a session with a password, when Better Auth's password hook is Access's
55
+ * and the user's credential holds a hash {@link needsRehash} names (a scrypt hash
56
+ * under `legacyScrypt`, or argon2id at other parameters than the profile's), and
57
+ * this password matches it (checked again here, so no route's own reading of
58
+ * `password` is trusted), the password is hashed at the profile and stored in its
59
+ * place.
60
+ *
61
+ * The write is a compare-and-set through Better Auth's adapter: it changes the
62
+ * account only while it still holds the hash just checked. A password changed or
63
+ * reset meanwhile is never overwritten, and of two sign-ins at once the later write
64
+ * wins with an equally valid hash. It goes past Better Auth's account update hooks:
65
+ * the password is the same one, encoded anew. On a current hash it costs one read
66
+ * and nothing else. A failure is logged (never the password or a hash) and leaves
67
+ * the stored hash; the sign-in stands.
68
+ */
69
+ export const passwordRehashHook = (modules) => ({
70
+ matcher: (ctx) => typeof ctx?.path === "string" && ctx.path.startsWith("/sign-in/"),
71
+ handler: modules.createAuthMiddleware(async (ctx) => {
72
+ const context = ctx?.context;
73
+ const userId = context?.newSession?.user?.id;
74
+ const password = ctx?.body?.password;
75
+ const policy = rehashPolicyOf(context?.password?.verify);
76
+ if (context === undefined ||
77
+ policy === undefined ||
78
+ typeof userId !== "string" ||
79
+ typeof password !== "string") {
80
+ return undefined;
81
+ }
82
+ try {
83
+ const account = await context.internalAdapter.findCredentialAccount(userId);
84
+ const stored = account?.password;
85
+ const accountId = account?.id;
86
+ if (typeof stored !== "string" ||
87
+ typeof accountId !== "string" ||
88
+ !needsRehash(stored, policy)) {
89
+ return undefined;
90
+ }
91
+ const matches = isLegacyScryptHash(stored)
92
+ ? await modules.passwords.verifyScrypt({ hash: stored, password })
93
+ : await context.password.verify({ hash: stored, password });
94
+ if (matches !== true) {
95
+ return undefined;
96
+ }
97
+ await context.adapter.updateMany({
98
+ model: "account",
99
+ where: [
100
+ { field: "id", value: accountId },
101
+ { field: "password", value: stored },
102
+ ],
103
+ update: { password: await context.password.hash(password) },
104
+ });
105
+ }
106
+ catch {
107
+ context.logger.error("Aventara Access: rehashing a password at the current profile failed; the stored hash stays until the next sign-in.");
108
+ }
109
+ return undefined;
110
+ }),
111
+ });
112
+ /**
113
+ * The password hook Better Auth runs with: argon2id at the checked block's profile,
114
+ * or `undefined` when the app set `emailAndPassword.password` itself (its hook
115
+ * passes through). `at` names that option as the app writes it.
116
+ */
117
+ export const passwordHookOf = (settings, emailAndPassword, modules,
118
+ /** Better Auth's context, once it is built: where the hook logs. */
119
+ context, at) => {
120
+ const given = emailAndPassword ?? {};
121
+ if (!isRecord(given)) {
122
+ return refuse(`\`${at}\` holds Better Auth's email and password options, as \`betterAuth({...})\` takes them.`);
123
+ }
124
+ if (given.password !== undefined) {
125
+ if (settings !== undefined) {
126
+ refuse(`\`passwords\` and \`${at}.password\` both say how passwords are hashed: keep \`passwords\` for Access's argon2id, or your own \`password\` hook alone.`);
127
+ }
128
+ return undefined;
129
+ }
130
+ const { crypto, verifyScrypt } = modules.passwords;
131
+ if (typeof crypto.argon2 !== "function") {
132
+ return refuse(`argon2id needs Node.js's \`crypto.argon2\` (Node.js 24.7.0 and later), which this runtime does not have: on Node.js 22, install @node-rs/argon2 (npm install --save-exact @node-rs/argon2@${NODE_RS_ARGON2_VERSION}); or set \`${at}.password\` to hash passwords yourself.`);
133
+ }
134
+ return argon2idHasher({
135
+ profile: settings?.profile ?? DEFAULT_ARGON2ID_PROFILE,
136
+ // Better Auth's own reading of it, so the cap is the one its routes check.
137
+ maxPasswordLength: given.maxPasswordLength ||
138
+ BETTER_AUTH_MAX_PASSWORD_LENGTH,
139
+ crypto: crypto,
140
+ verifyScrypt,
141
+ legacyScrypt: settings?.legacyScrypt === true,
142
+ tooLong: () => new modules.APIError("BAD_REQUEST", {
143
+ code: "PASSWORD_TOO_LONG",
144
+ message: "Password too long",
145
+ }),
146
+ log: async (level, message) => {
147
+ try {
148
+ const built = (await context());
149
+ built.logger[level](`Aventara Access: ${message}`);
150
+ }
151
+ catch {
152
+ // Better Auth could not be built: there is no logger to report to.
153
+ }
154
+ },
155
+ });
156
+ };
157
+ /**
158
+ * What `createAccess` builds Better Auth with for passwords: its `emailAndPassword`
159
+ * block, the app's with the argon2id hook in it, or `undefined` when the app set
160
+ * `password` itself and its block passes through as given; and, with the argon2id
161
+ * hook, the after hook the Access plugin adds to rehash a password at the profile.
162
+ */
163
+ export const passwordsOf = (passwords, betterAuth, modules,
164
+ /** Better Auth's context, once it is built: where the hook logs. */
165
+ context) => {
166
+ const settings = checkPasswordsOptions(passwords);
167
+ const hook = passwordHookOf(settings, betterAuth.emailAndPassword, modules, context, "betterAuth.emailAndPassword");
168
+ return hook === undefined
169
+ ? { emailAndPassword: undefined, rehash: undefined }
170
+ : {
171
+ emailAndPassword: {
172
+ ...betterAuth.emailAndPassword,
173
+ password: hook,
174
+ },
175
+ rehash: passwordRehashHook(modules),
176
+ };
177
+ };
178
+ /**
179
+ * The kiosk profile's PIN hasher: argon2id at the `passwords` block's profile (the
180
+ * default without one), whatever hashes the app's passwords, since a PIN is a weak
181
+ * secret and its hash must be slow wherever it is read. A PIN is at most
182
+ * `maxPinLength` digits; Better Auth's scrypt hashes never verify as PINs.
183
+ */
184
+ export const pinHasherOf = (passwords, modules,
185
+ /** Better Auth's context, once it is built: where the hasher logs. */
186
+ context, maxPinLength) => {
187
+ const settings = checkPasswordsOptions(passwords);
188
+ const { crypto, verifyScrypt } = modules.passwords;
189
+ if (typeof crypto.argon2 !== "function") {
190
+ return refuse(`the kiosk profile hashes PINs with argon2id, which needs Node.js's \`crypto.argon2\` (Node.js 24.7.0 and later): on Node.js 22, install @node-rs/argon2 (npm install --save-exact @node-rs/argon2@${NODE_RS_ARGON2_VERSION}).`);
191
+ }
192
+ return argon2idHasher({
193
+ profile: settings?.profile ?? DEFAULT_ARGON2ID_PROFILE,
194
+ maxPasswordLength: maxPinLength,
195
+ crypto: crypto,
196
+ verifyScrypt,
197
+ legacyScrypt: false,
198
+ tooLong: () => new modules.APIError("BAD_REQUEST", {
199
+ code: "PIN_TOO_WEAK",
200
+ message: "A PIN is at most 12 digits.",
201
+ }),
202
+ log: async (level, message) => {
203
+ try {
204
+ const built = (await context());
205
+ built.logger[level](`Aventara Access: ${message.replace(/password/g, "PIN")}`);
206
+ }
207
+ catch {
208
+ // Better Auth could not be built: there is no logger to report to.
209
+ }
210
+ },
211
+ });
212
+ };
@@ -0,0 +1,219 @@
1
+ /** The format marker of every `@1` file. */
2
+ export declare const POLICIES_FORMAT = "aventara-access/policies@1";
3
+ /** The format marker of the unstable v0 files, refused by name (no users, no shim). */
4
+ export declare const RETIRED_POLICIES_FORMAT = "aventara-access/policies@0";
5
+ /** The four standard operations a rule names. */
6
+ export type AccessAction = "read" | "create" | "update" | "delete";
7
+ /** The layer a request runs under (core's `AvRequestOrigin`, copied by shape). */
8
+ export type AccessOrigin = "application" | "client" | "agent";
9
+ /** The two built-in subjects: `signedIn` (a non-null principal) and `anyone` (null included). */
10
+ export type AccessBuiltInSubject = "signedIn" | "anyone";
11
+ /** A literal a rule compares with. */
12
+ export type AccessLiteral = string | number | boolean | null;
13
+ /** A value read off the principal at question time (`me.id` → `{ principal: ["id"] }`). */
14
+ export type AccessPrincipalOperand = {
15
+ readonly principal: readonly [string, ...string[]];
16
+ };
17
+ /** What a field comparison compares with. */
18
+ export type AccessOperand = AccessLiteral | AccessPrincipalOperand;
19
+ /** The comparisons a rule may make. Each maps one-to-one to an Aventara filter operator. */
20
+ export type AccessComparison = "eq" | "ne" | "in" | "notIn";
21
+ /**
22
+ * A row condition in Access's neutral form. Field names are the Resource's own;
23
+ * a relation step names its target so the file needs no schema to walk it.
24
+ */
25
+ export type AccessPredicate = {
26
+ readonly op: "true";
27
+ } | {
28
+ readonly op: "false";
29
+ } | {
30
+ readonly op: "and";
31
+ readonly args: readonly AccessPredicate[];
32
+ } | {
33
+ readonly op: "or";
34
+ readonly args: readonly AccessPredicate[];
35
+ } | {
36
+ readonly op: "not";
37
+ readonly arg: AccessPredicate;
38
+ } | {
39
+ readonly op: "cmp";
40
+ readonly field: string;
41
+ readonly cmp: AccessComparison;
42
+ readonly value: AccessOperand | readonly AccessOperand[];
43
+ } | {
44
+ readonly op: "rel";
45
+ readonly relation: string;
46
+ readonly target: string;
47
+ readonly where: AccessPredicate;
48
+ } | {
49
+ readonly op: "some" | "every" | "none";
50
+ readonly relation: string;
51
+ readonly target: string;
52
+ readonly where: AccessPredicate;
53
+ } | {
54
+ readonly op: "role";
55
+ readonly role: string;
56
+ };
57
+ /** A condition on the principal alone: its claims and the request's origin. */
58
+ export type AccessPrincipalCondition = {
59
+ readonly op: "and";
60
+ readonly args: readonly AccessPrincipalCondition[];
61
+ } | {
62
+ readonly op: "cmp";
63
+ readonly claim: readonly [string, ...string[]];
64
+ readonly cmp: "eq";
65
+ readonly value: AccessLiteral;
66
+ } | {
67
+ readonly op: "origin";
68
+ readonly value: AccessOrigin;
69
+ };
70
+ /** A role: global (read from claims) or held on a row of one Resource. */
71
+ export type AccessRoleDefinition = {
72
+ readonly global: AccessPrincipalCondition;
73
+ } | {
74
+ readonly on: string;
75
+ readonly where: AccessPredicate;
76
+ };
77
+ /** One grant. Any grant allows (default deny). */
78
+ export type AccessGrantRule = {
79
+ readonly id: string;
80
+ readonly subjects: readonly string[];
81
+ readonly actions: readonly AccessAction[];
82
+ readonly resource: string;
83
+ /** The to-one relation whose target holds each subject's row role. */
84
+ readonly in?: string;
85
+ readonly where?: AccessPredicate;
86
+ /** Governed fields this grant opens, as `Resource.field`. */
87
+ readonly fields?: readonly string[];
88
+ /** Fields this grant does not let a create or update write. */
89
+ readonly write?: {
90
+ readonly except: readonly string[];
91
+ };
92
+ /** A condition the row an update leaves must still meet. */
93
+ readonly after?: AccessPredicate;
94
+ };
95
+ /** One deny. Any deny wins over every grant. */
96
+ export type AccessDenyRule = {
97
+ readonly id: string;
98
+ readonly actions: "*" | readonly AccessAction[];
99
+ readonly resource: string;
100
+ readonly where?: AccessPredicate;
101
+ readonly if?: AccessPrincipalCondition;
102
+ };
103
+ /** A relation as the engine needs it: where it leads, and the reference pairs when this end holds them. */
104
+ export type AccessSchemaRelationFact = {
105
+ readonly target: string;
106
+ readonly cardinality: "one" | "many";
107
+ readonly referenceFields?: readonly [string, ...string[]];
108
+ readonly references?: readonly [string, ...string[]];
109
+ };
110
+ /** The schema facts the engine reads, per Resource. */
111
+ export type AccessSchemaResourceFacts = {
112
+ readonly identifiers: readonly (readonly string[])[];
113
+ readonly fields: readonly string[];
114
+ /**
115
+ * The scalar fields that may hold NULL, when there are any. A scope negates a
116
+ * comparison on one of them null-safely (`NOT (f = v) OR f IS NULL`), so it keeps
117
+ * the rows `can` keeps.
118
+ */
119
+ readonly nullable?: readonly string[];
120
+ /**
121
+ * The scalar fields `can` compares by their type rather than by their JavaScript
122
+ * value, when there are any: a `decimal` as its canonical decimal digits (a decimal
123
+ * object, a string and a number alike), a `datetime` as its instant, a `bigint`
124
+ * exactly. A file without `types` has none of them.
125
+ */
126
+ readonly types?: {
127
+ readonly [field: string]: "bigint" | "datetime" | "decimal";
128
+ };
129
+ readonly relations: {
130
+ readonly [relation: string]: AccessSchemaRelationFact;
131
+ };
132
+ /**
133
+ * The namespace that owns this Resource, when it is not this file's. A foreign
134
+ * Resource is read in conditions and never granted on.
135
+ */
136
+ readonly foreign?: string;
137
+ };
138
+ /** A claim's type, never its value. */
139
+ export type AccessClaimType = {
140
+ readonly scalar: "string" | "number" | "boolean";
141
+ };
142
+ /**
143
+ * The `@1` policies file. `Governed` keeps the `govern` block's literal type through
144
+ * the builder: per Resource, the list form (each field governed for reading and
145
+ * writing) or the object form (a mode per field).
146
+ */
147
+ export type AccessPolicies<Governed extends {
148
+ readonly [resource: string]: readonly string[] | {
149
+ readonly [field: string]: "read" | "write" | "both";
150
+ };
151
+ } = {
152
+ readonly [resource: string]: readonly string[];
153
+ }> = {
154
+ readonly format: typeof POLICIES_FORMAT;
155
+ /** The namespace that owns this file's Resources. A single app needs none. */
156
+ readonly namespace?: string;
157
+ readonly claims: {
158
+ readonly [claim: string]: AccessClaimType;
159
+ };
160
+ readonly schema: {
161
+ readonly [resource: string]: AccessSchemaResourceFacts;
162
+ };
163
+ readonly roles: {
164
+ readonly [role: string]: AccessRoleDefinition;
165
+ };
166
+ /** Every governed field, by Resource, whatever its mode. */
167
+ readonly govern: {
168
+ readonly [R in keyof Governed]: Governed[R] extends readonly string[] ? Governed[R] : readonly (keyof Governed[R] & string)[];
169
+ };
170
+ /**
171
+ * The governed fields governed for one access only, by Resource: `"write"`, read by
172
+ * default and written only through a grant that names the field; `"read"`, written
173
+ * by default and read only through a grant that names it. A governed field with no
174
+ * entry is governed for both, and a file without `governModes` has no other kind.
175
+ */
176
+ readonly governModes?: {
177
+ readonly [resource: string]: {
178
+ readonly [field: string]: "read" | "write";
179
+ };
180
+ };
181
+ readonly grants: readonly AccessGrantRule[];
182
+ readonly denies: readonly AccessDenyRule[];
183
+ };
184
+ /**
185
+ * A policies file as a JSON import types it: `resolveJsonModule` widens `format` to
186
+ * `string` and each tuple to an array, so the file `avaccess pull` wrote, imported
187
+ * with `import policies from "./access/blog.policies.json" with { type: "json" }`,
188
+ * is not an `AccessPolicies`. `policies.rules` and `createAccessEngine` take it as it
189
+ * is: only its outline is typed, and the engine checks the rest when it is built.
190
+ */
191
+ export type AccessPoliciesJson<Governed extends {
192
+ readonly [resource: string]: readonly string[] | {
193
+ readonly [field: string]: "read" | "write" | "both";
194
+ };
195
+ } = {
196
+ readonly [resource: string]: readonly string[];
197
+ }> = {
198
+ readonly format: string;
199
+ readonly namespace?: string;
200
+ readonly claims: {
201
+ readonly [claim: string]: {
202
+ readonly scalar: string;
203
+ };
204
+ };
205
+ readonly schema: {
206
+ readonly [resource: string]: object;
207
+ };
208
+ readonly roles: {
209
+ readonly [role: string]: object;
210
+ };
211
+ readonly govern: Governed;
212
+ readonly governModes?: {
213
+ readonly [resource: string]: {
214
+ readonly [field: string]: string;
215
+ };
216
+ };
217
+ readonly grants: readonly object[];
218
+ readonly denies: readonly object[];
219
+ };
@@ -0,0 +1,8 @@
1
+ // The declarative policies file, format `aventara-access/policies@1`. The builder
2
+ // compiles to it and the engine runs from it alone. It is passive JSON: no closures,
3
+ // no claim values, no provider names. `@1` is frozen: a change to its shape is `policies@2` beside it, never an
4
+ // edit of `@1`. Its canonical byte form and hash are `policies.hash.ts`'s.
5
+ /** The format marker of every `@1` file. */
6
+ export const POLICIES_FORMAT = "aventara-access/policies@1";
7
+ /** The format marker of the unstable v0 files, refused by name (no users, no shim). */
8
+ export const RETIRED_POLICIES_FORMAT = "aventara-access/policies@0";
@@ -0,0 +1,38 @@
1
+ import type { AccessDefinition } from "./access.builder.js";
2
+ import { type AccessAction, type AccessClaimType, type AccessOrigin, type AccessPolicies, type AccessPrincipalCondition } from "./policies-contract.js";
3
+ /** The marker a `me` value carries; the compiler writes it as `{ principal: [...] }`. */
4
+ export declare const PRINCIPAL_REF: unique symbol;
5
+ export declare const ACTIONS: readonly AccessAction[];
6
+ export declare const ORIGINS: readonly AccessOrigin[];
7
+ export declare const CLAIM_SCALARS: readonly string[];
8
+ export declare const fail: (where: string, message: string) => never;
9
+ export declare const isRecord: (value: unknown) => value is {
10
+ readonly [key: string]: unknown;
11
+ };
12
+ export declare const asList: <T>(value: T | readonly T[]) => readonly T[];
13
+ /** The claims a file compares, by the type each is compared as. */
14
+ export type ClaimTypes = Record<string, AccessClaimType>;
15
+ export declare const idAllocator: () => (prefix: string, name: unknown) => string;
16
+ /**
17
+ * A rule's `if` (`{ claims?, origin? }`), compiled to a condition on the principal
18
+ * alone. Each claim's type is recorded in `claimTypes`, and a claim compared as two
19
+ * types is refused.
20
+ */
21
+ export declare const compilePrincipalIf: (rule: string, condition: unknown, claimTypes: ClaimTypes) => AccessPrincipalCondition;
22
+ /** Compiles the builder's rules to the `@1` policies file and checks it. */
23
+ export declare const compilePolicies: <Governed extends {
24
+ readonly [resource: string]: readonly string[] | {
25
+ readonly [field: string]: "read" | "write" | "both";
26
+ };
27
+ }>(definition: AccessDefinition) => AccessPolicies<Governed>;
28
+ export declare const checkActions: (rule: string, actions: readonly string[]) => void;
29
+ /** Whether `value` is a name a file may give a Resource or a field. */
30
+ export declare const isName: (value: unknown) => value is string;
31
+ /** Whether `value` is a namespace name. */
32
+ export declare const isNamespace: (value: unknown) => value is string;
33
+ /**
34
+ * Checks a policies file before an engine runs from it. Throws an `Error` naming
35
+ * the rule. The file is trusted for nothing it can be checked for. `composed`: the
36
+ * file the engine composed itself, whose denies may take the reserved ids.
37
+ */
38
+ export declare const checkPolicies: (policies: AccessPolicies, composed?: boolean) => void;