@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,295 @@
1
+ import { isValidIP, normalizeIP } from "./ip.mjs";
2
+ //#region src/utils/host.ts
3
+ /**
4
+ * Cloud provider instance metadata service FQDNs. These resolve to link-local
5
+ * IPs (usually `169.254.169.254`) inside their respective clouds and are
6
+ * prime SSRF targets.
7
+ *
8
+ * The IPs themselves are already caught by the `linkLocal` kind; this set
9
+ * only exists for the FQDN form that a naive server-side fetch might resolve
10
+ * via its own resolver.
11
+ */
12
+ const CLOUD_METADATA_HOSTS = new Set([
13
+ "metadata.google.internal",
14
+ "metadata.goog",
15
+ "metadata",
16
+ "instance-data",
17
+ "instance-data.ec2.internal"
18
+ ]);
19
+ /** Strip `[...]` if the entire input is bracketed (IPv6 literal form). */
20
+ function stripBrackets(host) {
21
+ if (host.length >= 2 && host.startsWith("[") && host.endsWith("]")) return host.slice(1, -1);
22
+ return host;
23
+ }
24
+ /**
25
+ * Strip trailing `:port` from host-with-port strings.
26
+ *
27
+ * - Bracketed IPv6 with port: `[::1]:8080` → `[::1]`
28
+ * - IPv4/FQDN with port: `127.0.0.1:3000` / `example.com:443` → base form
29
+ * - Bare IPv6: `::1` / `fe80::1` → unchanged (multiple colons means no port)
30
+ */
31
+ function stripPort(host) {
32
+ if (host.startsWith("[")) {
33
+ const end = host.indexOf("]");
34
+ if (end === -1) return host;
35
+ return host.slice(0, end + 1);
36
+ }
37
+ const firstColon = host.indexOf(":");
38
+ if (firstColon === -1) return host;
39
+ if (host.indexOf(":", firstColon + 1) !== -1) return host;
40
+ return host.slice(0, firstColon);
41
+ }
42
+ /** Strip IPv6 zone identifier: `fe80::1%eth0` → `fe80::1`. */
43
+ function stripZoneId(host) {
44
+ const zone = host.indexOf("%");
45
+ if (zone === -1) return host;
46
+ return host.slice(0, zone);
47
+ }
48
+ /**
49
+ * Strip trailing dots (RFC 1034 absolute DNS form): `localhost.` → `localhost`.
50
+ * Without this, `metadata.google.internal.` would fall through to `public` and
51
+ * bypass the cloud-metadata / `.localhost` checks, since WHATWG URL parsing
52
+ * preserves the trailing dot in `url.hostname`.
53
+ */
54
+ function stripTrailingDot(host) {
55
+ return host.replace(/\.+$/, "");
56
+ }
57
+ /** Fast dotted-decimal shape check. Does NOT validate octet bounds. */
58
+ function looksLikeIPv4(host) {
59
+ return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
60
+ }
61
+ /** Pack a validated dotted-decimal IPv4 into a 32-bit unsigned integer. */
62
+ function ipv4ToUint32(ip) {
63
+ const parts = ip.split(".");
64
+ return (Number(parts[0]) << 24 | Number(parts[1]) << 16 | Number(parts[2]) << 8 | Number(parts[3])) >>> 0;
65
+ }
66
+ /** Check whether a 32-bit value matches `prefix/length` (both unsigned). */
67
+ function inIPv4Range(value, prefix, length) {
68
+ if (length === 0) return true;
69
+ const mask = length === 32 ? 4294967295 : -1 << 32 - length >>> 0;
70
+ return (value & mask) === (prefix & mask);
71
+ }
72
+ function classifyIPv4(ip) {
73
+ if (ip === "0.0.0.0") return "unspecified";
74
+ if (ip === "255.255.255.255") return "broadcast";
75
+ const n = ipv4ToUint32(ip);
76
+ if (inIPv4Range(n, ipv4ToUint32("127.0.0.0"), 8)) return "loopback";
77
+ if (inIPv4Range(n, ipv4ToUint32("10.0.0.0"), 8)) return "private";
78
+ if (inIPv4Range(n, ipv4ToUint32("172.16.0.0"), 12)) return "private";
79
+ if (inIPv4Range(n, ipv4ToUint32("192.168.0.0"), 16)) return "private";
80
+ if (inIPv4Range(n, ipv4ToUint32("169.254.0.0"), 16)) return "linkLocal";
81
+ if (inIPv4Range(n, ipv4ToUint32("100.64.0.0"), 10)) return "sharedAddressSpace";
82
+ if (inIPv4Range(n, ipv4ToUint32("192.0.2.0"), 24)) return "documentation";
83
+ if (inIPv4Range(n, ipv4ToUint32("198.51.100.0"), 24)) return "documentation";
84
+ if (inIPv4Range(n, ipv4ToUint32("203.0.113.0"), 24)) return "documentation";
85
+ if (inIPv4Range(n, ipv4ToUint32("198.18.0.0"), 15)) return "benchmarking";
86
+ if (inIPv4Range(n, ipv4ToUint32("224.0.0.0"), 4)) return "multicast";
87
+ if (inIPv4Range(n, ipv4ToUint32("0.0.0.0"), 8)) return "reserved";
88
+ if (inIPv4Range(n, ipv4ToUint32("192.0.0.0"), 24)) return "reserved";
89
+ if (inIPv4Range(n, ipv4ToUint32("240.0.0.0"), 4)) return "reserved";
90
+ return "public";
91
+ }
92
+ /**
93
+ * Extract an IPv4 address embedded in an expanded IPv6 literal.
94
+ *
95
+ * Used to recurse into tunnel/translation forms (6to4, NAT64, Teredo) so a
96
+ * private destination cannot be smuggled behind a syntactically-public IPv6
97
+ * literal. `startGroup` is the index of the first of two 16-bit groups in the
98
+ * expanded form (`0000:0000:...`). With `xor: true`, the 32-bit value is XORed
99
+ * with `0xffffffff` before decoding (Teredo obfuscates the client IPv4 this
100
+ * way).
101
+ */
102
+ function extractEmbeddedIPv4(expanded, startGroup, options = {}) {
103
+ const offset = startGroup * 5;
104
+ const g1 = Number.parseInt(expanded.slice(offset, offset + 4), 16);
105
+ const g2 = Number.parseInt(expanded.slice(offset + 5, offset + 9), 16);
106
+ if (!Number.isFinite(g1) || !Number.isFinite(g2)) return null;
107
+ let combined = (g1 << 16 | g2) >>> 0;
108
+ if (options.xor) combined = (combined ^ 4294967295) >>> 0;
109
+ return `${combined >>> 24 & 255}.${combined >>> 16 & 255}.${combined >>> 8 & 255}.${combined & 255}`;
110
+ }
111
+ /**
112
+ * Classify an expanded, full-form, lowercase IPv6 address (no IPv4-mapped
113
+ * input — those are unmapped to IPv4 before reaching here).
114
+ *
115
+ * 6to4 (`2002::/16`), NAT64 (`64:ff9b::/96`) and Teredo (`2001:0000::/32`)
116
+ * embed an IPv4 that can route to private/loopback space. If the embedded
117
+ * IPv4 classifies as non-`public`, return `reserved` — blocks SSRF without
118
+ * advertising the address as a loopback literal for RFC 8252 §7.3 matching.
119
+ */
120
+ function classifyIPv6(expanded) {
121
+ if (expanded === "0000:0000:0000:0000:0000:0000:0000:0000") return "unspecified";
122
+ if (expanded === "0000:0000:0000:0000:0000:0000:0000:0001") return "loopback";
123
+ const firstByte = Number.parseInt(expanded.slice(0, 2), 16);
124
+ const secondByte = Number.parseInt(expanded.slice(2, 4), 16);
125
+ if (firstByte === 255) return "multicast";
126
+ if (firstByte === 254 && (secondByte & 192) === 128) return "linkLocal";
127
+ if ((firstByte & 254) === 252) return "private";
128
+ if (expanded.startsWith("2001:0db8:")) return "documentation";
129
+ if (expanded.startsWith("2001:0002:0000:")) return "benchmarking";
130
+ if (expanded.startsWith("2002:")) {
131
+ const embedded = extractEmbeddedIPv4(expanded, 1);
132
+ if (embedded && classifyIPv4(embedded) !== "public") return "reserved";
133
+ return "public";
134
+ }
135
+ if (expanded.startsWith("0064:ff9b:0000:0000:0000:0000:")) {
136
+ const embedded = extractEmbeddedIPv4(expanded, 6);
137
+ if (embedded && classifyIPv4(embedded) !== "public") return "reserved";
138
+ return "reserved";
139
+ }
140
+ if (expanded.startsWith("0064:ff9b:0001:")) return "reserved";
141
+ if (expanded.startsWith("2001:0000:")) {
142
+ const embedded = extractEmbeddedIPv4(expanded, 6, { xor: true });
143
+ if (embedded && classifyIPv4(embedded) !== "public") return "reserved";
144
+ return "reserved";
145
+ }
146
+ if (expanded.startsWith("0100:0000:0000:0000:")) return "reserved";
147
+ if (expanded.startsWith("3fff:0")) return "documentation";
148
+ if (expanded.startsWith("5f00:")) return "reserved";
149
+ return "public";
150
+ }
151
+ /**
152
+ * Classify a host string according to RFC 6890 / RFC 6761.
153
+ *
154
+ * Accepts inputs in any of these shapes and normalizes before classifying:
155
+ *
156
+ * - Bare IPv4: `127.0.0.1`
157
+ * - Bare IPv6: `::1`, `fe80::1%eth0`
158
+ * - Bracketed IPv6: `[::1]`
159
+ * - Host with port: `localhost:3000`, `127.0.0.1:443`, `[::1]:8080`
160
+ * - FQDN: `example.com`, `tenant.localhost`
161
+ * - IPv4-mapped IPv6: `::ffff:192.0.2.1` (reported as `literal: "ipv4"`)
162
+ *
163
+ * Invalid or non-resolvable FQDNs are returned as `{ kind: "public", literal: "fqdn" }`
164
+ * — this function never throws. Callers that need structural validation must
165
+ * combine this with a URL/hostname validator upstream.
166
+ *
167
+ * @example
168
+ * classifyHost("127.0.0.1")
169
+ * // { kind: "loopback", literal: "ipv4", canonical: "127.0.0.1" }
170
+ *
171
+ * @example
172
+ * classifyHost("[::1]:8080")
173
+ * // { kind: "loopback", literal: "ipv6", canonical: "0000:0000:...:0001" }
174
+ *
175
+ * @example
176
+ * classifyHost("::ffff:192.0.2.1")
177
+ * // { kind: "documentation", literal: "ipv4", canonical: "192.0.2.1" }
178
+ *
179
+ * @example
180
+ * classifyHost("tenant-a.localhost")
181
+ * // { kind: "localhost", literal: "fqdn", canonical: "tenant-a.localhost" }
182
+ */
183
+ function classifyHost(host) {
184
+ const lowered = stripTrailingDot(stripZoneId(stripBrackets(stripPort(host.trim())))).toLowerCase();
185
+ if (lowered === "") return {
186
+ kind: "reserved",
187
+ literal: "fqdn",
188
+ canonical: ""
189
+ };
190
+ if (!isValidIP(lowered)) {
191
+ if (lowered === "localhost" || lowered.endsWith(".localhost")) return {
192
+ kind: "localhost",
193
+ literal: "fqdn",
194
+ canonical: lowered
195
+ };
196
+ if (CLOUD_METADATA_HOSTS.has(lowered)) return {
197
+ kind: "cloudMetadata",
198
+ literal: "fqdn",
199
+ canonical: lowered
200
+ };
201
+ return {
202
+ kind: "public",
203
+ literal: "fqdn",
204
+ canonical: lowered
205
+ };
206
+ }
207
+ if (looksLikeIPv4(lowered)) return {
208
+ kind: classifyIPv4(lowered),
209
+ literal: "ipv4",
210
+ canonical: lowered
211
+ };
212
+ const canonical = normalizeIP(lowered, { ipv6Subnet: 128 });
213
+ if (looksLikeIPv4(canonical)) return {
214
+ kind: classifyIPv4(canonical),
215
+ literal: "ipv4",
216
+ canonical
217
+ };
218
+ return {
219
+ kind: classifyIPv6(canonical),
220
+ literal: "ipv6",
221
+ canonical
222
+ };
223
+ }
224
+ /**
225
+ * Strict loopback-IP-literal check per RFC 8252 §7.3.
226
+ *
227
+ * Returns true ONLY for IPv4 `127.0.0.0/8` or IPv6 `::1`. The DNS name
228
+ * `localhost` returns false — RFC 8252 §8.3 explicitly recommends against
229
+ * relying on name resolution for loopback redirect URIs.
230
+ *
231
+ * Use this for OAuth redirect URI matching.
232
+ *
233
+ * @example
234
+ * isLoopbackIP("127.0.0.1") // true
235
+ * isLoopbackIP("::1") // true
236
+ * isLoopbackIP("[::1]:8080") // true
237
+ * isLoopbackIP("localhost") // false (use isLoopbackHost for DNS names)
238
+ * isLoopbackIP("0.0.0.0") // false (unspecified, not loopback)
239
+ */
240
+ function isLoopbackIP(host) {
241
+ return classifyHost(host).kind === "loopback";
242
+ }
243
+ /**
244
+ * Permissive loopback check for developer-ergonomics code paths.
245
+ *
246
+ * Returns true for IPv4 `127.0.0.0/8`, IPv6 `::1`, the literal name `localhost`,
247
+ * and any RFC 6761 `.localhost` subdomain (`tenant.localhost`, `app.localhost`).
248
+ *
249
+ * Use this for things like: allowing HTTP for dev servers, skipping Secure
250
+ * cookie requirements, browser-trust heuristics. Do NOT use this for OAuth
251
+ * redirect URI matching — use {@link isLoopbackIP} there.
252
+ *
253
+ * @example
254
+ * isLoopbackHost("localhost") // true
255
+ * isLoopbackHost("tenant.localhost") // true (RFC 6761)
256
+ * isLoopbackHost("127.0.0.1") // true
257
+ * isLoopbackHost("0.0.0.0") // false (unspecified, NOT loopback)
258
+ */
259
+ function isLoopbackHost(host) {
260
+ const kind = classifyHost(host).kind;
261
+ return kind === "loopback" || kind === "localhost";
262
+ }
263
+ /**
264
+ * First-line SSRF gate: returns true ONLY for hosts that classify as `public`.
265
+ *
266
+ * Every RFC 6890 special-purpose range (loopback, private, link-local,
267
+ * unspecified, documentation, multicast, broadcast, reserved, shared address
268
+ * space, benchmarking) and cloud-metadata FQDN returns false.
269
+ *
270
+ * Use this BEFORE issuing a server-side fetch to a user-supplied URL, e.g.
271
+ * OAuth introspection endpoints, webhook targets, or metadata-document
272
+ * fetches (CIMD).
273
+ *
274
+ * Limitations (this is a syntactic check, not a complete SSRF mitigation):
275
+ * - No DNS resolution: a public-looking FQDN that resolves to a private IP
276
+ * passes this check. Re-verify the resolved address before connecting, or
277
+ * pin the socket to the resolved IP.
278
+ * - No DNS-rebinding defense: attackers can return a public IP on the first
279
+ * lookup and a private IP on the second. Resolve once and reuse the IP.
280
+ * - No redirect following: HTTP 3xx responses can redirect to private hosts.
281
+ * Re-run this check on every redirect target, or disable auto-follow.
282
+ *
283
+ * @example
284
+ * isPublicRoutableHost("example.com") // true
285
+ * isPublicRoutableHost("127.0.0.1") // false (loopback)
286
+ * isPublicRoutableHost("169.254.169.254") // false (linkLocal / AWS IMDS)
287
+ * isPublicRoutableHost("metadata.google.internal") // false (cloudMetadata)
288
+ * isPublicRoutableHost("10.0.0.1") // false (private)
289
+ * isPublicRoutableHost("::ffff:127.0.0.1") // false (mapped loopback)
290
+ */
291
+ function isPublicRoutableHost(host) {
292
+ return classifyHost(host).kind === "public";
293
+ }
294
+ //#endregion
295
+ export { classifyHost, isLoopbackHost, isLoopbackIP, isPublicRoutableHost };
@@ -0,0 +1,4 @@
1
+ //#region src/utils/id.d.ts
2
+ declare const generateId: (size?: number) => string;
3
+ //#endregion
4
+ export { generateId };
@@ -0,0 +1,7 @@
1
+ import { createRandomStringGenerator } from "@better-auth/utils/random";
2
+ //#region src/utils/id.ts
3
+ const generateId = (size) => {
4
+ return createRandomStringGenerator("a-z", "A-Z", "0-9")(size || 32);
5
+ };
6
+ //#endregion
7
+ export { generateId };
@@ -0,0 +1,77 @@
1
+ import { BetterAuthOptions } from "../types/init-options.mjs";
2
+ //#region src/utils/ip.d.ts
3
+ /**
4
+ * Normalizes an IP address for consistent rate limiting.
5
+ *
6
+ * Features:
7
+ * - Normalizes IPv6 to canonical lowercase form
8
+ * - Converts IPv4-mapped IPv6 to IPv4
9
+ * - Supports IPv6 subnet extraction
10
+ * - Handles all edge cases (::1, ::, etc.)
11
+ */
12
+ interface NormalizeIPOptions {
13
+ /**
14
+ * Prefix length used to collapse IPv6 addresses before keying.
15
+ * Any integer from 0 to 128 is accepted. Common values: 32, 48, 56, 64, 128.
16
+ * Values outside 0-128 are clamped.
17
+ *
18
+ * @default 64
19
+ */
20
+ ipv6Subnet?: number;
21
+ }
22
+ /**
23
+ * Checks if an IP is valid IPv4 or IPv6
24
+ */
25
+ declare function isValidIP(ip: string): boolean;
26
+ /**
27
+ * Normalizes an IP address (IPv4 or IPv6) for consistent rate limiting.
28
+ *
29
+ * @param ip - The IP address to normalize
30
+ * @param options - Normalization options
31
+ * @returns Normalized IP address
32
+ *
33
+ * @example
34
+ * normalizeIP("2001:DB8::1")
35
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0000"
36
+ *
37
+ * @example
38
+ * normalizeIP("::ffff:192.0.2.1")
39
+ * // -> "192.0.2.1" (converted to IPv4)
40
+ *
41
+ * @example
42
+ * normalizeIP("2001:db8::1", { ipv6Subnet: 64 })
43
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0000" (subnet /64)
44
+ */
45
+ declare function normalizeIP(ip: string, options?: NormalizeIPOptions): string;
46
+ /**
47
+ * Trusted-proxy entries that are not a valid IP address or CIDR range.
48
+ */
49
+ declare function findInvalidTrustedProxies(entries: string[]): string[];
50
+ /**
51
+ * Resolves the client IP from a forwarded header. The leftmost token is spoofable,
52
+ * so with `trustedProxies` the chain is stripped from the right to the first
53
+ * untrusted hop. Otherwise only a single-value header is trusted. Returns `null`
54
+ * when no trustworthy client IP can be resolved.
55
+ */
56
+ declare function getIPFromHeader(value: string, options?: {
57
+ ipv6Subnet?: number;
58
+ trustedProxies?: string[];
59
+ }): string | null;
60
+ /**
61
+ * Resolves the client IP for a request from the configured IP headers.
62
+ * Honors `disableIpTracking`, walks `ipAddressHeaders` in order (default
63
+ * `x-forwarded-for`), and falls back to localhost in development and test.
64
+ * Returns `null` when tracking is disabled or no trustworthy IP can be resolved.
65
+ */
66
+ declare function getIp(req: Request | Headers, options: BetterAuthOptions): string | null;
67
+ /**
68
+ * Creates a rate limit key from IP and path
69
+ * Uses a separator to prevent collision attacks
70
+ *
71
+ * @param ip - The IP address (should be normalized)
72
+ * @param path - The request path
73
+ * @returns Rate limit key
74
+ */
75
+ declare function createRateLimitKey(ip: string, path: string): string;
76
+ //#endregion
77
+ export { createRateLimitKey, findInvalidTrustedProxies, getIPFromHeader, getIp, isValidIP, normalizeIP };
@@ -0,0 +1,230 @@
1
+ import { isDevelopment, isTest } from "../env/env-impl.mjs";
2
+ import * as z from "zod";
3
+ //#region src/utils/ip.ts
4
+ /**
5
+ * Checks if an IP is valid IPv4 or IPv6
6
+ */
7
+ function isValidIP(ip) {
8
+ return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
9
+ }
10
+ /**
11
+ * Checks if an IP is IPv6
12
+ */
13
+ function isIPv6(ip) {
14
+ return z.ipv6().safeParse(ip).success;
15
+ }
16
+ /**
17
+ * Converts IPv4-mapped IPv6 address to IPv4
18
+ * e.g., "::ffff:192.0.2.1" -> "192.0.2.1"
19
+ */
20
+ function extractIPv4FromMapped(ipv6) {
21
+ const lower = ipv6.toLowerCase();
22
+ if (lower.startsWith("::ffff:")) {
23
+ const ipv4Part = lower.substring(7);
24
+ if (z.ipv4().safeParse(ipv4Part).success) return ipv4Part;
25
+ }
26
+ const parts = ipv6.split(":");
27
+ if (parts.length === 7 && parts[5]?.toLowerCase() === "ffff") {
28
+ const ipv4Part = parts[6];
29
+ if (ipv4Part && z.ipv4().safeParse(ipv4Part).success) return ipv4Part;
30
+ }
31
+ if (lower.includes("::ffff:") || lower.includes(":ffff:")) {
32
+ const groups = expandIPv6(ipv6);
33
+ if (groups.length === 8 && groups[0] === "0000" && groups[1] === "0000" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "ffff" && groups[6] && groups[7]) return `${Number.parseInt(groups[6].substring(0, 2), 16)}.${Number.parseInt(groups[6].substring(2, 4), 16)}.${Number.parseInt(groups[7].substring(0, 2), 16)}.${Number.parseInt(groups[7].substring(2, 4), 16)}`;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Expands a compressed IPv6 address to full form
39
+ * e.g., "2001:db8::1" -> ["2001", "0db8", "0000", "0000", "0000", "0000", "0000", "0001"]
40
+ */
41
+ function expandIPv6(ipv6) {
42
+ if (ipv6.includes("::")) {
43
+ const sides = ipv6.split("::");
44
+ const left = sides[0] ? sides[0].split(":") : [];
45
+ const right = sides[1] ? sides[1].split(":") : [];
46
+ const missingGroups = 8 - left.length - right.length;
47
+ const zeros = Array(missingGroups).fill("0000");
48
+ const paddedLeft = left.map((g) => g.padStart(4, "0"));
49
+ const paddedRight = right.map((g) => g.padStart(4, "0"));
50
+ return [
51
+ ...paddedLeft,
52
+ ...zeros,
53
+ ...paddedRight
54
+ ];
55
+ }
56
+ return ipv6.split(":").map((g) => g.padStart(4, "0"));
57
+ }
58
+ /**
59
+ * Normalizes an IPv6 address to canonical form
60
+ * e.g., "2001:DB8::1" -> "2001:0db8:0000:0000:0000:0000:0000:0001"
61
+ */
62
+ function normalizeIPv6(ipv6, subnetPrefix) {
63
+ const groups = expandIPv6(ipv6);
64
+ if (subnetPrefix !== void 0 && subnetPrefix < 128) {
65
+ let bitsRemaining = Math.max(0, Math.floor(subnetPrefix));
66
+ return groups.map((group) => {
67
+ if (bitsRemaining <= 0) return "0000";
68
+ if (bitsRemaining >= 16) {
69
+ bitsRemaining -= 16;
70
+ return group;
71
+ }
72
+ const masked = Number.parseInt(group, 16) & (65535 << 16 - bitsRemaining & 65535);
73
+ bitsRemaining = 0;
74
+ return masked.toString(16).padStart(4, "0");
75
+ }).join(":").toLowerCase();
76
+ }
77
+ return groups.join(":").toLowerCase();
78
+ }
79
+ /**
80
+ * Normalizes an IP address (IPv4 or IPv6) for consistent rate limiting.
81
+ *
82
+ * @param ip - The IP address to normalize
83
+ * @param options - Normalization options
84
+ * @returns Normalized IP address
85
+ *
86
+ * @example
87
+ * normalizeIP("2001:DB8::1")
88
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0000"
89
+ *
90
+ * @example
91
+ * normalizeIP("::ffff:192.0.2.1")
92
+ * // -> "192.0.2.1" (converted to IPv4)
93
+ *
94
+ * @example
95
+ * normalizeIP("2001:db8::1", { ipv6Subnet: 64 })
96
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0000" (subnet /64)
97
+ */
98
+ function normalizeIP(ip, options = {}) {
99
+ if (z.ipv4().safeParse(ip).success) return ip.toLowerCase();
100
+ if (!isIPv6(ip)) return ip.toLowerCase();
101
+ const ipv4 = extractIPv4FromMapped(ip);
102
+ if (ipv4) return ipv4.toLowerCase();
103
+ return normalizeIPv6(ip, options.ipv6Subnet ?? 64);
104
+ }
105
+ /**
106
+ * Raw bytes of an IP for CIDR comparison. Returns `null` for an invalid IP.
107
+ */
108
+ function ipToBytes(ip) {
109
+ if (z.ipv4().safeParse(ip).success) return Uint8Array.from(ip.split(".").map((octet) => Number(octet)));
110
+ if (!isIPv6(ip)) return null;
111
+ const mapped = extractIPv4FromMapped(ip);
112
+ if (mapped) return Uint8Array.from(mapped.split(".").map((octet) => Number(octet)));
113
+ const groups = expandIPv6(ip);
114
+ const bytes = new Uint8Array(16);
115
+ for (let i = 0; i < 8; i++) {
116
+ const group = Number.parseInt(groups[i] ?? "0", 16);
117
+ bytes[i * 2] = group >> 8 & 255;
118
+ bytes[i * 2 + 1] = group & 255;
119
+ }
120
+ return bytes;
121
+ }
122
+ const CIDR_PREFIX_PATTERN = /^\d+$/;
123
+ /**
124
+ * Parses an IP or `IP/prefix` string into network bytes and a prefix length.
125
+ * The prefix must be digits only and within the address family. `null` if the
126
+ * value is not a valid IP or CIDR range, which keeps a malformed entry from
127
+ * silently behaving like a non-match.
128
+ */
129
+ function parseCIDR(value) {
130
+ const slash = value.lastIndexOf("/");
131
+ const bytes = ipToBytes(slash === -1 ? value : value.slice(0, slash));
132
+ if (!bytes) return null;
133
+ const maxBits = bytes.length * 8;
134
+ if (slash === -1) return {
135
+ bytes,
136
+ prefix: maxBits
137
+ };
138
+ const prefixPart = value.slice(slash + 1);
139
+ if (!CIDR_PREFIX_PATTERN.test(prefixPart)) return null;
140
+ const prefix = Number(prefixPart);
141
+ return prefix <= maxBits ? {
142
+ bytes,
143
+ prefix
144
+ } : null;
145
+ }
146
+ /**
147
+ * Whether `ipBytes` falls inside an already-parsed CIDR network.
148
+ */
149
+ function matchesCIDR(ipBytes, net) {
150
+ if (ipBytes.length !== net.bytes.length) return false;
151
+ let bitsRemaining = net.prefix;
152
+ for (let i = 0; i < ipBytes.length && bitsRemaining > 0; i++) {
153
+ const take = bitsRemaining >= 8 ? 8 : bitsRemaining;
154
+ const mask = take === 8 ? 255 : 255 << 8 - take & 255;
155
+ if (((ipBytes[i] ?? 0) & mask) !== ((net.bytes[i] ?? 0) & mask)) return false;
156
+ bitsRemaining -= 8;
157
+ }
158
+ return true;
159
+ }
160
+ /**
161
+ * Trusted-proxy entries that are not a valid IP address or CIDR range.
162
+ */
163
+ function findInvalidTrustedProxies(entries) {
164
+ return entries.filter((entry) => parseCIDR(entry) === null);
165
+ }
166
+ /**
167
+ * Resolves the client IP from a forwarded header. The leftmost token is spoofable,
168
+ * so with `trustedProxies` the chain is stripped from the right to the first
169
+ * untrusted hop. Otherwise only a single-value header is trusted. Returns `null`
170
+ * when no trustworthy client IP can be resolved.
171
+ */
172
+ function getIPFromHeader(value, options = {}) {
173
+ const forwardedIps = value.split(",").map((ip) => ip.trim()).filter(Boolean);
174
+ if (forwardedIps.length === 0) return null;
175
+ const trustedProxies = (options.trustedProxies ?? []).map(parseCIDR).filter((proxy) => {
176
+ return proxy !== null;
177
+ });
178
+ if (trustedProxies.length > 0) {
179
+ for (let i = forwardedIps.length - 1; i >= 0; i--) {
180
+ const ip = forwardedIps[i];
181
+ const ipBytes = ip ? ipToBytes(ip) : null;
182
+ if (!ip || !ipBytes) return null;
183
+ if (trustedProxies.some((proxy) => matchesCIDR(ipBytes, proxy))) continue;
184
+ return normalizeIP(ip, { ipv6Subnet: options.ipv6Subnet });
185
+ }
186
+ return null;
187
+ }
188
+ if (forwardedIps.length !== 1) return null;
189
+ const selectedIp = forwardedIps[0];
190
+ if (!selectedIp || !isValidIP(selectedIp)) return null;
191
+ return normalizeIP(selectedIp, { ipv6Subnet: options.ipv6Subnet });
192
+ }
193
+ const LOCALHOST_IP = "127.0.0.1";
194
+ const DEFAULT_IP_HEADERS = ["x-forwarded-for"];
195
+ /**
196
+ * Resolves the client IP for a request from the configured IP headers.
197
+ * Honors `disableIpTracking`, walks `ipAddressHeaders` in order (default
198
+ * `x-forwarded-for`), and falls back to localhost in development and test.
199
+ * Returns `null` when tracking is disabled or no trustworthy IP can be resolved.
200
+ */
201
+ function getIp(req, options) {
202
+ if (options.advanced?.ipAddress?.disableIpTracking) return null;
203
+ const headers = "headers" in req ? req.headers : req;
204
+ const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || DEFAULT_IP_HEADERS;
205
+ for (const key of ipHeaders) {
206
+ const value = "get" in headers ? headers.get(key) : headers[key];
207
+ if (typeof value === "string") {
208
+ const ip = getIPFromHeader(value, {
209
+ ipv6Subnet: options.advanced?.ipAddress?.ipv6Subnet,
210
+ trustedProxies: options.advanced?.ipAddress?.trustedProxies
211
+ });
212
+ if (ip) return ip;
213
+ }
214
+ }
215
+ if (isTest() || isDevelopment()) return LOCALHOST_IP;
216
+ return null;
217
+ }
218
+ /**
219
+ * Creates a rate limit key from IP and path
220
+ * Uses a separator to prevent collision attacks
221
+ *
222
+ * @param ip - The IP address (should be normalized)
223
+ * @param path - The request path
224
+ * @returns Rate limit key
225
+ */
226
+ function createRateLimitKey(ip, path) {
227
+ return `${ip}|${path}`;
228
+ }
229
+ //#endregion
230
+ export { createRateLimitKey, findInvalidTrustedProxies, getIPFromHeader, getIp, isValidIP, normalizeIP };
@@ -0,0 +1,6 @@
1
+ import { APIError } from "../error/index.mjs";
2
+
3
+ //#region src/utils/is-api-error.d.ts
4
+ declare function isAPIError(error: unknown): error is APIError;
5
+ //#endregion
6
+ export { isAPIError };
@@ -0,0 +1,8 @@
1
+ import { APIError as APIError$1 } from "../error/index.mjs";
2
+ import { APIError } from "better-call";
3
+ //#region src/utils/is-api-error.ts
4
+ function isAPIError(error) {
5
+ return error instanceof APIError || error instanceof APIError$1 || error?.name === "APIError";
6
+ }
7
+ //#endregion
8
+ export { isAPIError };
@@ -0,0 +1,4 @@
1
+ //#region src/utils/json.d.ts
2
+ declare function safeJSONParse<T>(data: unknown): T | null;
3
+ //#endregion
4
+ export { safeJSONParse };
@@ -0,0 +1,41 @@
1
+ import { logger } from "../env/logger.mjs";
2
+ //#region src/utils/json.ts
3
+ const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
4
+ function reviveDate(value) {
5
+ if (typeof value === "string" && iso8601Regex.test(value)) {
6
+ const date = new Date(value);
7
+ if (!isNaN(date.getTime())) return date;
8
+ }
9
+ return value;
10
+ }
11
+ /**
12
+ * Recursively walk a pre-parsed object and convert ISO 8601 date strings
13
+ * to Date instances. This handles the case where a Redis client (or similar)
14
+ * returns already-parsed JSON objects whose date fields are still strings.
15
+ */
16
+ function reviveDates(value) {
17
+ if (value === null || value === void 0) return value;
18
+ if (typeof value === "string") return reviveDate(value);
19
+ if (value instanceof Date) return value;
20
+ if (Array.isArray(value)) return value.map(reviveDates);
21
+ if (typeof value === "object") {
22
+ const result = {};
23
+ for (const key of Object.keys(value)) result[key] = reviveDates(value[key]);
24
+ return result;
25
+ }
26
+ return value;
27
+ }
28
+ function safeJSONParse(data) {
29
+ try {
30
+ if (typeof data !== "string") {
31
+ if (data === null || data === void 0) return null;
32
+ return reviveDates(data);
33
+ }
34
+ return JSON.parse(data, (_, value) => reviveDate(value));
35
+ } catch (e) {
36
+ logger.error("Error parsing JSON", { error: e });
37
+ return null;
38
+ }
39
+ }
40
+ //#endregion
41
+ export { safeJSONParse };