// ==UserScript==
// @name Anti-Visibility Cloak
// @namespace https://spin.rip/
// @version 1.1
// @description Force pages to always think the tab is visible/focused; optionally spoofs mouse as always "in page" and blocks exit/enter intent; shows a tiny popup when a visibility or mouse check is detected
// @author Spinfal
// @match *://*/*
// @run-at document-start
// @grant none
// @all-frames true
// @icon https://cdn.spin.rip/r/antivisibility.png
// @license gpl-3.0-or-later
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
notify: {
properties: true,
hasFocusCall: false,
addListener: {
visibilitychange: true,
webkitvisibilitychange: true,
mozvisibilitychange: true,
msvisibilitychange: true,
pagehide: true,
freeze: true,
resume: true,
pageshow: true,
blur: false,
focus: false,
mouseout: true,
mouseleave: true,
pointerout: true,
pointerleave: true,
mouseover: true,
mouseenter: true,
pointerover: true,
mousemove: false
},
invoke: {
visibilitychange: true,
webkitvisibilitychange: true,
mozvisibilitychange: true,
msvisibilitychange: true,
pagehide: true,
freeze: true,
resume: true,
pageshow: true,
blur: false,
focus: false,
mouseout: true,
mouseleave: true,
pointerout: true,
pointerleave: true,
mouseover: true,
mouseenter: true,
pointerover: true,
mousemove: false
}
},
suppressFocusOnEditable: true,
mouse: {
spoofExit: true, // block/neutralize exit-intent listeners on window/document
ignoreGlobalLeave: true, // ignore global leave events where relatedTarget is null
blockGlobalEnter: true, // swallow global enter events where relatedTarget is null
initialEnterOnce: true, // allow exactly one initial global enter after load
fakeMovement: true, // periodically dispatch synthetic mousemove to suggest presence
fakeIntervalMs: 12000, // how often to synthesize a mousemove
moveJitterPx: 2 // tiny jitter so it doesn’t look robotic
}
};
// popup system
const makeNotifier = () => {
let shadow, wrap, root, last, lastAt = 0, hideTimer;
root = document.createElement('div');
root.style.all = 'initial';
root.style.position = 'fixed';
root.style.zIndex = '2147483647';
root.style.right = '10px';
root.style.bottom = '10px';
root.style.pointerEvents = 'none';
shadow = root.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
.wrap{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:12px;line-height:1.2;background:#111;color:#fff;border-radius:8px;padding:8px 10px;box-shadow:0 2px 12px rgba(0,0,0,.35);opacity:.95;max-width:260px;pointer-events:auto}
.wrap{display:flex;gap:8px;align-items:start}
.dot{width:8px;height:8px;border-radius:50%;background:#4ade80;margin-top:4px;flex:0 0 8px}
.msg{white-space:pre-line}
.hide{animation:fadeout .4s forwards}
@keyframes fadeout{to{opacity:0;transform:translateY(4px)}}
`;
wrap = document.createElement('div');
wrap.className = 'wrap';
wrap.innerHTML = `<div class="dot"></div><div class="msg"></div>`;
shadow.append(style, wrap);
const set = (text) => {
if (!root.isConnected) document.documentElement.appendChild(root);
wrap.querySelector('.msg').textContent = text;
wrap.classList.remove('hide');
clearTimeout(hideTimer);
hideTimer = setTimeout(() => {
wrap.classList.add('hide');
setTimeout(() => { if (root.isConnected) root.remove(); }, 450);
}, 1600);
};
return (what) => {
const now = Date.now();
if (what === last && (now - lastAt) < 250) return;
last = what; lastAt = now;
set(`visibility/mouse check bypassed:\n${what}`);
};
};
const notify = makeNotifier();
// tiny helper for safe define
const define = (target, key, descriptor) => {
try {
const desc = Object.getOwnPropertyDescriptor(target, key);
if (desc && desc.configurable === false) return false;
Object.defineProperty(target, key, { configurable: true, ...descriptor });
return true;
} catch { return false; }
};
// force document.* visibility values
const forceVisibilityProps = () => {
const docProto = Document.prototype;
const makeGetter = (valName, value) => function() {
if (CONFIG.notify.properties) notify(`${valName} read`);
return value;
};
define(docProto, 'visibilityState', { get: makeGetter('document.visibilityState', 'visible') }) ||
define(document, 'visibilityState', { get: makeGetter('document.visibilityState', 'visible') });
define(docProto, 'hidden', { get: makeGetter('document.hidden', false) }) ||
define(document, 'hidden', { get: makeGetter('document.hidden', false) });
const legacy = [
['webkitVisibilityState','visible'],
['mozVisibilityState','visible'],
['msVisibilityState','visible'],
['webkitHidden',false],
['mozHidden',false],
['msHidden',false]
];
for (const [k, v] of legacy) {
define(docProto, k, { get: makeGetter(`document.${k}`, v) }) ||
define(document, k, { get: makeGetter(`document.${k}`, v) });
}
const origHasFocus = docProto.hasFocus;
const announceHasFocus = () => { if (CONFIG.notify.hasFocusCall) notify('document.hasFocus() call'); return true; };
define(docProto, 'hasFocus', { value: announceHasFocus }) ||
define(document, 'hasFocus', { value: announceHasFocus });
if (!document.__origHasFocus) Object.defineProperty(document, '__origHasFocus', { value: origHasFocus, configurable: true });
};
// patch event listeners to swallow exit/enter-intent and wrap handlers
const forceVisibilityEvents = () => {
const TYPES = [
'visibilitychange','webkitvisibilitychange','mozvisibilitychange','msvisibilitychange',
'pagehide','freeze','resume','pageshow','blur','focus',
'mouseleave','mouseout','pointerleave','pointerout',
'mouseover','mouseenter','pointerover',
'mousemove'
];
const EXIT_TYPES = new Set(['mouseleave','mouseout','pointerleave','pointerout']);
const ENTER_TYPES = new Set(['mouseover','mouseenter','pointerover']);
const isEditableTarget = (ev) => {
if (!ev || !ev.target) return false;
const t = ev.target;
if (t.isContentEditable) return true;
const tag = (t.tagName || '').toLowerCase();
return tag === 'input' || tag === 'textarea' || tag === 'select';
};
const isGlobal = (ctx) =>
ctx === window || ctx === document || ctx === document.documentElement;
let allowOneGlobalEnter = !!CONFIG.mouse.initialEnterOnce;
const wrapHandler = (type, fn, ctx) => {
return function(event) {
try {
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
} catch {}
// block global "leaving window" signals
if (
CONFIG.mouse.ignoreGlobalLeave &&
EXIT_TYPES.has(type) &&
isGlobal(ctx) &&
(event == null || event.relatedTarget == null)
) {
if (CONFIG.notify.invoke[type]) notify(`${type} ignored (global exit)`);
return;
}
// block global "entering window" signals
if (
CONFIG.mouse.blockGlobalEnter &&
ENTER_TYPES.has(type) &&
isGlobal(ctx) &&
(event == null || event.relatedTarget == null)
) {
if (allowOneGlobalEnter) {
allowOneGlobalEnter = false; // let exactly one through
} else {
if (CONFIG.notify.invoke[type]) notify(`${type} ignored (global enter)`);
return;
}
}
if (CONFIG.notify.invoke[type]) {
if (!(CONFIG.suppressFocusOnEditable && (type === 'focus' || type === 'blur') && isEditableTarget(event))) {
notify(`${type} listener invoked`);
}
}
try { return fn.call(this, event); } catch(e) { setTimeout(() => { throw e; }); }
};
};
const patchAEL = (proto, label) => {
const orig = proto.addEventListener;
define(proto, 'addEventListener', {
value: function(type, listener, options) {
const t = String(type);
if (!listener) return orig.call(this, type, listener, options);
if (TYPES.includes(t)) {
// block registration of exit-intent listeners on global targets
if (CONFIG.mouse.spoofExit && EXIT_TYPES.has(t) && isGlobal(this)) {
if (CONFIG.notify.addListener[t]) notify(`${label}.addEventListener("${t}") blocked on global target`);
return;
}
// block registration of enter-intent listeners on global targets
if (CONFIG.mouse.blockGlobalEnter && ENTER_TYPES.has(t) && isGlobal(this)) {
if (CONFIG.notify.addListener[t]) notify(`${label}.addEventListener("${t}") blocked on global target`);
return;
}
if (CONFIG.notify.addListener[t]) notify(`${label}.addEventListener("${t}")`);
const wrapped = wrapHandler(t, listener, this);
try { Object.defineProperty(listener, '__visible_wrap__', { value: wrapped }); } catch {}
return orig.call(this, type, wrapped, options);
}
return orig.call(this, type, listener, options);
}
});
const origRel = proto.removeEventListener;
define(proto, 'removeEventListener', {
value: function(type, listener, options) {
const l = listener && listener.__visible_wrap__ ? listener.__visible_wrap__ : listener;
return origRel.call(this, type, l, options);
}
});
};
patchAEL(EventTarget.prototype, 'EventTarget');
};
// synthetic mouse activity to suggest presence in page
const installFakeMouse = () => {
if (!CONFIG.mouse.fakeMovement) return;
let lastX = Math.max(10, Math.min(window.innerWidth - 10, Math.floor(window.innerWidth / 2)));
let lastY = Math.max(10, Math.min(window.innerHeight - 10, Math.floor(window.innerHeight / 2)));
const update = (e) => {
if (!e) return;
if (typeof e.clientX === 'number') lastX = e.clientX;
if (typeof e.clientY === 'number') lastY = e.clientY;
};
window.addEventListener('mousemove', update, { passive: true, capture: true });
const tick = () => {
try {
const jx = (Math.random() * CONFIG.mouse.moveJitterPx * 2 - CONFIG.mouse.moveJitterPx) | 0;
const jy = (Math.random() * CONFIG.mouse.moveJitterPx * 2 - CONFIG.mouse.moveJitterPx) | 0;
const x = Math.max(0, Math.min(window.innerWidth - 1, lastX + jx));
const y = Math.max(0, Math.min(window.innerHeight - 1, lastY + jy));
const ev = new MouseEvent('mousemove', {
bubbles: true,
cancelable: false,
clientX: x,
clientY: y,
screenX: x,
screenY: y,
view: window
});
document.dispatchEvent(ev);
if (CONFIG.notify.invoke.mousemove) notify('synthetic mousemove dispatched');
} catch {}
};
setInterval(tick, Math.max(4000, CONFIG.mouse.fakeIntervalMs | 0));
};
// one-time visibility ping
const dispatchInitialPing = () => {
try {
const ev = new Event('visibilitychange');
document.dispatchEvent(ev);
} catch {}
};
// apply
forceVisibilityProps();
forceVisibilityEvents();
installFakeMouse();
document.addEventListener('DOMContentLoaded', dispatchInitialPing, { once: true });
// reapply on dom mutations (defensive)
const reapply = () => {
try {
if (document.visibilityState !== 'visible') forceVisibilityProps();
if (document.hidden !== false) forceVisibilityProps();
} catch {}
};
const mo = new MutationObserver(reapply);
mo.observe(document.documentElement, { childList: true, subtree: true });
})();