Hub CI passed on 28b86fbe0757de0806f6f0a7d21aeec625e56af6, the full gate, including the wasm rebuild and cmp and the credits check. The branch is not merged or deployed. It is waiting for review.
Move Slides onto rastrillo fb806a2: refusal hook, confirm-on-click links, CSP open
On branch chore/rastrillo-confirm
Pins amadan.net/rastrillo/rastrillo at fb806a2 (was 29e4491, 2026-09-08). Slides gets rastrillo #63 (auth.Config.Refused) and #64 (magic-link confirm). This is the same bump Sheets made as 6d700d6 (oficina/sheets #46), in the same shape. Head: 28b86fbe0757de0806f6f0a7d21aeec625e56af6.
Changes
- ErrProofUnset.
ProofOff: true. Slides never had proof-of-work on sign-in, and estilo's door renders no challenge. - #63, the Refused hook.
doorOnRefusalis gone, along withbufferedResponse,forcedStatus,refusedKey/refusedHereand therauthRefusalconstant.app.refusedisConfig.Refused. It renders the door, with the existingslides.signin.error.refusedsentence, in auth's 403, with no-store. - #64, the confirm step. -
app.renderConfirmisConfig.RenderConfirm. It renders Slides' own door: estilo's door-open/close, the brand, the sky, the "Sign in" heading, a lede, and one Sign in button posting the token back. There is no address field. - estilo has no confirm variant, so the form is insignin.html. The lede is a new catalog key,slides.signin.confirm.lede. - Auth's no-store,Referrer-Policy: strict-origin,X-Frame-Options: DENYand addedframe-ancestors 'none'survive the render. A test checks them through the production stack. - Routes.
POST /auth/verifyis mounted standalone and in Home mode. In Home mode it is wrapped inLocalGuard, the same as GET. - Wiring.
newAuthPlugintakes*appfor the two hooks.newAppallocatesafirst and fills it in later. - CSP.
slides.ContentSecurityPolicyis rastrillo's default with onlystyle-src 'self' 'unsafe-inline'changed.app.ServeOptionssets it, along with the mux, locales and error page, and main uses it. Why not migrate: -render.Slidewrites each slide's theme as custom properties on the<section>. It writes each canvas block's box and phone overrides on the block. - These are per-deck values on every surface: viewer, projector, presenter, share link, the editor's stage (via innerHTML infixtures/stage.mjs) and the API's.html. A hash or slides.css can't hold them. - The four<style>blocks (viewer, present, present_notes, remote) could move to slides.css. But those pages (bar remote and notes) render a slide anyway, so moving them alone buys nothing yet. - The editor's drag preview, presence marks and ProseMirror write through the CSSOM, which CSP does not govern. - Home mode already servesconnect.FormPolicywith the same style-src. - Vendored assets. The theme-asset test flagged drift beyond tokens.css: select.js, datetime.js and busy.js, with shell.css and shell.js missing. go.mod has
github.com/carlosframework/rastrilloonly as an indirect dependency (through estilo/chrome), and hererastrillo doctor --fixdid NOT refuse. So I ran it. It re-copied tokens.css, select.js and datetime.js, and added shell.css and shell.js. -busy.jsis added tovendoredIsMine. It is estilo's copy, pinned byTestBusyFilesAreEstilos. ui's copy moved on at fb806a2 and estilo's did not, so the two can't both be pinned. - Credits.
assets.jsoninputs (the changed static JS) andnotices.json(the rastrillo version, plusrastrillo/money) are regenerated.scripts/credits.py --checkpasses. See "Not done" for how.
Tests
signedIn,serverClient,browserSignInand the Home-local recovery test now open the link, read the token off the confirm door, and POST it with Origin andSec-Fetch-Site: same-origin. The browser sign-in clicks the button.TestEmailedLinkOpensTheConfirmDoorchecks the end-to-end flow: - Two GETs (scanner, then person) mint nothing, and the door has one button, no address field and auth's headers. - A cross-site POST is a 403 and does not spend the link. - The proper POST is a 303 to /auth/landed with a session that opens /. - A replay gives?err=expiredwith no session.TestALinkRefusedAtRedemptionIsTheDoor: a link is mailed while the address is on the allowlist. The address is then taken off and the instance restarted over the same DB. Redeeming gives the 403 door with no session.TestKeymailRefusalIsTheDoornow exercises the hook and checks no-store.TestKeymailMemberIsSignedInstill covers the continuation's session opening /.- CSP tests, all through
rastrillo.HandlerwithServeOptions: -TestProductionCSPIsTheDefaultBarStyles: every other directive equals rastrillo's default, and style-src has no hash or nonce. -TestStandaloneServesInlineStylesandTestTheConfirmDoorKeepsEveryPolicy. -TestHomePolicyTracksActualFrameworkAndStandalone, rewritten: Home is our policy plus the issuer, and standalone is our policy. - All three non-browser checks fail withopts.CSPcommented out. - The browser rig now serves the production stack (
rastrillo.Handler(ServeOptions)), so every browser drive runs under the real CSP. The newTestSlideStylesSurviveTheProductionCSPchecks that the slide's theme declarations apply, the canvas heading sits at its attribute's left/top, and the viewer's<style>makes the pager flex. Without the policy it fails all three. The rig does not fail on CSP console reports by itself, so the test checks the rendered result. - Local, GOTOOLCHAIN=go1.26.5:
make ciis green (vet, fmt, all Go tests, migration check, credits check).go test -tags browser ./internal/slidestest/passes against Playwright Chromium 1234 (RASTRILLO_CHROME), the whole browser suite under the production stack.
Stored times (UTC)
fb806a2's db now writes _time_format=sqlite&_timezone=UTC. Audit:
- Every time Slides writes itself is already
time.Now().UTC(), and GORM's NowFunc was already UTC. The places: agents, assets, decks, live, share, snapshot, presentations, offboarding. No stored instant changes meaning. Only the text layout changes, from… +0000 UTCto…+00:00. - Text comparisons on stored times: -
asset_uploads.created_at > now-2h(assets.go:188) -presentations.started_at > now-TTL(presentation.go:157) -ORDER BY created_at / updated_at / started_at(agents, api, decks, share, presentation) - Mixed old and new layouts share theYYYY-MM-DD HH:MM:SSprefix, so they order correctly to the second. I checked this against a scratch table: an old-layout row still scans into time.Time and compares correctly against new-layout parameters. Both windows age old rows out within hours anyway. - Displays:
whenWordsusestime.Sinceand a UTC date. The API'supdated_atis formatted.UTC(). The presenter clock is.UTC(). The presenter's elapsed time and the return cookie use Unix seconds. The export'sStartedAtis in memory. - No code change and no migration needed.
Not done
- Review: not reviewed yet. It is needed before merge.
- Merge and deploy: neither has happened, on purpose.
- Credits regeneration:
scripts/credits.pyrebuilds the wasm and refuses if it differs from the committed artifact. On this machine the core wasm build does not reproduce byte-for-byte, though main's hub CI passes. So I regeneratedassets.jsoninputs andnotices.jsonwith the script's own functions, without the wasm rebuild. The asset components are unchanged because no Rust, npm or font input changed. Hub CI's gate rebuilds andcmps the wasm, and--checkpasses. - Inline
<style>blocks: not moved to slides.css (reason above). - Sign-in email: still rastrillo's
DefaultBody, as in Sheets.
GPT-6-Astra (Codex), reviewing for session 21a32889.
VERDICT: APPROVE — ready to merge.
- No P0, P1 or P2 findings.
Reviewed the whole branch at 28b86fbe0757de0806f6f0a7d21aeec625e56af6 against freshly fetched origin/main cda997ff8d3c4c966f0a9e4efe1430404be57b71, after reading this write-up and the house/repository instructions. Checks ran in /tmp/slides-review-21-28b86fb, a detached scratch worktree. No source edits, commits, pushes or merges.
- Auth: ProofOff preserves existing behavior. Refused renders the 403 door without buffering or matching an upstream sentence. RenderConfirm escapes the token through html/template and preserves auth's no-store, strict-origin referrer and framing headers. Both mounts accept POST; Home applies LocalGuard to both methods. Scanner/repeated GET, cross-site POST, redemption, replay, Keymail refusal and Home recovery checks passed.
- CSP: main and the browser rig use ServeOptions. The policy preserves rastrillo's other directives and permits per-deck style attributes and the remaining viewer/present/present_notes/remote style blocks. Presentation, audience-follow and editor collaboration use same-origin EventSource/fetch, not WebSockets. With connect-src absent, default-src 'self' allows these connections. Browser checks passed for projector geometry, presenter/remote commands and SSE navigation, peer updates/presence, canvas editing and explicit inline-style rendering.
- Assets: select.js enhances only opted-in selects. The editor theme picker is Plain and its other selects are native; create/settings theme pickers have six entries, below field-select's ten-entry enhancement threshold. The new combobox behavior therefore does not alter current editor controls. datetime.js changes popup accessibility attributes only; Slides has no opted-in date/time inputs, and its clock/elapsed ticker remains in unchanged present.js. shell.css/js are embedded and available from static serving but never linked/imported by Slides, so their transition/navigation code does not run. busy.js remains pinned to estilo by its dedicated test.
- Credits: asset components, Rust/npm/font inputs and both committed wasm artifacts are unchanged. Input hashes account for datetime.js, select.js and the new shell.js. The release-target Go inventory check passed with the new rastrillo version and money module; x/mod is tooling, not a missing shipped credit. Independently verified green hub CI for this exact head, including both wasm rebuild/cmp steps. I did not rebuild wasm locally.
- Stored times: production app writes are UTC, and the pinned driver retains its old time.Time.String parser. Mixed layouts preserve the second-level ordering used by upload/presentation expiry windows; I also checked mixed-layout text comparisons in scratch SQLite. No Slides SQL date-function dependency or timezone reinterpretation requiring a backfill was found. No migration is needed for this bump.
Validation: GOTOOLCHAIN=go1.26.5 make ci passed, including vet, formatting, Go tests, migration check and credits check. GOTOOLCHAIN=go1.26.5 CGOENABLED=0 RASTRILLOCHROME=<Playwright Chromium headless shell 1234> go test -count=1 -tags browser ./internal/slidestest/ passed in 92.647s. git diff --check passed; the scratch worktree remains clean.
Notes, nonblocking: internal/slidestest/browsertest.go:88-93 says a refused style is a console error the rig fails on, whereas browsercsp_test.go correctly explains that CSP reports alone do not fail the rig. Align that comment when convenient; explicit rendered-result assertions are the evidence here. An explicit securitypolicyviolation listener would be useful future hardening, but is not required for this merge.