@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,97 @@
1
+ /**
2
+ * The argon2id parameter profiles, by name. Each keeps a 16-byte salt (RFC 9106
3
+ * section 3.1: "16 bytes is RECOMMENDED for password hashing") and a 32-byte tag (RFC 9106
4
+ * section 4: "256-bit tag size").
5
+ */
6
+ export declare const ARGON2ID_PROFILES: {
7
+ readonly [P in AccessArgon2idProfile]: Argon2idCost;
8
+ };
9
+ /**
10
+ * The argon2id parameter profiles `createAccess` takes, by name: `"owasp-minimum"`
11
+ * (m=19456 KiB, t=2, p=1) or `"rfc9106-low-memory"` (m=65536 KiB, t=3, p=4).
12
+ */
13
+ export type AccessArgon2idProfile = "owasp-minimum" | "rfc9106-low-memory";
14
+ /** The profile `createAccess` hashes with when the app chose none. */
15
+ export declare const DEFAULT_ARGON2ID_PROFILE: AccessArgon2idProfile;
16
+ /** An argon2id cost: memory in KiB, passes (iterations) and lanes. */
17
+ type Argon2idCost = {
18
+ readonly memory: number;
19
+ readonly passes: number;
20
+ readonly parallelism: number;
21
+ };
22
+ /** What the hasher takes from Node.js's `node:crypto`. */
23
+ export type NodePasswordCrypto = {
24
+ readonly argon2: (algorithm: "argon2id", parameters: Argon2idCost & {
25
+ readonly message: string;
26
+ readonly nonce: Uint8Array;
27
+ readonly tagLength: number;
28
+ }, done: (error: Error | null, derivedKey: Uint8Array) => void) => void;
29
+ readonly randomBytes: (size: number) => Uint8Array;
30
+ readonly timingSafeEqual: (a: Uint8Array, b: Uint8Array) => boolean;
31
+ };
32
+ /** Better Auth's `emailAndPassword.password` hook: what the hasher answers as. */
33
+ export type PasswordHook = {
34
+ readonly hash: (password: string) => Promise<string>;
35
+ readonly verify: (input: {
36
+ readonly hash: string;
37
+ readonly password: string;
38
+ }) => Promise<boolean>;
39
+ };
40
+ type Argon2idHasherOptions = {
41
+ readonly profile: AccessArgon2idProfile;
42
+ /** The longest password hashed or checked, in UTF-16 code units: Better Auth's `maxPasswordLength`. */
43
+ readonly maxPasswordLength: number;
44
+ readonly crypto: NodePasswordCrypto;
45
+ /** Better Auth's own scrypt `verifyPassword`, for hashes stored before argon2id. */
46
+ readonly verifyScrypt: (input: {
47
+ readonly hash: string;
48
+ readonly password: string;
49
+ }) => Promise<boolean>;
50
+ /**
51
+ * Whether Better Auth's scrypt hashes verify. Off: a scrypt hash fails the check.
52
+ * On: it verifies, and every hash and check also runs the other algorithm once,
53
+ * so a sign-in costs the same whichever hash an account holds, or none.
54
+ */
55
+ readonly legacyScrypt: boolean;
56
+ /** The error `hash` rejects a password over `maxPasswordLength` with. */
57
+ readonly tooLong: () => Error;
58
+ /** Where a refused stored hash or a failed computation is reported; never the hash or the password. */
59
+ readonly log: (level: "warn" | "error", message: string) => void;
60
+ };
61
+ /** Whether a stored hash is in Better Auth's scrypt format. */
62
+ export declare const isLegacyScryptHash: (hash: string) => boolean;
63
+ /**
64
+ * Better Auth's `emailAndPassword.password` hook, hashing with argon2id at
65
+ * `profile`. `verify` checks argon2id PHC strings at the parameters they name (up
66
+ * to {@link CEILING}), and, with `legacyScrypt`, Better Auth's scrypt hashes through
67
+ * Better Auth's own verify. It never throws: anything it cannot check is `false`.
68
+ *
69
+ * Every check costs one argon2id computation at the profile, whatever is stored
70
+ * (an argon2id hash, a malformed one, a scrypt hash with `legacyScrypt` off), as
71
+ * Better Auth's hash of a decoy costs for an unknown user; with `legacyScrypt` on,
72
+ * every hash and check also costs one scrypt. So the time a sign-in takes does not
73
+ * say what an account holds, but for one gap: a stored argon2id hash is checked at
74
+ * the parameters it names, so after a profile change a hash at the old profile
75
+ * costs the old profile's time until its account's next sign-in rehashes it at the
76
+ * profile ({@link needsRehash}). Passwords are NFKC-normalised first, as Better
77
+ * Auth's scrypt does, so one password typed on two keyboards hashes alike.
78
+ */
79
+ export declare const argon2idHasher: (options: Argon2idHasherOptions) => PasswordHook;
80
+ /** What decides whether a hash this hasher verified is rehashed: its profile, and `legacyScrypt`. */
81
+ type RehashPolicy = {
82
+ readonly profile: AccessArgon2idProfile;
83
+ readonly legacyScrypt: boolean;
84
+ };
85
+ /** The rehash policy of a password hook's `verify`, `undefined` unless {@link argon2idHasher} built it. */
86
+ export declare const rehashPolicyOf: (verify: unknown) => RehashPolicy | undefined;
87
+ /**
88
+ * Whether a stored hash a password has just matched is rehashed at the policy's
89
+ * profile: a scrypt hash with `legacyScrypt` (without it, none verifies), or an
90
+ * argon2id PHC string whose parameters, salt or tag length are not the profile's,
91
+ * whether they cost less or more (the profile the app names is the one every hash
92
+ * is checked at, so a sign-in's time says nothing of when the account last signed
93
+ * in). A string Access cannot check (malformed, over the ceiling) never verifies,
94
+ * and is never rehashed.
95
+ */
96
+ export declare const needsRehash: (stored: string, policy: RehashPolicy) => boolean;
97
+ export {};
@@ -0,0 +1,256 @@
1
+ // Password hashing with argon2id, on Node.js's own `crypto.argon2` (added in Node.js
2
+ // 24.7.0; no native dependency). Better Auth hashes with scrypt; `createAccess` puts
3
+ // this hasher in Better Auth's `emailAndPassword.password` hook unless the app set
4
+ // that hook itself. It imports nothing: the caller hands it Node.js's primitives and
5
+ // Better Auth's scrypt verify, so the Better Auth plugin entry can use it the same way.
6
+ //
7
+ // A hash is a PHC string, `$argon2id$v=19$m=<KiB>,t=<passes>,p=<lanes>$<salt>$<tag>`
8
+ // (salt and tag in base64 without padding). `verify` reads the parameters from the
9
+ // stored string, so a profile can change later and every older hash still verifies.
10
+ // A stored value that is not a well-formed argon2id PHC string within the ceiling
11
+ // below (or, with `legacyScrypt`, Better Auth's scrypt format) fails the check
12
+ // (`false`); `verify` never throws.
13
+ /**
14
+ * The argon2id parameter profiles, by name. Each keeps a 16-byte salt (RFC 9106
15
+ * section 3.1: "16 bytes is RECOMMENDED for password hashing") and a 32-byte tag (RFC 9106
16
+ * section 4: "256-bit tag size").
17
+ */
18
+ export const ARGON2ID_PROFILES = {
19
+ /**
20
+ * OWASP Password Storage Cheat Sheet, Argon2id minimum configuration
21
+ * "m=19456 (19 MiB), t=2, p=1". The default.
22
+ */
23
+ "owasp-minimum": { memory: 19_456, passes: 2, parallelism: 1 },
24
+ /**
25
+ * RFC 9106 section 4, the SECOND RECOMMENDED option: "t=3 iterations, p=4 lanes,
26
+ * m=2^(16) (64 MiB of RAM)", for when 2 GiB per hash is not available.
27
+ */
28
+ "rfc9106-low-memory": { memory: 65_536, passes: 3, parallelism: 4 },
29
+ };
30
+ /** The profile `createAccess` hashes with when the app chose none. */
31
+ export const DEFAULT_ARGON2ID_PROFILE = "owasp-minimum";
32
+ const SALT_BYTES = 16;
33
+ const TAG_BYTES = 32;
34
+ /** Argon2 version 0x13, as PHC strings write it. */
35
+ const VERSION = 19;
36
+ /** The bounds on a stored hash's salt and tag, in bytes. */
37
+ const STORED_BYTES = {
38
+ salt: { min: 8, max: 64 },
39
+ tag: { min: 16, max: 64 },
40
+ };
41
+ /**
42
+ * The most a stored hash may cost to check: each parameter at the largest any
43
+ * profile uses, and memory times passes too. A stored hash past it (a tampered row,
44
+ * or one written elsewhere) is refused without being computed, so it cannot make a
45
+ * sign-in allocate gigabytes or run for seconds. Derived from the profiles, so a
46
+ * profile added later raises it.
47
+ */
48
+ const CEILING = (() => {
49
+ const ceiling = { memory: 0, passes: 0, parallelism: 0, work: 0 };
50
+ for (const profile of Object.keys(ARGON2ID_PROFILES)) {
51
+ const cost = ARGON2ID_PROFILES[profile];
52
+ ceiling.memory = Math.max(ceiling.memory, cost.memory);
53
+ ceiling.passes = Math.max(ceiling.passes, cost.passes);
54
+ ceiling.parallelism = Math.max(ceiling.parallelism, cost.parallelism);
55
+ ceiling.work = Math.max(ceiling.work, cost.memory * cost.passes);
56
+ }
57
+ return ceiling;
58
+ })();
59
+ /** Better Auth's scrypt format: a 16-byte hex salt, `:`, a 64-byte hex key. */
60
+ const SCRYPT_HASH = /^[0-9a-f]{32}:[0-9a-f]{128}$/;
61
+ /** Whether a stored hash is in Better Auth's scrypt format. */
62
+ export const isLegacyScryptHash = (hash) => SCRYPT_HASH.test(hash);
63
+ const DECIMAL = "(0|[1-9][0-9]{0,9})";
64
+ const B64 = "([A-Za-z0-9+/]+)";
65
+ const PHC_ARGON2ID = new RegExp(`^\\$argon2id\\$v=${DECIMAL}\\$m=${DECIMAL},t=${DECIMAL},p=${DECIMAL}\\$${B64}\\$${B64}$`);
66
+ /** Bytes as unpadded standard base64, as PHC strings write them. */
67
+ const toB64 = (bytes) => {
68
+ let binary = "";
69
+ for (const byte of bytes) {
70
+ binary += String.fromCharCode(byte);
71
+ }
72
+ return btoa(binary).replace(/=+$/, "");
73
+ };
74
+ /** Unpadded base64 as bytes; `undefined` unless it is the canonical spelling of them. */
75
+ const fromB64 = (text) => {
76
+ if (text.length % 4 === 1) {
77
+ return undefined;
78
+ }
79
+ const binary = atob(text + "=".repeat((4 - (text.length % 4)) % 4));
80
+ const bytes = new Uint8Array(binary.length);
81
+ for (let index = 0; index < binary.length; index++) {
82
+ bytes[index] = binary.charCodeAt(index);
83
+ }
84
+ // Unused low bits in the last character would let two strings name one value.
85
+ return toB64(bytes) === text ? bytes : undefined;
86
+ };
87
+ const within = (value, bounds) => value >= bounds.min && value <= bounds.max;
88
+ /**
89
+ * A stored argon2id PHC string, read: its parameters and bytes, `"over-ceiling"`
90
+ * when it is well formed and costs more than {@link CEILING}, `undefined` when it is
91
+ * not an argon2id PHC string Access writes (another version, a parameter Argon2
92
+ * refuses, non-canonical base64, a salt or tag out of bounds).
93
+ */
94
+ const readPhc = (stored) => {
95
+ const match = PHC_ARGON2ID.exec(stored);
96
+ if (match === null) {
97
+ return undefined;
98
+ }
99
+ // By index: destructuring the match costs the type checker a thousand instantiations.
100
+ const cost = {
101
+ memory: Number(match[2]),
102
+ passes: Number(match[3]),
103
+ parallelism: Number(match[4]),
104
+ };
105
+ const saltBytes = fromB64(match[5]);
106
+ const tagBytes = fromB64(match[6]);
107
+ if (Number(match[1]) !== VERSION ||
108
+ cost.passes < 1 ||
109
+ cost.parallelism < 1 ||
110
+ // Argon2 needs at least 8 blocks of 1 KiB per lane.
111
+ cost.memory < 8 * cost.parallelism ||
112
+ saltBytes === undefined ||
113
+ !within(saltBytes.length, STORED_BYTES.salt) ||
114
+ tagBytes === undefined ||
115
+ !within(tagBytes.length, STORED_BYTES.tag)) {
116
+ return undefined;
117
+ }
118
+ if (cost.memory > CEILING.memory ||
119
+ cost.passes > CEILING.passes ||
120
+ cost.parallelism > CEILING.parallelism ||
121
+ cost.memory * cost.passes > CEILING.work) {
122
+ return "over-ceiling";
123
+ }
124
+ return { ...cost, salt: saltBytes, tag: tagBytes };
125
+ };
126
+ /** A well-formed scrypt hash no password matches: the legacy check's cost, run as a decoy. */
127
+ const DECOY_SCRYPT = `${"0".repeat(32)}:${"0".repeat(128)}`;
128
+ /**
129
+ * Better Auth's `emailAndPassword.password` hook, hashing with argon2id at
130
+ * `profile`. `verify` checks argon2id PHC strings at the parameters they name (up
131
+ * to {@link CEILING}), and, with `legacyScrypt`, Better Auth's scrypt hashes through
132
+ * Better Auth's own verify. It never throws: anything it cannot check is `false`.
133
+ *
134
+ * Every check costs one argon2id computation at the profile, whatever is stored
135
+ * (an argon2id hash, a malformed one, a scrypt hash with `legacyScrypt` off), as
136
+ * Better Auth's hash of a decoy costs for an unknown user; with `legacyScrypt` on,
137
+ * every hash and check also costs one scrypt. So the time a sign-in takes does not
138
+ * say what an account holds, but for one gap: a stored argon2id hash is checked at
139
+ * the parameters it names, so after a profile change a hash at the old profile
140
+ * costs the old profile's time until its account's next sign-in rehashes it at the
141
+ * profile ({@link needsRehash}). Passwords are NFKC-normalised first, as Better
142
+ * Auth's scrypt does, so one password typed on two keyboards hashes alike.
143
+ */
144
+ export const argon2idHasher = (options) => {
145
+ const { crypto, maxPasswordLength, verifyScrypt, legacyScrypt, tooLong, log, } = options;
146
+ const cost = ARGON2ID_PROFILES[options.profile];
147
+ const derive = (password, at, nonce, tagLength) => new Promise((resolve, reject) => {
148
+ crypto.argon2("argon2id", {
149
+ message: password.normalize("NFKC"),
150
+ nonce,
151
+ tagLength,
152
+ memory: at.memory,
153
+ passes: at.passes,
154
+ parallelism: at.parallelism,
155
+ }, (error, key) => (error === null ? resolve(key) : reject(error)));
156
+ });
157
+ /** A scrypt check, `false` on any failure. */
158
+ const scrypt = async (hash, password) => {
159
+ try {
160
+ return (await verifyScrypt({ hash, password })) === true;
161
+ }
162
+ catch {
163
+ log("error", "a scrypt password check failed; it counts as a mismatch.");
164
+ return false;
165
+ }
166
+ };
167
+ /** The argon2id check of a stored hash; a decoy at the profile when there is none to check. */
168
+ const argon2id = async (stored, password) => {
169
+ try {
170
+ const derived = await derive(password, stored ?? cost, stored?.salt ?? crypto.randomBytes(SALT_BYTES), stored?.tag.length ?? TAG_BYTES);
171
+ return (stored !== undefined && crypto.timingSafeEqual(derived, stored.tag));
172
+ }
173
+ catch {
174
+ log("error", "an argon2id password check failed; it counts as a mismatch.");
175
+ return false;
176
+ }
177
+ };
178
+ const hook = {
179
+ hash: async (password) => {
180
+ if (typeof password !== "string") {
181
+ throw new TypeError("A password is a string.");
182
+ }
183
+ if (password.length > maxPasswordLength) {
184
+ throw tooLong();
185
+ }
186
+ const salt = crypto.randomBytes(SALT_BYTES);
187
+ const tag = await derive(password, cost, salt, TAG_BYTES);
188
+ if (legacyScrypt) {
189
+ await scrypt(DECOY_SCRYPT, password);
190
+ }
191
+ return `$argon2id$v=${VERSION}$m=${cost.memory},t=${cost.passes},p=${cost.parallelism}$${toB64(salt)}$${toB64(tag)}`;
192
+ },
193
+ verify: async ({ hash, password }) => {
194
+ if (typeof hash !== "string" ||
195
+ typeof password !== "string" ||
196
+ password.length > maxPasswordLength) {
197
+ return false;
198
+ }
199
+ if (SCRYPT_HASH.test(hash)) {
200
+ if (!legacyScrypt) {
201
+ log("warn", "a stored password hash is Better Auth's scrypt, and `passwords.legacyScrypt` is off: the sign-in is refused.");
202
+ await argon2id(undefined, password);
203
+ return false;
204
+ }
205
+ const matches = await scrypt(hash, password);
206
+ await argon2id(undefined, password);
207
+ return matches;
208
+ }
209
+ const stored = readPhc(hash);
210
+ if (stored === "over-ceiling") {
211
+ log("warn", `a stored argon2id password hash costs more than Access checks (m <= ${CEILING.memory}, t <= ${CEILING.passes}, p <= ${CEILING.parallelism}, m*t <= ${CEILING.work}): the sign-in is refused.`);
212
+ }
213
+ const matches = await argon2id(typeof stored === "object" ? stored : undefined, password);
214
+ if (legacyScrypt) {
215
+ await scrypt(DECOY_SCRYPT, password);
216
+ }
217
+ return matches;
218
+ },
219
+ };
220
+ REHASH_POLICIES.set(hook.verify, { profile: options.profile, legacyScrypt });
221
+ return hook;
222
+ };
223
+ /**
224
+ * Each hook {@link argon2idHasher} built, by its `verify`: Better Auth puts that
225
+ * same function in its context (`context.password.verify`), so a sign-in hook can
226
+ * tell Access's hasher, and its profile, from an app's own.
227
+ */
228
+ const REHASH_POLICIES = new WeakMap();
229
+ /** The rehash policy of a password hook's `verify`, `undefined` unless {@link argon2idHasher} built it. */
230
+ export const rehashPolicyOf = (verify) => typeof verify === "function"
231
+ ? REHASH_POLICIES.get(verify)
232
+ : undefined;
233
+ /**
234
+ * Whether a stored hash a password has just matched is rehashed at the policy's
235
+ * profile: a scrypt hash with `legacyScrypt` (without it, none verifies), or an
236
+ * argon2id PHC string whose parameters, salt or tag length are not the profile's,
237
+ * whether they cost less or more (the profile the app names is the one every hash
238
+ * is checked at, so a sign-in's time says nothing of when the account last signed
239
+ * in). A string Access cannot check (malformed, over the ceiling) never verifies,
240
+ * and is never rehashed.
241
+ */
242
+ export const needsRehash = (stored, policy) => {
243
+ if (SCRYPT_HASH.test(stored)) {
244
+ return policy.legacyScrypt;
245
+ }
246
+ const read = readPhc(stored);
247
+ if (typeof read !== "object") {
248
+ return false;
249
+ }
250
+ const cost = ARGON2ID_PROFILES[policy.profile];
251
+ return (read.memory !== cost.memory ||
252
+ read.passes !== cost.passes ||
253
+ read.parallelism !== cost.parallelism ||
254
+ read.salt.length !== SALT_BYTES ||
255
+ read.tag.length !== TAG_BYTES);
256
+ };
@@ -0,0 +1,25 @@
1
+ import type { NodePasswordCrypto } from "./argon2id.hasher.js";
2
+ /** The exact `@node-rs/argon2` version Access is built and tested against. */
3
+ export declare const NODE_RS_ARGON2_VERSION = "2.2.2";
4
+ /** What Access takes from `@node-rs/argon2`: the raw tag of one argon2 computation. */
5
+ export type NodeRsArgon2 = {
6
+ readonly hashRaw: (password: string, options: {
7
+ /** `2` is `Algorithm.Argon2id`. */
8
+ readonly algorithm: 2;
9
+ /** `1` is `Version.V0x13`, Argon2 version 19. */
10
+ readonly version: 1;
11
+ readonly memoryCost: number;
12
+ readonly timeCost: number;
13
+ readonly parallelism: number;
14
+ readonly outputLen: number;
15
+ readonly salt: Uint8Array;
16
+ }) => Promise<Uint8Array>;
17
+ };
18
+ /** `@node-rs/argon2`'s `hashRaw` as Node.js's `crypto.argon2("argon2id", …)`. */
19
+ export declare const nodeRsArgon2: (module: NodeRsArgon2) => NodePasswordCrypto["argon2"];
20
+ /**
21
+ * What hashing takes from Node.js: its `node:crypto` when it has `crypto.argon2`
22
+ * (Node.js 24.7.0 and later; `@node-rs/argon2` is then never loaded, installed or
23
+ * not), else the same with `@node-rs/argon2` behind `argon2`, else without `argon2`.
24
+ */
25
+ export declare const passwordCryptoOf: (nodeCrypto: Partial<NodePasswordCrypto>, loadNodeRs: () => NodeRsArgon2 | undefined) => Partial<NodePasswordCrypto>;
@@ -0,0 +1,42 @@
1
+ // The argon2id backend on Node.js 22, which has no `crypto.argon2`: `@node-rs/argon2`,
2
+ // an exact optional peer, loaded only when Node.js's own is missing. Its `hashRaw`
3
+ // is put behind Node.js's `crypto.argon2` signature, so the hasher runs the same code
4
+ // on either: it writes the PHC string and compares the tags itself, and the two
5
+ // backends compute the same RFC 9106 function, so a hash made on one verifies on the
6
+ // other. It imports nothing: the package's Node.js module loader hands it over.
7
+ /** The exact `@node-rs/argon2` version Access is built and tested against. */
8
+ export const NODE_RS_ARGON2_VERSION = "2.2.2";
9
+ /** `@node-rs/argon2`'s `hashRaw` as Node.js's `crypto.argon2("argon2id", …)`. */
10
+ export const nodeRsArgon2 = (module) => (_algorithm, parameters, done) => {
11
+ module
12
+ .hashRaw(parameters.message, {
13
+ algorithm: 2,
14
+ version: 1,
15
+ memoryCost: parameters.memory,
16
+ timeCost: parameters.passes,
17
+ parallelism: parameters.parallelism,
18
+ outputLen: parameters.tagLength,
19
+ salt: parameters.nonce,
20
+ })
21
+ .then((key) => done(null, key), (error) => done(error, new Uint8Array(0)));
22
+ };
23
+ /**
24
+ * What hashing takes from Node.js: its `node:crypto` when it has `crypto.argon2`
25
+ * (Node.js 24.7.0 and later; `@node-rs/argon2` is then never loaded, installed or
26
+ * not), else the same with `@node-rs/argon2` behind `argon2`, else without `argon2`.
27
+ */
28
+ export const passwordCryptoOf = (nodeCrypto, loadNodeRs) => {
29
+ if (typeof nodeCrypto.argon2 === "function") {
30
+ return nodeCrypto;
31
+ }
32
+ const nodeRs = loadNodeRs();
33
+ return {
34
+ ...(nodeCrypto.randomBytes === undefined
35
+ ? {}
36
+ : { randomBytes: nodeCrypto.randomBytes }),
37
+ ...(nodeCrypto.timingSafeEqual === undefined
38
+ ? {}
39
+ : { timingSafeEqual: nodeCrypto.timingSafeEqual }),
40
+ ...(nodeRs === undefined ? {} : { argon2: nodeRsArgon2(nodeRs) }),
41
+ };
42
+ };
@@ -0,0 +1,79 @@
1
+ import type { BetterAuthModules } from "../access/better-auth.loader.js";
2
+ import { type AccessArgon2idProfile, type PasswordHook } from "./argon2id.hasher.js";
3
+ /** The `passwords` block: the argon2id parameter profile passwords are hashed at. */
4
+ export type AccessPasswordsOptions = {
5
+ /**
6
+ * `"owasp-minimum"` (the default: m=19456 KiB, t=2, p=1, OWASP's minimum) or
7
+ * `"rfc9106-low-memory"` (m=65536 KiB, t=3, p=4, RFC 9106's second recommended
8
+ * option). Hashes stored at another profile still verify, and each is rehashed
9
+ * at this profile at its account's next sign-in.
10
+ */
11
+ readonly argon2id: AccessArgon2idProfile;
12
+ /**
13
+ * `true`: passwords Better Auth hashed with scrypt still verify, and each is
14
+ * rehashed with argon2id at the user's next sign-in. Every sign-in then also costs
15
+ * one scrypt, so its time does not say which hash an account holds. Off by
16
+ * default: a scrypt hash fails the check.
17
+ */
18
+ readonly legacyScrypt?: boolean;
19
+ };
20
+ /** The `passwords` block, checked. */
21
+ type PasswordsSettings = {
22
+ readonly profile: AccessArgon2idProfile;
23
+ readonly legacyScrypt: boolean;
24
+ };
25
+ /** The checked `passwords` block, `undefined` when it is absent. */
26
+ export declare const checkPasswordsOptions: (passwords: unknown) => PasswordsSettings | undefined;
27
+ /**
28
+ * The after hook that moves a password to the current profile: on a sign-in that
29
+ * started a session with a password, when Better Auth's password hook is Access's
30
+ * and the user's credential holds a hash {@link needsRehash} names (a scrypt hash
31
+ * under `legacyScrypt`, or argon2id at other parameters than the profile's), and
32
+ * this password matches it (checked again here, so no route's own reading of
33
+ * `password` is trusted), the password is hashed at the profile and stored in its
34
+ * place.
35
+ *
36
+ * The write is a compare-and-set through Better Auth's adapter: it changes the
37
+ * account only while it still holds the hash just checked. A password changed or
38
+ * reset meanwhile is never overwritten, and of two sign-ins at once the later write
39
+ * wins with an equally valid hash. It goes past Better Auth's account update hooks:
40
+ * the password is the same one, encoded anew. On a current hash it costs one read
41
+ * and nothing else. A failure is logged (never the password or a hash) and leaves
42
+ * the stored hash; the sign-in stands.
43
+ */
44
+ export declare const passwordRehashHook: (modules: BetterAuthModules) => unknown;
45
+ /**
46
+ * The password hook Better Auth runs with: argon2id at the checked block's profile,
47
+ * or `undefined` when the app set `emailAndPassword.password` itself (its hook
48
+ * passes through). `at` names that option as the app writes it.
49
+ */
50
+ export declare const passwordHookOf: (settings: PasswordsSettings | undefined, emailAndPassword: unknown, modules: BetterAuthModules,
51
+ /** Better Auth's context, once it is built: where the hook logs. */
52
+ context: () => Promise<unknown>, at: string) => PasswordHook | undefined;
53
+ /**
54
+ * What `createAccess` builds Better Auth with for passwords: its `emailAndPassword`
55
+ * block, the app's with the argon2id hook in it, or `undefined` when the app set
56
+ * `password` itself and its block passes through as given; and, with the argon2id
57
+ * hook, the after hook the Access plugin adds to rehash a password at the profile.
58
+ */
59
+ export declare const passwordsOf: (passwords: unknown, betterAuth: {
60
+ readonly [key: string]: unknown;
61
+ }, modules: BetterAuthModules,
62
+ /** Better Auth's context, once it is built: where the hook logs. */
63
+ context: () => Promise<unknown>) => {
64
+ readonly emailAndPassword: {
65
+ readonly [key: string]: unknown;
66
+ } | undefined;
67
+ /** The Access plugin's after hook that rehashes a password at the profile, with the argon2id hook. */
68
+ readonly rehash: unknown;
69
+ };
70
+ /**
71
+ * The kiosk profile's PIN hasher: argon2id at the `passwords` block's profile (the
72
+ * default without one), whatever hashes the app's passwords, since a PIN is a weak
73
+ * secret and its hash must be slow wherever it is read. A PIN is at most
74
+ * `maxPinLength` digits; Better Auth's scrypt hashes never verify as PINs.
75
+ */
76
+ export declare const pinHasherOf: (passwords: unknown, modules: BetterAuthModules,
77
+ /** Better Auth's context, once it is built: where the hasher logs. */
78
+ context: () => Promise<unknown>, maxPinLength: number) => PasswordHook;
79
+ export {};