Widgets & SDKs

Your widget runs on pages you don’t control, for people who could become competitors.

Every other kind of JavaScript is read by strangers who stumbled across it. An embeddable widget is handed deliberately to the companies closest to your market — integrators, agencies, and the customer who is quietly evaluating whether to build it themselves. It is also the one script whose size, CSP behavior and failure modes are judged by someone else’s standards.

Reality Check

<script src="//cdn.acme.io/widget.js">

One URL, cached by every customer, readable by all of them, and impossible to recall once copied.

Domain lockA copied file refuses to run on an origin you did not authorise.
WatermarkA signed marker proves which build a stray file came from.
Reserved surfaceYour documented API is a contract with code you cannot see.
Threat Model

Four distinct problems, not one

“Protect the widget” bundles together threats that want different answers. Separating them is most of the work, because the control that solves one does nothing for the others.

1. Reimplementation

A competitor reads your bundle to learn how the product works — the scoring model, the layout engine, the rules you spent two years tuning. Obfuscation is the direct answer here, and it is the classic case.

2. Self-hosting

A customer saves widget.js, serves it from their own CDN, and stops paying — or keeps using it after churning. Obfuscation does nothing about this. Domain locking does.

3. Tampering

Someone edits your file to remove a limit, spoof a plan, or fake a result before your API ever sees the request. That is a runtime problem, answered by integrity checks and server-side authority.

4. Attribution

Your code turns up somewhere it should not be and you need to prove it. Nothing prevents this; watermarking makes it provable afterwards.

A widget release, protected
npm run build                        # bundle -> dist/widget.js

npx jso-protector \
  --config jso.config.json \
  --label "$GIT_SHA" \
  --watermark "acme-widget-$RELEASE" \
  --max-growth-ratio 3 \
  --manifest dist-protected/jso-manifest.json \
  --report   dist-protected/jso-report.json

# keep the report - it is the only way to read
# a stack trace from a customer's page later
The Constraints

You are a guest on that page

  • Size is judged. Customers measure third-party scripts against their performance budget. Cap growth in the build rather than discovering it in a support thread.
  • The CSP is theirs. Ship output that runs under a strict policy; you cannot ask hundreds of customers to add unsafe-eval.
  • You cannot debug in place. No console access, no repro. Reports and symbolication are your only diagnostics.
  • Your public API is a contract. Reserve it deliberately; the calling code is on a page you have never seen.
  • Nothing client-side is authoritative. Entitlements are decided by your server, always.
Domain Locking

The control that makes a copy useless

This is the one protection genuinely specific to distributed scripts. The engine prepends a guard that reads location.hostname, lowercases it, and compares it against the list of origins you authorised — before any of your program runs.

Subdomains, on request

Match acme.com exactly, or opt in to accepting any subdomain of it. Customers with app., staging. and checkout. hosts need the latter, so ask during onboarding rather than after the incident.

Choose the failure

An unauthorised origin can throw, blank the page, redirect, reload, invoke a callback, or degrade. For a widget, degrading or a quiet callback beats blanking — you do not want a misconfigured customer’s page destroyed by your script.

No location, no run

The guard treats a missing location as a failure, so the file will not simply execute under a plain script runtime. It is a real barrier to casual lifting, not a barrier to a determined one.

Per-customer builds make the lock stronger and the leak attributable: a file that names exactly one origin can only have come from one account. The cost is a build and cache matrix that grows with your customer list, and a caching story that needs deliberate handling. A shared build listing many domains is the pragmatic middle, and is where most teams should start. Note the honest limit throughout: domain locking stops the file running elsewhere, never the file being read. Someone who wants your algorithm is not blocked by it.

What To Reserve

Every name a host page can touch

A widget has a larger public surface than almost any other artifact, and none of the calling code is visible to the tool. Renaming inside your own modules is free; renaming anything on this list is an outage on someone else’s site.

The global and its methods

window.AcmeWidget and every documented method on it — init, open, destroy, on. Also the command-queue pattern, if you use one: the array name and the shape of its queued entries are both public.

Options object keys

Every key a customer writes in the snippet you documented. These are object literals in their code, so nothing in your bundle references them by that name until runtime.

Callback argument fields

If you hand a customer an object and they read result.status, that field name is a contract. Callbacks are the surface people forget, because the names appear only where the object is built.

