Skip to content

fix(serve): echo Allow-Private-Network so hosted studio can reach loopback daemon#138

Merged
Necmttn merged 2 commits into
mainfrom
fix/129-studio-pna
Jun 7, 2026
Merged

fix(serve): echo Allow-Private-Network so hosted studio can reach loopback daemon#138
Necmttn merged 2 commits into
mainfrom
fix/129-studio-pna

Conversation

@Necmttn
Copy link
Copy Markdown
Owner

@Necmttn Necmttn commented Jun 7, 2026

Problem

Closes #129 — "cannot access studio".

Clicking open in studio opens https://ax.necmttn.com/studio/?endpoint=http://127.0.0.1:1738. The hosted (HTTPS) studio then CORS-fetches the local ax serve daemon on loopback.

Chrome's Private Network Access sends a preflight carrying Access-Control-Request-Private-Network: true whenever a public/HTTPS page targets a private/loopback address, and blocks the request unless the response echoes Access-Control-Allow-Private-Network: true. Our preflight set Allow-Origin/Methods/Headers but not the PNA header, so studio silently failed to connect.

Reproduced against the daemon — preflight returned 204 without the PNA header:

$ curl -i -X OPTIONS http://127.0.0.1:1738/api/version \
    -H 'Origin: https://ax.necmttn.com' \
    -H 'Access-Control-Request-Private-Network: true'
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://ax.necmttn.com
# no Access-Control-Allow-Private-Network

Fix

On the OPTIONS preflight, echo Access-Control-Allow-Private-Network: true — but only for an already-allowed origin (studio or local dev) and only when the request actually asks for it. Disallowed origins still get no CORS/PNA headers.

After:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://ax.necmttn.com
Access-Control-Allow-Private-Network: true

Tests

3 new cases in server.test.ts (origin+PNA → header; no PNA req → omitted; evil origin → nothing). bun run test 10/10 pass; bun run typecheck exit 0.

🤖 Generated with Claude Code

…pback daemon

Chrome's Private Network Access blocks an HTTPS page (the hosted studio at
ax.necmttn.com) from fetching a loopback address (the local `ax serve` daemon)
unless the preflight response echoes `Access-Control-Allow-Private-Network: true`.
Our CORS preflight set Allow-Origin/Methods/Headers but not the PNA header, so the
studio could not connect to the daemon ("cannot access studio").

Echo the header on OPTIONS only for an allowed origin and only when the request
carries `Access-Control-Request-Private-Network: true`.

Fixes #129

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 7, 2026

Deploying ax with  Cloudflare Pages  Cloudflare Pages

Latest commit: e877b46
Status: ✅  Deploy successful!
Preview URL: https://0a56df01.ax-62d.pages.dev
Branch Preview URL: https://fix-129-studio-pna.ax-62d.pages.dev

View logs

The CLI prints `open in studio https://ax.necmttn.com/studio/?endpoint=...`,
but ax.necmttn.com (apps/site on Cloudflare Pages) never served the studio app
there - the site-wide SPA catch-all `/* /index.html 200` swallowed `/studio/`,
rendering the marketing SPA, which has no /studio route -> "Invariant failed" ->
blank page. The daemon was never contacted (so the PNA/CORS fix alone could not
surface studio).

Stage the studio bundle into the deploy:
- scripts/build-studio.ts builds the mock studio (base /studio/, `?endpoint=`
  live-connect) and copies it (sans sourcemaps) into apps/site/public/studio.
- wire it into the site `prebuild`/`build` chain (same pattern as `cp install.sh
  public/install`), and commit the bundle (same precedent as public/install).
- add `/studio/* /studio/index.html 200` ABOVE the catch-all so studio's own
  client routes resolve to its shell while real /studio/assets/* win by static
  precedence.

Verified: served apps/site/public + a local `ax serve`, opened
/studio/?endpoint=http://127.0.0.1:1738 -> studio loads, "Live. Connected",
/api/version + /api/skills + /api/events all 200, Skill Triage populated.

Refs #129

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Necmttn Necmttn merged commit 3f7eee1 into main Jun 7, 2026
2 checks passed
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.

cannot access studio

1 participant