Calendar: CalDAV accounts (client) with a derived cache — no local .ics copies #40

Closed
opened 2026-09-24 15:12:47 +00:00 by kayg · 8 comments
Owner

Owner decision C1 (DESIGN §30): connect any CalDAV provider (iCloud, Fastmail, Nextcloud, Google via CalDAV, …) like NotePlan.

  • Truth stays at the provider. calternal keeps a derived cache in the index (never files, rebuildable), so views are instant, offline reads work, ⌘K and agents can search events.
  • Refresh with CalDAV sync-collection (RFC 6578) every few minutes, when a view opens, and on demand; honour provider rate limits.
  • Writes (create/edit/delete/move, RSVP where supported) go straight to the provider with If-Match ETags; on 412 re-fetch and show the conflict; the cache updates after success.
  • Staleness is visible: UI "as of 2 min ago"; CLI calternal events [--from --to] [--fresh] where --fresh forces a live fetch.
  • Credentials: app-specific passwords / OAuth where the provider supports it, stored encrypted at rest in the index (security state), never in files or logs; per-account enable/disable, per-calendar colour and visibility.
  • Recurrence (RRULE/EXDATE/overrides) and VTIMEZONE handled via the calcard crate; test against real-world iCloud/Fastmail/Google samples.
  • Adversarial pass after merge (malformed ICS, huge calendars, provider timeouts).

Context for the owning job

  • Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§9, §17, §24, §25, §29, §30) first.
  • Vocabulary: a log entry is a retrospective - HH:MM[ - HH:MM] title #tags ^blockid line under ## 📝 Log in a daily note (Notes/Journal/YYYYMMDD-dailynote.md); an event is a scheduled calendar item from an external CalDAV provider. calternal is not a calendar store: log entries are authoritative for calternal's own time data; external events live at their provider.
  • calternal started as a lifelogging app and stays one; Calendar is the lifelog hub (everything by date). The mode tray defaults to Files, Calendar, Photos.
  • Owner rules: file over app; server is the single writer; data loss is unacceptable; performance first but never at the cost of finesse; UI copies Apple Calendar's structure (the owner shared macOS Calendar Week and Day screenshots: Day/Week/Month/Year segmented control top centre, large "September 2026" title with the month bold, ‹ Today › pager top right, all-day lane, hour grid with 09:00-style labels, red circle on today's date, event blocks with a coloured left rule and title/location/time lines, Day view with a mini month and an inspector on the right) plus Fantastical and BusyCal day/week ideas, rendered in the calternal.js design system (copy components verbatim, Claude reviews screenshots side by side); never ship sample/mock data; atomic commits; adversarial testing after API work; good enough, not perfect.
  • Comment on this issue when you start (branch, base SHA), on each finding, when blocked, and when finished (head SHA + gate output). Never close it.
Owner decision C1 (DESIGN §30): connect any CalDAV provider (iCloud, Fastmail, Nextcloud, Google via CalDAV, …) like NotePlan. - **Truth stays at the provider.** calternal keeps a **derived cache in the index** (never files, rebuildable), so views are instant, offline reads work, ⌘K and agents can search events. - Refresh with CalDAV sync-collection (RFC 6578) every few minutes, when a view opens, and on demand; honour provider rate limits. - Writes (create/edit/delete/move, RSVP where supported) go straight to the provider with If-Match ETags; on 412 re-fetch and show the conflict; the cache updates after success. - Staleness is visible: UI "as of 2 min ago"; CLI `calternal events [--from --to] [--fresh]` where `--fresh` forces a live fetch. - Credentials: app-specific passwords / OAuth where the provider supports it, stored encrypted at rest in the index (security state), never in files or logs; per-account enable/disable, per-calendar colour and visibility. - Recurrence (RRULE/EXDATE/overrides) and VTIMEZONE handled via the `calcard` crate; test against real-world iCloud/Fastmail/Google samples. - Adversarial pass after merge (malformed ICS, huge calendars, provider timeouts). ## Context for the owning job - Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§9, §17, §24, §25, §29, §30) first. - Vocabulary: a **log entry** is a retrospective `- HH:MM[ - HH:MM] title #tags ^blockid` line under `## 📝 Log` in a daily note (`Notes/Journal/YYYYMMDD-dailynote.md`); an **event** is a scheduled calendar item from an external CalDAV provider. calternal is **not** a calendar store: log entries are authoritative for calternal's own time data; external events live at their provider. - calternal started as a lifelogging app and stays one; Calendar is the lifelog hub (everything by date). The mode tray defaults to Files, Calendar, Photos. - Owner rules: file over app; server is the single writer; data loss is unacceptable; performance first but never at the cost of finesse; UI copies Apple Calendar's structure (the owner shared macOS Calendar Week and Day screenshots: Day/Week/Month/Year segmented control top centre, large "September 2026" title with the month bold, ‹ Today › pager top right, all-day lane, hour grid with 09:00-style labels, red circle on today's date, event blocks with a coloured left rule and title/location/time lines, Day view with a mini month and an inspector on the right) plus Fantastical and BusyCal day/week ideas, rendered in the calternal.js design system (copy components verbatim, Claude reviews screenshots side by side); never ship sample/mock data; atomic commits; adversarial testing after API work; good enough, not perfect. - Comment on this issue when you start (branch, base SHA), on each finding, when blocked, and when finished (head SHA + gate output). Never close it.
Author
Owner

