@sockethub/platform-caldav 1.0.0-beta.4 → 1.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ it is used.
18
18
 
19
19
  ## Discovery cache
20
20
 
21
- Each `query`, `create`, `update`, and `delete` needs the account's calendar list to
21
+ Each `query`, `sync`, `create`, `update`, and `delete` needs the account's calendar list to
22
22
  check the target. The platform caches that discovery result in memory for
23
23
  `packageConfig.discoveryCacheTtlMs` (default `300000`, 5 minutes; `0`
24
24
  disables the cache). Entries are keyed by an HMAC of the protocol, service URL,
@@ -82,6 +82,8 @@ never forwarded to another origin.
82
82
  - `fetch`: discover calendars and their event/task support.
83
83
  - `query`: read items from one discovered calendar, optionally filtering by
84
84
  component type or UTC date range.
85
+ - `sync`: report what changed in one discovered calendar since a sync token
86
+ (RFC 6578 WebDAV Sync).
85
87
  - `read`: list the items in a published `.ics` feed without credentials,
86
88
  with the same optional filters as `query`.
87
89
  - `create`: create an event or task with collision protection.
@@ -131,9 +133,86 @@ The collection contains calendar objects such as:
131
133
  ```
132
134
 
133
135
  Each returned item contains its resource `id`, `uid`, `etag`, and
134
- `updateSupported`. Keep the ETag for later update or delete requests. Use date
136
+ `updateSupported`, plus `created` and `lastModified` (UTC) when the resource
137
+ records them; `lastModified` falls back to `DTSTAMP`. These two are output
138
+ only and ignored on create and update. Keep the ETag for later update or
139
+ delete requests. Use date
135
140
  ranges for large calendars; responses larger than 10 MiB are rejected.
136
141
 
142
+ ### Sync changes
143
+
144
+ `sync` reports changes without downloading the whole calendar each time. Send
145
+ it without an `object` (or without `object.syncToken`) for the initial sync:
146
+
147
+ ```json
148
+ {
149
+ "type": "sync",
150
+ "actor": { "id": "caldav:alice", "type": "person" },
151
+ "target": {
152
+ "id": "https://calendar.example/calendars/alice/personal/",
153
+ "type": "calendar"
154
+ }
155
+ }
156
+ ```
157
+
158
+ The response is a collection with every event and task, plus a token:
159
+
160
+ ```json
161
+ {
162
+ "type": "collection",
163
+ "summary": "CalDAV changes",
164
+ "totalItems": 1,
165
+ "items": [
166
+ {
167
+ "id": "https://calendar.example/calendars/alice/personal/a.ics",
168
+ "type": "task",
169
+ "uid": "a",
170
+ "name": "Pay rent",
171
+ "etag": "\"2\"",
172
+ "updateSupported": true
173
+ }
174
+ ],
175
+ "removed": [],
176
+ "syncToken": "http://sabre.io/ns/sync/42",
177
+ "truncated": false
178
+ }
179
+ ```
180
+
181
+ Store `syncToken` as an opaque string per calendar and send it back next time
182
+ as `"object": { "syncToken": "..." }`. The response then holds only the items
183
+ added or changed since that token, with their current ETag and content, and
184
+ `removed` lists the ids of items deleted since then. Items in the same shape
185
+ as `query` results replace the stored copy with the same `id`.
186
+
187
+ When `truncated` is `true`, the server limited the result. The returned token
188
+ covers exactly the changes delivered, so store it and sync again until
189
+ `truncated` is `false`.
190
+
191
+ To track changes without downloading a calendar's history, start from the
192
+ current token instead of an initial sync. Send `"object": { "tokenOnly": true }`
193
+ to get the calendar's current `syncToken` with empty `items` and `removed`,
194
+ then `query` the date range you need, then `sync` with the token on later
195
+ passes. Read the token before the `query`, so nothing changed in between is
196
+ missed. Later syncs still report changes to any item in the calendar,
197
+ including old ones. A server that exposes no token returns
198
+ `caldav:sync-unsupported`.
199
+
200
+ Calendar resources that hold neither an event nor a task, such as journals, are
201
+ left out of `items`.
202
+
203
+ Handle these errors:
204
+
205
+ - `caldav:sync-token-invalid`: the server no longer accepts the token (it
206
+ expired, or the calendar was reset). Drop the stored token and sync again
207
+ without one.
208
+ - `caldav:sync-unsupported`: the server does not offer WebDAV Sync. Fall back
209
+ to `query`, which returns a full snapshot.
210
+ - `caldav:sync-failed`: any other sync error.
211
+
212
+ When a server rejects a token without saying why (a 400, 409, or 412 with
213
+ no `DAV:` precondition), the platform reports `caldav:sync-token-invalid`. A
214
+ retry without a token then reports the real cause.
215
+
137
216
  ### Read a published iCalendar feed
138
217
 
139
218
  Many calendars are shared as a read-only `.ics` link (often shown as a
@@ -232,8 +311,12 @@ Update sends the complete event or task returned by `query`, including `id`,
232
311
  `uid`, and `etag`. Increment `sequence` when changing scheduling information.
233
312
  An update is rejected when `updateSupported` is false, which protects recurrence
234
313
  exceptions, exclusion dates, and multi-component resources from being lost.
235
- Properties outside Sockethub's item model, such as `CATEGORIES` and `X-`
236
- properties, are not preserved when an otherwise supported item is updated.
314
+ Update changes only the stored properties whose fields differ from the stored
315
+ item, plus `DTSTAMP` and `LAST-MODIFIED`. Everything else stays as it was,
316
+ including `CATEGORIES`, `X-` properties, `CREATED`, `VTIMEZONE` blocks, and
317
+ parameters on unchanged attendees and alarms. A field left out of the update
318
+ removes its property. A changed field is written from the item model, so
319
+ parameters the model does not carry are dropped from that property.
237
320
 
238
321
  ```json
