SCORM · xAPI · Courseware

Your course runs on someone else’s LMS, at a hostname you will never know.

Almost every guide to protecting web JavaScript quietly assumes you control the server it is served from. Courseware breaks that assumption completely: you hand a customer an archive, they upload it to their own learning management system, and your code executes on their domain, inside their frame, under their session. That one distribution fact decides which controls are useful here and which are actively counterproductive.

The Short Version

Package · Contract · Trace

Three decisions cover most of the risk in a courseware pipeline.

Protect after publishTransform the authoring tool’s output, then zip. Never during authoring.
Reserve the API namesThe LMS contract is external. Renaming it breaks tracking silently.
Watermark per customerRedistribution is the real threat, and attribution is the real control.
What You Actually Shipped

An archive, not a deployment

A SCORM or xAPI package is a ZIP file containing a manifest, one or more launch pages, and the HTML, JavaScript, CSS and media that make up the course. The customer uploads that archive to their learning management system, which unpacks it and serves the contents from its own content directory. Nothing about that flow resembles deploying a web application. You never touch the server, you never see the hostname, you cannot issue a hotfix without the customer re-importing, and several of your customers are running an LMS version that was current three years ago.

The asset is the courseware

For most publishers the valuable thing is not one clever function. It is the authored content, the interaction and simulation engine that took two years to build, and the question bank. The realistic threats are wholesale copying of a package and rebranding of it, not somebody studying your event handlers.

The environment is a frame

Learning management systems typically launch content in an iframe or a popup window, and your code walks up through parent or opener windows to find the tracking interface. Anything that assumes a top-level document, or that fights framing, will misbehave in the one arrangement your product must support.

The hostname is the customer’s

The same package runs on a university’s installation, a corporate LMS, a reseller’s platform and a customer’s local test copy. Every runtime control that keys off location makes an assumption you cannot satisfy across that set.

The Control That Does Not Transfer

Why domain locking is the wrong primary control here

On a hosted web application, restricting protected code to your own domains is a cheap and sensible measure. In courseware it inverts: it is the option most likely to generate support tickets while preventing nothing. It is worth understanding exactly why, because the mechanism is simple and the failure modes are predictable.

The guard is an allowlist comparison

The generated guard reads the current page’s hostname, lowercases it, and walks a comma-separated list of permitted domains supplied at build time. There is an option to accept subdomains of a listed domain. If nothing matches, the configured failure action runs. That is the whole mechanism, and it is a good fit for “this code runs on my site” and a poor one for “this code runs wherever my customer installed it”.

An empty hostname fails first

The guard treats a missing or empty hostname as a failure before it ever looks at your allowlist. A page opened directly from disk has no hostname, and course packages are opened from disk constantly: authors reviewing output, customers doing acceptance testing, support engineers reproducing a defect from a ZIP a customer emailed. That is a locked course that appears simply broken.

The list has to be maintained forever

Every new customer, every LMS migration, every vanity subdomain and every acquisition changes the list, and a stale list breaks a paying customer’s live training. Maintaining that across a courseware catalogue is real ongoing work with no security payoff proportional to it.

Where it does make sense

If you already build a separate package per customer — which you should, for the attribution reason below — then you know that customer’s LMS hostname and can lock to it, with subdomain matching enabled and a documented process for updating it. That is domain locking used as a licence-enforcement aid inside a per-customer pipeline, not as a blanket product setting.

The Contract You Must Not Rename

The LMS interface is external API surface

Tracking works because your content finds an object the learning management system placed in a parent window and calls methods on it whose names come from a published specification. Those names are not yours. They belong to a piece of software written by somebody else, and they are matched literally at runtime. Renaming them is the single most common way a protected course breaks, and the breakage is quiet: the course plays perfectly, and nothing is recorded.

Reserve the adapter and its methods

Use rule-based member protection and reserve the names your content uses to locate and call the LMS interface, including the discovery loop that walks parent and opener windows. Member renaming is safe and valuable on your own internal objects; it is unsafe the moment a name has to match a string another vendor’s code is looking for.

Data model keys are strings from a spec

The values you read and write are addressed by specified key names. Anything that transforms literals must leave those keys intact and must not depend on them being reconstructed at runtime in a way an older LMS parser will not recognise. Keep them out of aggressive string handling if you have any doubt.

