JuliusBrussee/caveman · error · Error

device credential delivery acknowledgement failed (${lastErr

Error message

device credential delivery acknowledgement failed (${lastError}); credentials were persisted locally but the server may revoke them after the delivery window

What it means

acknowledge() retries POST /api/v1/auth/device/ack up to 5 times with exponential backoff (only retrying on 5xx/429 or thrown errors). If every attempt fails, it throws with the last error embedded, warning that credentials were saved locally but the server may revoke them after the delivery window.

Source

Thrown at packages/device-auth/src/index.ts:82

        headers: {
          authorization: `Bearer ${options.credentials.access_token}`,
          "content-type": "application/json",
          "x-cave-client": options.client,
        },
        body: JSON.stringify({ device_code: options.code.device_code, ack_token: ackToken }),
        signal: requestSignal(options.signal, 5000),
      });
      if (response.ok) return;
      const body = await response.json().catch(() => null) as { error?: { code?: unknown } } | null;
      const code = typeof body?.error?.code === "string" ? body.error.code : `HTTP ${response.status}`;
      lastError = code;
      if (response.status < 500 && response.status !== 429) break;
    } catch (error) {
      lastError = error instanceof Error ? error.message : String(error);
    }
    if (attempt < 4) await options.sleep(Math.min(2000, 200 * 2 ** attempt));
  }
  throw new Error(`device credential delivery acknowledgement failed (${lastError}); credentials were persisted locally but the server may revoke them after the delivery window`);
}

export async function runCavemanDeviceFlow(options: {
  baseURL: string;
  client: string;
  fetch?: typeof globalThis.fetch;
  signal?: AbortSignal;
  sleep?: (ms: number) => Promise<void>;
  onCode?: (code: DeviceCode) => void | Promise<void>;
}): Promise<DeviceGrant> {
  const fetcher = options.fetch ?? globalThis.fetch;
  const wait = options.sleep ?? defaultSleep;
  const baseURL = options.baseURL.replace(/\/$/, "");
  const codeResponse = await fetcher(`${baseURL}/api/v1/auth/device/code`, {
    method: "POST",
    headers: { "content-type": "application/json", "x-cave-client": options.client },
    body: "{}",
    signal: requestSignal(options.signal, 5000),

View on GitHub (pinned to df2ccd85c9)

Solutions

  1. Confirm the ack endpoint is reachable: `curl -i ${baseURL}/api/v1/auth/device/ack` from the same environment.
  2. Re-run the device login once the network/server recovers — the error is transient by design; a successful re-login re-acks.
  3. Verify baseURL points at the correct environment (staging vs production) and isn't blocked by a proxy (check HTTPS_PROXY/NO_PROXY).
  4. Check server-side rate limits if lastError references 429 and back off before retrying.
  5. If credentials were already revoked server-side, redo the device flow to obtain fresh ones.

Example fix

// before (wrong baseURL, unreachable ack endpoint)
await acknowledge({ baseURL: "https://internal-staging.invalid", ... });
// after
await acknowledge({ baseURL: "https://api.example.com", ... });
Defensive patterns

Strategy: retry

Validate before calling

// pre-check reachability of the ack endpoint
const ping = await fetch(`${baseURL}/api/v1/auth/device/ack`, { method: "HEAD" }).catch(() => null);
if (!ping) throw new Error("ack endpoint unreachable; fix network/baseURL before login");

Try / catch

try {
  await acknowledge(options);
} catch (e) {
  if (e instanceof Error && e.message.includes("device credential delivery acknowledgement failed")) {
    // transient: wait and redo the device login to re-ack fresh credentials
    await sleep(5000);
    await redoDeviceFlow();
  } else throw e;
}

Prevention

When it happens

Trigger: All 5 ack attempts against ${baseURL}/api/v1/auth/device/ack fail — network outages, sustained 5xx server errors, or repeated 429 rate-limiting across the full retry window.

Common situations: Corporate proxy/firewall blocking the ack endpoint; auth server outage or deploy in progress; client aggressively rate-limited (429) on every retry; baseURL misconfigured to a wrong host so every request errors.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@df2ccd85c9 (2026-08-31). Data as JSON: /api/errors/2ab389acfc4b7ae8. Report an issue: GitHub.