@s4e/jsentinel
v0.4.3
Published
Script integrity and clipboard tamper detection for browsers.
Downloads
74
Readme
@s4e/jsentinel
Lightweight browser sentinel for script integrity, clipboard tamper detection, extension activity monitoring, and security telemetry. Framework‑agnostic, event‑driven.
- Script integrity (same‑origin): hashes JS/CSS at runtime and compares to your manifest (SRI‑style)
- Inline scripts: distinguishes stable vs volatile inline blocks; hashes stable in manifest, warns on volatile
- External scripts (CDN/remote): accepts pinned versioned CDNs; supports dynamic‑loader pinning/allow rules
- Dynamic script monitoring: MutationObserver detects newly added scripts and src attribute changes
- Clipboard tamper: detects when pasted text differs from what the user copied
- Extension activity monitoring: detects suspicious browser extension behaviors and form data access patterns
- Anti‑tamper: runtime guard against critical API patching
- Network telemetry: fetch/XHR/beacon start/end with timing, status, headers
- Headers visibility: minimal security headers snapshot
- CSP violation monitoring: captures and reports Content Security Policy violations for security analysis
Step 1 — Generate the manifest (CLI)
Use the companion CLI to scan your build output and write manifest.json.
CLI on npm: @s4e/jsentinel-cli
npm i -D @s4e/jsentinel-cliCreate jsentinel.config.json for clean CI usage (with schema IntelliSense):
Obtain your dashboard API token (s4eToken) via [email protected].
{
"$schema": "./node_modules/@s4e/jsentinel-cli/schema/jsentinel.config.schema.json",
"baseUrl": "/",
"inputs": [".next/static", "public"],
"output": "public/manifest.json",
"s4eToken": "<YOUR_S4E_TOKEN>"
}Run after your build:
npx @s4e/jsentinel-cli jsentinel manifest -c jsentinel.config.jsonThis writes public/manifest.json, which the runtime reads by default.
Step 2 — Install the runtime
npm i @s4e/jsentinel
# yarn add @s4e/jsentinel
# pnpm add @s4e/jsentinel
# bun add @s4e/jsentinelStep 3 — Integrate (React, Vue, Svelte, Angular)
Place initialization in any client‑only part of your app and listen to a single DOM event with a typed payload.
Use your license token (request via [email protected]). You can initialize in any client‑only file, page, or component across frameworks.
React example:
import { useEffect } from "react";
import { initJSentinel } from "@s4e/jsentinel";
export function Security() {
useEffect(() => {
initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" });
const onMsg = (e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => {
const msg = e.detail;
if (msg.type === "ready") console.log("JSentinel ready", msg.data);
if (msg.type === "alert") console.warn("Security alert", msg.data);
};
window.addEventListener("jsentinel", onMsg as EventListener);
return () => window.removeEventListener("jsentinel", onMsg as EventListener);
}, []);
return null;
}Vue 3 example (src/main.ts):
import { createApp } from "vue";
import App from "./App.vue";
import { initJSentinel } from "@s4e/jsentinel";
initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" });
window.addEventListener(
"jsentinel",
(e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => {
const msg = e.detail;
if (msg.type === "alert") console.warn(msg);
},
);
createApp(App).mount("#app");Svelte example:
<script lang="ts">
import { onMount } from "svelte";
import { initJSentinel } from "@s4e/jsentinel";
onMount(() => {
initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" });
const onMsg = (e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => {
const msg = e.detail;
if (msg.type === "alert") console.warn(msg);
};
window.addEventListener("jsentinel", onMsg as EventListener);
return () => window.removeEventListener("jsentinel", onMsg as EventListener);
});
</script>Angular example:
import { Component, OnInit } from "@angular/core";
import { initJSentinel } from "@s4e/jsentinel";
@Component({ selector: "app-root", templateUrl: "./app.component.html" })
export class AppComponent implements OnInit {
ngOnInit(): void {
initJSentinel({ license: "<YOUR_LICENSE_TOKEN>" });
window.addEventListener(
"jsentinel",
(e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => {
const msg = e.detail;
if (msg.type === "alert") console.warn(msg);
},
);
}
}The important part is the typed event listener:
e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>;
Step 4 — Options
type JSentinelOptions = {
license: string; // REQUIRED
manifestPath?: string; // default: "/manifest.json"
trustedHosts?: string[]; // Allow specific external hosts (e.g., ["beacon-v2.helpscout.net", "https://cdn.example.com"])
features?: {
clipboard?: boolean; // default: true
headers?: boolean; // default: false
csp?: boolean; // default: true
network?: boolean; // default: false
extension?: boolean; // default: true (extension activity monitoring)
};
skipHostCheck?: boolean; // default: false
debug?: boolean; // default: false
};Configuration options:
license(required): Your JSentinel license token from [email protected]manifestPath(optional): Path to your manifest.json (default:/manifest.json)trustedHosts(optional): Whitelist specific external hosts whose scripts should not triggerunpinned-urlalerts. You can provide either just the hostname (e.g.,"beacon-v2.helpscout.net") or a full URL (e.g.,"https://scripts.simpleanalyticscdn.com"). This is useful for CDNs and third-party services you trust.features(optional): Enable/disable specific features
Step 5 — Events (what you’ll receive)
Listen to a single DOM CustomEvent("jsentinel") with typed payloads:
window.addEventListener(
"jsentinel",
(e: CustomEvent<import("@s4e/jsentinel").JSentinelMessage>) => {
const msg = e.detail;
// switch on msg.type
},
);ready: emitted after initialization and the first scan
- Payload:
{ version, scan: { total, alerts, verified } } - Example:
{ "type": "ready", "data": { "version": "0.2.0", "scan": { "total": 12, "alerts": 1, "verified": 11 } } }
- Payload:
alert: unified security alerts for all violations and suspicious behaviors (includes
userAgentin data)- Common reasons:
- Script integrity:
hash-mismatch,not-in-manifest,fetch-failed,volatile-content,unpinned-url,insecure-protocol,unable-to-hash - Extension monitoring:
suspicious-script-detected,excessive-form-access - Anti-tamper:
watchdog-timeout,wasm-verification-failed,native-api-modified - Clipboard:
clipboard-tamper,clipboard-permission-denied - CSP:
csp-violation
- Script integrity:
- Example (includes user-friendly description):
{ "type": "alert", "data": { "key": "/assets/app.js", "src": "/assets/app.js", "inline": false, "expected": "sha256-...", "got": "sha256-...", "severity": "error", "reason": "hash-mismatch", "description": "Script hash doesn't match manifest", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } - Example (same‑origin hash mismatch):
{ "type": "alert", "data": { "key": "/assets/app.js", "src": "/assets/app.js", "inline": false, "expected": "sha256-...", "got": "sha256-...", "severity": "error", "reason": "hash-mismatch", "description": "Script hash doesn't match manifest", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } - Example (inline not listed in manifest):
{ "type": "alert", "data": { "key": "inline", "src": null, "inline": true, "got": "sha256-...", "severity": "error", "reason": "not-in-manifest", "description": "Script not found in integrity manifest", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } } - Example (native API modification detected):
{ "type": "alert", "data": { "key": "native-api-tamper", "src": null, "inline": false, "severity": "error", "reason": "native-api-modified", "description": "Native API has been modified", "meta": { "modifiedAPIs": [ { "api": "EventTarget.prototype.addEventListener", "newSource": "function addEventListener() { /* extension modified */ }" }, { "api": "window.fetch", "newSource": "function fetch() { /* intercepted */ }" } ], "count": 2, "ts": 1710000000000 }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } }
- Common reasons:
network.request / network.response: fetch/XHR/beacon telemetry
- Example request:
{ "type": "network.request", "data": { "id": "k9x", "kind": "fetch", "url": "https://api.example.com/?***", "method": "GET", "ts": 1710000000000 } } - Example response:
{ "type": "network.response", "data": { "id": "k9x", "kind": "fetch", "url": "https://api.example.com/?***", "status": 200, "ok": true, "duration_ms": 132, "headers": { "content-type": "application/json" }, "ts": 1710000000123 } }
- Example request:
headers.checked / headers.error: minimal security headers snapshot or an error
- Example:
{ "type": "headers.checked", "data": { "url": "https://example.com/", "status": 200, "headers": { "content-security-policy": "default-src 'self'" }, "ts": 1710000000000 } }{ "type": "alert", "data": { "key": "csp-script-src", "src": "https://example.com/loader.js", "inline": false, "expected": undefined, "got": "https://example.com/loader.js", "severity": "error", "reason": "csp-violation", "description": "Content Security Policy violation detected", "meta": { "effectiveDirective": "script-src", "blockedURI": "https://example.com/loader.js", "ts": 1710000000000 }, "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..." } }
- Example:
headers.error: minimal security headers error
- Example:
{ "type": "headers.error", "data": { "message": "Failed to fetch headers", "ts": 1710000000000 } }
- Example:
error: library‑level error (e.g., license or init step)
- Example:
{ "type": "error", "data": { "stage": "license", "message": "LICENSE_DENIED" } }
- Example:
Manifest format (reference)
{
"version": "2024-07-01T12:34:56.000Z",
"scripts": [
{ "src": "/assets/app.js", "type": "same-origin", "hash": "sha256-..." },
{ "inline": "console.log('hi')", "type": "inline-stable", "hash": "sha256-..." },
{ "src": "https://cdn.example.com/[email protected]/index.js", "type": "versioned-cdn" }
]
}License
Commercial license required. Contact [email protected] for licensing information and pricing.
