Skip to content

fix(funnel): magic-link/claim recovery + CLI device-flow web half + llms anon-deploy sync#215

Merged
mastermanas805 merged 1 commit into
mainfrom
fix/funnel-recovery-cli-csp
Jun 10, 2026
Merged

fix(funnel): magic-link/claim recovery + CLI device-flow web half + llms anon-deploy sync#215
mastermanas805 merged 1 commit into
mainfrom
fix/funnel-recovery-cli-csp

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Funnel-recovery + CLI device-flow fixes for the auth/claim surfaces, the llms.txt anon-deploy contract sync, and the prerender pipeline gap that silently served the un-guarded llms.txt. All five items below; tests + gate green.

F4 — magic-link "we sent a link" is no longer a silent dead-end

src/pages/LoginPage.tsx. Email delivery is 100%-failing while the Brevo sender is unvalidated (CLAUDE.md P0), so the static "check your inbox" banner trapped every magic-link user. The sent state now renders:

  • a "Didn't get it? Resend the link" affordance (re-POSTs /auth/email/start, surfaces resend errors inline), and
  • an "or continue with GitHub" fallback routing to the one working auth path.

Tests: magic-link-resend, magic-link-github-fallback, magic-link-resend-error render + behave.

F6 — point funnel recovery at the one working path (GitHub OAuth)

src/pages/ClaimPage.tsx. The /claim dead-ends — tokenless "Missing claim link" and invalid/expired token — now surface a primary "Continue with GitHub" CTA (claim-github-oauth). The expired-link state keeps "See plans" as a secondary action. Test: GitHub CTA present + triggers the OAuth redirect on both states.

D2 (web half) — finish the CLI device-flow

