mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 07:18:56 -05:00
fix(rate-limit): support IPv6 address normalization and subnet (#7470)
This commit is contained in:
@@ -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
|
||||
|
||||
<Callout type="info">
|
||||
IPv6 subnet configuration only affects IPv6 addresses. IPv4 addresses are always rate limited individually.
|
||||
</Callout>
|
||||
|
||||
### Rate Limit Window
|
||||
|
||||
```ts title="auth.ts"
|
||||
|
||||
46
e2e/smoke/test/fixtures/ipv6/index.ts
vendored
Normal file
46
e2e/smoke/test/fixtures/ipv6/index.ts
vendored
Normal file
@@ -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);
|
||||
},
|
||||
);
|
||||
10
e2e/smoke/test/fixtures/ipv6/package.json
vendored
Normal file
10
e2e/smoke/test/fixtures/ipv6/package.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
10
e2e/smoke/test/fixtures/ipv6/tsconfig.json
vendored
Normal file
10
e2e/smoke/test/fixtures/ipv6/tsconfig.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
205
e2e/smoke/test/ipv6.spec.ts
Normal file
205
e2e/smoke/test/ipv6.spec.ts
Normal file
@@ -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<number>((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<number>((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<number>((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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<string, string>();
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
243
packages/core/src/utils/ip.test.ts
Normal file
243
packages/core/src/utils/ip.test.ts
Normal file
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
211
packages/core/src/utils/ip.ts
Normal file
211
packages/core/src/utils/ip.ts
Normal file
@@ -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}`;
|
||||
}
|
||||
43
packages/core/src/utils/url.ts
Normal file
43
packages/core/src/utils/url.ts
Normal file
@@ -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;
|
||||
}
|
||||
483
pnpm-lock.yaml
generated
483
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user