koala73/worldmonitor · error · ApiError
Desktop authentication failed
Error message
Desktop authentication failed
What it means
verifyDesktopAuth (register-interest.ts:107) throws 403 when the server has no WM_DESKTOP_SHARED_SECRET configured AND the legacy escape hatch does not apply (WM_DESKTOP_AUTH_ALLOW_LEGACY=true only accepts requests that carry no auth headers at all). This is the server refusing an unauthenticated Turnstile bypass from a desktop source — it is a deployment configuration problem, not a client signing bug.
Source
Thrown at server/worldmonitor/leads/v1/register-interest.ts:107
): Promise<string> {
return `sha256=${await hmacSha256Hex(secret, desktopAuthMessage(timestamp, req))}`;
}
async function verifyDesktopAuth(request: Request, req: RegisterInterestRequest): Promise<void> {
const secret = process.env[DESKTOP_AUTH_SECRET_ENV];
const timestamp = request.headers.get(DESKTOP_AUTH_TIMESTAMP_HEADER);
const signature = request.headers.get(DESKTOP_AUTH_SIGNATURE_HEADER);
if (!secret) {
if (!timestamp && !signature && process.env[DESKTOP_AUTH_ALLOW_LEGACY_ENV] === 'true') {
console.warn(
`[register-interest] ${DESKTOP_AUTH_ALLOW_LEGACY_ENV}=true and ${DESKTOP_AUTH_SECRET_ENV} is unset; accepting unsigned legacy desktop bypass`,
);
return;
}
console.warn(`[register-interest] ${DESKTOP_AUTH_SECRET_ENV} not set; rejecting desktop bypass`);
throw new ApiError(403, 'Desktop authentication failed', '');
}
if (!timestamp || !signature) {
throw new ApiError(403, 'Desktop authentication failed', '');
}
const timestampMs = Number(timestamp);
if (!Number.isSafeInteger(timestampMs) || Math.abs(Date.now() - timestampMs) > DESKTOP_AUTH_WINDOW_MS) {
throw new ApiError(403, 'Desktop authentication failed', '');
}
const supplied = signature.trim();
if (!/^sha256=[a-f0-9]{64}$/.test(supplied)) {
throw new ApiError(403, 'Desktop authentication failed', '');
}
const expected = await createDesktopAuthSignature(secret, timestamp, req);
if (!timingSafeStringEqual(supplied, expected)) {View on GitHub (pinned to eeab0a219f)
Solutions
- Set WM_DESKTOP_SHARED_SECRET in the serving environment to the same value the desktop app signs with, then redeploy
- Verify with a health/env dump that the variable is actually visible to the process
- For a legacy unsigned client only: set WM_DESKTOP_AUTH_ALLOW_LEGACY=true AND stop sending the auth headers
- Do not rotate the secret on one side only — clients sign with the old value and fall into error 306 instead
Example fix
# before (server env missing) vercel env ls | grep WM_DESKTOP # nothing # after vercel env add WM_DESKTOP_SHARED_SECRET production vercel deploy --prod
Defensive patterns
Strategy: validation
Validate before calling
// Desktop client: refuse to send the desktop source unless we hold a secret to sign with
if (source === 'desktop-settings' && !desktopSecret) {
source = 'website'; // fall back to the Turnstile path
deleteHeaders(DESKTOP_AUTH_TIMESTAMP_HEADER, DESKTOP_AUTH_SIGNATURE_HEADER);
} else if (desktopSecret) {
const ts = Date.now().toString();
setHeader(DESKTOP_AUTH_TIMESTAMP_HEADER, ts);
setHeader(DESKTOP_AUTH_SIGNATURE_HEADER, await createDesktopAuthSignature(desktopSecret, ts, req));
}
await post(req); Type guard
function canSignDesktopRequests(secret: string | undefined): secret is string {
return typeof secret === 'string' && secret.length > 0;
} Try / catch
try { await register(req); }
catch (e) {
if (isApiError(e, 403, 'Desktop authentication failed') && !desktopSecret) {
reportOps('WM_DESKTOP_SHARED_SECRET missing on server — desktop signups are hard-down');
}
throw e;
} Prevention
- Include WM_DESKTOP_SHARED_SECRET in every environment's checklist and in deploy smoke tests
- Use the same secret store entry for server and desktop release pipelines so they cannot diverge
- Treat a 100% 403 rate on desktop-source traffic as a config alarm, not user error
When it happens
Trigger: POSTing to register-interest with source="desktop-settings" while the deployment env lacks WM_DESKTOP_SHARED_SECRET. The legacy bypass cannot save you if you also sent x-worldmonitor-desktop-* headers, because that path requires both headers to be absent.
Common situations: Promoting to a new Vercel/Railway environment without copying the secret; setting the secret on the API service but not on the Node sidecar; env name typo (WM_DESKTOP_SECRET instead of WM_DESKTOP_SHARED_SECRET); local dev against prod while the secret only exists in CI secrets.
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Acquisition provider '${name}' is not configured. Set the re
- EXA_API_KEY is required for exa-search adapter
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/8d2dd984b0fbb565.
Report an issue: GitHub.