Content-Security-Policy
What a strict CSP must allow for the Userz browser SDK, per release channel — and what it does not have to allow.
script-src, the API host and the upload host in connect-src, and blob: + data: in img-src. Styles need no 'unsafe-inline' and no nonce. Which hosts depends on the release channel your build points at, never on a URL you type.Hosts per release channel
The SDK resolves its CDN and API hosts from the release channel it was built for. A production build (the npm package and the cdn.userz.ai script tag) talks to the .ai hosts; a staging build talks to the .net hosts. There is no option to point a build at another host — the channel is the only switch, so a CSP written against these rows stays true.
| Name | Type | Description |
|---|---|---|
| production | https://cdn.userz.ai | script-src https://cdn.userz.ai · connect-src https://api.userz.ai |
| staging | https://cdn.userz.net | script-src https://cdn.userz.net · connect-src https://api.userz.net |
Production is where customer sites run. The staging row is here so a staging build under a strict CSP is explainable, not because a customer site should ever load one.
The policy, production
A complete policy for a page that embeds the SDK from the CDN. Merge each directive into the one you already send; nothing below needs to be the whole directive.
Content-Security-Policy:
script-src 'self' https://cdn.userz.ai;
connect-src 'self' https://api.userz.ai https://*.r2.cloudflarestorage.com;
img-src 'self' blob: data:;| Name | Type | Description |
|---|---|---|
| script-src | https://cdn.userz.ai | The script-tag core and every chunk it fetches (widget, telemetry, replay) come from this host at an exact-version URL with a Subresource Integrity digest and crossorigin="anonymous". An npm install fetches no remote code, so a bundled app needs no entry here. |
| connect-src (API) | https://api.userz.ai | Feedback submissions, telemetry batches (fetch, keepalive fetch, and the sendBeacon fallback all share this directive), the configuration document, and presign requests. |
| connect-src (uploads) | https://*.r2.cloudflarestorage.com | Screenshots, annotated images, files and replay clips are uploaded straight from the page to a presigned object-storage URL on this host. The URL is the access gate (it expires in minutes); the host is a per-account subdomain, so the pattern is a wildcard. |
| img-src | blob: data: | Screenshot previews in the panel are blob: URLs; screenshot capture rasterises the page through an SVG data: image before it is cropped. Omit data: and capture fails; omit blob: and the preview is blank. The capture adds no style-src requirement: it works under style-src 'self' without a single violation. |
The policy, staging
Identical shape, the .net hosts. Only a staging build of the SDK reaches them.
Content-Security-Policy:
script-src 'self' https://cdn.userz.net;
connect-src 'self' https://api.userz.net https://*.r2.cloudflarestorage.com;
img-src 'self' blob: data:;Styles: no unsafe-inline, no nonce
The widget renders inside an open shadow root and delivers its stylesheet as a constructed stylesheet (new CSSStyleSheet() adopted onto the root). Measured on Chromium under style-src 'self': the sheet applies and the page receives zero securitypolicyviolation events, while a <style> element placed beside it is refused. Brand colours are written as CSS custom properties through the CSSOM (style.setProperty), which is not a CSP sink either. So a strict style-src needs no 'unsafe-inline' and no nonce for the widget.
Where constructed stylesheets are unavailable
The SDK falls back to a <style> element that carries a nonce. Pass your page's nonce explicitly, or call createUserz from a nonced inline snippet — the SDK reads the nonce of the script that called it.
createUserz({
publicKey: 'pub_...',
csp: { nonce: document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ?? undefined },
});Trusted Types
Under require-trusted-types-for 'script' the chunk loader assigns script URLs through a policy named userz. If your policy also restricts policy names, allow it:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types userz;What you do not need
'unsafe-inline'or'unsafe-eval'inscript-src: the SDK injects no inline script and evaluates no string.worker-src: compression at page teardown is synchronous, in the page.- A wildcard
*.userz.ai: the two hosts above are the whole surface. - A separate entry for telemetry: it ships in the same package and posts to the same API host as feedback.
Verifying
Deploy the policy, open your app, and watch the browser console for Refused to … lines naming a Userz host — each names the directive to extend. A blocked chunk fetch surfaces as one console.error from the SDK naming the chunk; the rest of the widget keeps working.