Starting caldav-client work on branch job/caldav-client at base 63f3bdd6cb.

Starting caldav-client work on branch job/caldav-client at base 63f3bdd6cb299d8619d8ed1b7ff83a40be5e4c3f.
Author
Owner

Finding: the existing code has no credential-at-rest encryption or server key-management helper. calternal-db uses ordinary SQLite, and the only matching encryption references are the design principles; C1 requires CalDAV credentials to be encrypted in the Index. The client needs a defined encryption-key source before it can persist app passwords or OAuth refresh tokens. I am evaluating a plugin-scoped environment key as the smallest fail-closed option.

Finding: the existing code has no credential-at-rest encryption or server key-management helper. `calternal-db` uses ordinary SQLite, and the only matching encryption references are the design principles; C1 requires CalDAV credentials to be encrypted in the Index. The client needs a defined encryption-key source before it can persist app passwords or OAuth refresh tokens. I am evaluating a plugin-scoped environment key as the smallest fail-closed option.
Author
Owner

Finding: Google CalDAV requires HTTPS with OAuth 2.0 according to Google's CalDAV guide (https://developers.google.com/workspace/calendar/caldav/v2/guide). The current PluginContext does not carry a public callback URL, and the server has no outbound OAuth client configuration. A full Google connect flow therefore needs server auth/config wiring beyond plugin registration. I recommend a provider-specific OAuth authorization-code flow using instance-configured client credentials; the smaller alternative is to accept user-supplied OAuth tokens and Basic app passwords without an interactive Google flow.

Finding: Google CalDAV requires HTTPS with OAuth 2.0 according to Google's CalDAV guide (https://developers.google.com/workspace/calendar/caldav/v2/guide). The current PluginContext does not carry a public callback URL, and the server has no outbound OAuth client configuration. A full Google connect flow therefore needs server auth/config wiring beyond plugin registration. I recommend a provider-specific OAuth authorization-code flow using instance-configured client credentials; the smaller alternative is to accept user-supplied OAuth tokens and Basic app passwords without an interactive Google flow.
Author
Owner

Blocked on the OAuth boundary: I asked whether #40 should include an interactive Google OAuth authorization-code flow (requiring server callback/config wiring outside the listed ownership scope) or accept user-supplied OAuth tokens with Basic app passwords. I will resume the dependent implementation after that decision. The separate credential-at-rest finding and proposed fail-closed instance key source are recorded above.

Blocked on the OAuth boundary: I asked whether #40 should include an interactive Google OAuth authorization-code flow (requiring server callback/config wiring outside the listed ownership scope) or accept user-supplied OAuth tokens with Basic app passwords. I will resume the dependent implementation after that decision. The separate credential-at-rest finding and proposed fail-closed instance key source are recorded above.
Author
Owner

Starting the resumed implementation on branch job/caldav-client at base 63f3bdd6cb.

Starting the resumed implementation on branch job/caldav-client at base 63f3bdd6cb299d8619d8ed1b7ff83a40be5e4c3f.
Author
Owner

Finding: §30 and the orchestration decision do not define the text encoding for CALTERNAL_SECRET_KEY or cross-origin redirect behavior. The implementation uses padded standard Base64 for exactly 32 key bytes; .system/secret.key stores 32 raw bytes. It accepts HTTPS endpoints (HTTP only for loopback tests), disables redirects, and rejects provider hrefs outside the endpoint origin so Basic credentials cannot cross origins. Sync responses are capped at 8 MiB and 5,000 resources per collection. Please confirm these implementation choices during review.

Finding: §30 and the orchestration decision do not define the text encoding for CALTERNAL_SECRET_KEY or cross-origin redirect behavior. The implementation uses padded standard Base64 for exactly 32 key bytes; `.system/secret.key` stores 32 raw bytes. It accepts HTTPS endpoints (HTTP only for loopback tests), disables redirects, and rejects provider hrefs outside the endpoint origin so Basic credentials cannot cross origins. Sync responses are capped at 8 MiB and 5,000 resources per collection. Please confirm these implementation choices during review.
Author
Owner

