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

@tindalabs/shield

v0.1.1

Published

Browser tamper detection for hostile environments. Detects DevTools, automation drivers, extension injection, and environment spoofing — surfaces findings as structured risk signals, not just boolean flags.

Readme

@tindalabs/shield

npm version CI License: MIT Zero runtime dependencies

Browser tamper detection for hostile environments.

Shield detects DevTools, automation drivers, extension injection, and environment spoofing — surfaces findings as structured risk signals composable with Blindspot spans and Scent identity risk scoring.


Install

npm install @tindalabs/shield

Which API do I want?

Shield ships three top-level APIs. Pick by what you want to do with the result:

| Goal | API | What it does | |---|---|---| | Observe / score the session — log signals, gate a feature, feed a risk model | assess() | Detects DevTools / automation / extensions / frame embedding. Returns structured signals + risk.score (0–1) + OTel span attributes. No side effects. | | Block devtools / copy / print / screenshots / clipboard / keyboard shortcuts on an element | ContentProtector | Manual control: pass an element + options, get an instance with dispose(). Always-on for everyone who hits the page. | | Both — observe first, then activate protections only when the session warrants it | assessAndProtect() | Runs assess(), evaluates a declarative PolicyRule[] (e.g. "watermark when riskScore ≥ 0.3"), activates the matching strategies. Legitimate users see no overhead. |

For OTel-instrumented protection (every block / detection becomes a span event), wrap any of the above with attachShieldToSpan().


Quick start — assess()

The primary API is a single async call that returns structured signals, a risk summary, and OTel-compatible span attributes:

import { assess } from '@tindalabs/shield';

const result = await assess();

console.log(result.signals);
// {
//   'shield.devtools.open': false,
//   'shield.automation.webdriver': false,
//   'shield.automation.headless': false,
//   'shield.frame.embedded': false,
//   'shield.extension.detected': false,
//   'shield.extension.names': '',
// }

console.log(result.risk);
// { score: 0, flags: [] }

// Attach to a Blindspot / OpenTelemetry span
span.setAttributes(result.spanAttributes);

With Blindspot

import { assess } from '@tindalabs/shield';
import { useSpan } from '@tindalabs/blindspot-react';

const { setAttribute } = useSpan();
const shield = await assess();
Object.entries(shield.spanAttributes).forEach(([k, v]) => setAttribute(k, v));

With Scent

Shield's signals compose directly with Scent's identity and risk engine:

import { assess } from '@tindalabs/shield';
import { init as initScent } from '@tindalabs/scent-sdk';

const scent = initScent({ apiKey: '...', endpoint: '...' });
const shield = await assess();

// shield.signals merges into the snapshot alongside browser fingerprint signals
const obs = await scent.observe({ extraSignals: shield.signals });
await scent.flush();
// The server risk engine now sees webdriver/headless/devtools signals
// alongside canvas, fonts, hardware and all other collected signals.

assess(options?) reference

| Option | Type | Default | Description | |---|---|---|---| | devtools | boolean | true | Run DevTools detection (async, timing-based) | | extensions | boolean | true | Run browser extension DOM/JS scan | | timeout | number | 400 | Max ms before async detections resolve with false | | extensionConfig | ExtensionConfig[] | built-in | Extension signatures to check against |

ShieldAssessment

interface ShieldAssessment {
  signals: {
    'shield.devtools.open': boolean;
    'shield.automation.webdriver': boolean;
    'shield.automation.headless': boolean;
    'shield.frame.embedded': boolean;
    'shield.extension.detected': boolean;
    'shield.extension.names': string; // comma-separated
  };
  risk: {
    score: number;   // 0–1 normalised threat score
    flags: string[]; // ['webdriver', 'devtools_open', ...]
  };
  spanAttributes: Record<string, string | boolean | number>;
}

Risk flags and weights

| Flag | Score contribution | Triggered by | |---|---|---| | webdriver | 0.9 | navigator.webdriver === true | | headless | 0.7 | Headless UA string, zero plugins, missing Permissions API | | devtools_open | 0.4 | Timing/debugger/size detectors | | frame_embedded | 0.3 | window.self !== window.top (cross-origin) | | extension | 0.2 | DOM selector or JS global signature match |


Risk-gated protection — assessAndProtect()

assessAndProtect() bridges both APIs with a declarative policy engine. It runs assess(), evaluates a set of rules against the result, and activates a ContentProtector with exactly the strategies each session warrants — zero overhead for legitimate users, full defence for automation and high-risk sessions.