Global names the LMS looks for

Some integrations work in the other direction, with the platform expecting a specific global to exist in your window. Renaming globals across a project is a strong transform, but any name the outside world reaches for has to be excluded explicitly.

Test the contract, not the page

A visual smoke test tells you nothing here, because the course looks correct while tracking silently fails. The test that matters is a packaged course imported into a real LMS, with a confirmed initialisation, a written score, a commit and a clean termination, run against the protected build every release.

Be Honest About Scoring

The content reports the score, which means the browser does

Under the standard tracking model, the course tells the platform what happened: this learner scored this much, the attempt is complete, the objective is satisfied. Everything that produces those values runs in the learner’s browser. Protection makes the relevant call harder to locate and edit, which genuinely deters casual tampering, and it does not change who is in control of the runtime. Any page that talks honestly about courseware protection has to say that plainly.

Low stakes: protection is proportionate

Compliance refreshers, onboarding, product training. Nobody is building a tool to cheat them, and raising the effort past idle curiosity is the whole requirement. Protect the package and move on.

Real stakes: move the decision server-side

Certification, licensure, anything that produces a credential someone values. Score on a server you control, deliver questions in a way that does not ship the whole bank to the client, and treat the package as presentation. See the online exam guide for that architecture.

Either way, protect the engine

The interaction and simulation code is the part with genuine commercial value and the part a competitor would want. That case for protection stands regardless of how scoring is decided.

Redistribution Is The Real Threat

Build per customer and watermark the build

Ask a courseware publisher what actually goes wrong and the answer is rarely “somebody read our JavaScript”. It is a package that appears on a platform that never licensed it, a reseller who kept serving a catalogue after the agreement ended, or a customer who quietly extended a site licence across a group. Those are attribution problems, and attribution has a direct technical answer.

1
One protected build per customer

Same source, separate protected output per licensee. This is what makes every control below possible, and it costs one loop in your release pipeline.

2
Embed a signed watermark

A keyed tag placed in the protected output, verifiable later against a recovered file, tying the archive back to the release it came from.

3
Record the release digests

A manifest with the SHA-256 of every source and output file per build, so a package someone sends you can be identified rather than argued about.

4
Fix the seed if you need reproducibility

A fixed seed makes protected output reproducible for a given input, which is what lets you regenerate an old release and compare it byte for byte.

5
Then, optionally, lock the domain

Now that you know each licensee’s LMS hostname, a domain guard becomes maintainable. Enable subdomain matching, and choose a failure action that is diagnosable rather than silent.

Pipeline

Where protection fits around an authoring tool

Most courseware is not built with a JavaScript bundler. It comes out of an authoring application that publishes a folder, and that folder is what you protect. The ordering below keeps authoring fast and the shipped archive protected.

Publish first, protect after

Let the authoring tool publish its normal output. Do not attempt to protect intermediate project files or preview builds — the tool owns those, and it will overwrite or reject them. Protection reads the published folder and writes a protected copy.

Protect the JavaScript, leave the rest

Course JavaScript is in scope. The manifest, media, fonts, captions and any player runtime the tool requires by exact filename are not. Point the protection step at the script files and exclude everything the platform parses itself.

Zip from the protected folder

Create the distributable archive from the protected output, not from the original publish directory. This sounds obvious and is the most common way an unprotected package ships by accident.

Keep the unprotected output

Archive the published folder alongside the protected one. When a customer reports a defect eighteen months later you want to reproduce and fix it without re-opening the authoring project, and you want to diff the two.

Runtime Options To Leave Off

Two defences that cost more than they return in courseware

Training content reaches a wider and more varied audience than most software, frequently under an accessibility obligation, on managed corporate machines with extensions you have never heard of. Two runtime options that are reasonable elsewhere are usually the wrong call here.

Developer-tools blocking

It registers a global handler that suppresses the context menu along with the usual inspection shortcuts. Right-click is a normal interaction for many learners and some assistive workflows, and suppressing it across a mandatory training course is a support and accessibility problem that buys very little. See does obfuscation break accessibility.

Anti-monkey-patching

It watches the same interfaces — event registration, timers, network calls, storage — that accessibility overlays, screen-reader helpers and corporate security extensions legitimately wrap. In a locked-down enterprise LMS environment that produces false alarms and, depending on the configured action, broken courses.

