LLM-Ready Documentation

Patchstack Vulnerability
Disclosure Widget

A self-contained JavaScript widget that adds a vulnerability report form to any website. Shadow DOM isolation, zero dependencies, two lines to integrate.

📦
Package
patchstack-widget.js
🌐
Global
window.PatchstackWidget
🔌
Entry Point
.init(config)
🛠
Modes
floating · embed

Quick Start

Add these two snippets before the closing </body> tag:

html Minimal floating widget
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script> <script> PatchstackWidget.init({ userToken: 'your-public-token' }); </script>
That's it — a floating button appears in the bottom-right corner of the page. No build step required.

🔒 Pinning & Integrity Verification

patchstack-widget.js is a mutable URL: Patchstack ships fixes to it, and every site embedding it picks them up automatically (cache TTL 5 minutes). Every deploy also publishes an immutable pinned artifact and its integrity hash, resolvable from a manifest:

json https://cdn.patchstack.com/widget-manifest.json
{ "latest": "https://cdn.patchstack.com/patchstack-widget.js", "pinned": { "version": "<git short SHA>", "url": "https://cdn.patchstack.com/patchstack-widget.<version>.js", "integrity": "sha384-…" }, "generatedAt": "<ISO 8601>" }

Auditing what the CDN serves: fetch the pinned URL, hash it (openssl dgst -sha384 -binary | openssl base64 -A), and compare with the manifest's integrity value. The pinned artifact is served with cache-control: immutable and never changes after publish.

Pinning in the script tag: if your policy requires it, reference the pinned URL instead of the mutable one. The tradeoff is real: a pinned site does not receive widget updates — including security fixes — until you update the tag yourself. The mutable URL is the default for exactly that reason.

Adding the integrity attribute to the <script> tag also requires crossorigin="anonymous" and CORS headers (Access-Control-Allow-Origin) from the CDN — confirm the CDN sends them before adding the attribute, or the browser will refuse to run the script.

📦 Frontend Install (AI-Agent Prompt)

Installing the widget into an existing frontend project (Next.js, TanStack Start, Vite SPA, etc.)? Read this section first. It exists because env-var-based installs silently break in production builds.

1. The one-liner install (recommended)

Drop one <script> tag into your root HTML shell with data-site-uuid. That's the entire install — no init() call, no JSON import, no useEffect:

html One-liner — auto-init via data attribute
<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="YOUR_SITE_UUID" defer ></script>

The widget reads the attribute off its own <script> tag and auto-initialises on DOMContentLoaded. If you later call PatchstackWidget.init() manually, the auto-init is skipped — the explicit call always wins.

2. Optional data attributes

All of these are optional; supply them on the same <script> tag to override defaults:

Attribute Maps to Example
data-site-uuid userToken data-site-uuid="abc-123" required
data-mode mode data-mode="embed"
data-el el (CSS selector) data-el="#vuln-report"
data-position position data-position="bottom-left"
data-offset-x offsetX data-offset-x="32"
data-offset-y offsetY data-offset-y="32"
data-accent-color accentColor data-accent-color="#4a3aff"
data-terms-url termsUrl data-terms-url="https://…/terms"
data-scope-url scopeUrl data-scope-url="https://…/scope"
data-scope-note scopeNote data-scope-note="This site only"
data-plugin-report-url pluginReportUrl data-plugin-report-url="https://…"
data-build-mode buildMode data-build-mode="false" on by default
data-api-base apiBaseUrl data-api-base="https://…" testing only

Site onboarding (data-build-mode, on by default): while the site is unclaimed the widget shows a one-time “connect this website” panel (social login or e-mail/password) instead of the report button; once the site is claimed it never appears again. The install UUID is public and claiming is first-come, so until the site is claimed this panel is visible to any visitor — connect promptly, or set data-build-mode="false" to disable it and always show the report widget.

Production builds hide the connect/login UI — owners log in via #patchstack. When you wire up @patchstack/connect (see below), its postbuild mark-build hook injects window.__PATCHSTACK_PROD__ = true into the built HTML. On that published site the widget shows the report form only — the connect/claim panel and the owner “Log in” link are hidden from ordinary visitors. The site owner reaches login on the live site with a discreet hash: open the URL with #patchstack appended — e.g. https://example.com/#patchstack (?patchstack also works) — and the widget reveals the Log in → vulnerability dashboard flow. Claiming itself is meant to happen in the builder’s edit preview, where the flag is absent and the connect panel shows by default. Owners can also use app.patchstack.com.

vulnerabilityTypes (an array of objects) is not supported via data attributes — use programmatic init() for that.

3. Where to put the script tag

