oficina / slides Public

Home SSO over the deployed Slides editor open

On branch feat-home-sso

Base is the exact live bd2d16d release, not scaffold main298e. Consume the shared Home connect module through existing-session and existing-user lookup hooks. Explicit fresh local and Home confirmation links immutable identities; no email matching, account creation or sample seeding through Home. Preserve local recovery, allowlist rechecks, actors, presentation and export ownership. Local synthetic tests only. Root owns independent review, merge and release.

Candidate 433e39cdb19fb6f4052472d125eaa05c1d9b52f3 is pushed from the exact live bd2d16d editor. Full Rust, wasm artifact, JS, browser, Go and migration gate passed; focused Home race tests and Linux ARM64 build passed. Exact remote CI is running.

The adapter uses the shared Home bridge, sealed navigation, router guards and UI partial. It preserves local user IDs, existing presentation actors and export ownership. Tests exercise a real signed assertion through the mounted router with explicit local/Home confirmation, refuse replay and email matching, prove no Home-created users or sample decks, revoke both session methods and end talks on offboarding, and run the actual local magic-link recovery without sending human mail. CSP is compared against the running framework middleware. AUTOINCREMENT deletion/recreation tests prove linked IDs are not reused.

Review and release remain with the suite coordinator. Shared connect R2 contract/test amendments must be pinned before release. No merge into scaffold main and no deployment happened. Evidence and immutable source are under /home/paulca/.cache/oficina-slides-home-sso.

Model: claude-fable-5-1 (Fable 5.1).

Verdict: REQUEST CHANGES. No High or Critical findings. Every security property you asked me to inspect holds in the pinned source. Three code fixes belong in the repin cycle you already require, and one piece of test evidence is missing. Scope: this is a delta review of branch.diff plus the three pinned modules. The pre-existing editor base (bd2d16d) was not re-reviewed.

Findings

  1. Medium. Shared-module contract violation in the person lookup. LookupPerson returns errHomeIdentity for a missing or de-allowlisted user (internal/slides/home.go:57-61), but linkstore requires sql.ErrNoRows (linkstore/store.go:214-217). Resolve therefore reports ErrStorage (store.go:189-195), so a Home sign-in for a deleted or removed person renders the 503 "failed" page instead of the 403 "unknown" page (connect/bridge.go:208-214, handoff/http.go:228-231), and linking for such a person 503s too (linkstore/flows.go:86-95). Still fail-closed, but admission refusals are misreported as storage failures. Fix before repin, since the R2 contract corrections are pending.
  1. Medium. A transient database error revokes Home sessions. The Home branch of currentUser treats any homeUser error as offboarding (internal/slides/auth.go:384-391) and deletes every session row for that subject across devices (home.go:112-127). The local branch keeps the session on a read error (auth.go:396-399). Distinguish linkstore.ErrRefused from storage errors.
  1. Low. Migration ledger namespace is ad hoc. homeSchema uses home_handoff and home_connect (home.go:19) and ignores linkstore.SchemaNamespace (store.go:29). If the reviewed final module applies its own namespace, an already-migrated database would re-run CREATE TABLE and refuse boot. Settle this before the repin.
  1. Low. Home tables are created before bridge construction can refuse boot. Credentials are validated before migrate (internal/slides/app.go:260-268) but connect.New and NewRoutes run after (app.go:310). Only the DecodeCredentials failure is tested (home_test.go:186-196). Harmless empty tables, noted for completeness.
  1. Low. Host check is new and app-wide. Routes.Middleware 404s any chi route whose Host is not the origin host (connect/routes.go:69-72). Base csrf.Protect never inspected Host (csrf/csrf.go:54-67). /static/ and /api/ are mounted outside chi (app.go:213-231). Confirm the activator does not probe by IP or localhost Host.
  1. Low. Issuer form-action is on every chi response, not just Home pages (routes.go:64-68). Asset and thumbnail handlers still override their own CSP (assets.go:813, thumbs.go:290). Standalone CSP verified unchanged by home_test.go:296-336.
  1. Low. Hygiene. go.mod marks connect // indirect while it is imported directly; amadan.net imports sit inside the stdlib group in app.go, auth.go, handlers.go, render.go, presentation.go, config.go.

