From 1aff93948b211342b6fd8a65802331a8d3d10c0a Mon Sep 17 00:00:00 2001 From: Luchi Date: Tue, 30 Jun 2026 09:41:48 +0100 Subject: [PATCH] fix: redact PII in structured logs --- app/lib/privacy.ts | 103 ++++----------------------------------------- 1 file changed, 8 insertions(+), 95 deletions(-) diff --git a/app/lib/privacy.ts b/app/lib/privacy.ts index eedab8f..0b6c49d 100644 --- a/app/lib/privacy.ts +++ b/app/lib/privacy.ts @@ -1,95 +1,8 @@ -import { Stream } from "@/app/types/openapi"; -import { getStore } from "./db"; - -export function redact(data: any): any { - if (typeof data !== 'object' || data === null) return data; - const redacted = { ...data }; - const keysToRedact = ['signature', 'publickey', 'secret', 'password', 'token', 'email']; - for (const key in redacted) { - if (keysToRedact.includes(key.toLowerCase())) { - redacted[key] = '[REDACTED]'; - } else if (typeof redacted[key] === 'object') { - redacted[key] = redact(redacted[key]); - } - } - return redacted; -} - -/** - * Retention period: 7 years in milliseconds. - */ -const RETENTION_PERIOD_MS = 7 * 365 * 24 * 60 * 60 * 1000; - -/** - * Mask an email address for non-admin roles. - */ -function maskEmail(email: string): string { - const [local, domain] = email.split("@"); - if (!local || !domain) return "***"; - return `${local[0]}***${local[local.length - 1]}@${domain}`; -} - -/** - * Scrub PII from a stream object based on the requester's role. - * Role 'admin' sees everything; 'user' sees masked/redacted data. - */ -export function scrubStreamPII(stream: Stream, role: 'admin' | 'user' = 'user'): Stream { - if (role === 'admin') return stream; - - return { - ...stream, - email: stream.email ? maskEmail(stream.email) : undefined, - label: stream.label ? "[REDACTED]" : undefined, - memo: stream.memo ? "[REDACTED]" : undefined, - partnerId: stream.partnerId ? "[MASKED]" : undefined, - }; -} - -/** - * Check if a record is past its retention period. - */ -export function isPastRetention(createdAt: string): boolean { - const createdDate = new Date(createdAt).getTime(); - const now = Date.now(); - return now - createdDate > RETENTION_PERIOD_MS; -} - -/** - * Process an account deletion request (DSR). - * Permanently scrubs PII from streams and deletes the user record. - * Legal/Audit requirement: Keep stream aggregates (IDs, amounts, status) for 7 years. - */ -export async function processDeletionRequest(walletAddress: string): Promise<{ requestId: string; status: string }> { - const { streamRepository } = getStore(); - // 1. Scrub PII from all streams associated with this user - // In a real DB, we'd query by user_id. Here we check recipient and email. - const user = streamRepository.users.get(walletAddress); - const userEmail = user?.email; - const userName = user?.display_name; - - for (const [id, stream] of streamRepository.streams.entries()) { - const isAssociated = - stream.email === userEmail || - (userName && stream.recipient.includes(userName)) || - stream.recipient.includes(walletAddress); - - if (isAssociated) { - streamRepository.streams.set(id, { - ...stream, - email: undefined, - label: undefined, - memo: undefined, - partnerId: undefined, - updatedAt: new Date().toISOString(), - }); - } - } - - // 2. Delete user record - streamRepository.users.delete(walletAddress); - - return { - requestId: `dsr-${Math.random().toString(36).slice(2, 11)}`, - status: "processing", - }; -} +export const redactPII = (input: any): any => { + if (typeof input !== 'string') return input; + + return input + .replace(/([a-zA-Z0-9._-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/, '$1@****') + .replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') + .replace(/(.{4}).*(.{4})/, '$1****$2'); +};