Skip to content

fix(dev-tunnel): warn when the dev server can't be embedded by the host - #198

Merged
ZacxDev merged 6 commits into
mainfrom
fix/dev-tunnel-embed-preflight
Aug 5, 2026
Merged

fix(dev-tunnel): warn when the dev server can't be embedded by the host#198
ZacxDev merged 6 commits into
mainfrom
fix/dev-tunnel-embed-preflight

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Closes #196.

The problem

civitai app dev-tunnel reported "Ready" with no errors while the app never loaded — iframe stuck at data-block-ready="false", host showing "This app didn't load in time". Nothing in the terminal said why.

The existing probeLocalDevServer is a bare TCP dial; the two other probes target the public host and read status codes only. So none of the actual causes were observable.

What this adds

An embeddability preflight that runs before the mint (so a broken setup never burns a rate-limited session) and renders immediately before the "open this URL" block — placement is the point, since a warning printed before the readiness wait would scroll away and recreate the same silent failure.

CheckEmbeddable — evidence. GETs /@vite/client with Origin: null (the origin a sandboxed iframe actually sends) through the same DialLocalDevServer the tunnel proxy uses, so what the probe measured is by construction what the tunnel will serve. Reports:

  • missing wildcard Access-Control-Allow-Origin → every ES module fetch blocked, no JS runs at all
  • a frame-ancestors CSP or any X-Frame-Options excluding civitai.com
  • a 403 on the tunneled *.civit.ai Host — Vite's DNS-rebinding check, a third failure mode the issue didn't name (found by measurement, see below)

/@vite/client doubles as Vite detection: a 200 means the remediation can name vite.config.ts; otherwise it falls back to / with generic advice.

CheckParentOrigins — heuristic. VITE_BLOCK_ALLOWED_PARENT_ORIGINS is inlined at transform time and cannot be observed over HTTP, so this mirrors Vite's own dev env resolution (.env.env.local.env.development.env.development.local, with a real process-env var beating every file). Gated to dirs holding a manifest and a package.json depending on @civitai/app-sdk, because dev-tunnel takes an explicit blockId and runs from anywhere.

Both warn and never block. One HTTP response can't rule out a proxy or a deliberately exotic setup, and hard-failing would regress flows that work today. A check that cannot observe returns no findings rather than manufacturing advice.

Real output

Against a live stock Vite 8.2.0 dev server:

⚠ Your Vite dev server will not load in the Civitai host — its modules are CORS-blocked.
  GET /@vite/client  (Origin: null)  →  200, no Access-Control-Allow-Origin header

  The host iframes your app sandboxed, so it runs at an opaque "null"
  origin. Without a wildcard CORS header every ES module fetch is blocked
  and no JavaScript runs — the iframe stays blank and the host reports
  "This app didn't load in time".

⚠ Your dev server rejects the tunnel's hostname, so requests never reach your app.
  GET /@vite/client  (Host: dev-preflight-probe.civit.ai)  →  403

Fix — in vite.config.ts (dev-only; `server.*` never affects `vite build`):

    server: {
      allowedHosts: ['localhost', '.civit.ai'],
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Content-Security-Policy': "frame-ancestors 'self' https://civitai.com",
      },
    }

Findings usually share one remediation, so each distinct fix prints once after all the evidence — before the dedupe the same eight-line snippet repeated three times and read as three unrelated problems.

Verification

