fix(license): abort on invalid or unvalidatable key instead of silent free fallback

A supplied license key the server rejects, or one that cannot be validated
(server unreachable, no cache), now raises instead of silently downloading the
free binary. No-key (keyless) and valid-key (Pro) paths unchanged. Python, JS,
and .NET, with mirror tests in each.
This commit is contained in:
CloakHQ committed 2026-08-27 00:22:05 +02:00
1 parent 0811704e7f
commit 8fb3350dd1
7 files changed
+113 -8

No files matched your search

+6
View File
@@ -6,6 +6,12 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
---
## [Unreleased]
- **[wrapper]** A license key the server rejects (invalid or expired), or one that cannot be validated at all (license server unreachable with no cached result), now raises a clear error instead of silently downloading the older free binary. Passing no key still uses the free binary as before. Python, JavaScript, and .NET.
---
## [0.5.9] — 2026-08-25
- **[wrapper]** Fix `humanize=True` selecting the wrong element, so humanized actions now resolve the same visible target Playwright would (#512). Text matching ignores non-rendered document content, open Shadow DOM matches are ordered exactly as Playwright orders them for broad `first`/`nth` selectors, and elements with no box of their own (`display: contents`) get actionable geometry derived from their rendered text and visible descendants. Python, JavaScript Playwright/Puppeteer, and .NET.
+16 -4
View File
@@ -193,7 +193,11 @@ def ensure_binary(
requested_version = normalize_requested_version(browser_version)
# Pro license key check (custom download URL overrides Pro path)
from .license import resolve_license_key, validate_license
from .license import (
CloakBrowserLicenseError,
resolve_license_key,
validate_license,
)
key = resolve_license_key(license_key)
if os.environ.get("CLOAKBROWSER_DOWNLOAD_URL"):
@@ -232,11 +236,19 @@ def ensure_binary(
f"CLOAKBROWSER_LICENSE_KEY."
) from e
elif info:
logger.warning(
"License validation failed (plan=%s), using free tier", info.plan
# Key supplied but rejected — abort, never downgrade to free.
raise CloakBrowserLicenseError(
f"CloakBrowser Pro: license key is invalid or expired "
f"(plan={info.plan}). Check CLOAKBROWSER_LICENSE_KEY, or unset it "
f"to use the free binary."
)
else:
logger.warning("License validation unavailable, using free tier")
# Key supplied but unvalidatable (server down, no cache) — abort.
raise CloakBrowserLicenseError(
"CloakBrowser Pro: license could not be validated (server "
"unreachable and no cached validation). Retry in a moment, or "
"unset CLOAKBROWSER_LICENSE_KEY to use the free binary."
)
# Fail fast if no binary available for this platform
check_platform_available()
+9 -2
View File
@@ -251,11 +251,18 @@ public static class Download
}
else if (info != null)
{
CloakLog.Warning("License validation failed (plan={0}), using free tier", info.Plan);
// Key supplied but rejected - abort, never downgrade to free.
throw new InvalidOperationException(
$"CloakBrowser Pro: license key is invalid or expired (plan={info.Plan}). " +
"Check CLOAKBROWSER_LICENSE_KEY, or unset it to use the free binary.");
}
else
{
CloakLog.Warning("License validation unavailable, using free tier");
// Key supplied but unvalidatable (server down, no cache) - abort.
throw new InvalidOperationException(
"CloakBrowser Pro: license could not be validated (server unreachable " +
"and no cached validation). Retry in a moment, or unset " +
"CLOAKBROWSER_LICENSE_KEY to use the free binary.");
}
}
@@ -208,6 +208,30 @@ public class LicenseTests : IDisposable
Assert.Equal("team", info!.Plan);
}
// =======================================================================
// EnsureBinary Pro routing - a supplied key that isn't valid aborts,
// never silently downgrades to the free binary.
// =======================================================================
[Fact]
public async Task InvalidKey_aborts_not_free()
{
License.ValidateLicenseOverride = key => new LicenseInfo(false, "solo", null);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => Download.EnsureBinaryAsync("cb_bad"));
Assert.Contains("invalid or expired", ex.Message);
}
[Fact]
public async Task UnvalidatableKey_aborts_not_free()
{
// validate returns null (server unreachable, no cache) -> abort.
License.ValidateLicenseOverride = key => null;
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => Download.EnsureBinaryAsync("cb_x"));
Assert.Contains("could not be validated", ex.Message);
}
// =======================================================================
// GetProLatestVersion - rate limiting + marker
// =======================================================================
+11 -2
View File
@@ -127,9 +127,18 @@ export async function ensureBinary(
);
}
} else if (info) {
console.log(`[cloakbrowser] License validation failed (plan=${info.plan}), using free tier`);
// Key supplied but rejected — abort, never downgrade to free.
throw new Error(
`CloakBrowser Pro: license key is invalid or expired (plan=${info.plan}). ` +
`Check CLOAKBROWSER_LICENSE_KEY, or unset it to use the free binary.`,
);
} else {
console.log("[cloakbrowser] License validation unavailable, using free tier");
// Key supplied but unvalidatable (server down, no cache) — abort.
throw new Error(
"CloakBrowser Pro: license could not be validated (server unreachable " +
"and no cached validation). Retry in a moment, or unset " +
"CLOAKBROWSER_LICENSE_KEY to use the free binary.",
);
}
}
+17
View File
@@ -547,6 +547,23 @@ describe("preview Pro selection", () => {
errSpy.mock.calls.some((c) => String(c[0]).includes("no preview build is available")),
).toBe(false);
});
it("aborts (never free) when the server rejects the key", async () => {
delete process.env.CLOAKBROWSER_DOWNLOAD_URL;
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => ({ valid: false, plan: "solo", expires: null }),
} as Response);
await expect(ensureBinary("cb_bad")).rejects.toThrow(/invalid or expired/);
});
it("aborts (never free) when the key cannot be validated (server down, no cache)", async () => {
delete process.env.CLOAKBROWSER_DOWNLOAD_URL;
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("network down"));
await expect(ensureBinary("cb_test")).rejects.toThrow(/could not be validated/);
});
});
describe("ensureBinary", () => {
+30
View File
@@ -11,6 +11,7 @@ import pytest
from cloakbrowser.download import BinaryVerificationError, ensure_binary
from cloakbrowser.license import (
CloakBrowserLicenseError,
LicenseInfo,
build_launch_env,
get_active_session_count,
@@ -839,6 +840,35 @@ class TestEnsureBinaryProRouting:
with pytest.raises(RuntimeError, match="Pro binary unavailable"):
ensure_binary("cb_x")
def test_invalid_key_aborts_not_free(self):
"""A supplied key the server rejects (valid=False) must abort with a clear
license error — NOT silently downgrade to the free binary."""
with patch.dict(os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": ""}, clear=False), \
patch("cloakbrowser.download.get_local_binary_override", return_value=None), \
patch("cloakbrowser.license.resolve_license_key", return_value="cb_bad"), \
patch("cloakbrowser.license.validate_license",
return_value=LicenseInfo(valid=False, plan="solo", expires=None)), \
patch("cloakbrowser.download._ensure_pro_binary",
side_effect=AssertionError("MUST NOT reach the Pro path")), \
patch("cloakbrowser.download.check_platform_available",
side_effect=AssertionError("MUST NOT reach the free-tier path")):
with pytest.raises(CloakBrowserLicenseError, match="invalid or expired"):
ensure_binary("cb_bad")
def test_unvalidatable_key_aborts_not_free(self):
"""A supplied key that can't be validated (server unreachable, no cache →
validate_license returns None) must abort — NOT silently downgrade."""
with patch.dict(os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": ""}, clear=False), \
patch("cloakbrowser.download.get_local_binary_override", return_value=None), \
patch("cloakbrowser.license.resolve_license_key", return_value="cb_x"), \
patch("cloakbrowser.license.validate_license", return_value=None), \
patch("cloakbrowser.download._ensure_pro_binary",
side_effect=AssertionError("MUST NOT reach the Pro path")), \
patch("cloakbrowser.download.check_platform_available",
side_effect=AssertionError("MUST NOT reach the free-tier path")):
with pytest.raises(CloakBrowserLicenseError, match="could not be validated"):
ensure_binary("cb_x")
# ── build_launch_env ──────────────────────────────────