oficina / calendar Public

Move Calendar onto rastrillo fb806a2: refusal hook and confirm-on-click links open

On branch chore/rastrillo-confirm

Pins amadan.net/rastrillo/rastrillo at fb806a2 (was 29e4491, 2026-09-08), so Calendar gets rastrillo #63 (auth.Config.Refused) and #64 (magic-link confirm). This follows the shape of oficina/sheets #46 (6d700d6). Head: f354e49.

Changes

  • #63, the refusal hook. doorOnRefusal, its buffering writers, the authRefusal constant and the refusedHere flag are gone. handlers.refused is now Config.Refused: it renders the door with "That address can't sign in to Calendar…" in auth's 403, with no-store.
  • #64, confirm on click. - GET /auth/verify spends nothing. It renders via handlers.renderConfirm as Calendar's own door: brand, sky, the "Sign in" heading, a lede, and one Sign in button that posts the token back. There is no address field. - POST /auth/verify is mounted standalone and Home-local (inside LocalGuard). - The bar on every door, including /auth/*, shows the brand only.
  • ErrProofUnset. ProofOff: true, because Calendar never had pow on sign-in.
  • CSP. - The constant inline styles moved to calendar.css: agenda, event page, share page, and the Calendars swatch's size and shape. - The rest are per event or per calendar, so they cannot be classes: the day/week chip and slot positions (top, height, left, width in %) and --cal-hue. Standalone therefore serves ContentSecurityPolicy (serving.go, via App.ServeOptions), which is rastrillo's default with style-src 'self' 'unsafe-inline' only. - TestProductionCSPIsTheDefaultBarStyles pins the other directives to rastrillo's default. - Home mode's connect.FormPolicy already carries the same style-src, and TestHomeCSPPreservesPinnedFrameworkDirectives now checks it the same way.
  • Vendored assets. tokens.css, select.js and datetime.js are re-copied by hand from the new ui/, because doctor --fix refuses while go.mod requires the old path (native/server's presessions). - busy.js joins vendoredIsMine. It is pinned to estilo by TestBusyFilesAreEstilos, and rastrillo's copy now diverges, adding pow handling Calendar doesn't need. - shell.css and shell.js are listed as not vendored: Calendar draws estilo's chrome.
  • Credits are regenerated (new pin, plus rastrillo/money).

Stored times (_timezone=UTC and the new _time_format=sqlite layout)

  • Every scheduling instant is an int64 unix column: startutc/endutc/untilutc, dueat, retryat, claimedat, firedat, holdexpiresat, ipforgetat and the booking-mail columns. The record itself is `startlocal + tz` strings. All-day events are DATE strings. None of these is touched by the change.
  • time.Time columns are createdat, updatedat, deletedat, archivedat, revokedat and lastpolledat. Every write already uses .UTC(). - SQL: apart from IS NULL checks, the only comparison is the booking-attempt window, `bookings.createdat > now-24h, plus ORDER BY created_at on feeds. Old-layout rows (... +0000 UTC) still compare correctly against new-layout parameters to the second. The only effect is a sub-second edge on the 24h window during the first day after deploy, so there is no backfill migration. - **Display:** the share page printed a feed's created and last-polled times straight off the row, which is UTC and doesn't say so. They are now converted to the viewer's zone (TestTheSharePageSaysFeedTimesInTheViewersZone). The ICS DTSTAMP already formats .UTC()`.

Tests

  • SignInForTest, the Home-local fixture, signInOverHTTP and the browser walk now open the link, read the token off the confirm door (ConfirmFormForTest), and POST it with Origin and Sec-Fetch-Site: same-origin.
  • New TestEmailedLinkOpensTheConfirmDoor walks this sequence: 1. GET twice, as a scanner and then the person. Nothing is minted, and the door markup and headers are right. 2. A cross-site POST is refused (403) without spending the link. 3. The proper POST signs in, and the session opens a view naming the person. 4. A replay gives ?err=expired.
  • TestASessionIsRefusedForANonPermittedAddress now reopens the same database with a different allowlist and asserts the 403 refusal door with no session. Before, it used a separate database, so it never reached admission.
  • TestKeymailRefusalIsTheDoor exercises the hook in both modes, and now also checks that the admitted Keymail continuation's session opens a view.
  • TestTheConfirmDoorKeepsEveryPolicy sends the confirm door through the production stack. It checks Calendar's CSP plus auth's frame-ancestors 'none', X-Frame-Options DENY, Referrer-Policy: strict-origin and no-store.
  • New browser test TestPlacedChipsSurviveTheProductionCSP (in make browser) runs through rastrillo.Handler(app.ServeOptions(...)). It checks that a 14:00 chip sits below a 09:00 one, a 2h chip is twice as tall as a 1h one, and team and personal hues differ. With opts.CSP removed it fails on all three checks (both chips at the same top, both 42px, same hue).
  • Local runs with GOTOOLCHAIN=go1.26.5: make ci and make browser are green.

Not done

  • No review by the highest-capability model yet (AGENTS.md §Review).
  • Not merged, not deployed.

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

VERDICT: APPROVE — ready to merge.

Reviewed the whole branch at f354e496653662268f8bb22d15caaaebdc2c1e36 against freshly fetched origin/main (52b14862d83b8708320ec641047aa12eade60312), in detached scratch worktree /tmp/calendar-review-21a32889. Also traced the relevant rastrillo changes from 29e4491 to fb806a2. No source edits, commits, pushes, merges or deployments.

Findings:

  1. No blocking P0, P1 or P2 findings.

Checks and reasoning:

  • Confirm door: GET renders without spending the link; the form carries the escaped token in a hidden input and posts to the query-free local action. The signed-out door has one Sign in button and no address field. Removing the account/sign-in controls also removes their return-URL copies of the token. Calendar's renderer preserves auth's no-store, strict-origin referrer policy, DENY, and additional frame-ancestors policy through the production stack. POST redeems once; replay fails.
  • CSRF: standalone POST /auth/verify is inside csrf.Protect; Home-local POST is inside connect's protection and LocalGuard. The only Home middleware exemption is the signed assertion callback, not /auth/verify. Auth's redeem handler independently checks same origin before spending the token. The standalone cross-site/replay test and Home-local completion tests pass.
  • Refusal and Keymail: Refused runs after proof and before session minting, renders the door as a no-store 403, and does not turn query text into an admission assertion. The repaired allowlist test now reopens the same database and reaches this path. Keymail continuation/admission/refusal tests pass in both modes. SigninScreen remains off, so the new remembered-attempt/continuation machinery does not silently replace Calendar's existing continuation. ProofOff explicitly preserves the existing sign-in contract.
  • CSP: only style-src is relaxed in standalone; scripts still inherit default-src 'self', with no inline/eval allowance. Home additionally retains its already-authorized issuer in form-action. The production browser check verifies chip positions, heights and distinct hues under the actual serving policy.
  • Vendored assets: the copied files match the pinned library. select.js does change behavior: ranked/folded search, unambiguous selection on Tab/blur, and popup placement/rebuild fixes. An independent Chromium fixture using Calendar's copied JS/CSS verified zone selection with Tab and Enter, ambiguous/cancelled searches preserving the prior choice, and native form values. datetime.js changes popup accessibility attributes only. Calendar's browser gate passes with and without script. A separate reset probe found the same display issue on main and the branch; it is not introduced here.
  • UTC: event scheduling, reminder/retry/claim times, booking/hold deadlines and booking-mail scheduling remain Unix integers; local date/time strings and IANA zones remain the authored record. Metadata writes already use UTC. The relevant text operations are the booking-attempt cutoff and feed ordering, alongside null checks. A driver compatibility probe read legacy timestamps unchanged and verified a mixed-format cutoff query. The possible subsecond boundary discrepancy does not justify a backfill; I found no migration required for this bump. Share timestamps now use the viewer's zone.
  • Other upstream changes: sessions/auth migration SQL is unchanged; the migration API changes do not alter Calendar's SQL migrations. The new asset ETags and host-only HSTS are compatible. Speculation rules do not match Calendar's estilo shell. CARLOS-aware client-IP counting changes the sign-in rate-limit key from the shared proxy to the visitor as intended.

Validation: GOTOOLCHAIN=go1.26.5 make ci PASS; GOTOOLCHAIN=go1.26.5 make browser PASS (60.914s); git diff --check PASS. Remote CI reports passed for f354e49. All review validation was in scratch; no deployed canary was inspected.

Nonblocking notes, not P2s: add a direct Home-local confirm regression covering missing/cross-site Origin evidence followed by a successful redemption of the same token, and an adversarial token-escaping assertion in Calendar's own door test. The current implementation is protected; these would make future route/template changes easier to police.