docs(01-04): record CAL-08 GO decision — broker read path proven live

Live spike against me@lucasberger.ca: one app password enumerated both account
calendars (PROPFIND), and a REPORT fetched + cached 503 real events into MariaDB
with correct D-13 timed/all-day handling (CAL-01). Per-member app-password model
(D-09) validated; no cross-account ACL needed; no fallback required. Gate 2 (live
Authelia/Pangolin login + SSE smoke test) remains pending operator action.
This commit is contained in:
Lucas Berger
2026-06-04 11:39:44 -04:00
parent d6f5e7d7a5
commit 0b074cd568
@@ -3,10 +3,11 @@
**Requirement:** CAL-08 **Requirement:** CAL-08
**Phase:** 01-foundation-broker-spike **Phase:** 01-foundation-broker-spike
**Spike script:** `apps/api/src/broker/spike.ts` **Spike script:** `apps/api/src/broker/spike.ts`
**Run:** 2026-06-04, live against Fastmail account `me@lucasberger.ca`
## Status ## Status
**Decision: [PENDING — fill in after running the spike]** **Decision: GO** (per-member app-password model — D-09)
--- ---
@@ -14,50 +15,63 @@
```bash ```bash
cd apps/api cd apps/api
FASTMAIL_EMAIL=<lucas@fastmail.com> \ # build first (no tsx dependency): pnpm --filter @familysync/api build
FASTMAIL_EMAIL=<you@fastmail.com> \
FASTMAIL_APP_PASSWORD=<app-password-from-fastmail-settings> \ FASTMAIL_APP_PASSWORD=<app-password-from-fastmail-settings> \
pnpm exec tsx src/broker/spike.ts node dist/broker/spike.js
``` ```
The script will print every calendar collection returned by Fastmail's CalDAV PROPFIND. The script prints every calendar collection returned by Fastmail's CalDAV PROPFIND (URL,
displayName, ctag, syncToken). It never logs the password.
--- ---
## Results ## Results
> Fill in after running the spike.
### Calendars Discovered ### Calendars Discovered
| displayName | URL | ctag returned? | syncToken returned? | | displayName | URL | ctag returned? | syncToken returned? |
|-------------|-----|----------------|---------------------| |-------------|-----|----------------|---------------------|
| (fill in) | (fill in) | (fill in) | (fill in) | | 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 ### Questions Resolved
| Question | Finding | | Question | Finding |
|----------|---------| |----------|---------|
| Does Lucas's app password see the shared family calendar? | (fill in) | | 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 Lucas's app password see Lucas's personal calendar? | (fill in) | | 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? | (fill in) | | 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
**Decision: GO / NO-GO** (circle one — delete the other after filling in) **Decision: GO**
**Rationale:** (fill in after seeing the spike output) **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.
**If NO-GO — Fallback:** **Unified-view consequence:** No degradation. The unified color-coded calendar (shared +
Proceed with shared-family-only calendar in Phase 1. Personal calendar overlay moves to v1.x. each member's personal) is achievable by storing one encrypted app password per member and
Document the Fastmail restriction that prevented per-member personal calendar access. 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 ## Notes
- App password scope required: "Mail, Contacts & Calendars" - App password scope required: "Mail, Contacts & Calendars" (Fastmail Settings → Privacy & Security → App Passwords).
- Fastmail Settings → Privacy & Security → App Passwords - The password was passed via a gitignored `apps/api/.env.spike` for this one-off read; it is not committed anywhere.
- The password is passed via env for this one-off read-only enumeration; 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.
- Results here resolve RESEARCH Open Questions 1 + Assumptions A1/A2/A3 - Encrypted-credential storage (`member_credentials` via 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.