import { assessAndProtect } from '@tindalabs/shield';

const { assessment, protector } = await assessAndProtect(contentEl, {
  policies: [
    // Watermark any session with measurable risk — embed score for traceability
    {
      when: { riskScore: { gte: 0.2 } },
      enable: ['enableWatermark'],
      watermarkOptions: (a) => ({ text: `RISK-${Math.round(a.risk.score * 100)}` }),
    },
    // Selection + clipboard lockdown for high-risk sessions
    {
      when: { riskScore: { gte: 0.6 } },
      enable: ['preventSelection', 'preventClipboard', 'preventKeyboardShortcuts'],
    },
    // Always block headless browsers regardless of score
    {
      when: { signals: { 'shield.automation.headless': true } },
      enable: ['preventScreenshots', 'preventContextMenu'],
    },
  ],
});

// protector is null when no rules matched (legitimate session — no overhead)
if (protector) {
  console.log('Protection active — score:', assessment.risk.score);
}

All matched rules are merged: a session with score 0.8 triggers watermark + selection + clipboard + keyboard in one pass.

With OTel / Blindspot

Pass a spanEmitter to emit shield.policy.triggered events and wire ContentProtector callbacks to child spans:

import { assessAndProtect } from '@tindalabs/shield';
import { getTracer, getRouteContext } from '@tindalabs/blindspot';

await assessAndProtect(contentEl, {
  policies: [/* ... */],
  spanEmitter: (name, attrs) => {
    const span = getTracer().startSpan(name, { attributes: attrs }, getRouteContext());
    span.end();
  },
});

PolicyEngineOptions

| Option | Type | Description | |---|---|---| | policies | PolicyRule[] | Ordered list of rules. All matching rules are merged. | | targetElement | HTMLElement \| null | Element to protect. Defaults to document.body. | | customHandlers | CustomEventHandlers | Forwarded to ContentProtector. | | spanEmitter | SpanEmitter | Uses attachShieldToSpan and emits policy OTel events. | | assessOptions | AssessOptions | Forwarded to the internal assess() call. |

PolicyRule

| Field | Type | Description | |---|---|---| | when | PolicyCondition | Conditions that must all match. An empty {} always matches. | | enable | StrategyKey[] | Strategies to activate when the condition matches. | | watermarkOptions | WatermarkOptions \| (a: ShieldAssessment) => WatermarkOptions | Static or factory watermark config. Last matched rule wins. |

PolicyCondition

| Field | Type | Description | |---|---|---| | riskScore.gte | number | Score must be ≥ this value. | | riskScore.lt | number | Score must be < this value. | | signals | Partial<ShieldSignals> | All listed signal values must match. |

OTel events emitted

| Event | When | |---|---| | shield.policy.triggered | At least one rule matched — includes shield.policy.risk_score, shield.policy.matched_rules, shield.policy.enabled_strategies | | shield.policy.evaluated | No rules matched — includes shield.policy.matched_rules: 0, shield.policy.protection_activated: false |


Use cases — adaptive content protection

assessAndProtect() exists for the awkward middle ground: blanket protection breaks the experience for legitimate users, but no protection lets scrapers walk away with everything. The policy engine activates only the strategies the session's risk profile warrants — humans see nothing, automation hits a wall.

Anti-AI scraping

LLM training crawlers, prompt-based scrapers, and headless research agents share the same signal profile as conventional automation: shield.automation.webdriver, shield.automation.headless, patched-API heuristics, missing plugin metadata. A single policy rule flips watermarking and selection/clipboard lockdown on those sessions while human visitors get the unmodified page.

The watermarkOptions factory receives the full assessment, so anything that does get scraped carries a forensic trace back to the session that extracted it:

{
  when: { signals: { 'shield.automation.headless': true } },
  enable: ['enableWatermark', 'preventSelection', 'preventClipboard'],
  watermarkOptions: (a) => ({
    text: `SHIELD-${Math.round(a.risk.score * 100)}-${Date.now().toString(36)}`,
  }),
}

Pair with spanEmitter and every triggered rule emits shield.policy.triggered to your OTel pipeline — operators can see in real time how often, and by what signal, their content is being targeted, without instrumenting strategies by hand.

Risk-proportional DRM

Financial, legal, and media documents need strong protection — but blanket DRM breaks screen readers, accessibility tooling, and developers debugging their own portal. Risk-keyed policy rules flip protection on only when warranted (high risk, automation signals, specific extensions) and leave the long tail of normal sessions completely untouched. The same engine handles both ends of the risk spectrum with one config.

