fix(funnel): magic-link/claim recovery + CLI device-flow web half + llms anon-deploy sync#215
Merged
Merged
Conversation
…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>
size-limit report 📦
|
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.
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:/auth/email/start, surfaces resend errors inline), andTests:
magic-link-resend,magic-link-github-fallback,magic-link-resend-errorrender + behave.F6 — point funnel recovery at the one working path (GitHub OAuth)
src/pages/ClaimPage.tsx. The/claimdead-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(newcompleteCliSession()),LoginPage.tsx,LoginCallbackPage.tsx.?cli_session=<id>through the OAuth and magic-linkreturn_toso it survives the round-trip./auth/cli/{id}/completewith the session Bearer after the token is stored +/auth/meverifies — best-effort (a completion failure never blocks the browser user's sign-in).Before this,
App.tsxforwarded?cli_session=to/loginthen dropped it; the CLI polled forever. Contract from the api agent: 200{ok:true}flips the pending session. Test: a mocked callback withcli_sessionPOSTs 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 toPOST /deploy/new— which is RequireAuth and 401s for an anonymous caller. Corrected toPOST /stacks/new(memoryproject_anonymous_deploy_via_stacks_not_deploy_new). Pinned with afetch-content.mjsrequireMarkersguard + anllmsContract.test.tsrow so a content-repo sync can't silently revert it (rules 18/22). Also fixed a real pipeline bug:scripts/prerender.mjsStep 5 publisheddist/llms.txtstraight from.content/, bypassing therequireMarkersguard — so the served/llms.txtreverted to stale upstream even whenpublic/llms.txtcarried the correction. It now publishes the guarded copy (verified:dist/llms.txtcarries the marker post-build).S6 — CSP/X-Frame-Options on /app/* — VERDICT: operator/edge, NOT fixable in-repo
Verified against the actual served headers, not guessed:
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 reponginx.confis only consumed by theDockerfilebuild, 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.confin front of GitHub Pages.In-repo, I documented this in
nginx.confand fixed a genuine latent nginx bug there:location /(the SPA fallback serving/app/*) redefinedadd_header, which makes nginx drop all server-leveladd_headerdirectives — 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 spece2e/funnel-recovery.spec.ts(10 cases) passes in chromium.🤖 Generated with Claude Code