@depup/better-auth__core 1.6.23-depup.2

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 (310) hide show
  1. package/LICENSE.md +20 -0
  2. package/README.md +32 -0
  3. package/changes.json +14 -0
  4. package/dist/api/index.d.mts +281 -0
  5. package/dist/api/index.mjs +92 -0
  6. package/dist/async_hooks/index.d.mts +6 -0
  7. package/dist/async_hooks/index.mjs +20 -0
  8. package/dist/async_hooks/pure.index.d.mts +6 -0
  9. package/dist/async_hooks/pure.index.mjs +33 -0
  10. package/dist/context/endpoint-context.d.mts +18 -0
  11. package/dist/context/endpoint-context.mjs +29 -0
  12. package/dist/context/global.d.mts +6 -0
  13. package/dist/context/global.mjs +36 -0
  14. package/dist/context/index.d.mts +5 -0
  15. package/dist/context/index.mjs +5 -0
  16. package/dist/context/request-state.d.mts +27 -0
  17. package/dist/context/request-state.mjs +47 -0
  18. package/dist/context/transaction.d.mts +27 -0
  19. package/dist/context/transaction.mjs +96 -0
  20. package/dist/db/adapter/factory.d.mts +17 -0
  21. package/dist/db/adapter/factory.mjs +905 -0
  22. package/dist/db/adapter/get-default-field-name.d.mts +18 -0
  23. package/dist/db/adapter/get-default-field-name.mjs +36 -0
  24. package/dist/db/adapter/get-default-model-name.d.mts +12 -0
  25. package/dist/db/adapter/get-default-model-name.mjs +30 -0
  26. package/dist/db/adapter/get-field-attributes.d.mts +26 -0
  27. package/dist/db/adapter/get-field-attributes.mjs +37 -0
  28. package/dist/db/adapter/get-field-name.d.mts +18 -0
  29. package/dist/db/adapter/get-field-name.mjs +31 -0
  30. package/dist/db/adapter/get-id-field.d.mts +36 -0
  31. package/dist/db/adapter/get-id-field.mjs +64 -0
  32. package/dist/db/adapter/get-model-name.d.mts +12 -0
  33. package/dist/db/adapter/get-model-name.mjs +21 -0
  34. package/dist/db/adapter/index.d.mts +615 -0
  35. package/dist/db/adapter/index.mjs +24 -0
  36. package/dist/db/adapter/types.d.mts +105 -0
  37. package/dist/db/adapter/utils.d.mts +7 -0
  38. package/dist/db/adapter/utils.mjs +37 -0
  39. package/dist/db/get-tables.d.mts +6 -0
  40. package/dist/db/get-tables.mjs +266 -0
  41. package/dist/db/index.d.mts +10 -0
  42. package/dist/db/index.mjs +8 -0
  43. package/dist/db/plugin.d.mts +12 -0
  44. package/dist/db/schema/account.d.mts +28 -0
  45. package/dist/db/schema/account.mjs +17 -0
  46. package/dist/db/schema/rate-limit.d.mts +18 -0
  47. package/dist/db/schema/rate-limit.mjs +9 -0
  48. package/dist/db/schema/session.d.mts +23 -0
  49. package/dist/db/schema/session.mjs +12 -0
  50. package/dist/db/schema/shared.d.mts +10 -0
  51. package/dist/db/schema/shared.mjs +9 -0
  52. package/dist/db/schema/user.d.mts +22 -0
  53. package/dist/db/schema/user.mjs +11 -0
  54. package/dist/db/schema/verification.d.mts +21 -0
  55. package/dist/db/schema/verification.mjs +10 -0
  56. package/dist/db/type.d.mts +194 -0
  57. package/dist/env/color-depth.d.mts +4 -0
  58. package/dist/env/color-depth.mjs +86 -0
  59. package/dist/env/env-impl.d.mts +32 -0
  60. package/dist/env/env-impl.mjs +81 -0
  61. package/dist/env/index.d.mts +4 -0
  62. package/dist/env/index.mjs +4 -0
  63. package/dist/env/logger.d.mts +48 -0
  64. package/dist/env/logger.mjs +79 -0
  65. package/dist/error/codes.d.mts +69 -0
  66. package/dist/error/codes.mjs +55 -0
  67. package/dist/error/index.d.mts +26 -0
  68. package/dist/error/index.mjs +27 -0
  69. package/dist/index.d.mts +9 -0
  70. package/dist/index.mjs +1 -0
  71. package/dist/instrumentation/api.mjs +12 -0
  72. package/dist/instrumentation/attributes.d.mts +11 -0
  73. package/dist/instrumentation/attributes.mjs +10 -0
  74. package/dist/instrumentation/index.d.mts +3 -0
  75. package/dist/instrumentation/index.mjs +3 -0
  76. package/dist/instrumentation/noop.mjs +42 -0
  77. package/dist/instrumentation/pure.index.d.mts +7 -0
  78. package/dist/instrumentation/pure.index.mjs +7 -0
  79. package/dist/instrumentation/tracer.d.mts +13 -0
  80. package/dist/instrumentation/tracer.mjs +53 -0
  81. package/dist/oauth2/client-credentials-token.d.mts +56 -0
  82. package/dist/oauth2/client-credentials-token.mjs +64 -0
  83. package/dist/oauth2/create-authorization-url.d.mts +44 -0
  84. package/dist/oauth2/create-authorization-url.mjs +41 -0
  85. package/dist/oauth2/index.d.mts +8 -0
  86. package/dist/oauth2/index.mjs +7 -0
  87. package/dist/oauth2/oauth-provider.d.mts +192 -0
  88. package/dist/oauth2/refresh-access-token.d.mts +54 -0
  89. package/dist/oauth2/refresh-access-token.mjs +73 -0
  90. package/dist/oauth2/reject-redirects.mjs +60 -0
  91. package/dist/oauth2/utils.d.mts +24 -0
  92. package/dist/oauth2/utils.mjs +48 -0
  93. package/dist/oauth2/validate-authorization-code.d.mts +85 -0
  94. package/dist/oauth2/validate-authorization-code.mjs +86 -0
  95. package/dist/oauth2/verify.d.mts +65 -0
  96. package/dist/oauth2/verify.mjs +202 -0
  97. package/dist/social-providers/apple.d.mts +133 -0
  98. package/dist/social-providers/apple.mjs +117 -0
  99. package/dist/social-providers/atlassian.d.mts +70 -0
  100. package/dist/social-providers/atlassian.mjs +80 -0
  101. package/dist/social-providers/cognito.d.mts +85 -0
  102. package/dist/social-providers/cognito.mjs +162 -0
  103. package/dist/social-providers/discord.d.mts +124 -0
  104. package/dist/social-providers/discord.mjs +62 -0
  105. package/dist/social-providers/dropbox.d.mts +69 -0
  106. package/dist/social-providers/dropbox.mjs +72 -0
  107. package/dist/social-providers/facebook.d.mts +79 -0
  108. package/dist/social-providers/facebook.mjs +157 -0
  109. package/dist/social-providers/figma.d.mts +61 -0
  110. package/dist/social-providers/figma.mjs +83 -0
  111. package/dist/social-providers/github.d.mts +102 -0
  112. package/dist/social-providers/github.mjs +92 -0
  113. package/dist/social-providers/gitlab.d.mts +123 -0
  114. package/dist/social-providers/gitlab.mjs +79 -0
  115. package/dist/social-providers/google.d.mts +123 -0
  116. package/dist/social-providers/google.mjs +137 -0
  117. package/dist/social-providers/huggingface.d.mts +83 -0
  118. package/dist/social-providers/huggingface.mjs +73 -0
  119. package/dist/social-providers/index.d.mts +1834 -0
  120. package/dist/social-providers/index.mjs +78 -0
  121. package/dist/social-providers/kakao.d.mts +161 -0
  122. package/dist/social-providers/kakao.mjs +70 -0
  123. package/dist/social-providers/kick.d.mts +73 -0
  124. package/dist/social-providers/kick.mjs +68 -0
  125. package/dist/social-providers/line.d.mts +105 -0
  126. package/dist/social-providers/line.mjs +110 -0
  127. package/dist/social-providers/linear.d.mts +68 -0
  128. package/dist/social-providers/linear.mjs +85 -0
  129. package/dist/social-providers/linkedin.d.mts +67 -0
  130. package/dist/social-providers/linkedin.mjs +73 -0
  131. package/dist/social-providers/microsoft-entra-id.d.mts +174 -0
  132. package/dist/social-providers/microsoft-entra-id.mjs +152 -0
  133. package/dist/social-providers/naver.d.mts +92 -0
  134. package/dist/social-providers/naver.mjs +65 -0
  135. package/dist/social-providers/notion.d.mts +64 -0
  136. package/dist/social-providers/notion.mjs +72 -0
  137. package/dist/social-providers/paybin.d.mts +71 -0
  138. package/dist/social-providers/paybin.mjs +81 -0
  139. package/dist/social-providers/paypal.d.mts +131 -0
  140. package/dist/social-providers/paypal.mjs +188 -0
  141. package/dist/social-providers/polar.d.mts +74 -0
  142. package/dist/social-providers/polar.mjs +71 -0
  143. package/dist/social-providers/railway.d.mts +65 -0
  144. package/dist/social-providers/railway.mjs +74 -0
  145. package/dist/social-providers/reddit.d.mts +62 -0
  146. package/dist/social-providers/reddit.mjs +81 -0
  147. package/dist/social-providers/roblox.d.mts +70 -0
  148. package/dist/social-providers/roblox.mjs +57 -0
  149. package/dist/social-providers/salesforce.d.mts +79 -0
  150. package/dist/social-providers/salesforce.mjs +87 -0
  151. package/dist/social-providers/slack.d.mts +83 -0
  152. package/dist/social-providers/slack.mjs +66 -0
  153. package/dist/social-providers/spotify.d.mts +63 -0
  154. package/dist/social-providers/spotify.mjs +69 -0
  155. package/dist/social-providers/tiktok.d.mts +168 -0
  156. package/dist/social-providers/tiktok.mjs +60 -0
  157. package/dist/social-providers/twitch.d.mts +79 -0
  158. package/dist/social-providers/twitch.mjs +75 -0
  159. package/dist/social-providers/twitter.d.mts +126 -0
  160. package/dist/social-providers/twitter.mjs +85 -0
  161. package/dist/social-providers/vercel.d.mts +62 -0
  162. package/dist/social-providers/vercel.mjs +58 -0
  163. package/dist/social-providers/vk.d.mts +70 -0
  164. package/dist/social-providers/vk.mjs +81 -0
  165. package/dist/social-providers/wechat.d.mts +113 -0
  166. package/dist/social-providers/wechat.mjs +81 -0
  167. package/dist/social-providers/zoom.d.mts +163 -0
  168. package/dist/social-providers/zoom.mjs +69 -0
  169. package/dist/types/context.d.mts +319 -0
  170. package/dist/types/cookie.d.mts +15 -0
  171. package/dist/types/helper.d.mts +10 -0
  172. package/dist/types/index.d.mts +9 -0
  173. package/dist/types/init-options.d.mts +1430 -0
  174. package/dist/types/plugin-client.d.mts +123 -0
  175. package/dist/types/plugin.d.mts +124 -0
  176. package/dist/types/secret.d.mts +11 -0
  177. package/dist/utils/async.d.mts +22 -0
  178. package/dist/utils/async.mjs +32 -0
  179. package/dist/utils/db.d.mts +9 -0
  180. package/dist/utils/db.mjs +15 -0
  181. package/dist/utils/deprecate.d.mts +9 -0
  182. package/dist/utils/deprecate.mjs +16 -0
  183. package/dist/utils/error-codes.d.mts +13 -0
  184. package/dist/utils/error-codes.mjs +10 -0
  185. package/dist/utils/fetch-metadata.d.mts +4 -0
  186. package/dist/utils/fetch-metadata.mjs +6 -0
  187. package/dist/utils/host.d.mts +147 -0
  188. package/dist/utils/host.mjs +295 -0
  189. package/dist/utils/id.d.mts +4 -0
  190. package/dist/utils/id.mjs +7 -0
  191. package/dist/utils/ip.d.mts +77 -0
  192. package/dist/utils/ip.mjs +230 -0
  193. package/dist/utils/is-api-error.d.mts +6 -0
  194. package/dist/utils/is-api-error.mjs +8 -0
  195. package/dist/utils/json.d.mts +4 -0
  196. package/dist/utils/json.mjs +41 -0
  197. package/dist/utils/redirect-uri.d.mts +20 -0
  198. package/dist/utils/redirect-uri.mjs +48 -0
  199. package/dist/utils/string.d.mts +8 -0
  200. package/dist/utils/string.mjs +25 -0
  201. package/dist/utils/url.d.mts +37 -0
  202. package/dist/utils/url.mjs +61 -0
  203. package/package.json +216 -0
  204. package/src/api/index.ts +185 -0
  205. package/src/async_hooks/index.ts +40 -0
  206. package/src/async_hooks/pure.index.ts +46 -0
  207. package/src/context/endpoint-context.ts +50 -0
  208. package/src/context/global.ts +57 -0
  209. package/src/context/index.ts +23 -0
  210. package/src/context/request-state.ts +91 -0
  211. package/src/context/transaction.ts +169 -0
  212. package/src/db/adapter/factory.ts +1740 -0
  213. package/src/db/adapter/get-default-field-name.ts +59 -0
  214. package/src/db/adapter/get-default-model-name.ts +51 -0
  215. package/src/db/adapter/get-field-attributes.ts +62 -0
  216. package/src/db/adapter/get-field-name.ts +43 -0
  217. package/src/db/adapter/get-id-field.ts +150 -0
  218. package/src/db/adapter/get-model-name.ts +36 -0
  219. package/src/db/adapter/index.ts +656 -0
  220. package/src/db/adapter/types.ts +134 -0
  221. package/src/db/adapter/utils.ts +61 -0
  222. package/src/db/get-tables.ts +304 -0
  223. package/src/db/index.ts +43 -0
  224. package/src/db/plugin.ts +11 -0
  225. package/src/db/schema/account.ts +47 -0
  226. package/src/db/schema/rate-limit.ts +36 -0
  227. package/src/db/schema/session.ts +29 -0
  228. package/src/db/schema/shared.ts +7 -0
  229. package/src/db/schema/user.ts +31 -0
  230. package/src/db/schema/verification.ts +28 -0
  231. package/src/db/type.ts +360 -0
  232. package/src/env/color-depth.ts +172 -0
  233. package/src/env/env-impl.ts +123 -0
  234. package/src/env/index.ts +23 -0
  235. package/src/env/logger.ts +145 -0
  236. package/src/error/codes.ts +72 -0
  237. package/src/error/index.ts +44 -0
  238. package/src/index.ts +1 -0
  239. package/src/instrumentation/api.ts +17 -0
  240. package/src/instrumentation/attributes.ts +22 -0
  241. package/src/instrumentation/index.ts +2 -0
  242. package/src/instrumentation/noop.ts +74 -0
  243. package/src/instrumentation/pure.index.ts +31 -0
  244. package/src/instrumentation/tracer.ts +95 -0
  245. package/src/oauth2/client-credentials-token.ts +126 -0
  246. package/src/oauth2/create-authorization-url.ts +89 -0
  247. package/src/oauth2/index.ts +34 -0
  248. package/src/oauth2/oauth-provider.ts +222 -0
  249. package/src/oauth2/refresh-access-token.ts +157 -0
  250. package/src/oauth2/reject-redirects.ts +70 -0
  251. package/src/oauth2/utils.ts +70 -0
  252. package/src/oauth2/validate-authorization-code.ts +190 -0
  253. package/src/oauth2/verify.ts +408 -0
  254. package/src/social-providers/apple.ts +249 -0
  255. package/src/social-providers/atlassian.ts +133 -0
  256. package/src/social-providers/cognito.ts +281 -0
  257. package/src/social-providers/discord.ts +170 -0
  258. package/src/social-providers/dropbox.ts +112 -0
  259. package/src/social-providers/facebook.ts +288 -0
  260. package/src/social-providers/figma.ts +118 -0
  261. package/src/social-providers/github.ts +184 -0
  262. package/src/social-providers/gitlab.ts +155 -0
  263. package/src/social-providers/google.ts +263 -0
  264. package/src/social-providers/huggingface.ts +119 -0
  265. package/src/social-providers/index.ts +132 -0
  266. package/src/social-providers/kakao.ts +179 -0
  267. package/src/social-providers/kick.ts +109 -0
  268. package/src/social-providers/line.ts +169 -0
  269. package/src/social-providers/linear.ts +121 -0
  270. package/src/social-providers/linkedin.ts +110 -0
  271. package/src/social-providers/microsoft-entra-id.ts +391 -0
  272. package/src/social-providers/naver.ts +113 -0
  273. package/src/social-providers/notion.ts +108 -0
  274. package/src/social-providers/paybin.ts +118 -0
  275. package/src/social-providers/paypal.ts +371 -0
  276. package/src/social-providers/polar.ts +111 -0
  277. package/src/social-providers/railway.ts +100 -0
  278. package/src/social-providers/reddit.ts +126 -0
  279. package/src/social-providers/roblox.ts +112 -0
  280. package/src/social-providers/salesforce.ts +159 -0
  281. package/src/social-providers/slack.ts +112 -0
  282. package/src/social-providers/spotify.ts +94 -0
  283. package/src/social-providers/tiktok.ts +211 -0
  284. package/src/social-providers/twitch.ts +112 -0
  285. package/src/social-providers/twitter.ts +199 -0
  286. package/src/social-providers/vercel.ts +87 -0
  287. package/src/social-providers/vk.ts +125 -0
  288. package/src/social-providers/wechat.ts +220 -0
  289. package/src/social-providers/zoom.ts +230 -0
  290. package/src/types/context.ts +460 -0
  291. package/src/types/cookie.ts +10 -0
  292. package/src/types/helper.ts +27 -0
  293. package/src/types/index.ts +40 -0
  294. package/src/types/init-options.ts +1679 -0
  295. package/src/types/plugin-client.ts +143 -0
  296. package/src/types/plugin.ts +163 -0
  297. package/src/types/secret.ts +8 -0
  298. package/src/utils/async.ts +53 -0
  299. package/src/utils/db.ts +20 -0
  300. package/src/utils/deprecate.ts +21 -0
  301. package/src/utils/error-codes.ts +68 -0
  302. package/src/utils/fetch-metadata.ts +3 -0
  303. package/src/utils/host.ts +416 -0
  304. package/src/utils/id.ts +5 -0
  305. package/src/utils/ip.ts +395 -0
  306. package/src/utils/is-api-error.ts +10 -0
  307. package/src/utils/json.ts +56 -0
  308. package/src/utils/redirect-uri.ts +54 -0
  309. package/src/utils/string.ts +40 -0
  310. package/src/utils/url.ts +77 -0
