@sockethub/platform-carddav 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
@@ -14,7 +14,7 @@ through the standard Sockethub credentials flow.
14
14
 
15
15
  ## Discovery cache
16
16
 
17
- Each `query`, `create`, `update`, and `delete` needs the account's address book list to
17
+ Each `query`, `sync`, `create`, `update`, and `delete` needs the account's address book list to
18
18
  check the target. The platform caches that discovery result in memory for
19
19
  `packageConfig.discoveryCacheTtlMs` (default `300000`, 5 minutes; `0`
20
20
  disables the cache). Entries are keyed by an HMAC of the protocol, service URL,
@@ -52,6 +52,8 @@ that endpoint.
52
52
 
53
53
  - `fetch`: discover address books.
54
54
  - `query`: list or search contacts.
55
+ - `sync`: report what changed in one address book since a sync token
56
+ (RFC 6578 WebDAV Sync).
55
57
  - `create`: create a vCard with collision protection.
56
58
  - `update`: update a vCard only when its ETag still matches.
57
59
  - `delete`: delete a vCard only when its ETag still matches.
@@ -96,6 +98,42 @@ Use `sc.contextFor("carddav")` as the request context.
96
98
  }
97
99
  ```
98
100
 
101
+ ### Sync changes
102
+
103
+ `sync` reports changes without listing the whole address book each time. Send
104
+ it without an `object` for the initial sync, which returns every contact and a
105
+ `syncToken`:
106
+
107
+ ```json
108
+ {
109
+ "type": "sync",
110
+ "actor": { "id": "carddav:alice", "type": "person" },
111
+ "target": {
112
+ "id": "https://contacts.example/addressbooks/alice/personal/",
113
+ "type": "addressBook"
114
+ }
115
+ }
116
+ ```
117
+
118
+ The response is a collection with `items` (contacts in the same shape as
119
+ `query` results), `removed` (ids of contacts deleted since the token),
120
+ `syncToken`, and `truncated`. Store the token as an opaque string per address
121
+ book and send it back as `"object": { "syncToken": "..." }` to receive only
122
+ what changed since then. When `truncated` is `true`, store the new token and
123
+ sync again until it is `false`.
124
+
125
+ Send `"object": { "tokenOnly": true }` to get the address book's current
126
+ `syncToken` with empty `items` and `removed`, without listing every contact.
127
+ Read it before loading the contacts you need, then sync with it later. A
128
+ server that exposes no token returns `carddav:sync-unsupported`.
129
+
130
+ On `carddav:sync-token-invalid`, drop the stored token and sync again without
131
+ one. On `carddav:sync-unsupported`, the server does not offer WebDAV Sync; fall
132
+ back to `query`. Other sync errors are `carddav:sync-failed`. A server that
133
+ rejects a token without a `DAV:` precondition (a 400, 409, or 412) is
134
+ reported as `carddav:sync-token-invalid`, so a retry without a token reports
135
+ the real cause.
136
+
99
137
  ### Create a contact
100
138
 
101
139
  ```json
@@ -138,4 +176,5 @@ array removes them.
138
176
  The initial implementation reads vCard 3.0 and 4.0 and writes vCard 4.0. Stable
139
177
  errors use the `carddav:` prefix, including `authentication-failed`,
140
178
  `unsupported-authentication`, `invalid-address-book`, `invalid-resource`,
141
- `conflict`, `not-found`, and `unsupported-update`.
179
+ `conflict`, `not-found`, `unsupported-update`, `sync-token-invalid`,
180
+ `sync-unsupported`, and `sync-failed`.
package/dist/dav.d.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  import { type DavAuthentication, DavClient, DavFailure, type DavNetworkOptions } from "@sockethub/dav";
2
- import type { AddressBookDescription, Contact, ContactInput, ContactQuery } from "./types.js";
2
+ import type { AddressBookDescription, Contact, ContactInput, ContactQuery, ContactSyncResult } from "./types.js";
3
3
  export { DavFailure as CardDavFailure };