239
322
  {
@@ -257,7 +340,8 @@ Query again, reconcile the change, and retry with the new ETag.
257
340
  ## Interoperability and security
258
341
 
259
342
  The implementation follows WebDAV/CalDAV discovery, calendar-query REPORT,
260
- iCalendar content-line escaping/folding, and conditional PUT/DELETE semantics.
343
+ iCalendar content-line escaping/folding, conditional PUT/DELETE semantics, and
344
+ WebDAV Sync (RFC 6578).
261
345
 
262
346
  ### Server and provider compatibility
263
347
 
@@ -269,8 +353,8 @@ until they are included in automated or repeatable manual testing.
269
353
 
270
354
  | Service | Deployment | Verification | Authentication exercised | Notes |
271
355
  | --- | --- | --- | --- | --- |
272
- | Radicale 3.7.7 | Self-hosted | Automated integration | Basic | Complete discovery and event/task create, query, update, and delete lifecycle. |
273
- | Baïkal 0.10.1 | Self-hosted | Automated integration | Digest MD5 with `qop=auth` | Complete lifecycle through Baïkal's `dav.php/` endpoint, including its encoded ETags. |
356
+ | Radicale 3.7.7 | Self-hosted | Automated integration | Basic | Complete discovery and event/task create, query, update, and delete lifecycle, and incremental sync. |
357
+ | Baïkal 0.10.1 | Self-hosted | Automated integration | Digest MD5 with `qop=auth` | Complete lifecycle and incremental sync through Baïkal's `dav.php/` endpoint, including its encoded ETags. |
274
358
  | Nextcloud | Self-hosted or hosted | Manually verified during initial development | Basic with username and password or app password | Use the final `/remote.php/dav/` endpoint when cross-origin discovery redirects are involved. OAuth is not covered by this verification. |
275
359
  | ownCloud | Self-hosted or hosted | Not yet verified | Not yet verified | Expected to work when standard discovery and a supported authentication method are enabled. |
276
360
  | DAViCal | Self-hosted | Not yet verified | Not yet verified | Expected to work when standard discovery and a supported authentication method are enabled. |
@@ -300,17 +384,15 @@ administrator must explicitly enable `allowPrivateAddresses` or
300
384
  | Shared and delegated calendars | Read-only discovery only | Calendars returned from the user's calendar home can be queried, but creating shares, accepting invitations, proxy delegation, and ACL management are not implemented. |
301
385
  | Calendar collection management | Not supported | The platform does not create, rename, or delete calendar collections. |
302
386
  | Free/busy queries | Not supported | CalDAV free-busy REPORT and scheduling availability are not implemented. |
303
- | Incremental sync | Not supported | WebDAV sync tokens (RFC 6578) are not used yet; queries return snapshots. |
387
+ | Incremental sync | Supported | `sync` uses RFC 6578 WebDAV Sync tokens, including truncated results. Members returned without calendar data are fetched with one `calendar-multiget`. |
304
388
  | Managed attachments | Not supported | iCalendar URL and inline attachment properties are supported, but CalDAV managed attachments are not. |
305
389
 
306
390
  <!-- markdownlint-enable MD013 -->
307
391
 
308
- Queries currently return calendar snapshots. CalDAV sync tokens (RFC 6578) are
309
- the intended follow-up for efficient incremental synchronization.
310
-
311
392
  Stable errors include `caldav:authentication-failed`,
312
393
  `caldav:unsupported-authentication`,
313
394
  `caldav:connection-failed`, `caldav:invalid-calendar`,
314
395
  `caldav:invalid-resource`, `caldav:unsupported-component`,
315
- `caldav:conflict`, `caldav:not-found`, `caldav:invalid-feed`, and
316
- `caldav:feed-failed`.
396
+ `caldav:conflict`, `caldav:not-found`, `caldav:invalid-feed`,
397
+ `caldav:feed-failed`, `caldav:sync-token-invalid`,
398
+ `caldav:sync-unsupported`, and `caldav:sync-failed`.
package/dist/dav.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type DavAuthentication, DavClient, DavFailure, type DavNetworkOptions } from "@sockethub/dav";
2
- import type { CalendarDescription, CalendarItem, QueryInput } from "./types.js";
2
+ import type { CalendarDescription, CalendarItem, CalendarSyncResult, QueryInput } from "./types.js";
3
3
  export declare const CalDavFailure: typeof DavFailure;
