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
24 changes: 23 additions & 1 deletion src/__tests__/hooks/useOptimizedClipboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('useOptimizedClipboard', () => {
expect(result.current.isPasting).toBe(false);
expect(result.current.copySuccess).toBe(false);
expect(result.current.error).toBeNull();
expect(result.current.clipboardContent).toBe('');
expect(result.current.clipboardContent).toBeNull();
expect(result.current.metrics).toBeNull();
});

Expand Down Expand Up @@ -110,4 +110,26 @@ describe('useOptimizedClipboard', () => {
expect(result.current.clipboardContent).toBe('test pasted text');
expect(result.current.error).toBeNull();
});

it('clears clipboard content from state 30 seconds after paste', async () => {
const { result } = renderHook(() => useOptimizedClipboard());

let promise: Promise<string>;
act(() => {
promise = result.current.pasteFromClipboard();
});

await act(async () => {
jest.runAllTimers();
await promise;
});

expect(result.current.clipboardContent).toBe('test pasted text');

act(() => {
jest.advanceTimersByTime(30_000);
});

expect(result.current.clipboardContent).toBeNull();
});
});
34 changes: 31 additions & 3 deletions src/hooks/useOptimizedClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,33 @@ export interface UseOptimizedClipboardResult {
isPasting: boolean;
copySuccess: boolean;
error: Error | null;
clipboardContent: string;
clipboardContent: string | null;
metrics: ClipboardOperationMetrics | null;
copyToClipboard: (text: string) => Promise<boolean>;
pasteFromClipboard: () => Promise<string>;
clearError: () => void;
}

/**
* Optimized clipboard hook with a 30-second TTL on pasted content.
*
* Clipboard access is only performed in response to explicit user gestures —
* never on mount or auto-focus. Pasted content is automatically cleared from
* state after 30 s (TTL) to limit the exposure window for sensitive data such
* as passwords, auth tokens, and payment card numbers held in JS heap memory.
* No clipboard content is forwarded to analytics or error-reporting services.
*/
export function useOptimizedClipboard(): UseOptimizedClipboardResult {
const [isCopying, setIsCopying] = useState(false);
const [isPasting, setIsPasting] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [clipboardContent, setClipboardContent] = useState('');
const [clipboardContent, setClipboardContent] = useState<string | null>(null);
const [metrics, setMetrics] = useState<ClipboardOperationMetrics | null>(null);

const isMounted = useRef(true);
const successTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const clipboardTtlRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
isMounted.current = true;
Expand All @@ -36,6 +46,24 @@ export function useOptimizedClipboard(): UseOptimizedClipboardResult {
};
}, []);

// Clear clipboard content from state 30 s after it is set to limit the
// in-memory exposure window for sensitive data.
useEffect(() => {
if (clipboardContent !== null) {
clipboardTtlRef.current = setTimeout(() => {
if (isMounted.current) {
setClipboardContent(null);
}
}, 30_000);
}
return () => {
if (clipboardTtlRef.current) {
clearTimeout(clipboardTtlRef.current);
clipboardTtlRef.current = null;
}
};
}, [clipboardContent]);

const clearError = useCallback(() => {
setError(null);
}, []);
Expand Down