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: '2026.08.27.03', platform: '>=1.0.0', /** * What it is called in the store, in words that assume nothing. * * Not "Calendar", which is one of its two screens and undersells it; not "CalDAV" or "caldav", which * are protocol names a person browsing has no reason to know. The convention across the store is that * a plugin is named for what you get, not for how it works. */ label: 'Contacts and Calendar', summary: 'Calendars and contacts on your own machine, synced to your phone over CalDAV and CardDAV.', color: '#ef4444', /** * Personal because that is what it holds; Degoogling because replacing Google Calendar and Google * Contacts is the reason most people would install it. */ tags: ['Personal', 'Degoogling'], /** * 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' }, }, ], };