Address each component the way its reader can reach it (AP-3) - #117
Merged
Merged
Conversation
`lt dev` is URL-first: a project lives at `https://<slug>.localhost` and Caddy proxies to an opaque internal port. That promise holds for **browsers**. It does not hold for Node — on Windows `*.localhost` subdomains do not resolve at all (`dns.lookup` → ENOTFOUND, `curl` agrees), while Chromium resolves them internally without asking a resolver. So the question at each call site is not "which URL" but **"who reads it"**, and there are four answers, not two: | | example | address | |---|---|---| | a browser resolves it | Playwright `baseURL`, a printed link | the public name | | Node resolves it | readiness probes, the SSR/proxy target | **loopback** | | another binary resolves it | `cloudflared` | loopback (see "not changed") | | nobody resolves it — it is COMPARED | `APP_URL`, the Caddy vhost matcher, the tunnel `Host:` header | the public name, **untouched** | That last row is why "rewrite every internal URL to 127.0.0.1" would be wrong. `APP_URL` is the CORS allow-list and Better-Auth's `trustedOrigins` — matched as a string against the `Origin` header a browser sends, and the browser arrives from `https://<slug>.localhost`. Rewriting it would break **every login**, on a platform where only a health probe was broken before. A test pins that. ## What changed - **`internalUrl(port)`** (`dev-env.ts`) — loopback on EVERY platform, not behind an `isWindows()` branch. 127.0.0.1 works everywhere, so the path Windows depends on is the one macOS exercises daily. A branch only the other platform runs is an unchecked branch. - **`NUXT_API_URL` → loopback**, `NUXT_PUBLIC_API_URL` stays the public name. The two already existed side by side and carried the same value; nuxt-extensions' `buildLtApiUrl()` already prefers the server-only one during SSR, so no change is needed there. - **The `lt dev test` readiness probes** (`dev-test-session.ts`) probe loopback. This is the one that blocked the command outright: Node asking a name it cannot resolve timed out against a stack that was up, and bring-up aborted before Playwright started. Probing the port also measures the component rather than Caddy — the better question for "is it alive". The failure MESSAGES keep the public name, because that is what a developer opens. - **`LT_DEV_API_INTERNAL_URL` / `LT_DEV_APP_INTERNAL_URL`** are published and written to the `.lt-dev/.env` bridge, so an external runner's Node-side helper has an address it can reach. - **The consumer CLAUDE.md URL block names both**, with who each is for. It tells an agent never to assume `localhost:3000/3001`, so naming only the `*.localhost` host left an agent with a name that does not resolve for Node. This moved the patch after port allocation — it ran before, where only the name existed, which is why the ports were never passed. ## Not changed, deliberately `tunnel.ts`'s `upstreamUrl` is dialled by `cloudflared`, so it has the same problem. But pointing it at a component's port would **bypass Caddy**, which is the whole point of the tunnel. The right value is `https://127.0.0.1` with the `Host:` header unchanged (`--no-tls-verify` is already passed). I cannot verify that here — it needs cloudflared and a live tunnel — so it is recorded in the inventory rather than changed on a hunch. Two residuals in the same class, both recorded: Playwright's `request` fixture resolves `baseURL` from Node, and nuxt-extensions' `validateSession()` has no SSR guard and fetches the public URL. Both are project-dependent, neither is triggered by the framework itself. Tests: 77 suites / 1198 tests. Mutations, each turning tests red: `NUXT_API_URL` back to the public name; `APP_URL` rewritten to loopback (the mistake this change argues against); the readiness probe back to the name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AP-3 of the
lt devWindows port. The largest package, and the one that decides whetherlt dev testruns there at all.The problem, and why the obvious fix is wrong
lt devis URL-first: a project lives athttps://<slug>.localhost, Caddy proxies to an opaque internal port. That promise holds for browsers. It does not hold for Node: on Windows*.localhostdoes not resolve at all (dns.lookup→ ENOTFOUND,curlagrees), while Chromium resolves it internally without asking a resolver.The tempting fix — "rewrite every internal URL to 127.0.0.1" — breaks the product. The question at each call site is not which URL but who reads it, and there are four answers:
baseURL, a printed linkcloudflaredAPP_URL, the Caddy vhost matcher, the tunnelHost:headerAPP_URLis the CORS allow-list and Better-Auth'strustedOrigins. It is matched as a string against theOriginheader a browser sends, and the browser arrives fromhttps://<slug>.localhost. Rewriting it to loopback would break every login — on a platform where only a health probe was broken before. A test pins it, and one of the mutations below is exactly that mistake.What changed
internalUrl(port)— loopback on every platform, not behind anisWindows()branch. 127.0.0.1 works everywhere, so the path Windows depends on is the one macOS exercises daily. A branch only the other platform runs is an unchecked branch.NUXT_API_URL→ loopback,NUXT_PUBLIC_API_URLstays the public name. The two already existed side by side carrying the same value; nuxt-extensions'buildLtApiUrl()already prefers the server-only one during SSR, so nothing outside this repo needs to change.lt dev testreadiness probes probe loopback. This is the one that blocked the command outright: Node asking a name it cannot resolve timed out against a stack that was up, and bring-up aborted before Playwright started. Probing the port also measures the component rather than Caddy — the better question for "is it alive". The failure messages keep the public name, because that is what a developer opens.LT_DEV_API_INTERNAL_URL/LT_DEV_APP_INTERNAL_URLare published and written to the.lt-dev/.envbridge, so an external runner's Node-side helper has an address it can reach.localhost:3000/3001— so naming only the*.localhosthost left an agent with a name Node cannot resolve. This required moving the patch after port allocation; it ran before, where only the name existed, which is why the ports were never passed.Not changed, deliberately
tunnel.ts'supstreamUrlis dialled bycloudflaredand has the same problem. But pointing it at a component's port would bypass Caddy, which is the entire point of the tunnel. The right value ishttps://127.0.0.1with theHost:header unchanged (--no-tls-verifyis already passed). I cannot verify that here — it needs cloudflared and a live tunnel — so it is recorded in the inventory rather than changed on a hunch.Two residuals in the same class, both recorded and both project-dependent rather than framework-driven:
requestfixture resolvesbaseURLfrom Node.validateSession()has no SSR guard and fetches the public URL. Its own docblock anticipates projects calling it during SSR.Checks
tsc --noEmit,npm run lintclean. Jest: 77 suites / 1198 tests.Mutations, each turning tests red:
NUXT_API_URLback to the public nameAPP_URLrewritten to loopback — the mistake this change argues against🤖 Generated with Claude Code
https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH