diff --git a/apps/api/src/broker/write.ts b/apps/api/src/broker/write.ts index 4ab54da..82fea6b 100644 --- a/apps/api/src/broker/write.ts +++ b/apps/api/src/broker/write.ts @@ -65,6 +65,17 @@ export async function updateCalendarEvent( icsString: string, etag: string | null, ): Promise { + // WR-03: a missing etag maps to NO If-Match header → an UNCONDITIONAL PUT, which + // defeats D-08 conflict detection for exactly the rows most likely to be stale (an + // event cached before an etag was captured, or one Fastmail omitted the etag for). + // We do not block the write (it would strand the user's edit), but we make the + // unconditional-write path observable so it can be diagnosed instead of silently + // overwriting a concurrent external edit with no 412. + if (etag == null || etag === '') { + console.warn( + `[write] updateCalendarObject dispatching with NO If-Match (unconditional PUT) — conflict detection disabled for url=${calendarObjectUrl}`, + ) + } return client.updateCalendarObject({ calendarObject: { url: calendarObjectUrl, @@ -87,6 +98,13 @@ export async function deleteCalendarEvent( calendarObjectUrl: string, etag: string | null, ): Promise { + // WR-03: see updateCalendarEvent — a missing etag is an unconditional DELETE that + // bypasses D-08 conflict detection. Log so the path is observable rather than silent. + if (etag == null || etag === '') { + console.warn( + `[write] deleteCalendarObject dispatching with NO If-Match (unconditional DELETE) — conflict detection disabled for url=${calendarObjectUrl}`, + ) + } return client.deleteCalendarObject({ calendarObject: { url: calendarObjectUrl,