feat(download): ship macOS Pro binary, drop the macOS free-fallback stopgap
The macOS Pro build (darwin arm64 + Intel) is now served alongside Linux and Windows, so a Pro license downloads the latest binary on macOS like every other platform. Reverts the v0.4.2 stopgap that, on macOS only, silently fell back to the free binary when the Pro download returned 404 (there was no macOS Pro build yet). A valid license now hard-fails on any Pro download error on every platform — no silent downgrade — restoring the wrapper's stated invariant. Applied across the Python, JS, and .NET wrappers; removes the now-unused DownloadHttpError type and its tests. README: macOS listed as available for Pro.
This commit is contained in:
@@ -185,7 +185,7 @@ The wrapper (Python + JS) is MIT, free forever. The binary uses a delayed
|
||||
free-release model:
|
||||
|
||||
- **Free (v146)** — the previous binary, on [GitHub Releases](https://github.com/CloakHQ/cloakbrowser/releases). Goes stale within weeks as detection evolves.
|
||||
- **Pro (latest, Chromium 148.0.7778.215.2)** — the newest patches and Chromium upgrades first, so the [results below](#test-results) stay green as anti-bot systems change. Linux + Windows (macOS coming).
|
||||
- **Pro (latest, Chromium 148.0.7778.215.2)** — the newest patches and Chromium upgrades first, so the [results below](#test-results) stay green as anti-bot systems change. Linux, Windows, and macOS (Apple Silicon + Intel).
|
||||
|
||||
Anti-bot detection updates constantly, and an older binary degrades fast.
|
||||
Pro keeps you on the build that's actively maintained against it.
|
||||
|
||||
@@ -143,31 +143,14 @@ def ensure_binary(license_key: str | None = None) -> str:
|
||||
# Authenticity could not be confirmed — surface verbatim.
|
||||
raise
|
||||
except Exception as e:
|
||||
# macOS has no Pro binary yet. Rather than hard-failing a paying
|
||||
# customer, fall back to the free binary with a clear notice.
|
||||
# Scoped to the 404 (binary-not-found) case so that (a) transient
|
||||
# and verification failures still hard-fail — no silent downgrade —
|
||||
# and (b) the moment the macOS Pro build ships, the 404 disappears
|
||||
# and Pro is served automatically with no wrapper change.
|
||||
if (
|
||||
get_platform_tag().startswith("darwin")
|
||||
and isinstance(e, httpx.HTTPStatusError)
|
||||
and e.response.status_code == 404
|
||||
):
|
||||
logger.warning(
|
||||
"macOS Pro binary is not available yet — using the free "
|
||||
"binary for now. Your license stays valid and you'll get "
|
||||
"the Pro binary on macOS automatically once the build ships."
|
||||
)
|
||||
else:
|
||||
# Transient failure with no cached Pro binary to use — surface a
|
||||
# clear error rather than silently downloading the free binary.
|
||||
raise RuntimeError(
|
||||
f"Pro binary unavailable: {e}. Your license is valid but the "
|
||||
f"Pro binary could not be downloaded right now. Retry in a "
|
||||
f"moment. To use the free binary instead, unset "
|
||||
f"CLOAKBROWSER_LICENSE_KEY."
|
||||
) from e
|
||||
# Transient failure with no cached Pro binary to use — surface a
|
||||
# clear error rather than silently downloading the free binary.
|
||||
raise RuntimeError(
|
||||
f"Pro binary unavailable: {e}. Your license is valid but the "
|
||||
f"Pro binary could not be downloaded right now. Retry in a "
|
||||
f"moment. To use the free binary instead, unset "
|
||||
f"CLOAKBROWSER_LICENSE_KEY."
|
||||
) from e
|
||||
elif info:
|
||||
logger.warning("License validation failed (plan=%s), using free tier", info.plan)
|
||||
else:
|
||||
|
||||
@@ -33,20 +33,6 @@ public sealed class BinaryVerificationError : Exception
|
||||
public BinaryVerificationError(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A non-2xx HTTP response during a binary download. Carries the status code so
|
||||
/// callers can distinguish a 404 (binary not built for this platform - e.g. the
|
||||
/// macOS Pro build hasn't shipped yet) from transient failures. Mirrors the JS
|
||||
/// <c>DownloadHttpError</c> introduced in v0.4.2.
|
||||
/// </summary>
|
||||
public sealed class DownloadHttpError : Exception
|
||||
{
|
||||
public System.Net.HttpStatusCode Status { get; }
|
||||
public DownloadHttpError(System.Net.HttpStatusCode status, string? reason)
|
||||
: base($"Download failed: HTTP {(int)status} {reason}")
|
||||
=> Status = status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary download and cache management for CloakBrowser.
|
||||
/// Downloads the patched Chromium binary on first use, caches it locally.
|
||||
@@ -183,31 +169,12 @@ public static class Download
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// macOS has no Pro binary yet. Rather than hard-failing a paying
|
||||
// customer, fall back to the free binary with a clear notice.
|
||||
// Scoped to the 404 (binary-not-found) case so that (a) transient
|
||||
// and verification failures still hard-fail - no silent downgrade -
|
||||
// and (b) the moment the macOS Pro build ships, the 404 disappears
|
||||
// and Pro is served automatically with no wrapper change. (v0.4.2)
|
||||
if (Config.GetPlatformTag().StartsWith("darwin", StringComparison.Ordinal)
|
||||
&& e is DownloadHttpError he
|
||||
&& he.Status == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
CloakLog.Warning(
|
||||
"macOS Pro binary is not available yet - using the free binary " +
|
||||
"for now. Your license stays valid and you'll get the Pro binary " +
|
||||
"on macOS automatically once the build ships.");
|
||||
// fall through to the free-tier download below
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transient failure with no cached Pro binary to use - surface a
|
||||
// clear error rather than silently downloading the free binary.
|
||||
throw new InvalidOperationException(
|
||||
$"Pro binary unavailable: {e.Message}. Your license is valid but the " +
|
||||
"Pro binary could not be downloaded right now. Retry in a moment. " +
|
||||
"To use the free binary instead, unset CLOAKBROWSER_LICENSE_KEY.", e);
|
||||
}
|
||||
// Transient failure with no cached Pro binary to use - surface a
|
||||
// clear error rather than silently downloading the free binary.
|
||||
throw new InvalidOperationException(
|
||||
$"Pro binary unavailable: {e.Message}. Your license is valid but the " +
|
||||
"Pro binary could not be downloaded right now. Retry in a moment. " +
|
||||
"To use the free binary instead, unset CLOAKBROWSER_LICENSE_KEY.", e);
|
||||
}
|
||||
}
|
||||
else if (info != null)
|
||||
@@ -761,7 +728,8 @@ public static class Download
|
||||
using var resp = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct)
|
||||
.ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
throw new DownloadHttpError(resp.StatusCode, resp.ReasonPhrase);
|
||||
throw new InvalidOperationException(
|
||||
$"Download failed: HTTP {(int)resp.StatusCode} {resp.ReasonPhrase}");
|
||||
|
||||
long total = resp.Content.Headers.ContentLength ?? 0;
|
||||
long downloaded = 0;
|
||||
|
||||
@@ -114,33 +114,6 @@ public class WrapperVersionNewerTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DownloadHttpError"/>, the typed HTTP-status carrier the
|
||||
/// v0.4.2 macOS Pro fallback relies on to distinguish a 404 (no Pro binary built
|
||||
/// for this platform yet) from transient download failures.
|
||||
/// </summary>
|
||||
public class DownloadHttpErrorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Carries_status_code_and_includes_it_in_message()
|
||||
{
|
||||
var ex = new DownloadHttpError(System.Net.HttpStatusCode.NotFound, "Not Found");
|
||||
Assert.Equal(System.Net.HttpStatusCode.NotFound, ex.Status);
|
||||
Assert.Contains("404", ex.Message);
|
||||
Assert.Contains("Not Found", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Distinguishes_404_from_transient_5xx()
|
||||
{
|
||||
var notFound = new DownloadHttpError(System.Net.HttpStatusCode.NotFound, "Not Found");
|
||||
var transient = new DownloadHttpError(System.Net.HttpStatusCode.ServiceUnavailable, "Unavailable");
|
||||
// The macOS Pro fallback only triggers on 404; a 5xx stays a hard failure.
|
||||
Assert.Equal(System.Net.HttpStatusCode.NotFound, notFound.Status);
|
||||
Assert.NotEqual(System.Net.HttpStatusCode.NotFound, transient.Status);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the archive-extraction path-traversal (zip-slip) guard
|
||||
/// <see cref="Download.ResolveSafeEntryPath(string, string)"/>, shared by
|
||||
|
||||
+9
-42
@@ -57,20 +57,6 @@ export class BinaryVerificationError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A non-2xx HTTP response during a binary download. Carries the status code so
|
||||
* callers can distinguish a 404 (binary not built for this platform) from
|
||||
* transient failures.
|
||||
*/
|
||||
export class DownloadHttpError extends Error {
|
||||
status: number;
|
||||
constructor(status: number, statusText: string) {
|
||||
super(`Download failed: HTTP ${status} ${statusText}`);
|
||||
this.name = "DownloadHttpError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -107,33 +93,14 @@ export async function ensureBinary(licenseKey?: string): Promise<string> {
|
||||
} catch (e) {
|
||||
// Authenticity could not be confirmed — surface verbatim.
|
||||
if (e instanceof BinaryVerificationError) throw e;
|
||||
// macOS has no Pro binary yet. Rather than hard-failing a paying
|
||||
// customer, fall back to the free binary with a clear notice. Scoped to
|
||||
// the 404 (binary-not-found) case so that (a) transient and verification
|
||||
// failures still hard-fail — no silent downgrade — and (b) the moment the
|
||||
// macOS Pro build ships, the 404 disappears and Pro is served
|
||||
// automatically with no wrapper change.
|
||||
if (
|
||||
getPlatformTag().startsWith("darwin") &&
|
||||
e instanceof DownloadHttpError &&
|
||||
e.status === 404
|
||||
) {
|
||||
console.warn(
|
||||
"[cloakbrowser] macOS Pro binary is not available yet — using the " +
|
||||
"free binary for now. Your license stays valid and you'll get the " +
|
||||
"Pro binary on macOS automatically once the build ships."
|
||||
);
|
||||
// fall through to the free-tier download below
|
||||
} else {
|
||||
// Transient failure with no cached Pro binary to use — surface a clear
|
||||
// error rather than silently downloading the free binary.
|
||||
throw new Error(
|
||||
`Pro binary unavailable: ${e}. Your license is valid but the Pro ` +
|
||||
`binary could not be downloaded right now. Retry in a moment. To use ` +
|
||||
`the free binary instead, unset CLOAKBROWSER_LICENSE_KEY.`,
|
||||
{ cause: e }
|
||||
);
|
||||
}
|
||||
// Transient failure with no cached Pro binary to use — surface a clear
|
||||
// error rather than silently downloading the free binary.
|
||||
throw new Error(
|
||||
`Pro binary unavailable: ${e}. Your license is valid but the Pro ` +
|
||||
`binary could not be downloaded right now. Retry in a moment. To use ` +
|
||||
`the free binary instead, unset CLOAKBROWSER_LICENSE_KEY.`,
|
||||
{ cause: e }
|
||||
);
|
||||
}
|
||||
} else if (info) {
|
||||
console.log(`[cloakbrowser] License validation failed (plan=${info.plan}), using free tier`);
|
||||
@@ -569,7 +536,7 @@ async function downloadFile(url: string, dest: string, headers?: Record<string,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new DownloadHttpError(response.status, response.statusText);
|
||||
throw new Error(`Download failed: HTTP ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
|
||||
@@ -406,3 +406,25 @@ class TestEnsureBinaryProRouting:
|
||||
side_effect=AssertionError("MUST NOT reach the free-tier path")):
|
||||
with pytest.raises(RuntimeError, match="Pro binary unavailable: network blip"):
|
||||
ensure_binary("cb_x")
|
||||
|
||||
def test_macos_pro_404_hard_errors_not_free(self):
|
||||
"""macOS now has a Pro binary, so a 404 on the Pro download is a real error
|
||||
and must hard-fail like every other platform — NOT silently fall back to the
|
||||
free binary (the v0.4.2 darwin-404→free stopgap was reverted in v0.4.3)."""
|
||||
import httpx
|
||||
|
||||
req = httpx.Request("GET", "https://example.com/download")
|
||||
not_found = httpx.HTTPStatusError(
|
||||
"404 Not Found", request=req, response=httpx.Response(404, request=req)
|
||||
)
|
||||
with patch.dict(os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": ""}, clear=False), \
|
||||
patch("cloakbrowser.download.get_local_binary_override", return_value=None), \
|
||||
patch("cloakbrowser.download.get_platform_tag", return_value="darwin-x64"), \
|
||||
patch("cloakbrowser.license.resolve_license_key", return_value="cb_x"), \
|
||||
patch("cloakbrowser.license.validate_license",
|
||||
return_value=LicenseInfo(valid=True, plan="solo", expires=None)), \
|
||||
patch("cloakbrowser.download._ensure_pro_binary", side_effect=not_found), \
|
||||
patch("cloakbrowser.download.check_platform_available",
|
||||
side_effect=AssertionError("MUST NOT reach the free-tier path on macOS")):
|
||||
with pytest.raises(RuntimeError, match="Pro binary unavailable"):
|
||||
ensure_binary("cb_x")
|
||||
|
||||
Reference in New Issue
Block a user