Kiosks, Signage & Embedded

The device boots, and there is nobody there.

A checkout kiosk, a menu board, a ticket machine, a smart-TV app, an industrial HMI panel. Your JavaScript runs in a webview inside hardware you sold, in a location you will never visit, possibly with no network for months at a time. Every assumption behind a normal runtime licence check — a hostname, a real browser, a correct clock, a route back to your server — is either absent or unreliable.

Reality Check

location.hostname === ""

A page loaded from a local file has no hostname. The domain guard reads that as a failure and runs your lockout action — on every boot, on every device.

Four locks fail closedDomain, browser, OS and clock all misread an embedded target.
No hotfix pathVerification has to happen before the image is signed.
Per-device attributionA watermark names the unit a leaked image came from.
What Changes

Four things an embedded target takes away

Protection advice is written for a browser talking to a server. Ship the same JavaScript inside hardware and four of the load-bearing assumptions quietly stop holding.

1. There is no origin

Kiosk shells and signage players routinely load their UI from a local path or a custom scheme. There is no hostname, no origin, and nothing for a domain allowlist to compare against — which matters more than it sounds, as the next section shows.

2. There is no browser to identify

The runtime is an embedded engine with whatever user-agent string the integrator felt like setting. Detection logic written for Chrome, Edge, Firefox and Safari sees a string it cannot classify and has to decide what to do about it.

3. The clock is not authoritative

Devices lose power, sit in warehouses, and ship with dead real-time-clock batteries. Any check that reads local time is reading a value the person in front of the device can change with a settings menu.

4. You cannot ship a fix

Updates travel on a fleet schedule, through a customer’s change window, sometimes on a USB stick. A build that goes out wrong stays wrong for a very long time, which moves the whole cost of a mistake to before the image is signed.

The Important Part

The locks that fail closed on a device

These guards are designed to deny by default, which is correct on the web and surprising on hardware. Read this section before enabling any of them on a device image — each one can take a fleet down on first boot.

Domain lock — empty hostname counts as a mismatch

The injected guard reads location.hostname, lowercases it, and if the result is an empty string it runs your failure action and returns without ever consulting the allowed list. A locally loaded kiosk page hits that branch every single time. This is the single most common way a domain lock bricks an embedded fleet.

Browser lock — “unknown” is not on your list

The guard classifies the user agent into one of chrome, edge, firefox, safari, opera or ie. Anything else classifies as unknown, and unknown never appears in your allowlist, so the failure action runs. Custom signage user agents land here constantly.

OS lock — same trap, different list

Detection resolves to windows, macos, linux, android or ios. An Android-based player is usually fine; a stripped embedded Linux build with a custom agent often is not. The guard also fires when the user-agent platform and the reported platform disagree, which some device shells do by accident.

Date lock — works offline, trusts the device

This one genuinely functions with no network: it compares a YYYYMMDD value against a local new Date(). That is exactly why it is useful here, and exactly why it is not a boundary. It is honest expiry, enforced by a clock your customer administers.

The failure action itself is configurable — throw, blank the page, redirect, reload, invoke your callback, or degrade to a reduced mode. On a kiosk, degrade and callback are usually the only humane choices. A blanked screen on a wall-mounted display with no keyboard is an outage that nobody on site can explain, let alone fix.

One image, one build, reproducible
npm run build                          # player UI -> dist/

npx jso-protector dist/ \
  --out dist-protected/ \
  --preset maximum \
  --seed "$FIRMWARE_VERSION" \        # byte-identical rebuilds
  --lock-date 20271231 \              # offline expiry, device clock
  --watermark "unit:$SERIAL" \        # attribution, not prevention
  --watermark-key "$WM_KEY" \
  --report build/report.json \        # keep this. forever.
  --manifest build/manifest.json

npx jso-protector --verify-manifest build/manifest.json \
  --audit-source-maps                 # nothing leaked into the image

Verify before the image is signed

On the web, a bad release is a rollback and an apology. On a fleet it is a service visit per unit, so the verification you would normally do after deploying has to move in front of the signing step.

Three things do most of the work. A seed makes the protected output reproducible, so the image you sign can be rebuilt byte-for-byte from the same commit six months later when someone asks what exactly is running on the device. The manifest records a hash per output file, so you can prove the image contains what you built. And --audit-source-maps fails the build if a .map file or a sourceMappingURL comment survived, which is the difference between shipping protected code and shipping the source next to it on a device somebody can take apart.

Run your end-to-end suite against dist-protected/, not against dist/. A green pipeline that never executed the protected bytes is the failure mode described in testing obfuscated JavaScript, and on hardware it is expensive.

No Bundler Required

When the player is just an HTML file

A large share of signage and kiosk software is a folder of HTML with inline <script> blocks, dropped onto a device by an installer. There is no build step to hook into, which is usually where protection advice runs out.

Protect the script inside the markup

Mixed-file protection reads .html, .php, .asp, .aspx and .jsp files, protects the JavaScript inside them, and writes the surrounding markup back unchanged. The playlist page stays a playlist page; only the script between the tags changes. MixedServer is an Enterprise-tier option.

Or work from the folder itself

The Windows application and the CLI both operate on a directory tree, so a device image can be protected as a whole without introducing a bundler to a codebase that never had one. Protecting JavaScript without a build step covers this path in full.

After It Ships

Attribution and diagnostics for hardware in the field

Watermark per unit or per customer

