aegis-bridge 2.3.3 → 2.3.5

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.
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { webhookEndpointSchema, getErrorMessage } from '../validation.js';
8
8
  import { validateWebhookUrl } from '../ssrf.js';
9
+ import { redactSecretsFromText } from '../utils/redact-headers.js';
9
10
  export class WebhookChannel {
10
11
  name = 'webhook';
11
12
  endpoints;
@@ -124,7 +125,7 @@ export class WebhookChannel {
124
125
  }
125
126
  }
126
127
  catch (e) {
127
- lastError = getErrorMessage(e);
128
+ lastError = redactSecretsFromText(getErrorMessage(e), ep.headers);
128
129
  if (attempt < maxRetries) {
129
130
  const delay = WebhookChannel.backoff(attempt);
130
131
  console.warn(`Webhook ${ep.url} error for ${event} (attempt ${attempt}/${maxRetries}): ${lastError}, retrying in ${Math.round(delay)}ms`);
package/dist/server.js CHANGED
@@ -448,19 +448,30 @@ app.get('/v1/sessions', async (req) => {
448
448
  app.get('/sessions', async () => sessions.listSessions());
449
449
  /** Validate workDir — delegates to validation.ts (Issue #435). */
450
450
  const validateWorkDirWithConfig = (workDir) => validateWorkDir(workDir, config.allowedWorkDirs);
451
- // Create session
451
+ // Create session (Issue #607: reuse idle session for same workDir)
452
452
  app.post('/v1/sessions', async (req, reply) => {
453
453
  const parsed = createSessionSchema.safeParse(req.body);
454
454
  if (!parsed.success) {
455
455
  return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
456
456
  }
457
457
  const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove } = parsed.data;
458
- console.time("POST_CREATE_SESSION");
459
458
  if (!workDir)
460
459
  return reply.status(400).send({ error: 'workDir is required' });
461
460
  const safeWorkDir = await validateWorkDirWithConfig(workDir);
462
461
  if (typeof safeWorkDir === 'object')
463
462
  return reply.status(400).send({ error: safeWorkDir.error, code: safeWorkDir.code });
463
+ // Issue #607: Check for an existing idle session with the same workDir
464
+ const existing = sessions.findIdleSessionByWorkDir(safeWorkDir);
465
+ if (existing) {
466
+ // Send prompt to the existing session if provided
467
+ let promptDelivery;
468
+ if (prompt) {
469
+ promptDelivery = await sessions.sendInitialPrompt(existing.id, prompt);
470
+ metrics.promptSent(promptDelivery.delivered);
471
+ }
472
+ return reply.status(200).send({ ...existing, reused: true, promptDelivery });
473
+ }
474
+ console.time("POST_CREATE_SESSION");
464
475
  const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
465
476
  console.timeEnd("POST_CREATE_SESSION");
466
477
  console.time("POST_CHANNEL_CREATED");
@@ -490,7 +501,7 @@ app.post('/v1/sessions', async (req, reply) => {
490
501
  }
491
502
  return reply.status(201).send({ ...session, promptDelivery });
492
503
  });
493
- // Backwards compat
504
+ // Backwards compat (Issue #607: same reuse logic as v1 route)
494
505
  app.post('/sessions', async (req, reply) => {
495
506
  const parsed = createSessionSchema.safeParse(req.body);
496
507
  if (!parsed.success) {
@@ -502,6 +513,16 @@ app.post('/sessions', async (req, reply) => {
502
513
  const safeWorkDir = await validateWorkDirWithConfig(workDir);
503
514
  if (typeof safeWorkDir === 'object')
504
515
  return reply.status(400).send({ error: safeWorkDir.error, code: safeWorkDir.code });
516
+ // Issue #607: Check for an existing idle session with the same workDir
517
+ const existing = sessions.findIdleSessionByWorkDir(safeWorkDir);
518
+ if (existing) {
519
+ let promptDelivery;
520
+ if (prompt) {
521
+ promptDelivery = await sessions.sendInitialPrompt(existing.id, prompt);
522
+ metrics.promptSent(promptDelivery.delivered);
523
+ }
524
+ return reply.status(200).send({ ...existing, reused: true, promptDelivery });
525
+ }
505
526
  const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
506
527
  // Issue #46: Topic first, then prompt (same fix as v1 route)
507
528
  await channels.sessionCreated({
package/dist/session.d.ts CHANGED
@@ -154,6 +154,10 @@ export declare class SessionManager {
154
154
  isWindowAlive(id: string): Promise<boolean>;
155
155
  /** List all sessions. */
156
156
  listSessions(): SessionInfo[];
157
+ /** Issue #607: Find an idle session for the given workDir.
158
+ * Returns the most recently active idle session, or null if none found.
159
+ * Used to resume existing sessions instead of creating duplicates. */
160
+ findIdleSessionByWorkDir(workDir: string): SessionInfo | null;
157
161
  /** Get health info for a session.
158
162
  * Issue #2: Returns comprehensive health status for orchestrators.
159
163
  */
package/dist/session.js CHANGED
@@ -652,6 +652,17 @@ export class SessionManager {
652
652
  listSessions() {
653
653
  return Object.values(this.state.sessions);
654
654
  }
655
+ /** Issue #607: Find an idle session for the given workDir.
656
+ * Returns the most recently active idle session, or null if none found.
657
+ * Used to resume existing sessions instead of creating duplicates. */
658
+ findIdleSessionByWorkDir(workDir) {
659
+ const candidates = Object.values(this.state.sessions).filter((s) => s.workDir === workDir && s.status === 'idle');
660
+ if (candidates.length === 0)
661
+ return null;
662
+ // Return the most recently active session
663
+ candidates.sort((a, b) => b.lastActivity - a.lastActivity);
664
+ return candidates[0];
665
+ }
655
666
  /** Get health info for a session.
656
667
  * Issue #2: Returns comprehensive health status for orchestrators.
657
668
  */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * redact-headers.ts — Redact sensitive header values for safe logging.
3
+ *
4
+ * Issue #582: Prevent webhook custom headers (Authorization, Cookie, etc.)
5
+ * from leaking into error logs on delivery failures.
6
+ */
7
+ /** Return a copy of `headers` with sensitive values replaced. */
8
+ export declare function redactHeaders(headers: Record<string, string>): Record<string, string>;
9
+ /**
10
+ * Scrub any sensitive header *values* from arbitrary text.
11
+ * If a fetch error message happens to include a header value, this removes it.
12
+ */
13
+ export declare function redactSecretsFromText(text: string, headers: Record<string, string> | undefined): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * redact-headers.ts — Redact sensitive header values for safe logging.
3
+ *
4
+ * Issue #582: Prevent webhook custom headers (Authorization, Cookie, etc.)
5
+ * from leaking into error logs on delivery failures.
6
+ */
7
+ /** Header names whose values should be treated as secrets. Case-insensitive. */
8
+ const SENSITIVE_HEADER_NAMES = new Set([
9
+ 'authorization',
10
+ 'cookie',
11
+ 'set-cookie',
12
+ 'x-api-key',
13
+ 'x-auth-token',
14
+ 'api-key',
15
+ 'apikey',
16
+ 'proxy-authorization',
17
+ 'x-csrf-token',
18
+ 'www-authenticate',
19
+ 'proxy-authenticate',
20
+ ]);
21
+ function isSensitive(headerName) {
22
+ return SENSITIVE_HEADER_NAMES.has(headerName.toLowerCase());
23
+ }
24
+ function redactValue(value) {
25
+ if (value.length <= 8)
26
+ return '[REDACTED]';
27
+ return `${value.slice(0, 4)}...[REDACTED]`;
28
+ }
29
+ /** Return a copy of `headers` with sensitive values replaced. */
30
+ export function redactHeaders(headers) {
31
+ const result = {};
32
+ for (const [name, value] of Object.entries(headers)) {
33
+ result[name] = isSensitive(name) ? redactValue(value) : value;
34
+ }
35
+ return result;
36
+ }
37
+ /**
38
+ * Scrub any sensitive header *values* from arbitrary text.
39
+ * If a fetch error message happens to include a header value, this removes it.
40
+ */
41
+ export function redactSecretsFromText(text, headers) {
42
+ if (!headers)
43
+ return text;
44
+ let result = text;
45
+ for (const [name, value] of Object.entries(headers)) {
46
+ if (!isSensitive(name) || !value)
47
+ continue;
48
+ // Skip very short values — too many false positives
49
+ if (value.length < 4)
50
+ continue;
51
+ result = result.replaceAll(value, '[REDACTED]');
52
+ }
53
+ return result;
54
+ }
55
+ //# sourceMappingURL=redact-headers.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",