What The Guards Actually Read

Can you lock JavaScript to a device?

It is one of the most common questions that arrives about the lock options, and it usually arrives with a licensing plan already attached to the answer. The short version is that you can express a deployment expectation and detect when it is violated, which is worth having, but you cannot bind delivered code to a machine from inside that machine. The interesting part is why, and what the generated guards are actually doing instead, because both are more specific than the documentation summary suggests.

Everything a page can read, the page was told

A script running in a browser has no privileged view of the computer it is running on. It has an interface to an environment, and every fact that interface exposes about the machine is a value the environment chose to report. The hostname comes from the location object. The browser identity and the platform come from the navigator object. The screen dimensions, the language, the time-zone offset and the touch-point count come from the same place. None of these is measured by your code, and all of them are supplied by the software you are trying to evaluate.

That is the whole difficulty in one sentence, and it is not a defect in any particular product. Binding requires an asymmetry: the party being checked holds something they can use but cannot read or move, and a verifier elsewhere can tell a genuine response from a fabricated one. A page holds nothing of the kind. Whatever it examines, it examines with the cooperation of the thing it is examining.

What the three locks actually do

It is worth reading the generated guards rather than the option names, because the names promise a category the code does not claim.

  • The domain lock resolves the browser hostname, lowercases it, and compares it against your allowlist, with an optional subdomain match so a wildcard entry covers hosts beneath it.
  • The browser lock reads the navigator object and classifies the user agent string into one of a small set of families, then cross-checks that against the structured brand list the browser exposes.
  • The operating-system lock reads the navigator object as well, classifying both the user agent and the reported platform, and uses the maximum touch-point count to separate a touch device from a desktop.

Read as a group, these are deployment checks. They answer the question is this code running where I expected it to run, using the environment's own account of itself. That is a reasonable thing to want to know. It is a different question from is this the machine I licensed, and the gap between the two is where licensing plans get into trouble.

The surprise in the browser and platform locks

The browser lock does not attempt to determine whether a browser is genuine, because from inside a page there is no way to do that. What it does instead is compare two things the client reported about itself. One classification is derived from the user agent string; the other from the structured brand list exposed alongside it. When both produce a confident answer and those answers disagree, the guard fires.

The operating-system lock works the same way, comparing a classification of the user agent against a classification of the reported platform, with an explicit allowance for the case where a mobile platform legitimately reports a desktop-style kernel. Again the guard fires on disagreement between two self-reported sources.

This is genuinely useful for catching a clumsily edited user agent, and it is worth knowing precisely what kind of signal it is. It is an internal consistency check, not an independent measurement, so anything capable of setting one value coherently is capable of setting the other. Treat a trip as evidence that something in the environment is inconsistent, which is a good reason to log, and a weak reason to block.

They fail rather than skip, and that matters more than the bypass

The most consequential property of these guards in practice is not how a determined person gets past them. It is what they do when the environment is simply unusual.

The domain lock resolves the hostname and, if there is no usable value, fires the failure action before it consults your allowlist at all. The browser and operating-system locks both begin by reading the navigator object and take the same failure path when it is absent. An environment that matches none of the known patterns classifies as unknown, and unknown is never a member of an allowlist.

Now list the places your bundle might run that have no hostname or no navigator: a file opened from disk, an embedded viewer or kiosk shell, a test runner, a build-time render, a server-side rendering pass, an edge runtime. In every one of those a guard configured to protect against copying will instead fire against you. This is the mechanism behind most reports that protection broke the build, and it is a configuration outcome rather than a defect.

Choose the failure action deliberately

When a guard trips it performs the failure action you configured, and the differences between them are larger than the names imply. Blanking clears the document element. Reloading calls the location reload function. Redirecting assigns to location. Every one of those paths still ends by throwing. The single exception is the degrade action, which sets a global flag and raises a custom event, then leaves your own code to decide what happens next.

That distinction decides how the feature behaves on a bad day. A thrown error inside a framework boundary usually becomes a blank view for a user who has done nothing wrong. The same error inside a request handler fails that request, which on a server means every user rather than one session. Degrade turns the same condition into something your application reports and handles, which is almost always what you actually wanted from a guard whose signal is advisory to begin with.

What these options are genuinely worth

Set against the wrong benchmark they look weak, and against the right one they are a reasonable feature. Three uses hold up.

The first is deterring the casual copy. A substantial share of real-world misuse is somebody lifting a bundle onto their own host and expecting it to keep working. A domain check ends that without ceremony, and the person doing it is usually not equipped to go further.

The second is configuration-drift detection. Knowing that your code is executing somewhere your deployment did not intend is useful operational information, particularly for an embeddable component distributed to named customers.

The third, and the one most often left on the table, is the signal. Configured to degrade, a tripped guard becomes an event your monitoring can record and correlate. That turns a blunt enforcement idea into telemetry, which is both more useful and far less likely to hurt a legitimate user.

What binding actually requires

The shape of a real answer is worth spelling out, because it clarifies why no build setting can substitute for it. You need a key held by the user's platform in a store that will not release it, an operation that produces a signature without ever exposing the key to the page, and a server that verifies that signature against a credential registered earlier. Platform authenticators and passkeys have this shape, which is why they are the right tool when the requirement is genuinely to tie a session to something a user holds.

Even there, the accurate description is narrower than the marketing one. It binds an authenticator rather than a device, and the guarantee comes from your server checking a signature, not from anything the page concluded. That is the general rule underneath this entire topic: the decision has to be made somewhere the person it constrains does not control. Once it is, protecting the client path is a sensible second layer, and it is a second layer rather than the mechanism.

A configuration that holds up