An HMAC-SHA256 marker is prepended to each protected file, carrying a tag you choose and a signature only your key can produce. If a firmware image turns up on a forum or inside a competitor’s product, the tag names the unit or account it was issued to. Bulk-check a recovered image with --scan-watermarks. This is attribution after the fact, not prevention — a determined party can strip a header comment once they know it is there.

Keep every report you ever cut

Symbolication turns an obfuscated stack trace back into real names, but only against the report from that exact build. Devices produce the worst diagnostics in software — frequently a photo of a screen — so the report is often the only thing standing between a support ticket and a guess. Archive it alongside the firmware image, not in a build directory that CI reaps in thirty days.

Decide about the beacon deliberately

Runtime defense telemetry is opt-in via RuntimeDefenseBeaconUrl and silent unless configured. For an offline fleet, leave it unset. For a managed fleet, point it at a collector the customer controls. Then write down which you chose — an unexplained outbound request from a device is the fastest route to a failed network review.

Expect the security questionnaire

Enterprise buyers scan what you install on their network, and unreadable JavaScript on an appliance reads as an anomaly. Arrive with the manifest, the hashes and a named contact rather than improvising later — what to do when a scan flags your obfuscated code covers the whole conversation.

Frequently Asked

Embedded and kiosk protection questions, answered

Why does my domain lock fail on a kiosk that loads files locally?

Because there is no hostname to match. The injected guard reads location.hostname and lowercases it; when a page is loaded from a file:// path or from a custom scheme in an embedded webview, that value is an empty string. The guard treats an empty hostname as a failure and runs your configured action immediately, before it ever compares against the allowed list. A device that never touches a web origin will therefore trip the lock on every boot. Use date locking and per-device builds for offline hardware, and keep LockDomain for the browser deployments it was designed for.

Will the browser and OS locks work inside an embedded webview?

Often not, and they fail closed rather than open. Both guards classify the user-agent string against a fixed list — chrome, edge, firefox, safari, opera, ie for the browser lock; windows, macos, linux, android, ios for the OS lock. Signage players and kiosk shells frequently ship a custom or stripped user agent, which classifies as unknown, matches nothing in your allowed list, and runs the failure action. Test the guard on the actual hardware before shipping, because a device that bricks its own player on first boot is indistinguishable from a bad build.

Can I expire a licence on a device that has no network?

Yes, with the caveat that you are trusting the device clock. LockDate and LockStartDate take a YYYYMMDD value and compare it against a local new Date() on the device, so no server is involved and no network is needed. The trade is that a device without a reliable time source — one that lost power, has a dead RTC battery, or was deliberately set back — reports whatever the clock says. Treat the date lock as a contractual tripwire that makes overrun deliberate and provable, not as a technical boundary.

How do I protect JavaScript that is inline in a signage HTML file?

Use mixed-file protection, which reads .html, .php, .asp, .aspx and .jsp files, protects the script inside them, and writes the surrounding markup back untouched. That covers the common signage and kiosk layout where a playlist page carries its own inline script and there is no bundler in the picture at all. MixedServer is an Enterprise-tier option. If your device build does have a bundler, protect the build output instead and leave the markup alone.

Should protected device code send runtime telemetry back to me?

Only if you have decided it should and said so. The runtime defense beacon is opt-in through RuntimeDefenseBeaconUrl and sends nothing unless you set it. On shipped hardware an unexplained outbound request is a genuine problem: it fails customer network reviews, it breaks on air-gapped installs, and it can silently retry forever on a device with no route out. Either leave it unset for offline fleets or point it at a collector inside the customer’s own network, and document which you chose in the deployment notes.

How do I debug a crash on a device I cannot log into?

Keep the protection report for every firmware image you cut, labelled with the commit and the image version. Symbolication translates an obfuscated stack trace back to real names, but only against the report that produced that exact build — so if the report is gone, the trace is unreadable for good. Embedded fleets make this worse than usual, because a photograph of an error on a screen is sometimes the only evidence you will ever receive.

Does a firmware image need a different protection setup from a web app?

The transforms are the same; the release discipline is not. A device image cannot be hotfixed on your schedule, so everything you would normally verify after deploying has to happen before the image is signed. Build with a seed so the artifact is reproducible, keep the manifest and its per-file hashes, run the end-to-end suite against the protected output rather than the source, and verify no source maps survived. A web app with a bad release is a rollback; a device fleet with a bad release is a truck roll.

Is obfuscation worth it if someone can just image the flash?

Physical access does defeat confidentiality of the storage — that is true of every device ever shipped, and no client-side control changes it. What protection changes is cost. Pulling an image is one step; turning a protected bundle back into a maintainable codebase someone can fork and sell is a much longer one, and the watermark means the copy they pulled still identifies itself. Set expectations accordingly and see is obfuscation reversible.

Related Guides

Protecting other JavaScript targets

Self-hosted & on-premises

The customer runs your code on their servers — per-customer builds, offline expiry and signed attestation.

Tauri apps

The one popular desktop stack with a compiled, offline, on-device place to move authority to.

No build step

Script tags, legacy pages and folders of files, protected without introducing a bundler.

HTML5 games

Packaged players, cheat resistance and the parts of a game bundle worth the overhead.

Cordova & Ionic

Hybrid mobile apps where the JavaScript sits in an archive anyone can unpack.

Widgets & SDKs

Code that runs on somebody else’s page, where domain locking is the primary control.

Runtime defense

The full set of guards, failure actions and telemetry options behind every lock on this page.