/** * Screenshot masker — blur or black-box PII regions in captured screenshots. * * Strategy: inject a CSS overlay into the page before screenshot capture that * covers detected PII-likely elements (input[type=email], input[type=password], * elements with PII-containing text) with a blur or solid black mask. * * This is a CSS-based approach (no image processing library needed): * 1. Identify likely-PII DOM elements via selectors * 2. Add an overlaid
with blur/black filter at each element's bounding box * 3. Take screenshot * 4. Remove overlays * * Falls back gracefully if elements are not found or overlay injection fails. */ import type { Page } from 'playwright'; export interface MaskingOpts { /** Additional CSS selectors to mask (in addition to defaults) */ extraSelectors?: string[]; /** Masking style: 'blur' or 'blackout' */ style?: 'blur' | 'blackout'; /** Blur strength in px (default 8) */ blurPx?: number; } // Default PII-sensitive selectors const DEFAULT_PII_SELECTORS = [ 'input[type="email"]', 'input[type="password"]', 'input[type="tel"]', 'input[autocomplete="cc-number"]', 'input[autocomplete="cc-csc"]', 'input[autocomplete="ssn"]', '[data-pii="true"]', '[data-sensitive="true"]', ]; export async function maskScreenshot( page: Page, opts: MaskingOpts = {}, ): Promise<{ maskedCount: number; screenshot: Buffer }> { const { extraSelectors = [], style = 'blur', blurPx = 8 } = opts; const selectors = [...DEFAULT_PII_SELECTORS, ...extraSelectors]; let maskedCount = 0; const overlayIds: string[] = []; // Inject CSS overlays try { const result = await page.evaluate(({ selectors, style, blurPx }) => { const ids: string[] = []; for (const sel of selectors) { try { const elements = Array.from(document.querySelectorAll(sel)); for (const el of elements) { const rect = el.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) continue; const overlay = document.createElement('div'); const id = `__zeta_mask_${Math.random().toString(36).slice(2)}`; overlay.id = id; overlay.style.cssText = [ 'position: fixed', `top: ${rect.top + window.scrollY}px`, `left: ${rect.left + window.scrollX}px`, `width: ${rect.width}px`, `height: ${rect.height}px`, 'z-index: 2147483647', style === 'blur' ? `backdrop-filter: blur(${blurPx}px); -webkit-backdrop-filter: blur(${blurPx}px); background: rgba(0,0,0,0.1);` : 'background: #000000;', 'pointer-events: none', ].join('; '); document.body.appendChild(overlay); ids.push(id); } } catch { /* selector error */ } } return ids; }, { selectors, style, blurPx }); overlayIds.push(...result); maskedCount = result.length; } catch { /* non-fatal */ } // Take screenshot with overlays active const screenshot = await page.screenshot({ type: 'png', fullPage: false }); // Remove overlays if (overlayIds.length > 0) { await page.evaluate((ids) => { for (const id of ids) document.getElementById(id)?.remove(); }, overlayIds).catch(() => {}); } return { maskedCount, screenshot }; }