diff --git a/apps/docs/content/docs/dev/auth.mdx b/apps/docs/content/docs/dev/config/auth.mdx similarity index 97% rename from apps/docs/content/docs/dev/auth.mdx rename to apps/docs/content/docs/dev/config/auth.mdx index 686c371cb..83e5104be 100644 --- a/apps/docs/content/docs/dev/auth.mdx +++ b/apps/docs/content/docs/dev/config/auth.mdx @@ -13,9 +13,7 @@ You can configure the authentication settings in the `VitNodeAPI` function and ` For example you can change the expiration time of the cookie: -import { TypeTable } from 'fumadocs-ui/components/type-table'; - -```ts title="src/app/api/[...route]/route.ts" +```ts title="src/vitnode.api.config.ts" VitNodeAPI({ app, plugins: [], @@ -28,6 +26,8 @@ VitNodeAPI({ }); ``` +import { TypeTable } from 'fumadocs-ui/components/type-table'; + ### Options diff --git a/apps/docs/content/docs/dev/deployments/cloud/meta.json b/apps/docs/content/docs/dev/deployments/cloud/meta.json new file mode 100644 index 000000000..9b083a7d9 --- /dev/null +++ b/apps/docs/content/docs/dev/deployments/cloud/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Cloud", + "pages": ["..."] +} diff --git a/apps/docs/content/docs/dev/deployments/meta.json b/apps/docs/content/docs/dev/deployments/meta.json new file mode 100644 index 000000000..35ccfdba5 --- /dev/null +++ b/apps/docs/content/docs/dev/deployments/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Deployments", + "icon": "HardDriveUpload", + "pages": ["..."] +} diff --git a/apps/docs/content/docs/dev/deployments/self-hosted/meta.json b/apps/docs/content/docs/dev/deployments/self-hosted/meta.json new file mode 100644 index 000000000..5b6b3ae79 --- /dev/null +++ b/apps/docs/content/docs/dev/deployments/self-hosted/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Self-Hosted", + "pages": ["..."] +} diff --git a/apps/docs/content/docs/dev/meta.json b/apps/docs/content/docs/dev/meta.json index 990412e35..8e654a7bb 100644 --- a/apps/docs/content/docs/dev/meta.json +++ b/apps/docs/content/docs/dev/meta.json @@ -6,11 +6,9 @@ "pages": [ "index", "contribution", - "structure", "debugging", "swagger", - "---Framework---", - "auth", + "deployments", "---Plugins---", "plugins", "rest-api", @@ -20,6 +18,8 @@ "layouts-and-pages", "admin-page", "i18n", + "---Framework---", + "config", "---Advanced---", "..." ] diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css index a3f1e1bf4..83d5e327d 100644 --- a/apps/docs/src/app/global.css +++ b/apps/docs/src/app/global.css @@ -22,8 +22,8 @@ --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); + --border: oklch(0.9 0 0); + --input: oklch(0.9 0 0); --ring: oklch(0.7 0.16 262.61); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); @@ -45,22 +45,23 @@ } .dark { - --background: oklch(0.12 0 0); - --foreground: oklch(0.98 0.005 240); - --card: oklch(0.18 0.005 240); - --card-foreground: oklch(0.98 0.005 240); - --popover: oklch(0.18 0.005 240); - --popover-foreground: oklch(0.98 0.005 240); + --background: oklch(0.14 0 0); + --foreground: oklch(0.98 0 0); + --card: oklch(0.18 0 0); + --card-foreground: oklch(0.98 0 0); + --popover: oklch(0.18 0 0); + --popover-foreground: oklch(0.98 0 0); --primary: oklch(0.51 0.16 262.61); - --primary-foreground: oklch(0.99 0.005 240); + --primary-foreground: oklch(0.99 0.16 262.61); --secondary: oklch(0.24 0.01 240); --secondary-foreground: oklch(0.98 0.005 240); - --muted: oklch(0.22 0.01 240); + --muted: oklch(0.22 0 0); --muted-foreground: oklch(0.7 0 0); - --accent: oklch(0.28 0.015 240); + --accent: oklch(0.28 0 0); --destructive: oklch(0.704 0.191 22.216); - --border: oklch(0.269 0.005 240); - --input: oklch(0.269 0.005 240); + --destructive-foreground: oklch(0.704 0.191 22.216); + --border: oklch(0.24 0 0); + --input: oklch(0.24 0 0); --ring: oklch(0.51 0.16 262.61); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); @@ -68,12 +69,12 @@ --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: var(--card); - --sidebar-foreground: oklch(0.98 0.005 240); + --sidebar-foreground: oklch(0.98 0 0); --sidebar-primary: var(--primary); --sidebar-primary-foreground: var(--primary-foreground); - --sidebar-accent: oklch(0.22 0.005 240); - --sidebar-accent-foreground: oklch(0.98 0.005 240); - --sidebar-border: oklch(0.269 0.005 240); + --sidebar-accent: oklch(0.22 0 0); + --sidebar-accent-foreground: oklch(0.98 0 0); + --sidebar-border: oklch(0.269 0 0); --sidebar-ring: oklch(0.51 0.16 262.61); --dev-color: oklch(0.75 0.18 50); diff --git a/apps/web/src/app/global.css b/apps/web/src/app/global.css index 957e4f23c..1457f4a03 100644 --- a/apps/web/src/app/global.css +++ b/apps/web/src/app/global.css @@ -24,8 +24,8 @@ --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); + --border: oklch(0.9 0 0); + --input: oklch(0.9 0 0); --ring: oklch(0.7 0.16 262.61); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); @@ -43,22 +43,23 @@ } .dark { - --background: oklch(0.12 0 0); - --foreground: oklch(0.98 0.005 240); - --card: oklch(0.18 0.005 240); - --card-foreground: oklch(0.98 0.005 240); - --popover: oklch(0.18 0.005 240); - --popover-foreground: oklch(0.98 0.005 240); + --background: oklch(0.14 0 0); + --foreground: oklch(0.98 0 0); + --card: oklch(0.18 0 0); + --card-foreground: oklch(0.98 0 0); + --popover: oklch(0.18 0 0); + --popover-foreground: oklch(0.98 0 0); --primary: oklch(0.51 0.16 262.61); - --primary-foreground: oklch(0.99 0.005 240); + --primary-foreground: oklch(0.99 0.16 262.61); --secondary: oklch(0.24 0.01 240); --secondary-foreground: oklch(0.98 0.005 240); - --muted: oklch(0.22 0.01 240); + --muted: oklch(0.22 0 0); --muted-foreground: oklch(0.7 0 0); - --accent: oklch(0.28 0.015 240); + --accent: oklch(0.28 0 0); --destructive: oklch(0.704 0.191 22.216); - --border: oklch(0.269 0.005 240); - --input: oklch(0.269 0.005 240); + --destructive-foreground: oklch(0.704 0.191 22.216); + --border: oklch(0.24 0 0); + --input: oklch(0.24 0 0); --ring: oklch(0.51 0.16 262.61); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); @@ -66,12 +67,12 @@ --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: var(--card); - --sidebar-foreground: oklch(0.98 0.005 240); + --sidebar-foreground: oklch(0.98 0 0); --sidebar-primary: var(--primary); --sidebar-primary-foreground: var(--primary-foreground); - --sidebar-accent: oklch(0.22 0.005 240); - --sidebar-accent-foreground: oklch(0.98 0.005 240); - --sidebar-border: oklch(0.269 0.005 240); + --sidebar-accent: oklch(0.22 0 0); + --sidebar-accent-foreground: oklch(0.98 0 0); + --sidebar-border: oklch(0.269 0 0); --sidebar-ring: oklch(0.51 0.16 262.61); } diff --git a/apps/web/src/vitnode.api.config.ts b/apps/web/src/vitnode.api.config.ts index 748a4fa60..937195701 100644 --- a/apps/web/src/vitnode.api.config.ts +++ b/apps/web/src/vitnode.api.config.ts @@ -1,8 +1,8 @@ import { blogApiPlugin } from '@vitnode/blog/config.api'; -import { NodemailerEmailPlugin } from '@vitnode/core/api/plugins/email/nodemailer'; -import { DiscordSSOApiPlugin } from '@vitnode/core/api/plugins/sso/discord'; -import { FacebookSSOApiPlugin } from '@vitnode/core/api/plugins/sso/facebook'; -import { GoogleSSOApiPlugin } from '@vitnode/core/api/plugins/sso/google'; +import { NodemailerEmailAdapter } from '@vitnode/core/api/adapters/email/nodemailer'; +import { DiscordSSOApiPlugin } from '@vitnode/core/api/adapters/sso/discord'; +import { FacebookSSOApiPlugin } from '@vitnode/core/api/adapters/sso/facebook'; +import { GoogleSSOApiPlugin } from '@vitnode/core/api/adapters/sso/google'; import { buildApiConfig } from '@vitnode/core/vitnode.config'; import * as dotenv from 'dotenv'; import { drizzle } from 'drizzle-orm/postgres-js'; @@ -22,12 +22,16 @@ export const vitNodeApiConfig = buildApiConfig({ connection: POSTGRES_URL, casing: 'camelCase', }), - // emailProvider: NodemailerEmailPlugin({ - // from: process.env.NODE_MAILER_FROM, - // host: process.env.NODE_MAILER_HOST, - // password: process.env.NODE_MAILER_PASSWORD, - // user: process.env.NOD_EMAILER_USER, - // }), + rateLimiter: { + points: 20, // 20 requests + duration: 60, // per 60 seconds + }, + emailAdapter: NodemailerEmailAdapter({ + from: process.env.NODE_MAILER_FROM, + host: process.env.NODE_MAILER_HOST, + password: process.env.NODE_MAILER_PASSWORD, + user: process.env.NOD_EMAILER_USER, + }), authorization: { ssoProviders: [ DiscordSSOApiPlugin({ diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index 611ea329c..8bbbb9b57 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -19,21 +19,21 @@ "core" ], "peerDependencies": { + "@hono/zod-openapi": "0.19.x", + "@swc/cli": "0.6.x", + "@swc/core": "1.12.x", + "@types/react": "19.1.x", + "@types/react-dom": "19.1.x", "drizzle-kit": "0.31.x", "drizzle-orm": "^0.44.x", "hono": "4.8.x", "next": "15.3.x", + "next-intl": "4.1.x", "react": "19.1.x", "react-dom": "19.1.x", - "@swc/cli": "0.6.x", - "@hono/zod-openapi": "0.19.x", - "next-intl": "4.1.x", "react-hook-form": "^7.0.0", - "zod": "3.25.x", - "@swc/core": "1.12.x", - "@types/react": "19.1.x", - "@types/react-dom": "19.1.x", - "typescript": "^5.8.x" + "typescript": "^5.8.x", + "zod": "3.25.x" }, "devDependencies": { "@hono/zod-openapi": "^0.19.8", @@ -119,6 +119,7 @@ "nodemailer": "^7.0.3", "postgres": "^3.4.7", "radix-ui": "^1.4.2", + "rate-limiter-flexible": "^7.1.1", "react-scan": "^0.3.4", "resend": "^4.6.0", "tailwind-merge": "^3.3.1", diff --git a/packages/vitnode/src/api/plugins/email/nodemailer.ts b/packages/vitnode/src/api/adapters/email/nodemailer.ts similarity index 96% rename from packages/vitnode/src/api/plugins/email/nodemailer.ts rename to packages/vitnode/src/api/adapters/email/nodemailer.ts index 24ca59b41..57e856203 100644 --- a/packages/vitnode/src/api/plugins/email/nodemailer.ts +++ b/packages/vitnode/src/api/adapters/email/nodemailer.ts @@ -2,7 +2,7 @@ import { createTransport } from 'nodemailer'; import type { EmailApiPlugin } from '@/api/models/email'; -export const NodemailerEmailPlugin = ({ +export const NodemailerEmailAdapter = ({ host = '', port = 587, secure = false, diff --git a/packages/vitnode/src/api/plugins/email/resend.ts b/packages/vitnode/src/api/adapters/email/resend.ts similarity index 95% rename from packages/vitnode/src/api/plugins/email/resend.ts rename to packages/vitnode/src/api/adapters/email/resend.ts index d14fa6cfd..26dd0986d 100644 --- a/packages/vitnode/src/api/plugins/email/resend.ts +++ b/packages/vitnode/src/api/adapters/email/resend.ts @@ -2,7 +2,7 @@ import { Resend } from 'resend'; import type { EmailApiPlugin } from '@/api/models/email'; -export const ResendEmailPlugin = ({ +export const ResendEmailAdapter = ({ apiKey, from, }: { diff --git a/packages/vitnode/src/api/plugins/sso/discord.ts b/packages/vitnode/src/api/adapters/sso/discord.ts similarity index 100% rename from packages/vitnode/src/api/plugins/sso/discord.ts rename to packages/vitnode/src/api/adapters/sso/discord.ts diff --git a/packages/vitnode/src/api/plugins/sso/facebook.ts b/packages/vitnode/src/api/adapters/sso/facebook.ts similarity index 100% rename from packages/vitnode/src/api/plugins/sso/facebook.ts rename to packages/vitnode/src/api/adapters/sso/facebook.ts diff --git a/packages/vitnode/src/api/plugins/sso/google.ts b/packages/vitnode/src/api/adapters/sso/google.ts similarity index 100% rename from packages/vitnode/src/api/plugins/sso/google.ts rename to packages/vitnode/src/api/adapters/sso/google.ts diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index 719424908..13511351f 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -14,7 +14,8 @@ import { CONFIG_PLUGIN } from '@/config'; import { globalAdminMiddleware, globalMiddleware, -} from './middlewares/global/global'; +} from './middlewares/global.middleware'; +import { rateLimiterMiddleware } from './middlewares/rate-limiter.middleware'; interface CORSOptions { allowHeaders?: string[]; @@ -55,11 +56,12 @@ export function VitNodeAPI({ }); app.use(cors(corsOptions)); app.use(csrf(csrfOptions)); + app.use('*', rateLimiterMiddleware(vitNodeApiConfig.rateLimiter)); app.get('/swagger', swaggerUI({ url: `/api/swagger/doc` })); app.use( '*', globalMiddleware({ - emailProvider: vitNodeApiConfig.emailProvider, + emailAdapter: vitNodeApiConfig.emailAdapter, metadata: vitNodeConfig.metadata, authorization: vitNodeApiConfig.authorization, dbProvider: vitNodeApiConfig.dbProvider, diff --git a/packages/vitnode/src/api/lib/route.ts b/packages/vitnode/src/api/lib/route.ts index 0854aedd2..a63e72d0b 100644 --- a/packages/vitnode/src/api/lib/route.ts +++ b/packages/vitnode/src/api/lib/route.ts @@ -5,7 +5,7 @@ import { createRoute as createRouteHono } from '@hono/zod-openapi'; import { type EnvVitNode, pluginMiddleware, -} from '../middlewares/global/global'; +} from '../middlewares/global.middleware'; type RoutingPath

