caldav/carddav: officer-caldav sidecar and the /dav door

first two steps of docs/nextcloud-replacement.md — the half that has to work on
a phone, because that is the half that cannot be faked.

radicale is supervised by the sidecar rather than reimplemented. nextcloud does
not implement caldav either; it vendors sabre/dav. icalendar and vcard are a
weekend, but sync-collection, rrule expansion, vtimezone and ctag/etag are not,
and when they are subtly wrong a phone does not error — it silently stops
syncing, or silently duplicates every event.

two doors, because a browser should not speak dav:

  /dav/*        top-level, http basic against a scoped app password, every
                verb and every dav header forwarded verbatim. this is what
                davx5 and ios talk to. same reasoning as /api/vault being
                mounted outside protectedRouter.
  /api/caldav/* the ordinary sidecar proxy, for officer's own ui. json.

the shared proxy factory could not carry the dav door: it forwards three
headers and dav dies without Depth, and it derives the user from a jwt a phone
cannot hold. so it is a separate file, per that factory's own instruction never
to grow per-app logic.

new `dav_app_passwords` — a phone cannot do jwt, and the alternative is the
account password living in a phone's account manager. argon2, shown once,
revocable per device, and accepted ONLY by /dav.

.well-known/caldav and carddav redirect to the dav root. they are most of what
makes adding an account feel transparent, and they need naming explicitly in
server.tsx or the SPA `/*` fallback answers the phone with html.

verified end to end against the running stack: 401 + WWW-Authenticate
unauthenticated; 207 with calendar-access and addressbook advertised; MKCALENDAR,
PUT and GET of a real VEVENT; calendar-query and sync-collection REPORTs; MKCOL,
PUT and GET of a real vCard. X-Script-Name is set because radicale otherwise
generates hrefs at / and the client follows them into the SPA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 03:16:17 +00:00
co-authored by Claude Opus 5
parent 86d979046b
commit ccd104a28b
14 changed files with 955 additions and 0 deletions
+10
View File
@@ -92,6 +92,16 @@ module.exports = {
args: 'run src/servers/sidecar/invoiceshelf/index.ts',
watch: false,
},
// Calendar and contacts. Supervises Radicale (CalDAV/CardDAV) on a loopback port and owns the
// collections under DATA_PATH/dav. Two doors: /dav for phones (DAVx5, iOS, Thunderbird — HTTP Basic
// against a scoped app password) and /api/caldav for Officer's own UI. The protocol is Radicale's;
// the platform authenticates and forwards. See docs/nextcloud-replacement.md.
{
name: 'officer-caldav',
script: 'bun',
args: 'run src/servers/sidecar/caldav/index.ts',
watch: false,
},
// The photo library. Wraps a self-hosted Immich. The instance and its key are set by the owner from
// /photos/settings and stored encrypted in `photos_config` — read here, never from the environment,
// because Bun auto-loads `.env` into every process in this directory and `officer` would hold it too.