@@ -0,0 +1,395 @@
1
+ import * as z from "zod";
2
+ import { isDevelopment, isTest } from "../env";
3
+ import type { BetterAuthOptions } from "../types";
4
+
5
+ /**
6
+ * Normalizes an IP address for consistent rate limiting.
7
+ *
8
+ * Features:
9
+ * - Normalizes IPv6 to canonical lowercase form
10
+ * - Converts IPv4-mapped IPv6 to IPv4
11
+ * - Supports IPv6 subnet extraction
12
+ * - Handles all edge cases (::1, ::, etc.)
13
+ */
14
+
15
+ interface NormalizeIPOptions {
16
+ /**
17
+ * Prefix length used to collapse IPv6 addresses before keying.
18
+ * Any integer from 0 to 128 is accepted. Common values: 32, 48, 56, 64, 128.
19
+ * Values outside 0-128 are clamped.
20
+ *
21
+ * @default 64
22
+ */
23
+ ipv6Subnet?: number;
24
+ }
25
+
26
+ /**
27
+ * Checks if an IP is valid IPv4 or IPv6
28
+ */
29
+ export function isValidIP(ip: string): boolean {
30
+ return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
31
+ }
32
+
33
+ /**
34
+ * Checks if an IP is IPv6
35
+ */
36
+ function isIPv6(ip: string): boolean {
37
+ return z.ipv6().safeParse(ip).success;
38
+ }
39
+
40
+ /**
41
+ * Converts IPv4-mapped IPv6 address to IPv4
42
+ * e.g., "::ffff:192.0.2.1" -> "192.0.2.1"
43
+ */
44
+ function extractIPv4FromMapped(ipv6: string): string | null {
45
+ const lower = ipv6.toLowerCase();
46
+
47
+ // Handle ::ffff:192.0.2.1 format
48
+ if (lower.startsWith("::ffff:")) {
49
+ const ipv4Part = lower.substring(7);
50
+ // Check if it's a valid IPv4
51
+ if (z.ipv4().safeParse(ipv4Part).success) {
52
+ return ipv4Part;
53
+ }
54
+ }
55
+
56
+ // Handle full form: 0:0:0:0:0:ffff:192.0.2.1
57
+ const parts = ipv6.split(":");
58
+ if (parts.length === 7 && parts[5]?.toLowerCase() === "ffff") {
59
+ const ipv4Part = parts[6];
60
+ if (ipv4Part && z.ipv4().safeParse(ipv4Part).success) {
61
+ return ipv4Part;
62
+ }
63
+ }
64
+
65
+ // Handle hex-encoded IPv4 in mapped address
66
+ // e.g., ::ffff:c000:0201 -> 192.0.2.1
67
+ if (lower.includes("::ffff:") || lower.includes(":ffff:")) {
68
+ const groups = expandIPv6(ipv6);
69
+ if (
70
+ groups.length === 8 &&
71
+ groups[0] === "0000" &&
72
+ groups[1] === "0000" &&
73
+ groups[2] === "0000" &&
74
+ groups[3] === "0000" &&
75
+ groups[4] === "0000" &&
76
+ groups[5] === "ffff" &&
77
+ groups[6] &&
78
+ groups[7]
79
+ ) {
80
+ // Convert last two groups to IPv4
81
+ const byte1 = Number.parseInt(groups[6].substring(0, 2), 16);
82
+ const byte2 = Number.parseInt(groups[6].substring(2, 4), 16);
83
+ const byte3 = Number.parseInt(groups[7].substring(0, 2), 16);
84
+ const byte4 = Number.parseInt(groups[7].substring(2, 4), 16);
85
+ return `${byte1}.${byte2}.${byte3}.${byte4}`;
86
+ }
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Expands a compressed IPv6 address to full form
94
+ * e.g., "2001:db8::1" -> ["2001", "0db8", "0000", "0000", "0000", "0000", "0000", "0001"]
95
+ */
96
+ function expandIPv6(ipv6: string): string[] {
97
+ // Handle :: notation (zero compression)
98
+ if (ipv6.includes("::")) {
99
+ const sides = ipv6.split("::");
100
+ const left = sides[0] ? sides[0].split(":") : [];
101
+ const right = sides[1] ? sides[1].split(":") : [];
102
+
103
+ // Calculate missing groups
104
+ const totalGroups = 8;
105
+ const missingGroups = totalGroups - left.length - right.length;
106
+ const zeros = Array(missingGroups).fill("0000");
107
+
108
+ // Pad existing groups to 4 digits
109
+ const paddedLeft = left.map((g) => g.padStart(4, "0"));
110
+ const paddedRight = right.map((g) => g.padStart(4, "0"));
111
+
112
+ return [...paddedLeft, ...zeros, ...paddedRight];
113
+ }
114
+
115
+ // No compression, just pad each group
116
+ return ipv6.split(":").map((g) => g.padStart(4, "0"));
117
+ }
118
+
119
+ /**
120
+ * Normalizes an IPv6 address to canonical form
121
+ * e.g., "2001:DB8::1" -> "2001:0db8:0000:0000:0000:0000:0000:0001"
122
+ */
123
+ function normalizeIPv6(ipv6: string, subnetPrefix?: number): string {
124
+ const groups = expandIPv6(ipv6);
125
+
126
+ if (subnetPrefix !== undefined && subnetPrefix < 128) {
127
+ // Clamp to a valid bit range so out-of-spec inputs degrade safely:
128
+ // negative or fractional values would otherwise produce malformed masks.
129
+ const prefix = Math.max(0, Math.floor(subnetPrefix));
130
+ let bitsRemaining: number = prefix;
131
+
132
+ const maskedGroups = groups.map((group) => {
133
+ if (bitsRemaining <= 0) {
134
+ return "0000";
135
+ }
136
+ if (bitsRemaining >= 16) {
137
+ bitsRemaining -= 16;
138
+ return group;
139
+ }
140
+
141
+ // Partial mask for this group
142
+ const value = Number.parseInt(group, 16);
143
+ const mask = (0xffff << (16 - bitsRemaining)) & 0xffff;
144
+ const masked = value & mask;
145
+ bitsRemaining = 0;
146
+ return masked.toString(16).padStart(4, "0");
147
+ });
148
+
149
+ return maskedGroups.join(":").toLowerCase();
150
+ }
151
+
152
+ return groups.join(":").toLowerCase();
153
+ }
154
+
155
+ /**
156
+ * Normalizes an IP address (IPv4 or IPv6) for consistent rate limiting.
157
+ *
158
+ * @param ip - The IP address to normalize
159
+ * @param options - Normalization options
160
+ * @returns Normalized IP address
161
+ *
162
+ * @example
163
+ * normalizeIP("2001:DB8::1")
164
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0000"
165
+ *
166
+ * @example
167
+ * normalizeIP("::ffff:192.0.2.1")
168
+ * // -> "192.0.2.1" (converted to IPv4)
169
+ *
170
+ * @example
171
+ * normalizeIP("2001:db8::1", { ipv6Subnet: 64 })
172
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0000" (subnet /64)
173
+ */
174
+ export function normalizeIP(
175
+ ip: string,
176
+ options: NormalizeIPOptions = {},
177
+ ): string {
178
+ // IPv4 addresses are already normalized
179
+ if (z.ipv4().safeParse(ip).success) {
180
+ return ip.toLowerCase();
181
+ }
182
+
183
+ // Check if it's IPv6
184
+ if (!isIPv6(ip)) {
185
+ // Return as-is if not valid (shouldn't happen due to prior validation)
186
+ return ip.toLowerCase();
187
+ }
188
+
189
+ // Check for IPv4-mapped IPv6
190
+ const ipv4 = extractIPv4FromMapped(ip);
191
+ if (ipv4) {
192
+ return ipv4.toLowerCase();
193
+ }
194
+
195
+ // Normalize IPv6. Use ?? so an explicit 0 (mask-all) is honoured.
196
+ const subnetPrefix = options.ipv6Subnet ?? 64;
197
+ return normalizeIPv6(ip, subnetPrefix);
198
+ }
199
+
200
+ /**
201
+ * Raw bytes of an IP for CIDR comparison. Returns `null` for an invalid IP.
202
+ */
203
+ function ipToBytes(ip: string): Uint8Array | null {
204
+ if (z.ipv4().safeParse(ip).success) {
205
+ return Uint8Array.from(ip.split(".").map((octet) => Number(octet)));
206
+ }
207
+ if (!isIPv6(ip)) {
208
+ return null;
209
+ }
210
+ const mapped = extractIPv4FromMapped(ip);
211
+ if (mapped) {
212
+ return Uint8Array.from(mapped.split(".").map((octet) => Number(octet)));
213
+ }
214
+ const groups = expandIPv6(ip);
215
+ const bytes = new Uint8Array(16);
216
+ for (let i = 0; i < 8; i++) {
217
+ const group = Number.parseInt(groups[i] ?? "0", 16);
218
+ bytes[i * 2] = (group >> 8) & 0xff;
219
+ bytes[i * 2 + 1] = group & 0xff;
220
+ }
221
+ return bytes;
222
+ }
223
+
224
+ // A CIDR prefix length must be decimal digits only, so values like "8x" or
225
+ // "1e3" that `Number()` would otherwise coerce are rejected.
226
+ const CIDR_PREFIX_PATTERN = /^\d+$/;
227
+
228
+ /**
229
+ * Parses an IP or `IP/prefix` string into network bytes and a prefix length.
230
+ * The prefix must be digits only and within the address family. `null` if the
231
+ * value is not a valid IP or CIDR range, which keeps a malformed entry from
232
+ * silently behaving like a non-match.
233
+ */
234
+ function parseCIDR(
235
+ value: string,
236
+ ): { bytes: Uint8Array; prefix: number } | null {
237
+ const slash = value.lastIndexOf("/");
238
+ const bytes = ipToBytes(slash === -1 ? value : value.slice(0, slash));
239
+ if (!bytes) {
240
+ return null;
241
+ }
242
+ const maxBits = bytes.length * 8;
243
+ if (slash === -1) {
244
+ return { bytes, prefix: maxBits };
245
+ }
246
+ const prefixPart = value.slice(slash + 1);
247
+ if (!CIDR_PREFIX_PATTERN.test(prefixPart)) {
248
+ return null;
249
+ }
250
+ const prefix = Number(prefixPart);
251
+ return prefix <= maxBits ? { bytes, prefix } : null;
252
+ }
253
+
254
+ /**
255
+ * Whether `ipBytes` falls inside an already-parsed CIDR network.
256
+ */
257
+ function matchesCIDR(
258
+ ipBytes: Uint8Array,
259
+ net: { bytes: Uint8Array; prefix: number },
260
+ ): boolean {
261
+ if (ipBytes.length !== net.bytes.length) {
262
+ return false;
263
+ }
264
+ let bitsRemaining = net.prefix;
265
+ for (let i = 0; i < ipBytes.length && bitsRemaining > 0; i++) {
266
+ const take = bitsRemaining >= 8 ? 8 : bitsRemaining;
267
+ const mask = take === 8 ? 0xff : (0xff << (8 - take)) & 0xff;
268
+ if (((ipBytes[i] ?? 0) & mask) !== ((net.bytes[i] ?? 0) & mask)) {
269
+ return false;
270
+ }
271
+ bitsRemaining -= 8;
272
+ }
273
+ return true;
274
+ }
275
+
276
+ /**
277
+ * Trusted-proxy entries that are not a valid IP address or CIDR range.
278
+ */
279
+ export function findInvalidTrustedProxies(entries: string[]): string[] {
280
+ return entries.filter((entry) => parseCIDR(entry) === null);
281
+ }
282
+
283
+ /**
284
+ * Resolves the client IP from a forwarded header. The leftmost token is spoofable,
285
+ * so with `trustedProxies` the chain is stripped from the right to the first
286
+ * untrusted hop. Otherwise only a single-value header is trusted. Returns `null`
287
+ * when no trustworthy client IP can be resolved.
288
+ */
289
+ export function getIPFromHeader(
290
+ value: string,
291
+ options: {
292
+ ipv6Subnet?: number;
293
+ trustedProxies?: string[];
294
+ } = {},
295
+ ): string | null {
296
+ const forwardedIps = value
297
+ .split(",")
298
+ .map((ip) => ip.trim())
299
+ .filter(Boolean);
300
+ if (forwardedIps.length === 0) {
301
+ return null;
302
+ }
303
+
304
+ // Parse trusted proxies once, dropping malformed entries so a config typo
305
+ // cannot leave the chain enabled-but-empty and return a real proxy hop as
306
+ // the client. With no valid proxy the chain mode does not engage.
307
+ const trustedProxies = (options.trustedProxies ?? [])
308
+ .map(parseCIDR)
309
+ .filter((proxy): proxy is { bytes: Uint8Array; prefix: number } => {
310
+ return proxy !== null;
311
+ });
312
+
313
+ if (trustedProxies.length > 0) {
314
+ for (let i = forwardedIps.length - 1; i >= 0; i--) {
315
+ const ip = forwardedIps[i];
316
+ const ipBytes = ip ? ipToBytes(ip) : null;
317
+ // A malformed hop breaks the chain: fail closed.
318
+ if (!ip || !ipBytes) {
319
+ return null;
320
+ }
321
+ if (trustedProxies.some((proxy) => matchesCIDR(ipBytes, proxy))) {
322
+ continue;
323
+ }
324
+ return normalizeIP(ip, { ipv6Subnet: options.ipv6Subnet });
325
+ }
326
+ return null;
327
+ }
328
+
329
+ // Without valid trusted proxies a multi-hop chain is unresolvable.
330
+ if (forwardedIps.length !== 1) {
331
+ return null;
332
+ }
333
+ const selectedIp = forwardedIps[0];
334
+ if (!selectedIp || !isValidIP(selectedIp)) {
335
+ return null;
336
+ }
337
+
338
+ return normalizeIP(selectedIp, { ipv6Subnet: options.ipv6Subnet });
339
+ }
340
+
341
+ const LOCALHOST_IP = "127.0.0.1";
342
+ const DEFAULT_IP_HEADERS = ["x-forwarded-for"];
343
+
344
+ /**
345
+ * Resolves the client IP for a request from the configured IP headers.
346
+ * Honors `disableIpTracking`, walks `ipAddressHeaders` in order (default
347
+ * `x-forwarded-for`), and falls back to localhost in development and test.
348
+ * Returns `null` when tracking is disabled or no trustworthy IP can be resolved.
349
+ */
350
+ export function getIp(
351
+ req: Request | Headers,
352
+ options: BetterAuthOptions,
353
+ ): string | null {
354
+ if (options.advanced?.ipAddress?.disableIpTracking) {
355
+ return null;
356
+ }
357
+
358
+ const headers = "headers" in req ? req.headers : req;
359
+
360
+ const ipHeaders =
361
+ options.advanced?.ipAddress?.ipAddressHeaders || DEFAULT_IP_HEADERS;
362
+
363
+ for (const key of ipHeaders) {
364
+ const value = "get" in headers ? headers.get(key) : headers[key];
365
+ if (typeof value === "string") {
366
+ const ip = getIPFromHeader(value, {
367
+ ipv6Subnet: options.advanced?.ipAddress?.ipv6Subnet,
368
+ trustedProxies: options.advanced?.ipAddress?.trustedProxies,
369
+ });
370
+ if (ip) {
371
+ return ip;
372
+ }
373
+ }
374
+ }
375
+
376
+ if (isTest() || isDevelopment()) {
377
+ return LOCALHOST_IP;
378
+ }
379
+
380
+ return null;
381
+ }
382
+
383
+ /**
384
+ * Creates a rate limit key from IP and path
385
+ * Uses a separator to prevent collision attacks
386
+ *
387
+ * @param ip - The IP address (should be normalized)
388
+ * @param path - The request path
389
+ * @returns Rate limit key
390
+ */
391
+ export function createRateLimitKey(ip: string, path: string): string {
392
+ // Use | as separator to prevent collision attacks
393
+ // e.g., "192.0.2.1" + "/sign-in" vs "192.0.2" + ".1/sign-in"
394
+ return `${ip}|${path}`;
395
+ }
@@ -0,0 +1,10 @@
1
+ import { APIError as BaseAPIError } from "better-call";
2
+ import { APIError } from "../error";
3
+
4
+ export function isAPIError(error: unknown): error is APIError {
5
+ return (
6
+ error instanceof BaseAPIError ||
7
+ error instanceof APIError ||
8
+ (error as { name?: string })?.name === "APIError"
9
+ );
10
+ }
@@ -0,0 +1,56 @@
1
+ import { logger } from "../env";
2
+
3
+ const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
4
+
5
+ function reviveDate(value: unknown): any {
6
+ if (typeof value === "string" && iso8601Regex.test(value)) {
7
+ const date = new Date(value);
8
+ if (!isNaN(date.getTime())) {
9
+ return date;
10
+ }
11
+ }
12
+ return value;
13
+ }
14
+
15
+ /**
16
+ * Recursively walk a pre-parsed object and convert ISO 8601 date strings
17
+ * to Date instances. This handles the case where a Redis client (or similar)
18
+ * returns already-parsed JSON objects whose date fields are still strings.
19
+ */
20
+ function reviveDates(value: unknown): any {
21
+ if (value === null || value === undefined) {
22
+ return value;
23
+ }
24
+ if (typeof value === "string") {
25
+ return reviveDate(value);
26
+ }
27
+ if (value instanceof Date) {
28
+ return value;
29
+ }
30
+ if (Array.isArray(value)) {
31
+ return value.map(reviveDates);
32
+ }
33
+ if (typeof value === "object") {
34
+ const result: Record<string, any> = {};
35
+ for (const key of Object.keys(value)) {
36
+ result[key] = reviveDates((value as Record<string, any>)[key]);
37
+ }
38
+ return result;
39
+ }
40
+ return value;
41
+ }
42
+
43
+ export function safeJSONParse<T>(data: unknown): T | null {
44
+ try {
45
+ if (typeof data !== "string") {
46
+ if (data === null || data === undefined) {
47
+ return null;
48
+ }
49
+ return reviveDates(data) as T;
50
+ }
51
+ return JSON.parse(data, (_, value) => reviveDate(value));
52
+ } catch (e) {
53
+ logger.error("Error parsing JSON", { error: e });
54
+ return null;
55
+ }
56
+ }
@@ -0,0 +1,54 @@
1
+ import * as z from "zod";
2
+ import { isLoopbackHost } from "./host";
3
+ import { DANGEROUS_URL_SCHEMES } from "./url";
4
+
5
+ /**
6
+ * Zod schema for OAuth redirect URIs and other developer-supplied URLs that the
7
+ * server stores and later hands back to a browser.
8
+ *
9
+ * - Rejects dangerous schemes (`javascript:`, `data:`, `vbscript:`).
10
+ * - Rejects URIs with a fragment component (`#...`) per RFC 6749 §3.1.2.
11
+ * - Requires HTTPS, except for loopback hosts (`127.0.0.0/8`, `[::1]`,
12
+ * `*.localhost` per RFC 6761), where HTTP is allowed for local development.
13
+ * - Allows custom schemes for mobile apps (e.g. `myapp://callback`).
14
+ *
15
+ * This is the single source of truth for redirect-URI validation across the
16
+ * OAuth provider plugins. Consume it from `@better-auth/core/utils/redirect-uri`
17
+ * rather than re-implementing the scheme policy per plugin.
18
+ */
19
+ export const SafeUrlSchema = z.url().superRefine((val, ctx) => {
20
+ let u: URL;
21
+ try {
22
+ u = new URL(val);
23
+ } catch {
24
+ ctx.addIssue({
25
+ code: "custom",
26
+ message: "URL must be parseable",
27
+ fatal: true,
28
+ });
29
+ return z.NEVER;
30
+ }
31
+
32
+ if (DANGEROUS_URL_SCHEMES.includes(u.protocol)) {
33
+ ctx.addIssue({
34
+ code: "custom",
35
+ message: "URL cannot use javascript:, data:, or vbscript: scheme",
36
+ });
37
+ return;
38
+ }
39
+
40
+ if (val.includes("#")) {
41
+ ctx.addIssue({
42
+ code: "custom",
43
+ message: "Redirect URI must not contain a fragment component",
44
+ });
45
+ }
46
+
47
+ if (u.protocol === "http:" && !isLoopbackHost(u.host)) {
48
+ ctx.addIssue({
49
+ code: "custom",
50
+ message:
51
+ "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)",
52
+ });
53
+ }
54
+ });
@@ -0,0 +1,40 @@
1
+ export function capitalizeFirstLetter(str: string) {
2
+ return str.charAt(0).toUpperCase() + str.slice(1);
3
+ }
4
+
5
+ const WORD_PATTERN =
6
+ /[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
7
+ const APOSTROPHE_PATTERN = /['\u2019]/g;
8
+
9
+ function splitWords(input: string): string[] {
10
+ return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
11
+ }
12
+
13
+ export function toSnakeCase(input: string): string {
14
+ return splitWords(input)
15
+ .map((word) => word.toLowerCase())
16
+ .join("_");
17
+ }
18
+
19
+ export function toKebabCase(input: string): string {
20
+ return splitWords(input)
21
+ .map((word) => word.toLowerCase())
22
+ .join("-");
23
+ }
24
+
25
+ export function toCamelCase(input: string): string {
26
+ return splitWords(input).reduce((acc, word, i) => {
27
+ return (
28
+ acc +
29
+ (i === 0
30
+ ? word.toLowerCase()
31
+ : `${word[0]!.toUpperCase()}${word.slice(1)}`)
32
+ );
33
+ }, "");
34
+ }
35
+
36
+ export function toPascalCase(input: string): string {
37
+ return splitWords(input)
38
+ .map((word) => `${word[0]!.toUpperCase()}${word.slice(1).toLowerCase()}`)
39
+ .join("");
40
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Normalizes a request pathname by removing the basePath prefix and trailing slashes.
3
+ * This is useful for matching paths against configured path lists.
4
+ *
5
+ * @param requestUrl - The full request URL
6
+ * @param basePath - The base path of the auth API (e.g., "/api/auth")
7
+ * @returns The normalized path without basePath prefix or trailing slashes,
8
+ * or "/" if URL parsing fails
9
+ *
10
+ * @example
11
+ * normalizePathname("http://localhost:3000/api/auth/sso/saml2/callback/provider1", "/api/auth")
12
+ * // Returns: "/sso/saml2/callback/provider1"
13
+ *
14
+ * normalizePathname("http://localhost:3000/sso/saml2/callback/provider1/", "/")
15
+ * // Returns: "/sso/saml2/callback/provider1"
16
+ */
17
+ export function normalizePathname(
18
+ requestUrl: string,
19
+ basePath: string,
20
+ ): string {
21
+ let pathname: string;
22
+ try {
23
+ pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
24
+ } catch {
25
+ return "/";
26
+ }
27
+
28
+ // Canonicalize the basePath the same way as the request pathname. A baseURL
29
+ // with a trailing slash yields a basePath like "/api/auth/"; without this it
30
+ // would never match the slash-stripped pathname and the prefix would leak
31
+ // through to disabledPaths and rate-limit special-rule matching.
32
+ const normalizedBasePath = basePath.replace(/\/+$/, "");
33
+
34
+ if (normalizedBasePath === "") {
35
+ return pathname;
36
+ }
37
+
38
+ // Check for exact match or proper path boundary (basePath followed by "/" or end)
39
+ // This prevents "/api/auth" from matching "/api/authevil/..."
40
+ if (pathname === normalizedBasePath) {
41
+ return "/";
42
+ }
43
+
44
+ if (pathname.startsWith(normalizedBasePath + "/")) {
45
+ return pathname.slice(normalizedBasePath.length).replace(/\/+$/, "") || "/";
46
+ }
47
+
48
+ return pathname;
49
+ }
50
+
51
+ /**
52
+ * Schemes that execute or embed code when navigated to or accepted as a
53
+ * redirect target. These are never safe as an OAuth `redirect_uri` or as a
54
+ * client-side navigation target (`window.location.href`, `location.assign`, ...).
55
+ */
56
+ export const DANGEROUS_URL_SCHEMES = ["javascript:", "data:", "vbscript:"];
57
+
58
+ /**
59
+ * Returns `false` only when `value` is an absolute URL using a dangerous scheme
60
+ * (`javascript:`, `data:`, `vbscript:`). Relative URLs (e.g. `/dashboard`) and
61
+ * safe absolute schemes (`http`, `https`, custom app schemes such as
62
+ * `myapp://`) return `true`.
63
+ *
64
+ * Use this to guard browser navigation sinks and any redirect target that may
65
+ * originate from untrusted input. It is intentionally narrow: it blocks code
66
+ * execution schemes without rejecting relative paths or mobile deep links.
67
+ */
68
+ export function isSafeUrlScheme(value: string): boolean {
69
+ let parsed: URL;
70
+ try {
71
+ parsed = new URL(value);
72
+ } catch {
73
+ // Relative URLs carry no scheme to abuse.
74
+ return true;
75
+ }
76
+ return !DANGEROUS_URL_SCHEMES.includes(parsed.protocol);
77
+ }