Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,10 @@ ALLOW_API_KEY_REVEAL=false
# PII detection in incoming requests (emails, phone numbers, SSNs, etc.).
# Used by: src/middleware/promptInjectionGuard.ts — extends injection guard.
# PII_REDACTION_ENABLED=false
# Reversible PII Token Vault (zero PII leakage upstream)
PII_REVERSIBLE_VAULT_ENABLED=false
PII_VAULT_TTL_MS=120000
PII_VAULT_MAX_ENTRIES=5000

# Redacts well-known API-key / secret-token patterns (OpenAI, Anthropic, GitHub,
# Slack, etc.) from request/response payloads before they reach providers/clients.
Expand Down
1 change: 1 addition & 0 deletions changelog.d/features/18017-reversible-pii-token-vault.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **feat(pii):** reversible token vault with request-scoped nonces for streaming and non-streaming responses — thanks @Ardem2025
3 changes: 3 additions & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
| `INPUT_SANITIZER_BLOCK_THRESHOLD` | `high` | `src/shared/utils/injectionSeverity.ts` | Minimum severity that `MODE=block` rejects: `high` (default), `medium`, or `low`. Medium patterns are observe-only unless lowered. |
| `INJECTION_GUARD_BLOCK_THRESHOLD` | _(unset)_ | `src/shared/utils/injectionSeverity.ts` | Legacy alias for `INPUT_SANITIZER_BLOCK_THRESHOLD` — same behavior. |
| `PII_REDACTION_ENABLED` | `false` | `src/lib/guardrails/piiMasker.ts` | When `true`, redact PII in incoming requests (independent of injection mode). |
| `PII_REVERSIBLE_VAULT_ENABLED` | `false` | `src/lib/pii/tokenVault.ts` | When `true`, replace request PII with nonced surrogate tokens and detokenize upstream response transparently (zero PII leakage upstream). |
| `PII_VAULT_TTL_MS` | `120000` | `src/lib/pii/tokenVault.ts` | Sliding time-to-live (ms) for tokens in the reversible vault. Default: 120000 (2 minutes). |
| `PII_VAULT_MAX_ENTRIES` | `5000` | `src/lib/pii/tokenVault.ts` | Maximum surrogate tokens held in memory per vault before LRU eviction. Default: 5000. |
| `CREDENTIAL_REDACTION_ENABLED` | `false` | `src/lib/guardrails/credentialMasker.ts` | Redact well-known API-key / secret-token patterns from request/response payloads. Opt-in; mirrors `PII_REDACTION_ENABLED`. |

### Response-Side: PII Sanitizer
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/FEATURE_FLAGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ A boolean flag is considered **enabled** when its effective value is `"true"`,

## Flag Catalog

112 flags across 6 categories. **Default** is the definition default — the value
113 flags across 6 categories. **Default** is the definition default — the value
used when neither a DB override nor an environment variable is present.

### Security (10)
### Security (11)