Data attributes

data-acme-key on the embed tag is read as a string from markup you do not own. The attribute names and any values you match against are contracts.

Custom events

Event names and every field of the detail object customers subscribe to. The web components article covers the full list of platform-owned names if you ship a custom element.

CSS hooks

Class names, part names and CSS custom properties customers style against. They live in strings and stylesheets, but anything your JavaScript builds dynamically must still be reserved.

Layered Defense

Protect the part worth protecting

Obfuscation — the comprehension layer

String protection, renamed internals and flattened control flow over your algorithm, not your plumbing. What survives a formatter is what counts — see minification vs obfuscation.

VM protection — for one function

VM bytecode for the licensing check or the proprietary core. It is slower than native JavaScript, so scope it narrowly — never to a render or scroll path on a customer’s page.

Runtime defense — the behavior layer

Integrity checks notice a tampered bundle. Choose the action carefully: a widget that throws inside a customer’s checkout is a worse outcome than the tampering.

Operations

Shipping to pages you cannot see

Keep every report

Write --report and --manifest for each release and archive them with a --label carrying the commit. A protected stack trace from a customer page is unreadable without the matching report, and you will not get a second chance to capture one.

Watermark and sweep

A signed HMAC marker with a per-release tag lets you walk a CDN mirror or a suspect directory and list every file carrying it — answering both “is our marker still in production?” and “which build is this stray file?”

Mind the integrity attribute

Security-conscious customers pin your script with Subresource Integrity. Polymorphic output changes the hash on every build, so publish the hash from the manifest with each release, or version the URL.

Budget the size

Set --max-growth-ratio and --max-output-bytes so a configuration change cannot quietly triple the file every customer loads. Failing the build is cheaper than failing a performance review.

Start Now

Read your widget the way an integrator would

Open the URL you give customers, save the file, and run it through a formatter. Everything you can read is what an evaluating competitor reads on day one of their build-versus-buy decision. Then paste it into the online obfuscator and compare.

Frequently Asked

Widget and SDK protection questions, answered

Can I stop a customer from self-hosting my widget?

Domain locking is the control for this. The engine injects a guard that reads location.hostname before your program starts, compares it against an allowed list you supply, optionally accepts subdomains, and takes a configured action - throw, blank, redirect, reload, callback or degrade - when there is no match. A copied file therefore does not run on an origin you did not authorise.

Does domain locking mean a build per customer?

Not necessarily. The allowed list accepts multiple domains, so a shared build can carry every authorised origin. Per-customer builds are the stronger option because a leaked file names only one customer, which makes the leak attributable, but they multiply your build and cache matrix.

Which names in my widget must not be renamed?

Everything a host page touches: the global you attach yourself to, every documented method, every key in the options object customers pass you, the fields of any callback argument, the data attribute names you read off the embed tag, and any custom event names and detail fields you dispatch. All of these are matched as strings from code you do not control.

Will obfuscation break my customers' Content Security Policy?

Not by default. Most transforms emit ordinary JavaScript that runs under a strict policy. Only self compression and the eval variants of code reordering produce eval-based output, which needs unsafe-eval. Leave those off for a widget, because you cannot ask every customer to loosen their policy for you.

How do I debug a protected widget failing on a customer site?

Save the protection report for every release and use symbolication to translate the obfuscated stack traces your error reporting collects back into real names. Without a retained report per release you cannot read production traces, and on a customer's page you have no other way in.

Can I prove a competitor copied my widget?

Watermarking embeds an HMAC-SHA256 marker carrying a tag you choose. The tag is visible in the output and the signature proves the file was produced with your key. You can walk a directory or CDN mirror and list every file carrying your marker, which turns a suspicion into evidence about which build a file came from.

Related Guides

Protecting other JavaScript targets

Same protection engine, different distribution model. These guides cover the cases closest to this one:

Does obfuscation break UMD and module wrappers? · Obfuscation and cross-site cookie access · Document viewers · Your postMessage protocol is a public API · White-label and OEM redistribution · Published npm packages · Can you lock JavaScript to a device? · Web components · License checks · Browser extensions · WordPress plugins · Protecting JavaScript (overview) · Framing and clickjacking · Obfuscating ad tech JavaScript · Marketplace add-ins and plugins