import type { PluginManifest } from '@@/plugins/manifest'; /** * Calendar and contacts, over CalDAV and CardDAV. * * One sidecar (Radicale on loopback), one permission, one install — and TWO screens, because Calendar and * Contacts are two places you go rather than two sections of one page. That is what `screens` below is * for, and caldav is the first plugin to use it. */ export const manifest: PluginManifest = { publisher: 'officerdev', version: '1.0.0', platform: '>=1.0.0', label: 'Calendar', summary: 'Calendars and contacts on your own machine, synced to your phone over CalDAV and CardDAV.', color: '#ef4444', /** * Two screens at URLs this plugin chose — `/calendar` and `/contacts`, not `/caldav/calendar`. * * They were core routes until 2026-08-27 and keeping them is not cosmetic: a bookmark, a dock tile a * user has arranged, and the layout persisted under `screens/plugin/caldav/...` all address them. * * Only a first-party plugin may choose its own segment — a third-party one is confined to * `/p//` so it cannot shadow a core route. Each ships its own artwork as * `assets/.png`, because two identical tiles side by side say nothing about which is which. */ screens: [ { segment: 'calendar', label: 'Calendar', icon: 'CalendarDays' }, { segment: 'contacts', label: 'Contacts', icon: 'Contact' }, ], permissions: [ { key: 'calendar', label: 'Calendar and contacts', description: 'Calendars, contacts, and the app passwords that sync them to a phone', // App passwords are minted for and revoked by their own owner, and are how a member's own phone // reaches their own calendar. Nothing under here is shared, so a read grant may still write them. personal: ['/passwords', '/provision'], }, ], /** * The top-level doors, above the account gate — and the reason is that phones are not browsers. * * DAVx5, iOS and Thunderbird send HTTP Basic on every request and have nowhere to put a platform JWT, * so the account gate would 401 every real client. `sync-router` demands its own credential instead: a * scoped DAV app password, resolved to one user, whose id is then what Radicale is told the principal * is. "Unauthenticated" here means "not authenticated by the PLATFORM's session", not "open". * * The two `.well-known` names are RFC 6764 autodiscovery: the client is given a bare domain and probes * them before it has any credential at all. Without them iOS degrades to demanding a full collection * URL, which is exactly the sort of thing that makes self-hosting feel worse than the product it * replaces. * * Every one of these was the platform's until this plugin existed, and each is in `RESERVED_ROOT` for * that reason — claimable by a first-party plugin, refused to everyone else. */ unauthenticatedRootPaths: ['/dav', '/.well-known/caldav', '/.well-known/carddav'], /** * Radicale — a ~2MB Python package, spawned on loopback by this plugin's sidecar. * * Not a container: it was one only because the old app store made everything one. No Homebrew formula * exists (confirmed live: "No available formula with the name radicale"), so a macOS dev install has * nowhere for the sidecar to find it — which is why `resolveRadicale()` failing is handled rather than * fatal. */ osDependencies: [ { binary: 'radicale', reason: 'Serves the CalDAV and CardDAV collections. Without it there are no calendars and no contacts.', packages: { apt: 'radicale', pacman: 'radicale', dnf: 'radicale' }, }, ], };