JSO ships server-side client libraries for seven languages. Every client targets the same HttpApi.ashx endpoint, sends the same JSON shape, parses the same Report back, and exposes the same three named presets. Pick the one that matches the language your build pipeline already runs in — there's no functional difference between them.
The client matrix
| Language |
Package |
Install |
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) |
pip install jso-protector |
Python ≥3.8 |
stdlib urllib (no requests dep) |
Yes — 8 tests |
| Go |
jso-protector-go |
go get github.com/javascriptobfuscator/jso-protector-go |
Go ≥1.21 |
stdlib net/http |
Tests written (no Go toolchain locally) |
| .NET |
JsoProtector (NuGet) |
dotnet add package JsoProtector |
.NET Standard 2.0 |
HttpClient (IHttpClientFactory-friendly) |
Yes — 8 xUnit tests |
| Ruby |
jso_protector (RubyGems) |
gem install jso_protector |
Ruby ≥2.7 |
stdlib net/http |
Tests written (no Ruby toolchain locally) |
| PHP |
javascriptobfuscator/jso-protector (Packagist) |
composer require javascriptobfuscator/jso-protector |
PHP ≥7.4 |
ext-curl when present; stream context fallback |
Tests written (no PHP 7.4+ locally) |
| Rust |
jso-protector (crates.io) |
cargo add jso-protector |
Rust ≥1.70 |
ureq (sync, rustls) |
Tests written (no Rust toolchain locally) |
| Java |
com.javascriptobfuscator:jso-protector (Maven Central) |
<dependency><groupId>com.javascriptobfuscator</groupId><artifactId>jso-protector</artifactId></dependency> |
JDK 11+ |
stdlib java.net.http.HttpClient |
Tests written (JDK 8 only locally) |
| 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);
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 build / scripted release / ML deploy →
jso-protector on PyPI. Stdlib-only, easy to wire into Bazel + py_binary.
- Go-based DevOps tooling →
jso-protector-go. Stdlib + context.Context cancellation.
- .NET Framework / .NET 8 / Unity / Xamarin →
JsoProtector NuGet. .NET Standard 2.0, HttpClient-injectable.
- Rails / Ruby script →
jso_protector gem. Stdlib-only.
- Laravel / Symfony / WordPress build →
javascriptobfuscator/jso-protector on Packagist. ext-curl-preferred, no third-party dep.
- Rust / Cargo-driven build →
jso-protector crate. Sync via ureq, typed error enum.
The wire format is one HTTP POST per protection call. If your language isn't listed above, write the POST by hand — every JSO client implementation linked above is small enough (200–400 lines) to serve as the reference.
Symbolication remains Node-only today via
jso-symbolicate. The identifier map in
Report.GlobalIdentifierMap /
Report.MemberIdentifierMap is plain JSON, though, so any of the language clients above can implement local demangling in 30 lines of code if needed.