161 lines
9.1 KiB
Markdown
161 lines
9.1 KiB
Markdown
---
|
|
phase: 03-event-write-back-pwa-install
|
|
plan: 02
|
|
type: tdd
|
|
wave: 2
|
|
depends_on: ["03-01"]
|
|
files_modified:
|
|
- apps/api/src/broker/vevent.ts
|
|
- apps/api/src/broker/write.ts
|
|
- apps/api/tests/broker/vevent.test.ts
|
|
- apps/api/tests/broker/write.test.ts
|
|
autonomous: true
|
|
requirements: [CAL-04, CAL-05, CAL-06, CAL-07]
|
|
user_setup: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- "buildVeventString produces a valid VCALENDAR/VEVENT for timed, all-day, and recurring events"
|
|
- "All-day events serialize as DATE (no time component, no TZID) per D-13 — never coerced to DATETIME"
|
|
- "createCalendarEvent / updateCalendarEvent / deleteCalendarEvent route all Fastmail writes through tsdav with correct If-Match/If-None-Match"
|
|
artifacts:
|
|
- path: "apps/api/src/broker/vevent.ts"
|
|
provides: "buildVeventString(NewEventParams) → { uid, icsString }"
|
|
exports: ["buildVeventString", "NewEventParams", "RRULE_PRESETS"]
|
|
min_lines: 40
|
|
- path: "apps/api/src/broker/write.ts"
|
|
provides: "tsdav PUT/DELETE wrappers (broker boundary, D-12)"
|
|
exports: ["createCalendarEvent", "updateCalendarEvent", "deleteCalendarEvent"]
|
|
key_links:
|
|
- from: "apps/api/src/broker/vevent.ts"
|
|
to: "ical.js ICAL.Component / ICAL.Time"
|
|
via: "VEVENT construction"
|
|
pattern: "ICAL\\.(Component|Time)"
|
|
- from: "apps/api/src/broker/write.ts"
|
|
to: "tsdav createCalendarObject/updateCalendarObject/deleteCalendarObject"
|
|
via: "FastmailClient methods"
|
|
pattern: "(create|update|delete)CalendarObject"
|
|
---
|
|
|
|
<objective>
|
|
Build the two pure broker primitives every write slice depends on: `vevent.ts`
|
|
(construct a valid iCalendar VEVENT from form params) and `write.ts` (wrap tsdav's
|
|
three CalDAV write methods to enforce the broker boundary, D-12). These are the most
|
|
testable units in the phase — defined input → defined ICS/HTTP output — so they are
|
|
built TDD against the RED stubs from Plan 01.
|
|
|
|
Purpose: CAL-04/05/06/07 all reduce to "produce the right VEVENT and PUT/DELETE it
|
|
through tsdav." Getting the D-13 DATE-vs-DATETIME split and the If-Match wiring right
|
|
here means the worker (Plan 03) and endpoints (Plan 04) just orchestrate.
|
|
|
|
Output: `vevent.ts`, `write.ts`, both GREEN against their Plan 01 test files.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md
|
|
@.planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md
|
|
@apps/api/src/broker/client.ts
|
|
@apps/api/src/broker/sync.ts
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="tdd" tdd="true">
|
|
<name>Task 1: GREEN — buildVeventString VEVENT builder (vevent.ts)</name>
|
|
<files>apps/api/src/broker/vevent.ts, apps/api/tests/broker/vevent.test.ts</files>
|
|
<read_first>
|
|
- apps/api/tests/broker/vevent.test.ts (RED stubs from Plan 01 — these define the contract)
|
|
- .planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md (§Pattern 1 — full buildVeventString reference incl. NewEventParams; §Pitfall 3 — DATE vs DATETIME)
|
|
- apps/api/src/broker/sync.ts (lines ~89-101 — the existing D-13 isDate split this must mirror in reverse)
|
|
- .planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md (§vevent.ts — ICAL import, D-13 split, error isolation)
|
|
</read_first>
|
|
<behavior>
|
|
RED → GREEN. Tests assert:
|
|
- Timed event: output contains `BEGIN:VEVENT`, `DTSTART:` with a `Z` UTC suffix (no TZID param), matching UID and SUMMARY.
|
|
- All-day event (allDay:true): DTSTART is a DATE value (`VALUE=DATE` or 8-digit YYYYMMDD with no `T`/time), NO TZID, NO time component (D-13). End is also DATE.
|
|
- Recurring: passing `rruleString: 'FREQ=WEEKLY'` yields an `RRULE:FREQ=WEEKLY` line.
|
|
- location/description optional properties appear only when provided.
|
|
- omitting `uid` generates a `<uuid>@familysync` UID via crypto.randomUUID().
|
|
</behavior>
|
|
<action>
|
|
Implement `buildVeventString(params: NewEventParams): { uid: string; icsString: string }` exactly per RESEARCH.md Pattern 1. Export the `NewEventParams` interface and a `RRULE_PRESETS` map (`daily:'FREQ=DAILY'`, `weekly:'FREQ=WEEKLY'`, `monthly:'FREQ=MONTHLY'`, `yearly:'FREQ=YEARLY'`). Use `import ICAL from 'ical.js'` and `import { randomUUID } from 'crypto'`. For all-day use `new ICAL.Time({ year, month, day, isDate: true })`; for timed use `ICAL.Time.fromJSDate(date, true)` (useUTC=true → Z suffix, no TZID). Always add VERSION 2.0 and PRODID `-//FamilySync//FamilySync//EN`. Use `.js`-suffixed relative imports if any. Never coerce DATE→DATETIME.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api test -- broker/vevent</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `pnpm --filter @familysync/api test -- broker/vevent` is GREEN (all assertions pass).
|
|
- All-day test asserts no `T000000`/time component and no `TZID` in the DATE DTSTART.
|
|
- `grep -q "RRULE_PRESETS" apps/api/src/broker/vevent.ts`.
|
|
</acceptance_criteria>
|
|
<done>buildVeventString passes all vevent.test.ts cases including the D-13 DATE-vs-DATETIME split and RRULE serialization.</done>
|
|
</task>
|
|
|
|
<task type="tdd" tdd="true">
|
|
<name>Task 2: GREEN — tsdav write wrappers (write.ts)</name>
|
|
<files>apps/api/src/broker/write.ts, apps/api/tests/broker/write.test.ts</files>
|
|
<read_first>
|
|
- apps/api/tests/broker/write.test.ts (RED stubs from Plan 01 — the contract)
|
|
- .planning/phases/03-event-write-back-pwa-install/03-RESEARCH.md (§Pattern 2 — full write.ts reference; status-code interpretation; §Pitfall 4 — etag may be null)
|
|
- apps/api/src/broker/client.ts (FastmailClient type; .js import convention; named-export style)
|
|
- .planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md (§write.ts — header/imports/exports pattern)
|
|
</read_first>
|
|
<behavior>
|
|
RED → GREEN. With a mock FastmailClient, tests assert:
|
|
- createCalendarEvent({client, calendar, uid, icsString}) calls `client.createCalendarObject` with `filename === \`${uid}.ics\`` and the iCalString, and returns the raw Response.
|
|
- updateCalendarEvent(client, calendarObjectUrl, icsString, etag) calls `client.updateCalendarObject` with calendarObject `{ url, data, etag }` — etag drives the If-Match header.
|
|
- deleteCalendarEvent(client, calendarObjectUrl, etag) calls `client.deleteCalendarObject` with `{ url, etag }`.
|
|
- A null etag is passed through as `''` (no crash).
|
|
</behavior>
|
|
<action>
|
|
Implement `createCalendarEvent`, `updateCalendarEvent`, `deleteCalendarEvent` per RESEARCH.md Pattern 2 as named exports returning `Promise<Response>`. Import `FastmailClient` from `./client.js` and `DAVCalendar` from `tsdav`. These functions are the ONLY place outside client.ts/sync.ts/poller.ts that touch tsdav write methods (D-12 broker boundary). Do not interpret status codes here — return the raw Response so the worker (Plan 03) classifies transient/hard/conflict. If `deleteCalendarObject` requires a `data` field, pass `''`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/api test -- broker/write && pnpm --filter @familysync/api exec tsc --noEmit</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `pnpm --filter @familysync/api test -- broker/write` is GREEN.
|
|
- `grep -Eq "createCalendarObject|updateCalendarObject|deleteCalendarObject" apps/api/src/broker/write.ts` (all three present).
|
|
- tsc --noEmit passes.
|
|
</acceptance_criteria>
|
|
<done>write.ts wraps all three tsdav write methods with correct filenames/If-Match wiring; tests GREEN; types compile.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| broker → Fastmail CalDAV | Only write.ts issues PUT/DELETE to Fastmail (D-12) |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-03-03 | Tampering | VEVENT field serialization (summary/location/description with special chars) | mitigate | ical.js ICAL.Component handles line-folding + escaping (commas, semicolons, newlines); never hand-roll ICS strings (RESEARCH §Don't Hand-Roll) |
|
|
| T-03-04 | Spoofing | etag forgery to bypass conflict detection | mitigate | etag is sourced server-side (calendarEvents.etag) by the worker, never accepted from the browser; write.ts only forwards what the server supplies |
|
|
| T-03-05 | Elevation of Privilege | write.ts called with another member's calendar | accept (here) | Calendar ownership is enforced at the route layer (Plan 04, V4); write.ts is a low-level primitive with no auth context |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `pnpm --filter @familysync/api test -- broker/vevent` GREEN.
|
|
- `pnpm --filter @familysync/api test -- broker/write` GREEN.
|
|
- `pnpm --filter @familysync/api exec tsc --noEmit` passes.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- VEVENT builder correct for timed, all-day (DATE), and recurring events.
|
|
- tsdav write wrappers enforce the broker boundary with correct If-Match/filename wiring.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/03-event-write-back-pwa-install/03-02-SUMMARY.md` when done.
|
|
</output>
|