Framework File
TanStack Start src/routes/__root.tsx — inside <body>
Next.js (app dir) app/layout.tsx — inside <body>
Vite SPA index.html — before </body>
Static HTML / CMS Anywhere before </body>

4. Provisioning the UUID with @patchstack/connect

Add @patchstack/connect as a dev dependency and wire it into package.json so the site is provisioned at build time:

json package.json
{ "scripts": { "prebuild": "patchstack-connect scan" }, "devDependencies": { "@patchstack/connect": "^1.0.0" } }

The first run provisions a site on Patchstack and writes the UUID to .patchstackrc.json in the repo root. Commit that file — it's the source of truth for the UUID across every environment.

If you'd rather not hard-code the UUID into the script tag, import it from .patchstackrc.json and interpolate it:

tsx Interpolated from .patchstackrc.json
import patchstackConfig from "../../.patchstackrc.json"; // Inside <body> of your root shell: <script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid={patchstackConfig.siteUuid} defer />

Ensure tsconfig.json has "resolveJsonModule": true (default in most modern setups).

5. Do NOT read the UUID from an env var

ts ❌ Wrong — silently breaks in prod
// Do not do any of these: PatchstackWidget.init({ userToken: import.meta.env.VITE_PATCHSTACK_SITE_UUID }); PatchstackWidget.init({ userToken: process.env.NEXT_PUBLIC_PATCHSTACK_SITE_UUID }); PatchstackWidget.init({ userToken: process.env.PATCHSTACK_SITE_UUID });
Why env vars are the wrong default here:
  • Preview and production are separate build environments. Env vars set in preview (e.g. Lovable, Vercel preview) are routinely missing from production builds, so the widget renders in preview but disappears in prod.
  • .patchstackrc.json is committed to the repo, so importing it (or hard-coding the UUID into the script tag) works identically in preview, prod, CI, and any host.
  • The site UUID is not a secret — it's a public site identifier (like a Plausible site ID or a GA tracking ID). Bundling it client-side is correct.
  • If userToken is missing at init() time the widget logs a clear console.warn with a link back to these instructions.

Configuration Reference

PatchstackWidget.init(config) accepts a single configuration object:

Property Type Default Description
userToken string Public token sent as UserToken header with every API request. required
mode 'floating' | 'embed' 'floating' Display mode. floating: FAB + popup panel. embed: renders inside a container. optional
el string | Element CSS selector or DOM element to mount into (embed mode only). optional
position 'bottom-right' | 'bottom-left' 'bottom-right' Screen corner for the floating button (floating mode only). optional
offsetX number 24 Horizontal distance from the edge in pixels. optional
offsetY number 24 Vertical distance from the bottom in pixels. optional
accentColor string '#afe614' Primary accent colour for buttons and links (hex). optional
termsUrl string Patchstack T&C URL to your custom Terms & Conditions page. optional
scopeUrl string Patchstack scope URL to your custom "What's in scope" page. optional
scopeNote string Short note shown under the form title clarifying what should be reported here. optional
pluginReportUrl string Destination for third-party plugin/theme (off-scope) reports. When set, the widget first asks what is being reported and routes off-scope reports here instead of this site's VDP. optional
vulnerabilityTypes Array<{label, value}> 14 built-in types Custom list of vulnerability types for the dropdown. Each item needs label (display text) and value (submitted value). optional

Floating Mode

Shows a small floating action button (FAB). Clicking it opens the vulnerability report panel. Styles are fully encapsulated in Shadow DOM.

Minimal

html Floating — minimal
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script> <script> PatchstackWidget.init({ userToken: 'your-public-token' }); </script>

Custom position & colour

html Floating — custom position & accent
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script> <script> PatchstackWidget.init({ userToken: 'your-public-token', position: 'bottom-left', offsetX: 32, offsetY: 32, accentColor: '#16a34a' }); </script>

Embed Mode

Renders the report form directly inside a container element — no floating button. Ideal for dedicated vulnerability disclosure pages.

html Embed — inline form
<!-- 1. Add a container anywhere on your page --> <div id="vuln-report"></div> <!-- 2. Load and initialise in embed mode --> <script src="https://cdn.patchstack.com/patchstack-widget.js"></script> <script> PatchstackWidget.init({ userToken: 'your-public-token', mode: 'embed', el: '#vuln-report' }); </script>
The el property accepts either a CSS selector string (e.g. '#vuln-report') or a direct DOM element reference.

Full Example

A complete configuration showing every available property:

