fix(download): fall back to free binary on macOS when Pro 404s

A Pro license on macOS currently has no binary to download. Rather than
hard-failing a paying customer, fall back to the free binary with a clear
notice. Scoped to the 404 case only: transient and checksum-verification
failures still hard-fail (no silent downgrade), and once the macOS Pro
build ships the 404 disappears and Pro is served automatically.

JS side adds DownloadHttpError to carry the HTTP status through fetch's
generic error path so the 404 can be distinguished from transient failures.
This commit is contained in:
CloakHQ
2026-06-23 10:41:04 +02:00
parent 6acd9fe277
commit 343a3e8e28
2 changed files with 67 additions and 17 deletions
+25 -8
View File
@@ -143,14 +143,31 @@ def ensure_binary(license_key: str | None = None) -> str:
# Authenticity could not be confirmed — surface verbatim.
raise
except Exception as 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
# 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
elif info:
logger.warning("License validation failed (plan=%s), using free tier", info.plan)
else:
+42 -9
View File
@@ -57,6 +57,20 @@ 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
// ---------------------------------------------------------------------------
@@ -93,14 +107,33 @@ export async function ensureBinary(licenseKey?: string): Promise<string> {
} catch (e) {
// Authenticity could not be confirmed — surface verbatim.
if (e instanceof BinaryVerificationError) throw 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 }
);
// 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 }
);
}
}
} else if (info) {
console.log(`[cloakbrowser] License validation failed (plan=${info.plan}), using free tier`);
@@ -536,7 +569,7 @@ async function downloadFile(url: string, dest: string, headers?: Record<string,
});
if (!response.ok) {
throw new Error(`Download failed: HTTP ${response.status} ${response.statusText}`);
throw new DownloadHttpError(response.status, response.statusText);
}
if (!response.body) {