4
4
  export declare class CalDavClient extends DavClient {
5
5
  constructor(url: string, authentication: DavAuthentication, timeoutMs?: number, networkOptions?: DavNetworkOptions);
@@ -10,7 +10,17 @@ export declare class CalDavClient extends DavClient {
10
10
  etag?: string;
11
11
  }>;
12
12
  query(calendar: CalendarDescription, query?: QueryInput): Promise<CalendarItem[]>;
13
- update(id: string, etag: string, body: string): Promise<{
13
+ /**
14
+ * Incremental sync (RFC 6578). Without `syncToken`, lists every event and
15
+ * task in the calendar; with one, lists only what changed since then.
16
+ */
17
+ sync(calendar: CalendarDescription, syncToken?: string): Promise<CalendarSyncResult>;
18
+ /**
19
+ * Replace a resource if its ETag still matches. `body` is either the new
20
+ * resource or a function that builds it from the stored one, so changes
21
+ * can be applied without losing what the item model does not carry.
22
+ */
23
+ update(id: string, etag: string, body: string | ((stored: string) => string)): Promise<{
14
24
  id: string;
15
25
  etag?: string;
16
26
  }>;
package/dist/ical.d.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type { CalendarItem, CalendarObjectInput } from "./types.js";
2
2
  /** Fold one iCalendar content line at 75 UTF-8 octets (74 after continuation). */
3
3
  export declare function foldLine(line: string): string;
4
- export declare function buildICalendar(input: CalendarObjectInput, now?: Date): {
4
+ /**
5
+ * Serialize an item as a single-component iCalendar resource. `created`
6
+ * stamps `CREATED` with `now`; pass it only for a new resource, so an update
7
+ * does not move the creation time.
8
+ */
9
+ export declare function buildICalendar(input: CalendarObjectInput, now?: Date, { created }?: {
10
+ created?: boolean;
11
+ }): {
5
12
  uid: string;
6
13
  body: string;
7
14
  };
package/dist/index.d.ts CHANGED
@@ -30,6 +30,22 @@ export default class CalDav implements PlatformInterface {
30
30
  read(job: ActivityStream, done: PlatformCallback): void;
31
31
  create(job: ActivityStream, credentials: CalDavCredentials, done: PlatformCallback): void;
32
32
  query(job: ActivityStream, credentials: CalDavCredentials, done: PlatformCallback): void;
33
+ /**
34
+ * Incremental calendar sync (RFC 6578 WebDAV Sync). Without
35
+ * `object.syncToken`, returns every event and task plus a token; with
36
+ * one, returns only items changed and ids removed since that token.
37
+ * Store the returned `syncToken` opaquely and send it next time. When
38
+ * `truncated` is true, sync again with the new token before relying on
39
+ * the result being complete. `caldav:sync-token-invalid` means start
40
+ * over without a token; `caldav:sync-unsupported` means fall back to
41
+ * `query`.
42
+ *
43
+ * With `object.tokenOnly: true`, returns the calendar's current token
44
+ * and no items, so a client can start tracking changes without
45
+ * downloading history: get the token first, then `query` the range it
46
+ * needs, then `sync` with the token on later passes.
47
+ */
48
+ sync(job: ActivityStream, credentials: CalDavCredentials, done: PlatformCallback): void;
33
49
  update(job: ActivityStream, credentials: CalDavCredentials, done: PlatformCallback): void;
34
50
  delete(job: ActivityStream, credentials: CalDavCredentials, done: PlatformCallback): void;
35
51
  private client;