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.
@@ -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-4UlRaqol.js"></script>
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 {
@@ -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().
@@ -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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",