Put the entitlement decision on your server and have the client render its result rather than compute it. Use the domain lock where the hostname is genuinely stable and known, keep the allowlist under change control next to the deployment that depends on it, and choose degrade so an omission produces a reported anomaly instead of a customer-visible outage. Leave the browser and operating-system locks off unless you have a specific reason to want that consistency signal, and if you enable them, enable them to report.

Then protect the client path properly, because raising the cost of reading and editing that code is worthwhile once the important decision is somewhere else. Transformed identifiers and rebuilt string literals mean the branch cannot be found by searching for an obvious name. That is a real increase in effort, spent in the right place, on a path whose correctness no longer depends on it.

The short version

You cannot bind delivered code to a machine from inside that machine, because every signal available is one the machine volunteered. The lock options express a deployment expectation and detect violations of it, two of them by comparing self-reported values against each other. They fail rather than skip when the environment is unfamiliar, so choose degrade unless you have thought carefully about the alternative. Put the decision on your server, use the locks for deterrence and telemetry, and protect the client path as a second layer rather than a first.

Frequently asked questions

Can JavaScript be locked to a specific device?

Not by the code itself, and the reason is structural rather than a limitation of any particular tool. Every signal available to a script in a page about the machine it is running on is a value that machine volunteered about itself. The hostname, the user agent, the reported platform, the screen dimensions and the touch-point count are all strings supplied by the environment being examined, and anything the environment supplies it can also supply differently. Binding requires a secret or a key that the party being checked holds but cannot read or move, and a verifier somewhere else that can tell the difference. A page has neither.

What do the lock options in this engine actually read?

Three different self-reported values, and it is worth knowing exactly which. The domain lock resolves the browser hostname, lowercases it, and compares it against your allowlist with an optional subdomain match. The browser lock reads the navigator object and classifies the user agent string into one of a handful of families. The operating-system lock reads the navigator object as well, classifying both the user agent and the reported platform, with the touch-point count used to separate a tablet from a desktop. None of those is a measurement of the machine. Each is a claim the machine makes, which is why the honest description of the feature is a configuration check rather than a device binding.

Is the browser lock checking whether the browser is genuine?

No, and this is the part that most often surprises people who read the generated code. It performs a consistency check between two things the client reported about itself. One classification comes from the user agent string, the other from the structured brand list the browser exposes alongside it, and the guard fires when both produce a confident answer and those answers disagree. That is a useful signal for spotting a clumsily edited user agent, and it is a comparison of two self-reported sources rather than an independent measurement. Anything able to set one consistently is able to set the other.

What happens to these guards when the environment is unusual?

They fail rather than skip, which is the single most important operational fact about them. The domain lock resolves the hostname and, when there is no usable value, fires the failure action before it ever consults your allowlist. The browser and operating-system locks both begin by reading the navigator object and take the same failure path when it is absent. An environment that does not match any known pattern classifies as unknown, and unknown is never a member of an allowlist. Local files, embedded viewers, kiosk shells, test runners and any server-side runtime are all environments where those assumptions break, so a guard intended to stop copying can instead break your own build pipeline.

What does the failure action do when a lock trips?

More than most configurations assume, so choose it deliberately. Blanking clears the document element, reloading calls the location reload function, redirecting assigns to location, and each of those paths still ends by throwing. Only the degrade action behaves differently: it sets a global flag and raises a custom event, leaving your own code to decide what happens next. Anywhere a thrown error will be caught by a framework boundary and turned into a blank view, or anywhere the code runs inside a request handler, degrade is the option that keeps you in control of the outcome.

If these options are not device binding, what are they genuinely useful for?

Three things, all real and all modest. They deter the casual copy, where somebody lifts a bundle onto their own host and expects it to keep working, which is a common and low-effort form of misuse. They act as configuration-drift detection, telling you when your code is running somewhere your deployment did not intend. And configured to degrade rather than to halt, they give you a signal you can report to your own monitoring, which is often the most valuable of the three. What they are not is an enforcement boundary, and describing them internally as one is how teams end up with a licensing model resting on a string comparison.

What would actually bind a session to a device?

A key the user's platform holds and will not hand over, plus a server that checks a signature made with it. That is the shape of platform authenticators and passkeys: the private key stays in the authenticator, the page can ask for a signature but never obtain the key, and the verification happens on your server against a credential you registered earlier. Even then, the honest description is that it binds an authenticator rather than a device, and the guarantee comes from your server checking the signature, not from anything the page decided. See <a href="/blog/obfuscation-passkeys-and-webauthn.aspx">obfuscation, passkeys and WebAuthn</a> for how that fits alongside a protected bundle.

Should we use the domain lock at all?

Yes, in the situation it was built for, and with the failure action chosen on purpose. If you ship a widget or an embeddable component to named customer domains, the lock expresses a deployment expectation and quietly discourages redistribution, which is genuinely worth having. Enable it where the hostname is stable and known, keep the allowlist under change control alongside the deployment that depends on it, and prefer degrade so that a hostname you forgot produces a reported anomaly rather than a customer-visible outage. Where it is a poor fit is anywhere the hostname is variable, absent, or an address that changes as a matter of routine maintenance.

Does obfuscating the lock check make it harder to remove?

It raises the cost of finding and editing it, and it does not change what the check is capable of deciding. A guard living in transformed code takes longer to locate than one sitting in a readable function, and combined with string transformation it stops being findable by searching for an obvious literal. That is a real and worthwhile increase in effort. It remains a check running on the machine of the person it is evaluating, with a result that machine can ultimately influence, so the correct order of work is to move anything consequential to your server first and then protect the client path as a second layer.

Related reading