innoshield
v1.0.0
Published
Client-side web protection and browser interaction control for JavaScript applications.
Maintainers
Readme
InnoShield
Client-Side Web Protection & Browser Interaction Control
InnoShield is a small, zero-dependency JavaScript library that helps you control how people interact with your web page. It can block certain keyboard shortcuts, stop right-click, block copy/paste, stop text selection, and try to notice when someone has opened DevTools or a debugger. You set it up with one plain config object and it does the rest.
Read this first: InnoShield runs in the browser, on the user's own machine, so it can never be a real security wall. It can't fully stop DevTools from opening, and it can't block operating system shortcuts like the Windows key. Think of it as a way to discourage casual poking around and to control everyday browser behavior, not as a replacement for real server-side security. See Limitations and Security Considerations below before you rely on it for anything important.
By the numbers
A quick, honest snapshot of what's actually in this package right now:
| | |
| --- | --- |
| Runtime dependencies | 0 |
| Dev dependencies | 0 |
| Build tools used | 0 |
| Guards | 6 (keyboard, mouse, selection, clipboard, DevTools, debug) |
| Detectors | 3 (DevTools, debugger, console) |
| Source code | ~1,600 lines across 28 files |
| Test code | ~1,275 lines, 83 tests, all passing |
| Example pages | 6, plain HTML and JS, no build step |
| Packed size | about 24 kB (npm pack --dry-run) |
| Node.js needed for development | 18 or newer |
Features
- Keyboard Guard blocks single keys, modifier keys, or any key
combination you want, like
CTRL+SHIFT+IorSHIFT+F10. It understands common aliases too, soCMD,WIN, andMETAall mean the same thing. - Mouse Guard can block the right-click menu, right-click itself, middle-click, and dragging (this also covers dragging images).
- Selection Guard stops users from selecting text, either everywhere on the page or only on the elements you choose.
- Clipboard Guard can block copy, cut, and paste, with a callback for each one.
- DevTools Guard checks, on a timer, whether the browser window's dimensions look like DevTools is open, and can respond in a few different ways (warn, show an overlay, lock the page, replace the page, redirect, reload, or just run your own callback).
- Debug Guard runs a short, one-time
debuggercheck on a timer to spot an attached debugger, plus an optional, weaker check for an open console. - Violation Engine turns every blocked or detected event into one simple shape, so you can handle everything through a single event listener if you want.
- Every guard lets you exclude form fields,
contenteditableareas, or your own selectors, so normal typing and accessibility still work by default. - Safe to import on the server too. It never touches
windowordocumentuntil you actually callstart(). - No network calls, no analytics, no data collection of any kind.
Why InnoShield
A lot of "block DevTools" snippets you find online are just a few keydown
checks bolted onto a page, with no clean way to turn them off, no way to
know what actually got blocked, and no honesty about what they can't do.
InnoShield is built to be a bit more grown up about it:
- it has a real start/stop/destroy lifecycle instead of listeners you can never clean up,
- every guard reports through the same event shape, so you can log or react to anything the same way,
- you can define your own key combinations instead of being stuck with a fixed list of DevTools shortcuts,
- and the docs tell you plainly where the detection stops being reliable, instead of pretending it always works.
Installation
npm install innoshieldThat's it. No extra setup and no other packages to install.
Quick start
You can use InnoShield as a default import or a named import, whichever you like:
import InnoShield from 'innoshield';
const shield = InnoShield.protect({
preset: 'strict',
});import { InnoShield } from 'innoshield';
const shield = new InnoShield({ preset: 'strict' });
shield.start();InnoShield.protect(config) just creates the instance and calls .start()
for you right away.
Configuration
The config is one object, with a section for each guard, plus a couple of
shared sections (policy and replacementPage) and one top level callback
(onViolation). You only need to set the fields you actually want to
change, everything else falls back to a sensible default. The full list of
defaults lives in src/core/defaults.js if you want
to see it directly.
{
keyboard: {
enabled: false,
blockKeys: [], // e.g. ['F12'] or ['CTRL'] for single keys
blockCombinations: [], // e.g. ['CTRL+SHIFT+I']
exclude: ["input", "textarea", "select", "[contenteditable='true']"],
preventDefault: true,
stopPropagation: false,
stopImmediatePropagation: false,
onBlockedKey: null, // (event) => void
},
mouse: {
enabled: false,
contextMenu: false, // block the right-click menu
rightClick: false, // block the right mouse button press itself
middleClick: false, // block middle-click
drag: false, // block dragging (also covers image dragging)
exclude: [],
},
selection: {
enabled: false,
mode: 'global', // 'global' or 'selector'
selectors: [], // only used when mode is 'selector'
exclude: ["input", "textarea", "[contenteditable='true']"],
},
clipboard: {
enabled: false,
copy: false,
cut: false,
paste: false,
exclude: ["input", "textarea", "[contenteditable='true']"],
onCopyBlocked: null,
onCutBlocked: null,
onPasteBlocked: null,
},
devtools: {
enabled: false,
interval: 1000, // how often to check, in ms (minimum 300ms)
action: 'none', // see "DevTools detection" below
onDetected: null, // (result) => void
},
debug: {
enabled: false,
debuggerDetection: false,
interval: 1500, // how often to check, in ms (minimum 500ms)
// this also controls consoleDetection's timing, see below
// threshold: 100, // optional, in ms, see "Debugger detection"
},
consoleDetection: {
enabled: false,
},
policy: {}, // custom responses per violation type, see "Violation events"
replacementPage: {
title: 'Restricted Access',
message: 'Developer tools or a restricted browser interaction was detected.',
redirectUrl: '',
showReloadButton: false,
},
onViolation: null, // (event) => void, called for every single violation
}You can also start from a preset ('basic', 'strict', or 'kiosk', more
on those below) and only override the parts you want to change. If you pass
something invalid, like a bad preset name, the wrong type for a field, or a
negative interval, InnoShield throws an InvalidConfigurationError right
away so you find out early.
Guards
Keyboard Guard
It listens for keydown and blocks two kinds of things:
- Single keys, through
blockKeys. For exampleblockKeys: ['F12']blocks a lone F12 press.blockKeys: ['CTRL']blocks a lone Ctrl press, but not every shortcut that happens to use Ctrl. - Combinations, through
blockCombinations. This is a general parser, not a fixed list of DevTools shortcuts, so you can write things like'CTRL+ALT+S','SHIFT+F10','META+ALT+I','CTRL+P', or'CTRL+S'and they all work the same way.
Aliases and spacing get cleaned up automatically, so all of these count as the same combination:
CTRL, CONTROL -> CONTROL
CMD, COMMAND, WIN, WINDOWS, META -> META
OPTION, ALT -> ALT
"ctrl+shift+i"
"CTRL + SHIFT + I"
"Shift+Ctrl+I"Single keys you can use include F1 through F24, ESC/ESCAPE, TAB,
ENTER/RETURN, SPACE/SPACEBAR, the arrow keys, HOME, END,
PAGEUP, PAGEDOWN, INSERT, DELETE/DEL, BACKSPACE,
PRINTSCREEN/PRTSC/PRTSCN (only in browsers that actually send a key
event for it, some don't), plus the four modifier keys above.
preventDefault (on by default), stopPropagation, and
stopImmediatePropagation only apply to keys that actually get blocked.
exclude (form fields and contenteditable by default) is checked by
walking up from wherever the key was pressed, so it still works even if the
event target is nested a few levels deep inside an excluded element.
Each blocked key reports: { type: 'keyboard', action: 'blocked', source:
'KeyboardGuard', metadata: { key, matchType, combo } }.
Mouse Guard
mouse: {
enabled: true,
contextMenu: true, // block the right-click menu
rightClick: true, // block the right mouse button press itself
middleClick: false, // block middle-click
drag: false, // block dragging, images included
}Nothing here is on by default, you turn on exactly what you need. One thing worth knowing: blocking the context menu and blocking right-click/ middle-click/drag report two different event types.
- Blocking the context menu reports
{ type: 'contextmenu', action: 'blocked', source: 'MouseGuard', metadata: { button } } - Blocking right-click, middle-click, or drag reports
{ type: 'mouse', action: 'blocked', source: 'MouseGuard', metadata: { action: 'rightclick' | 'middleclick' | 'drag', ... } }
So if you're filtering events by type, remember there are two possible types coming from this one guard.
Selection Guard
selection: {
enabled: true,
mode: 'global', // or 'selector'
selectors: [], // only used with 'selector' mode
exclude: ["input", "textarea", "[contenteditable='true']"],
}This works by listening for the selectstart event and calling
preventDefault() on it, it doesn't rely on CSS. In 'global' mode it
blocks selection everywhere except excluded elements. In 'selector' mode
it only blocks selection on the elements matching selectors.
Clipboard Guard
clipboard: {
enabled: true,
copy: true,
cut: true,
paste: false,
exclude: ["input", "textarea", "[contenteditable='true']"],
onCopyBlocked(event) {},
onCutBlocked(event) {},
onPasteBlocked(event) {},
}It listens for the copy, cut, and paste events and calls
preventDefault(). It never reads or stores what's actually on the
clipboard, it only knows that a copy/cut/paste was attempted and blocks it.
The event metadata only ever says which kind it was, nothing more.
DevTools detection
devtools: {
enabled: true,
interval: 1000, // ms, minimum is 300
action: 'none', // 'none' | 'callback' | 'warn' | 'overlay' | 'lock' |
// 'replace-page' | 'redirect' | 'reload'
onDetected(result) {
// result looks like: { detected, method: 'viewport', confidence, orientation, timestamp }
},
}Right now this uses one method: it checks, on a timer, whether there's a
big gap between window.outerWidth/outerHeight and
innerWidth/innerHeight. That gap tends to show up when DevTools is
docked to the side or bottom of the browser window. orientation is a
guess at which side, and confidence just tells you how far past the
threshold the gap is. It's not a guaranteed probability, just a rough
signal.
Be aware this is a heuristic (a rule of thumb, not a certainty), and it won't catch everything:
- It can miss real DevTools sessions. If DevTools opens as its own separate window, or the user never resizes anything, there's no gap to see. And in some real testing during development, this check simply didn't fire reliably on certain browser and OS combinations.
- It can also trigger by mistake. Browser zoom, extra toolbars, or unusual window setups can shift these numbers too.
If this check isn't reliable enough for your setup, pair it with (or lean more on) the debugger detection below, which doesn't care about window size at all.
devtools.action is just a shortcut for policy.devtools (explained under
"Violation events" below). If you set both, policy.devtools wins. Leaving
action as 'none' (the default) means nothing extra happens beyond the
event itself.
Each detection reports: { type: 'devtools', action: 'detected', source:
'DevToolsGuard', metadata: result }. It only fires once when DevTools first
becomes detected, not on every single check while it stays open.
Debugger detection
DebugGuard is a separate guard from DevToolsGuard, and it reports a
different event type: 'debugger', not 'devtools'. If your code only
listens for 'devtools' events, you'll miss these.
debug: {
enabled: true,
debuggerDetection: true,
interval: 1500, // ms, minimum is 500
threshold: 100, // ms, optional, defaults to 100 if you don't set it
}On each check, it runs a single debugger; line and times how long it
takes with performance.now(). If a debugger is attached (meaning DevTools
is open, on any tab, not just the Sources tab), that line actually pauses
the page, so the timing spikes way above normal. If nothing is attached, it
does nothing and the timing stays close to zero. This doesn't care about
window size, docking side, or which monitor DevTools is on, so it tends to
be a stronger signal than the viewport check above. But there's a real cost
to it:
Turning on
debuggerDetectionwill actually pause your page for a moment, every time a check runs while DevTools is attached. That's the check doing its job, not a bug. If you or your users often keep DevTools open while working on the site, this will interrupt that fairly often. Use a longerinterval(2000 to 3000ms) if that gets annoying, and only turn it on where you actually want this trade-off. It's off by default in every preset exceptkiosk.
Each detection reports: { type: 'debugger', action: 'detected', source:
'DebugGuard', metadata: { detected, method, confidence, elapsed, timestamp } }.
Console detection
consoleDetection: {
enabled: true, // off by default, even in the kiosk preset
}This also lives inside DebugGuard and shares its timer, so it runs on
debug.interval, not its own separate interval (there isn't one). It runs
whenever consoleDetection.enabled is true, whether or not
debug.enabled/debuggerDetection is also on.
Each check logs one small object with a getter to console.log and checks
whether that getter actually got read. This only works if the browser's
console eagerly looks at object properties to display them. In practice
that's unreliable: modern Chrome made this lazy on purpose to stop tricks
like this, and it also comes back false under Node's own console (checked
directly in this project's tests). Treat it as a weak, optional signal, not
something as solid as the other two checks. It never logs your page's
content, just the one marker object, and it never makes a network request.
Each detection reports: { type: 'console', action: 'detected', source:
'DebugGuard', metadata: { detected, method, confidence, timestamp } }.
Violation events
Every blocked or detected thing, from any guard, gets turned into the same shape:
{
id: 'isv_...', // made locally, no outside library involved
type: 'keyboard' | 'mouse' | 'contextmenu' | 'selection' | 'clipboard' |
'devtools' | 'debugger' | 'console' | 'custom',
action: 'blocked' | 'detected' | 'warning' | 'locked' | 'redirected' | 'replaced',
source: 'KeyboardGuard' | 'MouseGuard' | 'SelectionGuard' | 'ClipboardGuard' |
'DevToolsGuard' | 'DebugGuard',
timestamp: 1234567890,
metadata: { /* only details about the event itself, never page content */ },
}Remember,
'devtools'and'debugger'are two separate types, coming from two separate guards. If you turned ondebuggerDetectiontoo, make sure your code checks for both, not just'devtools'.
shield.onViolation((event) => { /* ... */ }); // same as addViolationListener
const unsubscribe = shield.addViolationListener((event) => { /* ... */ });
shield.removeViolationListener(listener);If one listener throws an error, the others still run fine, and the guard
that reported the event isn't affected either. You can also set one
top-level onViolation callback in the config, which runs for every event
in addition to any listeners you add.
Policies and responses
You can set up custom responses in one place, under policy, keyed by
violation type rather than tied to a specific guard:
const shield = new InnoShield({
policy: {
devtools: 'lock',
clipboard(event) {
return { action: 'redirect', url: '/security-warning' };
},
},
});The available actions are: none, callback, warn, overlay, lock,
replace-page, redirect, reload. A few things worth knowing about how
they actually behave:
callbackonly does something if you also return ahandler(orcallback) function, like() => ({ action: 'callback', handler: myFn }). Just writingpolicy: { keyboard: 'callback' }on its own doesn't do anything beyond firing the normal event, useonViolationinstead if you just want to watch for events.redirectgets its URL from what the policy function returns (result.urlorresult.redirectUrl), not fromreplacementPage.replace-pageusesreplacementPage.redirectUrlinstead, if you set one. If not, it shows the plain screen described below.overlayandlockboth show that same screen as an overlay on top of the page.lockalso turns off pointer events and text selection on the whole page.warnjust runs oneconsole.warn(...)call, nothing more.reloadwaits a few seconds between reloads usingsessionStorage, so it can't get stuck in a reload loop (it just skips the reload if storage isn't available, instead of failing).
The overlay and replacement screen are built using plain
document.createElement and textContent, never innerHTML, so your
title/message text always shows up as plain text and never gets parsed
as HTML:
replacementPage: {
title: 'Restricted Access',
message: 'Developer tools or a restricted browser interaction was detected.',
redirectUrl: '',
showReloadButton: false,
}Presets
| Preset | Keyboard | Mouse | Selection | Clipboard | DevTools | Debug |
| --- | --- | --- | --- | --- | --- | --- |
| basic | blocks F12 only | off | off | off | off | off |
| strict | F12, CTRL+SHIFT+I/J/C, CTRL+U, CTRL+SHIFT+K | context menu and right-click on | on, whole page | copy/cut on, paste off | on, warns you | off |
| kiosk | strict's combos plus CTRL+W/T/N/R, ALT+F4, and blocks the Meta/Windows key | context menu, right-click, middle-click, drag all on | on, whole page | copy/cut/paste all on | on, locks the page | on, with debugger detection |
consoleDetection stays off in every preset, kiosk included. Presets are
just a starting point, anything you also pass in overrides that one field:
InnoShield.protect({ preset: 'basic' });
InnoShield.protect({ preset: 'strict' });
InnoShield.protect({ preset: 'kiosk' });
InnoShield.protect({ preset: 'strict', mouse: { contextMenu: false } }); // override one fieldKeep in mind that blocking the Meta/Windows key in kiosk only stops the
keydown event the page receives. It can't stop the operating system from
acting on that key first, see Limitations below.
Examples
Plain HTML and JS pages, no framework and no build step. Open them straight in a browser, or serve the repo folder with any static file server since they use normal ES module imports:
examples/basic/index.html, thebasicpresetexamples/strict/index.html, thestrictpresetexamples/kiosk/index.html, thekioskpreset with a couple of exclusionsexamples/custom/index.html, no preset, custom combos, a callback policy, and buttons for start/stop/destroyexamples/devtools-debug/index.html, shows the raw DevTools detector numbers live, handy for checking this in your own browserexamples/debugger-debug/index.html, shows the raw debugger timing numbers live
Lifecycle
shield.start(); // turns on the enabled guards, safe to call again if already started
shield.stop(); // turns everything off and clears listeners/timers, keeps your config
shield.destroy(); // stop(), plus the instance can't be used again after this
shield.isActive(); // true only when started and not destroyed
shield.getConfig(); // a copy of the current config, safe to look at without affecting anything
shield.getState(); // { active, destroyed, guards: [...names] }
shield.updateConfig(partialConfig);A few guarantees worth knowing:
- Calling
start()while it's already running does nothing extra, it never adds duplicate listeners. - Calling
stop()ordestroy()more than once is fine, nothing breaks. updateConfig()merges your changes into the current config and, if the instance is running, quietly stops and restarts it so nothing gets duplicated or left behind.- Once you call
destroy(), that's final. Callingstart()orupdateConfig()after that throws aProtectionRuntimeErroron purpose, so you notice the mistake instead of things silently not working.
Browser compatibility
InnoShield is built for modern, evergreen browsers (Chrome and other
Chromium browsers, Edge, Firefox, Safari) using only standard web APIs like
addEventListener, preventDefault, window.outerWidth/innerWidth,
performance.now(), and sessionStorage. If a browser doesn't support one
of these checks, or window/document just isn't there, that one check
quietly reports nothing found instead of crashing anything.
To be clear about what's actually been checked:
- Keyboard, mouse, selection, and clipboard blocking use standard, long-established browser events and behave consistently across current browsers. These were tested by hand in a Chromium-based browser (Brave) while building this library, and worked as expected.
- DevTools and debugger detection depend a lot on the specific browser, operating system, and even window manager. During real testing, the viewport check didn't reliably catch DevTools in at least one real setup, which is exactly the kind of gap described above, while the debugger timing check did work there. Expect this kind of variation elsewhere too, it's not really a flaw in this library so much as a limit of the technique itself.
- The automated tests below check the code's logic using small hand-made stand-ins for the browser, running in Node. They prove the logic is correct, they don't prove every real browser behaves exactly the same. Before you rely on anything DevTools or debugger related, test it in a real browser using the example pages above, don't just trust the test suite for that part.
Limitations
- The browser belongs to the user, not you. They can turn off JavaScript, edit it live in DevTools, override your event handlers, use browser extensions, drive the browser with automation tools, watch network requests directly, or just use a different browser entirely. Nothing here changes that.
- Operating system shortcuts can't be reliably blocked. Keys like the Windows or Command key, and most OS-level shortcuts in general, are handled by the operating system before the page even sees them. InnoShield can only react to the keydown event the browser actually hands to the page, and sometimes it never gets one at all (for example, if focus was on the browser's address bar instead of the page itself).
- DevTools detection is a best guess, not a guarantee, in both directions, see DevTools detection above for the specific cases where it can be wrong.
- Debugger detection pauses the page on purpose while it checks, see Debugger detection above.
- Browsers just don't all behave the same way. Keyboard events, context menus, DevTools internals, window sizing, debugger behavior, clipboard events, text selection, and OS shortcuts can all differ across browsers, operating systems, and setups.
Security considerations
Please don't use InnoShield for:
- logging people in or checking who they are
- deciding what someone is allowed to access
- hiding secrets, API keys, or your source code from someone determined to see them
- securing an API or a server
- encryption
- stopping a determined person from reading or changing your app's code
InnoShield is meant for client-side control and discouraging casual poking around, not real security. Anything that actually matters for security needs to be enforced on your server, always assuming the browser itself can't be trusted.
It also makes zero network requests and collects zero data of any kind. No IP addresses, no device fingerprints, no browsing history, no keystroke logs, and no clipboard content. The event metadata only ever describes the interaction itself (which key, which action), never anything from the page or the clipboard.
Testing
npm testThis runs node test/run-tests.js, a small test runner built only on
Node's own node:assert/strict. There's no test framework involved, no
Jest, no Vitest, no Mocha, nothing extra added just for testing.
Right now there are 83 tests, and they all pass. They cover:
- keyboard alias and combo handling, single key vs combination blocking, blocking a modifier alone without blocking every combo that uses it, exclusions, disabled mode, and no duplicate listeners
- mouse context menu, right-click, middle-click, and drag blocking, plus exclusions
- selection blocking in both global and selector mode, plus exclusions
- clipboard copy/cut/paste blocking, exclusions, and callbacks
- the violation engine itself, event shape, unique ids, adding and removing listeners, one listener's error not breaking the others, and policy handling
- configuration, defaults, all three presets, your own overrides, and invalid values
- the full lifecycle, start/stop/restart/destroy, calling things twice,
using a destroyed instance, and
updateConfig()'s restart behavior - DevTools detection, results across different scenarios, not repeating a callback while still detected, policies running end to end, and timers actually getting cleared
- debugger and console detection, well formed results, making sure
'debugger'and'console'events are reported separately from'devtools', and timers getting cleared here too
The browser bits (window, document, events) are replaced with small,
hand-written stand-ins in test/mocks/, just enough to test
the logic, not a full browser. That's why real browser testing (using the
example pages) still matters for anything DevTools or debugger related, the
test suite checks the code, not every real browser.
Zero dependencies
"dependencies": {} // not even present, not just empty
"devDependencies": {} // same hereThis package really doesn't depend on anything else. No TypeScript, no
bundler, no test framework, no linter or formatter, no outside utility
library. It's plain JavaScript using standard web APIs, with Node's own
node:assert/strict for testing. Nothing but npm itself is needed to
install it, test it, or publish it, and npm pack --dry-run shows the whole
package comes in under 25 kB.
License
MIT, copyright Innoartive Labs. See LICENSE for the full text.
