@qwickapps/server 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +114 -0
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/types.d.ts +19 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
- package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.js +166 -0
- package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
- package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
- package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
- package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
- package/dist/plugins/auth/config-store.d.ts +11 -0
- package/dist/plugins/auth/config-store.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.js +232 -0
- package/dist/plugins/auth/config-store.js.map +1 -0
- package/dist/plugins/auth/config-store.test.d.ts +7 -0
- package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
- package/dist/plugins/auth/config-store.test.js +299 -0
- package/dist/plugins/auth/config-store.test.js.map +1 -0
- package/dist/plugins/auth/env-config.d.ts +51 -1
- package/dist/plugins/auth/env-config.d.ts.map +1 -1
- package/dist/plugins/auth/env-config.js +640 -7
- package/dist/plugins/auth/env-config.js.map +1 -1
- package/dist/plugins/auth/index.d.ts +6 -2
- package/dist/plugins/auth/index.d.ts.map +1 -1
- package/dist/plugins/auth/index.js +5 -1
- package/dist/plugins/auth/index.js.map +1 -1
- package/dist/plugins/auth/types.d.ts +106 -0
- package/dist/plugins/auth/types.d.ts.map +1 -1
- package/dist/plugins/index.d.ts +4 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
- package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
- package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
- package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
- package/dist/plugins/rate-limit/cleanup.js +72 -0
- package/dist/plugins/rate-limit/cleanup.js.map +1 -0
- package/dist/plugins/rate-limit/env-config.d.ts +91 -0
- package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
- package/dist/plugins/rate-limit/env-config.js +318 -0
- package/dist/plugins/rate-limit/env-config.js.map +1 -0
- package/dist/plugins/rate-limit/index.d.ts +76 -0
- package/dist/plugins/rate-limit/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/index.js +79 -0
- package/dist/plugins/rate-limit/index.js.map +1 -0
- package/dist/plugins/rate-limit/middleware.d.ts +40 -0
- package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
- package/dist/plugins/rate-limit/middleware.js +169 -0
- package/dist/plugins/rate-limit/middleware.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
- package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
- package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
- package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
- package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
- package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
- package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
- package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
- package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/index.js +8 -0
- package/dist/plugins/rate-limit/stores/index.js.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
- package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
- package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
- package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
- package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/index.js +27 -0
- package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
- package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
- package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
- package/dist/plugins/rate-limit/types.d.ts +265 -0
- package/dist/plugins/rate-limit/types.d.ts.map +1 -0
- package/dist/plugins/rate-limit/types.js +9 -0
- package/dist/plugins/rate-limit/types.js.map +1 -0
- package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
- package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +141 -0
- package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
- package/dist-ui-lib/index.js +3332 -2343
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
- package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/control-panel.ts +128 -0
- package/src/core/types.ts +17 -0
- package/src/index.ts +38 -0
- package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
- package/src/plugins/auth/adapter-wrapper.ts +205 -0
- package/src/plugins/auth/config-store.test.ts +417 -0
- package/src/plugins/auth/config-store.ts +305 -0
- package/src/plugins/auth/env-config.ts +714 -7
- package/src/plugins/auth/index.ts +22 -1
- package/src/plugins/auth/types.ts +138 -0
- package/src/plugins/index.ts +49 -0
- package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
- package/src/plugins/rate-limit/cleanup.ts +117 -0
- package/src/plugins/rate-limit/env-config.ts +400 -0
- package/src/plugins/rate-limit/index.ts +128 -0
- package/src/plugins/rate-limit/middleware.ts +212 -0
- package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
- package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
- package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
- package/src/plugins/rate-limit/stores/index.ts +8 -0
- package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
- package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
- package/src/plugins/rate-limit/strategies/index.ts +30 -0
- package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
- package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
- package/src/plugins/rate-limit/types.ts +338 -0
- package/ui/src/App.tsx +32 -14
- package/ui/src/api/controlPanelApi.ts +226 -0
- package/ui/src/dashboard/builtInWidgets.tsx +5 -1
- package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
- package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
- package/ui/src/dashboard/widgets/index.ts +2 -0
- package/ui/src/pages/AuthPage.tsx +986 -142
- package/ui/src/pages/IntegrationsPage.tsx +288 -0
- package/ui/src/pages/RateLimitPage.tsx +292 -0
- package/dist-ui/assets/index-BY8OxNgO.js +0 -465
- package/dist-ui/assets/index-BY8OxNgO.js.map +0 -1
|
@@ -16,7 +16,20 @@ export {
|
|
|
16
16
|
} from './auth-plugin.js';
|
|
17
17
|
|
|
18
18
|
// Environment-based configuration
|
|
19
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
createAuthPluginFromEnv,
|
|
21
|
+
getAuthStatus,
|
|
22
|
+
setAuthConfigStore,
|
|
23
|
+
getAdapterWrapper,
|
|
24
|
+
} from './env-config.js';
|
|
25
|
+
export type { AuthEnvPluginOptionsExtended } from './env-config.js';
|
|
26
|
+
|
|
27
|
+
// Config store
|
|
28
|
+
export { postgresAuthConfigStore } from './config-store.js';
|
|
29
|
+
|
|
30
|
+
// Adapter wrapper
|
|
31
|
+
export { createAdapterWrapper } from './adapter-wrapper.js';
|
|
32
|
+
export type { AdapterWrapper } from './adapter-wrapper.js';
|
|
20
33
|
|
|
21
34
|
// Types
|
|
22
35
|
export type {
|
|
@@ -32,6 +45,14 @@ export type {
|
|
|
32
45
|
AuthPluginState,
|
|
33
46
|
AuthEnvPluginOptions,
|
|
34
47
|
AuthConfigStatus,
|
|
48
|
+
// Runtime config types
|
|
49
|
+
AuthAdapterType,
|
|
50
|
+
RuntimeAuthConfig,
|
|
51
|
+
UpdateAuthConfigRequest,
|
|
52
|
+
TestProviderRequest,
|
|
53
|
+
TestProviderResponse,
|
|
54
|
+
AuthConfigStore,
|
|
55
|
+
PostgresAuthConfigStoreConfig,
|
|
35
56
|
} from './types.js';
|
|
36
57
|
export { isAuthenticatedRequest } from './types.js';
|
|
37
58
|
|
|
@@ -243,3 +243,141 @@ export interface AuthConfigStatus {
|
|
|
243
243
|
/** Current configuration with secrets masked */
|
|
244
244
|
config?: Record<string, string>;
|
|
245
245
|
}
|
|
246
|
+
|
|
247
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
248
|
+
// Runtime Configuration Types
|
|
249
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Supported adapter types for runtime configuration
|
|
253
|
+
*/
|
|
254
|
+
export type AuthAdapterType = 'auth0' | 'supabase' | 'supertokens' | 'basic';
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Runtime auth configuration (persisted to database)
|
|
258
|
+
*/
|
|
259
|
+
export interface RuntimeAuthConfig {
|
|
260
|
+
/** Which adapter to use */
|
|
261
|
+
adapter: AuthAdapterType | null;
|
|
262
|
+
|
|
263
|
+
/** Adapter-specific configuration */
|
|
264
|
+
config: {
|
|
265
|
+
auth0?: Auth0AdapterConfig;
|
|
266
|
+
supabase?: SupabaseAdapterConfig;
|
|
267
|
+
supertokens?: SupertokensAdapterConfig;
|
|
268
|
+
basic?: BasicAdapterConfig;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** General auth settings */
|
|
272
|
+
settings: {
|
|
273
|
+
authRequired?: boolean;
|
|
274
|
+
excludePaths?: string[];
|
|
275
|
+
debug?: boolean;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** When the config was last updated */
|
|
279
|
+
updatedAt: string;
|
|
280
|
+
|
|
281
|
+
/** Who updated the config (optional) */
|
|
282
|
+
updatedBy?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Request body for PUT /api/auth/config
|
|
287
|
+
*/
|
|
288
|
+
export interface UpdateAuthConfigRequest {
|
|
289
|
+
/** Which adapter to use */
|
|
290
|
+
adapter: AuthAdapterType;
|
|
291
|
+
|
|
292
|
+
/** Adapter-specific configuration */
|
|
293
|
+
config: Record<string, unknown>;
|
|
294
|
+
|
|
295
|
+
/** General settings (optional) */
|
|
296
|
+
settings?: {
|
|
297
|
+
authRequired?: boolean;
|
|
298
|
+
excludePaths?: string[];
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Request body for POST /api/auth/test-provider
|
|
304
|
+
*/
|
|
305
|
+
export interface TestProviderRequest {
|
|
306
|
+
/** Which adapter to test */
|
|
307
|
+
adapter: AuthAdapterType;
|
|
308
|
+
|
|
309
|
+
/** Adapter configuration to test */
|
|
310
|
+
config: Record<string, unknown>;
|
|
311
|
+
|
|
312
|
+
/** For social provider test (optional) */
|
|
313
|
+
provider?: 'google' | 'github' | 'apple';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Response for POST /api/auth/test-provider
|
|
318
|
+
*/
|
|
319
|
+
export interface TestProviderResponse {
|
|
320
|
+
/** Whether the test was successful */
|
|
321
|
+
success: boolean;
|
|
322
|
+
|
|
323
|
+
/** Human-readable message */
|
|
324
|
+
message: string;
|
|
325
|
+
|
|
326
|
+
/** Additional details */
|
|
327
|
+
details?: {
|
|
328
|
+
latency?: number;
|
|
329
|
+
version?: string;
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Auth configuration store interface
|
|
335
|
+
*/
|
|
336
|
+
export interface AuthConfigStore {
|
|
337
|
+
/** Store name for identification */
|
|
338
|
+
name: string;
|
|
339
|
+
|
|
340
|
+
/** Initialize the store (create tables if needed) */
|
|
341
|
+
initialize(): Promise<void>;
|
|
342
|
+
|
|
343
|
+
/** Load configuration from store */
|
|
344
|
+
load(): Promise<RuntimeAuthConfig | null>;
|
|
345
|
+
|
|
346
|
+
/** Save configuration to store */
|
|
347
|
+
save(config: RuntimeAuthConfig): Promise<void>;
|
|
348
|
+
|
|
349
|
+
/** Delete configuration (revert to env vars) */
|
|
350
|
+
delete(): Promise<boolean>;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Subscribe to configuration changes
|
|
354
|
+
* Returns unsubscribe function
|
|
355
|
+
*/
|
|
356
|
+
onChange(callback: (config: RuntimeAuthConfig | null) => void): () => void;
|
|
357
|
+
|
|
358
|
+
/** Shutdown and cleanup */
|
|
359
|
+
shutdown(): Promise<void>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* PostgreSQL auth config store configuration
|
|
364
|
+
*/
|
|
365
|
+
export interface PostgresAuthConfigStoreConfig {
|
|
366
|
+
/** PostgreSQL pool instance or factory function for lazy initialization */
|
|
367
|
+
pool: unknown | (() => unknown);
|
|
368
|
+
|
|
369
|
+
/** Table name (default: 'auth_config') */
|
|
370
|
+
tableName?: string;
|
|
371
|
+
|
|
372
|
+
/** Schema name (default: 'public') */
|
|
373
|
+
schema?: string;
|
|
374
|
+
|
|
375
|
+
/** Auto-create table on initialization (default: true) */
|
|
376
|
+
autoCreateTable?: boolean;
|
|
377
|
+
|
|
378
|
+
/** Enable pg_notify for cross-instance config updates (default: true) */
|
|
379
|
+
enableNotify?: boolean;
|
|
380
|
+
|
|
381
|
+
/** Channel name for pg_notify (default: 'auth_config_changed') */
|
|
382
|
+
notifyChannel?: string;
|
|
383
|
+
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ export {
|
|
|
30
30
|
createAuthPlugin,
|
|
31
31
|
createAuthPluginFromEnv,
|
|
32
32
|
getAuthStatus,
|
|
33
|
+
setAuthConfigStore,
|
|
34
|
+
postgresAuthConfigStore,
|
|
33
35
|
isAuthenticated,
|
|
34
36
|
getAuthenticatedUser,
|
|
35
37
|
getAccessToken,
|
|
@@ -54,6 +56,8 @@ export type {
|
|
|
54
56
|
AuthPluginState,
|
|
55
57
|
AuthEnvPluginOptions,
|
|
56
58
|
AuthConfigStatus,
|
|
59
|
+
AuthConfigStore,
|
|
60
|
+
PostgresAuthConfigStoreConfig,
|
|
57
61
|
} from './auth/index.js';
|
|
58
62
|
|
|
59
63
|
// Users plugin
|
|
@@ -156,3 +160,48 @@ export type {
|
|
|
156
160
|
PostgresPreferencesStoreConfig,
|
|
157
161
|
PreferencesApiConfig,
|
|
158
162
|
} from './preferences/index.js';
|
|
163
|
+
|
|
164
|
+
// Rate Limit plugin
|
|
165
|
+
export {
|
|
166
|
+
createRateLimitPlugin,
|
|
167
|
+
createRateLimitPluginFromEnv,
|
|
168
|
+
getRateLimitConfigStatus,
|
|
169
|
+
postgresRateLimitStore,
|
|
170
|
+
createRateLimitCache,
|
|
171
|
+
createNoOpCache,
|
|
172
|
+
createSlidingWindowStrategy,
|
|
173
|
+
createFixedWindowStrategy,
|
|
174
|
+
createTokenBucketStrategy,
|
|
175
|
+
getStrategy,
|
|
176
|
+
rateLimitMiddleware,
|
|
177
|
+
rateLimitStatusMiddleware,
|
|
178
|
+
RateLimitService,
|
|
179
|
+
getRateLimitService,
|
|
180
|
+
isLimited,
|
|
181
|
+
checkLimit,
|
|
182
|
+
incrementLimit,
|
|
183
|
+
getRemainingRequests,
|
|
184
|
+
getLimitStatus,
|
|
185
|
+
clearLimit,
|
|
186
|
+
createCleanupJob,
|
|
187
|
+
} from './rate-limit/index.js';
|
|
188
|
+
export type {
|
|
189
|
+
RateLimitPluginConfig,
|
|
190
|
+
RateLimitEnvPluginOptions,
|
|
191
|
+
RateLimitStrategy,
|
|
192
|
+
LimitStatus,
|
|
193
|
+
Strategy,
|
|
194
|
+
StrategyOptions,
|
|
195
|
+
StrategyContext,
|
|
196
|
+
StoredLimit,
|
|
197
|
+
IncrementOptions,
|
|
198
|
+
RateLimitStore,
|
|
199
|
+
PostgresRateLimitStoreConfig,
|
|
200
|
+
CachedLimit,
|
|
201
|
+
RateLimitCache,
|
|
202
|
+
RateLimitCacheConfig,
|
|
203
|
+
RateLimitMiddlewareOptions,
|
|
204
|
+
CheckLimitOptions,
|
|
205
|
+
CleanupJob,
|
|
206
|
+
CleanupJobConfig,
|
|
207
|
+
} from './rate-limit/index.js';
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limit Plugin Tests
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import type { RateLimitStore, RateLimitCache, CachedLimit, StoredLimit, IncrementOptions } from '../types.js';
|
|
9
|
+
import { RateLimitService } from '../rate-limit-service.js';
|
|
10
|
+
import { createSlidingWindowStrategy } from '../strategies/sliding-window.js';
|
|
11
|
+
import { createFixedWindowStrategy } from '../strategies/fixed-window.js';
|
|
12
|
+
import { createTokenBucketStrategy } from '../strategies/token-bucket.js';
|
|
13
|
+
|
|
14
|
+
// Mock store implementation
|
|
15
|
+
function createMockStore(): RateLimitStore {
|
|
16
|
+
const records = new Map<string, StoredLimit>();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
name: 'mock',
|
|
20
|
+
|
|
21
|
+
async initialize(): Promise<void> {
|
|
22
|
+
// No-op
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async get(key: string): Promise<StoredLimit | null> {
|
|
26
|
+
return records.get(key) || null;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async increment(key: string, options: IncrementOptions): Promise<StoredLimit> {
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const windowMs = options.windowMs;
|
|
32
|
+
const windowStart = new Date(now.getTime() - (now.getTime() % windowMs));
|
|
33
|
+
const windowEnd = new Date(windowStart.getTime() + windowMs);
|
|
34
|
+
|
|
35
|
+
const existing = records.get(key);
|
|
36
|
+
if (existing && existing.windowStart.getTime() === windowStart.getTime()) {
|
|
37
|
+
existing.count += options.amount || 1;
|
|
38
|
+
existing.updatedAt = now;
|
|
39
|
+
return existing;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const newRecord: StoredLimit = {
|
|
43
|
+
id: `mock-${Date.now()}`,
|
|
44
|
+
key,
|
|
45
|
+
count: options.amount || 1,
|
|
46
|
+
maxRequests: options.maxRequests,
|
|
47
|
+
windowMs: options.windowMs,
|
|
48
|
+
windowStart,
|
|
49
|
+
windowEnd,
|
|
50
|
+
strategy: options.strategy,
|
|
51
|
+
userId: options.userId,
|
|
52
|
+
tenantId: options.tenantId,
|
|
53
|
+
ipAddress: options.ipAddress,
|
|
54
|
+
createdAt: now,
|
|
55
|
+
updatedAt: now,
|
|
56
|
+
};
|
|
57
|
+
records.set(key, newRecord);
|
|
58
|
+
return newRecord;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async clear(key: string): Promise<boolean> {
|
|
62
|
+
return records.delete(key);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async cleanup(): Promise<number> {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
let deleted = 0;
|
|
68
|
+
for (const [key, record] of records) {
|
|
69
|
+
if (record.windowEnd.getTime() < now) {
|
|
70
|
+
records.delete(key);
|
|
71
|
+
deleted++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return deleted;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async shutdown(): Promise<void> {
|
|
78
|
+
records.clear();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Mock cache implementation
|
|
84
|
+
function createMockCache(): RateLimitCache {
|
|
85
|
+
const cache = new Map<string, { value: CachedLimit; expiresAt: number }>();
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
name: 'mock',
|
|
89
|
+
|
|
90
|
+
async get(key: string): Promise<CachedLimit | null> {
|
|
91
|
+
const entry = cache.get(key);
|
|
92
|
+
if (!entry) return null;
|
|
93
|
+
if (entry.expiresAt <= Date.now()) {
|
|
94
|
+
cache.delete(key);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return entry.value;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async set(key: string, value: CachedLimit, ttlMs: number): Promise<void> {
|
|
101
|
+
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async increment(key: string, amount = 1): Promise<number | null> {
|
|
105
|
+
const entry = cache.get(key);
|
|
106
|
+
if (!entry || entry.expiresAt <= Date.now()) return null;
|
|
107
|
+
entry.value.count += amount;
|
|
108
|
+
return entry.value.count;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async delete(key: string): Promise<boolean> {
|
|
112
|
+
return cache.delete(key);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
isAvailable(): boolean {
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async shutdown(): Promise<void> {
|
|
120
|
+
cache.clear();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('RateLimitService', () => {
|
|
126
|
+
let store: RateLimitStore;
|
|
127
|
+
let cache: RateLimitCache;
|
|
128
|
+
let service: RateLimitService;
|
|
129
|
+
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
store = createMockStore();
|
|
132
|
+
cache = createMockCache();
|
|
133
|
+
service = new RateLimitService({
|
|
134
|
+
store,
|
|
135
|
+
cache,
|
|
136
|
+
defaults: {
|
|
137
|
+
windowMs: 60000,
|
|
138
|
+
maxRequests: 100,
|
|
139
|
+
strategy: 'sliding-window',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(async () => {
|
|
145
|
+
await store.shutdown();
|
|
146
|
+
await cache.shutdown();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('checkLimit', () => {
|
|
150
|
+
it('should return not limited for first request', async () => {
|
|
151
|
+
const status = await service.checkLimit('test:key', { increment: false });
|
|
152
|
+
|
|
153
|
+
expect(status.limited).toBe(false);
|
|
154
|
+
expect(status.current).toBe(0);
|
|
155
|
+
expect(status.limit).toBe(100);
|
|
156
|
+
expect(status.remaining).toBe(100);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should use provided options over defaults', async () => {
|
|
160
|
+
const status = await service.checkLimit('test:key', {
|
|
161
|
+
maxRequests: 50,
|
|
162
|
+
windowMs: 30000,
|
|
163
|
+
increment: false,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(status.limit).toBe(50);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('incrementLimit', () => {
|
|
171
|
+
it('should increment the counter', async () => {
|
|
172
|
+
const status1 = await service.incrementLimit('test:key');
|
|
173
|
+
expect(status1.current).toBe(1);
|
|
174
|
+
expect(status1.remaining).toBe(99);
|
|
175
|
+
|
|
176
|
+
const status2 = await service.incrementLimit('test:key');
|
|
177
|
+
expect(status2.current).toBe(2);
|
|
178
|
+
expect(status2.remaining).toBe(98);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return limited when max reached', async () => {
|
|
182
|
+
// Set low limit for testing
|
|
183
|
+
for (let i = 0; i < 5; i++) {
|
|
184
|
+
await service.incrementLimit('test:key', { maxRequests: 5 });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const status = await service.incrementLimit('test:key', { maxRequests: 5 });
|
|
188
|
+
expect(status.limited).toBe(true);
|
|
189
|
+
expect(status.remaining).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('isLimited', () => {
|
|
194
|
+
it('should return false when not limited', async () => {
|
|
195
|
+
const limited = await service.isLimited('test:key');
|
|
196
|
+
expect(limited).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return true when limited', async () => {
|
|
200
|
+
// Exhaust the limit
|
|
201
|
+
for (let i = 0; i < 5; i++) {
|
|
202
|
+
await service.incrementLimit('test:key', { maxRequests: 5 });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const limited = await service.isLimited('test:key', { maxRequests: 5 });
|
|
206
|
+
expect(limited).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('clearLimit', () => {
|
|
211
|
+
it('should clear the limit', async () => {
|
|
212
|
+
// Create some limits
|
|
213
|
+
await service.incrementLimit('test:key');
|
|
214
|
+
await service.incrementLimit('test:key');
|
|
215
|
+
|
|
216
|
+
// Verify exists
|
|
217
|
+
const beforeStatus = await service.checkLimit('test:key', { increment: false });
|
|
218
|
+
expect(beforeStatus.current).toBeGreaterThan(0);
|
|
219
|
+
|
|
220
|
+
// Clear
|
|
221
|
+
await service.clearLimit('test:key');
|
|
222
|
+
|
|
223
|
+
// Verify cleared
|
|
224
|
+
const afterStatus = await service.checkLimit('test:key', { increment: false });
|
|
225
|
+
expect(afterStatus.current).toBe(0);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('Strategies', () => {
|
|
231
|
+
describe('Sliding Window', () => {
|
|
232
|
+
it('should create strategy with correct name', () => {
|
|
233
|
+
const strategy = createSlidingWindowStrategy();
|
|
234
|
+
expect(strategy.name).toBe('sliding-window');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('Fixed Window', () => {
|
|
239
|
+
it('should create strategy with correct name', () => {
|
|
240
|
+
const strategy = createFixedWindowStrategy();
|
|
241
|
+
expect(strategy.name).toBe('fixed-window');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('Token Bucket', () => {
|
|
246
|
+
it('should create strategy with correct name', () => {
|
|
247
|
+
const strategy = createTokenBucketStrategy();
|
|
248
|
+
expect(strategy.name).toBe('token-bucket');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Types', () => {
|
|
254
|
+
it('should export all required types', async () => {
|
|
255
|
+
// This test verifies that the types module compiles correctly
|
|
256
|
+
const types = await import('../types.js');
|
|
257
|
+
expect(types).toBeDefined();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limit Cleanup Job
|
|
3
|
+
*
|
|
4
|
+
* Periodically removes expired rate limit records from the database.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RateLimitStore } from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cleanup job configuration
|
|
13
|
+
*/
|
|
14
|
+
export interface CleanupJobConfig {
|
|
15
|
+
/** Store to clean up */
|
|
16
|
+
store: RateLimitStore;
|
|
17
|
+
|
|
18
|
+
/** Cleanup interval in milliseconds (default: 300000 = 5 min) */
|
|
19
|
+
intervalMs?: number;
|
|
20
|
+
|
|
21
|
+
/** Enable debug logging */
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cleanup job state
|
|
27
|
+
*/
|
|
28
|
+
export interface CleanupJob {
|
|
29
|
+
/** Start the cleanup job */
|
|
30
|
+
start(): void;
|
|
31
|
+
|
|
32
|
+
/** Stop the cleanup job */
|
|
33
|
+
stop(): void;
|
|
34
|
+
|
|
35
|
+
/** Run cleanup immediately */
|
|
36
|
+
runNow(): Promise<number>;
|
|
37
|
+
|
|
38
|
+
/** Check if job is running */
|
|
39
|
+
isRunning(): boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a cleanup job
|
|
44
|
+
*
|
|
45
|
+
* @param config Cleanup configuration
|
|
46
|
+
* @returns CleanupJob instance
|
|
47
|
+
*/
|
|
48
|
+
export function createCleanupJob(config: CleanupJobConfig): CleanupJob {
|
|
49
|
+
const {
|
|
50
|
+
store,
|
|
51
|
+
intervalMs = 300000, // 5 minutes default
|
|
52
|
+
debug = false,
|
|
53
|
+
} = config;
|
|
54
|
+
|
|
55
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
56
|
+
let isRunningCleanup = false;
|
|
57
|
+
|
|
58
|
+
function log(message: string, data?: Record<string, unknown>) {
|
|
59
|
+
if (debug) {
|
|
60
|
+
console.log(`[RateLimitCleanup] ${message}`, data || '');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runCleanup(): Promise<number> {
|
|
65
|
+
if (isRunningCleanup) {
|
|
66
|
+
log('Cleanup already in progress, skipping');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isRunningCleanup = true;
|
|
71
|
+
try {
|
|
72
|
+
log('Starting cleanup');
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
const deletedCount = await store.cleanup();
|
|
75
|
+
const duration = Date.now() - startTime;
|
|
76
|
+
|
|
77
|
+
log('Cleanup complete', { deletedCount, durationMs: duration });
|
|
78
|
+
return deletedCount;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('[RateLimitCleanup] Error during cleanup:', error);
|
|
81
|
+
return 0;
|
|
82
|
+
} finally {
|
|
83
|
+
isRunningCleanup = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
start() {
|
|
89
|
+
if (intervalId) {
|
|
90
|
+
log('Cleanup job already running');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
log('Starting cleanup job', { intervalMs });
|
|
95
|
+
intervalId = setInterval(runCleanup, intervalMs);
|
|
96
|
+
|
|
97
|
+
// Run initial cleanup after a short delay
|
|
98
|
+
setTimeout(runCleanup, 10000);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
stop() {
|
|
102
|
+
if (intervalId) {
|
|
103
|
+
log('Stopping cleanup job');
|
|
104
|
+
clearInterval(intervalId);
|
|
105
|
+
intervalId = null;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async runNow(): Promise<number> {
|
|
110
|
+
return runCleanup();
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
isRunning(): boolean {
|
|
114
|
+
return intervalId !== null;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|