Verified

  • Config before migration: connect.DecodeCredentials runs first (app.go:260-268); partial config refused (connect/config.go:33-69); main.go builds the struct only when any of the four env vars is set (cmd/slides/main.go:18-22).
  • CSRF exemption is exactly POST /auth/assertion (routes.go:73-76). That handler demands a single exact issuer Origin header, form content type, no query string, bounded body (handoff/http.go:169-202). /account/home, /account/home/confirm, /signout all pass through csrf.Protect via checked. Test confirms POST /account/home without Origin is 403.
  • No-JS: Home pages are plain forms and links (connect/templates/home_link.html); all transitions are 303s.
  • Duplicate cookies: more than one session cookie is 400 on every non-callback route (routes.go:77-80); LocalGuard also rejects duplicate pending cookies (routes.go:89-97); init and proof cookies validated by homeCookie (bridge.go:103-119).
  • Local recovery uses the real rauth plugin: /signin/local → POST /signin → gateBegin → Begin; verify lands on /auth/local/complete (app.go:73-79, auth.go:262-265). Test walks the actual mailed link and asserts session rotation (home_test.go:339-411).
  • Signed assertion, replay, freshness: P-256 verifier, jti replay table, nonce binding, max_age=300 enforced at Finish and burned on failure (handoff/store.go:269-319, 324-394; bridge.go:79-93). Test signs a real assertion and proves replay refusal (home_test.go:201-289).
  • Immutable mapping: PRIMARY KEY (issuer, subject) plus UNIQUE (issuer, person_id); INSERT ON CONFLICT DO NOTHING then re-read and compare (flows.go:294-308). No update or reassign path exists. TestHomeDeletedLocalIDCannotBeReused hard-deletes (User has no soft-delete field, models.go:39-53), recreates with a higher AUTOINCREMENT id, and proves the old link cannot resolve.
  • Explicit fresh dual confirmation: link start needs a local session verified within five minutes (bridge.go:241-256, connect/api.go:56-68); Home side needs fresh auth; explicit same-origin POST /account/home/confirm (link_handlers.go:84-107). Test asserts zero links before confirm and that the local session cookie survives linking.
  • No email matching, no creation: currentUser branches on method before the allowlist and ensureUser (auth.go:384-391); Home subjects resolve only via the links table (store.go:176-200); homeUser reads by id (home.go:83-95). The mutation log confirms the test catches removal of that branch (auth-mutation.go:384, mutation.log).
  • Allowlist recheck: per request in homeUser (home.go:92) and in LookupPerson.
  • Offboarding all methods: offboard deletes Home sessions via the link join then local by subject, ends presentations (auth.go:460-500); revokeHome hands a disallowed linked user to offboard (home.go:112-118). Test confirms zero sessions, ended presentation, user row retained.
  • Actors and visibility preserved: same users row, actorFor(u) (presentation.go:254-261); deck code untouched.
  • Export ownership: owner is the email for both methods via exportPrincipal (home.go:131-146) at all four call sites (exports.go:599,612,642,673,1140); jobs renderers receive the swapped request (jobs/handlers.go:85-111) and restore the verified Home identity before viewer (exports.go:894-897, 906-908).
  • Shared UI locale: all nineteen Page keys exist in en.toml under slides.home.*; only en.toml exists.
  • Standalone path unchanged: the homeRoutes == nil branches are byte-identical to base routing and csrf.Protect.

Evidence limitations

  • arm64-final.log and arm64.log are both 0 bytes. candidate.json claims "arm64-final.log passed". The cross-build claim is unverifiable from the evidence set.
  • remote_ci was "running" at capture.
  • Race run covers internal/slides only. Mutation covers one branch.
  • I could not run tests, go mod tidy, or go vet.

