/** * CAL-08 spike script: enumerate Fastmail calendar collections for a single account. * * Purpose: Confirm that a Fastmail app password ("Mail, Contacts & Calendars" scope) * can read BOTH the shared family calendar AND Lucas's personal calendar via * CalDAV PROPFIND. This is the go/no-go gate for the N-credential per-member model * described in D-02/D-05. * * Run: * cd apps/api * FASTMAIL_EMAIL=lucas@fastmail.com \ * FASTMAIL_APP_PASSWORD= \ * pnpm exec tsx src/broker/spike.ts * * Output: prints each returned calendar collection — url, displayName, ctag, syncToken. * Expected: shared family calendar + Lucas's personal calendar both appear. * * Security (T-04-04): * - App password is read from env, never logged. * - Output prints only calendar URLs and display names, not the password. * - This script is dev-only; NOT imported by the API or Docker image (T-04-SC). * * After running, record results in: * .planning/phases/01-foundation-broker-spike/CAL-08-DECISION.md */ import { createFastmailClient } from './client.js'; async function main() { const email = process.env.FASTMAIL_EMAIL; const appPassword = process.env.FASTMAIL_APP_PASSWORD; if (!email || !appPassword) { console.error( 'Usage: FASTMAIL_EMAIL= FASTMAIL_APP_PASSWORD= pnpm exec tsx src/broker/spike.ts', ); process.exit(1); } console.log(`[CAL-08 spike] Connecting to Fastmail CalDAV as: ${email}`); console.log('[CAL-08 spike] Creating tsdav client...'); const client = await createFastmailClient(email, appPassword); console.log('[CAL-08 spike] Fetching calendars via PROPFIND...'); const calendars = await client.fetchCalendars(); console.log(`\n[CAL-08 spike] Found ${calendars.length} calendar collection(s):\n`); for (const cal of calendars) { console.log('---'); console.log(` url: ${cal.url}`); // displayName may be a string or a Record (language-tagged value) per CalDAV spec const displayName = typeof cal.displayName === 'string' ? cal.displayName : JSON.stringify(cal.displayName ?? '(none)'); console.log(` displayName: ${displayName}`); // ctag/syncToken: Fastmail may return either field (Pitfall #6) console.log(` ctag: ${(cal as { ctag?: string }).ctag ?? '(not returned)'}`); console.log(` syncToken: ${(cal as { syncToken?: string }).syncToken ?? '(not returned)'}`); } console.log('\n[CAL-08 spike] Done.'); console.log('\nNext steps:'); console.log(' 1. Confirm the shared family calendar URL appears above.'); console.log(" 2. Confirm Lucas's personal calendar URL appears above."); console.log(' 3. Record both URLs and ctag/syncToken findings in:'); console.log(' .planning/phases/01-foundation-broker-spike/CAL-08-DECISION.md'); } main().catch((err: unknown) => { console.error('[CAL-08 spike] Fatal error:', err instanceof Error ? err.message : String(err)); process.exit(1); });