4
4
  export declare class CardDavClient extends DavClient {
5
5
  constructor(url: string, authentication: DavAuthentication, timeoutMs?: number, networkOptions?: DavNetworkOptions);
6
6
  discoverAddressBooks(): Promise<AddressBookDescription[]>;
7
7
  private discoverFrom;
8
8
  query(book: AddressBookDescription, query?: ContactQuery): Promise<Contact[]>;
9
+ /**
10
+ * Incremental sync (RFC 6578). Without `syncToken`, lists every contact
11
+ * in the address book; with one, lists only what changed since then.
12
+ */
13
+ sync(book: AddressBookDescription, syncToken?: string): Promise<ContactSyncResult>;
9
14
  create(book: AddressBookDescription, input: ContactInput): Promise<{
10
15
  id: string;
11
16
  uid: string;
package/dist/index.d.ts CHANGED
@@ -12,6 +12,21 @@ export default class CardDav implements PlatformInterface {
12
12
  cleanup(done: PlatformCallback): void;
13
13
  fetch(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
14
14
  query(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
15
+ /**
16
+ * Incremental address book sync (RFC 6578 WebDAV Sync). Without
17
+ * `object.syncToken`, returns every contact plus a token; with one,
18
+ * returns only contacts changed and ids removed since that token. Store
19
+ * the returned `syncToken` opaquely and send it next time. When
20
+ * `truncated` is true, sync again with the new token before relying on
21
+ * the result being complete. `carddav:sync-token-invalid` means start
22
+ * over without a token; `carddav:sync-unsupported` means fall back to
23
+ * `query`.
24
+ *
25
+ * With `object.tokenOnly: true`, returns the address book's current
26
+ * token and no contacts, so a client can start tracking changes
27
+ * without downloading every contact first.
28
+ */
29
+ sync(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
15
30
  create(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
16
31
  update(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
17
32
  delete(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
package/dist/index.js CHANGED
@@ -42683,9 +42683,10 @@ function xmlText(value) {
42683
42683
  return predefined[entity.toLowerCase()] ?? reference;
42684
42684
  });
42685
42685
  }
42686
- function parseDavXml(xml) {
42686
+ function parseDavXml(xml, options = {}) {
42687
42687
  return new XMLParser({
42688
42688
  ignoreAttributes: false,
42689
+ parseTagValue: options.parseTagValue ?? true,
42689
42690
  removeNSPrefix: true,
42690
42691
  processEntities: false,
42691
42692
  tagValueProcessor: (_tagName, value) => xmlText(value),
@@ -42864,6 +42865,16 @@ function digestChallenge(header) {
42864
42865
  return;
42865
42866
  }
42866
42867
  var quote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
42868
+ var isStatus = (value, code) => new RegExp(`\\s${code}(?:\\s|$)`).test(String(value ?? ""));
42869
+ function davPreconditions(xml) {
42870
+ try {
42871
+ const error = parseDavXml(xml).error;
42872
+ return error && typeof error === "object" ? Object.keys(error) : [];
42873
+ } catch {
42874
+ return [];
42875
+ }
42876
+ }
42877
+ var xmlEscape = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
42867
42878
 
42868
42879
  class DavClient {
42869
42880
  authentication;
@@ -42908,9 +42919,10 @@ class DavClient {
42908
42919
  this.assertAllowed(url);
42909
42920
  let response;
42910
42921
  try {
42922
+ const { passForbidden: _, ...fetchInit } = init;
42911
42923
  const authorization = this.authorization(url, init);
42912
42924
  response = await this.fetchImpl(url, {
42913
- ...init,
42925
+ ...fetchInit,
42914
42926
  redirect: "manual",
42915
42927
  signal,
42916
42928
  headers: {
@@ -42954,7 +42966,7 @@ class DavClient {
42954
42966
  }
42955
42967
  throw new DavFailure(`${this.errorPrefix}:${authAttempts > 0 ? "authentication-failed" : "unsupported-authentication"}`);
42956
42968
  }
42957
- if (response.status === 401 || response.status === 403) {
42969
+ if (response.status === 401 || response.status === 403 && !init.passForbidden) {
42958
42970
  await response.body?.cancel().catch(() => {});
42959
42971
  throw new DavFailure(`${this.errorPrefix}:authentication-failed`);
42960
42972
  }
@@ -42975,6 +42987,175 @@ class DavClient {
42975
42987
  await response.body?.cancel().catch(() => {});
42976
42988
  throw new DavFailure(`${this.errorPrefix}:invalid-response`);
42977
42989
  }
42990
+ return this.readMultistatus(response);
42991
+ }
42992
+ async currentSyncToken(collection) {
42993
+ const response = await this.request(collection, {
42994
+ method: "PROPFIND",
42995
+ headers: {
42996
+ depth: "0",
42997
+ "content-type": "application/xml; charset=utf-8"
42998
+ },
42999
+ body: `<?xml version="1.0" encoding="utf-8"?>
43000
+ <d:propfind xmlns:d="DAV:"><d:prop><d:sync-token/></d:prop></d:propfind>`
43001
+ });
43002
+ if (response.status === 404)
43003
+ return rejectDavResponse(response, `${this.errorPrefix}:not-found`);
43004
+ if (response.status !== 207) {
43005
+ await response.body?.cancel().catch(() => {});
43006
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43007
+ }
43008
+ let responses;
43009
+ try {
43010
+ responses = asArray(parseDavXml(await response.text(), {
43011
+ parseTagValue: false
43012
+ }).multistatus?.response);
43013
+ } catch {
43014
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43015
+ }
43016
+ const token = successfulProps(responses[0] ?? {})["sync-token"];
43017
+ if (typeof token !== "string" || token.trim() === "")
43018
+ throw new DavFailure(`${this.errorPrefix}:sync-unsupported`);
43019
+ return token.trim();
43020
+ }
43021
+ async syncCollection(collection, prop, syncToken, namespaces = "") {
43022
+ const response = await this.request(collection, {
43023
+ method: "REPORT",
43024
+ passForbidden: true,
43025
+ headers: {
43026
+ depth: "0",
43027
+ "content-type": "application/xml; charset=utf-8"
43028
+ },
43029
+ body: `<?xml version="1.0" encoding="utf-8"?>
43030
+ <d:sync-collection xmlns:d="DAV:"${namespaces ? ` ${namespaces}` : ""}>
43031
+ ${syncToken ? `<d:sync-token>${xmlEscape(syncToken)}</d:sync-token>` : "<d:sync-token/>"}
43032
+ <d:sync-level>1</d:sync-level>
43033
+ <d:prop>${prop}</d:prop>
43034
+ </d:sync-collection>`
43035
+ });
43036
+ if (response.status !== 207) {
43037
+ let body = "";
43038
+ try {
43039
+ body = await response.text();
43040
+ } catch {}
43041
+ const preconditions = davPreconditions(body);
43042
+ const prefix = this.errorPrefix;
43043
+ if (preconditions.includes("valid-sync-token"))
43044
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
43045
+ if (preconditions.includes("supported-report"))
43046
+ throw new DavFailure(`${prefix}:sync-unsupported`);
43047
+ if (response.status === 404)
43048
+ throw new DavFailure(`${prefix}:not-found`);
43049
+ if ([400, 409, 412].includes(response.status) && syncToken)
43050
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
43051
+ if (response.status === 403)
43052
+ throw new DavFailure(`${prefix}:authentication-failed`);
43053
+ if ([400, 405, 415, 501].includes(response.status))
43054
+ throw new DavFailure(`${prefix}:sync-unsupported`);
43055
+ throw new DavFailure(`${prefix}:sync-failed`);
43056
+ }
43057
+ let multistatus;
43058
+ try {
43059
+ const parsed = parseDavXml(await response.text(), {
43060
+ parseTagValue: false
43061
+ }).multistatus;
43062
+ if (!parsed || typeof parsed !== "object")
43063
+ throw new Error;
43064
+ multistatus = parsed;
43065
+ } catch {
43066
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43067
+ }
43068
+ const token = multistatus["sync-token"];
43069
+ if (typeof token !== "string" || token.trim() === "")
43070
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43071
+ const result = {
43072
+ changed: [],
43073
+ removed: [],
43074
+ syncToken: token.trim(),
43075
+ truncated: false
43076
+ };
43077
+ for (const item of asArray(multistatus.response)) {
43078
+ const href = davHref(item.href);
43079
+ if (!href)
43080
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43081
+ const id = new URL(href, collection).href;
43082
+ const status = item.status;
43083
+ if (id === collection.href || `${id}/` === collection.href) {
43084
+ if (isStatus(status, 507))
43085
+ result.truncated = true;
43086
+ continue;
43087
+ }
43088
+ if (!isDavCollectionChild(collection.href, id))
43089
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43090
+ if (isStatus(status, 404))
43091
+ result.removed.push(id);
43092
+ else
43093
+ result.changed.push({ id, props: successfulProps(item) });
43094
+ }
43095
+ return result;
43096
+ }
43097
+ async syncResources(collection, options, syncToken) {
43098
+ const property = options.data.slice(options.data.indexOf(":") + 1);
43099
+ const sync = await this.syncCollection(collection, `<d:getetag/><${options.data}/>`, syncToken, options.namespaces);
43100
+ const resources = [];
43101
+ const missing = new Map;
43102
+ const add = (id, value, etag) => {
43103
+ const data = value && typeof value === "object" ? value["#text"] : value;
43104
+ if (typeof data !== "string")
43105
+ return false;
43106
+ resources.push({
43107
+ id,
43108
+ data,
43109
+ ...typeof etag === "string" ? { etag } : {}
43110
+ });
43111
+ return true;
43112
+ };
43113
+ for (const member of sync.changed)
43114
+ if (!add(member.id, member.props[property], member.props.getetag))
43115
+ missing.set(member.id, member.props.getetag);
43116
+ if (missing.size) {
43117
+ const hrefs = [...missing.keys()].map((id) => `<d:href>${xmlEscape(new URL(id).pathname)}</d:href>`).join("");
43118
+ const response = await this.request(collection, {
43119
+ method: "REPORT",
43120
+ headers: {
43121
+ depth: "1",
43122
+ "content-type": "application/xml; charset=utf-8"
43123
+ },
43124
+ body: `<?xml version="1.0" encoding="utf-8"?>
43125
+ <${options.multiget} xmlns:d="DAV:" ${options.namespaces}>
43126
+ <d:prop><d:getetag/><${options.data}/></d:prop>
43127
+ ${hrefs}
43128
+ </${options.multiget}>`
43129
+ });
43130
+ if (response.status !== 207)
43131
+ return rejectDavResponse(response, `${this.errorPrefix}:sync-failed`);
43132
+ for (const item of await this.readMultistatus(response)) {
43133
+ const href = davHref(item.href);
43134
+ if (!href)
43135
+ continue;
43136
+ const id = new URL(href, collection).href;
43137
+ if (!missing.has(id))
43138
+ continue;
43139
+ if (isStatus(item.status, 404)) {
43140
+ sync.removed.push(id);
43141
+ missing.delete(id);
43142
+ continue;
43143
+ }
43144
+ const props = successfulProps(item);
43145
+ if (add(id, props[property], props.getetag ?? missing.get(id)))
43146
+ missing.delete(id);
43147
+ }
43148
+ if (missing.size)
43149
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43150
+ }
43151
+ return {
43152
+ resources,
43153
+ removed: sync.removed,
43154
+ syncToken: sync.syncToken,
43155
+ truncated: sync.truncated
43156
+ };
43157
+ }
43158
+ async readMultistatus(response) {
42978
43159
  try {
42979
43160
  const parsed = parseDavXml(await response.text());
42980
43161
  return asArray(parsed.multistatus?.response);
@@ -43275,7 +43456,7 @@ var ActivityStreamSchema = {
43275
43456
  var package_default = {
43276
43457
  name: "@sockethub/schemas",
43277
43458
  description: "JSON schema files for validating Sockethub Activity Streams",
43278
- version: "3.0.0-beta.4",
43459
+ version: "3.0.0-beta.6",
43279
43460
  type: "module",
43280
43461
  private: false,
43281
43462
  author: "Nick Jennings <nick@silverbucket.net>",
@@ -43345,7 +43526,7 @@ var package_default = {
43345
43526
  "ajv-formats": "^2.1.1",
43346
43527
  "ajv-formats-draft2019": "^1.6.1"
43347
43528
  },
43348
- gitHead: "639c296a7332f192a1b3f7e86ef6603b5b67bbaa",
43529
+ gitHead: "604f3da8850a19a4fd5f5e5dcec10bf283e9085e",
43349
43530
  types: "./dist/index.d.ts"
43350
43531
  };
43351
43532
 
@@ -44306,6 +44487,14 @@ function queryBody(query) {
44306
44487
  ${filter}
44307
44488
  </c:addressbook-query>`;
44308
44489
  }
44490
+ function contact(data, id, etag) {
44491
+ try {
44492
+ return parseVCard(data, id, typeof etag === "string" ? etag : undefined);
44493
+ } catch {
44494
+ throw new DavFailure("carddav:invalid-response");
44495
+ }
44496
+ }
44497
+
44309
44498
  class CardDavClient extends DavClient {
44310
44499
  constructor(url, authentication, timeoutMs = 15000, networkOptions = {}) {
44311
44500
  super(url, authentication, "carddav", timeoutMs, networkOptions);
@@ -44371,29 +44560,30 @@ class CardDavClient extends DavClient {
44371
44560
  });
44372
44561
  if (response.status !== 207)
44373
44562
  return rejectDavResponse(response, "carddav:query-failed");
44374
- let parsed;
44375
- try {
44376
- parsed = parseDavXml(await response.text());
44377
- } catch {
44378
- throw new DavFailure("carddav:invalid-response");
44379
- }
44380
44563
  const contacts = [];
44381
- for (const item of asArray(parsed.multistatus?.response)) {
44564
+ for (const item of await this.readMultistatus(response)) {
44382
44565
  const props = successfulProps(item);
44383
44566
  const itemHref = davHref(item.href);
44384
44567
  const data = props["address-data"];
44385
44568
  if (!itemHref || typeof data !== "string")
44386
44569
  continue;
44387
- try {
44388
- contacts.push(parseVCard(data, new URL(itemHref, book.id).href, typeof props.getetag === "string" ? props.getetag : undefined));
44389
- } catch {
44390
- throw new DavFailure("carddav:invalid-response");
44391
- }
44570
+ contacts.push(contact(data, new URL(itemHref, book.id).href, props.getetag));
44392
44571
  if (query.limit && contacts.length >= query.limit)
44393
44572
  break;
44394
44573
  }
44395
44574
  return contacts;
44396
44575
  }
44576
+ async sync(book, syncToken) {
44577
+ const { resources, ...result } = await this.syncResources(new URL(book.id), {
44578
+ namespaces: 'xmlns:c="urn:ietf:params:xml:ns:carddav"',
44579
+ data: "c:address-data",
44580
+ multiget: "c:addressbook-multiget"
44581
+ }, syncToken);
44582
+ return {
44583
+ ...result,
44584
+ items: resources.map((resource) => contact(resource.data, resource.id, resource.etag))
44585
+ };
44586
+ }
44397
44587
  async create(book, input) {
44398
44588
  const card = buildVCard(input, []);
44399
44589
  const resource = new URL(`${encodeURIComponent(card.uid)}.vcf`, book.id.endsWith("/") ? book.id : `${book.id}/`);
@@ -44486,7 +44676,7 @@ class CardDavClient extends DavClient {
44486
44676
  var package_default2 = {
44487
44677
  name: "@sockethub/platform-carddav",
44488
44678
  description: "A Sockethub platform for CardDAV address books and contacts",
44489
- version: "1.0.0-beta.4",
44679
+ version: "1.0.0-beta.6",
44490
44680
  type: "module",
44491
44681
  private: false,
44492
44682
  license: "LGPL-3.0+",
@@ -44515,21 +44705,21 @@ var package_default2 = {
44515
44705
  test: "bun test src/"
44516
44706
  },
44517
44707
  dependencies: {
44518
- "@sockethub/dav": "^1.0.0-beta.4",
44519
- "@sockethub/schemas": "^3.0.0-beta.4"
44708
+ "@sockethub/dav": "^1.0.0-beta.6",
44709
+ "@sockethub/schemas": "^3.0.0-beta.6"
44520
44710
  },
44521
44711
  devDependencies: {
44522
44712
  "@types/bun": "latest"
44523
44713
  },
44524
44714
  peerDependencies: {
44525
- "@sockethub/server": "^5.0.0-beta.4"
44715
+ "@sockethub/server": "^5.0.0-beta.6"
44526
44716
  },
44527
44717
  peerDependenciesMeta: {
44528
44718
  "@sockethub/server": {
44529
44719
  optional: true
44530
44720
  }
44531
44721
  },
44532
- gitHead: "639c296a7332f192a1b3f7e86ef6603b5b67bbaa"
44722
+ gitHead: "604f3da8850a19a4fd5f5e5dcec10bf283e9085e"
44533
44723
  };
44534
44724
 
44535
44725
  // src/schema.ts
@@ -44637,7 +44827,7 @@ var shared = {
44637
44827
  vcardVersion: { enum: ["3.0", "4.0"] },
44638
44828
  updateSupported: { type: "boolean" }
44639
44829
  };
44640
- var contact = {
44830
+ var contact2 = {
44641
44831
  type: "object",
44642
44832
  required: ["type", "name"],
44643
44833
  additionalProperties: false,
@@ -44658,6 +44848,13 @@ var query = {
44658
44848
  limit: { type: "integer", minimum: 1, maximum: 1000 }
44659
44849
  }
44660
44850
  };
44851
+ var syncToken = { type: "string", minLength: 1, maxLength: 4096 };
44852
+ var sync = {
44853
+ type: "object",
44854
+ additionalProperties: false,
44855
+ properties: { syncToken, tokenOnly: { const: true } },
44856
+ not: { required: ["syncToken", "tokenOnly"] }
44857
+ };
44661
44858
  var deletion = {
44662
44859
  type: "object",
44663
44860
  required: ["id", "type", "etag"],
@@ -44665,11 +44862,11 @@ var deletion = {
44665
44862
  properties: { id: shared.id, type: shared.type, etag: shared.etag }
44666
44863
  };
44667
44864
  var updateContact = {
44668
- ...contact,
44865
+ ...contact2,
44669
44866
  required: ["type", "name", "id", "uid", "etag"]
44670
44867
  };
44671
44868
  var responseContact = {
44672
- ...contact,
44869
+ ...contact2,
44673
44870
  required: ["id", "type", "uid", "name", "vcardVersion", "updateSupported"]
44674
44871
  };
44675
44872
  var mutation = {
@@ -44727,10 +44924,20 @@ var PlatformCardDavSchema = {
44727
44924
  messages: {
44728
44925
  required: ["type", "actor"],
44729
44926
  properties: {
44730
- type: { enum: ["fetch", "query", "create", "update", "delete"] },
44927
+ type: {
44928
+ enum: ["fetch", "query", "sync", "create", "update", "delete"]
44929
+ },
44731
44930
  actor,
44732
44931
  target,
44733
- object: { oneOf: [contact, query, deletion] }
44932
+ object: {
44933
+ oneOf: [
44934
+ contact2,
44935
+ query,
44936
+ deletion,
44937
+ { ...sync, required: ["syncToken"] },
44938
+ { ...sync, required: ["tokenOnly"] }
44939
+ ]
44940
+ }
44734
44941
  }
44735
44942
  },
44736
44943
  messageConstraints: {
@@ -44747,7 +44954,11 @@ var PlatformCardDavSchema = {
44747
44954
  required: ["type", "actor", "target"]
44748
44955
  },
44749
44956
  {
44750
- properties: { type: { const: "create" }, object: contact },
44957
+ properties: { type: { const: "sync" }, object: sync },
44958
+ required: ["type", "actor", "target"]
44959
+ },
44960
+ {
44961
+ properties: { type: { const: "create" }, object: contact2 },
44751
44962
  required: ["type", "actor", "target", "object"]
44752
44963
  },
44753
44964
  {
@@ -44800,6 +45011,30 @@ var PlatformCardDavSchema = {
44800
45011
  }
44801
45012
  }
44802
45013
  },
45014
+ {
45015
+ type: "object",
45016
+ required: [
45017
+ "@context",
45018
+ "type",
45019
+ "totalItems",
45020
+ "items",
45021
+ "removed",
45022
+ "syncToken",
45023
+ "truncated"
45024
+ ],
45025
+ additionalProperties: false,
45026
+ properties: {
45027
+ "@context": { type: "array", items: { type: "string" } },
45028
+ id: { type: ["string", "null"] },
45029
+ type: { const: "collection" },
45030
+ summary: { type: "string" },
45031
+ totalItems: { type: "integer", minimum: 0 },
45032
+ items: { type: "array", items: responseContact },
45033
+ removed: { type: "array", items: shared.id },
45034
+ syncToken,
45035
+ truncated: { type: "boolean" }
45036
+ }
45037
+ },
44803
45038
  {
44804
45039
  type: "object",
44805
45040
  required: ["@context", "type", "actor", "target", "object"],
@@ -44825,7 +45060,14 @@ class CardDav {
44825
45060
  discovery = new DiscoveryCache;
44826
45061
  config = {
44827
45062
  persist: false,
44828
- requireCredentials: ["fetch", "query", "create", "update", "delete"],
45063
+ requireCredentials: [
45064
+ "fetch",
45065
+ "query",
45066
+ "sync",
45067
+ "create",
45068
+ "update",
45069
+ "delete"
45070
+ ],
44829
45071
  connectTimeoutMs: 15000,
44830
45072
  allowPrivateAddresses: false,
44831
45073
  allowInsecureHttp: false,
@@ -44866,6 +45108,23 @@ class CardDav {
44866
45108
  items
44867
45109
  })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44868
45110
  }
45111
+ sync(job, credentials, done) {
45112
+ const client = this.client(credentials);
45113
+ const input = job.object ?? {};
45114
+ this.book(client, credentials, job.target?.id ?? "").then((book) => input.tokenOnly ? client.currentSyncToken(new URL(book.id)).then((syncToken) => ({
45115
+ items: [],
45116
+ removed: [],
45117
+ syncToken,
45118
+ truncated: false
45119
+ })) : client.sync(book, input.syncToken)).then((result) => done(null, {
45120
+ "@context": CONTEXT,
45121
+ ...job.id ? { id: job.id } : {},
45122
+ type: "collection",
45123
+ summary: "CardDAV changes",
45124
+ totalItems: result.items.length,
45125
+ ...result
45126
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
45127
+ }
44869
45128
  create(job, credentials, done) {
44870
45129
  const client = this.client(credentials);
44871
45130
  this.book(client, credentials, job.target?.id ?? "").then((book) => client.create(book, job.object)).then((result) => done(null, this.mutation(job, "create", result))).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
@@ -44946,4 +45205,4 @@ export {
44946
45205
  parseVCard
44947
45206
  };
44948
45207
 
44949
- //# debugId=4AD528BB46F9866F64756E2164756E21
45208
+ //# debugId=9916E0836EEA9D2664756E2164756E21