html Full configuration — all options
<div id="vuln-report"></div> <script src="https://cdn.patchstack.com/patchstack-widget.js"></script> <script> var widget = PatchstackWidget.init({ // ── Required ───────────────────────────────────────── userToken: 'your-public-token', // ── Display mode ───────────────────────────────────── mode: 'floating', // 'floating' | 'embed' el: '#vuln-report', // CSS selector (embed mode only) // ── Position (floating mode only) ──────────────────── position: 'bottom-right', // 'bottom-right' | 'bottom-left' offsetX: 24, // pixels from the edge offsetY: 24, // pixels from the bottom // ── Theming ────────────────────────────────────────── accentColor: '#4a3aff', // ── Links ──────────────────────────────────────────── termsUrl: 'https://example.com/terms', scopeUrl: 'https://example.com/scope', scopeNote: 'Report security issues affecting this website only.', pluginReportUrl: 'https://patchstack.com/database/report', // off-scope plugin/theme reports // ── Custom vulnerability types ─────────────────────── vulnerabilityTypes: [ { label: 'XSS (Cross-Site Scripting)', value: 'xss' }, { label: 'SQL Injection', value: 'sqli' }, { label: 'CSRF', value: 'csrf' }, { label: 'Authentication Bypass', value: 'auth-bypass' }, { label: 'Privilege Escalation', value: 'priv-esc' }, { label: 'Remote Code Execution', value: 'rce' }, { label: 'Information Disclosure', value: 'info-disc' }, { label: 'IDOR', value: 'idor' }, { label: 'SSRF', value: 'ssrf' }, { label: 'Broken Access Control', value: 'broken-access' }, { label: 'File Upload Vulnerability', value: 'file-upload' }, { label: 'Path Traversal', value: 'path-traversal'}, { label: 'Open Redirect', value: 'open-redirect' }, { label: 'Other', value: 'other' } ] }); // To remove the widget later: // widget.destroy(); </script>

📄 Report Payload & Data Model

When a reporter submits the form, the widget POSTs a multipart/form-data request to {apiBaseUrl}/vdp/submit (multipart so file attachments ride along). Field names on the wire are snake_case; site_uuid (your userToken) is attached automatically.

Field Type Notes
site_uuid string Your userToken, added automatically. required
affected_url string URL where the vulnerability was found. required
vulnerability_type string One of the vulnerabilityTypes values (e.g. xss, sqli, other). required
description string Free-text description of the issue. required
cvss_severity 'low' | 'medium' | 'high' | 'critical' Reporter's CVSS severity estimate. optional
steps_to_reproduce string Reproduction steps. optional
files File[] Evidence attachments (screenshots, PoC files). optional
submitter_name string Reporter's name. optional
submitter_email string Reporter's contact e-mail. optional
accepted_terms boolean Reporter accepted the disclosure T&C. required

Spam protection (honeypot)

Every submission also carries an invisible honeypot, verified server-side. Three layers work together: a hidden website field that must stay empty (bots fill it, humans never see it), a render→submit timing check that rejects submissions faster than a human could type (default 3 s), and an obfuscated field name so scrapers don't skip it. The extra fields are hp_field, hp_rendered_at, and hp_submitted_at.

On HTTP 422 the API returns a Laravel error bag { errors: { field: [msg, …] } }; the widget maps snake_case field names back to the form and shows the first message per field inline.

📡 API Endpoints

All endpoints are relative to apiBaseUrl (default https://api.patchstack.com). The report endpoint sits at the API root; everything else lives under /monitor. The public site UUID is the only credential for reporting and claiming; owner-dashboard calls use a short-lived Site-Token (JWT, ~1 h) in the Site-Token header.

Endpoint Purpose
POST /vdp/submit Submit a vulnerability report (multipart). See Report Payload.
GET /monitor/claim/preview?site={uuid} Claim state: invalid · not-found · claimable · owned-by-you · owned-by-other.
POST /monitor/claim/register Create a free account, sign in, and claim the site in one call.
POST /monitor/claim/login Sign in with an existing account and claim the site.
POST /monitor/claim Claim with the current session (used after OAuth sign-in).
POST /monitor/widget/login Verify credentials & report ownership. Owner gets { owner, code, site_id }. Never claims.
POST /monitor/widget/token Exchange the one-time code for a Site-Token.
GET /monitor/widget/dashboard Owner dashboard (status, stats, VDP report counts). Auth: Site-Token header.
POST /monitor/widget/sync Re-sync this site's domain. Auth: Site-Token header.
POST /monitor/pulse/heartbeat Manifest-parity heartbeat. See Heartbeat.
The API returns Access-Control-Allow-Origin: *, so report and claim requests are sent without credentials (cookies). Social sign-in is available for the claim & login flows via google, github, and linkedin.

🔑 Owner Login & Dashboard

On a production build the connect/claim panel and the owner “Log in” link are hidden from ordinary visitors. The site owner reveals the Log in → vulnerability dashboard flow on the live site with a discreet URL marker:

  • https://example.com/#patchstack
  • https://example.com/#patchstack-login
  • https://example.com/?patchstack

Logging in returns a one-time code that is exchanged for a Site-Token (a JWT, ~1 h, carrying its own exp). The token is cached in localStorage under ps_widget_session_{siteUuid} so the owner stays signed in across panel re-opens and page reloads, and is dropped as soon as it expires. Owners can also manage everything at app.patchstack.com.

localStorage keys

The widget writes these keys on the host origin; all degrade gracefully (in-memory / no-op) when storage is blocked:

KeyPurpose
ps_onboarded_{siteUuid} Set once the site is claimed/onboarded so the connect panel never shows again.
ps_widget_session_{siteUuid} Cached owner Site-Token session (token + siteId + expiry).
__patchstackHeartbeat:v1:… Timestamp of the last manifest heartbeat, for hourly throttling.

💓 Manifest-Parity Heartbeat

After init() the widget fires a fire-and-forget heartbeat so Patchstack can (a) confirm the site is still live and (b) compare the running build against the last manifest @patchstack/connect scanned (“manifest parity”).

  • Production-only — runs only when the connector injected window.__PATCHSTACK_PROD__; never in dev or the builder edit preview.
  • Throttled — at most once per hour per (apiBase, siteUuid, checksum).
  • Non-blocking — never blocks the page, never throws, never surfaces errors.
  • UUID-only — the public site UUID is the only credential.
http POST {apiBaseUrl}/monitor/pulse/heartbeat
POST /monitor/pulse/heartbeat Content-Type: application/json { "site_uuid": "YOUR_SITE_UUID", "checksum": "<window.__PATCHSTACK_BUILD__ fingerprint, or null>" }

Cleanup / Destroy

PatchstackWidget.init() returns an object with a destroy() method. Call it to cleanly remove the widget:

js Remove the widget
var widget = PatchstackWidget.init({ userToken: 'your-public-token' }); // Later, when you want to remove it: widget.destroy();

How It Works

  • Shadow DOM — styles never leak in or out of the widget
  • Single IIFE bundle — zero external runtime dependencies
  • Scoped CSS — your page styles won't break the widget
  • Global exportwindow.PatchstackWidget

Integration Patterns

WordPress

Add the snippet to your theme's footer.php or use a plugin that injects scripts before </body>:

html WordPress
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script> <script> PatchstackWidget.init({ userToken: 'your-public-token' }); </script>

React / Next.js

tsx React / Next.js component
// components/PatchstackWidget.tsx import { useEffect } from 'react'; export default function PatchstackWidget() { useEffect(() => { const script = document.createElement('script'); script.src = 'https://cdn.patchstack.com/patchstack-widget.js'; script.onload = () => { window.PatchstackWidget.init({ userToken: 'your-public-token' }); }; document.body.appendChild(script); return () => { script.remove(); }; }, []); return null; }

Vue / Nuxt 3

ts Nuxt 3 client plugin
// plugins/patchstack-widget.client.ts (Nuxt 3) export default defineNuxtPlugin(() => { const script = document.createElement('script'); script.src = 'https://cdn.patchstack.com/patchstack-widget.js'; script.onload = () => { window.PatchstackWidget.init({ userToken: 'your-public-token' }); }; document.body.appendChild(script); });

Static HTML / Any CMS

Paste the script tags directly before </body> — works with Squarespace, Wix, Webflow, Ghost, Jekyll, Hugo, etc.

html Static HTML / CMS
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script> <script> PatchstackWidget.init({ userToken: 'your-public-token' }); </script>

Single-Page App (deferred load)

js SPA — Promise-based loader
function loadPatchstackWidget(token) { return new Promise(function (resolve, reject) { var script = document.createElement('script'); script.src = 'https://cdn.patchstack.com/patchstack-widget.js'; script.onload = function () { var widget = window.PatchstackWidget.init({ userToken: token }); resolve(widget); }; script.onerror = reject; document.body.appendChild(script); }); } // Usage: loadPatchstackWidget('your-public-token').then(function (widget) { console.log('Widget loaded'); });

Troubleshooting

Widget doesn't appear
Check that the script loaded (no 404 in DevTools Network tab). Ensure PatchstackWidget.init() is called after the script loads.
Embed mode shows nothing
Make sure the el container exists in the DOM before calling init(). Place the <script> after the container element.
Styles look broken
The widget uses Shadow DOM — your page styles can't affect it. If something looks off, check for Content Security Policy (CSP) rules blocking inline styles.
Form submission fails
Verify that your userToken is valid and the API endpoint is reachable. Check the browser console for specific error messages.