/**
* 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 };
}