oficina / slides Public

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. doorOnRefusal is gone, along with bufferedResponse, forcedStatus, refusedKey/refusedHere and the rauthRefusal constant. app.refused is Config.Refused. It renders the door, with the existing slides.signin.error.refused sentence, in auth's 403, with no-store.
  • #64, the confirm step. - app.renderConfirm is Config.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 in signin.html. The lede is a new catalog key, slides.signin.confirm.lede. - Auth's no-store, Referrer-Policy: strict-origin, X-Frame-Options: DENY and added frame-ancestors 'none' survive the render. A test checks them through the production stack.
  • Routes. POST /auth/verify is mounted standalone and in Home mode. In Home mode it is wrapped in LocalGuard, the same as GET.
  • Wiring. newAuthPlugin takes *app for the two hooks. newApp allocates a first and fills it in later.
  • CSP. slides.ContentSecurityPolicy is rastrillo's default with only style-src 'self' 'unsafe-inline' changed. app.ServeOptions sets it, along with the mux, locales and error page, and main uses it. Why not migrate: - render.Slide writes 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 in fixtures/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 serves connect.FormPolicy with 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/rastrillo only as an indirect dependency (through estilo/chrome), and here rastrillo doctor --fix did NOT refuse. So I ran it. It re-copied tokens.css, select.js and datetime.js, and added shell.css and shell.js. - busy.js is added to vendoredIsMine. It is estilo's copy, pinned by TestBusyFilesAreEstilos. ui's copy moved on at fb806a2 and estilo's did not, so the two can't both be pinned.
  • Credits. assets.json inputs (the changed static JS) and notices.json (the rastrillo version, plus rastrillo/money) are regenerated. scripts/credits.py --check passes. See "Not done" for how.

Tests

  • signedIn, serverClient, browserSignIn and the Home-local recovery test now open the link, read the token off the confirm door, and POST it with Origin and Sec-Fetch-Site: same-origin. The browser sign-in clicks the button.
  • TestEmailedLinkOpensTheConfirmDoor checks 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=expired with 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.
  • TestKeymailRefusalIsTheDoor now exercises the hook and checks no-store. TestKeymailMemberIsSignedIn still covers the continuation's session opening /.
  • CSP tests, all through rastrillo.Handler with ServeOptions: - TestProductionCSPIsTheDefaultBarStyles: every other directive equals rastrillo's default, and style-src has no hash or nonce. - TestStandaloneServesInlineStyles and TestTheConfirmDoorKeepsEveryPolicy. - TestHomePolicyTracksActualFrameworkAndStandalone, rewritten: Home is our policy plus the issuer, and standalone is our policy. - All three non-browser checks fail with opts.CSP commented out.
  • The browser rig now serves the production stack (rastrillo.Handler(ServeOptions)), so every browser drive runs under the real CSP. The new TestSlideStylesSurviveTheProductionCSP checks 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 ci is 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 UTC to …+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 the YYYY-MM-DD HH:MM:SS prefix, 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: whenWords uses time.Since and a UTC date. The API's updated_at is formatted .UTC(). The presenter clock is .UTC(). The presenter's elapsed time and the return cookie use Unix seconds. The export's StartedAt is 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.py rebuilds 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 regenerated assets.json inputs and notices.json with 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 and cmps the wasm, and --check passes.
  • Inline <style> blocks: not moved to slides.css (reason above).
  • Sign-in email: still rastrillo's DefaultBody, as in Sheets.

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.

GPT-6-Astra (Codex), reviewing for session 21a32889.

VERDICT: APPROVE — ready to merge.

  1. 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.