From 726f7ab7726ff3fe1cb8e6d7f65cdcae9b8d0802 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Sun, 12 Jul 2026 19:43:53 -0500 Subject: [PATCH] test(tess): cover streamed private key redaction --- .../src/chat/chat.gateway-redaction.spec.ts | 25 +++++++++++++++++++ apps/gateway/src/chat/chat.gateway.ts | 11 ++++++++ packages/log/src/redaction.ts | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/gateway/src/chat/chat.gateway-redaction.spec.ts b/apps/gateway/src/chat/chat.gateway-redaction.spec.ts index e4e6b64..23c7060 100644 --- a/apps/gateway/src/chat/chat.gateway-redaction.spec.ts +++ b/apps/gateway/src/chat/chat.gateway-redaction.spec.ts @@ -94,6 +94,31 @@ describe('ChatGateway redaction boundary', (): void => { expect(JSON.stringify(client.emit.mock.calls)).not.toContain('canaryvalue123'); }); + it('holds a streamed private key until it can be redacted', (): void => { + const { gateway } = buildGateway(); + const client = { + connected: true, + id: 'client-1', + data: { user: { id: 'user-1' } }, + emit: vi.fn(), + }; + + gateway.relayEvent(client, CONVERSATION_ID, { + type: 'message_update', + assistantMessageEvent: { type: 'text_delta', delta: '-----BEGIN PRIVATE KEY-----\ncanary' }, + }); + gateway.relayEvent(client, CONVERSATION_ID, { + type: 'message_update', + assistantMessageEvent: { type: 'text_delta', delta: '\n-----END PRIVATE KEY-----' }, + }); + + expect(client.emit).toHaveBeenCalledWith('agent:text', { + conversationId: CONVERSATION_ID, + text: '[REDACTED_SECRET]', + }); + expect(JSON.stringify(client.emit.mock.calls)).not.toContain('canary'); + }); + it('drops an oversized unterminated stream fragment rather than retaining it', (): void => { const { gateway } = buildGateway(); const client = { diff --git a/apps/gateway/src/chat/chat.gateway.ts b/apps/gateway/src/chat/chat.gateway.ts index ce9b47d..7b4f0b3 100644 --- a/apps/gateway/src/chat/chat.gateway.ts +++ b/apps/gateway/src/chat/chat.gateway.ts @@ -858,6 +858,17 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa ); } + const privateKeyStart = content.lastIndexOf('-----BEGIN'); + if (privateKeyStart >= 0) { + const privateKey = content.slice(privateKeyStart); + if (/-----END(?: [A-Z]+)* KEY-----/.test(privateKey)) { + // Release the complete block in one pass so the full-block classifier can redact it. + retainedFrom = content.length; + } else { + retainedFrom = Math.min(retainedFrom, privateKeyStart); + } + } + return retainedFrom; } diff --git a/packages/log/src/redaction.ts b/packages/log/src/redaction.ts index d0d54d2..3a9320b 100644 --- a/packages/log/src/redaction.ts +++ b/packages/log/src/redaction.ts @@ -11,7 +11,7 @@ const SECRET_PATTERNS: RegExp[] = [ /\b(?:authorization\s*:\s*)?bearer\s+[A-Za-z0-9._~+/-]+=*/gi, /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, - /-----BEGIN(?: [A-Z]+)* PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z]+)* PRIVATE KEY-----/g, + /-----BEGIN(?: [A-Z]+)* KEY-----[\s\S]*?-----END(?: [A-Z]+)* KEY-----/g, /https?:\/\/[^\s?#]+[^\s]*[?&](?:token|key|secret|signature|sig)=[^\s&#]+/gi, ]; const PII_PATTERNS: RegExp[] = [