@better-auth/core 1.7.0-beta.9 → 1.7.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 (34) hide show
  1. package/dist/context/global.mjs +1 -1
  2. package/dist/db/adapter/factory.mjs +1 -0
  3. package/dist/db/adapter/index.d.mts +8 -2
  4. package/dist/db/get-tables.mjs +2 -1
  5. package/dist/error/index.d.mts +7 -0
  6. package/dist/instrumentation/tracer.mjs +1 -1
  7. package/dist/oauth2/client-credentials-token.mjs +2 -2
  8. package/dist/oauth2/refresh-access-token.mjs +2 -2
  9. package/dist/oauth2/reject-redirects.mjs +65 -0
  10. package/dist/oauth2/validate-authorization-code.mjs +11 -4
  11. package/dist/oauth2/verify.mjs +3 -3
  12. package/dist/social-providers/google.d.mts +23 -3
  13. package/dist/social-providers/google.mjs +56 -9
  14. package/dist/social-providers/index.d.mts +2 -2
  15. package/dist/social-providers/index.mjs +2 -2
  16. package/dist/social-providers/paypal.d.mts +1 -0
  17. package/dist/social-providers/paypal.mjs +15 -0
  18. package/dist/types/init-options.d.mts +16 -1
  19. package/dist/utils/ip.d.mts +23 -1
  20. package/dist/utils/ip.mjs +115 -1
  21. package/package.json +10 -10
  22. package/src/db/adapter/factory.ts +9 -4
  23. package/src/db/adapter/index.ts +8 -2
  24. package/src/db/get-tables.ts +7 -1
  25. package/src/error/index.ts +9 -0
  26. package/src/oauth2/client-credentials-token.ts +2 -2
  27. package/src/oauth2/refresh-access-token.ts +2 -2
  28. package/src/oauth2/reject-redirects.ts +75 -0
  29. package/src/oauth2/validate-authorization-code.ts +14 -4
  30. package/src/oauth2/verify.ts +20 -19
  31. package/src/social-providers/google.ts +103 -17
  32. package/src/social-providers/paypal.ts +22 -0
  33. package/src/types/init-options.ts +16 -1
  34. package/src/utils/ip.ts +185 -0
package/src/utils/ip.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as z from "zod";
2
+ import { isDevelopment, isTest } from "../env";
3
+ import type { BetterAuthOptions } from "../types";
2
4
 
3
5
  /**
4
6
  * Normalizes an IP address for consistent rate limiting.
@@ -195,6 +197,189 @@ export function normalizeIP(
195
197
  return normalizeIPv6(ip, subnetPrefix);
196
198
  }
197
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
+
198
383
  /**
199
384
  * Creates a rate limit key from IP and path
200
385
  * Uses a separator to prevent collision attacks