mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-10-18 23:07:13 -05:00
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:
7 files changed
+113
-8
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 ──────────────────────────────────
|
||||
|
||||
|
||||
Reference in new issue
Block a user