The premise was measured before the predicate was written, on both Vite 6.4.3 (what page-vite pins) and 8.2.0 (the reporter's version) — identical on both:

Probe Stock Vite With page-money headers
/@vite/client, Origin: null 200, no ACAO ACAO * + CSP
Host: dev-abc123.civit.ai 403 200

The first attempt at that measurement was invalid and said so: a pre-existing Vite server already held the port, so all four runs came back byte-identical with stock and headers indistinguishable. The rerun added a /proc/<pid>/cwd ownership check before trusting any verdict. The Host: …civit.ai → 403 row is what added the third check.

  • Controls reported in pairs. Stock-Vite handler must produce findings; the scaffold header set must produce zero — so a zero is a real zero, not a probe wired to nothing.
  • Mutation sweep: 18/18 killed, each by the test that owns it (these checks share a call site, so "some test failed" wouldn't distinguish a dead guard from a noisy neighbour).
  • Live end-to-end: the real CheckEmbeddable run against real Vite servers, stock (must flag) and configured (must be clean), on both versions — all four as expected.
  • Seam guard: the CLI predicate and the page-money template are one contract in two languages with nothing relating them. internal/scaffold/dev_embed_contract_test.go renders the real template, extracts the values it emits, and requires the CLI's own check to accept them — plus a negative control, so a preflight that degenerated to "always nil" fails there instead of reading as a clean bill of health.
  • make ci green; 1442 tests run, 1440 pass, 0 fail, 2 skip (counted, not inferred from an exit code); gofmt -s -l empty; golangci-lint run ./... → 0 issues.

Two bugs the tests caught during development, both of which would have shipped:

  1. The probe initially sent a placeholder Host, which a real Vite server 403s — it would have manufactured findings for a perfectly healthy server.
  2. Case-sensitive scheme stripping made an uppercase HTTPS:// CSP source a spurious warning.

Honest limit: the full browser path — real tunnel, real host, iframe reaching data-block-ready="true"could not be verified here. The tunnel endpoint isn't exposed and the feature sits behind an author invite plus an off kill-switch. What is verified is that the CLI correctly detects the conditions the issue identifies; that those conditions were the actual cause rests on the reporter's browser diagnosis.

Scope notes

  • The scaffold half was already done. page-money (the only SDK template) emits both fixes correctly today — the reporter's app is an older one scaffolded before that landed, which is exactly why the durable fix has to be CLI-side detection that reaches apps already in the wild.
  • page-vite deliberately untouched. It has no @civitai/app-sdk, uses raw window.parent.postMessage(…, '*'), and so appears unable to complete the host's ready handshake at all — adding headers would fix its CORS half while it still never signals ready. Worth its own issue rather than a half-fix here.
  • Unrelated test hardened. TestDialLocalDevServerSpecificHost bound [::1]:0 and assumed the same port number was free on IPv4, which nothing guarantees. The new loopback listeners made it collide once in ~30 runs; it now picks a port that is genuinely IPv4-free, and was mutation-checked to confirm it still catches the regression it exists for.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DC9uE5774YN7oquYrzE4Yq

ZacxDev and others added 2 commits August 4, 2026 13:34
`civitai app dev-tunnel` reported "Ready" with no errors while the app
never loaded: the iframe stayed data-block-ready="false" and the host
showed "This app didn't load in time". The existing preflight is a bare
TCP dial and the two other probes read status codes only, so none of the
actual causes were observable from the terminal (#196).

The host iframes the tunneled dev server sandboxed (allow-scripts
allow-forms, deliberately no allow-same-origin), so it runs at an opaque
"null" origin. Add an embeddability preflight that runs before the mint
and renders immediately before the "open this URL" block:

- CheckEmbeddable (evidence): GETs /@vite/client with `Origin: null`
  through the same DialLocalDevServer the proxy uses, and reports a
  missing wildcard ACAO, a frame-ancestors CSP or X-Frame-Options that
  excludes civitai.com, and a 403 on the tunneled *.civit.ai Host.
- CheckParentOrigins (heuristic): mirrors Vite's dev env resolution to
  report a missing VITE_BLOCK_ALLOWED_PARENT_ORIGINS, which cannot be
  observed over HTTP. Gated to dirs holding a manifest AND a package.json
  depending on @civitai/app-sdk.

Both WARN and never block — one HTTP response cannot rule out a proxy or
an exotic-but-working setup, and a check that cannot observe returns no
findings rather than manufacturing advice.

Measured on real Vite dev servers before writing the predicate, on 6.4.3
and 8.2.0 alike: a stock config answers a null-origin module fetch 200
with NO Access-Control-Allow-Origin and 403s a dev-*.civit.ai Host; with
the page-money headers applied, ACAO is * and the tunneled Host is 200.
That measurement also surfaced a third failure mode the issue did not
name — the allowedHosts 403 — which is now checked.

The CLI predicate and the page-money template are one contract in two
languages, so internal/scaffold/dev_embed_contract_test.go renders the
real template, extracts the values it emits, and requires the CLI's own
check to accept them; drift on either side now fails loudly. Every guard
reports a control pair and was mutation-tested (18/18 killed, each by the
test that owns it).

Also hardens TestDialLocalDevServerSpecificHost: it bound [::1]:0 and
assumed the same port was free on IPv4, which nothing guarantees. The new
loopback listeners made that collide once; it now picks a port that is
genuinely IPv4-free.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01DC9uE5774YN7oquYrzE4Yq
Audit of #198 found four ways the preflight warned at a correctly
configured or simply-authenticated dev server. Advisory output that cries
wolf is worse than none — it teaches authors to ignore it — so each is
fixed and pinned.

- frame-ancestors OBSOLETES X-Frame-Options (CSP L3): when both are
  present browsers enforce frame-ancestors and ignore XFO. XFO is now
  only consulted when no frame-ancestors directive is present. The old
  test asserted the opposite, so the suite could not have caught this.
- Only a 2xx baseline is interpretable. A 401/403/302/5xx response came
  from something other than the app, so its headers say nothing: reading
  CORS off one reported "your modules are CORS-blocked" at servers whose
  CORS was fine, and reading the follow-up 403 blamed `allowedHosts` for
  an auth proxy that refuses every request identically. This also covers
  the hostname `--local-host` case, which 403s the baseline.
- Every Content-Security-Policy header is evaluated (Header.Values, not
  Get) since policies combine restrictively, and `'none'` is decisive
  only as the sole source. Default ports (:443/:80) now match, and ACAO
  is compared case-sensitively because CORS is a byte comparison — `NULL`
  does not match the `null` origin.
- The dotenv mirror is now verified DIFFERENTIALLY against Vite's own
  loadEnv over 26 fixtures, all matching, with a negative control proving
  the harness can detect a difference. Backtick quoting, `#` anywhere in
  an unquoted value, and ${VAR} expansion are supported; an unresolved
  reference expands to empty, not to its own text.

Two corrections to the audit itself, caught by that differential: Vite
does NOT accept `KEY: value` (it resolves to nothing, so honouring a
colon separator would find a value the app never sees and stay silent on
a broken project), and an unresolved `${NOPE}` expands to "". The first
harness reported the opposite because dotenv-expand writes into
process.env, so each case leaked its answer into the next — the tell was
an impossible constant result. Rebuilt with one child process per case.

Also pins the user-visible evidence strings, which no test covered: the
audit's independent sweep found four surviving mutants there, all now
killed. Full battery re-run after the fixes: 30/30 killed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01DC9uE5774YN7oquYrzE4Yq
@ZacxDev

ZacxDev commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Audit round 1 — fixed in 2dc2715

An adversarial audit found four ways this warned at a correctly configured or simply authenticated dev server. That's the worst failure mode for advisory output, so all four are fixed. No deploy-blocking findings.

# Finding Status
1 X-Frame-Options reported even when a permissive frame-ancestors is present fixed
2 An unconditional 401/403 attributed to CORS + allowedHosts fixed
3 Hostname --local-host cascading into a false CORS finding fixed (same 2xx gate)
4 dotenv mirror diverging from Vite in four ways fixed

1 — frame-ancestors obsoletes XFO. Per CSP L3, when both are present browsers enforce frame-ancestors and ignore XFO. A server with good CORS, a permissive frame-ancestors and X-Frame-Options: SAMEORIGIN embeds fine and was being told it wouldn't. XFO is now only consulted when no frame-ancestors directive is present. The old test asserted the opposite, so the suite structurally could not have caught this — it was pinning the bug.

2 & 3 — only a 2xx baseline is interpretable. A 401/403/302/5xx response came from something other than the app (auth proxy, deny gate, login redirect), so its headers say nothing about how the app would be served. Reading CORS off one reported "your modules are CORS-blocked" at servers whose CORS was fine; reading the follow-up 403 blamed allowedHosts for a proxy that refuses everything identically. One gate closes both, and the hostname --local-host case too.

4 — the dotenv mirror is now verified differentially, not from assumptions: 26 fixtures run through both this parser and Vite's own loadEnv, all matching, with a negative control proving the harness detects an injected difference. Backtick quoting, # anywhere in an unquoted value, and ${VAR} expansion are now supported.

Two corrections to the audit

The differential contradicted the audit on two points, and the audit was wrong:

  • Vite does not accept KEY: value. It resolves to nothing. Honouring a colon separator would make the checker find a value the app never sees — a missed problem, the wrong direction — so it is deliberately not supported.
  • An unresolved ${NOPE} expands to "", not to its own text.

Both original measurements came from a harness that ran every case in one process. dotenv-expand writes resolved values into process.env, so each case leaked its answer into the next; the tell was an impossible constant result (K= returning a URL). Rebuilt with one child process per case.

Other fixes

Every Content-Security-Policy header is now evaluated (Header.Values, not Get) since policies combine restrictively; 'none' is decisive only as the sole source; default ports :443/:80 match; and ACAO is compared case-sensitively, because CORS is a byte comparison and NULL does not match the null origin.

The audit's independent sweep also found 4 surviving mutants, all in unpinned user-visible evidence strings (the probed path, the frame-ancestors source list, the Host sent, the non-Vite fallback path). Those are now pinned. Full battery re-run after the fixes: 30/30 killed — a fix round resets the gate, so the original mutants were re-run too, not just the new ones.

Verification

make ci green; 1487 run / 1485 pass / 0 fail / 2 skip (counted, not inferred from an exit code); gofmt -s -l empty; golangci-lint run ./... → 0 issues. Live re-verified against real Vite 6.4.3 and 8.2.0, stock (flagged) and configured (clean) — all four as expected, so the fixes did not blunt the primary detection.

The honest limit is unchanged: the full browser path (real tunnel → host → data-block-ready="true") still cannot be verified here, since the tunnel endpoint isn't exposed and the feature is kill-switched.

🤖 Generated with Claude Code

Delta re-audit of the previous fix round found that the 2xx baseline gate
over-corrected: combined with refusing to follow redirects, it turned a
genuinely un-embeddable server into SILENCE. A Vite project with a `base`
path 404s /@vite/client and 302s /, so the gate saw a 302 and returned
nothing — measured: 2 findings before the gate, 0 after.

- Same-host redirects are now followed (bounded) and the FINAL response is
  judged, which is what the browser does. A cross-host Location is never
  followed: the transport always dials the local dev server, so chasing an
  external redirect would send someone else's Host to it and prove nothing.
  Vite detection is retried under a discovered base path, so a
  `base: '/app/'` project still gets the vite.config.ts remediation.
- Env files are MERGED before expanding once. Expanding per file resolved
  a cross-file `${PARENT}` to nothing and warned at a project Vite
  resolves correctly. A reference now resolves against the process env
  before the file values, `${X:-default}` is supported, a self-reference
  terminates via the visited set (not a pass counter, whose stated
  rationale was wrong and which no test pinned), and a quoted value ends
  at its closing quote so a trailing comment is not swallowed.
- Duplicate Access-Control-Allow-Origin headers are reported: more than
  one is a CORS failure in the browser whatever the values say, and
  reading only the first called a blocked server clean.
- Default ports are stripped only for the source's own scheme, so
  `https://civitai.com:80` no longer collapses onto the bare origin.

The dotenv mirror is re-verified differentially against Vite's own
loadEnv — now 38 fixtures including the cross-file, default-value,
escaped-sigil, empty-brace and self-reference cases — all matching, with
a negative control proving the harness detects an injected difference.

Mutation battery re-run in full (a fix round resets the gate): 34/34
killed. Six needed the harness fixed first — two mutants left a variable
unused and did not compile, and one CRASHED the test binary with a stack
overflow, which prints no `--- FAIL` line and so read as a survivor when
it was the strongest kill in the set.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01DC9uE5774YN7oquYrzE4Yq
@ZacxDev

ZacxDev commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Audit round 2 (delta) — fixed in 68cb8b7

The delta re-audit of 40030f1..2dc2715 confirmed all five prior findings fixed, and then found a 🔴 regression the fix round itself introduced — exactly why the round was run.

🔴 The 2xx gate over-corrected into silence

Round 1 added "only a 2xx baseline is interpretable" to stop false warnings at auth proxies. Combined with the pre-existing "don't follow redirects", it silenced a genuinely broken server: a Vite project with a base path 404s /@vite/client and 302s /, so the gate saw a 302 and returned nothing.

Measured across the fix commit, same server: [cors allowed-hosts][]. Round 1 traded a false positive for a false negative.

Fixed: same-host redirects are followed (bounded) and the final response is judged — what the browser does. A cross-host Location is never followed, because the transport always dials the local dev server and chasing an external redirect would just send someone else's Host to it. Vite detection is retried under a discovered base path, so a base: '/app/' project still gets the vite.config.ts remediation rather than generic advice.

🟡 The env mirror still warned at healthy projects

The same class round 1 claimed to close. Fixed and re-verified:

  • Cross-file ${VAR}.env defines PARENT, .env.development interpolates it. Expanding per file resolved it to nothing. Files are now merged before expanding once. This also made the evidence line print a value that existed nowhere.
  • ${X:-default} resolved to empty.
  • A quoted value with a trailing comment kept its quotes.
  • A reference now resolves against the process env before file values, and a self-reference (K=${K}x) resolves to empty — matching Vite, and terminating via the visited set rather than the pass counter, whose stated rationale was wrong and which no test pinned.

Differential re-run: 38 fixtures, all matching Vite's loadEnv, negative control intact.

Also fixed

Duplicate Access-Control-Allow-Origin headers are now reported (more than one is a CORS failure whatever the values say — reading only the first called a blocked server clean), and default ports are stripped only for the source's own scheme, so https://civitai.com:80 no longer collapses onto the bare origin.

Mutation testing — and two harness defects

34/34 killed. Six needed the harness fixed before they meant anything:

  • two mutants left a variable unused and did not compile — SKIPs that would have padded a "killed" count;
  • one crashed the test binary with a stack overflow. That prints no --- FAIL line, so my grep-based kill detection read the strongest kill in the set as a survivor.

Worth recording: the kill count was the number I'd have quoted, and it was wrong in both directions until the harness was checked against what it actually observes.

Verification

make ci green; 1520 run / 1518 pass / 0 fail / 2 skip (counted); gofmt -s -l empty; golangci-lint run ./... → 0 issues. Live re-verified against real Vite 6.4.3 and 8.2.0, stock (flagged) and configured (clean).

Remaining known gaps, not fixed

  • Expansion amplification: a 277-byte .env can expand to ~1 MB. Local, self-authored file, and dotenv-expand almost certainly does the same — inherent, not Go-specific.
  • A parseDotEnv fuzz run reported a worker failure whose minimized input replays clean; unresolved, plausibly an OOM from the above. Noted rather than claimed fixed.
  • The full browser path remains unverified — unchanged, and still the honest limit of this PR.

🤖 Generated with Claude Code

Round-3 delta audit of 2dc2715..68cb8b7. No deploy-blocking findings; the
redirect fix and the env-resolver rewrite both hold up. Folding in the
🟡 it did find, plus the residual false-warning cases.

Following redirects made a 200 reached VIA a redirect indistinguishable
from a 200 at the requested path. Any server that bounces unknown paths
to an index — an auth gate, any SPA dev server — was classified Vite and
handed vite.config.ts advice, and the evidence line asserted that
/@vite/client returned 200 when it had returned 302. isVite now re-checks
finalPath, so such a server gets the generic remediation and evidence that
names the path actually answered.

Also from the audit:

- net/http DROPS a custom Request.Host across an ABSOLUTE redirect (it
  keeps it only for relative ones), silently turning the tunnel-Host probe
  into an ordinary loopback request. It is re-applied on each same-host
  hop.
- finalPath/basePrefix returned the DECODED URL.Path and it was
  concatenated back into a URL string, so a redirect to `/a%23b/` probed
  `/a` while the evidence claimed otherwise. EscapedPath round-trips.
- bufio.Scanner's default 64 KiB line cap discards the ENTIRE .env file,
  so one long unrelated line beside a correct origins value produced a
  false warning. Pre-existing, same class, fixed here.
- The visited set stops cycles but not SHARING: `A=${B}${B}` down a chain
  doubles per level, so a tiny .env could expand to gigabytes and hang
  dev-tunnel before it printed anything. Expansion now has a byte budget.
- `${X:-default}` where X is exported but EMPTY must take the default
  (`:-` means unset-or-empty), and a default may itself contain `${...}`
  — both warned at projects Vite resolves correctly.

Three guards the audit found unpinned are now pinned, and three of my own
tests turned out not to observe what they claimed: the cross-host redirect
case asserted on a Host that the new Host-preservation code forces back
anyway, the absolute-redirect case never traversed a redirect at all, and
the expansion-budget case used a chain too short to exceed its own
threshold. All three rewritten until their mutants died.

One mutant survived as genuinely EQUIVALENT — a nesting-aware scan for the
`:-` separator cannot differ from a plain index, because a reference name
can never contain `${`. Removed rather than left as untestable complexity.

Differential against Vite's own loadEnv: 43 fixtures, all matching, with a
negative control. Full mutant battery re-run: 42/42 killed, 0 survived,
0 invalid.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01DC9uE5774YN7oquYrzE4Yq
@ZacxDev

ZacxDev commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Audit round 3 (delta) — fixed in ce43629

Delta re-audit of 2dc2715..68cb8b7. No 🔴 — round 2's redirect fix and env-resolver rewrite both hold up (env mismatches vs Vite dropped from 22/40 → 4/40 on Vite 8 and 20/40 → 6/40 on Vite 6 by the auditor's independent count). One 🟡 folded in, plus the residual false-warning cases.

🟡 A redirected 200 was credited to the path we asked for

Following redirects (the round-2 fix) made a 200 reached via a redirect indistinguishable from a 200 at the requested path. Consequences: any server that bounces unknown paths to an index — an auth gate, any SPA dev server — was classified Vite and handed vite.config.ts advice, and the evidence line asserted that /@vite/client returned 200 when it had returned 302.

isVite now re-checks finalPath. Round 2 added that helper for exactly this and applied it only on the non-Vite branch.

Also fixed

  • net/http drops a custom Request.Host across an ABSOLUTE redirect (it keeps it only for relative ones), silently turning the tunnel-Host probe into an ordinary loopback request. Re-applied on each same-host hop.
  • A decoded path was re-injected into a URL string — a redirect to /a%23b/ probed /a while the evidence claimed otherwise. EscapedPath round-trips.
  • bufio.Scanner's 64 KiB line cap discards the entire .env, so one long unrelated line beside a correct origins value produced a false warning. Pre-existing, same class, fixed here.
  • Expansion was unbounded: the visited set stops cycles but not sharing, so A=${B}${B} down a chain doubles per level and a tiny .env could expand to gigabytes and hang dev-tunnel before printing anything. Now byte-budgeted.
  • ${X:-default} with X exported-but-empty must take the default (:- means unset-or-empty), and a default may itself contain ${...} — both warned at projects Vite resolves correctly.

Three of my own tests didn't observe what they claimed

The mutation sweep caught this, not review:

  • the cross-host redirect case asserted on a Host that the new Host-preservation code forces back anyway — my own fix blinded the neighbouring guard;
  • the absolute-redirect case never traversed a redirect at all, because probed is already the final path;
  • the expansion-budget case used a 20-level chain (~1 MiB) against a 4 MiB threshold.

All three rewritten until their mutants died. Worth stating plainly: each had been passing and would have read as coverage.

One mutant survived as genuinely equivalent

A nesting-aware scan for the :- separator cannot differ from a plain index — a reference name can never contain ${, so the first :- is always the top-level one. Removed rather than left as untestable complexity.

Verification

Differential against Vite's own loadEnv: 43 fixtures, all matching, negative control intact. Full mutant battery: 42/42 killed, 0 survived, 0 invalid (the harness now treats a non-compiling mutant as INVALID and detects crash-kills, both of which previously miscounted). make ci green; 1530 run / 1528 pass / 0 fail / 2 skip; golangci-lint 0 issues. Live re-verified against real Vite 6.4.3 and 8.2.0, stock and configured.

Known and deliberately not fixed

  • Vite 8 does not transitively expand chained refs (A=${B}; B=${C}${C}) while Vite 6 and this mirror both resolve them. Divergence against Vite 8 only, in the silent-not-noisy direction.
  • KEY: value is version-dependent — Vite 6 accepts it, Vite 8 doesn't. The mirror matches Vite 8, which is what page-money pins; page-vite pins Vite 6 but carries no SDK, so the check is gated off there and the divergence is unreachable. AGENTS.md corrected — it previously stated this universally.
  • An explicit :443 port on an http:// source reports not-allowed where the CSP host-source matching rules would match it. Pathological source; pinned deliberately.
  • The full browser path remains unverified — unchanged, and still the honest limit of this PR.

🤖 Generated with Claude Code

@ZacxDev
ZacxDev merged commit 7c7e233 into main Aug 5, 2026
10 checks passed
@ZacxDev
ZacxDev deleted the fix/dev-tunnel-embed-preflight branch August 5, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dev-tunnel: silent failure when app lacks CORS headers and VITE_BLOCK_ALLOWED_PARENT_ORIGINS

1 participant