"@hono/zod-validator": "^0.7.6",
"hono": "^4.12.23",
"hono-tailwind": "^2.2.0",
- "postcss": "^8.5.15",
"tailwindcss": "^4.3.0",
"yaml": "^2.9.0",
"zod": "^3.25.76",
"@hono/zod-validator": "^0.7.6",
"hono": "^4.12.23",
"hono-tailwind": "^2.2.0",
- "postcss": "^8.5.15",
"tailwindcss": "^4.3.0",
"yaml": "^2.9.0",
"zod": "^3.25.76"
// Conflict resolution based on Microsoft Presidio's logic
// https://github.com/microsoft/presidio/blob/main/presidio-anonymizer/presidio_anonymizer/anonymizer_engine.py
-/**
- * Base interface for items with position (used by both PII and secrets)
- */
export interface Span {
start: number;
end: number;
}
-/**
- * Extended interface for PII entities with confidence scores
- */
export interface EntityWithScore extends Span {
score: number;
entity_type: string;
return result;
}
-/** For PII entities with scores. Merges same-type overlaps, removes cross-type conflicts. */
export function resolveConflicts<T extends EntityWithScore>(entities: T[]): T[] {
if (entities.length <= 1) return [...entities];
return removeConflicting(afterMerge);
}
-/**
- * Simple conflict resolution for items without scores (secrets)
- * Keeps non-overlapping spans, longer span wins ties.
- */
+// Secrets have no confidence score, so overlapping ties keep the longer span.
export function resolveOverlaps<T extends Span>(items: T[]): T[] {
if (items.length <= 1) return [...items];
-/**
- * Placeholder context and text transformation utilities
- */
-
import { findPartialPlaceholderStart } from "../masking/placeholders";
import type { Span } from "./conflict-resolver";
-/**
- * Generic context for placeholder-based transformations
- * Used by both PII masking and secrets masking
- */
export interface PlaceholderContext {
- /** Maps placeholder -> original value */
mapping: Record<string, string>;
- /** Maps original value -> placeholder (for deduplication) */
reverseMapping: Record<string, string>;
- /** Counter per type for sequential numbering */
counters: Record<string, number>;
}
-/**
- * Result of masking text with placeholders
- * Used by both PII masking and secrets masking
- */
-export interface MaskResult {
- /** Text with sensitive data replaced by placeholders */
- masked: string;
- /** Context for unmasking (maps placeholders to original values) */
- context: PlaceholderContext;
-}
-
-/**
- * Creates a new placeholder context
- */
export function createPlaceholderContext(): PlaceholderContext {
return {
mapping: {},
};
}
-/**
- * Increments counter for type and generates placeholder using format function
- *
- * Shared counter logic for both PII masking and secrets masking.
- */
export function incrementAndGenerate(
type: string,
context: PlaceholderContext,
return format(type, count);
}
-/**
- * Restores placeholders in text with original values
- *
- * Generic function used by both PII unmasking and secrets unmasking.
- *
- * @param text - Text containing placeholders
- * @param context - Context with placeholder mappings
- * @param formatValue - Optional function to format restored values (e.g., add markers)
- */
export function restorePlaceholders(
text: string,
context: PlaceholderContext,
return result;
}
-/**
- * Replaces items in text with placeholders
- *
- * Generic function used by both PII masking and secrets masking.
- * Handles: conflict resolution, placeholder assignment, and replacement.
- *
- * @param text - Text to process
- * @param items - Items with start/end positions to replace
- * @param context - Placeholder context for tracking mappings
- * @param getType - Function to get the type string from an item
- * @param generatePlaceholder - Function to generate placeholder for a type
- * @param resolveConflicts - Function to resolve overlapping items
- */
export function replaceWithPlaceholders<T extends Span>(
text: string,
items: T[],
return result;
}
-/**
- * Processes a stream chunk, buffering partial placeholders
- *
- * Generic function used by both PII unmasking and secrets unmasking.
- *
- * @param buffer - Previous buffer content
- * @param newChunk - New chunk to process
- * @param context - Placeholder context
- * @param restore - Function to restore placeholders in text
- */
export function processStreamChunk(
buffer: string,
newChunk: string,
};
}
-/**
- * Flushes remaining buffer at end of stream
- *
- * @param buffer - Remaining buffer content
- * @param context - Placeholder context
- * @param restore - Function to restore placeholders in text
- */
export function flushBuffer(
buffer: string,
context: PlaceholderContext,
-/**
- * PII masking
- */
-
import type { MaskingConfig } from "../config";
import { resolveConflicts } from "../masking/conflict-resolver";
import { incrementAndGenerate } from "../masking/context";
export { createMaskingContext, type PlaceholderContext } from "../masking/service";
-/**
- * Result of masking operation
- */
export interface MaskResult {
masked: string;
context: PlaceholderContext;
}
-/**
- * Generates a placeholder for a PII entity type
- */
function generatePlaceholder(entityType: string, context: PlaceholderContext): string {
return incrementAndGenerate(entityType, context, (type, count) =>
generatePlaceholderFromFormat(PII_PLACEHOLDER_FORMAT, type, count),
);
}
-/**
- * Creates formatValue function from masking config
- */
function getFormatValue(config: MaskingConfig): ((original: string) => string) | undefined {
return config.show_markers ? (original: string) => `${config.marker_text}${original}` : undefined;
}
-/**
- * Masks PII entities in text, replacing them with placeholders
- */
export function mask(
text: string,
entities: PIIEntity[],
};
}
-/**
- * Unmasks text by replacing placeholders with original values
- */
export function unmask(text: string, context: PlaceholderContext, config: MaskingConfig): string {
return unmaskText(text, context, getFormatValue(config));
}
-/**
- * Streaming unmask helper - processes chunks and unmasks when complete placeholders are found
- */
export function unmaskStreamChunk(
buffer: string,
newChunk: string,
return unmaskChunk(buffer, newChunk, context, getFormatValue(config));
}
-/**
- * Flushes remaining buffer at end of stream
- */
export function flushMaskingBuffer(
buffer: string,
context: PlaceholderContext,
return flushBuffer(buffer, context, getFormatValue(config));
}
-/**
- * Result of masking a request
- */
export interface MaskRequestResult<TRequest> {
- /** The masked request */
request: TRequest;
- /** Masking context for unmasking response */
context: PlaceholderContext;
}
-/**
- * Masks PII in a request using an extractor
- */
export function maskRequest<TRequest, TResponse>(
request: TRequest,
detection: PIIDetectionResult,
);
}
-/**
- * Unmasks a response using a request extractor
- */
export function unmaskResponse<TRequest, TResponse>(
response: TResponse,
context: PlaceholderContext,
-/**
- * Anthropic SSE stream transformer for unmasking PII and secrets
- *
- * Anthropic uses a different SSE format than OpenAI:
- * - event: message_start / content_block_start / content_block_delta / etc.
- * - data: {...}
- *
- * Text content comes in content_block_delta events with delta.type === "text_delta"
- */
+// Anthropic SSE differs from OpenAI: event lines identify message/content events,
+// and text arrives as content_block_delta data with delta.type === "text_delta".
import type { MaskingConfig } from "../../config";
import type { PlaceholderContext } from "../../masking/context";
import { flushSecretsMaskingBuffer, unmaskSecretsStreamChunk } from "../../secrets/mask";
import type { ContentBlockDeltaEvent, TextDelta } from "./types";
-/**
- * Creates a transform stream that unmasks Anthropic SSE content
- */
export function createAnthropicUnmaskingStream(
source: ReadableStream<Uint8Array>,
piiContext: PlaceholderContext | undefined,
-/**
- * Anthropic-compatible messages route
- *
- * Flow:
- * 1. Validate request
- * 2. Process secrets (detect, maybe block, mask, or route_local)
- * 3. Detect PII
- * 4. Route mode: if PII found, send to local provider
- * 5. Mask mode: mask PII if found, send to Anthropic, unmask response
- */
-
import { zValidator } from "@hono/zod-validator";
import type { Context } from "hono";
import { Hono } from "hono";
handleProviderError,
setBlockedHeaders,
setResponseHeaders,
+ setStreamingHeaders,
toPIIHeaderData,
toPIILogData,
toSecretsHeaderData,
export const anthropicRoutes = new Hono();
-/**
- * POST /v1/messages - Anthropic-compatible messages endpoint
- */
anthropicRoutes.post(
"/v1/messages",
zValidator("json", AnthropicRequestSchema, (result, c) => {
},
);
-/**
- * Proxy all other requests to Anthropic
- *
- * Transparent header forwarding - all auth headers from client are passed through.
- */
anthropicRoutes.all("/*", async (c) => {
const config = getConfig();
);
if (result.isStreaming) {
- c.header("Content-Type", "text/event-stream");
- c.header("Cache-Control", "no-cache");
- c.header("Connection", "keep-alive");
+ setStreamingHeaders(c);
return c.body(result.response as ReadableStream);
}
secretsContext: PlaceholderContext | undefined,
) {
const config = getConfig();
- c.header("Content-Type", "text/event-stream");
- c.header("Cache-Control", "no-cache");
- c.header("Connection", "keep-alive");
+ setStreamingHeaders(c);
if (piiMaskingContext || secretsContext) {
const unmaskingStream = createAnthropicUnmaskingStream(
handleProviderError,
setBlockedHeaders,
setResponseHeaders,
+ setStreamingHeaders,
toPIIHeaderData,
toPIILogData,
toSecretsHeaderData,
})
.passthrough();
-/**
- * POST /responses
- *
- * Inspected Codex Responses route. This mirrors the OpenAI/Anthropic protected
- * endpoints: detect secrets/PII, mask before upstream, unmask streamed response,
- * and log the request in the dashboard.
- */
codexRoutes.post(
"/responses",
zValidator("json", CodexResponsesRequestSchema, (result, c) => {
},
);
-/**
- * Wildcard pass-through proxy for /models and any future Codex endpoints that do
- * not carry prompt content.
- */
codexRoutes.all("/*", (c) => {
const config = getConfig();
const normalizedBaseUrl = config.providers.codex.base_url.replace(/\/$/, "");
secretsContext?: PlaceholderContext,
maskingConfig = getConfig().masking,
) {
- c.header("Content-Type", "text/event-stream");
- c.header("Cache-Control", "no-cache");
- c.header("Connection", "keep-alive");
+ setStreamingHeaders(c);
if (piiContext || secretsContext) {
return c.body(createCodexUnmaskingStream(stream, piiContext, maskingConfig, secretsContext));
-/**
- * OpenAI-compatible chat completion route
- *
- * Flow:
- * 1. Validate request
- * 2. Process secrets (detect, maybe block or mask)
- * 3. Detect PII
- * 4. Based on mode:
- * - mask: mask PII, send to OpenAI, unmask response
- * - route: send to local (if PII) or OpenAI (if clean)
- * 5. Return response
- */
-
import { zValidator } from "@hono/zod-validator";
import type { Context } from "hono";
import { Hono } from "hono";
handleProviderError,
setBlockedHeaders,
setResponseHeaders,
+ setStreamingHeaders,
toPIIHeaderData,
toPIILogData,
toSecretsHeaderData,
export const openaiRoutes = new Hono();
-/**
- * POST /v1/chat/completions
- */
openaiRoutes.post(
"/v1/chat/completions",
zValidator("json", OpenAIRequestSchema, (result, c) => {
},
);
-/**
- * Wildcard proxy for /models, /embeddings, /audio/*, /images/*, etc.
- */
openaiRoutes.all("/*", (c) => {
const config = getConfig();
const { baseUrl } = getOpenAIInfo(config.providers.openai);
);
if (result.isStreaming) {
- c.header("Content-Type", "text/event-stream");
- c.header("Cache-Control", "no-cache");
- c.header("Connection", "keep-alive");
+ setStreamingHeaders(c);
return c.body(result.response as ReadableStream);
}
secretsContext?: PlaceholderContext,
maskingConfig?: MaskingConfig,
) {
- c.header("Content-Type", "text/event-stream");
- c.header("Cache-Control", "no-cache");
- c.header("Connection", "keep-alive");
+ setStreamingHeaders(c);
if (piiContext || secretsContext) {
const stream = createUnmaskingStream(
-/**
- * Shared route utilities
- *
- * Common utilities for route handlers including error formatting,
- * response headers, and logging helpers.
- */
-
import type { Context } from "hono";
import { getConfig } from "../config";
import { ProviderError } from "../providers/errors";
// Error Response Types & Formatting
// ============================================================================
-/**
- * Error response format for OpenAI
- */
export interface OpenAIErrorResponse {
error: {
message: string;
};
}
-/**
- * Error response format for Anthropic
- */
export interface AnthropicErrorResponse {
type: "error";
error: {
};
}
-/**
- * Format adapters for different API schemas
- */
export const errorFormats = {
openai: {
error(
masked: boolean;
}
-/**
- * Set common PasteGuard response headers
- */
export function setResponseHeaders(
c: Context,
mode: string,
}
}
-/**
- * Set headers for blocked request (secrets detected)
- */
export function setBlockedHeaders(c: Context, secretTypes: string[]): void {
c.header("X-PasteGuard-Secrets-Detected", "true");
c.header("X-PasteGuard-Secrets-Types", secretTypes.join(","));
}
+export function setStreamingHeaders(c: Context): void {
+ c.header("Content-Type", "text/event-stream");
+ c.header("Cache-Control", "no-cache");
+ c.header("Connection", "keep-alive");
+}
+
// ============================================================================
// Logging Helpers
// ============================================================================
-/**
- * PII detection result for logging
- */
export interface PIILogData {
hasPII: boolean;
entityTypes: string[];
scanTimeMs: number;
}
-/**
- * Secrets detection result for logging
- */
export interface SecretsLogData {
detected?: boolean;
types?: string[];
masked: boolean;
}
-/**
- * Convert PIIDetectResult to PIILogData
- */
export function toPIILogData(piiResult: PIIDetectResult): PIILogData {
return {
hasPII: piiResult.hasPII,
};
}
-/**
- * Convert PIIDetectResult to PIIHeaderData
- */
export function toPIIHeaderData(piiResult: PIIDetectResult): PIIHeaderData {
return {
hasPII: piiResult.hasPII,
};
}
-/**
- * Convert SecretsProcessResult to SecretsLogData
- */
export function toSecretsLogData<T>(
secretsResult: SecretsProcessResult<T>,
): SecretsLogData | undefined {
};
}
-/**
- * Convert SecretsProcessResult to SecretsHeaderData
- */
export function toSecretsHeaderData<T>(
secretsResult: SecretsProcessResult<T>,
): SecretsHeaderData | undefined {
errorMessage?: string;
}
-/**
- * Create log data object for request logging
- */
export function createLogData(options: CreateLogDataOptions): RequestLogData {
const config = getConfig();
const { provider, model, startTime, pii, secrets, maskedContent, statusCode, errorMessage } =
userAgent: string | null;
}
-/**
- * Handle provider errors with logging
- *
- * Returns the appropriate response for the error type.
- * For ProviderError, returns the original error body.
- * For other errors, returns a formatted error response.
- */
export function handleProviderError(
c: Context,
error: unknown,
export type {
MessageSecretsResult,
- SecretEntityType,
SecretLocation,
- SecretsDetectionResult,
- SecretsMatch,
} from "./patterns/types";
-/**
- * Detects secret material (e.g. private keys, API keys, tokens) in text
- *
- * Uses the pattern registry to scan for various secret types:
- * - Private keys: OpenSSH, PEM (RSA, generic, encrypted)
- * - API keys: OpenAI, AWS, GitHub
- * - Tokens: JWT, Bearer
- * - Environment variables: Passwords, secrets, connection strings
- *
- * Respects max_scan_chars limit for performance.
- */
+// Scans private keys, API keys, tokens, env secrets, and connection strings.
+// Respects max_scan_chars to cap regex work on large prompts.
export function detectSecrets(
text: string,
config: SecretsDetectionConfig,
};
}
-/**
- * Detects secrets in a request using an extractor
- */
export function detectSecretsInRequest<TRequest, TResponse>(
request: TRequest,
config: SecretsDetectionConfig,
return detectSecretsInSpans(spans, config);
}
-/**
- * Detects secrets in text spans (low-level)
- */
-export function detectSecretsInSpans(
+function detectSecretsInSpans(
spans: TextSpan[],
config: SecretsDetectionConfig,
): MessageSecretsResult {
tokensDetector,
envVarsDetector,
];
-
-export type { PatternDetector, SecretEntityType, SecretsDetectionResult } from "./types";
-export { detectPattern } from "./utils";
error_message: string | null;
}
-/**
- * Statistics summary
- */
export interface Stats {
total_requests: number;
pii_requests: number;
return "api";
}
-/**
- * SQLite-based logger for request tracking
- */
export class Logger {
private db: Database;
private retentionDays: number;
);
}
- /**
- * Gets recent logs
- */
getLogs(limit: number = 100, offset: number = 0): RequestLog[] {
const stmt = this.db.prepare(`
SELECT
}));
}
- /**
- * Gets statistics
- */
getStats(): Stats {
// Total requests
const totalResult = this.db.prepare(`SELECT COUNT(*) as count FROM request_logs`).get() as {
};
}
- /**
- * Gets entity breakdown
- */
getEntityStats(): Array<{ entity: string; count: number }> {
const logs = this.db
.prepare(`
.sort((a, b) => b.count - a.count);
}
- /**
- * Cleans up old logs based on retention policy
- */
cleanup(): number {
if (this.retentionDays <= 0) {
return 0; // Keep forever
return result.changes;
}
- /**
- * Closes database connection
- */
close(): void {
this.db.close();
}