allowlist: [],
denylist: [],
};
+const markerConfig: MaskingConfig = {
+ ...defaultConfig,
+ show_markers: true,
+};
/**
* Helper to create a ReadableStream from Anthropic SSE data
expect(result).toContain("secret-key-value");
});
+ test("adds markers to streamed secrets when show_markers is true", async () => {
+ const piiContext = createMaskingContext();
+ const secretsContext = createMaskingContext();
+ secretsContext.mapping["[[SECRET_API_KEY_1]]"] = "sk-secret";
+
+ const chunks = [createTextDelta("Key: [[SECRET_"), createTextDelta("API_KEY_1]]")];
+ const source = createSSEStream(chunks);
+
+ const unmaskedStream = createAnthropicUnmaskingStream(
+ source,
+ piiContext,
+ markerConfig,
+ secretsContext,
+ );
+ const result = await consumeStream(unmaskedStream);
+
+ expect(result).toContain("[protected]sk-secret");
+ expect(result).not.toContain("[[SECRET_API_KEY_1]]");
+ });
+
test("unmasks both PII and secrets", async () => {
const piiContext = createMaskingContext();
piiContext.mapping["[[PERSON_1]]"] = "Alice";
}
if (secretsBuffer && secretsContext) {
- flushed += flushSecretsMaskingBuffer(secretsBuffer, secretsContext);
+ flushed += flushSecretsMaskingBuffer(secretsBuffer, secretsContext, config);
} else if (secretsBuffer) {
flushed += secretsBuffer;
}
secretsBuffer,
processedText,
secretsContext,
+ config,
);
secretsBuffer = remainingBuffer;
processedText = output;
allowlist: [],
denylist: [],
};
+const markerConfig: MaskingConfig = {
+ ...defaultConfig,
+ show_markers: true,
+};
function createSSEStream(chunks: string[]): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
expect(result).toContain('"type":"text"');
expect(result).toContain('"text":"Hello John"');
});
+
+ test("adds markers to streamed secrets when show_markers is true", async () => {
+ const piiContext = createMaskingContext();
+ const secretsContext = createMaskingContext();
+ secretsContext.mapping["[[API_KEY_SK_1]]"] = "sk-secret";
+
+ const chunks = [
+ `data: {"choices":[{"delta":{"content":"Key: [[API_KEY"}}]}\n\n`,
+ `data: {"choices":[{"delta":{"content":"_SK_1]]"}}]}\n\n`,
+ ];
+ const source = createSSEStream(chunks);
+
+ const unmaskedStream = createUnmaskingStream(source, piiContext, markerConfig, secretsContext);
+ const result = await consumeStream(unmaskedStream);
+
+ expect(result).toContain("[protected]sk-secret");
+ expect(result).not.toContain("[[API_KEY_SK_1]]");
+ });
});
nextSecretsBuffer,
processedText,
secretsContext,
+ config,
);
nextSecretsBuffer = remainingBuffer;
processedText = output;
}
if (secretsBuffer && secretsContext) {
- flushed += flushSecretsMaskingBuffer(secretsBuffer, secretsContext);
+ flushed += flushSecretsMaskingBuffer(secretsBuffer, secretsContext, config);
} else if (secretsBuffer) {
flushed += secretsBuffer;
}
}
if (secretsContext) {
- result = unmaskSecretsResponse(result, secretsContext, anthropicExtractor);
+ result = unmaskSecretsResponse(result, secretsContext, config.masking, anthropicExtractor);
}
return c.json(result);
const config = getConfig();
const originalMode = config.mode;
const originalSecretsAction = config.secrets_detection.action;
+const originalShowMarkers = config.masking.show_markers;
+const originalMarkerText = config.masking.marker_text;
interface CapturedRequest {
url: string;
globalThis.fetch = originalFetch;
config.mode = originalMode;
config.secrets_detection.action = originalSecretsAction;
+ config.masking.show_markers = originalShowMarkers;
+ config.masking.marker_text = originalMarkerText;
mockAnalyzeRequest.mockResolvedValue({
hasPII: false,
spanEntities: [],
expect(parsed.delta).toBe(`Key ${secret}`);
});
+ test("adds markers to streamed Codex secrets when show_markers is true", async () => {
+ config.masking.show_markers = true;
+ config.masking.marker_text = "[protected]";
+
+ const secret =
+ "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtest\n-----END RSA PRIVATE KEY-----";
+
+ globalThis.fetch = (async (_input: string | URL | Request, _init?: RequestInit) =>
+ Promise.resolve(
+ new Response(
+ 'data: {"type":"response.output_text.delta","delta":"Key [[PEM_PRIVATE_KEY_1]]"}\n\n',
+ {
+ status: 200,
+ headers: { "Content-Type": "text/event-stream" },
+ },
+ ),
+ )) as typeof fetch;
+
+ const res = await app.request("/codex/responses", {
+ method: "POST",
+ body: JSON.stringify({ model: "gpt-5.5", input: `Key ${secret}` }),
+ headers: {
+ Authorization: "Bearer chatgpt-token",
+ "Content-Type": "application/json",
+ },
+ });
+
+ expect(res.status).toBe(200);
+ const text = await res.text();
+ const dataLine = text.split("\n").find((line) => line.startsWith("data: "));
+ expect(dataLine).toBeDefined();
+ const parsed = JSON.parse(dataLine!.slice(6)) as { delta: string };
+ expect(parsed.delta).toBe(`Key [protected]${secret}`);
+ });
+
test("logs only an error when a non-streaming Codex response is invalid JSON", async () => {
const originalConsoleError = console.error;
console.error = mock(() => {}) as typeof console.error;
result = unmaskPIIResponse(result, piiContext, maskingConfig, codexExtractor);
}
if (secretsContext) {
- result = unmaskSecretsResponse(result, secretsContext, codexExtractor);
+ result = unmaskSecretsResponse(result, secretsContext, maskingConfig, codexExtractor);
}
return c.json(result);
secretsBuffer,
span.text,
secretsContext,
+ maskingConfig,
);
secretsBuffer = remainingBuffer;
return { ...span, maskedText: output };
if (secretsContext && secretsBuffer) {
finalOutput += `data: ${JSON.stringify({
type: "response.output_text.delta",
- delta: flushSecretsMaskingBuffer(secretsBuffer, secretsContext),
+ delta: flushSecretsMaskingBuffer(secretsBuffer, secretsContext, maskingConfig),
})}\n\n`;
}
if (finalOutput) {
return respondStreaming(
c,
result,
+ config.masking,
piiMaskingContext,
secretsResult.maskingContext,
- config.masking,
);
}
return respondJson(
c,
result.response,
+ config.masking,
piiMaskingContext,
secretsResult.maskingContext,
- config.masking,
);
} catch (error) {
return handleProviderError(
function respondStreaming(
c: Context,
result: ProviderResult & { isStreaming: true },
+ maskingConfig: MaskingConfig,
piiContext?: PlaceholderContext,
secretsContext?: PlaceholderContext,
- maskingConfig?: MaskingConfig,
) {
setStreamingHeaders(c);
const stream = createUnmaskingStream(
result.response,
piiContext,
- maskingConfig!,
+ maskingConfig,
secretsContext,
);
return c.body(stream);
function respondJson(
c: Context,
response: OpenAIResponse,
+ maskingConfig: MaskingConfig,
piiContext?: PlaceholderContext,
secretsContext?: PlaceholderContext,
- maskingConfig?: MaskingConfig,
) {
let result = response;
if (piiContext) {
- result = unmaskPIIResponse(result, piiContext, maskingConfig!, openaiExtractor);
+ result = unmaskPIIResponse(result, piiContext, maskingConfig, openaiExtractor);
}
if (secretsContext) {
- result = unmaskSecretsResponse(result, secretsContext, openaiExtractor);
+ result = unmaskSecretsResponse(result, secretsContext, maskingConfig, openaiExtractor);
}
return c.json(result);
import { describe, expect, test } from "bun:test";
+import type { MaskingConfig } from "../config";
+import { anthropicExtractor } from "../masking/extractors/anthropic";
+import { type CodexResponsesResponse, codexExtractor } from "../masking/extractors/codex";
import { openaiExtractor } from "../masking/extractors/openai";
+import type { AnthropicResponse } from "../providers/anthropic/types";
import type { OpenAIMessage, OpenAIRequest, OpenAIResponse } from "../providers/openai/types";
import { createSecretsResultFromSpans } from "../test-utils/detection-results";
import type { SecretLocation } from "./detect";
const sampleSecret = "sk-proj-abc123def456ghi789jkl012mno345pqr678stu901vwx";
const stripeSecret = "sk_live_abc123def456ghi789jkl012";
+const defaultConfig: MaskingConfig = {
+ show_markers: false,
+ marker_text: "[protected]",
+ allowlist: [],
+ denylist: [],
+};
+const markerConfig: MaskingConfig = {
+ ...defaultConfig,
+ show_markers: true,
+};
/** Helper to create a minimal request from messages */
function createRequest(messages: OpenAIMessage[]): OpenAIRequest {
const context = createSecretsMaskingContext();
context.mapping["[[API_KEY_SK_1]]"] = sampleSecret;
- const { output, remainingBuffer } = unmaskSecretsStreamChunk("", "Key: [[API_KEY", context);
+ const { output, remainingBuffer } = unmaskSecretsStreamChunk(
+ "",
+ "Key: [[API_KEY",
+ context,
+ defaultConfig,
+ );
expect(output).toBe("Key: ");
expect(remainingBuffer).toBe("[[API_KEY");
"[[API_KEY",
"_SK_1]] done",
context,
+ defaultConfig,
);
expect(output).toBe(`${sampleSecret} done`);
test("flushes incomplete buffer as-is", () => {
const context = createSecretsMaskingContext();
- const result = flushSecretsMaskingBuffer("[[API_KEY", context);
+ const result = flushSecretsMaskingBuffer("[[API_KEY", context, defaultConfig);
expect(result).toBe("[[API_KEY");
});
+
+ test("adds markers to streamed secret restoration when show_markers is true", () => {
+ const context = createSecretsMaskingContext();
+ context.mapping["[[API_KEY_SK_1]]"] = sampleSecret;
+
+ const { output, remainingBuffer } = unmaskSecretsStreamChunk(
+ "",
+ "Key: [[API_KEY_SK_1]]",
+ context,
+ markerConfig,
+ );
+
+ expect(output).toBe(`Key: [protected]${sampleSecret}`);
+ expect(remainingBuffer).toBe("");
+ });
});
describe("mask -> unmask roundtrip", () => {
expect(masked).not.toContain(sampleSecret);
expect(masked).toContain("[[API_KEY_SK_1]]");
- const restored = unmaskSecrets(masked, context);
+ const restored = unmaskSecrets(masked, context, defaultConfig);
expect(restored).toBe(originalText);
});
+
+ test("does not add markers when show_markers is false", () => {
+ const context = createSecretsMaskingContext();
+ context.mapping["[[API_KEY_SK_1]]"] = sampleSecret;
+
+ const restored = unmaskSecrets("Key: [[API_KEY_SK_1]]", context, defaultConfig);
+
+ expect(restored).toBe(`Key: ${sampleSecret}`);
+ });
});
describe("unmaskSecretsResponse", () => {
- test("unmasks all choices in response", () => {
+ test("unmasks all OpenAI choices in response", () => {
const context = createSecretsMaskingContext();
context.mapping["[[API_KEY_SK_1]]"] = sampleSecret;
],
};
- const result = unmaskSecretsResponse(response, context, openaiExtractor);
+ const result = unmaskSecretsResponse(response, context, defaultConfig, openaiExtractor);
expect(result.choices[0].message.content).toBe(`Your key is ${sampleSecret}`);
});
+ test("adds markers to OpenAI response secrets when show_markers is true", () => {
+ const context = createSecretsMaskingContext();
+ context.mapping["[[API_KEY_SK_1]]"] = sampleSecret;
+
+ const response: OpenAIResponse = {
+ id: "test",
+ object: "chat.completion",
+ created: Date.now(),
+ model: "gpt-4",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "Your key is [[API_KEY_SK_1]]",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+
+ const result = unmaskSecretsResponse(response, context, markerConfig, openaiExtractor);
+ expect(result.choices[0].message.content).toBe(`Your key is [protected]${sampleSecret}`);
+ });
+
+ test("adds markers to Anthropic response secrets when show_markers is true", () => {
+ const context = createSecretsMaskingContext();
+ context.mapping["[[API_KEY_SK_1]]"] = sampleSecret;
+
+ const response: AnthropicResponse = {
+ id: "msg_test",
+ type: "message",
+ role: "assistant",
+ content: [{ type: "text", text: "Your key is [[API_KEY_SK_1]]" }],
+ model: "claude-3-5-sonnet",
+ stop_reason: "end_turn",
+ stop_sequence: null,
+ usage: { input_tokens: 10, output_tokens: 5 },
+ };
+
+ const result = unmaskSecretsResponse(response, context, markerConfig, anthropicExtractor);
+ expect(result.content[0]).toEqual({
+ type: "text",
+ text: `Your key is [protected]${sampleSecret}`,
+ });
+ });
+
+ test("adds markers to Codex response secrets when show_markers is true", () => {
+ const context = createSecretsMaskingContext();
+ context.mapping["[[API_KEY_SK_1]]"] = sampleSecret;
+
+ const response: CodexResponsesResponse = {
+ output: [{ content: [{ type: "output_text", text: "Your key is [[API_KEY_SK_1]]" }] }],
+ };
+
+ const result = unmaskSecretsResponse(response, context, markerConfig, codexExtractor);
+ expect(result).toEqual({
+ output: [
+ { content: [{ type: "output_text", text: `Your key is [protected]${sampleSecret}` }] },
+ ],
+ });
+ });
+
test("preserves response structure", () => {
const context = createSecretsMaskingContext();
const response: OpenAIResponse = {
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
};
- const result = unmaskSecretsResponse(response, context, openaiExtractor);
+ const result = unmaskSecretsResponse(response, context, defaultConfig, openaiExtractor);
expect(result.id).toBe("test-id");
expect(result.model).toBe("gpt-4-turbo");
expect(result.usage).toEqual({ prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 });
* Secrets masking
*/
+import type { MaskingConfig } from "../config";
import { resolveOverlaps } from "../masking/conflict-resolver";
import { incrementAndGenerate } from "../masking/context";
import { generateSecretPlaceholder } from "../masking/placeholders";
return incrementAndGenerate(secretType, context, generateSecretPlaceholder);
}
+function getFormatValue(config: MaskingConfig): ((original: string) => string) | undefined {
+ return config.show_markers ? (original: string) => `${config.marker_text}${original}` : undefined;
+}
+
/**
* Masks secrets in text, replacing them with placeholders
*/
/**
* Unmasks text by replacing placeholders with original secrets
*/
-export function unmaskSecrets(text: string, context: PlaceholderContext): string {
- return unmaskText(text, context);
+export function unmaskSecrets(
+ text: string,
+ context: PlaceholderContext,
+ config: MaskingConfig,
+): string {
+ return unmaskText(text, context, getFormatValue(config));
}
/**
buffer: string,
newChunk: string,
context: PlaceholderContext,
+ config: MaskingConfig,
): { output: string; remainingBuffer: string } {
- return unmaskChunk(buffer, newChunk, context);
+ return unmaskChunk(buffer, newChunk, context, getFormatValue(config));
}
/**
* Flushes remaining buffer at end of stream
*/
-export function flushSecretsMaskingBuffer(buffer: string, context: PlaceholderContext): string {
- return flushBuffer(buffer, context);
+export function flushSecretsMaskingBuffer(
+ buffer: string,
+ context: PlaceholderContext,
+ config: MaskingConfig,
+): string {
+ return flushBuffer(buffer, context, getFormatValue(config));
}
/**
export function unmaskSecretsResponse<TRequest, TResponse>(
response: TResponse,
context: PlaceholderContext,
+ config: MaskingConfig,
extractor: RequestExtractor<TRequest, TResponse>,
): TResponse {
- return extractor.unmaskResponse(response, context);
+ return extractor.unmaskResponse(response, context, getFormatValue(config));
}
/**