diff --git a/cli/src/__tests__/cloud.test.ts b/cli/src/__tests__/cloud.test.ts index 0c5aeae8b6d..c66fd33f465 100644 --- a/cli/src/__tests__/cloud.test.ts +++ b/cli/src/__tests__/cloud.test.ts @@ -8,6 +8,7 @@ import { buildBundleFromLocalCompany, cloudCommandExitCodes, connectCloud, + resolveDeviceCodeExpiresAt, } from "../commands/client/cloud.js"; import { LocalUpstreamPushCoordinator, @@ -82,6 +83,15 @@ describe("cloud CLI helpers", () => { expect(cloudCommandExitCodes.schemaMismatch).toBe(3); }); + it("falls back to a bounded device-code expiry when the cloud omits or malforms expiresAt", () => { + const now = Date.UTC(2026, 4, 22, 13, 0, 0); + const validExpiry = "2026-05-22T13:05:00.000Z"; + + expect(resolveDeviceCodeExpiresAt(validExpiry, now)).toBe(Date.parse(validExpiry)); + expect(resolveDeviceCodeExpiresAt(undefined, now)).toBe(now + 15 * 60_000); + expect(resolveDeviceCodeExpiresAt("not-a-date", now)).toBe(now + 15 * 60_000); + }); + it("builds deterministic chunks with validated payload hashes", async () => { const bundle = await buildTestBundle(); diff --git a/cli/src/commands/client/cloud.ts b/cli/src/commands/client/cloud.ts index 891e44b1031..f87098a0b9b 100644 --- a/cli/src/commands/client/cloud.ts +++ b/cli/src/commands/client/cloud.ts @@ -40,6 +40,7 @@ import { const CLOUD_SYNC_CONFLICT_EXIT_CODE = 2; const CLOUD_SYNC_SCHEMA_MISMATCH_EXIT_CODE = 3; const CLOUD_SYNC_SCOPES = ["upstream_import:preview", "upstream_import:write", "upstream_import:read"]; +const DEVICE_CODE_FALLBACK_EXPIRES_MS = 15 * 60_000; interface CloudConnectOptions extends BaseClientOptions { noBrowser?: boolean; @@ -253,6 +254,11 @@ export function assertDiscoveryCompatible(discovery: UpstreamDiscovery): void { } } +export function resolveDeviceCodeExpiresAt(expiresAt: string | undefined, nowMs = Date.now()): number { + const parsed = typeof expiresAt === "string" ? Date.parse(expiresAt) : NaN; + return Number.isFinite(parsed) ? parsed : nowMs + DEVICE_CODE_FALLBACK_EXPIRES_MS; +} + export async function buildBundleFromLocalCompany(input: { localCompanyId: string; connection: CloudConnection; @@ -388,7 +394,7 @@ async function authorizeWithDeviceCode( deviceCode: string; userCode: string; verificationUri: string; - expiresAt: string; + expiresAt?: string; intervalSeconds?: number; }>(device.deviceCodeUrl, { method: "POST", @@ -405,9 +411,9 @@ async function authorizeWithDeviceCode( console.error(`Code: ${response.userCode}`); if (opts.openBrowser) openUrl(response.verificationUri); - const expiresAt = Date.parse(response.expiresAt); + const expiresAt = resolveDeviceCodeExpiresAt(response.expiresAt); const intervalMs = Math.max(500, (response.intervalSeconds ?? 5) * 1000); - while (!Number.isFinite(expiresAt) || Date.now() < expiresAt) { + while (Date.now() < expiresAt) { await sleep(intervalMs); try { return await requestCloudJson(device.tokenUrl, {