From 7b38418900f0ec74328ad1fe4fce0807bac94ec7 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov <7889985+andreybavt@users.noreply.github.com> Date: Mon, 11 May 2026 20:09:46 +0200 Subject: [PATCH] feat: add historic sql redaction helper --- .../adapters/historic-sql/redaction.test.ts | 36 ++++++++++++++++++ .../ingest/adapters/historic-sql/redaction.ts | 37 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 packages/context/src/ingest/adapters/historic-sql/redaction.test.ts create mode 100644 packages/context/src/ingest/adapters/historic-sql/redaction.ts diff --git a/packages/context/src/ingest/adapters/historic-sql/redaction.test.ts b/packages/context/src/ingest/adapters/historic-sql/redaction.test.ts new file mode 100644 index 00000000..c8f1d78b --- /dev/null +++ b/packages/context/src/ingest/adapters/historic-sql/redaction.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { compileHistoricSqlRedactionPatterns, redactHistoricSqlText } from './redaction.js'; + +describe('historic-SQL redaction', () => { + it('redacts regex matches and supports the (?i) case-insensitive prefix', () => { + const redactors = compileHistoricSqlRedactionPatterns([ + 'sk_live_[A-Za-z0-9]+', + '(?i)secret_token_[a-z0-9]+', + ]); + + const sql = + "select * from public.api_events where api_key = 'sk_live_abc123' and note = 'Secret_Token_9f'"; + + expect(redactHistoricSqlText(sql, redactors)).toBe( + "select * from public.api_events where api_key = '[REDACTED]' and note = '[REDACTED]'", + ); + }); + + it('returns the original SQL text when no redaction patterns are configured', () => { + const sql = "select * from public.orders where status = 'paid'"; + + expect(redactHistoricSqlText(sql, compileHistoricSqlRedactionPatterns([]))).toBe(sql); + }); + + it('throws a config-focused error for invalid redaction regex patterns', () => { + expect(() => compileHistoricSqlRedactionPatterns(['[broken'])).toThrow( + 'Invalid historicSql.redactionPatterns entry "[broken"', + ); + }); + + it('throws a config-focused error for empty redaction regex patterns', () => { + expect(() => compileHistoricSqlRedactionPatterns([' '])).toThrow( + 'Invalid historicSql.redactionPatterns entry " "', + ); + }); +}); diff --git a/packages/context/src/ingest/adapters/historic-sql/redaction.ts b/packages/context/src/ingest/adapters/historic-sql/redaction.ts new file mode 100644 index 00000000..a047b70f --- /dev/null +++ b/packages/context/src/ingest/adapters/historic-sql/redaction.ts @@ -0,0 +1,37 @@ +export interface HistoricSqlRedactionPattern { + pattern: string; + expression: RegExp; +} + +const CASE_INSENSITIVE_PREFIX = '(?i)'; +const REDACTION_TOKEN = '[REDACTED]'; + +export function compileHistoricSqlRedactionPatterns(patterns: readonly string[]): HistoricSqlRedactionPattern[] { + return patterns.map((pattern) => { + const trimmed = pattern.trim(); + const caseInsensitive = trimmed.startsWith(CASE_INSENSITIVE_PREFIX); + const source = caseInsensitive ? trimmed.slice(CASE_INSENSITIVE_PREFIX.length) : trimmed; + if (source.length === 0) { + throw new Error(`Invalid historicSql.redactionPatterns entry "${pattern}": pattern must not be empty`); + } + + try { + return { + pattern, + expression: new RegExp(source, caseInsensitive ? 'gi' : 'g'), + }; + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + throw new Error(`Invalid historicSql.redactionPatterns entry "${pattern}": ${reason}`); + } + }); +} + +export function redactHistoricSqlText(text: string, redactors: readonly HistoricSqlRedactionPattern[]): string { + let next = text; + for (const redactor of redactors) { + redactor.expression.lastIndex = 0; + next = next.replace(redactor.expression, REDACTION_TOKEN); + } + return next; +}