| Key | Type | Default | Description |
| --------------------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -59,6 +59,7 @@ used when neither a DB override nor an environment variable is present.
| `PII_REDACTION_ENABLED` | boolean | `false` | Redact PII from requests (independent of `INPUT_SANITIZER_MODE`). |
| `PII_RESPONSE_SANITIZATION` | boolean | `false` | Sanitize PII from provider responses. |
| `PII_RESPONSE_SANITIZATION_MODE` | enum | `redact` | Mode for PII response sanitization. Values: `redact`, `warn`, `block`, `off`. |
| `PII_REVERSIBLE_VAULT_ENABLED` | boolean | `false` | Opt-in reversible PII pseudonymization vault: replaces detected PII with surrogate tokens upstream and restores original values on outbound responses. |
| `OUTBOUND_SSRF_GUARD_ENABLED` | boolean | `true` | Legacy alias: a value saved on this flag's dashboard toggle is read before the environment; `false`, `0`, `no`, or `off` in either turns the outbound URL guard's host checks off like `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS`. |
| `ALLOW_API_KEY_REVEAL` | boolean | `false` | Allow authenticated dashboard users to reveal stored API keys instead of only seeing masked values. |
| `AUTH_LOG_INCLUDE_ACCOUNT_ID` | boolean | `false` | Include account prefix in AUTH log lines (e.g. "Using <provider> account: abc12345..."). Disabled by default so account identifiers are redacted from shared/multi-tenant process logs. Independent from Debug Mode; flipping Debug Mode does not reveal this. |
Expand Down
11 changes: 9 additions & 2 deletions open-sse/handlers/chatCore/nonStreamingJsonResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
* Build the final non-streaming JSON response body once and publish an accurate
* Content-Length for downstream HTTP clients and buffering proxies.
*/
import {
detokenizeResponsePayload,
isReversibleVaultEnabled,
} from "../../utils/tokenVault.ts";
export function buildNonStreamingJsonResponse(
body: unknown,
headers: Record<string, string>
headers: Record<string, string>,
): Response {
const payload = JSON.stringify(body);
const finalBody = isReversibleVaultEnabled()
? detokenizeResponsePayload(body)
: body;
const payload = JSON.stringify(finalBody);
return new Response(payload, {
headers: {
...headers,
Expand Down
39 changes: 32 additions & 7 deletions open-sse/handlers/chatCore/streamingPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
wantsProgress as defaultWantsProgress,
} from "../../utils/progressTracker.ts";
import { createPiiSseTransform as defaultPiiSse } from "@/lib/streamingPiiTransform";
import { isPiiTransformEnabled, resolvePiiGate } from "@/lib/streamingPiiTransform";
import {
isPiiTransformEnabled,
resolvePiiGate,
} from "@/lib/streamingPiiTransform";

// Single seam to the PII stream module: streamingTail resolves the per-request gate through this
// file instead of importing src/ itself (boundary ratchet, quality-baseline.json).
Expand All @@ -32,6 +35,10 @@ import {
import { isFeatureFlagEnabled as defaultFeatureFlag } from "@/shared/utils/featureFlags";
import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
import { SSE_HEARTBEAT_INTERVAL_MS } from "../../config/constants.ts";
import {
createDetokenizingStream,
isReversibleVaultEnabled,
} from "../../utils/tokenVault.ts";
/**
* Pipeline assembly instrumentation — performance.mark() along the SSE hot path.
* Marks are visible to Node.js perf_hooks consumers and DevTools' Performance
Expand All @@ -56,6 +63,8 @@ export interface StreamingPipelineDeps {
createSseHeartbeatTransform: typeof defaultHeartbeat;
shapeForClientFormat: typeof defaultShape;
createModelEchoTransform: typeof defaultModelEcho;
createDetokenizingStream?: typeof createDetokenizingStream;
isReversibleVaultEnabled?: typeof isReversibleVaultEnabled;
}

const DEFAULT_DEPS: StreamingPipelineDeps = {
Expand All @@ -68,6 +77,8 @@ const DEFAULT_DEPS: StreamingPipelineDeps = {
createSseHeartbeatTransform: defaultHeartbeat,
shapeForClientFormat: defaultShape,
createModelEchoTransform: defaultModelEcho,
createDetokenizingStream: createDetokenizingStream,
isReversibleVaultEnabled: isReversibleVaultEnabled,
};

export function assembleStreamingPipeline(
Expand Down Expand Up @@ -96,7 +107,7 @@ export function assembleStreamingPipeline(
* Omitted falls back to STREAM_ACTIVE_TIMEOUT_MS; 0 leaves the watchdog off. */
activeTimeoutMs?: number;
},
deps: StreamingPipelineDeps = DEFAULT_DEPS
deps: StreamingPipelineDeps = DEFAULT_DEPS,
) {
performance.clearMarks(PIPELINE_START);
performance.clearMarks(PIPELINE_END);
Expand All @@ -113,11 +124,17 @@ export function assembleStreamingPipeline(
{
contentStallTimeoutMs: args.contentStallTimeoutMs,
activeTimeoutMs: args.activeTimeoutMs,
}
},
);
if (isPiiTransformEnabled({ piiDisabledGuardrails: args.piiDisabledGuardrails ?? [] })) {
if (
isPiiTransformEnabled({
piiDisabledGuardrails: args.piiDisabledGuardrails ?? [],
})
) {
if (typeof args.createPiiTransform === "function") {
piiStream = piiStream.pipeThrough((args.createPiiTransform as () => TransformStream)());
piiStream = piiStream.pipeThrough(
(args.createPiiTransform as () => TransformStream)(),
);
} else if (deps.isFeatureFlagEnabled("PII_RESPONSE_SANITIZATION")) {
piiStream = piiStream.pipeThrough(deps.createPiiSseTransform());
}
Expand Down Expand Up @@ -150,11 +167,19 @@ export function assembleStreamingPipeline(
signal: args.streamController.signal,
intervalMs: SSE_HEARTBEAT_INTERVAL_MS,
shape: deps.shapeForClientFormat(args.clientResponseFormat),
})
}),
);
// #1311: echo the requested alias/combo name in each streamed SSE chunk's model field.
if (args.echoModel) {
finalStream = finalStream.pipeThrough(deps.createModelEchoTransform(args.echoModel));
finalStream = finalStream.pipeThrough(
deps.createModelEchoTransform(args.echoModel),
);
}
const checkVault = deps.isReversibleVaultEnabled ?? isReversibleVaultEnabled;
if (checkVault()) {
const makeDetokenizer =
deps.createDetokenizingStream ?? createDetokenizingStream;
finalStream = finalStream.pipeThrough(makeDetokenizer());
}
performance.mark(PIPELINE_END);
performance.measure(PIPELINE_MEASURE, PIPELINE_START, PIPELINE_END);
Expand Down
Loading
Loading