Test gaps

  • Finding 1: no test that a linked-then-deleted or de-allowlisted person gets the 403 "unknown" outcome at Finish.
  • Finding 2: no transient-error test on the Home session path.
  • No HTTP-level test of a Home-method session on /d/{sqid}/edit, live-sync POSTs, /agents, or an export job end to end. Export ownership is tested only at the helper level.
  • No test for h.Signout revoking the DB row and clearing the six Home cookies.
  • No test of presenterActor through an actual /p/* handler with a Home session.
  • No test of the Middleware Host mismatch, nor of a duplicate __Host-rastrillo_pending cookie on /auth/verify.
  • No test that connect.New failure after migration refuses boot.

Note: the harness blocked writing the plan file (Write is disabled this session), so this message is the review artifact.

Model: claude-fable-5-1 (Fable 5.1). Write is disabled this session, so this message is the review artifact.

Verdict: APPROVE, with the release conditions below. No merge or deploy is authorized by this review. Scope: review-fix.diff, branch-final.diff, the frozen source-6f01b94, and the pinned connect@…868f189c5c77 module. The pre-existing editor base was not re-reviewed.

Prior findings, status in 6f01b94

| # | Prior finding | Status | Evidence | |---|---|---|---| | 1 | Medium: LookupPerson broke the sql.ErrNoRows contract | Fixed | internal/slides/home.go:64-73 returns the raw scan error (ErrNoRows for a missing row) and ErrNoRows for a de-allowlisted address; LookupLocalSession matches (home.go:54-62). linkstore maps it to ErrRefused (linkstore/store.go:193-199), bridge to the 403 "unknown" page (bridge.go:208-211). TestHomeFinishRemovedPersonIsAdmissionRefusal runs a real signed callback, then deletes or de-allowlists the person, and asserts 403 with no session cookie (home_test.go:474-525). lookup-mutation.log shows both subtests failing with 503 under the old code. | | 2 | Medium: transient read revoked all Home sessions | Fixed | auth.go:386-391 revokes only on linkstore.ErrRefused or errHomeIdentity; other errors log and return sessionNone, which viewer renders as the app 500 (decks.go:460-475). homeUser wraps non-not-found gorm errors in ErrStorage (home.go:104-109). TestHomeTransientReadFailureKeepsAllSessions injects a gorm failure on users, asserts 500, two session rows retained, cookie untouched (home_test.go:443-472). revocation-mutation.log shows it failing (303) under the old code. | | 3 | Low: ad hoc ledger namespace | Fixed | home.go:21 uses linkstore.SchemaNamespace. See condition 3. | | 4 | Low: tables created before connect.New can refuse | Unchanged, accepted | Empty tables only. | | 5 | Low: app-wide Host check | Unchanged | routes.go:70-73. Root verifying preview host. | | 6 | Low: issuer form-action on every chi response | Unchanged, accepted | routes.go:65-69. | | 7 | Low: hygiene | Fixed | go.mod:6 direct require; imports regrouped in six files. |

App logic re-verified

  • Home subjects resolve only through Bridge.Resolve and the HMAC-bound links row (connect/api.go:47-50, linkstore/store.go:180-204). currentUser never consults the allowlist by subject or calls ensureUser on the Home branch (auth.go:383-393).
  • rauth.RequireSession only checks the row exists (auth/handlers.go:204-214), so currentUser remains the admission recheck for Home sessions.
  • revokeHome hands a still-linked, de-allowlisted person to offboard, otherwise deletes every Home session for that subject and clears the cookie (home.go:130-147). offboard deletes Home sessions via the link join, local sessions by email, and ends presentations (auth.go:463-503).
  • presenterActor fails closed on any Home error (presentation.go:252-258).
  • Export ownership stays the email at all sites (exports.go:595-599, 641-647, 726, 893-896, 1139-1147); the verified Home identity is restored before viewer.
  • TestHomeEditorPresentationAndExportUseExistingOwner drives the editor, live updates GET, /agents, present, presenter page, export start, status under both Home and local sessions, and polls for a real %PDF- body (home_test.go:527-584).
  • homeLinked now creates the link through the real Start, BindNonce, Complete, Confirm flow, so person_binding is populated as in production (home_test.go:68-100).

Dependency pin

go.mod pins connect 868f189c5c77 directly; go.sum updated. The module's own go.mod requires the same handoff and rastrillo versions Slides pins, so no skew. The pinned LookupPerson contract (linkstore/store.go:218-222) is now satisfied. Shared R2 result field: APPROVE.

New findings (all Low, none blocking)

  1. Low, home.go:58 with bridge.go:235-238. A transient DB error during link start or link Finish renders the 403 "failed" page instead of 503, because existingPerson folds every LookupLocalSession error into a refusal. Fail-closed, nothing minted. The hook's contract does not require ErrNoRows, so not a violation.
  2. Low, test gaps carried forward. Still no test for h.Signout clearing the six Home cookies, for the Middleware Host mismatch, or for connect.New failing after migration.
  3. Low, evidence provenance. Both mutation overlays replace files under the .claude/worktrees/home-sso worktree, not the frozen archive. Fine if that worktree was at 6f01b94; I cannot verify.

Evidence reviewed

full-gate-final-review.log ends "gate: ok" with go vet, all Go packages ok, migration check ok. home-race-final-review.log ok under race. arm64-build-receipt.json exit 0 from source-6f01b94 with binary and archive hashes. ci.json is 433e39c only. review-final-stderr.log is empty.

Release conditions

  1. Remote CI on exact 6f01b94 must run and pass. It is blocked by hub 503 and this review does not substitute for it.
  2. Root verifies the preview host name matches the origin, since Routes.Middleware 404s any other Host. Health, api and version are outside the app mux per your note.
  3. The ledger namespace changed from home_connect (433e39c) to oficina_connect_link (final). Any database that booted 433e39c with Home configured will refuse to boot on the final with a CREATE TABLE conflict. Discard such databases or rename the two ledger rows first. candidate.json records no deployment, so this is likely moot, but check any preview DB.
  4. Activation still requires all four SLIDES_HOME_* variables and an https origin; a partial set refuses boot.

Exact final adoption 6ed0951392e64fac38ab69edfe8930274929cdb4. Source archive bound byte-for-byte by root. Mandatory local gate and exact CI terminal success; release build complete. Preview activation will preserve app-local content keys and require live canonical-origin/version/browser checks. Fable final review:

Verdict: APPROVE all five (Calendar 6874345, Slides 6ed0951, Memoria 0ee92ed, Meet 34fc367, Sheets-core 6916ac8). Reviewer: Claude Fable 5.1 (claude-fable-5-1), read-only, 2026-09-10. No files written, no commands run.

What each delta contains

Every branch.diff is confined to go.mod, go.sum, and the private-module build configuration. No Go source, template, migration, CSS, or route changed in any app. Base vs source was cross-checked through the diffs plus the CI logs on the exact binding.json SHAs.

| App | connect | estilo | GOPRIVATE | Extra | |---|---|---|---|---| | Calendar | 868f189 → 5b8d841 | new indirect dec3908 | Makefile | none | | Slides | 868f189 → 5b8d841 | new indirect dec3908 | Makefile + 10-gate | none | | Memoria | 868f189 → 5b8d841 | new indirect dec3908 | Makefile | stale go.sum rows dropped | | Meet | 868f189 → 5b8d841 | new indirect dec3908 | Makefile | connect/handoff moved indirect → direct | | Sheets-core | 868f189 → 5b8d841 | 672a447 → dec3908 direct | already present | none |

All five pin identical h1 hashes for connect 5b8d841 and estilo dec3908, matching the module cache and the approved shared-library review.

Contract compatibility, verified against the module cache

  • Line-count fingerprint of connect 868f189 vs 5b8d841 shows only view.go, oauth.go, go.mod, go.sum, routes_test.go and the two deleted templates differ. bridge.go, routes.go, config.go, api.go, link_handlers.go, navigation*.go and linkstore/ are unchanged. No security or session logic moved.
  • connect.PageData and connect.OAuthData are now type aliases to Estilo's homeconnect structs with identical field sets. Memoria's home_render.go field of type connect.PageData compiles unchanged.
  • connect.Templates() now forwards Estilo's embed FS, which still contains templates/home_link.html and templates/oauth.html. Calendar, Slides, Memoria and Meet parse templates/home_link.html; Sheets parses templates/*.html. Both patterns resolve.
  • No duplicate or omitted definitions. Grep across all five source/ trees finds no local define "home-connect", home-oauth-head or home-oauth. Each app references them exactly once from its own wrapper template. The home-oauth* definitions are only parsed and only used by Sheets.
  • Meet's move of connect and handoff from // indirect to direct is a go mod tidy correction. Meet's base source already imported connect directly.

Behavioural change inherited from the library, and why it is safe

The failed page action moved from /account/home to /signin/local. All five apps mount GET /signin/local (Calendar routes.go:40, Slides app.go:71, Memoria app.go:79, Meet app.go:220, Sheets home_routes.go:11), so the link resolves. The template also adds <bdi> around the address and rst-btn="ghost" on the Back link. Rastrillo v0.23.0 ships the ghost twin, per the Estilo review. Purely presentational.

Build configuration

Calendar, Memoria and Meet CI steps exec make <target>, so the Makefile export covers them. Slides' 10-gate is a bash script that also runs make ci, so its own export is needed and present. Sheets already carried the comma-safe form. Every CI log shows go: downloading amadan.net/oficina/estilo.git ...dec3908b237c succeeding, which settles the private-fetch infrastructure question left open by the connect review.

Gate evidence

All five *-ci-final.json files are state: success on the exact binding.json SHAs with vet, fmt, test and migration check green. Sheets adds build and race; Slides adds the full Rust, wasm and Node gate. Memoria's sibling package passed, and LoadSiblings still returns an empty roster on empty MEMORIA_SIBLINGS without requiring MEMORIA_TEAM. No connector or sync code is in the delta.

Low, non-blocking

  1. export GOPRIVATE := $(GOPRIVATE),amadan.net/oficina/estilo.git yields a leading comma when the ambient value is empty. Go ignores empty patterns. Sheets' $(if ...)$(comma) form is cleaner; align in a follow-up.
  2. Calendar's go.mod puts the estilo require in a third require block. Cosmetic tidy output.
  3. Memoria's go.sum drops golang.org/x/net and x/term rows. CI's readonly module mode passed, so nothing needed them.

Bounded config concerns

None of the preview configuration is in these deltas. Origins, consumer IDs, issuer, DEK, allowlists, env and channel are runtime values. The following remain operator-side gates before runtime merge, unchanged by this review:

  • Routes.Middleware 404s any Host that is not the configured origin, so each https://overnight-APP.bds.oncarlos.com origin must match the served hostname exactly. Verify with an unsigned GET /signin returning 303, not /healthz.
  • Each app needs its full *_HOME_* set including a dedicated stable DEK; a partial set refuses boot. Existing instance and content keys are untouched by this delta.
  • Memoria: MEMORIA_SIBLINGS and MEMORIA_TEAM both empty.
  • Slides: any preview database that booted the earlier home_connect ledger namespace must be discarded, per the Slides final verdict.
  • Refresh the synthetic UI check on these exact dependencies and run live canary checks on Sheets canary/overnight-batch, Calendar canary/overnight-recurrence, and the new Slides, Meet and Memoria canary/overnight-home before enabling any Home launch link.

Preview deployed and verified: https://overnight-slides.bds.oncarlos.com/signin . Exact source 6ed0951392e64fac38ab69edfe8930274929cdb4; process /api/version and X-Carlos-Version both 6ed0951392e64fac38ab69edfe8930274929cdb4. Exact CI and mandatory local gate passed, Fable final adoption review APPROVE. Eight synthetic rendered account-link browser checks plus four live Chromium/WebKit JS on/off redirects passed, exact consumer slides-overnight and credential origin verified. Home preview launcher destination enabled. No human credentials or mail used for tests; user completes explicit dual-proof local account connection. Existing local content identities preserved. Source branch carries previously deployed baseline work not yet squashed into scaffold/older main. Retained for review of cumulative merge provenance; preview deployment does not claim a main merge.

Reviewer: Claude Fable 5.1 (claude-fable-5-1), read-only, 2026-09-11. No files written, no commands run. Git ancestry was not re-derived; it is taken from the binding, the history files, and the Sheets merge commit that names main.

Verdicts

| App | Candidate → main | Main source merge | Public promotion | |---|---|---|---| | Sheets-core | 6916ac8 → 4aea8f4 | APPROVE | APPROVE with residual checks | | Slides | 6ed0951 → 298e00c | APPROVE | APPROVE with residual checks | | Calendar | 6874345 → ecb0df9 | APPROVE as provenance-only merge, not release approval | REQUEST CHANGES |

Sheets-core

Provenance closes. Every commit on the branch has a recorded independent verdict: reconciliation merge of main at d1ecaaf (Fable APPROVE at a75c7a9), atomic batch d35cf94 and watermark 1aca44c (Fable APPROVE each, canary-driven on the preview), Estilo theme and durable-paste integration 20c2691 (Fable APPROVE, deployed and verified), Home adapter a0227cd (Fable APPROVE), the 1baa40e follow-up (Fable APPROVE, recorded only in the cache at oficina-sheets-home-review/r2/result.json, not in discussion 26), and the pin-only 6916ac8 delta. Exact CI is green including race. Journey tests exit 0.

Seams verified in frozen source. A Home subject never reaches member creation: members.go:85-93 resolves through the link then reads by id, and page.go:47-50 returns the link label. Member creation happens only at fresh, admitted local completion in home.go:139-147. Admission is rechecked per request, a storage fault answers 503 without sign-out, and a retained Home session with Home disabled answers 403. The person hook mirrors Admits including the live-grant join.

Residual checks, none blocking:

  • Issue 8 (revoking a teammate's viewer grant leaves them editor by team standing) was ruled by Paul to stay. Codex later rated it High. Re-affirm that ruling explicitly for a public instance, since the share screen says "Access removed" while write access remains.
  • The preview SMTP repair removed two empty overrides. No real sign-in has been evidenced since. One user sign-in with no 530 in logs is the proof. Confirm the empty overrides were not a deliberate "no production mail from preview" guard.
  • Advisory items carried from the atomic review: publish-after-unlock reorder, the 1 MiB batch body being below 500 cells at 8 KiB each, and menu Clear still firing concurrent single-cell writes.
  • Reconciliation follow-ups still open: narrow-viewport title wrap, no live region on the refused-drop overlay, rename not propagating live. Issues 11 and 12 remain open and minor.
  • Do not claim multi-worksheet import. Discussions 18, 22 and 23 are separate unmerged branches.

Slides

Provenance closes. Slices 1 to 3 each carry a whole-branch review with fixes recorded. The cold Codex spec review produced six P1 and nine P2 findings; ten are fixed on the branch and five P2s are deferred with spec amendments. The bounded-log-replay fix was ported by the owner and proven load-bearing. The krilla swap kept the ABI byte-identical and the gate rebuilds and compares both wasm artifacts. The Home adapter went REQUEST CHANGES at 433e39c, APPROVE at 6f01b94 with every Medium closed, then the pin-only 6ed0951 delta. The full gate is green on the exact SHA.

Seams verified in frozen source. currentUser revokes only on a refusal or identity error and logs plus 500s on a transient fault (auth.go:383-393). Both lookup hooks honour the ErrNoRows contract. The ledger uses the shared namespace. Offboarding deletes Home sessions through the link join and ends presentations. Export ownership is restored before render.

Residual checks, none blocking:

  • Three Fable-approved side branches are not in the candidate: the Estilo theme at 2c5ac01, the save-status adoption at 6d4c90a, and the CI support at 998534c. The gate already runs the JS, bundle and browser checks, so only theme consistency is deferred.
  • Uploads answer 503 until an object store is bound. That is a platform action, not code.
  • Moving to the public origin requires the CNAME and an origin flip together. The shared middleware 404s any Host that is not the configured origin, so verify with an unsigned GET /signin returning 303 after the flip.
  • The release condition about a preview database that booted the earlier ledger namespace is moot. No 433e39c deployment is recorded. Confirm rather than assume.
  • Test gaps carried forward: sign-out clearing the Home cookies, Host mismatch, and bridge construction failing after migration.

Calendar

Home adapter closes. The r2 review at 20e6dee is APPROVE with three conditions, all since met: the shared connect revision was approved, exact CI is green at 6874345, and the preview canary passed. The seams hold in source: person.go:31-43 resolves a Home subject only through the link and never calls person setup, the local branch rechecks the allowlist, and home.go:93 cross-checks id and address.

The cumulative branch is not release-approved, and this is substantive. The whole-release Fable review at 1a6dcd0 returned CHANGES REQUIRED with three code blockers in the initial calendar code. The candidate was cut from d961b65 and contains none of the fixes. I confirmed each absence in the frozen source:

  1. Fail-open expansion. occurrences.go:59-66 returns complete on a zone or start parse error, and the walk has no exhaustion or horizon distinction. The strict reader therefore treats a malformed or overlong busy series as complete with no occurrences, and a public booking page offers that time as free. This violates ruling B23. The approved fix (b0ef4fa, delta 1a6dcd0, Fable APPROVE, CI green) is on fix/recurrence-stepping-bounds only.
  2. DST gap and reminder repair. No inverted-span handling in ExpandComplete, and updateEventOn returns without rescheduling reminders. The approved patch 95dc52d (Fable APPROVE, patch scope) is on fix/edit-reminder-reschedule only.
  3. Feeds and exports emit a master per occurrence. store_feed.go:127-150 reads the expanded range and ics_export.go:87 writes RRULE on every view. Unfixed on any branch.
  4. No DEK correctness check at boot. app.go only resolves the key. Required by the whole-release review before any rotation or restore rehearsal.

Also not in the candidate: the Estilo theme adoption at 040eb25 and the themed 404 at cb87c75, both side branches.

What I recommend. Merge 6874345 to main now as provenance reconciliation, recorded in discussion 7 as explicitly not a release approval. Then rebase the two approved fix ranges onto main, take a delta review of the rebased result, redeploy the preview, and only then promote. Blocker 3 and the DEK check need new work before public promotion. Conflict risk is low: the Home commits touch auth, person, routes, app and config, while the fixes touch occurrences, event write, override and reminder files.

Low, for the record. A de-allowlisted Home person gets 404 per request but the Home session row is never revoked in Calendar, unlike Sheets and Slides.