improvement: tab switch and overlay detection

Improved the detection so that when candiate is selection screen share
OS Native modal, or allowing permissions should not detect as a
Suspecious activity
This commit is contained in:
kushal.saha 2026-08-06 10:43:06 +00:00
parent d913dded0b
commit 5e87dcfea2

View File

@ -565,6 +565,24 @@ function submitSolution() {
let isTabSwitchScreenshotEnabled = true; let isTabSwitchScreenshotEnabled = true;
let lastTabSwitchTime = 0; let lastTabSwitchTime = 0;
let pendingNativePrompts = 0; // counter for native OS prompts
let suppressFocusViolationsUntil = 0; // trailing buffer after prompt resolves
function beginNativePrompt() {
pendingNativePrompts++;
}
function endNativePrompt() {
pendingNativePrompts = Math.max(0, pendingNativePrompts - 1);
}
function armTrailingBuffer(ms) {
suppressFocusViolationsUntil = Math.max(suppressFocusViolationsUntil, Date.now() + ms);
}
function isFocusSuppressed() {
return pendingNativePrompts > 0 || Date.now() < suppressFocusViolationsUntil;
}
let latestCallStatus = '{{ $interview->call_status ?? "idle" }}'; let latestCallStatus = '{{ $interview->call_status ?? "idle" }}';
let screenShareVideoElem = null; let screenShareVideoElem = null;
@ -728,6 +746,7 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
// 1. Tab Switch Detection (Captures Candidate's SYSTEM DESKTOP MONITOR screenshot) // 1. Tab Switch Detection (Captures Candidate's SYSTEM DESKTOP MONITOR screenshot)
document.addEventListener('visibilitychange', function () { document.addEventListener('visibilitychange', function () {
if (isFocusSuppressed()) return;
if (document.hidden) { if (document.hidden) {
const now = Date.now(); const now = Date.now();
if (now - lastTabSwitchTime > 1000) { if (now - lastTabSwitchTime > 1000) {
@ -740,6 +759,7 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
// 2. Window Focus Loss / External AI Overlay Application Switch Detection // 2. Window Focus Loss / External AI Overlay Application Switch Detection
window.addEventListener('blur', function () { window.addEventListener('blur', function () {
if (isFocusSuppressed()) return;
const now = Date.now(); const now = Date.now();
if (now - lastTabSwitchTime > 1000) { if (now - lastTabSwitchTime > 1000) {
lastTabSwitchTime = now; lastTabSwitchTime = now;
@ -750,6 +770,7 @@ function uploadTabScreenshotBlob(imageData, reason = 'tab_switch') {
// Mouse Viewport Departure (Candidate moving cursor outside window to interact with Parakeet AI or secondary app overlay) // Mouse Viewport Departure (Candidate moving cursor outside window to interact with Parakeet AI or secondary app overlay)
document.addEventListener('mouseleave', function () { document.addEventListener('mouseleave', function () {
if (isFocusSuppressed()) return;
const now = Date.now(); const now = Date.now();
if (now - lastTabSwitchTime > 2500) { if (now - lastTabSwitchTime > 2500) {
lastTabSwitchTime = now; lastTabSwitchTime = now;
@ -1550,6 +1571,8 @@ function startCandidateCameraStream() {
autoGainControl: true autoGainControl: true
}; };
beginNativePrompt();
return navigator.mediaDevices.getUserMedia({ return navigator.mediaDevices.getUserMedia({
video: { video: {
width: { ideal: 1280 }, width: { ideal: 1280 },
@ -1563,6 +1586,7 @@ function startCandidateCameraStream() {
.catch(() => navigator.mediaDevices.getUserMedia({ video: false, audio: true })) .catch(() => navigator.mediaDevices.getUserMedia({ video: false, audio: true }))
.catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false })) .catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false }))
.then(stream => { .then(stream => {
endNativePrompt();
localMediaStream = stream; localMediaStream = stream;
const vid = document.getElementById('webcam'); const vid = document.getElementById('webcam');
if (vid) { if (vid) {
@ -1589,6 +1613,7 @@ function startCandidateCameraStream() {
return stream; return stream;
}) })
.catch(err => { .catch(err => {
endNativePrompt();
console.log('Candidate media access error:', err); console.log('Candidate media access error:', err);
if (avatarPlaceholder) avatarPlaceholder.style.display = 'flex'; if (avatarPlaceholder) avatarPlaceholder.style.display = 'flex';
initCandidatePeer(); initCandidatePeer();
@ -1729,6 +1754,17 @@ function toggleCam() {
} }
} }
function requestFullscreenAsPromise(elem) {
if (elem.requestFullscreen) {
return elem.requestFullscreen();
}
if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen();
return Promise.resolve();
}
return Promise.reject(new Error('Fullscreen not supported'));
}
function requestProctoringFullscreen() { function requestProctoringFullscreen() {
try { try {
const elem = document.documentElement; const elem = document.documentElement;
@ -1741,6 +1777,7 @@ function requestProctoringFullscreen() {
} }
document.addEventListener('fullscreenchange', function() { document.addEventListener('fullscreenchange', function() {
if (isFocusSuppressed()) return;
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
logViolation('tab_switch', 'Candidate exited full-screen proctoring mode (possible overlay AI window interaction)'); logViolation('tab_switch', 'Candidate exited full-screen proctoring mode (possible overlay AI window interaction)');
captureAndUploadTabScreenshot(); captureAndUploadTabScreenshot();
@ -1826,6 +1863,8 @@ function startScreenShare() {
return; return;
} }
beginNativePrompt();
navigator.mediaDevices.getDisplayMedia({ navigator.mediaDevices.getDisplayMedia({
video: { video: {
displaySurface: 'monitor' displaySurface: 'monitor'
@ -1833,6 +1872,7 @@ function startScreenShare() {
audio: false audio: false
}) })
.then(stream => { .then(stream => {
endNativePrompt();
const track = stream.getVideoTracks()[0]; const track = stream.getVideoTracks()[0];
const settings = track.getSettings ? track.getSettings() : {}; const settings = track.getSettings ? track.getSettings() : {};
@ -1870,7 +1910,13 @@ function startScreenShare() {
btn.style.background = '#10b981'; btn.style.background = '#10b981';
} }
requestProctoringFullscreen(); beginNativePrompt();
requestFullscreenAsPromise(document.documentElement)
.catch(() => {})
.finally(() => {
endNativePrompt();
armTrailingBuffer(1000);
});
// Connect screen stream to interviewer screen peer // Connect screen stream to interviewer screen peer
const screenPeer = new Peer('cand_screen_' + interviewId + '_' + Date.now()); const screenPeer = new Peer('cand_screen_' + interviewId + '_' + Date.now());
@ -1891,6 +1937,7 @@ function startScreenShare() {
initLiveOverlayDetector(); initLiveOverlayDetector();
}) })
.catch(err => { .catch(err => {
endNativePrompt();
console.log('Screen sharing cancelled/denied:', err); console.log('Screen sharing cancelled/denied:', err);
screenShareStream = null; screenShareStream = null;
logViolation('tab_switch', 'Candidate denied or cancelled screen sharing permission prompt'); logViolation('tab_switch', 'Candidate denied or cancelled screen sharing permission prompt');