Finished implementation on branch job/caldav-client.

Base: 63f3bdd6cb299d8619d8ed1b7ff83a40be5e4c3f
Head: a240ad34c7390cdc6c73aa68d8e4f90f6451aea7

Commits:

  • 7140e2e Add atomic private system secret key storage
  • f309d48 Add secure CalDAV account client and cache
  • f110c46 Wire Calendar core plugin startup
  • 20b34f0 Generate Calendar API client contracts
  • a240ad3 Probe CalDAV routes adversarially

Implemented encrypted Basic app-password accounts with the instance secret key, CalDAV discovery and sync-collection cache, stale cache and retry-after handling, ETag-conditional Event writes and same-account calendar moves, normalized Event search, API routes/contracts, startup registration, and adversarial endpoint probes. Provider recurrence data remains in the raw iCalendar object; normalized metadata includes its timezone.

Verification:

  • cargo fmt --all --check: exit 0, no output.
  • cargo clippy --workspace --all-targets -- -D warnings: Finished dev profile [unoptimized + debuginfo] target(s) in 1m 07s.
  • cargo test --workspace: exit 0. Calendar output included:
    • test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s
    • test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
    • test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s
  • bash packages/api-client/check-generated.sh:
    • ✨ openapi-typescript 7.13.0
    • 🚀 ../../contracts/openapi.json → src/generated.ts [252.3ms]
  • bash tests/adversarial/run.sh: ==== FINDINGS 0; server alive at end: True.
  • Required cleanup: Removed 14383 files, 7.6GiB total (cargo clean).

Known gaps: OAuth/Google remains out of scope as decided; key rotation is deferred; cross-account MOVE is unsupported because the provider operation is only atomic within one account; recurring Events are preserved as iCalendar but are not expanded into individual occurrences. No web UI changes were made, per ownership scope.

Implementation choices beyond the owner decisions already recorded: CALTERNAL_SECRET_KEY uses padded standard Base64 encoding for exactly 32 bytes; provider URLs require HTTPS except loopback and redirects are disabled; DAV response links are constrained to the account origin. The endpoint limits and cache/search representation are documented in the earlier issue comment.

Finished implementation on branch `job/caldav-client`. Base: `63f3bdd6cb299d8619d8ed1b7ff83a40be5e4c3f` Head: `a240ad34c7390cdc6c73aa68d8e4f90f6451aea7` Commits: - `7140e2e` Add atomic private system secret key storage - `f309d48` Add secure CalDAV account client and cache - `f110c46` Wire Calendar core plugin startup - `20b34f0` Generate Calendar API client contracts - `a240ad3` Probe CalDAV routes adversarially Implemented encrypted Basic app-password accounts with the instance secret key, CalDAV discovery and sync-collection cache, stale cache and retry-after handling, ETag-conditional Event writes and same-account calendar moves, normalized Event search, API routes/contracts, startup registration, and adversarial endpoint probes. Provider recurrence data remains in the raw iCalendar object; normalized metadata includes its timezone. Verification: - `cargo fmt --all --check`: exit 0, no output. - `cargo clippy --workspace --all-targets -- -D warnings`: `Finished dev profile [unoptimized + debuginfo] target(s) in 1m 07s`. - `cargo test --workspace`: exit 0. Calendar output included: - `test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s` - `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s` - `test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s` - `bash packages/api-client/check-generated.sh`: - `✨ openapi-typescript 7.13.0` - `🚀 ../../contracts/openapi.json → src/generated.ts [252.3ms]` - `bash tests/adversarial/run.sh`: `==== FINDINGS 0`; `server alive at end: True`. - Required cleanup: `Removed 14383 files, 7.6GiB total` (`cargo clean`). Known gaps: OAuth/Google remains out of scope as decided; key rotation is deferred; cross-account MOVE is unsupported because the provider operation is only atomic within one account; recurring Events are preserved as iCalendar but are not expanded into individual occurrences. No web UI changes were made, per ownership scope. Implementation choices beyond the owner decisions already recorded: `CALTERNAL_SECRET_KEY` uses padded standard Base64 encoding for exactly 32 bytes; provider URLs require HTTPS except loopback and redirects are disabled; DAV response links are constrained to the account origin. The endpoint limits and cache/search representation are documented in the earlier issue comment.
Author
Owner

Completed on dev in 6075442ee8 (Merge calendar-r4: Calendar round 4: overlap layout, Settings → Calendars, app passwords, future events (#39, #40, #41)).

Completed on dev in 6075442ee8632305346cdf56ac46b835e7ef8ef8 (Merge calendar-r4: Calendar round 4: overlap layout, Settings → Calendars, app passwords, future events (#39, #40, #41)).
kayg closed this issue 2026-10-01 05:08:46 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#40
No description provided.