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

  1. Set WM_DESKTOP_SHARED_SECRET in the serving environment to the same value the desktop app signs with, then redeploy
  2. Verify with a health/env dump that the variable is actually visible to the process
  3. For a legacy unsigned client only: set WM_DESKTOP_AUTH_ALLOW_LEGACY=true AND stop sending the auth headers
  4. 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

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.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/8d2dd984b0fbb565. Report an issue: GitHub.