feat(core): Add limits to fields used for rate limiter keys (#24665)

This commit is contained in:
Tomi Turtiainen
2026-01-22 12:09:20 +02:00
committed by GitHub
parent ee1a77cd46
commit 8e0de0be02
19 changed files with 343 additions and 138 deletions
@@ -73,6 +73,14 @@ describe('LoginRequestDto', () => {
},
expectedErrorPath: ['password'],
},
{
name: 'emailOrLdapLoginId exceeds max length',
request: {
emailOrLdapLoginId: 'a'.repeat(256),
password: 'securePassword123',
},
expectedErrorPath: ['emailOrLdapLoginId'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = LoginRequestDto.safeParse(request);
expect(result.success).toBe(false);
@@ -7,7 +7,7 @@ export class LoginRequestDto extends Z.class({
* is not enforced here. The controller determines whether this is an
* email and validates when LDAP is disabled
*/
emailOrLdapLoginId: z.string().trim(),
emailOrLdapLoginId: z.string().trim().max(255),
password: z.string().min(1),
mfaCode: z.string().optional(),
mfaRecoveryCode: z.string().optional(),
@@ -34,6 +34,11 @@ describe('ForgotPasswordRequestDto', () => {
request: { email: '' },
expectedErrorPath: ['email'],
},
{
name: 'email exceeds max length',
request: { email: 'a'.repeat(244) + '@example.com' },
expectedErrorPath: ['email'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = ForgotPasswordRequestDto.safeParse(request);
@@ -2,5 +2,5 @@ import { z } from 'zod';
import { Z } from 'zod-class';
export class ForgotPasswordRequestDto extends Z.class({
email: z.string().email(),
email: z.string().email().max(255),
}) {}
+2 -1
View File
@@ -24,7 +24,8 @@
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@types/express": "catalog:",
"@types/lodash": "catalog:"
"@types/lodash": "catalog:",
"zod": "catalog:"
},
"dependencies": {
"@n8n/constants": "workspace:*",
@@ -1,6 +1,7 @@
import { Container } from '@n8n/di';
import { ControllerRegistryMetadata } from '../controller-registry-metadata';
import { createBodyKeyedRateLimiter } from '../rate-limit';
import { Get, Post, Put, Patch, Delete } from '../route';
import type { Controller } from '../types';
@@ -50,7 +51,11 @@ describe('Route Decorators', () => {
usesTemplates: true,
skipAuth: true,
ipRateLimit: { limit: 10, windowMs: 60000 },
keyedRateLimit: { limit: 10, windowMs: 60000, source: 'body', field: 'email' },
keyedRateLimit: createBodyKeyedRateLimiter<{ email: string }>({
limit: 10,
windowMs: 60000,
field: 'email',
}),
})
testMethod() {}
}
@@ -64,7 +69,7 @@ describe('Route Decorators', () => {
expect(routeMetadata.usesTemplates).toBe(true);
expect(routeMetadata.skipAuth).toBe(true);
expect(routeMetadata.ipRateLimit).toEqual({ limit: 10, windowMs: 60000 });
expect(routeMetadata.keyedRateLimit).toEqual({
expect(routeMetadata.keyedRateLimit).toMatchObject({
limit: 10,
windowMs: 60000,
source: 'body',
@@ -11,7 +11,13 @@ export type {
Controller,
CorsOptions,
Method,
RateLimiterLimits,
KeyedRateLimiterConfig,
StaticRouterMetadata,
} from './types';
export {
type RateLimiterLimits,
type BodyKeyedRateLimiterConfig,
type UserKeyedRateLimiterConfig,
type KeyedRateLimiterConfig,
createBodyKeyedRateLimiter,
createUserKeyedRateLimiter,
} from './rate-limit';
@@ -0,0 +1,76 @@
export interface RateLimiterLimits {
/**
* The maximum number of requests to allow during the `window` before rate limiting the client.
* @default 5
*/
limit?: number;
/**
* How long we should remember the requests.
* @default 300_000 (5 minutes)
*/
windowMs?: number;
}
/**
* Configuration for extracting a key from the request body.
*/
export interface BodyKeyedRateLimiterConfig extends RateLimiterLimits {
/** How to extract key from request */
source: 'body';
/** The field name in the request body to use as the key */
field: string;
}
/**
* Configuration for extracting a key from the authenticated user.
*/
export interface UserKeyedRateLimiterConfig extends RateLimiterLimits {
/** How to extract key from request */
source: 'user';
}
export type KeyedRateLimiterConfig = BodyKeyedRateLimiterConfig | UserKeyedRateLimiterConfig;
/**
* Create a body keyed rate limiter configuration. This ends up creating
* a rate limiter that is keyed by the value of the specified field in the
* request body.
*
* @example
* createBodyKeyedRateLimiter<LoginRequestDto>({
* field: 'email',
* limit: 10,
* windowMs: 60000,
* });
*/
export const createBodyKeyedRateLimiter = <T extends object>({
limit,
windowMs,
field,
}: RateLimiterLimits & {
field: keyof T & string;
}): BodyKeyedRateLimiterConfig => ({
source: 'body',
limit,
windowMs,
field,
});
/**
* Create a user keyed rate limiter configuration. This ends up creating
* a rate limiter that is keyed by the authenticated user's ID.
*
* @example
* createUserKeyedRateLimiter({
* limit: 10,
* windowMs: 60000,
* });
*/
export const createUserKeyedRateLimiter = ({
limit,
windowMs,
}: RateLimiterLimits): UserKeyedRateLimiterConfig => ({
source: 'user',
limit,
windowMs,
});
@@ -2,13 +2,8 @@ import { Container } from '@n8n/di';
import type { RequestHandler } from 'express';
import { ControllerRegistryMetadata } from './controller-registry-metadata';
import type {
Controller,
CorsOptions,
Method,
RateLimiterLimits,
KeyedRateLimiterConfig,
} from './types';
import type { KeyedRateLimiterConfig, RateLimiterLimits } from './rate-limit';
import type { Controller, CorsOptions, Method } from './types';
interface RouteOptions {
middlewares?: RequestHandler[];
@@ -3,6 +3,8 @@ import type { Constructable } from '@n8n/di';
import type { Scope } from '@n8n/permissions';
import type { RequestHandler, Router } from 'express';
import type { KeyedRateLimiterConfig, RateLimiterLimits } from './rate-limit';
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options';
export type Arg = { type: 'body' | 'query' } | { type: 'param'; key: string };
@@ -15,43 +17,6 @@ export interface CorsOptions {
maxAge?: number;
}
export interface RateLimiterLimits {
/**
* The maximum number of requests to allow during the `window` before rate limiting the client.
* @default 5
*/
limit?: number;
/**
* How long we should remember the requests.
* @default 300_000 (5 minutes)
*/
windowMs?: number;
}
/**
* Configuration for extracting a key from the request body.
* @example
* { source: 'body', field: 'email' }
*/
export interface BodyKeyedRateLimiterConfig extends RateLimiterLimits {
/** How to extract key from request */
source: 'body';
/** The field name in the request body to use as the key */
field: string;
}
/**
* Configuration for extracting a key from the authenticated user.
* @example
* { source: 'user' }
*/
export interface UserKeyedRateLimiterConfig extends RateLimiterLimits {
/** How to extract key from request */
source: 'user';
}
export type KeyedRateLimiterConfig = BodyKeyedRateLimiterConfig | UserKeyedRateLimiterConfig;
export type HandlerName = string;
export interface AccessScope {
@@ -15,11 +15,15 @@ import {
Licensed,
RestController,
RootLevelController,
createBodyKeyedRateLimiter,
createUserKeyedRateLimiter,
} from '@n8n/decorators';
import { Container } from '@n8n/di';
import express, { json } from 'express';
import { mock } from 'jest-mock-extended';
import { agent as testAgent } from 'supertest';
import { z } from 'zod';
import { Z } from 'zod-class';
import type { AuthService } from '@/auth/auth.service';
import { ControllerRegistry } from '@/controller.registry';
@@ -48,7 +52,7 @@ describe('ControllerRegistry', () => {
globalConfig,
metadata,
lastActiveAtService,
new RateLimitService(mock()),
new RateLimitService(),
).activate(app);
agent = testAgent(app);
});
@@ -88,19 +92,22 @@ describe('ControllerRegistry', () => {
});
describe('Body-based keyed rate limiting', () => {
class TestBodyDto extends Z.class({
email: z.string().max(20),
}) {}
@RestController('/test')
// @ts-expect-error tsc complains about unused class
class TestController {
@Post('/body-keyed', {
skipAuth: true,
keyedRateLimit: {
keyedRateLimit: createBodyKeyedRateLimiter<TestBodyDto>({
limit: 3,
windowMs: 60_000,
source: 'body',
field: 'email',
},
}),
})
bodyKeyed(@Body _body: { email: string }) {
bodyKeyed(@Body _body: TestBodyDto) {
return { ok: true };
}
}
@@ -114,7 +121,7 @@ describe('ControllerRegistry', () => {
'should not rate limit when keyed value is %s',
async (identifier) => {
for (let i = 0; i < 5; i++) {
await agent.post('/rest/test/body-keyed').send({ email: identifier }).expect(200);
await agent.post('/rest/test/body-keyed').send({ email: identifier }).expect(400);
}
},
);
@@ -130,6 +137,112 @@ describe('ControllerRegistry', () => {
await agent.post('/rest/test/body-keyed').send({ email: 'other@example.com' }).expect(200);
});
it('should not rate limit when the value does not match the schema', async () => {
const tooLongEmail = 'a'.repeat(21);
for (let i = 0; i < 4; i++) {
await agent.post('/rest/test/body-keyed').send({ email: tooLongEmail }).expect(400);
}
});
it('should not rate limit when no body is sent', async () => {
for (let i = 0; i < 5; i++) {
await agent.post('/rest/test/body-keyed').expect(400);
}
});
it('should not rate limit when the field is missing from body', async () => {
for (let i = 0; i < 5; i++) {
await agent.post('/rest/test/body-keyed').send({}).expect(400);
}
for (let i = 0; i < 5; i++) {
await agent.post('/rest/test/body-keyed').send({ other: 'value' }).expect(400);
}
});
it('should not rate limit when numeric value fails string schema validation', async () => {
for (let i = 0; i < 5; i++) {
await agent.post('/rest/test/body-keyed').send({ email: 123 }).expect(400);
}
});
it('should rate limit empty string if it passes schema validation', async () => {
const emptyEmail = '';
for (let i = 0; i < 3; i++) {
await agent.post('/rest/test/body-keyed').send({ email: emptyEmail }).expect(200);
}
await agent.post('/rest/test/body-keyed').send({ email: emptyEmail }).expect(429);
});
it('should maintain separate rate limit counters for different identifiers', async () => {
const email1 = 'user1@example.com';
const email2 = 'user2@example.com';
const email3 = 'user3@example.com';
// Each email should have its own counter
await agent.post('/rest/test/body-keyed').send({ email: email1 }).expect(200); // email1: 1
await agent.post('/rest/test/body-keyed').send({ email: email2 }).expect(200); // email2: 1
await agent.post('/rest/test/body-keyed').send({ email: email1 }).expect(200); // email1: 2
await agent.post('/rest/test/body-keyed').send({ email: email3 }).expect(200); // email3: 1
await agent.post('/rest/test/body-keyed').send({ email: email1 }).expect(200); // email1: 3
// email1 should be rate limited now
await agent.post('/rest/test/body-keyed').send({ email: email1 }).expect(429);
// But email2 and email3 should still work
await agent.post('/rest/test/body-keyed').send({ email: email2 }).expect(200); // email2: 2
await agent.post('/rest/test/body-keyed').send({ email: email3 }).expect(200); // email3: 2
});
});
describe('Body-based keyed rate limiting with numeric field', () => {
class TestNumericDto extends Z.class({
userId: z.number(),
}) {}
@RestController('/test')
// @ts-expect-error tsc complains about unused class
class TestController {
@Post('/numeric-keyed', {
skipAuth: true,
keyedRateLimit: createBodyKeyedRateLimiter<TestNumericDto>({
limit: 3,
windowMs: 60_000,
field: 'userId',
}),
})
numericKeyed(@Body _body: TestNumericDto) {
return { ok: true };
}
}
beforeAll(() => {
authMiddleware.mockImplementation(async (_req, _res, next) => next());
lastActiveAtService.middleware.mockImplementation(async (_req, _res, next) => next());
});
it('should apply keyed rate limiting based on numeric field', async () => {
const userId = 12345;
for (let i = 0; i < 3; i++) {
await agent.post('/rest/test/numeric-keyed').send({ userId }).expect(200);
}
await agent.post('/rest/test/numeric-keyed').send({ userId }).expect(429);
// Different numeric ID should work
await agent.post('/rest/test/numeric-keyed').send({ userId: 67890 }).expect(200);
});
it('should not rate limit when string value fails numeric schema validation', async () => {
for (let i = 0; i < 5; i++) {
await agent.post('/rest/test/numeric-keyed').send({ userId: 'not-a-number' }).expect(400);
}
});
});
describe('User-based keyed rate limiting', () => {
@@ -137,11 +250,10 @@ describe('ControllerRegistry', () => {
// @ts-expect-error tsc complains about unused class
class TestController {
@Post('/user-keyed', {
keyedRateLimit: {
keyedRateLimit: createUserKeyedRateLimiter({
limit: 3,
windowMs: 60_000,
source: 'user',
},
}),
})
bodyKeyed(@Body _body: { email: string }) {
return { ok: true };
+17 -3
View File
@@ -110,7 +110,10 @@ export class ControllerRegistry {
return await controller[handlerName](...args);
};
const middlewares = this.buildMiddlewares(route, controllerMiddlewares);
const bodyArgIdx = route.args.findIndex((arg) => arg?.type === 'body');
const bodyArgType = bodyArgIdx !== -1 ? (argTypes[bodyArgIdx] as ZodClass) : undefined;
const middlewares = this.buildMiddlewares(route, controllerMiddlewares, bodyArgType);
const finalHandler = route.usesTemplates
? async (req: Request, res: Response) => {
await handler(req, res);
@@ -137,6 +140,7 @@ export class ControllerRegistry {
middlewares?: RequestHandler[];
},
controllerMiddlewares: RequestHandler[],
bodyDtoClass?: ZodClass,
): RequestHandler[] {
const middlewares: RequestHandler[] = [];
@@ -147,7 +151,17 @@ export class ControllerRegistry {
// LAYER 2a: Keyed rate limiting with body source (BEFORE auth)
if (inProduction && route.keyedRateLimit?.source === 'body') {
middlewares.push(this.rateLimitService.createKeyedRateLimitMiddleware(route.keyedRateLimit));
assert(
bodyDtoClass,
'Body argument type (@Body decorator) is required for body-based rate limiting',
);
middlewares.push(
this.rateLimitService.createBodyKeyedRateLimitMiddleware(
bodyDtoClass,
route.keyedRateLimit,
),
);
}
if (!route.skipAuth) {
@@ -170,7 +184,7 @@ export class ControllerRegistry {
// Separate ifs intentionally to prevent configuration errors in development
if (inProduction) {
middlewares.push(
this.rateLimitService.createKeyedRateLimitMiddleware(route.keyedRateLimit),
this.rateLimitService.createUserKeyedRateLimitMiddleware(route.keyedRateLimit),
);
}
}
@@ -2,7 +2,14 @@ import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import type { User, PublicUser } from '@n8n/db';
import { UserRepository, AuthenticatedRequest, GLOBAL_OWNER_ROLE } from '@n8n/db';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import {
Body,
createBodyKeyedRateLimiter,
Get,
Post,
Query,
RestController,
} from '@n8n/decorators';
import { Container } from '@n8n/di';
import { isEmail } from 'class-validator';
import { Response } from 'express';
@@ -50,12 +57,11 @@ export class AuthController {
limit: 1000,
windowMs: 5 * Time.minutes.toMilliseconds,
},
keyedRateLimit: {
keyedRateLimit: createBodyKeyedRateLimiter<LoginRequestDto>({
limit: 5,
windowMs: 1 * Time.minutes.toMilliseconds,
source: 'body',
field: 'emailOrLdapLoginId' satisfies keyof LoginRequestDto,
},
field: 'emailOrLdapLoginId',
}),
})
async login(
req: AuthlessRequest,
@@ -2,7 +2,14 @@ import { AcceptInvitationRequestDto, InviteUsersRequestDto } from '@n8n/api-type
import { Logger } from '@n8n/backend-common';
import type { User } from '@n8n/db';
import { UserRepository, AuthenticatedRequest } from '@n8n/db';
import { Post, GlobalScope, RestController, Body, Param } from '@n8n/decorators';
import {
Post,
GlobalScope,
RestController,
Body,
Param,
createBodyKeyedRateLimiter,
} from '@n8n/decorators';
import { Response } from 'express';
import { AuthService } from '@/auth/auth.service';
@@ -206,12 +213,11 @@ export class InvitationController {
// Two layered rate limit to ensure multiple users can accept an invitation from
// the same IP address but aggressive per inviteeId limit.
ipRateLimit: { limit: 100, windowMs: 5 * Time.minutes.toMilliseconds },
keyedRateLimit: {
keyedRateLimit: createBodyKeyedRateLimiter<AcceptInvitationRequestDto>({
limit: 10,
windowMs: 1 * Time.minutes.toMilliseconds,
source: 'body',
field: 'inviterId' satisfies keyof AcceptInvitationRequestDto,
},
field: 'inviterId',
}),
})
async acceptInvitation(
req: AuthlessRequest,
@@ -7,7 +7,7 @@ import {
import { Logger } from '@n8n/backend-common';
import type { User, PublicUser } from '@n8n/db';
import { UserRepository, AuthenticatedRequest } from '@n8n/db';
import { Body, Patch, Post, RestController } from '@n8n/decorators';
import { Body, createUserKeyedRateLimiter, Patch, Post, RestController } from '@n8n/decorators';
import { plainToInstance } from 'class-transformer';
import { Response } from 'express';
@@ -166,9 +166,7 @@ export class MeController {
* Update the logged-in user's password.
*/
@Patch('/password', {
keyedRateLimit: {
source: 'user',
},
keyedRateLimit: createUserKeyedRateLimiter({}),
})
async updatePassword(
req: AuthenticatedRequest,
+10 -10
View File
@@ -1,5 +1,11 @@
import { AuthenticatedRequest, UserRepository } from '@n8n/db';
import { Get, GlobalScope, Post, RestController } from '@n8n/decorators';
import {
createUserKeyedRateLimiter,
Get,
GlobalScope,
Post,
RestController,
} from '@n8n/decorators';
import { Response } from 'express';
import { AuthService } from '@/auth/auth.service';
@@ -86,9 +92,7 @@ export class MFAController {
@Post('/enable', {
allowSkipMFA: true,
keyedRateLimit: {
source: 'user',
},
keyedRateLimit: createUserKeyedRateLimiter({}),
})
async activateMFA(req: MFA.Activate, res: Response) {
const { mfaCode = null } = req.body;
@@ -129,9 +133,7 @@ export class MFAController {
@Post('/disable', {
ipRateLimit: true,
keyedRateLimit: {
source: 'user',
},
keyedRateLimit: createUserKeyedRateLimiter({}),
})
async disableMFA(req: MFA.Disable, res: Response) {
const { id: userId } = req.user;
@@ -175,9 +177,7 @@ export class MFAController {
@Post('/verify', {
allowSkipMFA: true,
keyedRateLimit: {
source: 'user',
},
keyedRateLimit: createUserKeyedRateLimiter({}),
})
async verifyMFA(req: MFA.Verify) {
const { id } = req.user;
@@ -5,7 +5,14 @@ import {
} from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { GLOBAL_OWNER_ROLE, UserRepository } from '@n8n/db';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import {
Body,
createBodyKeyedRateLimiter,
Get,
Post,
Query,
RestController,
} from '@n8n/decorators';
import { hasGlobalScope } from '@n8n/permissions';
import { Response } from 'express';
@@ -51,11 +58,10 @@ export class PasswordResetController {
@Post('/forgot-password', {
skipAuth: true,
ipRateLimit: { limit: 20, windowMs: 5 * Time.minutes.toMilliseconds },
keyedRateLimit: {
keyedRateLimit: createBodyKeyedRateLimiter<ForgotPasswordRequestDto>({
limit: 3,
source: 'body',
field: 'email' satisfies keyof ForgotPasswordRequestDto,
},
field: 'email',
}),
})
async forgotPassword(
_req: AuthlessRequest,
+44 -40
View File
@@ -1,11 +1,13 @@
import { Service } from '@n8n/di';
import type { RateLimiterLimits, KeyedRateLimiterConfig } from '@n8n/decorators';
import { Time } from '@n8n/constants';
import type { AuthenticatedRequest } from '@n8n/db';
import type { RateLimiterLimits, UserKeyedRateLimiterConfig } from '@n8n/decorators';
import { BodyKeyedRateLimiterConfig } from '@n8n/decorators';
import { Service } from '@n8n/di';
import type { Request, RequestHandler } from 'express';
import { rateLimit as expressRateLimit } from 'express-rate-limit';
import assert from 'node:assert';
import { ErrorReporter } from 'n8n-core';
import { Time } from '@n8n/constants';
import type { ZodTypeAny } from 'zod';
import type { ZodClass } from 'zod-class';
const defaultLimits: Required<RateLimiterLimits> = {
limit: 5,
@@ -22,8 +24,6 @@ const defaultLimits: Required<RateLimiterLimits> = {
*/
@Service()
export class RateLimitService {
constructor(private readonly errorReporter: ErrorReporter) {}
/**
* Creates Layer 1: IP-based rate limit middleware
* Always runs BEFORE authentication.
@@ -42,53 +42,57 @@ export class RateLimitService {
* Creates Layer 2: Keyed rate limit middleware
* Position (before/after auth) depends on identifier source
*/
createKeyedRateLimitMiddleware(config: KeyedRateLimiterConfig): RequestHandler {
createBodyKeyedRateLimitMiddleware(
bodyDtoClass: ZodClass,
config: BodyKeyedRateLimiterConfig,
): RequestHandler {
const fieldName = config.field;
const bodyFieldSchema = bodyDtoClass.shape[fieldName];
assert(bodyFieldSchema, `Missing field ${fieldName} in DTO schema`);
return expressRateLimit({
limit: config.limit ?? defaultLimits.limit,
windowMs: config.windowMs ?? defaultLimits.windowMs,
keyGenerator: (req: Request) => this.extractReqIdentifier(req, config),
keyGenerator: (req: Request) =>
this.extractBodyIdentifier(req.body, fieldName, bodyFieldSchema),
skip: (req: Request) => {
const identifier = this.extractReqIdentifier(req, config);
const identifier = this.extractBodyIdentifier(req.body, fieldName, bodyFieldSchema);
return identifier.startsWith('skip:');
},
message: { message: 'Too many requests' },
});
}
private extractReqIdentifier(req: Request, config: KeyedRateLimiterConfig): string {
const { source } = config;
createUserKeyedRateLimitMiddleware(config: UserKeyedRateLimiterConfig): RequestHandler {
return expressRateLimit({
limit: config.limit ?? defaultLimits.limit,
windowMs: config.windowMs ?? defaultLimits.windowMs,
keyGenerator: (req: AuthenticatedRequest) => this.extractUserIdentifier(req),
skip: (req: AuthenticatedRequest) => {
const identifier = this.extractUserIdentifier(req);
return identifier.startsWith('skip:');
},
});
}
if (source === 'body') {
const body = req.body;
if (!body) {
return 'skip:no-body';
}
const value = body[config.field];
if (typeof value !== 'string' && typeof value !== 'number') {
return 'skip:no-identifier';
}
return `body:${value}`;
private extractBodyIdentifier(body: unknown, fieldName: string, fieldSchema: ZodTypeAny): string {
if (!body || typeof body !== 'object') {
return 'skip:empty-body';
}
if (source === 'user') {
const authReq = req as AuthenticatedRequest;
if (!authReq.user) {
this.errorReporter.error(new Error('Missing user for user-based rate limited endpoint'), {
extra: {
request: {
method: req.method,
url: req.url,
},
},
});
return 'skip:not-authenticated';
}
return `user:${authReq.user.id}`;
const value = (body as Record<string, unknown>)[fieldName];
if (typeof value !== 'string' && typeof value !== 'number') {
return 'skip:unsupported-type';
}
assert.fail(`Unknown source for keyed rate limiting: ${JSON.stringify(config)}`);
const parseResult = fieldSchema.safeParse(value);
if (!parseResult.success) {
return 'skip:validation-failed';
}
return `body:${value}`;
}
private extractUserIdentifier(req: AuthenticatedRequest): string {
return `user:${req.user.id}`;
}
}
+4 -6
View File
@@ -923,6 +923,9 @@ importers:
'@types/lodash':
specifier: 'catalog:'
version: 4.17.17
zod:
specifier: 3.25.67
version: 3.25.67
packages/@n8n/di:
dependencies:
@@ -21684,7 +21687,7 @@ snapshots:
'@currents/commit-info': 1.0.1-beta.0
async-retry: 1.3.3
axios: 1.12.0(debug@4.4.3)
axios-retry: 4.5.0(axios@1.12.0(debug@4.4.3))
axios-retry: 4.5.0(axios@1.12.0)
c12: 1.11.2(magicast@0.3.5)
chalk: 4.1.2
commander: 12.1.0
@@ -27207,11 +27210,6 @@ snapshots:
axe-core@4.7.2: {}
axios-retry@4.5.0(axios@1.12.0(debug@4.4.3)):
dependencies:
axios: 1.12.0(debug@4.4.3)
is-retry-allowed: 2.2.0
axios-retry@4.5.0(axios@1.12.0):
dependencies:
axios: 1.12.0