@azumag/opencode-rate-limit-fallback 1.49.0 → 1.50.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.
|
@@ -29,7 +29,10 @@ export declare class ConfigReloader {
|
|
|
29
29
|
private worktree?;
|
|
30
30
|
private notifyOnReload;
|
|
31
31
|
private reloadMetrics;
|
|
32
|
-
|
|
32
|
+
private minReloadIntervalMs;
|
|
33
|
+
private recentReloadAttempts;
|
|
34
|
+
private maxReloadAttemptsPerMinute;
|
|
35
|
+
constructor(config: PluginConfig, configPath: string | null, logger: Logger, validator: ConfigValidator, client: OpenCodeClient, components: ComponentRefs, directory: string, worktree?: string, notifyOnReload?: boolean, minReloadIntervalMs?: number, maxReloadAttemptsPerMinute?: number);
|
|
33
36
|
/**
|
|
34
37
|
* Reload configuration from file
|
|
35
38
|
*/
|
|
@@ -50,6 +53,10 @@ export declare class ConfigReloader {
|
|
|
50
53
|
* Get reload metrics
|
|
51
54
|
*/
|
|
52
55
|
getReloadMetrics(): ReloadMetrics;
|
|
56
|
+
/**
|
|
57
|
+
* Check if reload is allowed based on rate limiting
|
|
58
|
+
*/
|
|
59
|
+
private checkRateLimit;
|
|
53
60
|
/**
|
|
54
61
|
* Show success toast notification
|
|
55
62
|
*/
|
|
@@ -17,7 +17,11 @@ export class ConfigReloader {
|
|
|
17
17
|
worktree;
|
|
18
18
|
notifyOnReload;
|
|
19
19
|
reloadMetrics;
|
|
20
|
-
|
|
20
|
+
// Rate limiting for reload operations
|
|
21
|
+
minReloadIntervalMs;
|
|
22
|
+
recentReloadAttempts;
|
|
23
|
+
maxReloadAttemptsPerMinute;
|
|
24
|
+
constructor(config, configPath, logger, validator, client, components, directory, worktree, notifyOnReload = true, minReloadIntervalMs = 1000, maxReloadAttemptsPerMinute = 10) {
|
|
21
25
|
this.config = config;
|
|
22
26
|
this.configPath = configPath;
|
|
23
27
|
this.logger = logger;
|
|
@@ -32,6 +36,10 @@ export class ConfigReloader {
|
|
|
32
36
|
successfulReloads: 0,
|
|
33
37
|
failedReloads: 0,
|
|
34
38
|
};
|
|
39
|
+
// Rate limiting settings
|
|
40
|
+
this.minReloadIntervalMs = minReloadIntervalMs;
|
|
41
|
+
this.maxReloadAttemptsPerMinute = maxReloadAttemptsPerMinute;
|
|
42
|
+
this.recentReloadAttempts = [];
|
|
35
43
|
}
|
|
36
44
|
/**
|
|
37
45
|
* Reload configuration from file
|
|
@@ -41,9 +49,17 @@ export class ConfigReloader {
|
|
|
41
49
|
success: false,
|
|
42
50
|
timestamp: Date.now(),
|
|
43
51
|
};
|
|
52
|
+
// Rate limiting check
|
|
53
|
+
const rateLimitCheck = this.checkRateLimit();
|
|
54
|
+
if (!rateLimitCheck.allowed) {
|
|
55
|
+
result.error = rateLimitCheck.reason || 'Rate limit exceeded';
|
|
56
|
+
this.logger.warn(`Config reload blocked: ${result.error}`);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
44
59
|
// Track reload metrics
|
|
45
60
|
this.reloadMetrics.totalReloads++;
|
|
46
61
|
this.reloadMetrics.lastReloadTime = result.timestamp;
|
|
62
|
+
this.recentReloadAttempts.push(result.timestamp);
|
|
47
63
|
if (!this.configPath) {
|
|
48
64
|
result.error = 'No config file path available';
|
|
49
65
|
this.reloadMetrics.failedReloads++;
|
|
@@ -173,6 +189,34 @@ export class ConfigReloader {
|
|
|
173
189
|
getReloadMetrics() {
|
|
174
190
|
return { ...this.reloadMetrics };
|
|
175
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if reload is allowed based on rate limiting
|
|
194
|
+
*/
|
|
195
|
+
checkRateLimit() {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const oneMinuteAgo = now - 60000;
|
|
198
|
+
// Clean up old reload attempts
|
|
199
|
+
this.recentReloadAttempts = this.recentReloadAttempts.filter(timestamp => timestamp > oneMinuteAgo);
|
|
200
|
+
// Check minimum interval between reloads
|
|
201
|
+
if (this.reloadMetrics.lastReloadTime) {
|
|
202
|
+
const timeSinceLastReload = now - this.reloadMetrics.lastReloadTime;
|
|
203
|
+
if (timeSinceLastReload < this.minReloadIntervalMs) {
|
|
204
|
+
const waitTime = this.minReloadIntervalMs - timeSinceLastReload;
|
|
205
|
+
return {
|
|
206
|
+
allowed: false,
|
|
207
|
+
reason: `Too soon. Wait ${waitTime}ms before reloading`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Check maximum attempts per minute
|
|
212
|
+
if (this.recentReloadAttempts.length >= this.maxReloadAttemptsPerMinute) {
|
|
213
|
+
return {
|
|
214
|
+
allowed: false,
|
|
215
|
+
reason: `Rate limit exceeded. Maximum ${this.maxReloadAttemptsPerMinute} reloads per minute`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { allowed: true };
|
|
219
|
+
}
|
|
176
220
|
/**
|
|
177
221
|
* Show success toast notification
|
|
178
222
|
*/
|
package/dist/src/utils/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Configuration loading and validation
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
5
|
+
import { join, resolve, normalize, relative } from "path";
|
|
6
6
|
import { DEFAULT_FALLBACK_MODELS, VALID_FALLBACK_MODES, VALID_RESET_INTERVALS, DEFAULT_RETRY_POLICY, VALID_RETRY_STRATEGIES, DEFAULT_CIRCUIT_BREAKER_CONFIG, } from '../types/index.js';
|
|
7
7
|
import { DEFAULT_HEALTH_TRACKER_CONFIG, DEFAULT_COOLDOWN_MS, DEFAULT_FALLBACK_MODE, DEFAULT_LOG_CONFIG, DEFAULT_METRICS_CONFIG, DEFAULT_CONFIG_RELOAD_CONFIG, } from '../config/defaults.js';
|
|
8
8
|
/**
|
|
@@ -20,6 +20,32 @@ export const DEFAULT_CONFIG = {
|
|
|
20
20
|
metrics: DEFAULT_METRICS_CONFIG,
|
|
21
21
|
configReload: DEFAULT_CONFIG_RELOAD_CONFIG,
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Validate that a path does not contain directory traversal attempts
|
|
25
|
+
*/
|
|
26
|
+
function validatePathSafety(path, allowedDirs) {
|
|
27
|
+
try {
|
|
28
|
+
const resolvedPath = resolve(path);
|
|
29
|
+
const normalizedPath = normalize(path);
|
|
30
|
+
// Check for obvious path traversal patterns
|
|
31
|
+
if (normalizedPath.includes('..')) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Check that resolved path is within allowed directories
|
|
35
|
+
for (const allowedDir of allowedDirs) {
|
|
36
|
+
const resolvedAllowedDir = resolve(allowedDir);
|
|
37
|
+
const relativePath = relative(resolvedAllowedDir, resolvedPath);
|
|
38
|
+
// If relative path does not start with '..', the path is within the allowed directory
|
|
39
|
+
if (!relativePath.startsWith('..')) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
23
49
|
/**
|
|
24
50
|
* Validate configuration values
|
|
25
51
|
*/
|
|
@@ -70,18 +96,18 @@ export function loadConfig(directory, worktree, logger) {
|
|
|
70
96
|
// Build search paths: worktree first, then directory, then home locations
|
|
71
97
|
const searchDirs = [];
|
|
72
98
|
if (worktree) {
|
|
73
|
-
searchDirs.push(worktree);
|
|
99
|
+
searchDirs.push(resolve(worktree));
|
|
74
100
|
}
|
|
75
101
|
if (!worktree || worktree !== directory) {
|
|
76
|
-
searchDirs.push(directory);
|
|
102
|
+
searchDirs.push(resolve(directory));
|
|
77
103
|
}
|
|
104
|
+
searchDirs.push(resolve(homedir));
|
|
105
|
+
searchDirs.push(resolve(xdgConfigHome));
|
|
78
106
|
const configPaths = [];
|
|
79
107
|
for (const dir of searchDirs) {
|
|
80
108
|
configPaths.push(join(dir, ".opencode", "rate-limit-fallback.json"));
|
|
81
109
|
configPaths.push(join(dir, "rate-limit-fallback.json"));
|
|
82
110
|
}
|
|
83
|
-
configPaths.push(join(homedir, ".opencode", "rate-limit-fallback.json"));
|
|
84
|
-
configPaths.push(join(xdgConfigHome, "opencode", "rate-limit-fallback.json"));
|
|
85
111
|
// Log search paths for debugging
|
|
86
112
|
if (logger) {
|
|
87
113
|
logger.debug(`Searching for config file in ${configPaths.length} locations`);
|
|
@@ -92,6 +118,13 @@ export function loadConfig(directory, worktree, logger) {
|
|
|
92
118
|
}
|
|
93
119
|
for (const configPath of configPaths) {
|
|
94
120
|
if (existsSync(configPath)) {
|
|
121
|
+
// Validate path safety before reading
|
|
122
|
+
if (!validatePathSafety(configPath, searchDirs)) {
|
|
123
|
+
if (logger) {
|
|
124
|
+
logger.warn(`Config file rejected due to path validation: ${configPath}`);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
95
128
|
try {
|
|
96
129
|
const content = readFileSync(configPath, "utf-8");
|
|
97
130
|
const userConfig = JSON.parse(content);
|
package/package.json
CHANGED