When not to reach for it

If every session needs the same protection (e.g. a known-private internal tool where any visitor is high-trust by definition), skip the engine and use ContentProtector directly (next section). assessAndProtect() adds an assess() round trip and is only worth it when protection should vary per session.


Active protection — ContentProtector

Shield also exports the full protection suite for active content defense: blocks DevTools, prevents copy/print/selection, adds watermarks, detects extension injection and iframe embedding.

import { ContentProtector } from '@tindalabs/shield';

const protector = new ContentProtector({
  preventDevTools: true,
  preventKeyboardShortcuts: true,
  preventPrinting: true,
  preventClipboard: true,
  clipboardOptions: { preventCopy: true, preventCut: true, preventPaste: false },
  enableWatermark: true,
  watermarkOptions: { text: 'Confidential', userId: 'user-123' },
  // …and more (selection, context menu, screenshots, extensions, iframe embedding)
  // — see REFERENCE.md for the complete options table.
});

protector.protect();

// Later:
protector.unprotect();
protector.dispose();

See REFERENCE.md for the full ContentProtector API and all strategy options.


OTel-instrumented protection — attachShieldToSpan()

attachShieldToSpan() is a thin wrapper around ContentProtector that turns every protection event into a span event. Each blocked copy, print, keyboard shortcut, devtools open, or screenshot attempt becomes a shield.* event in your tracing pipeline — no manual callback wiring per strategy.

Shield is framework-agnostic about OTel — it doesn't depend on @opentelemetry/api. You provide a SpanEmitter callback; Shield calls it.

import { attachShieldToSpan } from '@tindalabs/shield';

const protector = attachShieldToSpan(
  { preventClipboard: true, enableWatermark: true, watermarkOptions: { text: 'Confidential' } },
  (name, attrs) => span.addEvent(name, attrs),
);

protector.protect();

With Blindspot

import { attachShieldToSpan } from '@tindalabs/shield';
import { getTracer, getRouteContext } from '@tindalabs/blindspot';

const protector = attachShieldToSpan(
  { preventClipboard: true, preventScreenshots: true },
  (name, attrs) => {
    const span = getTracer().startSpan(name, { attributes: attrs }, getRouteContext());
    span.end();
  },
);

protector.protect();

Emitting each event as a short-lived span (start + immediately end) means findings reach Tempo/Honeycomb/Jaeger without waiting for the long-lived navigation span to close.

Events emitted

| Event name | Fires when | Attributes | |---|---|---| | shield.devtools.opened / shield.devtools.closed | DevTools state changes | — | | shield.selection.attempted | User tries to select content | — | | shield.context_menu.attempted | Right-click / long-press | — | | shield.print.attempted | Print dialog opens | — | | shield.keyboard_shortcut.blocked | Blocked shortcut fires | shield.keyboard.key, shield.keyboard.code | | shield.clipboard.copy / .cut / .paste | Clipboard action attempted | — | | shield.screenshot.attempted | PrintScreen / Win+Shift+S / Cmd+Shift+3/4/5 | — | | shield.extension.detected | Known scraping extension found | shield.extension.id, shield.extension.name, shield.extension.risk | | shield.frame.embedding.detected | Page rendered inside an iframe | shield.frame.external | | shield.protection.bypassed | A protection strategy was circumvented | shield.bypass.method | | shield.content.hidden / .restored | Protected content toggled | shield.hidden.reason (hidden only) |

Emitter exceptions are swallowed — telemetry sink failure never crashes the protected page. Any customHandlers you also pass in options still get called after the emit.

Pair with assess() for an end-to-end picture: route-level risk score plus per-interaction protection events, all keyed off the same span.


Demo

A local demo app is included at demo/. It exercises both APIs in a dark-themed single-page app:

  • Environment Assessment — runs assess() and displays each signal, the risk score bar, and the raw OTel span attributes ready to copy.
  • Active Content Protection — full ContentProtector controls: every strategy toggle, watermark options, live events log.
cd demo
npm install
npm run dev   # http://localhost:5175 (or next available port)

The Tindalabs stack

Shield is one of three composable browser-layer packages:

| Package | What it does | |---|---| | @tindalabs/blindspot | Privacy-first OTel frontend observability | | @tindalabs/shield | Tamper detection & active content protection | | @tindalabs/scent | Probabilistic identity continuity |


License

MIT © Tindalabs