4.3 KiB
CAL-08: Personal Calendar ACL Spike — Decision Record
Requirement: CAL-08
Phase: 01-foundation-broker-spike
Spike script: apps/api/src/broker/spike.ts
Run: 2026-06-04, live against Fastmail account me@lucasberger.ca
Status
Decision: GO (per-member app-password model — D-09)
How to Run the Spike
cd apps/api
# build first (no tsx dependency): pnpm --filter @familysync/api build
FASTMAIL_EMAIL=<you@fastmail.com> \
FASTMAIL_APP_PASSWORD=<app-password-from-fastmail-settings> \
node dist/broker/spike.js
The script prints every calendar collection returned by Fastmail's CalDAV PROPFIND (URL, displayName, ctag, syncToken). It never logs the password.
Results
Calendars Discovered
| displayName | URL | ctag returned? | syncToken returned? |
|---|---|---|---|
| Calendar | https://caldav.fastmail.com/dav/calendars/user/me@lucasberger.ca/2180A37A-806E-11EB-872C-AE53E9CB9923/ |
yes (1615249618-218118) |
yes (data:,1615249618-218118) |
| USA Holidays | https://caldav.fastmail.com/dav/calendars/user/me@lucasberger.ca/2da291bc-7962-4a0e-94bc-7bb135c90d6e/ |
yes (1635037263-211516) |
yes (data:,1635037263-211516) |
Questions Resolved
| Question | Finding |
|---|---|
| Does one Fastmail app password see all of that account's calendars? | Yes — a single app password (scope "Mail, Contacts & Calendars") enumerated every collection owned by the account via PROPFIND. |
| Does the broker need cross-account ACL sharing to read a member's personal calendar? | No — under the locked design (D-09) each member supplies their own app password; the broker stores one encrypted credential per member and aggregates. Cross-account ACL sharing (the original CAL-08 risk) is not on the critical path. |
| Which change-detection field does Fastmail use — ctag or syncToken? | Both are returned. The poller uses ctag as the primary change signal with syncToken available as a fallback (matches the SKELETON poller design). |
| Can the broker actually fetch + cache real events (CAL-01)? | Yes — a live REPORT against the Calendar collection fetched and cached 503 events into calendar_events, with correct D-13 handling (timed events → dtstart_utc set, dtstart_date null, all_day false). |
Decision
Decision: GO
Rationale: The personal-calendar read path is proven end-to-end against live Fastmail. A single app password reaches every calendar owned by its account, and the broker successfully fetched and cached real events (CAL-01). The original CAL-08 worry — whether the broker token could see another member's personal calendar via Fastmail cross-account share+accept — is moot: the project already locked the per-member app-password model (D-09), where each member contributes their own credential. That model is validated here. The wife's personal calendar is reached the same way (her own app password), onboarded in Phase 2.
Unified-view consequence: No degradation. The unified color-coded calendar (shared + each member's personal) is achievable by storing one encrypted app password per member and aggregating their collections — no fallback to shared-family-only is needed.
Fallback (not exercised): Had a single token been required to span accounts and failed, the fallback was shared-family-only in v1 with personal overlay deferred. Not needed.
Notes
- App password scope required: "Mail, Contacts & Calendars" (Fastmail Settings → Privacy & Security → App Passwords).
- The password was passed via a gitignored
apps/api/.env.spikefor this one-off read; it is not committed anywhere. - CAL-01 proof seeded a placeholder broker user (
oidc_sub = broker-me@lucasberger.ca) in the dev MariaDB to satisfy the FK; real users are created via Authelia OIDC login. The 503 cached events remain in the dev DB and back the landing-page EventProof. - Encrypted-credential storage (
member_credentialsvia AES-256-GCM) is unit-tested; the live insert + poller-driven sync per member is wired in Phase 2 (wife onboarding) once users exist via OIDC. - Results here resolve RESEARCH Open Question 1 + Assumptions A1/A2/A3.
- Gate 2 (live Authelia login over Pangolin + SSE smoke test) remains pending operator action; tracked as HUMAN-UAT for Phase 1.