lean-claudient-daemon 0.1.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 +39 -0
- package/dist/api.js +54 -0
- package/dist/bin/daemon.js +14 -0
- package/dist/metrics.js +31 -0
- package/dist/metricsStore.js +84 -0
- package/dist/middleware/rateLimiting.js +193 -0
- package/dist/middleware/securityHeaders.js +239 -0
- package/dist/middleware/validation.js +181 -0
- package/dist/security/envValidation.js +266 -0
- package/dist/service.js +47 -0
- package/dist/validation/schemas.js +157 -0
- package/package.json +49 -0
- package/src/api.ts +58 -0
- package/src/bin/daemon.ts +18 -0
- package/src/metrics.ts +45 -0
- package/src/metricsStore.ts +114 -0
- package/src/middleware/rateLimiting.ts +244 -0
- package/src/middleware/securityHeaders.ts +305 -0
- package/src/middleware/validation.ts +222 -0
- package/src/security/envValidation.ts +321 -0
- package/src/service.ts +62 -0
- package/src/validation/schemas.ts +190 -0
package/src/api.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { MetricsStore } from './metricsStore.js';
|
|
3
|
+
|
|
4
|
+
export function createApiRouter(store: MetricsStore): Router {
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
router.get('/status', (req, res) => {
|
|
8
|
+
const uptime = process.uptime();
|
|
9
|
+
const memory = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
10
|
+
res.json({
|
|
11
|
+
running: true,
|
|
12
|
+
uptime: Math.floor(uptime),
|
|
13
|
+
memory: parseFloat(memory.toFixed(2)),
|
|
14
|
+
pid: process.pid,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
router.get('/metrics', async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const metrics = await store.getAggregateMetrics();
|
|
21
|
+
res.json(metrics);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
res.status(500).json({ error: 'Failed to retrieve metrics' });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
router.get('/budget/:sessionId', async (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const budget = await store.getBudgetStatus(req.params.sessionId);
|
|
30
|
+
if (!budget) {
|
|
31
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
32
|
+
}
|
|
33
|
+
res.json(budget);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
res.status(500).json({ error: 'Failed to retrieve budget' });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.get('/sessions', async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const sessions = await store.getAllSessions();
|
|
42
|
+
res.json(sessions);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
res.status(500).json({ error: 'Failed to retrieve sessions' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
router.post('/sessions/:id/checkpoint', async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
await store.saveCheckpoint(req.params.id, req.body.checkpoint);
|
|
51
|
+
res.json({ saved: true });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
res.status(500).json({ error: 'Failed to save checkpoint' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return router;
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { LeanClaudientDaemon } from '../service.js';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('lean-claudient-daemon')
|
|
10
|
+
.version('0.1.0')
|
|
11
|
+
.option('-p, --port <port>', 'Port to listen on', '9831')
|
|
12
|
+
.option('-l, --log <path>', 'Log file path')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const daemon = new LeanClaudientDaemon(parseInt(options.port), options.log);
|
|
15
|
+
await daemon.start();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
program.parse();
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface MetricsRecord {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
inputTokens: number;
|
|
5
|
+
outputTokens: number;
|
|
6
|
+
totalTokens: number;
|
|
7
|
+
cost: number;
|
|
8
|
+
savedTokens: number;
|
|
9
|
+
compressionRatio: number;
|
|
10
|
+
subagentCount: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function aggregateMetrics(records: MetricsRecord[]): any {
|
|
14
|
+
if (records.length === 0) {
|
|
15
|
+
return {
|
|
16
|
+
totalInputTokens: 0,
|
|
17
|
+
totalOutputTokens: 0,
|
|
18
|
+
totalTokens: 0,
|
|
19
|
+
totalCost: '$0.00',
|
|
20
|
+
totalSavedTokens: 0,
|
|
21
|
+
averageCompressionRatio: 0,
|
|
22
|
+
sessionCount: 0,
|
|
23
|
+
averageSubagentCount: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const totalInputTokens = records.reduce((sum, r) => sum + r.inputTokens, 0);
|
|
28
|
+
const totalOutputTokens = records.reduce((sum, r) => sum + r.outputTokens, 0);
|
|
29
|
+
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
30
|
+
const totalCost = totalInputTokens * 0.000003 + totalOutputTokens * 0.000015;
|
|
31
|
+
const totalSavedTokens = records.reduce((sum, r) => sum + r.savedTokens, 0);
|
|
32
|
+
const avgCompressionRatio = records.reduce((sum, r) => sum + r.compressionRatio, 0) / records.length;
|
|
33
|
+
const avgSubagentCount = records.reduce((sum, r) => sum + r.subagentCount, 0) / records.length;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
totalInputTokens,
|
|
37
|
+
totalOutputTokens,
|
|
38
|
+
totalTokens,
|
|
39
|
+
totalCost: `$${totalCost.toFixed(2)}`,
|
|
40
|
+
totalSavedTokens,
|
|
41
|
+
averageCompressionRatio: parseFloat(avgCompressionRatio.toFixed(2)),
|
|
42
|
+
sessionCount: records.length,
|
|
43
|
+
averageSubagentCount: Math.round(avgSubagentCount),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import sqlite3 from 'sqlite3';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { MetricsRecord, aggregateMetrics } from './metrics.js';
|
|
6
|
+
|
|
7
|
+
export class MetricsStore {
|
|
8
|
+
private db?: sqlite3.Database;
|
|
9
|
+
private dbPath: string;
|
|
10
|
+
|
|
11
|
+
constructor(dbPath?: string) {
|
|
12
|
+
this.dbPath = dbPath || path.join(process.env.HOME!, '.lean-claudient', 'daemon.db');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async initialize() {
|
|
16
|
+
const dir = path.dirname(this.dbPath);
|
|
17
|
+
if (!fs.existsSync(dir)) {
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.db = new sqlite3.Database(this.dbPath);
|
|
22
|
+
|
|
23
|
+
const run = promisify(this.db.run.bind(this.db));
|
|
24
|
+
await run(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
sessionId TEXT NOT NULL,
|
|
28
|
+
timestamp INTEGER NOT NULL,
|
|
29
|
+
inputTokens INTEGER,
|
|
30
|
+
outputTokens INTEGER,
|
|
31
|
+
cost REAL,
|
|
32
|
+
savedTokens INTEGER,
|
|
33
|
+
compressionRatio REAL,
|
|
34
|
+
subagentCount INTEGER,
|
|
35
|
+
UNIQUE(sessionId, timestamp)
|
|
36
|
+
)
|
|
37
|
+
`);
|
|
38
|
+
|
|
39
|
+
await run(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
41
|
+
sessionId TEXT PRIMARY KEY,
|
|
42
|
+
checkpoint TEXT NOT NULL,
|
|
43
|
+
timestamp INTEGER NOT NULL
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async recordMetrics(metrics: MetricsRecord) {
|
|
49
|
+
const run = promisify(this.db!.run.bind(this.db!));
|
|
50
|
+
await run(
|
|
51
|
+
`INSERT OR REPLACE INTO metrics (sessionId, timestamp, inputTokens, outputTokens, cost, savedTokens, compressionRatio, subagentCount)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
53
|
+
[
|
|
54
|
+
metrics.sessionId,
|
|
55
|
+
metrics.timestamp,
|
|
56
|
+
metrics.inputTokens,
|
|
57
|
+
metrics.outputTokens,
|
|
58
|
+
metrics.cost,
|
|
59
|
+
metrics.savedTokens,
|
|
60
|
+
metrics.compressionRatio,
|
|
61
|
+
metrics.subagentCount,
|
|
62
|
+
]
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getAggregateMetrics() {
|
|
67
|
+
const all = promisify(this.db!.all.bind(this.db!));
|
|
68
|
+
const records = (await all(
|
|
69
|
+
'SELECT * FROM metrics WHERE timestamp > ? ORDER BY timestamp DESC LIMIT 1000',
|
|
70
|
+
[Date.now() - 7 * 24 * 60 * 60 * 1000]
|
|
71
|
+
)) as MetricsRecord[];
|
|
72
|
+
|
|
73
|
+
return aggregateMetrics(records);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getBudgetStatus(sessionId: string) {
|
|
77
|
+
const get = promisify(this.db!.get.bind(this.db!));
|
|
78
|
+
const record = (await get(
|
|
79
|
+
'SELECT * FROM metrics WHERE sessionId = ? ORDER BY timestamp DESC LIMIT 1',
|
|
80
|
+
[sessionId]
|
|
81
|
+
)) as MetricsRecord | undefined;
|
|
82
|
+
|
|
83
|
+
if (!record) return null;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
sessionId,
|
|
87
|
+
spent: record.cost,
|
|
88
|
+
limit: 500,
|
|
89
|
+
remaining: 500 - record.cost,
|
|
90
|
+
alert: record.cost > 375 ? 'warning' : 'ok',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getAllSessions() {
|
|
95
|
+
const all = promisify(this.db!.all.bind(this.db!));
|
|
96
|
+
return (await all(
|
|
97
|
+
'SELECT DISTINCT sessionId, MIN(timestamp) as startTime, COUNT(*) as recordCount FROM metrics GROUP BY sessionId'
|
|
98
|
+
)) as any[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async saveCheckpoint(sessionId: string, checkpoint: string) {
|
|
102
|
+
const run = promisify(this.db!.run.bind(this.db!));
|
|
103
|
+
await run(
|
|
104
|
+
'INSERT OR REPLACE INTO checkpoints (sessionId, checkpoint, timestamp) VALUES (?, ?, ?)',
|
|
105
|
+
[sessionId, checkpoint, Date.now()]
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async close() {
|
|
110
|
+
return new Promise<void>((resolve, reject) => {
|
|
111
|
+
this.db!.close((err) => (err ? reject(err) : resolve()));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiting Middleware
|
|
3
|
+
* Prevents DOS attacks by limiting requests per IP and session
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Request, Response, NextFunction } from 'express';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rate limit store - stores request counts by key
|
|
10
|
+
* Key format: "ip:address" or "session:sessionId"
|
|
11
|
+
*/
|
|
12
|
+
class RateLimitStore {
|
|
13
|
+
private store: Map<string, { count: number; resetTime: number }> = new Map();
|
|
14
|
+
private cleanupInterval: NodeJS.Timeout;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
// Clean up expired entries every minute
|
|
18
|
+
this.cleanupInterval = setInterval(() => {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [key, value] of this.store.entries()) {
|
|
21
|
+
if (value.resetTime < now) {
|
|
22
|
+
this.store.delete(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, 60000);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record a request and return remaining quota
|
|
30
|
+
*/
|
|
31
|
+
recordRequest(key: string, windowMs: number, limit: number): number {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const entry = this.store.get(key);
|
|
34
|
+
|
|
35
|
+
if (!entry || entry.resetTime < now) {
|
|
36
|
+
this.store.set(key, { count: 1, resetTime: now + windowMs });
|
|
37
|
+
return limit - 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
entry.count++;
|
|
41
|
+
return Math.max(0, limit - entry.count);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if limit is exceeded
|
|
46
|
+
*/
|
|
47
|
+
isLimited(key: string, windowMs: number, limit: number): boolean {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const entry = this.store.get(key);
|
|
50
|
+
|
|
51
|
+
if (!entry || entry.resetTime < now) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return entry.count >= limit;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get remaining time until reset
|
|
60
|
+
*/
|
|
61
|
+
getResetTime(key: string): number {
|
|
62
|
+
const entry = this.store.get(key);
|
|
63
|
+
if (!entry) return 0;
|
|
64
|
+
return Math.max(0, entry.resetTime - Date.now());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Cleanup
|
|
69
|
+
*/
|
|
70
|
+
destroy(): void {
|
|
71
|
+
clearInterval(this.cleanupInterval);
|
|
72
|
+
this.store.clear();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const globalStore = new RateLimitStore();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rate limiting configuration
|
|
80
|
+
*/
|
|
81
|
+
interface RateLimitConfig {
|
|
82
|
+
windowMs: number; // Time window in milliseconds
|
|
83
|
+
max: number; // Max requests per window
|
|
84
|
+
keyGenerator?: (req: Request) => string;
|
|
85
|
+
handler?: (req: Request, res: Response) => void;
|
|
86
|
+
skip?: (req: Request) => boolean;
|
|
87
|
+
onLimitReached?: (req: Request, key: string) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create rate limit middleware
|
|
92
|
+
*/
|
|
93
|
+
export function rateLimit(config: RateLimitConfig) {
|
|
94
|
+
const {
|
|
95
|
+
windowMs = 15 * 60 * 1000, // 15 minutes default
|
|
96
|
+
max = 100,
|
|
97
|
+
keyGenerator = (req) => `ip:${req.ip}`,
|
|
98
|
+
skip,
|
|
99
|
+
onLimitReached,
|
|
100
|
+
} = config;
|
|
101
|
+
|
|
102
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
103
|
+
if (skip && skip(req)) {
|
|
104
|
+
return next();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const key = keyGenerator(req);
|
|
108
|
+
const remaining = globalStore.recordRequest(key, windowMs, max);
|
|
109
|
+
const isLimited = globalStore.isLimited(key, windowMs, max);
|
|
110
|
+
const resetTime = globalStore.getResetTime(key);
|
|
111
|
+
|
|
112
|
+
// Set rate limit headers
|
|
113
|
+
res.setHeader('X-RateLimit-Limit', max);
|
|
114
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining));
|
|
115
|
+
res.setHeader('X-RateLimit-Reset', Math.ceil((Date.now() + resetTime) / 1000));
|
|
116
|
+
|
|
117
|
+
if (isLimited) {
|
|
118
|
+
res.setHeader('Retry-After', Math.ceil(resetTime / 1000));
|
|
119
|
+
|
|
120
|
+
if (onLimitReached) {
|
|
121
|
+
onLimitReached(req, key);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return res.status(429).json({
|
|
125
|
+
error: 'Too Many Requests',
|
|
126
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
127
|
+
details: {
|
|
128
|
+
retryAfter: Math.ceil(resetTime / 1000),
|
|
129
|
+
limit: max,
|
|
130
|
+
windowMs,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
next();
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generic rate limiter by key
|
|
141
|
+
*/
|
|
142
|
+
export function createRateLimiter(config: RateLimitConfig) {
|
|
143
|
+
return rateLimit(config);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Rate limiter by IP address
|
|
148
|
+
*/
|
|
149
|
+
export function createIPRateLimiter(maxRequests: number, windowMinutes: number = 15) {
|
|
150
|
+
return rateLimit({
|
|
151
|
+
windowMs: windowMinutes * 60 * 1000,
|
|
152
|
+
max: maxRequests,
|
|
153
|
+
keyGenerator: (req) => `ip:${req.ip || 'unknown'}`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Rate limiter by session ID
|
|
159
|
+
*/
|
|
160
|
+
export function createSessionRateLimiter(maxRequests: number, windowMinutes: number = 15) {
|
|
161
|
+
return rateLimit({
|
|
162
|
+
windowMs: windowMinutes * 60 * 1000,
|
|
163
|
+
max: maxRequests,
|
|
164
|
+
keyGenerator: (req) => {
|
|
165
|
+
const sessionId = req.params.sessionId || req.query.sessionId;
|
|
166
|
+
return `session:${sessionId || 'unknown'}`;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Rate limiter by user ID (from query or body)
|
|
173
|
+
*/
|
|
174
|
+
export function createUserRateLimiter(maxRequests: number, windowMinutes: number = 15) {
|
|
175
|
+
return rateLimit({
|
|
176
|
+
windowMs: windowMinutes * 60 * 1000,
|
|
177
|
+
max: maxRequests,
|
|
178
|
+
keyGenerator: (req) => {
|
|
179
|
+
const userId = (req.query.userId as string) || (req.body as any)?.userId;
|
|
180
|
+
return `user:${userId || 'anonymous'}`;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Tiered rate limiter configuration
|
|
187
|
+
* Different limits for different endpoints
|
|
188
|
+
*/
|
|
189
|
+
export const RATE_LIMITS = {
|
|
190
|
+
// High frequency - status checks
|
|
191
|
+
STATUS: { max: 100, windowMinutes: 1 },
|
|
192
|
+
|
|
193
|
+
// Medium frequency - metrics reads
|
|
194
|
+
METRICS: { max: 60, windowMinutes: 1 },
|
|
195
|
+
|
|
196
|
+
// Medium frequency - budget checks
|
|
197
|
+
BUDGET: { max: 60, windowMinutes: 1 },
|
|
198
|
+
|
|
199
|
+
// Low frequency - session creation
|
|
200
|
+
SESSIONS: { max: 30, windowMinutes: 1 },
|
|
201
|
+
|
|
202
|
+
// Very low frequency - checkpoints
|
|
203
|
+
CHECKPOINT: { max: 10, windowMinutes: 1 },
|
|
204
|
+
|
|
205
|
+
// Low frequency - admin operations
|
|
206
|
+
ADMIN: { max: 20, windowMinutes: 1 },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Create logging function for rate limit violations
|
|
211
|
+
*/
|
|
212
|
+
export function createRateLimitLogger(logFile?: string) {
|
|
213
|
+
return (req: Request, key: string) => {
|
|
214
|
+
const logEntry = {
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
key,
|
|
217
|
+
method: req.method,
|
|
218
|
+
path: req.path,
|
|
219
|
+
ip: req.ip,
|
|
220
|
+
userAgent: req.get('User-Agent'),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (logFile) {
|
|
224
|
+
// Would log to file in production
|
|
225
|
+
console.warn('[RATE_LIMIT]', JSON.stringify(logEntry));
|
|
226
|
+
} else {
|
|
227
|
+
console.warn('[RATE_LIMIT]', logEntry);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Cleanup rate limiter
|
|
234
|
+
*/
|
|
235
|
+
export function destroyRateLimiter(): void {
|
|
236
|
+
globalStore.destroy();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Reset rate limiter for testing
|
|
241
|
+
*/
|
|
242
|
+
export function resetRateLimiter(): void {
|
|
243
|
+
destroyRateLimiter();
|
|
244
|
+
}
|