@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.
Files changed (149) hide show
  1. package/README.md +157 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +114 -0
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/types.d.ts +19 -0
  6. package/dist/core/types.d.ts.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  12. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  13. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  14. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  15. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  16. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  18. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  19. package/dist/plugins/auth/config-store.d.ts +11 -0
  20. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  21. package/dist/plugins/auth/config-store.js +232 -0
  22. package/dist/plugins/auth/config-store.js.map +1 -0
  23. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  24. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  25. package/dist/plugins/auth/config-store.test.js +299 -0
  26. package/dist/plugins/auth/config-store.test.js.map +1 -0
  27. package/dist/plugins/auth/env-config.d.ts +51 -1
  28. package/dist/plugins/auth/env-config.d.ts.map +1 -1
  29. package/dist/plugins/auth/env-config.js +640 -7
  30. package/dist/plugins/auth/env-config.js.map +1 -1
  31. package/dist/plugins/auth/index.d.ts +6 -2
  32. package/dist/plugins/auth/index.d.ts.map +1 -1
  33. package/dist/plugins/auth/index.js +5 -1
  34. package/dist/plugins/auth/index.js.map +1 -1
  35. package/dist/plugins/auth/types.d.ts +106 -0
  36. package/dist/plugins/auth/types.d.ts.map +1 -1
  37. package/dist/plugins/index.d.ts +4 -2
  38. package/dist/plugins/index.d.ts.map +1 -1
  39. package/dist/plugins/index.js +3 -1
  40. package/dist/plugins/index.js.map +1 -1
  41. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  42. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  43. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  44. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  45. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  46. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  47. package/dist/plugins/rate-limit/cleanup.js +72 -0
  48. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  49. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  50. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  51. package/dist/plugins/rate-limit/env-config.js +318 -0
  52. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  53. package/dist/plugins/rate-limit/index.d.ts +76 -0
  54. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  55. package/dist/plugins/rate-limit/index.js +79 -0
  56. package/dist/plugins/rate-limit/index.js.map +1 -0
  57. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  58. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  59. package/dist/plugins/rate-limit/middleware.js +169 -0
  60. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  61. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  62. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  63. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  64. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  65. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  66. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  67. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  68. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  69. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  70. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  71. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  72. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  73. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  74. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  75. package/dist/plugins/rate-limit/stores/index.js +8 -0
  76. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  77. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  78. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  79. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  80. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  81. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  82. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  83. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  84. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  85. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  86. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  87. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  88. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  89. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  90. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  91. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  92. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  93. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  94. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  95. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  96. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  97. package/dist/plugins/rate-limit/types.d.ts +265 -0
  98. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  99. package/dist/plugins/rate-limit/types.js +9 -0
  100. package/dist/plugins/rate-limit/types.js.map +1 -0
  101. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  102. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  103. package/dist-ui/index.html +1 -1
  104. package/dist-ui-lib/api/controlPanelApi.d.ts +141 -0
  105. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  106. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  107. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  108. package/dist-ui-lib/index.js +3332 -2343
  109. package/dist-ui-lib/index.js.map +1 -1
  110. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  111. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  112. package/package.json +1 -1
  113. package/src/core/control-panel.ts +128 -0
  114. package/src/core/types.ts +17 -0
  115. package/src/index.ts +38 -0
  116. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  117. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  118. package/src/plugins/auth/config-store.test.ts +417 -0
  119. package/src/plugins/auth/config-store.ts +305 -0
  120. package/src/plugins/auth/env-config.ts +714 -7
  121. package/src/plugins/auth/index.ts +22 -1
  122. package/src/plugins/auth/types.ts +138 -0
  123. package/src/plugins/index.ts +49 -0
  124. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  125. package/src/plugins/rate-limit/cleanup.ts +117 -0
  126. package/src/plugins/rate-limit/env-config.ts +400 -0
  127. package/src/plugins/rate-limit/index.ts +128 -0
  128. package/src/plugins/rate-limit/middleware.ts +212 -0
  129. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  130. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  131. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  132. package/src/plugins/rate-limit/stores/index.ts +8 -0
  133. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  134. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  135. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  136. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  137. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  138. package/src/plugins/rate-limit/types.ts +338 -0
  139. package/ui/src/App.tsx +32 -14
  140. package/ui/src/api/controlPanelApi.ts +226 -0
  141. package/ui/src/dashboard/builtInWidgets.tsx +5 -1
  142. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  143. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  144. package/ui/src/dashboard/widgets/index.ts +2 -0
  145. package/ui/src/pages/AuthPage.tsx +986 -142
  146. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  147. package/ui/src/pages/RateLimitPage.tsx +292 -0
  148. package/dist-ui/assets/index-BY8OxNgO.js +0 -465
  149. 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 { createAuthPluginFromEnv, getAuthStatus } from './env-config.js';
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
+ }
@@ -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
+ }