aegis-bridge 2.6.1 → 2.6.2
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/dashboard/dist/assets/index-Bfabq3q-.js +262 -0
- package/dashboard/dist/index.html +1 -1
- package/dist/dashboard/assets/index-Bfabq3q-.js +262 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/server.js +54 -2
- package/dist/validation.d.ts +11 -0
- package/dist/validation.js +32 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/index-4UlRaqol.js +0 -262
- package/dist/dashboard/assets/index-4UlRaqol.js +0 -262
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Aegis Dashboard</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>" />
|
|
8
|
-
<script type="module" crossorigin src="/dashboard/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/dashboard/assets/index-Bfabq3q-.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-9Hkkvm_I.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-[#0a0a0f] text-gray-200 antialiased">
|
package/dist/server.js
CHANGED
|
@@ -40,7 +40,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
40
40
|
import { negotiate } from './handshake.js';
|
|
41
41
|
import { diagnosticsBus } from './diagnostics.js';
|
|
42
42
|
import { setStructuredLogSink } from './logger.js';
|
|
43
|
-
import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, } from './validation.js';
|
|
43
|
+
import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, compareSemver, extractCCVersion, MIN_CC_VERSION, } from './validation.js';
|
|
44
44
|
const __filename = fileURLToPath(import.meta.url);
|
|
45
45
|
const __dirname = path.dirname(__filename);
|
|
46
46
|
// ── Configuration ────────────────────────────────────────────────────
|
|
@@ -167,6 +167,31 @@ function checkIpRateLimit(ip, isMaster) {
|
|
|
167
167
|
const limit = isMaster ? IP_LIMIT_MASTER : IP_LIMIT_NORMAL;
|
|
168
168
|
return activeCount > limit;
|
|
169
169
|
}
|
|
170
|
+
const authFailLimits = new Map();
|
|
171
|
+
const AUTH_FAIL_WINDOW_MS = 60_000;
|
|
172
|
+
const AUTH_FAIL_MAX = 5;
|
|
173
|
+
function checkAuthFailRateLimit(ip) {
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
const cutoff = now - AUTH_FAIL_WINDOW_MS;
|
|
176
|
+
const bucket = authFailLimits.get(ip) || { timestamps: [] };
|
|
177
|
+
// Prune expired entries
|
|
178
|
+
bucket.timestamps = bucket.timestamps.filter(t => t >= cutoff);
|
|
179
|
+
bucket.timestamps.push(now);
|
|
180
|
+
authFailLimits.set(ip, bucket);
|
|
181
|
+
return bucket.timestamps.length > AUTH_FAIL_MAX;
|
|
182
|
+
}
|
|
183
|
+
function recordAuthFailure(ip) {
|
|
184
|
+
checkAuthFailRateLimit(ip);
|
|
185
|
+
}
|
|
186
|
+
/** #632: Prune stale auth-failure buckets. */
|
|
187
|
+
function pruneAuthFailLimits() {
|
|
188
|
+
const cutoff = Date.now() - AUTH_FAIL_WINDOW_MS;
|
|
189
|
+
for (const [ip, bucket] of authFailLimits) {
|
|
190
|
+
bucket.timestamps = bucket.timestamps.filter(t => t >= cutoff);
|
|
191
|
+
if (bucket.timestamps.length === 0)
|
|
192
|
+
authFailLimits.delete(ip);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
170
195
|
/** #357: Prune IPs whose timestamp arrays are entirely outside the rate-limit window. */
|
|
171
196
|
function pruneIpRateLimits() {
|
|
172
197
|
const cutoff = Date.now() - IP_WINDOW_MS;
|
|
@@ -242,15 +267,23 @@ function setupAuth(authManager) {
|
|
|
242
267
|
if (!token) {
|
|
243
268
|
return reply.status(401).send({ error: 'Unauthorized — Bearer token required' });
|
|
244
269
|
}
|
|
270
|
+
// #633: Only use req.ip — trustProxy controls whether X-Forwarded-For is considered
|
|
271
|
+
const clientIp = req.ip ?? 'unknown';
|
|
272
|
+
// #632: Block IPs that exceeded auth failure rate limit (5 attempts/min)
|
|
273
|
+
if (checkAuthFailRateLimit(clientIp)) {
|
|
274
|
+
return reply.status(429).send({ error: 'Too many auth failures — try again later' });
|
|
275
|
+
}
|
|
245
276
|
// #297: Check if this is a short-lived SSE token first
|
|
246
277
|
if (isSSERoute && token.startsWith('sse_')) {
|
|
247
278
|
if (await authManager.validateSSEToken(token)) {
|
|
248
279
|
return; // authenticated via short-lived SSE token
|
|
249
280
|
}
|
|
281
|
+
recordAuthFailure(clientIp);
|
|
250
282
|
return reply.status(401).send({ error: 'Unauthorized — SSE token invalid or expired' });
|
|
251
283
|
}
|
|
252
284
|
const result = authManager.validate(token);
|
|
253
285
|
if (!result.valid) {
|
|
286
|
+
recordAuthFailure(clientIp);
|
|
254
287
|
return reply.status(401).send({ error: 'Unauthorized — invalid API key' });
|
|
255
288
|
}
|
|
256
289
|
if (result.rateLimited) {
|
|
@@ -262,7 +295,6 @@ function setupAuth(authManager) {
|
|
|
262
295
|
req.authKeyId = result.keyId;
|
|
263
296
|
// #228: Per-IP rate limiting (applies to all authenticated requests)
|
|
264
297
|
// #633: Only use req.ip — trustProxy controls whether X-Forwarded-For is considered
|
|
265
|
-
const clientIp = req.ip ?? 'unknown';
|
|
266
298
|
const isMaster = result.keyId === 'master';
|
|
267
299
|
if (checkIpRateLimit(clientIp, isMaster)) {
|
|
268
300
|
return reply.status(429).send({ error: 'Rate limit exceeded — IP throttled' });
|
|
@@ -521,6 +553,21 @@ async function createSessionHandler(req, reply) {
|
|
|
521
553
|
const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove } = parsed.data;
|
|
522
554
|
if (!workDir)
|
|
523
555
|
return reply.status(400).send({ error: 'workDir is required' });
|
|
556
|
+
// Issue #564: Validate installed Claude Code version
|
|
557
|
+
try {
|
|
558
|
+
const raw = execFileSync('claude', ['--version'], { encoding: 'utf-8', timeout: 5000 });
|
|
559
|
+
const ccVer = extractCCVersion(raw);
|
|
560
|
+
if (ccVer !== null && compareSemver(ccVer, MIN_CC_VERSION) < 0) {
|
|
561
|
+
return reply.status(422).send({
|
|
562
|
+
error: `Claude Code version ${ccVer} is below minimum supported version ${MIN_CC_VERSION}. Please upgrade.`,
|
|
563
|
+
code: 'CC_VERSION_TOO_OLD',
|
|
564
|
+
upgrade: 'Run: claude update or npm install -g @anthropic-ai/claude-code@latest',
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
// claude CLI not found or timed out — skip version check (fails open)
|
|
570
|
+
}
|
|
524
571
|
const safeWorkDir = await validateWorkDirWithConfig(workDir);
|
|
525
572
|
if (typeof safeWorkDir === 'object')
|
|
526
573
|
return reply.status(400).send({ error: safeWorkDir.error, code: safeWorkDir.code });
|
|
@@ -544,6 +591,8 @@ async function createSessionHandler(req, reply) {
|
|
|
544
591
|
const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
|
|
545
592
|
console.timeEnd("POST_CREATE_SESSION");
|
|
546
593
|
console.time("POST_CHANNEL_CREATED");
|
|
594
|
+
// Issue #625: Track session in metrics so sessionsCreated counter is accurate
|
|
595
|
+
metrics.sessionCreated(session.id);
|
|
547
596
|
// Issue #46: Create Telegram topic BEFORE sending prompt.
|
|
548
597
|
// The monitor starts polling immediately after createSession().
|
|
549
598
|
// If we wait for sendInitialPrompt (up to 15s), the monitor may find
|
|
@@ -1410,6 +1459,8 @@ async function main() {
|
|
|
1410
1459
|
const metricsSaveInterval = setInterval(() => { void metrics.save(); }, 5 * 60 * 1000);
|
|
1411
1460
|
// #357: Prune stale IP rate-limit entries every minute
|
|
1412
1461
|
const ipPruneInterval = setInterval(pruneIpRateLimits, 60_000);
|
|
1462
|
+
// #632: Prune stale auth failure rate-limit buckets every minute
|
|
1463
|
+
const authFailPruneInterval = setInterval(pruneAuthFailLimits, 60_000);
|
|
1413
1464
|
// #398: Sweep stale API key rate limit buckets every 5 minutes
|
|
1414
1465
|
const authSweepInterval = setInterval(() => auth.sweepStaleRateLimits(), 5 * 60_000);
|
|
1415
1466
|
// Issue #361: Graceful shutdown handler
|
|
@@ -1431,6 +1482,7 @@ async function main() {
|
|
|
1431
1482
|
clearInterval(zombieReaperInterval);
|
|
1432
1483
|
clearInterval(metricsSaveInterval);
|
|
1433
1484
|
clearInterval(ipPruneInterval);
|
|
1485
|
+
clearInterval(authFailPruneInterval);
|
|
1434
1486
|
clearInterval(authSweepInterval);
|
|
1435
1487
|
// Issue #569: Kill all CC sessions and tmux windows before exit
|
|
1436
1488
|
try {
|
package/dist/validation.d.ts
CHANGED
|
@@ -270,6 +270,17 @@ export declare const ccSettingsSchema: z.ZodObject<{
|
|
|
270
270
|
}, z.core.$loose>;
|
|
271
271
|
/** Helper: extract error message from unknown catch value. */
|
|
272
272
|
export declare function getErrorMessage(e: unknown): string;
|
|
273
|
+
/** Minimum supported Claude Code version. */
|
|
274
|
+
export declare const MIN_CC_VERSION = "2.1.80";
|
|
275
|
+
/** Parse a semver string into [major, minor, patch], or null if invalid. */
|
|
276
|
+
export declare function parseSemver(v: string): [number, number, number] | null;
|
|
277
|
+
/**
|
|
278
|
+
* Compare two semver strings.
|
|
279
|
+
* Returns -1 if a < b, 0 if equal or either is unparseable (fails open), 1 if a > b.
|
|
280
|
+
*/
|
|
281
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
282
|
+
/** Extract version number from `claude --version` output. */
|
|
283
|
+
export declare function extractCCVersion(output: string): string | null;
|
|
273
284
|
/** Validate workDir to prevent path traversal attacks (Issue #435).
|
|
274
285
|
* 1. Reject raw strings containing ".." before any normalization.
|
|
275
286
|
* 2. Resolve to absolute path and resolve symlinks via fs.realpath().
|
package/dist/validation.js
CHANGED
|
@@ -245,6 +245,38 @@ export function getErrorMessage(e) {
|
|
|
245
245
|
return e;
|
|
246
246
|
return String(e);
|
|
247
247
|
}
|
|
248
|
+
// ── CC version validation (Issue #564) ─────────────────────────────────
|
|
249
|
+
/** Minimum supported Claude Code version. */
|
|
250
|
+
export const MIN_CC_VERSION = '2.1.80';
|
|
251
|
+
/** Parse a semver string into [major, minor, patch], or null if invalid. */
|
|
252
|
+
export function parseSemver(v) {
|
|
253
|
+
const match = v.trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
254
|
+
if (!match)
|
|
255
|
+
return null;
|
|
256
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Compare two semver strings.
|
|
260
|
+
* Returns -1 if a < b, 0 if equal or either is unparseable (fails open), 1 if a > b.
|
|
261
|
+
*/
|
|
262
|
+
export function compareSemver(a, b) {
|
|
263
|
+
const pa = parseSemver(a);
|
|
264
|
+
const pb = parseSemver(b);
|
|
265
|
+
if (!pa || !pb)
|
|
266
|
+
return 0;
|
|
267
|
+
for (let i = 0; i < 3; i++) {
|
|
268
|
+
if (pa[i] < pb[i])
|
|
269
|
+
return -1;
|
|
270
|
+
if (pa[i] > pb[i])
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
/** Extract version number from `claude --version` output. */
|
|
276
|
+
export function extractCCVersion(output) {
|
|
277
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
278
|
+
return match ? match[1] : null;
|
|
279
|
+
}
|
|
248
280
|
/** Default safe base directories used when allowedWorkDirs is not configured.
|
|
249
281
|
* Prevents sessions from running in system-critical directories. */
|
|
250
282
|
function getDefaultSafeDirs() {
|