npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

innoshield

v1.0.0

Published

Client-side web protection and browser interaction control for JavaScript applications.

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+I or SHIFT+F10. It understands common aliases too, so CMD, WIN, and META all 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 debugger check 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, contenteditable areas, or your own selectors, so normal typing and accessibility still work by default.
  • Safe to import on the server too. It never touches window or document until you actually call start().
  • 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 innoshield

That'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 example blockKeys: ['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 debuggerDetection will 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 longer interval (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 except kiosk.

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 on debuggerDetection too, 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:

  • callback only does something if you also return a handler (or callback) function, like () => ({ action: 'callback', handler: myFn }). Just writing policy: { keyboard: 'callback' } on its own doesn't do anything beyond firing the normal event, use onViolation instead if you just want to watch for events.
  • redirect gets its URL from what the policy function returns (result.url or result.redirectUrl), not from replacementPage.
  • replace-page uses replacementPage.redirectUrl instead, if you set one. If not, it shows the plain screen described below.
  • overlay and lock both show that same screen as an overlay on top of the page. lock also turns off pointer events and text selection on the whole page.
  • warn just runs one console.warn(...) call, nothing more.
  • reload waits a few seconds between reloads using sessionStorage, 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 field

Keep 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:

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() or destroy() 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. Calling start() or updateConfig() after that throws a ProtectionRuntimeError on 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 test

This 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 here

This 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.