From 239930327a394b74f13318b4e9e289091860124e Mon Sep 17 00:00:00 2001 From: Tamas Kalman Date: Mon, 4 May 2026 04:31:51 -0700 Subject: [PATCH] fix(login): default apiUrl to https://usetimebook.com (no api. subdomain exists) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default `apiUrl` was `https://api.usetimebook.com`, but the production deploy on DigitalOcean App Platform only has the bare `usetimebook.com` domain — both the React app and the JSON API are served from the same origin (`/api/*`). DNS for `api.usetimebook.com` returns nothing, so `verifyToken` after the browser callback always threw a bare `TypeError: fetch failed` and the CLI bailed before writing the config — making login appear to succeed (the loopback callback printed) but never persist a token. Also wrap the `fetch` call in `lib/api.ts` to surface the underlying cause (DNS, ECONNREFUSED, cert error, …) instead of the bare `fetch failed`. This would have made the misconfiguration above trivial to diagnose. Users with an already-saved bad apiUrl can re-run with `TIMEBOOK_API_URL=https://usetimebook.com timebook login` once; the corrected value will then be persisted. --- src/lib/api.ts | 27 ++++++++++++++++++++++----- src/lib/config.ts | 5 ++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index d42ad50..c38b020 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -41,11 +41,28 @@ async function rawRequest( if (opts.body !== undefined) headers['Content-Type'] = 'application/json'; if (token) headers.Authorization = `Bearer ${token}`; - const res = await fetch(url, { - method: opts.method ?? 'GET', - headers, - body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, - }); + let res: Response; + try { + res = await fetch(url, { + method: opts.method ?? 'GET', + headers, + body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, + }); + } catch (err) { + // Node's undici throws a bare `TypeError: fetch failed` and stashes the + // real reason (DNS, ECONNREFUSED, cert errors, …) on `cause`. Surface + // both so users can tell a misconfigured API URL from a real outage. + const cause = (err as { cause?: unknown }).cause; + const reason = + cause && typeof cause === 'object' + ? ((cause as { code?: string }).code ?? (cause as Error).message ?? '') + : ''; + throw new Error( + `Network error reaching ${url.origin}${reason ? ` (${reason})` : ''}. ` + + `Check TIMEBOOK_API_URL or your connection.`, + { cause: err }, + ); + } const text = await res.text(); let data: unknown = undefined; diff --git a/src/lib/config.ts b/src/lib/config.ts index 8805aac..0844f0c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -21,8 +21,11 @@ export interface StoredConfig { createdAt?: string; } +// Production serves both the React app and the JSON API from the same +// origin (`usetimebook.com/api/*`). There is no `api.usetimebook.com` +// subdomain, so the API URL defaults to the same host as the web URL. const DEFAULT_CONFIG: StoredConfig = { - apiUrl: process.env.TIMEBOOK_API_URL ?? 'https://api.usetimebook.com', + apiUrl: process.env.TIMEBOOK_API_URL ?? 'https://usetimebook.com', webUrl: process.env.TIMEBOOK_WEB_URL ?? 'https://usetimebook.com', };