The Node client is published today as jso-protector on npm. Reference clients for Python, Go, .NET, Ruby, PHP, Rust, Java, and Kotlin are implemented and tested in the product source tree, but they are not yet published to their public registries. Their package-manager commands will fail until release; contact us if one should be prioritised.
The client matrix
| Language |
Package |
Availability |
Min version |
HTTP transport |
Locally test-verified |
| Node |
jso-protector (npm) |
npm install --save-dev jso-protector |
Node ≥18 |
built-in fetch |
Yes — 152 tests |
| Python |
jso-protector (PyPI) |
Unreleased — not on PyPI |
Python ≥3.8 |
stdlib urllib (no requests dep) |
Yes — 8 tests |
| Go |
jso-protector-go |
Unreleased — no public repository |
Go ≥1.21 |
stdlib net/http |
Tests written (no Go toolchain locally) |
| .NET |
JsoProtector (NuGet) |
Unreleased — not on NuGet |
.NET Standard 2.0 |
HttpClient (IHttpClientFactory-friendly) |
Yes — 8 xUnit tests |
| Ruby |
jso_protector (RubyGems) |
Unreleased — not on RubyGems |
Ruby ≥2.7 |
stdlib net/http |
Tests written (no Ruby toolchain locally) |
| PHP |
javascriptobfuscator/jso-protector (Packagist) |
Unreleased — not on Packagist |
PHP ≥7.4 |
ext-curl when present; stream context fallback |
Tests written (no PHP 7.4+ locally) |
| Rust |
jso-protector (crates.io) |
Unreleased — not on crates.io |
Rust ≥1.70 |
ureq (sync, rustls) |
Tests written (no Rust toolchain locally) |
| Java |
com.javascriptobfuscator:jso-protector (Maven Central) |
Unreleased — not on Maven Central |
JDK 11+ |
stdlib java.net.http.HttpClient |
Tests written (JDK 8 only locally) |
| Kotlin |
com.javascriptobfuscator:jso-protector-kotlin (Maven Central) |
Unreleased — not on Maven Central |
Kotlin ≥1.9, JDK 11+ |
stdlib java.net.http.HttpClient with coroutines |
Tests written (awaiting Kotlin CI) |
| Anything else |
Wire format spec + examples/curl/protect.sh |
curl + jq |
POSIX shell |
curl |
Syntax-checked |
Shared design rules
Every client honors these invariants. If you switch languages, your protection code reads almost identically:
- Same
protect() surface. Inputs: files (map of filename to source), preset (one of standard, balanced, maximum), optional options override map, optional label (forwarded as ReleaseLabel).
- Same Result shape.
files (protected source by name), build_id (stable identifier for this run), polymorphism_fingerprint (short SHA-256 over output), report (full Report including identifier maps), raw (complete response body).
- Env-var-first credentials.
JSO_API_KEY / JSO_API_PASSWORD (or the long-form JAVASCRIPT_OBFUSCATOR_API_KEY / JAVASCRIPT_OBFUSCATOR_API_PASSWORD) read from the environment before falling back to constructor arguments. Use env vars on shared / CI machines.
- Same three preset definitions. Standard, balanced, maximum. Explicit options always override preset defaults.
- Typed error class.
Error / Exception with the API's Type and ErrorCode when the server replies non-Succeed. Messages never include the API key or password.
- No mandatory third-party deps. Except where the language ecosystem requires it (Rust has no stdlib HTTP client; we use
ureq). Everywhere else: stdlib transport, no async runtime.
Side-by-side example
The same "protect app.js and print the BuildId" task in each language. The body of the program is structurally identical because the surface is.
Node:
npx jso-protector --config jso.config.json --label "$GIT_COMMIT" --report jso-report.json
Python:
from jso_protector import protect
result = protect(
files={"app.js": open("dist/app.js").read()},
preset="balanced", label=os.environ.get("GIT_COMMIT"))
print("BuildId:", result.build_id)
Go:
res, err := jso.Protect(ctx, jso.Request{
Files: map[string]string{"app.js": string(src)},
Preset: "balanced", Label: os.Getenv("GIT_COMMIT"),
})
log.Println("BuildId:", res.BuildID)
.NET:
var r = await client.ProtectAsync(new ProtectOptions {
Files = new() { ["app.js"] = src },
Preset = "balanced",
Label = Environment.GetEnvironmentVariable("GIT_COMMIT"),
});
Console.WriteLine($"BuildId: {r.BuildId}");
Ruby:
result = JsoProtector.protect(
files: { "app.js" => File.read("dist/app.js") },
preset: "balanced", label: ENV["GIT_COMMIT"])
puts "BuildId: #{result.build_id}"
PHP:
$result = (new \JsoProtector\Client())->protect([
'files' => ['app.js' => file_get_contents('dist/app.js')],
'preset' => 'balanced',
'label' => getenv('GIT_COMMIT') ?: null,
]);
echo "BuildId: {$result->buildId}\n";
Rust:
let result = Client::new().protect(ProtectRequest {
files: HashMap::from([("app.js".into(), src)]),
preset: Some("balanced".into()),
label: env::var("GIT_COMMIT").ok(),
..Default::default()
})?;
println!("BuildId: {:?}", result.build_id);
Java:
var result = client.protect(ProtectOptions.builder()
.files(Map.of("app.js", Files.readString(Path.of("dist/app.js"))))
.preset("balanced")
.label(System.getenv("GIT_COMMIT"))
.build());
System.out.println("BuildId: " + result.buildId());
Kotlin:
val result = client.protect(ProtectRequest(
files = mapOf("app.js" to File("dist/app.js").readText()),
preset = "balanced",
label = System.getenv("GIT_COMMIT")
))
when (result) {
is ProtectResult.Success -> println("BuildId: ${result.buildId}")
is ProtectResult.Failure -> error(result.message)
}
Which client should you use?
- Node-first build (Vite, Webpack, Next.js, Bun) →
jso-protector npm CLI is the richest surface: 8 bundler plugins, dry-run, release-check, manifest, report flags.
- Python, Go, .NET, Ruby, PHP, Rust, Java, or Kotlin → use the wire-format reference for now or contact us to prioritise the relevant registry release. The implementations and examples below document the intended client shape, but they are not installable public packages today.
The wire format is one HTTP POST per protection call. If Node is not your release environment, use the documented wire format directly until the matching client is published.
Symbolication is available in the browser-only
interactive demo and as a versioned
jso-symbolicate direct download. The CLI is not registry-published. Identifier maps in
Report.GlobalIdentifierMap and
Report.MemberIdentifierMap remain plain JSON for local tooling.
Frequently asked questions
Which language clients can I install today?
The Node client, published on npm. Reference clients for Python, Go, .NET, Ruby, PHP, Rust, Java and Kotlin are implemented and tested in the product source tree, but they are not yet published to their public registries, so their package-manager commands will fail until release. That state is stated plainly on the matrix rather than implied, and if one of them should be prioritised for publication it is worth asking, because the ordering is driven by demand.
What can I use if my language has no published client yet?
The wire format directly. Every client is a thin wrapper over one HTTP POST per protection call, and that format is documented with examples including a shell script built on ordinary command line tools. Writing against it is a small amount of work, and because the clients share one surface, replacing your own code with an official client later is close to mechanical.
What stays the same when I switch languages?
The parts that matter for your build code. Every client exposes the same protect surface, taking a map of filename to source, one of the three presets, an optional options override map and an optional label. Every client returns the same result shape, with the protected files by name, a stable build identifier for the run, a short fingerprint over the output, the full report including identifier maps, and the complete response body. The three preset definitions are identical, and explicit options always override preset defaults.
How do the clients handle credentials?
Environment variables first, constructor arguments as a fallback. Both the short and long forms of the key and password variables are read from the environment before anything passed in code is consulted, which is what makes the environment the right place for them on shared and continuous integration machines. Error messages never include the key or the password, so a failed call can be logged without leaking either.
What dependencies does adding a client pull in?
As close to none as each ecosystem allows. The clients use the standard library transport wherever one exists, with no async runtime required, so most of them add nothing to your dependency tree at all. The single exception is where the language ships no standard HTTP client and one has to be chosen. That constraint is deliberate, because a release-time tool that drags in a dependency tree is a tool that has to be audited before every upgrade.
How do errors surface across the clients?
Through a typed error or exception class carrying the API's own type and error code whenever the server replies with anything other than success. Because the shape is the same everywhere, error handling written against one client transfers to another with only syntax changes, which matters most in a release pipeline where the failure path is the code least often exercised and most often needed.