diff --git a/docs/content/docs/concepts/rate-limit.mdx b/docs/content/docs/concepts/rate-limit.mdx
index 736c754b3b..64c8912445 100644
--- a/docs/content/docs/concepts/rate-limit.mdx
+++ b/docs/content/docs/concepts/rate-limit.mdx
@@ -52,7 +52,7 @@ Rate limiting uses the connecting IP address to track the number of requests mad
default header checked is `x-forwarded-for`, which is commonly used in production environments. If
you are using a different header to track the user's IP address, you'll need to specify it.
-```ts title="auth.ts"
+```ts title="auth.ts"
export const auth = betterAuth({
//...other options
advanced: {
@@ -68,6 +68,44 @@ export const auth = betterAuth({
})
```
+#### IPv6 Address Support
+
+Better Auth automatically normalizes IPv6 addresses to prevent bypass attacks where attackers use different representations of the same IPv6 address (e.g., `2001:db8::1` vs `2001:0db8:0000:0000:0000:0000:0000:0001`). This ensures that all representations of the same IPv6 address are treated as the same for rate limiting purposes.
+
+Additionally, IPv4-mapped IPv6 addresses (e.g., `::ffff:192.0.2.1`) are automatically converted to their IPv4 form (`192.0.2.1`) to prevent attackers from bypassing rate limits by switching between IPv4 and IPv6 representations.
+
+#### IPv6 Subnet Rate Limiting
+
+By default, IPv6 addresses are rate limited individually (using the full /128 address). However, since IPv6 typically allocates large address blocks to single users, attackers could potentially bypass rate limits by rotating through multiple IPv6 addresses from their allocation.
+
+To prevent this, you can configure rate limiting to apply to IPv6 subnets instead of individual addresses:
+
+```ts title="auth.ts"
+export const auth = betterAuth({
+ //...other options
+ advanced: {
+ ipAddress: {
+ ipv6Subnet: 64, // Rate limit by /64 subnet instead of individual addresses
+ },
+ },
+ rateLimit: {
+ enabled: true,
+ window: 60,
+ max: 100,
+ },
+})
+```
+
+Common IPv6 subnet prefix lengths:
+- `128` (default): Individual IPv6 address - most restrictive
+- `64`: /64 subnet - typical home/business allocation
+- `48`: /48 subnet - larger network allocation
+- `32`: /32 subnet - ISP-level allocation
+
+
+IPv6 subnet configuration only affects IPv6 addresses. IPv4 addresses are always rate limited individually.
+
+
### Rate Limit Window
```ts title="auth.ts"
diff --git a/e2e/smoke/test/fixtures/ipv6/index.ts b/e2e/smoke/test/fixtures/ipv6/index.ts
new file mode 100644
index 0000000000..0496035e0c
--- /dev/null
+++ b/e2e/smoke/test/fixtures/ipv6/index.ts
@@ -0,0 +1,46 @@
+import { DatabaseSync } from "node:sqlite";
+import { serve } from "@hono/node-server";
+import { betterAuth } from "better-auth";
+import { getMigrations } from "better-auth/db";
+import { Hono } from "hono";
+
+const database = new DatabaseSync(":memory:");
+
+export const auth = betterAuth({
+ baseURL: "http://localhost:3000",
+ database,
+ emailAndPassword: {
+ enabled: true,
+ },
+ trustedOrigins: [
+ "http://localhost:*", // Allow any localhost port for smoke tests
+ ],
+ rateLimit: {
+ enabled: true,
+ window: 60,
+ max: 3,
+ },
+ advanced: {
+ ipAddress: {
+ ipv6Subnet: 64, // Group IPv6 addresses by /64 subnet
+ },
+ },
+});
+
+const { runMigrations } = await getMigrations(auth.options);
+
+await runMigrations();
+
+const app = new Hono();
+
+app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));
+
+serve(
+ {
+ fetch: app.fetch,
+ port: 0,
+ },
+ (info) => {
+ console.log(info.port);
+ },
+);
diff --git a/e2e/smoke/test/fixtures/ipv6/package.json b/e2e/smoke/test/fixtures/ipv6/package.json
new file mode 100644
index 0000000000..fdfce65043
--- /dev/null
+++ b/e2e/smoke/test/fixtures/ipv6/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "fixtures-ipv6",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9",
+ "better-auth": "workspace:*",
+ "hono": "^4.11.4"
+ }
+}
diff --git a/e2e/smoke/test/fixtures/ipv6/tsconfig.json b/e2e/smoke/test/fixtures/ipv6/tsconfig.json
new file mode 100644
index 0000000000..6e3473bbab
--- /dev/null
+++ b/e2e/smoke/test/fixtures/ipv6/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "skipLibCheck": true,
+ "strict": true,
+ "module": "esnext",
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/e2e/smoke/test/ipv6.spec.ts b/e2e/smoke/test/ipv6.spec.ts
new file mode 100644
index 0000000000..82d47d0dbb
--- /dev/null
+++ b/e2e/smoke/test/ipv6.spec.ts
@@ -0,0 +1,205 @@
+import assert from "node:assert/strict";
+import { spawn } from "node:child_process";
+import { join } from "node:path";
+import { describe, it } from "node:test";
+import { fileURLToPath } from "node:url";
+
+const fixturesDir = fileURLToPath(new URL("./fixtures", import.meta.url));
+
+const nodejsWarnings = ["ExperimentalWarning"];
+
+describe("IPv6 rate limiting", () => {
+ const entryFile = join(fixturesDir, "ipv6", "index.ts");
+ it("should group IPv6 addresses from same /64 subnet", async (t) => {
+ const cp = spawn("node", [entryFile], {
+ stdio: "pipe",
+ });
+ t.after(() => {
+ cp.kill("SIGINT");
+ });
+ cp.stderr.on("data", (data) => {
+ console.error(data.toString());
+ });
+ const port = await new Promise((resolve) => {
+ cp.stdout.on("data", (data) => {
+ const output = data.toString().replace(/\u001b\[[0-9;]*m/g, "");
+ if (nodejsWarnings.some((warning) => output.includes(warning))) {
+ return;
+ }
+ const port = +output;
+ assert.ok(port > 0);
+ assert.ok(!Number.isNaN(port));
+ assert.ok(Number.isFinite(port));
+ resolve(port);
+ });
+ });
+
+ // Different IPv6 addresses from the same /64 subnet
+ // 2001:db8:abcd:1234::/64 prefix
+ const ipv6Addresses = [
+ "2001:db8:abcd:1234:0000:0000:0000:0001",
+ "2001:db8:abcd:1234:1111:2222:3333:4444",
+ "2001:db8:abcd:1234:ffff:ffff:ffff:ffff",
+ ];
+
+ // Make requests with different IPv6 addresses from same /64
+ // Rate limit is max 3 requests, so 4th should be blocked
+ for (let i = 0; i < 4; i++) {
+ const ipv6 = ipv6Addresses[i % ipv6Addresses.length]!;
+ const response = await fetch(
+ `http://localhost:${port}/api/auth/sign-in/email`,
+ {
+ method: "POST",
+ body: JSON.stringify({
+ email: "test@test.com",
+ password: "password",
+ }),
+ headers: {
+ "content-type": "application/json",
+ origin: `http://localhost:${port}`,
+ "x-forwarded-for": ipv6,
+ },
+ },
+ );
+
+ if (i >= 3) {
+ assert.equal(
+ response.status,
+ 429,
+ `Request ${i + 1} with IP ${ipv6} should be rate limited (429)`,
+ );
+ } else {
+ assert.notEqual(
+ response.status,
+ 429,
+ `Request ${i + 1} with IP ${ipv6} should not be rate limited`,
+ );
+ }
+ }
+ });
+
+ it("should not group IPv6 addresses from different /64 subnets", async (t) => {
+ const cp = spawn("node", [entryFile], {
+ stdio: "pipe",
+ });
+ t.after(() => {
+ cp.kill("SIGINT");
+ });
+ cp.stderr.on("data", (data) => {
+ console.error(data.toString());
+ });
+ const port = await new Promise((resolve) => {
+ cp.stdout.on("data", (data) => {
+ const output = data.toString().replace(/\u001b\[[0-9;]*m/g, "");
+ if (nodejsWarnings.some((warning) => output.includes(warning))) {
+ return;
+ }
+ const port = +output;
+ assert.ok(port > 0);
+ assert.ok(!Number.isNaN(port));
+ assert.ok(Number.isFinite(port));
+ resolve(port);
+ });
+ });
+
+ // Different /64 subnets
+ const differentSubnets = [
+ "2001:db8:abcd:1111:0000:0000:0000:0001", // /64 subnet 1
+ "2001:db8:abcd:2222:0000:0000:0000:0001", // /64 subnet 2
+ "2001:db8:abcd:3333:0000:0000:0000:0001", // /64 subnet 3
+ ];
+
+ // Each subnet should have its own rate limit counter
+ // So 3 requests from 3 different subnets should all succeed
+ for (const ipv6 of differentSubnets) {
+ const response = await fetch(
+ `http://localhost:${port}/api/auth/sign-in/email`,
+ {
+ method: "POST",
+ body: JSON.stringify({
+ email: "test@test.com",
+ password: "password",
+ }),
+ headers: {
+ "content-type": "application/json",
+ origin: `http://localhost:${port}`,
+ "x-forwarded-for": ipv6,
+ },
+ },
+ );
+
+ assert.notEqual(
+ response.status,
+ 429,
+ `Request with IP ${ipv6} should not be rate limited (different subnet)`,
+ );
+ }
+ });
+
+ it("should normalize different IPv6 representations", async (t) => {
+ const cp = spawn("node", [entryFile], {
+ stdio: "pipe",
+ });
+ t.after(() => {
+ cp.kill("SIGINT");
+ });
+ cp.stderr.on("data", (data) => {
+ console.error(data.toString());
+ });
+ const port = await new Promise((resolve) => {
+ cp.stdout.on("data", (data) => {
+ const output = data.toString().replace(/\u001b\[[0-9;]*m/g, "");
+ if (nodejsWarnings.some((warning) => output.includes(warning))) {
+ return;
+ }
+ const port = +output;
+ assert.ok(port > 0);
+ assert.ok(!Number.isNaN(port));
+ assert.ok(Number.isFinite(port));
+ resolve(port);
+ });
+ });
+
+ // Different representations of the same IPv6 address
+ const sameAddressDifferentFormats = [
+ "2001:db8::1",
+ "2001:DB8::1", // uppercase
+ "2001:0db8::1", // leading zeros
+ "2001:db8:0:0:0:0:0:1", // expanded
+ ];
+
+ // All should be normalized to the same address and share rate limit
+ for (let i = 0; i < 4; i++) {
+ const ipv6 = sameAddressDifferentFormats[i]!;
+ const response = await fetch(
+ `http://localhost:${port}/api/auth/sign-in/email`,
+ {
+ method: "POST",
+ body: JSON.stringify({
+ email: "test@test.com",
+ password: "password",
+ }),
+ headers: {
+ "content-type": "application/json",
+ origin: `http://localhost:${port}`,
+ "x-forwarded-for": ipv6,
+ },
+ },
+ );
+
+ if (i >= 3) {
+ assert.equal(
+ response.status,
+ 429,
+ `Request ${i + 1} with IP ${ipv6} should be rate limited (same address)`,
+ );
+ } else {
+ assert.notEqual(
+ response.status,
+ 429,
+ `Request ${i + 1} with IP ${ipv6} should not be rate limited`,
+ );
+ }
+ }
+ });
+});
diff --git a/packages/better-auth/src/api/rate-limiter/index.ts b/packages/better-auth/src/api/rate-limiter/index.ts
index f57be41998..a739afe311 100644
--- a/packages/better-auth/src/api/rate-limiter/index.ts
+++ b/packages/better-auth/src/api/rate-limiter/index.ts
@@ -1,5 +1,5 @@
import type { AuthContext } from "@better-auth/core";
-import { safeJSONParse } from "@better-auth/core/utils";
+import { createRateLimitKey, safeJSONParse } from "@better-auth/core/utils";
import type { RateLimit } from "../../types";
import { getIp } from "../../utils/get-request-ip";
import { wildcardMatch } from "../../utils/wildcard";
@@ -164,7 +164,7 @@ export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
if (!ip) {
return;
}
- const key = ip + path;
+ const key = createRateLimitKey(ip, path);
const specialRules = getDefaultSpecialRules();
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
diff --git a/packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts b/packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts
index bd96f0f4ed..911da7e351 100644
--- a/packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts
+++ b/packages/better-auth/src/api/rate-limiter/rate-limiter.test.ts
@@ -1,110 +1,105 @@
+import { normalizeIP } from "@better-auth/core/utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getTestInstance } from "../../test-utils/test-instance";
import type { RateLimit } from "../../types";
-describe(
- "rate-limiter",
- {
- timeout: 10000,
- },
- async () => {
- const { client, testUser } = await getTestInstance({
- rateLimit: {
- enabled: true,
- window: 10,
- max: 20,
- },
- });
+describe("rate-limiter", async () => {
+ const { client, testUser } = await getTestInstance({
+ rateLimit: {
+ enabled: true,
+ window: 10,
+ max: 20,
+ },
+ });
- it("should return 429 after 3 request for sign-in", async () => {
- for (let i = 0; i < 5; i++) {
- const response = await client.signIn.email({
- email: testUser.email,
- password: testUser.password,
- });
- if (i >= 3) {
- expect(response.error?.status).toBe(429);
- } else {
- expect(response.error).toBeNull();
- }
- }
- });
-
- it("should reset the limit after the window period", async () => {
- vi.useFakeTimers();
- vi.advanceTimersByTime(11000);
- for (let i = 0; i < 5; i++) {
- const res = await client.signIn.email({
- email: testUser.email,
- password: testUser.password,
- });
- if (i >= 3) {
- expect(res.error?.status).toBe(429);
- } else {
- expect(res.error).toBeNull();
- }
- }
- });
-
- it("should respond the correct retry-after header", async () => {
- vi.useFakeTimers();
- vi.advanceTimersByTime(3000);
- let retryAfter = "";
- await client.signIn.email(
- {
- email: testUser.email,
- password: testUser.password,
- },
- {
- onError(context) {
- retryAfter = context.response.headers.get("X-Retry-After") ?? "";
- },
- },
- );
- expect(retryAfter).toBe("7");
- });
-
- it("should rate limit based on the path", async () => {
- const signInRes = await client.signIn.email({
+ it("should return 429 after 3 request for sign-in", async () => {
+ for (let i = 0; i < 5; i++) {
+ const response = await client.signIn.email({
email: testUser.email,
password: testUser.password,
});
- expect(signInRes.error?.status).toBe(429);
+ if (i >= 3) {
+ expect(response.error?.status).toBe(429);
+ } else {
+ expect(response.error).toBeNull();
+ }
+ }
+ });
- const signUpRes = await client.signUp.email({
- email: "new-test@email.com",
+ it("should reset the limit after the window period", async () => {
+ vi.useFakeTimers();
+ vi.advanceTimersByTime(11000);
+ for (let i = 0; i < 5; i++) {
+ const res = await client.signIn.email({
+ email: testUser.email,
password: testUser.password,
- name: "test",
});
- expect(signUpRes.error).toBeNull();
- });
-
- it("non-special-rules limits", async () => {
- for (let i = 0; i < 25; i++) {
- const response = await client.getSession();
- expect(response.error?.status).toBe(i >= 20 ? 429 : undefined);
+ if (i >= 3) {
+ expect(res.error?.status).toBe(429);
+ } else {
+ expect(res.error).toBeNull();
}
- });
+ }
+ });
- it("query params should be ignored", async () => {
- for (let i = 0; i < 25; i++) {
- const response = await client.listSessions({
- fetchOptions: {
- query: {
- "test-query": Math.random().toString(),
- },
+ it("should respond the correct retry-after header", async () => {
+ vi.useFakeTimers();
+ vi.advanceTimersByTime(3000);
+ let retryAfter = "";
+ await client.signIn.email(
+ {
+ email: testUser.email,
+ password: testUser.password,
+ },
+ {
+ onError(context) {
+ retryAfter = context.response.headers.get("X-Retry-After") ?? "";
+ },
+ },
+ );
+ expect(retryAfter).toBe("7");
+ });
+
+ it("should rate limit based on the path", async () => {
+ const signInRes = await client.signIn.email({
+ email: testUser.email,
+ password: testUser.password,
+ });
+ expect(signInRes.error?.status).toBe(429);
+
+ const signUpRes = await client.signUp.email({
+ email: "new-test@email.com",
+ password: testUser.password,
+ name: "test",
+ });
+ expect(signUpRes.error).toBeNull();
+ });
+
+ it("non-special-rules limits", async () => {
+ for (let i = 0; i < 25; i++) {
+ const response = await client.getSession();
+ expect(response.error?.status).toBe(i >= 20 ? 429 : undefined);
+ }
+ });
+
+ it("query params should be ignored", async () => {
+ for (let i = 0; i < 25; i++) {
+ const response = await client.listSessions({
+ fetchOptions: {
+ query: {
+ "test-query": Math.random().toString(),
},
- });
+ },
+ });
- if (i >= 20) {
- expect(response.error?.status).toBe(429);
- } else {
- expect(response.error?.status).toBe(401);
- }
+ if (i >= 20) {
+ expect(response.error?.status).toBe(429);
+ } else {
+ expect(response.error?.status).toBe(401);
}
- });
- },
-);
+ }
+ });
+});
describe("custom rate limiting storage", async () => {
const store = new Map();
@@ -138,7 +133,7 @@ describe("custom rate limiting storage", async () => {
password: testUser.password,
});
const rateLimitData: RateLimit = JSON.parse(
- store.get("127.0.0.1/sign-in/email") ?? "{}",
+ store.get("127.0.0.1|/sign-in/email") ?? "{}",
);
expect(rateLimitData.lastRequest).toBeGreaterThanOrEqual(lastRequest);
lastRequest = rateLimitData.lastRequest;
@@ -149,7 +144,7 @@ describe("custom rate limiting storage", async () => {
expect(response.error).toBeNull();
expect(rateLimitData.count).toBe(i + 1);
}
- const rateLimitExp = expirationMap.get("127.0.0.1/sign-in/email");
+ const rateLimitExp = expirationMap.get("127.0.0.1|/sign-in/email");
expect(rateLimitExp).toBe(10);
}
});
@@ -277,7 +272,7 @@ describe("should work in development/test environment", () => {
);
expect(signInKeys.length).toBeGreaterThan(0);
- expect(signInKeys[0]).toBe(`${LOCALHOST_IP}${REQUEST_PATH}`);
+ expect(signInKeys[0]).toBe(`${LOCALHOST_IP}|${REQUEST_PATH}`);
});
it("should work in test environment", async () => {
@@ -321,6 +316,94 @@ describe("should work in development/test environment", () => {
);
expect(signInKeys.length).toBeGreaterThan(0);
- expect(signInKeys[0]).toBe(`${LOCALHOST_IP}${REQUEST_PATH}`);
+ expect(signInKeys[0]).toBe(`${LOCALHOST_IP}|${REQUEST_PATH}`);
+ });
+});
+
+describe("IPv6 address normalization and rate limiting", () => {
+ it("should normalize IPv6 addresses to canonical form", () => {
+ // All these representations of the same IPv6 address should normalize to the same value
+ const representations = [
+ "2001:db8::1",
+ "2001:DB8::1",
+ "2001:0db8::1",
+ "2001:db8:0::1",
+ "2001:0db8:0:0:0:0:0:1",
+ ];
+
+ const normalized = representations.map((ip) => normalizeIP(ip));
+ const uniqueValues = new Set(normalized);
+
+ // All should normalize to the same value
+ expect(uniqueValues.size).toBe(1);
+ expect(normalized[0]).toBe("2001:0db8:0000:0000:0000:0000:0000:0001");
+ });
+
+ it("should convert IPv4-mapped IPv6 to IPv4", () => {
+ const ipv4Mapped = [
+ "::ffff:192.0.2.1",
+ "::FFFF:192.0.2.1",
+ "::ffff:c000:0201", // hex-encoded
+ ];
+
+ const normalized = ipv4Mapped.map((ip) => normalizeIP(ip));
+
+ // All should normalize to the same IPv4 address
+ normalized.forEach((ip) => {
+ expect(ip).toBe("192.0.2.1");
+ });
+ });
+
+ it("should support IPv6 subnet rate limiting", () => {
+ // Simulate attacker rotating through IPv6 addresses in their /64 allocation
+ const attackIPs = [
+ "2001:db8:abcd:1234:0000:0000:0000:0001",
+ "2001:db8:abcd:1234:1111:2222:3333:4444",
+ "2001:db8:abcd:1234:ffff:ffff:ffff:ffff",
+ ];
+
+ const normalized = attackIPs.map((ip) =>
+ normalizeIP(ip, { ipv6Subnet: 64 }),
+ );
+
+ // All should map to same /64 subnet
+ const uniqueValues = new Set(normalized);
+ expect(uniqueValues.size).toBe(1);
+ expect(normalized[0]).toBe("2001:0db8:abcd:1234:0000:0000:0000:0000");
+ });
+
+ it("should rate limit different IPv6 subnets separately", () => {
+ // Different /64 subnets should have separate rate limits
+ const subnet1IPs = ["2001:db8:abcd:1111::1", "2001:db8:abcd:1111::2"];
+ const subnet2IPs = ["2001:db8:abcd:2222::1", "2001:db8:abcd:2222::2"];
+
+ const normalized1 = subnet1IPs.map((ip) =>
+ normalizeIP(ip, { ipv6Subnet: 64 }),
+ );
+ const normalized2 = subnet2IPs.map((ip) =>
+ normalizeIP(ip, { ipv6Subnet: 64 }),
+ );
+
+ // Same subnet should normalize to same value
+ expect(normalized1[0]).toBe(normalized1[1]);
+ expect(normalized2[0]).toBe(normalized2[1]);
+
+ // Different subnets should normalize to different values
+ expect(normalized1[0]).not.toBe(normalized2[0]);
+ });
+
+ it("should handle localhost IPv6 addresses", () => {
+ expect(normalizeIP("::1")).toBe("0000:0000:0000:0000:0000:0000:0000:0001");
+ });
+
+ it("should handle link-local IPv6 addresses", () => {
+ const linkLocal = normalizeIP("fe80::1");
+ expect(linkLocal).toBe("fe80:0000:0000:0000:0000:0000:0000:0001");
+ });
+
+ it("IPv6 subnet should not affect IPv4 addresses", () => {
+ const ipv4 = "192.168.1.1";
+ const normalized = normalizeIP(ipv4, { ipv6Subnet: 64 });
+ expect(normalized).toBe(ipv4);
});
});
diff --git a/packages/better-auth/src/utils/get-request-ip.ts b/packages/better-auth/src/utils/get-request-ip.ts
index e9d9b28af7..2bb02c7fc1 100644
--- a/packages/better-auth/src/utils/get-request-ip.ts
+++ b/packages/better-auth/src/utils/get-request-ip.ts
@@ -1,6 +1,6 @@
import type { BetterAuthOptions } from "@better-auth/core";
import { isDevelopment, isTest } from "@better-auth/core/env";
-import * as z from "zod";
+import { isValidIP, normalizeIP } from "@better-auth/core/utils";
// Localhost IP used for test and development environments
const LOCALHOST_IP = "127.0.0.1";
@@ -25,7 +25,9 @@ export function getIp(
if (typeof value === "string") {
const ip = value.split(",")[0]!.trim();
if (isValidIP(ip)) {
- return ip;
+ return normalizeIP(ip, {
+ ipv6Subnet: options.advanced?.ipAddress?.ipv6Subnet,
+ });
}
}
}
@@ -37,18 +39,3 @@ export function getIp(
return null;
}
-
-function isValidIP(ip: string): boolean {
- const ipv4 = z.ipv4().safeParse(ip);
-
- if (ipv4.success) {
- return true;
- }
-
- const ipv6 = z.ipv6().safeParse(ip);
- if (ipv6.success) {
- return true;
- }
-
- return false;
-}
diff --git a/packages/core/src/types/init-options.ts b/packages/core/src/types/init-options.ts
index b356e68664..42c7e1bb1b 100644
--- a/packages/core/src/types/init-options.ts
+++ b/packages/core/src/types/init-options.ts
@@ -142,6 +142,25 @@ export type BetterAuthAdvancedOptions = {
* ⚠︎ This is a security risk and it may expose your application to abuse
*/
disableIpTracking?: boolean;
+ /**
+ * IPv6 subnet prefix length for rate limiting.
+ *
+ * IPv6 addresses can be grouped by subnet to prevent attackers from
+ * bypassing rate limits by rotating through multiple addresses in
+ * their allocation.
+ *
+ * Common values:
+ * - 128 (default): Individual IPv6 address
+ * - 64: /64 subnet (typical home/business allocation)
+ * - 48: /48 subnet (larger network allocation)
+ * - 32: /32 subnet (ISP allocation)
+ *
+ * Note: This only affects IPv6 addresses. IPv4 addresses are always
+ * rate limited individually.
+ *
+ * @default 128 (individual address)
+ */
+ ipv6Subnet?: 128 | 64 | 48 | 32 | undefined;
}
| undefined;
/**
diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts
index 4b10aec1e2..0752500a49 100644
--- a/packages/core/src/utils/index.ts
+++ b/packages/core/src/utils/index.ts
@@ -1,5 +1,7 @@
export { deprecate } from "./deprecate";
export { defineErrorCodes } from "./error-codes";
export { generateId } from "./id";
+export { createRateLimitKey, isValidIP, normalizeIP } from "./ip";
export { safeJSONParse } from "./json";
export { capitalizeFirstLetter } from "./string";
+export { normalizePathname } from "./url";
diff --git a/packages/core/src/utils/ip.test.ts b/packages/core/src/utils/ip.test.ts
new file mode 100644
index 0000000000..191a51ec29
--- /dev/null
+++ b/packages/core/src/utils/ip.test.ts
@@ -0,0 +1,243 @@
+import { describe, expect, it } from "vitest";
+import { createRateLimitKey, isValidIP, normalizeIP } from "./ip";
+
+describe("IP Normalization", () => {
+ describe("isValidIP", () => {
+ it("should validate IPv4 addresses", () => {
+ expect(isValidIP("192.168.1.1")).toBe(true);
+ expect(isValidIP("127.0.0.1")).toBe(true);
+ expect(isValidIP("0.0.0.0")).toBe(true);
+ expect(isValidIP("255.255.255.255")).toBe(true);
+ });
+
+ it("should validate IPv6 addresses", () => {
+ expect(isValidIP("2001:db8::1")).toBe(true);
+ expect(isValidIP("::1")).toBe(true);
+ expect(isValidIP("::")).toBe(true);
+ expect(isValidIP("2001:0db8:0000:0000:0000:0000:0000:0001")).toBe(true);
+ });
+
+ it("should reject invalid IPs", () => {
+ expect(isValidIP("not-an-ip")).toBe(false);
+ expect(isValidIP("999.999.999.999")).toBe(false);
+ expect(isValidIP("gggg::1")).toBe(false);
+ });
+ });
+
+ describe("IPv4 Normalization", () => {
+ it("should return IPv4 addresses unchanged", () => {
+ expect(normalizeIP("192.168.1.1")).toBe("192.168.1.1");
+ expect(normalizeIP("127.0.0.1")).toBe("127.0.0.1");
+ expect(normalizeIP("10.0.0.1")).toBe("10.0.0.1");
+ });
+ });
+
+ describe("IPv6 Normalization", () => {
+ it("should normalize compressed IPv6 to full form", () => {
+ expect(normalizeIP("2001:db8::1")).toBe(
+ "2001:0db8:0000:0000:0000:0000:0000:0001",
+ );
+ expect(normalizeIP("::1")).toBe(
+ "0000:0000:0000:0000:0000:0000:0000:0001",
+ );
+ expect(normalizeIP("::")).toBe("0000:0000:0000:0000:0000:0000:0000:0000");
+ });
+
+ it("should normalize uppercase to lowercase", () => {
+ expect(normalizeIP("2001:DB8::1")).toBe(
+ "2001:0db8:0000:0000:0000:0000:0000:0001",
+ );
+ expect(normalizeIP("2001:0DB8:ABCD:EF00::1")).toBe(
+ "2001:0db8:abcd:ef00:0000:0000:0000:0001",
+ );
+ });
+
+ it("should handle various IPv6 formats consistently", () => {
+ // All these represent the same address
+ const normalized = "2001:0db8:0000:0000:0000:0000:0000:0001";
+ expect(normalizeIP("2001:db8::1")).toBe(normalized);
+ expect(normalizeIP("2001:0db8:0:0:0:0:0:1")).toBe(normalized);
+ expect(normalizeIP("2001:db8:0::1")).toBe(normalized);
+ expect(normalizeIP("2001:0db8::0:0:0:1")).toBe(normalized);
+ });
+
+ it("should handle IPv6 with :: at different positions", () => {
+ expect(normalizeIP("2001:db8:85a3::8a2e:370:7334")).toBe(
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ );
+ expect(normalizeIP("::ffff:192.0.2.1")).not.toContain("::");
+ });
+ });
+
+ describe("IPv4-mapped IPv6 Conversion", () => {
+ it("should convert IPv4-mapped IPv6 to IPv4", () => {
+ expect(normalizeIP("::ffff:192.0.2.1")).toBe("192.0.2.1");
+ expect(normalizeIP("::ffff:127.0.0.1")).toBe("127.0.0.1");
+ expect(normalizeIP("::FFFF:10.0.0.1")).toBe("10.0.0.1");
+ });
+
+ it("should handle hex-encoded IPv4 in mapped addresses", () => {
+ // ::ffff:c000:0201 = ::ffff:192.0.2.1 = 192.0.2.1
+ expect(normalizeIP("::ffff:c000:0201")).toBe("192.0.2.1");
+ // ::ffff:7f00:0001 = ::ffff:127.0.0.1 = 127.0.0.1
+ expect(normalizeIP("::ffff:7f00:0001")).toBe("127.0.0.1");
+ });
+
+ it("should handle full form IPv4-mapped IPv6", () => {
+ expect(normalizeIP("0:0:0:0:0:ffff:192.0.2.1")).toBe("192.0.2.1");
+ });
+ });
+
+ describe("IPv6 Subnet Support", () => {
+ it("should extract /64 subnet", () => {
+ /* cspell:disable-next-line */
+ const ip1 = normalizeIP("2001:db8:0:0:1234:5678:90ab:cdef", {
+ ipv6Subnet: 64,
+ });
+ const ip2 = normalizeIP("2001:db8:0:0:ffff:ffff:ffff:ffff", {
+ ipv6Subnet: 64,
+ });
+ // Both should have same /64 prefix
+ expect(ip1).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
+ expect(ip2).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
+ expect(ip1).toBe(ip2);
+ });
+
+ it("should extract /48 subnet", () => {
+ /* cspell:disable-next-line */
+ const ip1 = normalizeIP("2001:db8:1234:5678:90ab:cdef:1234:5678", {
+ ipv6Subnet: 48,
+ });
+ const ip2 = normalizeIP("2001:db8:1234:ffff:ffff:ffff:ffff:ffff", {
+ ipv6Subnet: 48,
+ });
+ // Both should have same /48 prefix
+ expect(ip1).toBe("2001:0db8:1234:0000:0000:0000:0000:0000");
+ expect(ip2).toBe("2001:0db8:1234:0000:0000:0000:0000:0000");
+ expect(ip1).toBe(ip2);
+ });
+
+ it("should extract /32 subnet", () => {
+ /* cspell:disable-next-line */
+ const ip1 = normalizeIP("2001:db8:1234:5678:90ab:cdef:1234:5678", {
+ ipv6Subnet: 32,
+ });
+ const ip2 = normalizeIP("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", {
+ ipv6Subnet: 32,
+ });
+ // Both should have same /32 prefix
+ expect(ip1).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
+ expect(ip2).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
+ expect(ip1).toBe(ip2);
+ });
+
+ it("should handle /128 (full address) by default", () => {
+ const ip1 = normalizeIP("2001:db8::1");
+ const ip2 = normalizeIP("2001:db8::1", { ipv6Subnet: 128 });
+ expect(ip1).toBe(ip2);
+ expect(ip1).toBe("2001:0db8:0000:0000:0000:0000:0000:0001");
+ });
+
+ it("should not affect IPv4 addresses when ipv6Subnet is set", () => {
+ expect(normalizeIP("192.168.1.1", { ipv6Subnet: 64 })).toBe(
+ "192.168.1.1",
+ );
+ });
+ });
+
+ describe("Rate Limit Key Creation", () => {
+ it("should create keys with separator", () => {
+ expect(createRateLimitKey("192.168.1.1", "/sign-in")).toBe(
+ "192.168.1.1|/sign-in",
+ );
+ expect(createRateLimitKey("2001:db8::1", "/api/auth")).toBe(
+ "2001:db8::1|/api/auth",
+ );
+ });
+
+ it("should prevent collision attacks", () => {
+ // Without separator: "192.0.2.1" + "/sign-in" = "192.0.2.1/sign-in"
+ // "192.0.2" + ".1/sign-in" = "192.0.2.1/sign-in"
+ // With separator: they're different
+ const key1 = createRateLimitKey("192.0.2.1", "/sign-in");
+ const key2 = createRateLimitKey("192.0.2", ".1/sign-in");
+ expect(key1).not.toBe(key2);
+ expect(key1).toBe("192.0.2.1|/sign-in");
+ expect(key2).toBe("192.0.2|.1/sign-in");
+ });
+ });
+
+ describe("Security: Bypass Prevention", () => {
+ it("should prevent IPv6 representation bypass", () => {
+ // Attacker tries different representations of same address
+ const representations = [
+ "2001:db8::1",
+ "2001:DB8::1",
+ "2001:0db8::1",
+ "2001:db8:0::1",
+ "2001:0db8:0:0:0:0:0:1",
+ "2001:db8::0:1",
+ ];
+
+ const normalized = representations.map((ip) => normalizeIP(ip));
+ // All should normalize to the same value
+ const uniqueValues = new Set(normalized);
+ expect(uniqueValues.size).toBe(1);
+ expect(normalized[0]).toBe("2001:0db8:0000:0000:0000:0000:0000:0001");
+ });
+
+ it("should prevent IPv4-mapped bypass", () => {
+ // Attacker switches between IPv4 and IPv4-mapped IPv6
+ const ip1 = normalizeIP("192.0.2.1");
+ const ip2 = normalizeIP("::ffff:192.0.2.1");
+ const ip3 = normalizeIP("::FFFF:192.0.2.1");
+ const ip4 = normalizeIP("::ffff:c000:0201");
+
+ // All should normalize to the same IPv4
+ expect(ip1).toBe("192.0.2.1");
+ expect(ip2).toBe("192.0.2.1");
+ expect(ip3).toBe("192.0.2.1");
+ expect(ip4).toBe("192.0.2.1");
+ });
+
+ it("should group IPv6 subnet attacks", () => {
+ // Attacker rotates through addresses in their /64 allocation
+ const attackIPs = [
+ "2001:db8:abcd:1234:0000:0000:0000:0001",
+ "2001:db8:abcd:1234:1111:2222:3333:4444",
+ "2001:db8:abcd:1234:ffff:ffff:ffff:ffff",
+ "2001:db8:abcd:1234:aaaa:bbbb:cccc:dddd",
+ ];
+
+ const normalized = attackIPs.map((ip) =>
+ normalizeIP(ip, { ipv6Subnet: 64 }),
+ );
+
+ // All should map to same /64 subnet
+ const uniqueValues = new Set(normalized);
+ expect(uniqueValues.size).toBe(1);
+ expect(normalized[0]).toBe("2001:0db8:abcd:1234:0000:0000:0000:0000");
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle localhost addresses", () => {
+ expect(normalizeIP("127.0.0.1")).toBe("127.0.0.1");
+ expect(normalizeIP("::1")).toBe(
+ "0000:0000:0000:0000:0000:0000:0000:0001",
+ );
+ });
+
+ it("should handle all-zeros address", () => {
+ expect(normalizeIP("0.0.0.0")).toBe("0.0.0.0");
+ expect(normalizeIP("::")).toBe("0000:0000:0000:0000:0000:0000:0000:0000");
+ });
+
+ it("should handle link-local addresses", () => {
+ expect(normalizeIP("169.254.0.1")).toBe("169.254.0.1");
+ expect(normalizeIP("fe80::1")).toBe(
+ "fe80:0000:0000:0000:0000:0000:0000:0001",
+ );
+ });
+ });
+});
diff --git a/packages/core/src/utils/ip.ts b/packages/core/src/utils/ip.ts
new file mode 100644
index 0000000000..4f2ac230bf
--- /dev/null
+++ b/packages/core/src/utils/ip.ts
@@ -0,0 +1,211 @@
+import * as z from "zod";
+
+/**
+ * Normalizes an IP address for consistent rate limiting.
+ *
+ * Features:
+ * - Normalizes IPv6 to canonical lowercase form
+ * - Converts IPv4-mapped IPv6 to IPv4
+ * - Supports IPv6 subnet extraction
+ * - Handles all edge cases (::1, ::, etc.)
+ */
+
+interface NormalizeIPOptions {
+ /**
+ * For IPv6 addresses, extract the subnet prefix instead of full address.
+ * Common values: 32, 48, 64, 128 (default: 128 = full address)
+ *
+ * @default 128
+ */
+ ipv6Subnet?: 128 | 64 | 48 | 32;
+}
+
+/**
+ * Checks if an IP is valid IPv4 or IPv6
+ */
+export function isValidIP(ip: string): boolean {
+ return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
+}
+
+/**
+ * Checks if an IP is IPv6
+ */
+function isIPv6(ip: string): boolean {
+ return z.ipv6().safeParse(ip).success;
+}
+
+/**
+ * Converts IPv4-mapped IPv6 address to IPv4
+ * e.g., "::ffff:192.0.2.1" -> "192.0.2.1"
+ */
+function extractIPv4FromMapped(ipv6: string): string | null {
+ const lower = ipv6.toLowerCase();
+
+ // Handle ::ffff:192.0.2.1 format
+ if (lower.startsWith("::ffff:")) {
+ const ipv4Part = lower.substring(7);
+ // Check if it's a valid IPv4
+ if (z.ipv4().safeParse(ipv4Part).success) {
+ return ipv4Part;
+ }
+ }
+
+ // Handle full form: 0:0:0:0:0:ffff:192.0.2.1
+ const parts = ipv6.split(":");
+ if (parts.length === 7 && parts[5]?.toLowerCase() === "ffff") {
+ const ipv4Part = parts[6];
+ if (ipv4Part && z.ipv4().safeParse(ipv4Part).success) {
+ return ipv4Part;
+ }
+ }
+
+ // Handle hex-encoded IPv4 in mapped address
+ // e.g., ::ffff:c000:0201 -> 192.0.2.1
+ if (lower.includes("::ffff:") || lower.includes(":ffff:")) {
+ const groups = expandIPv6(ipv6);
+ 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]
+ ) {
+ // Convert last two groups to IPv4
+ const byte1 = Number.parseInt(groups[6].substring(0, 2), 16);
+ const byte2 = Number.parseInt(groups[6].substring(2, 4), 16);
+ const byte3 = Number.parseInt(groups[7].substring(0, 2), 16);
+ const byte4 = Number.parseInt(groups[7].substring(2, 4), 16);
+ return `${byte1}.${byte2}.${byte3}.${byte4}`;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Expands a compressed IPv6 address to full form
+ * e.g., "2001:db8::1" -> ["2001", "0db8", "0000", "0000", "0000", "0000", "0000", "0001"]
+ */
+function expandIPv6(ipv6: string): string[] {
+ // Handle :: notation (zero compression)
+ if (ipv6.includes("::")) {
+ const sides = ipv6.split("::");
+ const left = sides[0] ? sides[0].split(":") : [];
+ const right = sides[1] ? sides[1].split(":") : [];
+
+ // Calculate missing groups
+ const totalGroups = 8;
+ const missingGroups = totalGroups - left.length - right.length;
+ const zeros = Array(missingGroups).fill("0000");
+
+ // Pad existing groups to 4 digits
+ const paddedLeft = left.map((g) => g.padStart(4, "0"));
+ const paddedRight = right.map((g) => g.padStart(4, "0"));
+
+ return [...paddedLeft, ...zeros, ...paddedRight];
+ }
+
+ // No compression, just pad each group
+ return ipv6.split(":").map((g) => g.padStart(4, "0"));
+}
+
+/**
+ * Normalizes an IPv6 address to canonical form
+ * e.g., "2001:DB8::1" -> "2001:0db8:0000:0000:0000:0000:0000:0001"
+ */
+function normalizeIPv6(
+ ipv6: string,
+ subnetPrefix?: 128 | 32 | 48 | 64,
+): string {
+ const groups = expandIPv6(ipv6);
+
+ if (subnetPrefix && subnetPrefix < 128) {
+ // Apply subnet mask
+ const prefix = subnetPrefix;
+ let bitsRemaining: number = prefix;
+
+ const maskedGroups = groups.map((group) => {
+ if (bitsRemaining <= 0) {
+ return "0000";
+ }
+ if (bitsRemaining >= 16) {
+ bitsRemaining -= 16;
+ return group;
+ }
+
+ // Partial mask for this group
+ const value = Number.parseInt(group, 16);
+ const mask = (0xffff << (16 - bitsRemaining)) & 0xffff;
+ const masked = value & mask;
+ bitsRemaining = 0;
+ return masked.toString(16).padStart(4, "0");
+ });
+
+ return maskedGroups.join(":").toLowerCase();
+ }
+
+ return groups.join(":").toLowerCase();
+}
+
+/**
+ * Normalizes an IP address (IPv4 or IPv6) for consistent rate limiting.
+ *
+ * @param ip - The IP address to normalize
+ * @param options - Normalization options
+ * @returns Normalized IP address
+ *
+ * @example
+ * normalizeIP("2001:DB8::1")
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0001"
+ *
+ * @example
+ * normalizeIP("::ffff:192.0.2.1")
+ * // -> "192.0.2.1" (converted to IPv4)
+ *
+ * @example
+ * normalizeIP("2001:db8::1", { ipv6Subnet: 64 })
+ * // -> "2001:0db8:0000:0000:0000:0000:0000:0000" (subnet /64)
+ */
+export function normalizeIP(
+ ip: string,
+ options: NormalizeIPOptions = {},
+): string {
+ // IPv4 addresses are already normalized
+ if (z.ipv4().safeParse(ip).success) {
+ return ip.toLowerCase();
+ }
+
+ // Check if it's IPv6
+ if (!isIPv6(ip)) {
+ // Return as-is if not valid (shouldn't happen due to prior validation)
+ return ip.toLowerCase();
+ }
+
+ // Check for IPv4-mapped IPv6
+ const ipv4 = extractIPv4FromMapped(ip);
+ if (ipv4) {
+ return ipv4.toLowerCase();
+ }
+
+ // Normalize IPv6
+ const subnetPrefix = options.ipv6Subnet || 128;
+ return normalizeIPv6(ip, subnetPrefix);
+}
+
+/**
+ * Creates a rate limit key from IP and path
+ * Uses a separator to prevent collision attacks
+ *
+ * @param ip - The IP address (should be normalized)
+ * @param path - The request path
+ * @returns Rate limit key
+ */
+export function createRateLimitKey(ip: string, path: string): string {
+ // Use | as separator to prevent collision attacks
+ // e.g., "192.0.2.1" + "/sign-in" vs "192.0.2" + ".1/sign-in"
+ return `${ip}|${path}`;
+}
diff --git a/packages/core/src/utils/url.ts b/packages/core/src/utils/url.ts
new file mode 100644
index 0000000000..a895ef4ccf
--- /dev/null
+++ b/packages/core/src/utils/url.ts
@@ -0,0 +1,43 @@
+/**
+ * Normalizes a request pathname by removing the basePath prefix and trailing slashes.
+ * This is useful for matching paths against configured path lists.
+ *
+ * @param requestUrl - The full request URL
+ * @param basePath - The base path of the auth API (e.g., "/api/auth")
+ * @returns The normalized path without basePath prefix or trailing slashes,
+ * or "/" if URL parsing fails
+ *
+ * @example
+ * normalizePathname("http://localhost:3000/api/auth/sso/saml2/callback/provider1", "/api/auth")
+ * // Returns: "/sso/saml2/callback/provider1"
+ *
+ * normalizePathname("http://localhost:3000/sso/saml2/callback/provider1/", "/")
+ * // Returns: "/sso/saml2/callback/provider1"
+ */
+export function normalizePathname(
+ requestUrl: string,
+ basePath: string,
+): string {
+ let pathname: string;
+ try {
+ pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
+ } catch {
+ return "/";
+ }
+
+ if (basePath === "/" || basePath === "") {
+ return pathname;
+ }
+
+ // Check for exact match or proper path boundary (basePath followed by "/" or end)
+ // This prevents "/api/auth" from matching "/api/authevil/..."
+ if (pathname === basePath) {
+ return "/";
+ }
+
+ if (pathname.startsWith(basePath + "/")) {
+ return pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
+ }
+
+ return pathname;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 44d75b5321..8a1a25a904 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -410,7 +410,7 @@ importers:
version: 12.23.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
geist:
specifier: ^1.4.2
- version: 1.4.2(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1))
+ version: 1.4.2(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1))
input-otp:
specifier: ^1.4.2
version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -522,7 +522,7 @@ importers:
version: 2.1.1
geist:
specifier: ^1.4.2
- version: 1.4.2(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1))
+ version: 1.4.2(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1))
lucide-react:
specifier: ^0.542.0
version: 0.542.0(react@19.2.3)
@@ -764,7 +764,7 @@ importers:
version: 16.4.3(@oramacloud/client@2.1.4)(@tanstack/react-router@1.151.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(algoliasearch@5.46.2)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)(zod@4.3.4)
geist:
specifier: ^1.4.2
- version: 1.4.2(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1))
+ version: 1.4.2(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1))
gray-matter:
specifier: ^4.0.3
version: 4.0.3
@@ -994,7 +994,7 @@ importers:
devDependencies:
'@cloudflare/vitest-pool-workers':
specifier: ^0.8.69
- version: 0.8.69(@cloudflare/workers-types@4.20260103.0)(@vitest/runner@4.0.17)(@vitest/snapshot@4.0.17)(vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.6)(happy-dom@20.0.11)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
+ version: 0.8.69(@cloudflare/workers-types@4.20260103.0)(@vitest/runner@4.0.17)(@vitest/snapshot@4.0.17)(vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.6)(happy-dom@20.0.11)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@cloudflare/workers-types':
specifier: ^4.20250903.0
version: 4.20260103.0
@@ -1005,6 +1005,18 @@ importers:
specifier: 4.33.2
version: 4.33.2(@cloudflare/workers-types@4.20260103.0)
+ e2e/smoke/test/fixtures/ipv6:
+ dependencies:
+ '@hono/node-server':
+ specifier: ^1.19.9
+ version: 1.19.9(hono@4.11.4)
+ better-auth:
+ specifier: workspace:*
+ version: link:../../../../../packages/better-auth
+ hono:
+ specifier: ^4.11.4
+ version: 4.11.4
+
e2e/smoke/test/fixtures/tsconfig-declaration:
dependencies:
'@better-auth/oauth-provider':
@@ -3077,9 +3089,6 @@ packages:
'@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
- '@emnapi/runtime@1.7.1':
- resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
-
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
@@ -4040,6 +4049,12 @@ packages:
peerDependencies:
hono: ^4
+ '@hono/node-server@1.19.9':
+ resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
+ engines: {node: '>=18.14.1'}
+ peerDependencies:
+ hono: ^4
+
'@hookform/resolvers@5.2.2':
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
peerDependencies:
@@ -6632,111 +6647,56 @@ packages:
rollup:
optional: true
- '@rollup/rollup-android-arm-eabi@4.54.0':
- resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==}
- cpu: [arm]
- os: [android]
-
'@rollup/rollup-android-arm-eabi@4.55.1':
resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==}
cpu: [arm]
os: [android]
- '@rollup/rollup-android-arm64@4.54.0':
- resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==}
- cpu: [arm64]
- os: [android]
-
'@rollup/rollup-android-arm64@4.55.1':
resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==}
cpu: [arm64]
os: [android]
- '@rollup/rollup-darwin-arm64@4.54.0':
- resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==}
- cpu: [arm64]
- os: [darwin]
-
'@rollup/rollup-darwin-arm64@4.55.1':
resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==}
cpu: [arm64]
os: [darwin]
- '@rollup/rollup-darwin-x64@4.54.0':
- resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==}
- cpu: [x64]
- os: [darwin]
-
'@rollup/rollup-darwin-x64@4.55.1':
resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==}
cpu: [x64]
os: [darwin]
- '@rollup/rollup-freebsd-arm64@4.54.0':
- resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==}
- cpu: [arm64]
- os: [freebsd]
-
'@rollup/rollup-freebsd-arm64@4.55.1':
resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==}
cpu: [arm64]
os: [freebsd]
- '@rollup/rollup-freebsd-x64@4.54.0':
- resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==}
- cpu: [x64]
- os: [freebsd]
-
'@rollup/rollup-freebsd-x64@4.55.1':
resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==}
cpu: [x64]
os: [freebsd]
- '@rollup/rollup-linux-arm-gnueabihf@4.54.0':
- resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
- cpu: [arm]
- os: [linux]
-
'@rollup/rollup-linux-arm-gnueabihf@4.55.1':
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm-musleabihf@4.54.0':
- resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
- cpu: [arm]
- os: [linux]
-
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm64-gnu@4.54.0':
- resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
- cpu: [arm64]
- os: [linux]
-
'@rollup/rollup-linux-arm64-gnu@4.55.1':
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-arm64-musl@4.54.0':
- resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
- cpu: [arm64]
- os: [linux]
-
'@rollup/rollup-linux-arm64-musl@4.55.1':
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-loong64-gnu@4.54.0':
- resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
- cpu: [loong64]
- os: [linux]
-
'@rollup/rollup-linux-loong64-gnu@4.55.1':
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
cpu: [loong64]
@@ -6747,11 +6707,6 @@ packages:
cpu: [loong64]
os: [linux]
- '@rollup/rollup-linux-ppc64-gnu@4.54.0':
- resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
- cpu: [ppc64]
- os: [linux]
-
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
cpu: [ppc64]
@@ -6762,51 +6717,26 @@ packages:
cpu: [ppc64]
os: [linux]
- '@rollup/rollup-linux-riscv64-gnu@4.54.0':
- resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
- cpu: [riscv64]
- os: [linux]
-
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
cpu: [riscv64]
os: [linux]
- '@rollup/rollup-linux-riscv64-musl@4.54.0':
- resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
- cpu: [riscv64]
- os: [linux]
-
'@rollup/rollup-linux-riscv64-musl@4.55.1':
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
cpu: [riscv64]
os: [linux]
- '@rollup/rollup-linux-s390x-gnu@4.54.0':
- resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
- cpu: [s390x]
- os: [linux]
-
'@rollup/rollup-linux-s390x-gnu@4.55.1':
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
cpu: [s390x]
os: [linux]
- '@rollup/rollup-linux-x64-gnu@4.54.0':
- resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
- cpu: [x64]
- os: [linux]
-
'@rollup/rollup-linux-x64-gnu@4.55.1':
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
cpu: [x64]
os: [linux]
- '@rollup/rollup-linux-x64-musl@4.54.0':
- resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
- cpu: [x64]
- os: [linux]
-
'@rollup/rollup-linux-x64-musl@4.55.1':
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
cpu: [x64]
@@ -6817,51 +6747,26 @@ packages:
cpu: [x64]
os: [openbsd]
- '@rollup/rollup-openharmony-arm64@4.54.0':
- resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
- cpu: [arm64]
- os: [openharmony]
-
'@rollup/rollup-openharmony-arm64@4.55.1':
resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==}
cpu: [arm64]
os: [openharmony]
- '@rollup/rollup-win32-arm64-msvc@4.54.0':
- resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==}
- cpu: [arm64]
- os: [win32]
-
'@rollup/rollup-win32-arm64-msvc@4.55.1':
resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==}
cpu: [arm64]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.54.0':
- resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==}
- cpu: [ia32]
- os: [win32]
-
'@rollup/rollup-win32-ia32-msvc@4.55.1':
resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==}
cpu: [ia32]
os: [win32]
- '@rollup/rollup-win32-x64-gnu@4.54.0':
- resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==}
- cpu: [x64]
- os: [win32]
-
'@rollup/rollup-win32-x64-gnu@4.55.1':
resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==}
cpu: [x64]
os: [win32]
- '@rollup/rollup-win32-x64-msvc@4.54.0':
- resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==}
- cpu: [x64]
- os: [win32]
-
'@rollup/rollup-win32-x64-msvc@4.55.1':
resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==}
cpu: [x64]
@@ -7785,6 +7690,9 @@ packages:
'@vitest/expect@4.0.16':
resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==}
+ '@vitest/expect@4.0.17':
+ resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==}
+
'@vitest/mocker@4.0.16':
resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==}
peerDependencies:
@@ -7796,6 +7704,17 @@ packages:
vite:
optional: true
+ '@vitest/mocker@4.0.17':
+ resolution: {integrity: sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
'@vitest/pretty-format@4.0.16':
resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==}
@@ -7817,6 +7736,9 @@ packages:
'@vitest/spy@4.0.16':
resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==}
+ '@vitest/spy@4.0.17':
+ resolution: {integrity: sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==}
+
'@vitest/utils@4.0.16':
resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==}
@@ -13639,11 +13561,6 @@ packages:
rollup:
optional: true
- rollup@4.54.0:
- resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
- engines: {node: '>=18.0.0', npm: '>=8.0.0'}
- hasBin: true
-
rollup@4.55.1:
resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -13925,10 +13842,6 @@ packages:
smob@1.5.0:
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
- smol-toml@1.5.2:
- resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==}
- engines: {node: '>= 18'}
-
smol-toml@1.6.0:
resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==}
engines: {node: '>= 18'}
@@ -15013,6 +14926,40 @@ packages:
jsdom:
optional: true
+ vitest@4.0.17:
+ resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.0.17
+ '@vitest/browser-preview': 4.0.17
+ '@vitest/browser-webdriverio': 4.0.17
+ '@vitest/ui': 4.0.17
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
vlq@1.0.1:
resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
@@ -15764,7 +15711,7 @@ snapshots:
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
- '@babel/types': 7.28.5
+ '@babel/types': 7.28.6
'@babel/helper-compilation-targets@7.27.2':
dependencies:
@@ -15914,7 +15861,7 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
dependencies:
'@babel/traverse': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/types': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -16953,7 +16900,7 @@ snapshots:
optionalDependencies:
workerd: 1.20250829.0
- '@cloudflare/vitest-pool-workers@0.8.69(@cloudflare/workers-types@4.20260103.0)(@vitest/runner@4.0.17)(@vitest/snapshot@4.0.17)(vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.6)(happy-dom@20.0.11)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
+ '@cloudflare/vitest-pool-workers@0.8.69(@cloudflare/workers-types@4.20260103.0)(@vitest/runner@4.0.17)(@vitest/snapshot@4.0.17)(vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.6)(happy-dom@20.0.11)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitest/runner': 4.0.17
'@vitest/snapshot': 4.0.17
@@ -16962,7 +16909,7 @@ snapshots:
devalue: 5.6.1
miniflare: 4.20250829.0
semver: 7.7.3
- vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.6)(happy-dom@20.0.11)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
+ vitest: 4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.6)(happy-dom@20.0.11)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
wrangler: 4.33.2(@cloudflare/workers-types@4.20260103.0)
zod: 3.25.76
transitivePeerDependencies:
@@ -17258,11 +17205,6 @@ snapshots:
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.7.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
@@ -18196,6 +18138,10 @@ snapshots:
dependencies:
hono: 4.11.4
+ '@hono/node-server@1.19.9(hono@4.11.4)':
+ dependencies:
+ hono: 4.11.4
+
'@hookform/resolvers@5.2.2(react-hook-form@7.68.0(react@19.2.3))':
dependencies:
'@standard-schema/utils': 0.3.0
@@ -18350,12 +18296,12 @@ snapshots:
'@img/sharp-wasm32@0.33.5':
dependencies:
- '@emnapi/runtime': 1.7.1
+ '@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.7.1
+ '@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -18874,12 +18820,12 @@ snapshots:
write-file-atomic: 5.0.1
optional: true
- '@netlify/functions@3.1.10(rollup@4.54.0)':
+ '@netlify/functions@3.1.10(rollup@4.55.1)':
dependencies:
'@netlify/blobs': 9.1.2
'@netlify/dev-utils': 2.2.0
'@netlify/serverless-functions-api': 1.41.2
- '@netlify/zip-it-and-ship-it': 12.2.1(rollup@4.54.0)
+ '@netlify/zip-it-and-ship-it': 12.2.1(rollup@4.55.1)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1
@@ -18915,13 +18861,13 @@ snapshots:
'@netlify/serverless-functions-api@2.2.1': {}
- '@netlify/zip-it-and-ship-it@12.2.1(rollup@4.54.0)':
+ '@netlify/zip-it-and-ship-it@12.2.1(rollup@4.55.1)':
dependencies:
'@babel/parser': 7.28.6
'@babel/types': 7.28.0
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 2.2.1
- '@vercel/nft': 0.29.4(rollup@4.54.0)
+ '@vercel/nft': 0.29.4(rollup@4.55.1)
archiver: 7.0.1
common-path-prefix: 3.0.0
copy-file: 11.1.0
@@ -21091,13 +21037,13 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.59': {}
- '@rollup/plugin-alias@5.1.1(rollup@4.54.0)':
+ '@rollup/plugin-alias@5.1.1(rollup@4.55.1)':
optionalDependencies:
- rollup: 4.54.0
+ rollup: 4.55.1
- '@rollup/plugin-commonjs@28.0.6(rollup@4.54.0)':
+ '@rollup/plugin-commonjs@28.0.6(rollup@4.55.1)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@4.54.0)
+ '@rollup/pluginutils': 5.2.0(rollup@4.55.1)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -21105,193 +21051,127 @@ snapshots:
magic-string: 0.30.21
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.54.0
+ rollup: 4.55.1
- '@rollup/plugin-inject@5.0.5(rollup@4.54.0)':
+ '@rollup/plugin-inject@5.0.5(rollup@4.55.1)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@4.54.0)
+ '@rollup/pluginutils': 5.2.0(rollup@4.55.1)
estree-walker: 2.0.2
magic-string: 0.30.21
optionalDependencies:
- rollup: 4.54.0
+ rollup: 4.55.1
- '@rollup/plugin-json@6.1.0(rollup@4.54.0)':
+ '@rollup/plugin-json@6.1.0(rollup@4.55.1)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@4.54.0)
+ '@rollup/pluginutils': 5.2.0(rollup@4.55.1)
optionalDependencies:
- rollup: 4.54.0
+ rollup: 4.55.1
- '@rollup/plugin-node-resolve@16.0.1(rollup@4.54.0)':
+ '@rollup/plugin-node-resolve@16.0.1(rollup@4.55.1)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@4.54.0)
+ '@rollup/pluginutils': 5.2.0(rollup@4.55.1)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.11
optionalDependencies:
- rollup: 4.54.0
+ rollup: 4.55.1
- '@rollup/plugin-replace@6.0.2(rollup@4.54.0)':
+ '@rollup/plugin-replace@6.0.2(rollup@4.55.1)':
dependencies:
- '@rollup/pluginutils': 5.2.0(rollup@4.54.0)
+ '@rollup/pluginutils': 5.2.0(rollup@4.55.1)
magic-string: 0.30.21
optionalDependencies:
- rollup: 4.54.0
+ rollup: 4.55.1
- '@rollup/plugin-terser@0.4.4(rollup@4.54.0)':
+ '@rollup/plugin-terser@0.4.4(rollup@4.55.1)':
dependencies:
serialize-javascript: 6.0.2
smob: 1.5.0
terser: 5.44.1
optionalDependencies:
- rollup: 4.54.0
+ rollup: 4.55.1
- '@rollup/pluginutils@5.2.0(rollup@4.54.0)':
+ '@rollup/pluginutils@5.2.0(rollup@4.55.1)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.54.0
-
- '@rollup/rollup-android-arm-eabi@4.54.0':
- optional: true
+ rollup: 4.55.1
'@rollup/rollup-android-arm-eabi@4.55.1':
optional: true
- '@rollup/rollup-android-arm64@4.54.0':
- optional: true
-
'@rollup/rollup-android-arm64@4.55.1':
optional: true
- '@rollup/rollup-darwin-arm64@4.54.0':
- optional: true
-
'@rollup/rollup-darwin-arm64@4.55.1':
optional: true
- '@rollup/rollup-darwin-x64@4.54.0':
- optional: true
-
'@rollup/rollup-darwin-x64@4.55.1':
optional: true
- '@rollup/rollup-freebsd-arm64@4.54.0':
- optional: true
-
'@rollup/rollup-freebsd-arm64@4.55.1':
optional: true
- '@rollup/rollup-freebsd-x64@4.54.0':
- optional: true
-
'@rollup/rollup-freebsd-x64@4.55.1':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.54.0':
- optional: true
-
'@rollup/rollup-linux-arm-gnueabihf@4.55.1':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.54.0':
- optional: true
-
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.54.0':
- optional: true
-
'@rollup/rollup-linux-arm64-gnu@4.55.1':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.54.0':
- optional: true
-
'@rollup/rollup-linux-arm64-musl@4.55.1':
optional: true
- '@rollup/rollup-linux-loong64-gnu@4.54.0':
- optional: true
-
'@rollup/rollup-linux-loong64-gnu@4.55.1':
optional: true
'@rollup/rollup-linux-loong64-musl@4.55.1':
optional: true
- '@rollup/rollup-linux-ppc64-gnu@4.54.0':
- optional: true
-
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.55.1':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.54.0':
- optional: true
-
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
optional: true
- '@rollup/rollup-linux-riscv64-musl@4.54.0':
- optional: true
-
'@rollup/rollup-linux-riscv64-musl@4.55.1':
optional: true
- '@rollup/rollup-linux-s390x-gnu@4.54.0':
- optional: true
-
'@rollup/rollup-linux-s390x-gnu@4.55.1':
optional: true
- '@rollup/rollup-linux-x64-gnu@4.54.0':
- optional: true
-
'@rollup/rollup-linux-x64-gnu@4.55.1':
optional: true
- '@rollup/rollup-linux-x64-musl@4.54.0':
- optional: true
-
'@rollup/rollup-linux-x64-musl@4.55.1':
optional: true
'@rollup/rollup-openbsd-x64@4.55.1':
optional: true
- '@rollup/rollup-openharmony-arm64@4.54.0':
- optional: true
-
'@rollup/rollup-openharmony-arm64@4.55.1':
optional: true
- '@rollup/rollup-win32-arm64-msvc@4.54.0':
- optional: true
-
'@rollup/rollup-win32-arm64-msvc@4.55.1':
optional: true
- '@rollup/rollup-win32-ia32-msvc@4.54.0':
- optional: true
-
'@rollup/rollup-win32-ia32-msvc@4.55.1':
optional: true
- '@rollup/rollup-win32-x64-gnu@4.54.0':
- optional: true
-
'@rollup/rollup-win32-x64-gnu@4.55.1':
optional: true
- '@rollup/rollup-win32-x64-msvc@4.54.0':
- optional: true
-
'@rollup/rollup-win32-x64-msvc@4.55.1':
optional: true
@@ -22383,10 +22263,10 @@ snapshots:
vue: 3.5.26(typescript@5.9.3)
vue-router: 4.6.4(vue@3.5.26(typescript@5.9.3))
- '@vercel/nft@0.29.4(rollup@4.54.0)':
+ '@vercel/nft@0.29.4(rollup@4.55.1)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.0
- '@rollup/pluginutils': 5.2.0(rollup@4.54.0)
+ '@rollup/pluginutils': 5.2.0(rollup@4.55.1)
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
async-sema: 3.1.1
@@ -22487,6 +22367,15 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.0.3
+ '@vitest/expect@4.0.17':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.0.17
+ '@vitest/utils': 4.0.17
+ chai: 6.2.2
+ tinyrainbow: 3.0.3
+
'@vitest/mocker@4.0.16(msw@2.12.7(@types/node@24.10.1)(typescript@5.9.3))(vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.0.16
@@ -22505,6 +22394,15 @@ snapshots:
msw: 2.12.7(@types/node@25.0.6)(typescript@5.9.3)
vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
+ '@vitest/mocker@4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
+ dependencies:
+ '@vitest/spy': 4.0.17
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ msw: 2.12.7(@types/node@25.0.6)(typescript@5.9.3)
+ vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
+
'@vitest/pretty-format@4.0.16':
dependencies:
tinyrainbow: 3.0.3
@@ -22537,6 +22435,8 @@ snapshots:
'@vitest/spy@4.0.16': {}
+ '@vitest/spy@4.0.17': {}
+
'@vitest/utils@4.0.16':
dependencies:
'@vitest/pretty-format': 4.0.16
@@ -22960,7 +22860,7 @@ snapshots:
babel-plugin-react-compiler@1.0.0:
dependencies:
- '@babel/types': 7.28.5
+ '@babel/types': 7.28.6
babel-plugin-react-native-web@0.21.2: {}
@@ -23846,7 +23746,7 @@ snapshots:
dependencies:
'@cspell/cspell-types': 9.4.0
comment-json: 4.4.1
- smol-toml: 1.5.2
+ smol-toml: 1.6.0
yaml: 2.8.2
cspell-dictionary@9.4.0:
@@ -25569,7 +25469,7 @@ snapshots:
function-bind@1.1.2: {}
- geist@1.4.2(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1)):
+ geist@1.4.2(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1)):
dependencies:
next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.97.1)
@@ -26227,7 +26127,7 @@ snapshots:
istanbul-lib-instrument@6.0.3:
dependencies:
'@babel/core': 7.28.5
- '@babel/parser': 7.28.5
+ '@babel/parser': 7.28.6
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
semver: 7.7.3
@@ -26839,8 +26739,8 @@ snapshots:
magicast@0.5.1:
dependencies:
- '@babel/parser': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/parser': 7.28.6
+ '@babel/types': 7.28.6
source-map-js: 1.2.1
mailchecker@6.0.18: {}
@@ -27300,7 +27200,7 @@ snapshots:
metro-transform-plugins@0.83.3:
dependencies:
'@babel/core': 7.28.5
- '@babel/generator': 7.28.5
+ '@babel/generator': 7.28.6
'@babel/template': 7.27.2
'@babel/traverse': 7.28.5
flow-enums-runtime: 0.0.6
@@ -27331,9 +27231,9 @@ snapshots:
metro-transform-worker@0.83.3:
dependencies:
'@babel/core': 7.28.5
- '@babel/generator': 7.28.5
- '@babel/parser': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/generator': 7.28.6
+ '@babel/parser': 7.28.6
+ '@babel/types': 7.28.6
flow-enums-runtime: 0.0.6
metro: 0.83.3
metro-babel-transformer: 0.83.3
@@ -27399,11 +27299,11 @@ snapshots:
dependencies:
'@babel/code-frame': 7.27.1
'@babel/core': 7.28.5
- '@babel/generator': 7.28.5
- '@babel/parser': 7.28.5
+ '@babel/generator': 7.28.6
+ '@babel/parser': 7.28.6
'@babel/template': 7.27.2
'@babel/traverse': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/types': 7.28.6
accepts: 1.3.8
chalk: 4.1.2
ci-info: 2.0.0
@@ -28018,15 +27918,15 @@ snapshots:
nitropack@2.12.4(@azure/identity@4.13.0)(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(@netlify/blobs@10.5.0)(better-sqlite3@12.5.0)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260103.0)(@electric-sql/pglite@0.3.14)(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.2.0(prisma@7.2.0(@types/react@19.2.7)(better-sqlite3@12.5.0)(magicast@0.3.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(better-sqlite3@12.5.0)(bun-types@1.3.5)(kysely@0.28.9)(mysql2@3.16.0)(pg@8.16.3)(postgres@3.4.7)(prisma@7.2.0(@types/react@19.2.7)(better-sqlite3@12.5.0)(magicast@0.3.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.16.0)(rolldown@1.0.0-beta.59):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
- '@netlify/functions': 3.1.10(rollup@4.54.0)
- '@rollup/plugin-alias': 5.1.1(rollup@4.54.0)
- '@rollup/plugin-commonjs': 28.0.6(rollup@4.54.0)
- '@rollup/plugin-inject': 5.0.5(rollup@4.54.0)
- '@rollup/plugin-json': 6.1.0(rollup@4.54.0)
- '@rollup/plugin-node-resolve': 16.0.1(rollup@4.54.0)
- '@rollup/plugin-replace': 6.0.2(rollup@4.54.0)
- '@rollup/plugin-terser': 0.4.4(rollup@4.54.0)
- '@vercel/nft': 0.29.4(rollup@4.54.0)
+ '@netlify/functions': 3.1.10(rollup@4.55.1)
+ '@rollup/plugin-alias': 5.1.1(rollup@4.55.1)
+ '@rollup/plugin-commonjs': 28.0.6(rollup@4.55.1)
+ '@rollup/plugin-inject': 5.0.5(rollup@4.55.1)
+ '@rollup/plugin-json': 6.1.0(rollup@4.55.1)
+ '@rollup/plugin-node-resolve': 16.0.1(rollup@4.55.1)
+ '@rollup/plugin-replace': 6.0.2(rollup@4.55.1)
+ '@rollup/plugin-terser': 0.4.4(rollup@4.55.1)
+ '@vercel/nft': 0.29.4(rollup@4.55.1)
archiver: 7.0.1
c12: 3.3.2(magicast@0.3.5)
chokidar: 4.0.3
@@ -28068,8 +27968,8 @@ snapshots:
pkg-types: 2.3.0
pretty-bytes: 6.1.1
radix3: 1.1.2
- rollup: 4.54.0
- rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.59)(rollup@4.54.0)
+ rollup: 4.55.1
+ rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.59)(rollup@4.55.1)
scule: 1.3.0
semver: 7.7.3
serve-placeholder: 2.0.2
@@ -29803,7 +29703,7 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.59
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.59
- rollup-plugin-visualizer@6.0.3(rolldown@1.0.0-beta.59)(rollup@4.54.0):
+ rollup-plugin-visualizer@6.0.3(rolldown@1.0.0-beta.59)(rollup@4.55.1):
dependencies:
open: 8.4.2
picomatch: 4.0.3
@@ -29811,35 +29711,7 @@ snapshots:
yargs: 17.7.2
optionalDependencies:
rolldown: 1.0.0-beta.59
- rollup: 4.54.0
-
- rollup@4.54.0:
- dependencies:
- '@types/estree': 1.0.8
- optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.54.0
- '@rollup/rollup-android-arm64': 4.54.0
- '@rollup/rollup-darwin-arm64': 4.54.0
- '@rollup/rollup-darwin-x64': 4.54.0
- '@rollup/rollup-freebsd-arm64': 4.54.0
- '@rollup/rollup-freebsd-x64': 4.54.0
- '@rollup/rollup-linux-arm-gnueabihf': 4.54.0
- '@rollup/rollup-linux-arm-musleabihf': 4.54.0
- '@rollup/rollup-linux-arm64-gnu': 4.54.0
- '@rollup/rollup-linux-arm64-musl': 4.54.0
- '@rollup/rollup-linux-loong64-gnu': 4.54.0
- '@rollup/rollup-linux-ppc64-gnu': 4.54.0
- '@rollup/rollup-linux-riscv64-gnu': 4.54.0
- '@rollup/rollup-linux-riscv64-musl': 4.54.0
- '@rollup/rollup-linux-s390x-gnu': 4.54.0
- '@rollup/rollup-linux-x64-gnu': 4.54.0
- '@rollup/rollup-linux-x64-musl': 4.54.0
- '@rollup/rollup-openharmony-arm64': 4.54.0
- '@rollup/rollup-win32-arm64-msvc': 4.54.0
- '@rollup/rollup-win32-ia32-msvc': 4.54.0
- '@rollup/rollup-win32-x64-gnu': 4.54.0
- '@rollup/rollup-win32-x64-msvc': 4.54.0
- fsevents: 2.3.3
+ rollup: 4.55.1
rollup@4.55.1:
dependencies:
@@ -30285,8 +30157,6 @@ snapshots:
smob@1.5.0: {}
- smol-toml@1.5.2: {}
-
smol-toml@1.6.0: {}
solid-js@1.9.9:
@@ -31312,7 +31182,7 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
- rollup: 4.54.0
+ rollup: 4.55.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 25.0.6
@@ -31445,6 +31315,45 @@ snapshots:
- tsx
- yaml
+ vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@25.0.6)(happy-dom@20.0.11)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
+ dependencies:
+ '@vitest/expect': 4.0.17
+ '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
+ '@vitest/pretty-format': 4.0.17
+ '@vitest/runner': 4.0.17
+ '@vitest/snapshot': 4.0.17
+ '@vitest/spy': 4.0.17
+ '@vitest/utils': 4.0.17
+ es-module-lexer: 1.7.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 1.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@opentelemetry/api': 1.9.0
+ '@types/node': 25.0.6
+ happy-dom: 20.0.11
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - yaml
+
vlq@1.0.1: {}
vscode-languageserver-textdocument@1.0.12: {}