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:
@@ -30,6 +30,9 @@ import { photosRouter } from './api/photos/router';
|
||||
import { walletRouter } from './api/wallet/router';
|
||||
import { vpnRouter } from './api/vpn/router';
|
||||
import { terminalRouter } from './api/terminal/sidecar-server';
|
||||
import { caldavRouter } from './api/dav/sidecar-server';
|
||||
import { davSyncRouter } from './api/dav/sync-router';
|
||||
import { davRouter } from './api/dav/router';
|
||||
import { notifyRouter } from './api/notify/router';
|
||||
import { systemMonitorRouter } from './api/system-monitor/system-monitor';
|
||||
import { activityRouter } from './api/activity/router';
|
||||
@@ -84,6 +87,20 @@ honoServer.route('/api/waitlist', waitlistRouter);
|
||||
honoServer.route('/api/vault', vaultRouter);
|
||||
honoServer.get('/api/integrations/google/callback', googleCallbackHandler);
|
||||
|
||||
// CalDAV/CardDAV for phones and desktop clients — mounted TOP-LEVEL for the same reason the vault is:
|
||||
// DAVx5, iOS and Thunderbird authenticate with HTTP Basic on every request and have nowhere to put a
|
||||
// platform JWT, so userMiddleware would 401 them. The credential is a scoped DAV app password; see
|
||||
// api/dav/sync-router.ts.
|
||||
honoServer.route('/dav', davSyncRouter);
|
||||
|
||||
// Autodiscovery. This is most of what makes adding an account on a phone feel transparent instead of
|
||||
// fiddly: the client is given a bare domain and probes these paths UNAUTHENTICATED before it has any
|
||||
// credential, so they must sit above every auth gate. Without them iOS in particular degrades to
|
||||
// demanding a full collection URL, which is exactly the sort of thing that makes self-hosting feel
|
||||
// worse than the commercial product it is replacing.
|
||||
honoServer.get('/.well-known/caldav', (ctx) => ctx.redirect('/dav/', 301));
|
||||
honoServer.get('/.well-known/carddav', (ctx) => ctx.redirect('/dav/', 301));
|
||||
|
||||
const protectedRouter = createRouter();
|
||||
protectedRouter.use(bodyParser());
|
||||
protectedRouter.use(userMiddleware);
|
||||
@@ -106,6 +123,8 @@ protectedRouter.route('/file-browser', fileBrowserRouter);
|
||||
protectedRouter.route('/music', musicRouter);
|
||||
protectedRouter.route('/slskd', slskdRouter);
|
||||
protectedRouter.route('/terminal', terminalRouter);
|
||||
protectedRouter.route('/caldav', caldavRouter); // the JSON door for Officer's own calendar/contacts UI
|
||||
protectedRouter.route('/dav', davRouter); // app-password management (the sync door is /dav, top-level)
|
||||
protectedRouter.route('/notify', notifyRouter);
|
||||
protectedRouter.route('/headscale', headscaleRouter);
|
||||
protectedRouter.route('/transmission', transmissionRouter);
|
||||
|
||||
Reference in New Issue
Block a user