src/api/index.ts (new completeCliSession()), LoginPage.tsx, LoginCallbackPage.tsx.

  • LoginPage now forwards ?cli_session=<id> through the OAuth and magic-link return_to so it survives the round-trip.
  • LoginCallbackPage POSTs /auth/cli/{id}/complete with the session Bearer after the token is stored + /auth/me verifies — best-effort (a completion failure never blocks the browser user's sign-in).

Before this, App.tsx forwarded ?cli_session= to /login then dropped it; the CLI polled forever. Contract from the api agent: 200 {ok:true} flips the pending session. Test: a mocked callback with cli_session POSTs to /auth/cli/{id}/complete; absent → never called; failure → still lands on /app.

llms.txt anon-deploy sync

public/llms.txt. The canonical homepage example (anonymous "no signup" flow) told the agent to POST /deploy/new — which is RequireAuth and 401s for an anonymous caller. Corrected to POST /stacks/new (memory project_anonymous_deploy_via_stacks_not_deploy_new). Pinned with a fetch-content.mjs requireMarkers guard + an llmsContract.test.ts row so a content-repo sync can't silently revert it (rules 18/22). Also fixed a real pipeline bug: scripts/prerender.mjs Step 5 published dist/llms.txt straight from .content/, bypassing the requireMarkers guard — so the served /llms.txt reverted to stale upstream even when public/llms.txt carried the correction. It now publishes the guarded copy (verified: dist/llms.txt carries the marker post-build).

Note (memory project_live_llms_txt_built_from_content_repo): the content repo is the SoT; this PR updates the instanode-web fallback. A matching content/llms.txt PR should land to clear the build-time WARNING and make the guard a no-op.

S6 — CSP/X-Frame-Options on /app/* — VERDICT: operator/edge, NOT fixable in-repo

Verified against the actual served headers, not guessed:

$ curl -sSI https://instanode.dev/        → server: cloudflare, x-github-request-id, via: 1.1 varnish (Fastly)
$ curl -sSI https://instanode.dev/app/resources → 301 (GitHub Pages), no CSP / X-Frame-Options

The live site is GitHub Pages behind Cloudflare — there is no nginx in the live path. GitHub Pages cannot emit custom response headers, so CSP/X-Frame-Options are absent on every route, not just /app/*. The repo nginx.conf is only consumed by the Dockerfile build, which does not serve instanode.dev today. X-Frame-Options is header-only (no <meta> equivalent), so a build-only fix is impossible for the clickjacking control.

Operator action required: add a Cloudflare Transform Rule (edge response-header rule) applying the header set from nginx.conf in front of GitHub Pages.

In-repo, I documented this in nginx.conf and fixed a genuine latent nginx bug there: location / (the SPA fallback serving /app/*) redefined add_header, which makes nginx drop all server-level add_header directives — so the security headers were silently lost for every HTML route under the Docker path too. The headers are now repeated in that block.

Gate

npm run gate (= tsc --noEmit && npm run build && vitest run) — green: 81 test files, 1169 passed / 3 skipped. New mocked Playwright spec e2e/funnel-recovery.spec.ts (10 cases) passes in chromium.

🤖 Generated with Claude Code

…lms anon-deploy sync

Funnel-recovery and CLI device-flow fixes for the auth/claim surfaces, plus
the llms.txt anon-deploy contract sync and the prerender pipeline gap that was
silently serving the un-guarded llms.txt.

F4 — magic-link "we sent a link" was a silent dead-end (email delivery is
100%-failing while the Brevo sender is unvalidated). The sent state now offers
a "Resend" affordance and an "or continue with GitHub" fallback to the one
working auth path. (src/pages/LoginPage.tsx)

F6 — the /claim dead-ends (tokenless "Missing claim link" + invalid/expired
token) now surface GitHub OAuth as a primary recovery CTA. (src/pages/ClaimPage.tsx)

D2 (web half) — finish the CLI device-flow. LoginPage now forwards
?cli_session=<id> through the OAuth/magic-link return_to; LoginCallbackPage
POSTs /auth/cli/{id}/complete with the session Bearer after sign-in (new
completeCliSession() helper, best-effort — never blocks the browser user).
Before this, App.tsx forwarded the param to /login then dropped it and the CLI
polled forever. (src/api/index.ts, src/pages/LoginPage.tsx, LoginCallbackPage.tsx)

llms.txt anon-deploy sync — the canonical homepage example (anonymous "no
signup" flow) told the agent to POST /deploy/new, which is RequireAuth and 401s
for an anonymous caller; corrected to POST /stacks/new (memory
project_anonymous_deploy_via_stacks_not_deploy_new). Pinned via a fetch-content
requireMarkers guard + llmsContract.test.ts row so a content-repo sync can't
silently revert it. Also fixed scripts/prerender.mjs Step 5, which published
dist/llms.txt straight from .content/ — bypassing the requireMarkers guard so
the SERVED /llms.txt reverted to stale upstream even when public/llms.txt
carried the correction. It now publishes the guarded copy.

S6 — VERDICT: operator/edge, NOT fixable in-repo. The live site is GitHub Pages
behind Cloudflare (verified via served headers: server:cloudflare +
x-github-request-id, Fastly via:varnish, no nginx). GitHub Pages cannot emit
custom response headers, so CSP/X-Frame-Options are absent on every route, not
just /app/*. The repo nginx.conf is only used by the Dockerfile path (not live).
X-Frame-Options is header-only (no <meta> equivalent), so a build-only fix is
impossible. The live fix needs a Cloudflare Transform Rule. Documented in
nginx.conf; ALSO fixed a real latent nginx add_header-inheritance bug there
(location / redefined add_header, dropping all server-level security headers for
HTML/app routes under the Docker path).

Tests: 9 new vitest cases (LoginPage F4+D2, LoginCallbackPage D2,
ClaimPage F6, completeCliSession wrapper) + 1 llmsContract row, and a new
mocked Playwright spec e2e/funnel-recovery.spec.ts (10 cases). Gate green:
tsc --noEmit + vite build + vitest (81 files, 1169 passed / 3 skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

size-limit report 📦

Path Size
dist/assets/index-_7cTZfvv.js 0 B (-100% 🔽)
dist/assets/index-BsJUZYRr.css 6.13 KB (0%)
dist/assets/index-CnOiaZX_.js 162.55 KB (+100% 🔺)

@mastermanas805 mastermanas805 merged commit 32f4c2c into main Jun 10, 2026
19 checks passed
@mastermanas805 mastermanas805 deleted the fix/funnel-recovery-cli-csp branch June 10, 2026 04:02
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.

1 participant