From 1220cf84d0771dbf985c86aba736007a23598a11 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Tue, 17 Sep 2024 08:56:38 +0300 Subject: [PATCH] feat(backend): allow rate limiting requests to the backend uses redis for storing the data if it has been configured; otherwise falls back to memory configuration allows controlling almost all possible configuration available in the `express-rate-limit` library. Signed-off-by: Heikki Hellgren --- .changeset/famous-terms-rescue.md | 15 ++ packages/backend-defaults/config.d.ts | 68 ++++- packages/backend-defaults/package.json | 2 + .../report-rootHttpRouter.api.md | 11 + .../rootHttpRouter/http/MiddlewareFactory.ts | 86 ++++++- .../http/RateLimitStoreFactory.test.ts | 84 +++++++ .../http/RateLimitStoreFactory.ts | 69 ++++++ .../entrypoints/rootHttpRouter/http/index.ts | 1 + .../rootHttpRouterServiceFactory.ts | 5 +- yarn.lock | 234 +++++++++++++++++- 10 files changed, 558 insertions(+), 17 deletions(-) create mode 100644 .changeset/famous-terms-rescue.md create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.test.ts create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.ts diff --git a/.changeset/famous-terms-rescue.md b/.changeset/famous-terms-rescue.md new file mode 100644 index 0000000000..073f3a02ba --- /dev/null +++ b/.changeset/famous-terms-rescue.md @@ -0,0 +1,15 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added new rate limit middleware to allow rate limiting requests to the backend + +Rate limiting can be turned on by adding the following configuration to `app-config.yaml`: + +```yaml +backend: + rateLimit: + enabled: true + windowMs: 60000 + limit: 100 +``` diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 1ef054a537..7866b17c29 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { HumanDuration } from '@backstage/types'; +import { HumanDuration, JsonValue } from '@backstage/types'; export interface Config { app: { @@ -790,6 +790,72 @@ export interface Config { headers?: { [name: string]: string }; }; + /** + * Rate limiting options + */ + rateLimit?: { + /** + * Store to use for rate limiting. If not defined, the store will be automatically + * decided based on `backend.cache.store` value. If + * redis is not available, the store will be memory. + */ + store?: 'memory' | 'redis'; + /** + * Rate limiting enabled. Defaults to false. + */ + enabled?: boolean; + /** + * Time frame in milliseconds or as human duration for which requests are checked/remembered. + * Defaults to 6000ms. + */ + window?: number | HumanDuration; + /** + * The maximum number of connections to allow during the `window` before rate limiting the client. + * Defaults to 5. + */ + limit?: number; + /** + * The response body to send back when a client is rate limited. + * Defaults to 'Too many requests, please try again later.'. + */ + message?: JsonValue; + /** + * The HTTP status code to send back when a client is rate limited. + * Defaults to 429. + */ + statusCode?: number; + /** + * Whether to send the legacy rate limit headers for the limit. + * Defaults to true. + */ + legacyHeaders?: boolean; + /** + * Whether to enable support for headers conforming the RateLimit header fields for HTTP + * standardization. Defaults to undefined. + */ + standardHeaders?: 'draft-6' | 'draft-7'; + /** + * Whether to pass requests in case of store failure. + * Defaults to false. + */ + passOnStoreError?: boolean; + /** + * List of allowed IP addresses that are not rate limited. + * Defaults to [127.0.0.1]. + */ + ipAllowList?: string[]; + /** + * Skip rate limiting for requests that have been successful. + * Defaults to false. + */ + skipSuccessfulRequests?: boolean; + /** + * Skip rate limiting for requests that have failed. + * Defaults to false. + */ + skipFailedRequests?: boolean; + }; + /** * Configuration related to URL reading, used for example for reading catalog info * files, scaffolder templates, and techdocs content. diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 3c74bad2d5..c10cb9f11f 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -168,6 +168,7 @@ "cron": "^3.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", + "express-rate-limit": "^7.4.0", "fs-extra": "^11.2.0", "git-url-parse": "^15.0.0", "helmet": "^6.0.0", @@ -187,6 +188,7 @@ "pg": "^8.11.3", "pg-connection-string": "^2.3.0", "pg-format": "^1.0.4", + "rate-limit-redis": "^4.2.0", "raw-body": "^2.4.1", "selfsigned": "^2.0.0", "tar": "^6.1.12", diff --git a/packages/backend-defaults/report-rootHttpRouter.api.md b/packages/backend-defaults/report-rootHttpRouter.api.md index 826d592e8b..91e760b7ea 100644 --- a/packages/backend-defaults/report-rootHttpRouter.api.md +++ b/packages/backend-defaults/report-rootHttpRouter.api.md @@ -20,6 +20,7 @@ import { RootHttpRouterService } from '@backstage/backend-plugin-api'; import { Router } from 'express'; import type { Server } from 'node:http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +import type { Store } from 'express-rate-limit'; // @public (undocumented) export function createHealthRouter(options: { @@ -93,6 +94,7 @@ export class MiddlewareFactory { helmet(): RequestHandler; logging(): RequestHandler; notFound(): RequestHandler; + rateLimit(): RequestHandler; } // @public @@ -109,6 +111,15 @@ export interface MiddlewareFactoryOptions { logger: LoggerService; } +// @public +export class RateLimitStoreFactory { + constructor(config: Config); + // (undocumented) + create(): Store | undefined; + // (undocumented) + redis(): Store; +} + // @public export function readCorsOptions(config?: Config): CorsOptions; diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts index 1ff9bd15d1..561a1999c9 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/MiddlewareFactory.ts @@ -15,15 +15,15 @@ */ import { - RootConfigService, LoggerService, + RootConfigService, } from '@backstage/backend-plugin-api'; import { - Request, - Response, ErrorRequestHandler, NextFunction, + Request, RequestHandler, + Response, } from 'express'; import cors from 'cors'; import helmet from 'helmet'; @@ -37,12 +37,19 @@ import { InputError, NotAllowedError, NotFoundError, + NotImplementedError, NotModifiedError, - ServiceUnavailableError, serializeError, + ServiceUnavailableError, } from '@backstage/errors'; -import { NotImplementedError } from '@backstage/errors'; import { applyInternalErrorFilter } from './applyInternalErrorFilter'; +import { DraftHeadersVersion, rateLimit } from 'express-rate-limit'; +import { + durationToMilliseconds, + HumanDuration, + JsonValue, +} from '@backstage/types'; +import { RateLimitStoreFactory } from './RateLimitStoreFactory'; type LogMeta = { date: string; @@ -227,6 +234,75 @@ export class MiddlewareFactory { return cors(readCorsOptions(this.#config.getOptionalConfig('backend'))); } + /** + * Returns a middleware that implements rate limiting. + * + * @remarks + * + * Rate limiting is a common technique to prevent abuse of APIs. This middleware is + * configured using the config key `backend.rateLimit`. + * + * @returns An Express request handler + */ + rateLimit(): RequestHandler { + const rateLimitOptions = + this.#config.getOptionalConfig('backend.rateLimit'); + const enabled = rateLimitOptions?.getOptionalBoolean('enabled') ?? false; + if (!rateLimitOptions || !enabled) { + return (_req: Request, _res: Response, next: NextFunction) => { + next(); + }; + } + + const window = rateLimitOptions.getOptional( + 'window', + ); + let windowMs: number | undefined; + if (window !== undefined) { + if (typeof window === 'number') { + windowMs = window; + } else if (typeof window === 'object' && !Array.isArray(window)) { + windowMs = durationToMilliseconds(window); + } else { + throw new Error( + `Invalid configuration backend.rateLimit.window: ${window}, expected milliseconds number or HumanDuration object`, + ); + } + } + + const ipAllowList = rateLimitOptions.getOptionalStringArray( + 'ipAllowList', + ) ?? ['127.0.0.1']; + + return rateLimit({ + windowMs, + limit: rateLimitOptions.getOptionalNumber('limit'), + message: rateLimitOptions.getOptional('message'), + statusCode: rateLimitOptions.getOptionalNumber('statusCode'), + skipSuccessfulRequests: rateLimitOptions.getOptionalBoolean( + 'skipSuccessfulRequests', + ), + skipFailedRequests: + rateLimitOptions.getOptionalBoolean('skipFailedRequests'), + legacyHeaders: rateLimitOptions.getOptionalBoolean('legacyHeaders'), + standardHeaders: rateLimitOptions.getOptionalString( + 'standardHeaders', + ) as DraftHeadersVersion, + passOnStoreError: rateLimitOptions.getOptionalBoolean('passOnStoreError'), + keyGenerator(req, _res): string { + if (!req.ip) { + return req.socket.remoteAddress!; + } + + return req.ip.replace(/:\d+[^:]*$/, ''); + }, + skip: (req, _res) => { + return Boolean(req.ip && ipAllowList.includes(req.ip)); + }, + store: new RateLimitStoreFactory(this.#config).create(), + }); + } + /** * Express middleware to handle errors during request processing. * diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.test.ts new file mode 100644 index 0000000000..105cf0cac6 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { mockServices } from '@backstage/backend-test-utils'; +import { RateLimitStoreFactory } from './RateLimitStoreFactory'; +import { RedisStore } from 'rate-limit-redis'; +import KeyvRedis from '@keyv/redis'; + +jest.mock('@keyv/redis'); +(KeyvRedis as jest.Mocked).mockImplementation(() => { + return { + redis: { + call: jest.fn().mockResolvedValue('OK'), + }, + }; +}); + +describe('CacheRateLimitStoreFactory', () => { + it('should return redis store with auto configuration', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + cache: { + store: 'redis', + }, + rateLimit: { + store: undefined, + }, + }, + }, + }); + const factory = new RateLimitStoreFactory(config); + const store = factory.create(); + expect(store).toBeInstanceOf(RedisStore); + }); + + it('should return undefined store with auto configuration if redis is not available', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + cache: { + store: 'memory', + }, + database: { + client: 'sqlite3', + }, + rateLimit: { + store: undefined, + }, + }, + }, + }); + const factory = new RateLimitStoreFactory(config); + const store = factory.create(); + expect(store).toBeUndefined(); + }); + + it('should return redis store if configured explicitly', () => { + const config = mockServices.rootConfig({ + data: { + backend: { + rateLimit: { + store: 'redis', + }, + }, + }, + }); + const factory = new RateLimitStoreFactory(config); + const store = factory.create(); + expect(store).toBeInstanceOf(RedisStore); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.ts new file mode 100644 index 0000000000..00e5440ebe --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/RateLimitStoreFactory.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Config } from '@backstage/config'; +import type { Store } from 'express-rate-limit'; +import KeyvRedis from '@keyv/redis'; +import { RedisStore } from 'rate-limit-redis'; + +/** + * Creates a store for `express-rate-limit` based on the configuration. + * + * @public + */ +export class RateLimitStoreFactory { + constructor(private readonly config: Config) {} + + create(): Store | undefined { + const storeType = this.config.getOptionalString('backend.rateLimit.store'); + if (!storeType) { + return this.auto(); + } + switch (storeType) { + case 'redis': + return this.redis(); + default: + throw new Error( + `Invalid 'backend.rateLimit.store' provided: ${storeType}`, + ); + } + } + + private auto(): Store | undefined { + const cacheStore = + this.config.getOptionalString('backend.cache.store') || 'memory'; + // Use redis as primary if available + if (cacheStore === 'redis') { + return this.redis(); + } + + // Fallback to undefined (memory) + return undefined; + } + + redis(): Store { + const connectionString = + this.config.getOptionalString('backend.cache.connection') || ''; + const useRedisSets = + this.config.getOptionalBoolean('backend.cache.useRedisSets') ?? true; + const keyv = new KeyvRedis(connectionString, { + useRedisSets, + }); + return new RedisStore({ + // Keyv uses ioredis under the hood + sendCommand: (...args: string[]) => keyv.redis.call(...args), + }); + } +} diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts index 4a9ec14cf8..5c63d887bb 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/http/index.ts @@ -28,3 +28,4 @@ export type { HttpServerCertificateOptions, HttpServerOptions, } from './types'; +export { RateLimitStoreFactory } from './RateLimitStoreFactory'; diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts index da7fcfe3f9..7d0d778b0c 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -15,13 +15,13 @@ */ import { - RootConfigService, coreServices, createServiceFactory, LifecycleService, LoggerService, + RootConfigService, } from '@backstage/backend-plugin-api'; -import express, { RequestHandler, Express } from 'express'; +import express, { Express, RequestHandler } from 'express'; import type { Server } from 'node:http'; import { createHttpServer, @@ -117,6 +117,7 @@ const rootHttpRouterServiceFactoryWithOptions = ( if (trustProxy !== undefined) { app.set('trust proxy', trustProxy); } + app.use(middleware.rateLimit()); app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); diff --git a/yarn.lock b/yarn.lock index d8736591ea..0a73d77844 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3335,7 +3335,16 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.26.7 + resolution: "@babel/runtime@npm:7.26.7" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/c7a661a6836b332d9d2e047cba77ba1862c1e4f78cec7146db45808182ef7636d8a7170be9797e5d8fd513180bffb9fa16f6ca1c69341891efec56113cf22bfc + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.26.10": version: 7.27.0 resolution: "@babel/runtime@npm:7.27.0" dependencies: @@ -3344,7 +3353,18 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.27.0, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.24.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e861180881507210150c1335ad94aff80fd9e9be6202e1efa752059c93224e2d5310186ddcdd4c0f0b0fc658ce48cb47823f15142b5c00c8456dde54f5de80b2 + languageName: node + linkType: hard + +"@babel/template@npm:^7.27.0": version: 7.27.0 resolution: "@babel/template@npm:7.27.0" dependencies: @@ -3370,7 +3390,17 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.5, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.5, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e + languageName: node + linkType: hard + +"@babel/types@npm:^7.27.0": version: 7.27.0 resolution: "@babel/types@npm:7.27.0" dependencies: @@ -3602,6 +3632,7 @@ __metadata: cron: "npm:^3.0.0" express: "npm:^4.17.1" express-promise-router: "npm:^4.1.0" + express-rate-limit: "npm:^7.4.0" fs-extra: "npm:^11.2.0" git-url-parse: "npm:^15.0.0" helmet: "npm:^6.0.0" @@ -3624,6 +3655,7 @@ __metadata: pg: "npm:^8.11.3" pg-connection-string: "npm:^2.3.0" pg-format: "npm:^1.0.4" + rate-limit-redis: "npm:^4.2.0" raw-body: "npm:^2.4.1" selfsigned: "npm:^2.0.0" supertest: "npm:^7.0.0" @@ -19193,6 +19225,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-darwin-arm64@npm:1.10.6" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-arm64@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-darwin-arm64@npm:1.11.24" @@ -19200,6 +19239,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-darwin-x64@npm:1.10.6" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-darwin-x64@npm:1.11.24" @@ -19207,6 +19253,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.6" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.11.24" @@ -19214,6 +19267,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-linux-arm64-gnu@npm:1.10.6" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-linux-arm64-gnu@npm:1.11.24" @@ -19221,6 +19281,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-linux-arm64-musl@npm:1.10.6" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-linux-arm64-musl@npm:1.11.24" @@ -19228,6 +19295,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-linux-x64-gnu@npm:1.10.6" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-linux-x64-gnu@npm:1.11.24" @@ -19235,6 +19309,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-linux-x64-musl@npm:1.10.6" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-linux-x64-musl@npm:1.11.24" @@ -19242,6 +19323,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-win32-arm64-msvc@npm:1.10.6" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-win32-arm64-msvc@npm:1.11.24" @@ -19249,6 +19337,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-win32-ia32-msvc@npm:1.10.6" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-win32-ia32-msvc@npm:1.11.24" @@ -19256,6 +19351,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-x64-msvc@npm:1.10.6": + version: 1.10.6 + resolution: "@swc/core-win32-x64-msvc@npm:1.10.6" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.11.24": version: 1.11.24 resolution: "@swc/core-win32-x64-msvc@npm:1.11.24" @@ -19263,7 +19365,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:^1.10.8, @swc/core@npm:^1.3.46": +"@swc/core@npm:^1.10.8": version: 1.11.24 resolution: "@swc/core@npm:1.11.24" dependencies: @@ -19309,6 +19411,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.3.46": + version: 1.10.6 + resolution: "@swc/core@npm:1.10.6" + dependencies: + "@swc/core-darwin-arm64": "npm:1.10.6" + "@swc/core-darwin-x64": "npm:1.10.6" + "@swc/core-linux-arm-gnueabihf": "npm:1.10.6" + "@swc/core-linux-arm64-gnu": "npm:1.10.6" + "@swc/core-linux-arm64-musl": "npm:1.10.6" + "@swc/core-linux-x64-gnu": "npm:1.10.6" + "@swc/core-linux-x64-musl": "npm:1.10.6" + "@swc/core-win32-arm64-msvc": "npm:1.10.6" + "@swc/core-win32-ia32-msvc": "npm:1.10.6" + "@swc/core-win32-x64-msvc": "npm:1.10.6" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.17" + peerDependencies: + "@swc/helpers": "*" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/51eccbba6ee8a41f57a6ba4213ec05859434f52fd6698f508c92a8bb467afda56a1e95b07985faf059b27cb28e77d479340de7210a8616976ea82f774a6d86a3 + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -19338,6 +19486,15 @@ __metadata: languageName: node linkType: hard +"@swc/types@npm:^0.1.17": + version: 0.1.17 + resolution: "@swc/types@npm:0.1.17" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10/ddef1ad5bfead3acdfc41f14e79ba43a99200eb325afbad5716058dbe36358b0513400e9f22aff32432be84a98ae93df95a20b94192f69b8687144270e4eaa18 + languageName: node + linkType: hard + "@swc/types@npm:^0.1.21": version: 0.1.21 resolution: "@swc/types@npm:0.1.21" @@ -20659,7 +20816,16 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:^22.0.0": +"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^22.0.0": + version: 22.10.5 + resolution: "@types/node@npm:22.10.5" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10/a5366961ffa9921e8f15435bc18ea9f8b7a7bb6b3d92dd5e93ebcd25e8af65708872bd8e6fee274b4655bab9ca80fbff9f0e42b5b53857790f13cf68cf4cbbfc + languageName: node + linkType: hard + +"@types/node@npm:>=12, @types/node@npm:>=12.0.0, @types/node@npm:>=18.0.0": version: 22.13.10 resolution: "@types/node@npm:22.13.10" dependencies: @@ -24011,13 +24177,20 @@ __metadata: languageName: node linkType: hard -"async@npm:^3.2.2, async@npm:^3.2.3, async@npm:^3.2.4, async@npm:^3.2.6": +"async@npm:^3.2.2, async@npm:^3.2.6": version: 3.2.6 resolution: "async@npm:3.2.6" checksum: 10/cb6e0561a3c01c4b56a799cc8bab6ea5fef45f069ab32500b6e19508db270ef2dffa55e5aed5865c5526e9907b1f8be61b27530823b411ffafb5e1538c86c368 languageName: node linkType: hard +"async@npm:^3.2.3, async@npm:^3.2.4": + version: 3.2.4 + resolution: "async@npm:3.2.4" + checksum: 10/bebb5dc2258c45b83fa1d3be179ae0eb468e1646a62d443c8d60a45e84041b28fccebe1e2d1f234bfc3dcad44e73dcdbf4ba63d98327c9f6556e3dbd47c2ae8b + languageName: node + linkType: hard + "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -24509,13 +24682,20 @@ __metadata: languageName: node linkType: hard -"before-after-hook@npm:^2.1.0, before-after-hook@npm:^2.2.0": +"before-after-hook@npm:^2.1.0": version: 2.2.3 resolution: "before-after-hook@npm:2.2.3" checksum: 10/e676f769dbc4abcf4b3317db2fd2badb4a92c0710e0a7da12cf14b59c3482d4febf835ad7de7874499060fd4e13adf0191628e504728b3c5bb4ec7a878c09940 languageName: node linkType: hard +"before-after-hook@npm:^2.2.0": + version: 2.2.2 + resolution: "before-after-hook@npm:2.2.2" + checksum: 10/34c190def503f771f8811db0bd0c62b35301fe6059c8d847664633ce0548e8253e2661104ba66c71a85548746ba87d5ff2ebf5278c1f3ad367d111ffc9a26bb4 + languageName: node + linkType: hard + "better-opn@npm:^3.0.2": version: 3.0.2 resolution: "better-opn@npm:3.0.2" @@ -29865,6 +30045,15 @@ __metadata: languageName: node linkType: hard +"express-rate-limit@npm:^7.4.0": + version: 7.4.0 + resolution: "express-rate-limit@npm:7.4.0" + peerDependencies: + express: 4 || 5 || ^5.0.0-beta.1 + checksum: 10/33178c652bb1472aad2022194b5cd7963bd3e74d3eaf5e49eb1491a968fdce54551cc76b097ac10d3a1646d62cec2e6f2405ccef5ef5b60152a0c4a148749a4d + languageName: node + linkType: hard + "express-session@npm:^1.17.1, express-session@npm:^1.17.3": version: 1.18.1 resolution: "express-session@npm:1.18.1" @@ -31040,7 +31229,25 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": + version: 1.2.6 + resolution: "get-intrinsic@npm:1.2.6" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + dunder-proto: "npm:^1.0.0" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + function-bind: "npm:^1.1.2" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.0.0" + checksum: 10/a1ffae6d7893a6fa0f4d1472adbc85095edd6b3b0943ead97c3738539cecb19d422ff4d48009eed8c3c27ad678c2b1e38a83b1a1e96b691d13ed8ecefca1068d + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -36734,7 +36941,7 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.1.0": +"math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd @@ -41835,6 +42042,15 @@ __metadata: languageName: node linkType: hard +"rate-limit-redis@npm:^4.2.0": + version: 4.2.0 + resolution: "rate-limit-redis@npm:4.2.0" + peerDependencies: + express-rate-limit: ">= 6" + checksum: 10/22adc67918ca906f613b45f9dcfd039f543d363921979d21ba56be5f3288c6e9973c9e4bb4ec59810fc6b3abb20defd572c102f607a8c3b4d273d5e09b63839f + languageName: node + linkType: hard + "rate-limiter-flexible@npm:^4.0.1": version: 4.0.1 resolution: "rate-limiter-flexible@npm:4.0.1"