Frequently Asked

Courseware protection, answered

Can you obfuscate a SCORM package?

Yes. A SCORM package is a ZIP archive containing ordinary HTML, JavaScript, CSS and media plus a manifest file, so the JavaScript inside it protects like any other web bundle. Two things must be left alone: the imsmanifest.xml file itself, which the learning management system parses to find your launch page and resources, and the names your code uses to talk to the LMS API. Protect the course logic, leave the contract intact, and repackage the archive.

Why does domain locking not work well for course packages?

Because you do not know the hostname at build time. The domain guard compares the browser’s current hostname against a comma-separated allowlist you supply when building, and a course package is downloaded by a customer and hosted on their own learning management system, at their own domain, possibly behind their own vanity subdomain. A courseware vendor with three hundred customers would need three hundred hostnames in the list, kept current forever. Domain locking is workable only when you build a separate package per customer and know their hostname in advance.

What happens if a reviewer opens the course from a local file instead of the LMS?

If domain locking is enabled, the guard fires its failure action before the allowlist is ever consulted. The generated guard reads the page hostname and treats an empty value as a failure, and a page opened directly from disk has no hostname. Course packages get opened from disk constantly during authoring review, customer acceptance testing and support triage, so this shows up as a course that mysteriously does nothing when unzipped locally. Test that path deliberately before shipping domain locking in courseware.

Will renaming break the SCORM API calls?

It will if member renaming is applied indiscriminately. The learning management system provides an object your content calls by exact name, with methods that are part of a published specification. Those names are a contract with software you do not control, so they have to be excluded from renaming in the same way you would exclude any external API surface. The safe pattern is rule-based member protection with the API adapter names reserved, plus a smoke test that runs the packaged course against a real LMS and confirms that initialisation, a score write and a commit all succeed.

Does obfuscating a course stop a learner from changing their score?

It raises the effort and does not settle the question. Under the standard tracking model the content tells the LMS what the score is, so anything running in the learner’s browser can in principle claim a passing result. Protection makes it harder to find and edit that call, which is a real deterrent against casual tampering, but a determined learner still owns the browser. If a result carries real consequences, the assessment logic and the scoring decision belong on a server you control, with the course package acting as presentation.

How do we trace a course package that has been shared beyond its licence?

Build per customer and watermark the build. A per-build watermark embeds an identifying tag with a keyed signature into the protected output, so a package recovered from somewhere it should not be can be scanned and attributed to the release it came from. That is the control that actually addresses courseware redistribution, because the threat is a whole archive being copied rather than an individual function being reverse-engineered.

Does protecting the JavaScript affect accessibility or assistive technology in a course?

Renaming and string transforms do not, because they change identifiers and literals rather than the accessibility tree the browser exposes. Two runtime options do carry a real cost and matter more in courseware than almost anywhere else. Blocking developer tools registers a global context menu handler, which interferes with normal right-click behaviour that some learners rely on. Anti-monkey-patching monitors the same interfaces that assistive extensions and accessibility overlays wrap. In a regulated training context, leave both off unless you have tested them with the assistive technology your learners use.

Where should the protection step sit in a course authoring pipeline?

After the authoring tool publishes and before the archive is zipped. Publish the course as normal, protect the JavaScript files in the published output, leave the manifest and media untouched, then create the distributable archive from the protected folder. Do not protect during authoring or preview, and keep the unprotected published output so you can reproduce a fix without re-authoring. Re-run your packaged-course smoke test against a real LMS after every protected build.

Start Now

Protect one published course and import it

Take the published output of a single course, protect the JavaScript with member renaming set to rule-based and the LMS adapter names reserved, zip it, and import it into a real learning management system. Confirm initialisation, a score write and a commit. That one round trip tells you more about your courseware pipeline than any amount of configuration planning.

Related Guides

Protecting other JavaScript targets

Same protection engine, different distribution model. These guides cover the closest neighbours:

Document viewers · Online exams and assessment · Self-hosted applications · Embeddable widgets and SDKs · Paywalled content · Sites with no build step · Protecting JavaScript (overview) · Can you geo-restrict JavaScript in the browser? · Your JavaScript expiry date runs on their clock · COPPA, FERPA and student data