## @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](https://www.npmjs.com/package/@s4e/jsentinel-cli)

```bash
npm i -D @s4e/jsentinel-cli
```

Create `jsentinel.config.json` for clean CI usage (with schema IntelliSense):

Obtain your dashboard API token (`s4eToken`) via sales@s4e.io.

```json
{
  "$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:

```bash
npx @s4e/jsentinel-cli jsentinel manifest -c jsentinel.config.json
```

This writes `public/manifest.json`, which the runtime reads by default.

### Step 2 — Install the runtime

```bash
npm i @s4e/jsentinel
# yarn add @s4e/jsentinel
# pnpm add @s4e/jsentinel
# bun add @s4e/jsentinel
```

### Step 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 sales@s4e.io). You can initialize in any client‑only file, page, or component across frameworks.

React example:

```tsx
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`):

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

```svelte
<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:

```ts
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

```ts
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 sales@s4e.io
- `manifestPath` (optional): Path to your manifest.json (default: `/manifest.json`)
- `trustedHosts` (optional): Whitelist specific external hosts whose scripts should not trigger `unpinned-url` alerts. 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:

```ts
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:
    ```json
    {
      "type": "ready",
      "data": { "version": "0.2.0", "scan": { "total": 12, "alerts": 1, "verified": 11 } }
    }
    ```

- **alert**: unified security alerts for all violations and suspicious behaviors (includes `userAgent` in 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`
  - Example (includes user-friendly description):
    ```json
    {
      "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):
    ```json
    {
      "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):
    ```json
    {
      "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):
    ```json
    {
      "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..."
      }
    }
    ```

- **network.request** / **network.response**: fetch/XHR/beacon telemetry
  - Example request:
    ```json
    {
      "type": "network.request",
      "data": {
        "id": "k9x",
        "kind": "fetch",
        "url": "https://api.example.com/?***",
        "method": "GET",
        "ts": 1710000000000
      }
    }
    ```
  - Example response:
    ```json
    {
      "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
      }
    }
    ```

- **headers.checked** / **headers.error**: minimal security headers snapshot or an error
  - Example:
    ```json
    {
      "type": "headers.checked",
      "data": {
        "url": "https://example.com/",
        "status": 200,
        "headers": { "content-security-policy": "default-src 'self'" },
        "ts": 1710000000000
      }
    }
    ```
    ```json
    {
      "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..."
      }
    }
    ```

- **headers.error**: minimal security headers error
  - Example:
    ```json
    {
      "type": "headers.error",
      "data": {
        "message": "Failed to fetch headers",
        "ts": 1710000000000
      }
    }
    ```

- **error**: library‑level error (e.g., license or init step)
  - Example:
    ```json
    {
      "type": "error",
      "data": { "stage": "license", "message": "LICENSE_DENIED" }
    }
    ```

### Manifest format (reference)

```json
{
  "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/pkg@1.2.3/index.js", "type": "versioned-cdn" }
  ]
}
```

### License

Commercial license required. Contact sales@s4e.io for licensing information and pricing.
