Uh oh!
There was an error while loading. Please reload this page.
fix(cli): serve says so when the auth base URL is unusable, instead of swallowing it - #10369
Conversation
…f swallowing it (#10202) `serve` resolved the auth base URL through a `??` chain and parsed it inside `try { new URL(baseUrl) } catch { /* ignore malformed baseUrl */ }`. That catch was the only witness that the configured value could not be parsed, and it discarded the witness: the deployment's own origin never reached the CSRF allow-list, boot continued, and `/api/v1/health` kept answering 200 while sign-in answered 403 INVALID_ORIGIN naming nothing. Extract the resolution into an exported seam (`resolveAuthBaseUrl`) that reports which variable supplied the value and whether it parses, plus `formatUnusableAuthBaseUrlDiagnostic` for the sentence. Resolution itself is byte-for-byte unchanged — same chain, same precedence, same `${protocol}//${host}` origin spelling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
📓 Docs Drift CheckThis PR changes 1 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 58f8e362f55abe4a1f91bd9ca0a3ea88cd849ab4 && git checkout 58f8e362f55abe4a1f91bd9ca0a3ea88cd849ab4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d23e3a00c0ad431320f598e168b40dda7bc74131 76de54c3430c1b2d79274674534c593587ea90c7 && git checkout -B drift-repro d23e3a00c0ad431320f598e168b40dda7bc74131 && git merge --no-ff 76de54c3430c1b2d79274674534c593587ea90c7
node scripts/docs-audit/affected-docs.mjs --json d23e3a00c0ad431320f598e168b40dda7bc74131
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10202
The measurement came first
The card filed this as not measured — read from source, never observed — and named the specific way it could be wrong: "whether better-auth has any behaviour of its own that masks an empty
baseURL." So the first deliverable was a real boot, not a patch.examples/app-todounderos serve,NODE_ENV=production,OS_AUTH_SECRETandOS_SECRET_KEYset,OS_TRUSTED_ORIGINS/OS_ROOT_DOMAIN/OS_BASE_URL/OS_CORS_ORIGIN/ preview mode all unset. Probed withPOST /api/v1/auth/sign-in/emailand deliberately wrong credentials, so a trusted origin answers401 INVALID_EMAIL_OR_PASSWORDand an untrusted one answers403 INVALID_ORIGIN— two distinct codes, neither of which can be confused with the429a repeated sign-up probe earns.OS_AUTH_URL=(empty)OS_AUTH_URL=https://app.example.comOS_AUTH_URLunsethttps://app.example.comhttp://localhost:PORThttp://tenant.localhost:PORThttps://tenant.localhosthttps://evil.example.net/api/v1/health·/api/v1/readyConfirmed. With a set-but-empty
OS_AUTH_URLthe deployment's own origin is refused while health and ready keep answering200. Authentication is dead and nothing says so. The three language semantics the card named all hold at head:readEnvWithDeprecationreturns''for a present-but-empty variable (packages/types/src/env.ts,preferredValue !== undefined),'' ?? xis'', andnew URL('')throws.Corrected. The card predicted
trustedOriginswould be[]and that "every origin is refused". It is not, and they are not.servepassestrustedOrigins.length ? trustedOrigins : undefined, andAuthManagersubstitutes a localhost wildcard trio (http://localhost:*,http://*.localhost:*,https://*.localhost:*) for an absent list — pre-existing behaviour, already pinned byauth-manager.test.ts. So better-auth receives a non-empty allow-list. The masking layer the filer suspected is real; it is ObjectStack's ownAuthManager, not better-auth. (better-auth never sees''either —getCanonicalOrigin()masks it tohttp://localhost:3000via||.)Worse than claimed. Column A versus column C is the finding the card did not have:
http://tenant.localhost:PORTis trusted when the variable is empty and refused when it is unset. Set-but-empty is strictly more permissive than unset — an env template rendering an absent key to the empty string silently widens a production CSRF allow-list to every localhost subdomain. Filed separately as #10366, unassigned.The fix — direction 2 only
The
catchwas the only witness that the configured base URL was unusable, and it discarded the witness:Resolution is now extracted into an exported seam —
resolveAuthBaseUrl()reports the value, which variable supplied it, and whether it parses;formatUnusableAuthBaseUrlDiagnostic()produces the sentence. The call site pushes an origin when there is one and warns when there is not.What is resolved did not change. Same chain, same precedence, same
${protocol}//${host}origin spelling (kept overURL.origin, which answers the string"null"for a non-special scheme). A set-but-emptyOS_AUTH_URLstill stops the chain exactly as before — that behaviour is now pinned by a test rather than left implicit.Direction 1 — treating empty as unset inside
readEnvWithDeprecation— is not touched, per the card and the dispatch. See the census below for the input that decision was missing.Per the dispatch, the diagnostic is a warning, not a refusal to boot: a deployment running set-but-empty today keeps starting, and now says why authentication will not work.
Verified reaching an operator, on a real boot
The dispatch made this conditional: if the message cannot reach an operator in the deployment shapes that produce the defect, stop and report the fork. It reaches them. Rebuilt CLI, same env as column A:
It lands on stderr next to the existing
StorageServicePluginproduction warning — the channel operators already read. Re-probing that same boot reproduced column A's status codes exactly, confirming the change is diagnostic-only.Census — reported, not fixed
The card named an unexamined population: how many other
readEnvWithDeprecationcall sites feed a??chain. 24 call sites outside the helper and its tests; 9 feed a??chain, of which 2 are unmitigated:cli/src/commands/serve.tsOS_AUTH_URL?? OS_BASE_URL ?? http://localhost:PORTmcp/src/plugin.tsOS_MCP_SERVER_TRANSPORT?? options.transport ?? 'stdio'''is neitherstdionorhttpcli/src/commands/serve.tsOS_PORT(flag default)?? '3000'parseInt('')→NaN(not traced further)cli/src/commands/dev.tsOS_PORT?? '3000'cli/src/commands/start.tsOS_PORT(banner)?? 3000cli/src/commands/serve.tsOS_AUTH_SECRET?? (isDev ? dev-secret : undefined)!secretcli/src/commands/start.tsOS_AUTH_SECRET?? readOrCreateAuthSecret(homeDir)mcp/src/plugin.tsOS_MCP_SERVER_NAME?? options.name ?? 'objectstack'cli/src/utils/log-level.tsOS_LOG_LEVEL?? DEFAULT_LOG_LEVEL(inresolveLogLevel)''The other 15 neutralise empty at or near the read (
|| '',if (value),=== 'false'/!== 'false',?.trim()then truthiness). One near-shape worth naming:resolveStorageLocalRootEnv()guards withvalue === undefined, so''passes through — but its consumerresolveStorageCapabilityArgusesenvRoot?.trim() || default, so it lands safely.Nine sites, two unmitigated. That is a small number, and it is an argument against reaching into the shared helper on this card's behalf.
Verification
Gate families re-derived with
node scripts/pm/dispatch-gates.mjsand no path arguments, against the real diff. Full results in the dev report on #10202.packages/clisuite: 138 files / 1525 tests passed — 138 test files exist in the worktree against 137 onmain, so the new file is the +1 and did run.Not addressed here
The localhost-wildcard-in-production behaviour that column A exposes is
AuthManager's pre-existing default and is out of scope here — filed separately as its own card, unassigned. No other issue is addressed by this PR.Verified at
76de54c34. Full gate results in the dev report on #10202.Generated by Claude Code
Generated by Claude Code