/** * CalDAV write wrappers — broker boundary enforcement (D-12). * * This file is the ONLY place outside client.ts/sync.ts/poller.ts that issues * PUT/DELETE to Fastmail via tsdav. Route handlers MUST NOT import tsdav directly * (D-12 broker boundary). The outbox worker (outboxWorker.ts) calls these functions. * * T-03-04 (Spoofing / etag forgery): etag is sourced server-side from calendarEvents.etag * by the worker — never accepted from the browser. write.ts only forwards what the * server supplies. * * T-03-05 (Elevation of privilege): Calendar ownership is enforced at the route layer * (Plan 04, V4). write.ts is a low-level primitive with no auth context. * * Status code interpretation is intentionally deferred to the caller (outboxWorker.ts): * - 201 / 204: success * - 412: conflict (D-08) → worker routes to conflict flow, no retry * - 400 / 401 / 403: hard fail → worker stops retrying immediately (D-07) * - 5xx / network error: transient → worker applies exponential backoff (D-07) * * Sources: * - https://tsdav.vercel.app/docs/caldav/createCalendarObject * - https://tsdav.vercel.app/docs/caldav/updateCalendarObject * - https://github.com/natelindev/tsdav/blob/main/src/request.ts (If-Match confirmed) */ import type { FastmailClient } from './client.js' import type { DAVCalendar } from 'tsdav' /** * Creates a new CalDAV object via PUT with If-None-Match: * (create semantics). * * @param client - Authenticated tsdav DAVClient * @param calendar - Target DAVCalendar (includes the calendar URL) * @param uid - Event UID; used as the object filename (`${uid}.ics`) * @param icsString - Full VCALENDAR/VEVENT string (from buildVeventString) * @returns Raw Response — caller interprets status codes */ export async function createCalendarEvent( client: FastmailClient, calendar: DAVCalendar, uid: string, icsString: string, ): Promise { return client.createCalendarObject({ calendar, filename: `${uid}.ics`, iCalString: icsString, }) } /** * Updates an existing CalDAV object via PUT with If-Match: (D-08 conflict check). * * @param client - Authenticated tsdav DAVClient * @param calendarObjectUrl - Full URL of the object (from calendarEvents.objectUrl) * @param icsString - Updated VCALENDAR/VEVENT string * @param etag - Cached etag from calendarEvents.etag; null is passed as '' * (tsdav: etag → If-Match header; '' = no If-Match = unconditional) * @returns Raw Response — 412 signals conflict (D-08) */ export async function updateCalendarEvent( client: FastmailClient, calendarObjectUrl: string, icsString: string, etag: string | null, ): Promise { return client.updateCalendarObject({ calendarObject: { url: calendarObjectUrl, data: icsString, etag: etag ?? '', // tsdav maps etag → If-Match header; '' skips the header (safe default) }, }) } /** * Deletes a CalDAV object via DELETE with If-Match: (D-08 conflict check). * * @param client - Authenticated tsdav DAVClient * @param calendarObjectUrl - Full URL of the object (from calendarEvents.objectUrl) * @param etag - Cached etag from calendarEvents.etag; null is passed as '' * @returns Raw Response — 412 signals conflict (D-08) */ export async function deleteCalendarEvent( client: FastmailClient, calendarObjectUrl: string, etag: string | null, ): Promise { return client.deleteCalendarObject({ calendarObject: { url: calendarObjectUrl, data: '', // tsdav deleteCalendarObject requires the DAVCalendarObject shape; data unused etag: etag ?? '', }, }) }