CI passed at b8894b7 (amadan ci status exit 0): vet (with -tags browser), fmt, build, go-test and race. Not merged; waiting for review, after #80.
Review: Home's own pages on the shared chrome (R2 row 8) open
On branch feat/r2-shared-chrome
Review request: Home's own signed-in pages on the shared chrome. This is estilo spec shared-chrome §12.1 row 8 (Home), plus Home findings C1, C2 and C4 from polish audit r3.
Stacked on chore/rastrillo-path (#80). I branched from it, not from main, because both branches touch the layout, tokens.css and every render call site. Stacking meant one set of conflicts instead of two. Merge #80 first, then merge origin/main into this branch before its squash, following R1-BRIEF's stacked-branch note. git diff chore/rastrillo-path..feat/r2-shared-chrome is this branch's own change.
Commits
- 535d41c
shared_chrome_test.go, red on purpose (polish-audit/r2-home-chrome-red.txt). It has seven tests: the bar on every page, the front page, Appearance, error pages split by Accept, the destination kept (C1), the hop's sky (C2) and the preloads (C4). One assertion was relaxed in b8894b7. A non-browser's GET to a POST-only path keeps its old 405 (an existing test pins it), so plain text may be 404 or 405. - b8894b7 the change.
What changes
- The bar. estilo's 52px
estilo-bar, branded Oficina, is on every page except the public front page. Stylesheets load in the spec's order (base, menu, chrome). - Directory (oficina.works), signed in. One estilo account menu: - The identity row says "Signed in", with the person glyph. The directory holds a person ref and never an address (the one rule), so it cannot show who you are. - Passkeys and Recovery codes link to id.*'s
/passkey/enroland/passkey/codes. Until now nothing linked to them. - Appearance: Light, Dark or System. It POSTs to/appearance, which sits inside Protect and outside any session requirement. The cookie is per host. - Help → Open source. - The one Sign out. - The lone Help dropdown and the hub's Sign out button are gone. - Directory, signed out. The corner says "Sign in" (
/auth/start?return_to=…). The signed-out page leaves the corner empty. - id.oficina.works. Every page is estilo's door (
estilo-door-open/close, the card on the sky) under a brand-only bar. The brand links to the directory. Nothing account-shaped renders on the credential origin. - Team home. "Your apps" sits on the sky in a full-width header band under the bar. The sky appears only on the door, this header and the front page's hero. The team chooser, Open source and the error pages are plain pages.
- Errors. estilo's error page on both origins for
Accept: text/html, with the account menu when signed in, andno-store. Other clients get plain text. Unknown hosts keep the empty 404. A template that fails to render, and the two passkeyhttp.Errorsites, now give the 500 page with a logged reference. - Titles. - Pages are titled
<Page> · Oficina. - The directory and front page are "Oficina", which was "Oficina — open your new office suite" (spec §10). - The Docs continuation gains " · Oficina". - No user-visible "Home" was left: door-copy had already removed the catalog's. The footer "Source → Home" link is left for Paul. - C1. "Use a different address" and "Start again" now go to
/signin?return_to=<the remembered return>. A bare/signincleared the return cookie, so the next link landed you on the signed-out front page. - C2. The hand-off hop keeps the bar and the sky's inline placeholder (a data: URI, so no request). Only its card waits 1.5s. It still has exactly one link, no skip link and no busy.js, as the continuation tests require.
- C4. Onest and the Oficina mark are preloaded on every page, and the six app marks on the front page.
- Not loaded: chrome.js (Escape and outside-click close). AGENTS.md's script rule prefers none, and every menu works without it. The cost, as estilo states it: an open menu closes from its own trigger. AGENTS.md now records this.
Evidence
make test-vet test-fmt test-build test-go: green.make test-browser: green.- Shared chrome check (polish-audit/r2-home-check.config.mjs; output in r2-home-check.output.txt): 691 ok, 0 FAIL. It covers 10 signed-out and 4 signed-in URLs across both origins at 1440, 840, 800, 390 and 320, plus the menu stacking, list-row colour, 404-with-bar and CLS hops. On the base branch it fails
has [estilo-bar]everywhere. - r2-home-extra.mjs: 53 ok, 0 FAIL. It checks what the shared check can't reach (output in r2-home-extra.output.txt): - id.'s account pages: brand-only bar, 52px, the right title, no Sign out and no browser-blue link. - The account menu reaches them. - The hop at 300ms shows the bar and the sky, with the card hidden. - The front page has no app bar. - C1 end to end:* Use a different address, then the second link, lands signed in on the directory. - Nothing overflows at 390 or 320. - On the base it fails (r2-home-extra-red-on-path.txt).
- CLS (r2-home-cls-chrome.output.txt): 0 on every hop. Front page cold load is 0.0028 → 0 at 1440 and 0.0034 → 0 at 390.
- Throttled cold load (120ms, 200KB/s; r2-home-flash-.output.txt): - On id./signin the first frame now has Onest and the mark. On main it painted the fallback face. - The front page's first frame now has the mark (main at 1440 had none), but is still the fallback face, because estilo's
font-display: swappaints before the font arrives. - The preload delays id.*'s first paint at that throttle (about 1.45s → 1.8s). - Screenshots: polish-audit/shots/r2-home-chrome-after[-touch]-.png. The before set is r2-home-path-after-.png, this branch's base. Both cover 19 pages at 1440, 390 and 320, plus the account menu open, both 404s signed in and out, sign-in, sent, error and after sign-out. There is also the hop at 300ms (r2-home-chrome-after-hop-1440-at300ms.png). No page overflows.
For Paul
- The directory can't say who you are (r3 item 8). Showing the address needs the assertion to carry it, which the one rule forbids at rest. As built, the menu says "Signed in".
- Appearance is per host (spec Q6). A choice made on oficina.works doesn't follow you to id.oficina.works' account pages.
- The team home heading is now "Your apps", with the lede "Every app here opens with the account you’re signed in with." That's my wording.
- Copy the audit flagged but this branch leaves alone: "Confirm it is you", the recovery page's engineering copy, "0 unused recovery codes", "Press the button…", "Select Sign out to finish…".
Review. AGENTS.md requires a whole-branch review by the highest-capability model before merge. Not reviewed yet.
🤖 Generated with Claude Code
Whole-branch review — feat/r2-shared-chrome @ b8894b7
Reviewer: Claude Fable 5.1 (claude-fable-5-1), the highest-capability model available to this session, as AGENTS.md requires. Read as one diff against origin/chore/rastrillo-path (b2f937b), on which it is stacked. Review only; nothing edited, committed, pushed or merged. Reviewed with the sign-in hub's blast radius in mind: every item in the security list below was probed on a production-shaped local stack, not only read.
VERDICT: APPROVE-WITH-NITS
Security checks (all hold)
- return_to / destination (r3 C1).
againHref(signin.go:200) builds the link fromreadReturn, which admits exactly one cookie and passes it throughcredentialReturn's closed set (/passkey/enrol,/passkey/codes,/authorize/resume); the link's target is validated again byrememberReturnon the next GET. Probed on the stack:return_to=https://evil.example,//evil.exampleand%2Fpasskey%2Fcodes%2F..%2F..%2Fxall clear the cookie; a forged__Host-oficina-return=https://evil.examplerenders a bare/signin. Not an open redirect by construction — it is a path from an allowlist, not a path from the request. - Appearance.
/appearanceis insidecsrf.Protect(directory.go:132) and outside any session group, as spec §8.3 says. Handler is estilo'schrome.HandleAppearance, which redirects via rastrillo'ssessions.SafeReturn(r, "/")— the spec's mandated validator, not a re-implementation. Probed:return_to=//evil.example/,/%09/evil.example,/%5c/evil.exampleandhttps://evil.example/all 303 to/; a cross-origin or Origin-less POST is 403; an unknownmodeis 400. The hiddenreturn_tothe menu emits isr.URL.EscapedPath()only — never the query (chrome.go:231). - Error pages. 404/405 render fixed copy; nothing from the request is in the body (a
?x=<script>404 shows only the fixed title). The 500 logsrefandr.URL.Pathonly — no query, so no token or address can reach the log — and shows the reference.Cache-Control: no-storeon both; the CSP andX-Frame-Optionsfrom rastrillo's securityHeaders are present on them. Unknown hosts still get the empty 421/404 fromserveKnownHost. Non-browsers (noAccept: text/html) keep plain text, including a plain 405 for a non-GET. - *Account menu links to id..** Built from
a.origins.Credential(config), not from the request or Host header;onCredentialcompares against the configured host afterserveKnownHosthas already refused anything else. Rendered exactly once each, and>Sign out<appears exactly once on the signed-in directory. - Sign-out chain unchanged.
signout.goandexchange.gochange only inrender/failAssertionsignatures;HOME_CONSUMERS, the 307 to id./signout and the same-origin "Finish signing out" page are as before. Probed: directoryPOST /signout→ 307 tohttps://id.home.test/signoutwith the directory cookie cleared; id.GET /signout→ the branded page. On the chain stack (home-run/chain, Home b8894b7 + Calendar origin/main), Chromium and WebKit both pass steps 1 (app Sign out → id. row revoked, next sign-in asks for the address), 2b (strict-origin page admitted) and 4 (foreign POST to id./signout refused with the branded page, session kept). The script's remaining failures are not Home's: Calendar origin/main's door-copy renamed its sign-in field and buttons and its/auth/finishnow stops the script (steps 1b/3), and step 5's confirm click returns before busy.js's 650 ms hold submits. I reproduced step 5 in isolation on this build: the click POSTs after the hold, lands onhome.test/signed-out, and the id.* cookie is gone. The chain e2e in polish-audit needs a selector/timing refresh, not this branch. - Hop and interstitial. With script:
assertion.jsstill findsform[data-assertion-handoff]and submits; the card is hidden 1.5 s. Without script:<noscript>shows the card at once with the visible Continue button; the hop still has exactly one link, no skip link, no busy.js (the existing continuation tests pin this). The sky on the hop is the inlinedata:placeholder only — no grain/filaments fetch. - Credential origin renders nothing account-authored. id.* pages carry a brand-only bar; the 500's "Try again" href is the request's own URI, HTML-escaped by the template.
Gates
GOTOOLCHAIN=go1.26.5 make test: green exceptinternal/provisionerunder-race(Linux-only; CI covered it).make test-browser: green (withRASTRILLO_CHROME, see #80 nit 3).- Shared chrome check (
r2-chrome-check.mjs+r2-home-check.config.mjs): 691 ok, 0 FAIL.r2-home-extra.mjs: 53 ok, 0 FAIL. Preloads verified real:/static/estilo/fonts/v1/onest-latin.woff2is served (33 KB) and is the one filefonts.cssnames, so the preload is used, not wasted; the brand icon preload carries the same?v=as the<img>. - Mutations, each red:
againHrefalways returning/signin→TestStartingAgainKeepsTheDestination; unwiringr.NotFound(a.notFound)on the directory →TestErrorPagesSplitByAccept(both directory cases);wantsHTMLforced false →TestErrorPagesSplitByAccept;hopSkykeeping every layer →TestHopKeepsTheSkyPlaceholder.
Findings (none blocking)
- LOW —
internal/home/chrome.go:291–293. On a 500 from a GET,data.Homeis replaced by "Try again" pointing at the failing URL, so the person loses the "Go to Oficina" way out on the one page where the app has just failed. Keep the Home link and add "Try again" beside it ifchrome.ErrorPagecan carry two links; otherwise keep Home. - LOW —
internal/home/chrome.go:233. The AppearanceReturnis the current path for every directory page, including the 404 page and POST responses (e.g. the chooser re-rendering afterPOST /apps/docs/choose). Choosing an appearance from the chooser's POST response then 303s to a POST-only path and lands on the new 404. SuggestReturn: "/"unlessr.Method == GET, and/on error pages. - LOW —
internal/home/shared_chrome_test.go:1013(TestAppearanceSetsTheTheme). It asserts the happy path only; the hostilereturn_tofallback is estilo's test to keep, but since this is the sign-in hub a two-line Home-side assertion (return_to=//evil.example→Location: /) would pin the spec's requirement where it matters most. - NIT —
internal/home/chrome.go:268.errorCopyhas entries for 404 and 500 only;errorPagewith any other status renders an empty title and body. Only those two are routed today; a comment or a fallback to 500's copy keeps the next caller honest. - NIT —
internal/home/templates/assertion.html:4–7. The comment still says "Spec shared-chrome §9.3" twice after the rewrite; harmless. - INFO — polish-audit/home-run/chain/e2e.mjs (not this branch): stale against Calendar origin/main's door copy (
#address→input[name=address], "Continue to Home"/"Connect accounts" → "Connect",summary:has-text("Account")→[estilo-account] summary) and against busy.js's hold on the id.* confirm page (wait for thePOST /signoutnavigation, notnetworkidle). The directory's Sign out is now inside the closed<details>(step 6 needs to open the summary first). I ran a scratch copy with those edits; the Home-relevant steps pass in both engines.
On the questions for Paul
- "Signed in" only. Right, and keep it. The directory holds a person ref and no address by the one rule; the only way to show who is to carry the address on the assertion into the directory session, which puts a readable address at rest on the directory side — the thing the two-origin split exists to prevent. The person glyph plus Passkeys/Recovery codes is enough of a "this is you" affordance. If it ever needs more, that is a spec change, not a chrome one.
- Appearance per host. Accept. The cookie is host-only by estilo's design (spec Q6); a parent-domain cookie would be the first thing shared between
oficina.worksandid.oficina.works, and id.'s pages are seconds long. If it grates, the cheapest fix is for id. to honourprefers-color-schemeonly, which is what it does now. - "Your apps". Fine. The lede "Every app here opens with the account you're signed in with" is a little knotty; "Every app here opens with your Oficina account." says the same in fewer words. Paul's call; not blocking.
- Preload under heavy throttle. Keep both preloads. At 200 KB/s id.* first paint moves 1.45 s → 1.8 s, but that first frame now has Onest and the mark instead of a fallback face that swaps 300 ms later; at ordinary bandwidth the cost is invisible and CLS went to 0 on every hop. The font is a single 33 KB file that every page needs anyway. If it is ever revisited, drop the font preload on hop pages first, not on the door.
- Out-of-scope copy ("Confirm it is you", the recovery page's engineering copy, "0 unused recovery codes", "Press the button…", "Select Sign out to finish…"). Agree it stays out of this branch; it is a copy pass on id.*'s door pages, small, and best done as its own branch after this merges so the diff is readable.
Merge order
Merge #80 first, then merge origin/main into this branch before its squash (the discussion already says so). AGENTS.md's new "shared chrome" paragraph is accurate against what I measured, including the chrome.js cost statement.