= P extends `${infer Head}/{${infer Param}}${infer Tail}` diff --git a/packages/vitnode/src/api/middlewares/global/global.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts similarity index 95% rename from packages/vitnode/src/api/middlewares/global/global.ts rename to packages/vitnode/src/api/middlewares/global.middleware.ts index bad5dec0f..1edd9c5d8 100644 --- a/packages/vitnode/src/api/middlewares/global/global.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -8,7 +8,7 @@ import type { VitNodeApiConfig, VitNodeConfig } from '@/vitnode.config'; import { SessionModel } from '@/api/models/session'; import { SessionAdminModel } from '@/api/models/session-admin'; -import type { SSOApiPlugin } from '../../models/sso'; +import type { SSOApiPlugin } from '../models/sso'; export interface EnvVitNode extends Env { Variables: EnvVariablesVitNode; @@ -40,7 +40,7 @@ interface EnvVariablesVitNode { deviceCookieName: string; ssoProviders: SSOApiPlugin[]; }; - emailProvider?: EmailApiPlugin; + emailAdapter?: EmailApiPlugin; metadata: { shortTitle?: string; title: string; @@ -72,16 +72,16 @@ declare module 'hono' { export const globalMiddleware = ({ authorization, metadata, - emailProvider, + emailAdapter, dbProvider, -}: Pick & +}: Pick & Pick) => { return async (c: Context, next: Next) => { c.set('db', dbProvider); c.set('core', { metadata, - emailProvider, + emailAdapter, authorization: { cookieName: authorization?.cookieName ?? 'vitnode_auth', cookie_expires: diff --git a/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts new file mode 100644 index 000000000..9d60ea859 --- /dev/null +++ b/packages/vitnode/src/api/middlewares/rate-limiter.middleware.ts @@ -0,0 +1,45 @@ +import type { Context, Next } from 'hono'; + +import { + type IRateLimiterOptions, + type RateLimiterAbstract, + RateLimiterMemory, +} from 'rate-limiter-flexible'; + +const createRateLimiter = ({ + keyPrefix, + ...options +}: { + keyPrefix: string; +} & Omit): RateLimiterAbstract => { + return new RateLimiterMemory({ + keyPrefix, + points: options?.points ?? 20, // 20 requests + duration: options?.duration ?? 60, // per 60 seconds + ...options, + }); +}; + +export const rateLimiterMiddleware = ( + options?: Omit, +) => { + const rateLimiter = createRateLimiter({ + ...options, + keyPrefix: 'vitnode-api-rate-limiter', + }); + + return async (c: Context, next: Next) => { + const key = + c.req.header('x-forwarded-for') ?? + c.req.raw.headers.get('x-real-ip') ?? + '127.0.0.1'; + + try { + await rateLimiter.consume(key); + + await next(); + } catch { + return c.text('Too Many Requests', 429); + } + }; +}; diff --git a/packages/vitnode/src/api/models/email.ts b/packages/vitnode/src/api/models/email.ts index 549c851e8..d0eb6f956 100644 --- a/packages/vitnode/src/api/models/email.ts +++ b/packages/vitnode/src/api/models/email.ts @@ -19,10 +19,6 @@ export class EmailModel { protected readonly c: Context; - isAvailable() { - return !!this.c.get('core').emailProvider; - } - async send(args: { html: string; replyTo?: string; @@ -30,7 +26,7 @@ export class EmailModel { to: string; }) { const core = this.c.get('core'); - const provider = core.emailProvider; + const provider = core.emailAdapter; if (!provider) { throw new HTTPException(500, { message: 'Email provider not found', diff --git a/packages/vitnode/src/api/models/user/sign-up.ts b/packages/vitnode/src/api/models/user/sign-up.ts index 4907ae6e1..d5f3a6615 100644 --- a/packages/vitnode/src/api/models/user/sign-up.ts +++ b/packages/vitnode/src/api/models/user/sign-up.ts @@ -60,7 +60,7 @@ const getDefaultData = async ( return { roleId: defaultRole.id, - emailVerified: !c.get('core').emailProvider, + emailVerified: !c.get('core').emailAdapter, }; }; diff --git a/packages/vitnode/src/api/modules/middleware/route.ts b/packages/vitnode/src/api/modules/middleware/route.ts index e8f33690a..985038652 100644 --- a/packages/vitnode/src/api/modules/middleware/route.ts +++ b/packages/vitnode/src/api/modules/middleware/route.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; import { buildRoute } from '@/api/lib/route'; -import { EmailModel } from '@/api/models/email'; import { CONFIG_PLUGIN } from '@/config'; export const routeMiddleware = buildRoute({ @@ -26,10 +25,9 @@ export const routeMiddleware = buildRoute({ }, handler: c => { const sso = c.get('core').authorization.ssoProviders; - const email = new EmailModel(c); return c.json({ - isEmail: email.isAvailable(), + isEmail: !!c.get('core').emailAdapter, sso: sso.map(s => ({ id: s.id, name: s.name })), }); }, diff --git a/packages/vitnode/src/api/modules/users/routes/test.route.ts b/packages/vitnode/src/api/modules/users/routes/test.route.ts index e441a8e5b..20b03143c 100644 --- a/packages/vitnode/src/api/modules/users/routes/test.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/test.route.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { buildRoute } from '@/api/lib/route'; import { CONFIG_PLUGIN } from '@/config'; +// import { EmailModel } from '../../../models/email'; export const testRoute = buildRoute({ ...CONFIG_PLUGIN, @@ -29,6 +30,12 @@ export const testRoute = buildRoute({ }, }, handler: c => { + // await new EmailModel(c).send({ + // html: '

Test email

', + // to: 'ithereplay@gmail.com', + // subject: 'Test Email', + // }); + return c.text('test'); }, }); diff --git a/packages/vitnode/src/vitnode.config.ts b/packages/vitnode/src/vitnode.config.ts index 18ece55a0..cca032094 100644 --- a/packages/vitnode/src/vitnode.config.ts +++ b/packages/vitnode/src/vitnode.config.ts @@ -1,5 +1,6 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import type { ThemeProvider } from 'next-themes'; +import type { IRateLimiterOptions } from 'rate-limiter-flexible'; import type { BuildPluginApiReturn } from './api/lib/plugin'; import type { EmailApiPlugin } from './api/models/email'; @@ -47,8 +48,9 @@ export interface VitNodeApiConfig { ssoProviders?: SSOApiPlugin[]; }; dbProvider: PostgresJsDatabase; - emailProvider?: EmailApiPlugin; + emailAdapter?: EmailApiPlugin; plugins: BuildPluginApiReturn[]; + rateLimiter?: Omit; } export function buildConfig( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 300124b94..e106e5f01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,7 +134,7 @@ importers: version: 15.3.4(@playwright/test@1.53.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-intl: specifier: ^4.1.0 - version: 4.1.0(next@15.3.4(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + version: 4.1.0(next@15.3.4(@playwright/test@1.53.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react: specifier: ^19.1.0 version: 19.1.0 @@ -306,6 +306,9 @@ importers: radix-ui: specifier: ^1.4.2 version: 1.4.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + rate-limiter-flexible: + specifier: ^7.1.1 + version: 7.1.1 react-scan: specifier: ^0.3.4 version: 0.3.4(@types/react@19.1.8)(next@15.3.4(@babel/core@7.27.4)(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.43.0) @@ -465,7 +468,7 @@ importers: version: 15.3.4(@playwright/test@1.53.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next-intl: specifier: ^4.1.0 - version: 4.1.0(next@15.3.4(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + version: 4.1.0(next@15.3.4(@playwright/test@1.53.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) react: specifier: ^19.1.0 version: 19.1.0 @@ -5522,6 +5525,9 @@ packages: '@types/react-dom': optional: true + rate-limiter-flexible@7.1.1: + resolution: {integrity: sha512-lsYRcqRSJrKBNt6pMzBJTiCJP5KnwsGWdObMZxd19JFUJRntM+yuHs4/2bs6NZweSLgpsDcykvzyQaumoslWQg==} + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -11538,7 +11544,7 @@ snapshots: optionalDependencies: typescript: 5.8.3 - next-intl@4.1.0(next@15.3.4(@playwright/test@1.53.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + next-intl@4.1.0(next@15.3.4(@playwright/test@1.53.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3): dependencies: '@formatjs/intl-localematcher': 0.5.10 negotiator: 1.0.0 @@ -12032,6 +12038,8 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + rate-limiter-flexible@7.1.1: {} + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0