diff --git a/AGENTS.md b/AGENTS.md index 505736a..dc9e56f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,11 +95,19 @@ npm run deploy - Public vote totals live in the `VOTE_STORE` SQLite Durable Object. A GitHub account may hold at most one vote per loop and can switch or remove it. -- Vote writes require a valid HMAC-signed, HTTP-only session cookie plus an - exact trusted `Origin`. Never accept provider IDs, usernames, or voter keys - from browser JSON. -- Keep OAuth state in short-lived, signed, HTTP-only cookies. Keep post-login - redirects on the canonical Loop Library path. +- The here.now proxy strips browser cookies and mutation `Origin` headers, and + it follows upstream redirects. Do not build voting auth around proxied + cookies, HTTP redirect responses, or forwarded authorization headers. +- Start OAuth with a browser-generated nonce held in `sessionStorage`. Bind the + nonce and safe return path into a short-lived HMAC-signed OAuth state value. + The callback must return a no-store HTML bridge that verifies the stored + nonce before saving the signed session token and returning to the canonical + Loop Library path. +- Keep the signed session token in tab-scoped `sessionStorage` and send it only + in JSON bodies to the session and vote endpoints. Vote writes must derive the + provider ID, username, and voter key exclusively from that verified token. + Reject explicit untrusted Origins; missing Origins are expected through the + here.now proxy and remain protected by the required bearer token. - Do not expose OAuth client secrets or `SESSION_SECRET` in Worker variables, browser code, logs, or committed development files. Configure them with: @@ -116,8 +124,9 @@ npm run deploy production release. Vote controls render hidden and disabled, then appear only when `/api/votes` returns `uiEnabled: true`; missing or malformed values must remain fail-closed. -- With the launch flag off, verify the canonical GitHub start, callback, - session, vote persistence, reload, and logout flow. Change the flag to the +- With the launch flag off, verify the canonical GitHub start, nonce-bound + callback bridge, session, vote persistence, reload, and local logout flow. + Change the flag to the exact string `true` and redeploy the Worker from newest integrated `main` only after that smoke test passes. No site republish is required to reveal the controls. diff --git a/README.md b/README.md index 43e21cb..0c0c9c1 100644 --- a/README.md +++ b/README.md @@ -302,9 +302,16 @@ secrets; use `worker/.dev.vars.example` for local variable names only. Register the canonical callbacks shown in `AGENTS.md`, then deploy the Worker before the site shell because the shell calls the new auth and vote routes. +The here.now proxy does not forward browser cookies or mutation Origin headers +and follows upstream redirects. The OAuth flow therefore uses an HMAC-signed, +browser-nonce-bound state value and a no-store callback bridge. The bridge saves +the signed session token in tab-scoped `sessionStorage`; session lookup and vote +writes send it only inside same-origin JSON request bodies. + The production launch is fail-closed. Keep `VOTING_UI_ENABLED=false` while the -Worker and proxy are deployed, then complete a GitHub login, session, vote, -reload, and logout smoke test on the canonical domain. Set the value to the +Worker and proxy are deployed, then complete a GitHub login, nonce-bound +callback, session, vote, reload, and logout smoke test on the canonical domain. +Set the value to the exact string `true` and redeploy only the Worker after the smoke test passes; the already-published site will reveal voting without another site publish. diff --git a/scripts/check.mjs b/scripts/check.mjs index d9ad62f..8938b32 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -125,15 +125,15 @@ for (const value of [ assert(html.includes("Search the library")); assert(html.includes("Search by title, task, or contributor")); assert(html.includes('class="search-field"')); -assert(html.includes("styles.css?v=20260623-auth-gate")); -assert(html.includes("script.js?v=20260623-auth-gate")); +assert(html.includes("styles.css?v=20260623-proxy-auth")); +assert(html.includes("script.js?v=20260623-proxy-auth")); assert(css.includes(".search-control-label")); assert(css.includes(".search-control:hover .search-field")); assert(css.includes(".search-control:focus-within .search-field")); assert.equal((html.match(/data-here-now-credit/g) || []).length, 2); for (const page of [learnHtml, agentHtml]) { - assert(page.includes("styles.css?v=20260623-auth-gate")); - assert(page.includes("script.js?v=20260623-auth-gate")); + assert(page.includes("styles.css?v=20260623-proxy-auth")); + assert(page.includes("script.js?v=20260623-proxy-auth")); } for (const page of [html, learnHtml, agentHtml]) { const brandPosition = page.indexOf('class="brand-lockup"'); @@ -191,6 +191,13 @@ assert(rendererSource.includes('aria-label="Vote on this loop" hidden')); assert(browserScript.includes("setVotingUiVisible(body.uiEnabled === true)")); assert(css.includes(".vote-controls[hidden]")); assert(authVotesSource.includes('scope: "read:user"')); +assert(authVotesSource.includes("function authBridge")); +assert(authVotesSource.includes("readSignedValue(state")); +assert(browserScript.includes('window.sessionStorage.setItem(OAUTH_NONCE_KEY')); +assert(browserScript.includes('window.sessionStorage.getItem(VOTE_SESSION_KEY)')); +assert(browserScript.includes('url.searchParams.set("client_nonce", nonce)')); +assert(browserScript.includes("sessionToken: readVoteSessionToken()")); +assert(!authVotesSource.includes("Set-Cookie")); assert(!authVotesSource.includes("X_OAUTH")); assert(!authVotesSource.includes('"/auth/x"')); assert(!browserScript.includes("Continue with X")); diff --git a/site/agents/index.html b/site/agents/index.html index 5decd87..4a94559 100644 --- a/site/agents/index.html +++ b/site/agents/index.html @@ -76,8 +76,8 @@ href="https://signals.forwardfuture.ai/loop-library/catalog.txt" /> - - + + - +