@sockethub/platform-carddav 1.0.0-beta.3 → 1.0.0-beta.5

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,37 @@ 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
+ On `carddav:sync-token-invalid`, drop the stored token and sync again without
126
+ one. On `carddav:sync-unsupported`, the server does not offer WebDAV Sync; fall
127
+ back to `query`. Other sync errors are `carddav:sync-failed`. A server that
128
+ rejects a token without a `DAV:` precondition (a 400, 409, or 412) is
129
+ reported as `carddav:sync-token-invalid`, so a retry without a token reports
130
+ the real cause.
131
+
99
132
  ### Create a contact
100
133
 
101
134
  ```json
@@ -138,4 +171,5 @@ array removes them.
138
171
  The initial implementation reads vCard 3.0 and 4.0 and writes vCard 4.0. Stable
139
172
  errors use the `carddav:` prefix, including `authentication-failed`,
140
173
  `unsupported-authentication`, `invalid-address-book`, `invalid-resource`,
141
- `conflict`, `not-found`, and `unsupported-update`.
174
+ `conflict`, `not-found`, `unsupported-update`, `sync-token-invalid`,
175
+ `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,17 @@ 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
+ sync(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
15
26
  create(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
16
27
  update(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
17
28
  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,146 @@ 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 syncCollection(collection, prop, syncToken, namespaces = "") {
42993
+ const response = await this.request(collection, {
42994
+ method: "REPORT",
42995
+ passForbidden: true,
42996
+ headers: {
42997
+ depth: "0",
42998
+ "content-type": "application/xml; charset=utf-8"
42999
+ },
43000
+ body: `<?xml version="1.0" encoding="utf-8"?>
43001
+ <d:sync-collection xmlns:d="DAV:"${namespaces ? ` ${namespaces}` : ""}>
43002
+ ${syncToken ? `<d:sync-token>${xmlEscape(syncToken)}</d:sync-token>` : "<d:sync-token/>"}
43003
+ <d:sync-level>1</d:sync-level>
43004
+ <d:prop>${prop}</d:prop>
43005
+ </d:sync-collection>`
43006
+ });
43007
+ if (response.status !== 207) {
43008
+ let body = "";
43009
+ try {
43010
+ body = await response.text();
43011
+ } catch {}
43012
+ const preconditions = davPreconditions(body);
43013
+ const prefix = this.errorPrefix;
43014
+ if (preconditions.includes("valid-sync-token"))
43015
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
43016
+ if (preconditions.includes("supported-report"))
43017
+ throw new DavFailure(`${prefix}:sync-unsupported`);
43018
+ if (response.status === 404)
43019
+ throw new DavFailure(`${prefix}:not-found`);
43020
+ if ([400, 409, 412].includes(response.status) && syncToken)
43021
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
43022
+ if (response.status === 403)
43023
+ throw new DavFailure(`${prefix}:authentication-failed`);
43024
+ if ([400, 405, 415, 501].includes(response.status))
43025
+ throw new DavFailure(`${prefix}:sync-unsupported`);
43026
+ throw new DavFailure(`${prefix}:sync-failed`);
43027
+ }
43028
+ let multistatus;
43029
+ try {
43030
+ const parsed = parseDavXml(await response.text(), {
43031
+ parseTagValue: false
43032
+ }).multistatus;
43033
+ if (!parsed || typeof parsed !== "object")
43034
+ throw new Error;
43035
+ multistatus = parsed;
43036
+ } catch {
43037
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43038
+ }
43039
+ const token = multistatus["sync-token"];
43040
+ if (typeof token !== "string" || token.trim() === "")
43041
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43042
+ const result = {
43043
+ changed: [],
43044
+ removed: [],
43045
+ syncToken: token.trim(),
43046
+ truncated: false
43047
+ };
43048
+ for (const item of asArray(multistatus.response)) {
43049
+ const href = davHref(item.href);
43050
+ if (!href)
43051
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43052
+ const id = new URL(href, collection).href;
43053
+ const status = item.status;
43054
+ if (id === collection.href || `${id}/` === collection.href) {
43055
+ if (isStatus(status, 507))
43056
+ result.truncated = true;
43057
+ continue;
43058
+ }
43059
+ if (!isDavCollectionChild(collection.href, id))
43060
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43061
+ if (isStatus(status, 404))
43062
+ result.removed.push(id);
43063
+ else
43064
+ result.changed.push({ id, props: successfulProps(item) });
43065
+ }
43066
+ return result;
43067
+ }
43068
+ async syncResources(collection, options, syncToken) {
43069
+ const property = options.data.slice(options.data.indexOf(":") + 1);
43070
+ const sync = await this.syncCollection(collection, `<d:getetag/><${options.data}/>`, syncToken, options.namespaces);
43071
+ const resources = [];
43072
+ const missing = new Map;
43073
+ const add = (id, value, etag) => {
43074
+ const data = value && typeof value === "object" ? value["#text"] : value;
43075
+ if (typeof data !== "string")
43076
+ return false;
43077
+ resources.push({
43078
+ id,
43079
+ data,
43080
+ ...typeof etag === "string" ? { etag } : {}
43081
+ });
43082
+ return true;
43083
+ };
43084
+ for (const member of sync.changed)
43085
+ if (!add(member.id, member.props[property], member.props.getetag))
43086
+ missing.set(member.id, member.props.getetag);
43087
+ if (missing.size) {
43088
+ const hrefs = [...missing.keys()].map((id) => `<d:href>${xmlEscape(new URL(id).pathname)}</d:href>`).join("");
43089
+ const response = await this.request(collection, {
43090
+ method: "REPORT",
43091
+ headers: {
43092
+ depth: "1",
43093
+ "content-type": "application/xml; charset=utf-8"
43094
+ },
43095
+ body: `<?xml version="1.0" encoding="utf-8"?>
43096
+ <${options.multiget} xmlns:d="DAV:" ${options.namespaces}>
43097
+ <d:prop><d:getetag/><${options.data}/></d:prop>
43098
+ ${hrefs}
43099
+ </${options.multiget}>`
43100
+ });
43101
+ if (response.status !== 207)
43102
+ return rejectDavResponse(response, `${this.errorPrefix}:sync-failed`);
43103
+ for (const item of await this.readMultistatus(response)) {
43104
+ const href = davHref(item.href);
43105
+ if (!href)
43106
+ continue;
43107
+ const id = new URL(href, collection).href;
43108
+ if (!missing.has(id))
43109
+ continue;
43110
+ if (isStatus(item.status, 404)) {
43111
+ sync.removed.push(id);
43112
+ missing.delete(id);
43113
+ continue;
43114
+ }
43115
+ const props = successfulProps(item);
43116
+ if (add(id, props[property], props.getetag ?? missing.get(id)))
43117
+ missing.delete(id);
43118
+ }
43119
+ if (missing.size)
43120
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
43121
+ }
43122
+ return {
43123
+ resources,
43124
+ removed: sync.removed,
43125
+ syncToken: sync.syncToken,
43126
+ truncated: sync.truncated
43127
+ };
43128
+ }
43129
+ async readMultistatus(response) {
42978
43130
  try {
42979
43131
  const parsed = parseDavXml(await response.text());
42980
43132
  return asArray(parsed.multistatus?.response);
@@ -43275,7 +43427,7 @@ var ActivityStreamSchema = {
43275
43427
  var package_default = {
43276
43428
  name: "@sockethub/schemas",
43277
43429
  description: "JSON schema files for validating Sockethub Activity Streams",
43278
- version: "3.0.0-beta.3",
43430
+ version: "3.0.0-beta.5",
43279
43431
  type: "module",
43280
43432
  private: false,
43281
43433
  author: "Nick Jennings <nick@silverbucket.net>",
@@ -43345,7 +43497,7 @@ var package_default = {
43345
43497
  "ajv-formats": "^2.1.1",
43346
43498
  "ajv-formats-draft2019": "^1.6.1"
43347
43499
  },
43348
- gitHead: "bc04522d60d1e2fa1b3098b339897de62c3b0549",
43500
+ gitHead: "cc4ffdd9714ebbd8372a88c4f2d795974e04029e",
43349
43501
  types: "./dist/index.d.ts"
43350
43502
  };
43351
43503
 
@@ -44306,6 +44458,14 @@ function queryBody(query) {
44306
44458
  ${filter}
44307
44459
  </c:addressbook-query>`;
44308
44460
  }
44461
+ function contact(data, id, etag) {
44462
+ try {
44463
+ return parseVCard(data, id, typeof etag === "string" ? etag : undefined);
44464
+ } catch {
44465
+ throw new DavFailure("carddav:invalid-response");
44466
+ }
44467
+ }
44468
+
44309
44469
  class CardDavClient extends DavClient {
44310
44470
  constructor(url, authentication, timeoutMs = 15000, networkOptions = {}) {
44311
44471
  super(url, authentication, "carddav", timeoutMs, networkOptions);
@@ -44371,29 +44531,30 @@ class CardDavClient extends DavClient {
44371
44531
  });
44372
44532
  if (response.status !== 207)
44373
44533
  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
44534
  const contacts = [];
44381
- for (const item of asArray(parsed.multistatus?.response)) {
44535
+ for (const item of await this.readMultistatus(response)) {
44382
44536
  const props = successfulProps(item);
44383
44537
  const itemHref = davHref(item.href);
44384
44538
  const data = props["address-data"];
44385
44539
  if (!itemHref || typeof data !== "string")
44386
44540
  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
- }
44541
+ contacts.push(contact(data, new URL(itemHref, book.id).href, props.getetag));
44392
44542
  if (query.limit && contacts.length >= query.limit)
44393
44543
  break;
44394
44544
  }
44395
44545
  return contacts;
44396
44546
  }
44547
+ async sync(book, syncToken) {
44548
+ const { resources, ...result } = await this.syncResources(new URL(book.id), {
44549
+ namespaces: 'xmlns:c="urn:ietf:params:xml:ns:carddav"',
44550
+ data: "c:address-data",
44551
+ multiget: "c:addressbook-multiget"
44552
+ }, syncToken);
44553
+ return {
44554
+ ...result,
44555
+ items: resources.map((resource) => contact(resource.data, resource.id, resource.etag))
44556
+ };
44557
+ }
44397
44558
  async create(book, input) {
44398
44559
  const card = buildVCard(input, []);
44399
44560
  const resource = new URL(`${encodeURIComponent(card.uid)}.vcf`, book.id.endsWith("/") ? book.id : `${book.id}/`);
@@ -44486,7 +44647,7 @@ class CardDavClient extends DavClient {
44486
44647
  var package_default2 = {
44487
44648
  name: "@sockethub/platform-carddav",
44488
44649
  description: "A Sockethub platform for CardDAV address books and contacts",
44489
- version: "1.0.0-beta.3",
44650
+ version: "1.0.0-beta.5",
44490
44651
  type: "module",
44491
44652
  private: false,
44492
44653
  license: "LGPL-3.0+",
@@ -44515,21 +44676,21 @@ var package_default2 = {
44515
44676
  test: "bun test src/"
44516
44677
  },
44517
44678
  dependencies: {
44518
- "@sockethub/dav": "^1.0.0-beta.3",
44519
- "@sockethub/schemas": "^3.0.0-beta.3"
44679
+ "@sockethub/dav": "^1.0.0-beta.5",
44680
+ "@sockethub/schemas": "^3.0.0-beta.5"
44520
44681
  },
44521
44682
  devDependencies: {
44522
44683
  "@types/bun": "latest"
44523
44684
  },
44524
44685
  peerDependencies: {
44525
- "@sockethub/server": "^5.0.0-beta.3"
44686
+ "@sockethub/server": "^5.0.0-beta.5"
44526
44687
  },
44527
44688
  peerDependenciesMeta: {
44528
44689
  "@sockethub/server": {
44529
44690
  optional: true
44530
44691
  }
44531
44692
  },
44532
- gitHead: "bc04522d60d1e2fa1b3098b339897de62c3b0549"
44693
+ gitHead: "cc4ffdd9714ebbd8372a88c4f2d795974e04029e"
44533
44694
  };
44534
44695
 
44535
44696
  // src/schema.ts
@@ -44637,7 +44798,7 @@ var shared = {
44637
44798
  vcardVersion: { enum: ["3.0", "4.0"] },
44638
44799
  updateSupported: { type: "boolean" }
44639
44800
  };
44640
- var contact = {
44801
+ var contact2 = {
44641
44802
  type: "object",
44642
44803
  required: ["type", "name"],
44643
44804
  additionalProperties: false,
@@ -44658,6 +44819,12 @@ var query = {
44658
44819
  limit: { type: "integer", minimum: 1, maximum: 1000 }
44659
44820
  }
44660
44821
  };
44822
+ var syncToken = { type: "string", minLength: 1, maxLength: 4096 };
44823
+ var sync = {
44824
+ type: "object",
44825
+ additionalProperties: false,
44826
+ properties: { syncToken }
44827
+ };
44661
44828
  var deletion = {
44662
44829
  type: "object",
44663
44830
  required: ["id", "type", "etag"],
@@ -44665,11 +44832,11 @@ var deletion = {
44665
44832
  properties: { id: shared.id, type: shared.type, etag: shared.etag }
44666
44833
  };
44667
44834
  var updateContact = {
44668
- ...contact,
44835
+ ...contact2,
44669
44836
  required: ["type", "name", "id", "uid", "etag"]
44670
44837
  };
44671
44838
  var responseContact = {
44672
- ...contact,
44839
+ ...contact2,
44673
44840
  required: ["id", "type", "uid", "name", "vcardVersion", "updateSupported"]
44674
44841
  };
44675
44842
  var mutation = {
@@ -44727,10 +44894,19 @@ var PlatformCardDavSchema = {
44727
44894
  messages: {
44728
44895
  required: ["type", "actor"],
44729
44896
  properties: {
44730
- type: { enum: ["fetch", "query", "create", "update", "delete"] },
44897
+ type: {
44898
+ enum: ["fetch", "query", "sync", "create", "update", "delete"]
44899
+ },
44731
44900
  actor,
44732
44901
  target,
44733
- object: { oneOf: [contact, query, deletion] }
44902
+ object: {
44903
+ oneOf: [
44904
+ contact2,
44905
+ query,
44906
+ deletion,
44907
+ { ...sync, required: ["syncToken"] }
44908
+ ]
44909
+ }
44734
44910
  }
44735
44911
  },
44736
44912
  messageConstraints: {
@@ -44747,7 +44923,11 @@ var PlatformCardDavSchema = {
44747
44923
  required: ["type", "actor", "target"]
44748
44924
  },
44749
44925
  {
44750
- properties: { type: { const: "create" }, object: contact },
44926
+ properties: { type: { const: "sync" }, object: sync },
44927
+ required: ["type", "actor", "target"]
44928
+ },
44929
+ {
44930
+ properties: { type: { const: "create" }, object: contact2 },
44751
44931
  required: ["type", "actor", "target", "object"]
44752
44932
  },
44753
44933
  {
@@ -44800,6 +44980,30 @@ var PlatformCardDavSchema = {
44800
44980
  }
44801
44981
  }
44802
44982
  },
44983
+ {
44984
+ type: "object",
44985
+ required: [
44986
+ "@context",
44987
+ "type",
44988
+ "totalItems",
44989
+ "items",
44990
+ "removed",
44991
+ "syncToken",
44992
+ "truncated"
44993
+ ],
44994
+ additionalProperties: false,
44995
+ properties: {
44996
+ "@context": { type: "array", items: { type: "string" } },
44997
+ id: { type: ["string", "null"] },
44998
+ type: { const: "collection" },
44999
+ summary: { type: "string" },
45000
+ totalItems: { type: "integer", minimum: 0 },
45001
+ items: { type: "array", items: responseContact },
45002
+ removed: { type: "array", items: shared.id },
45003
+ syncToken,
45004
+ truncated: { type: "boolean" }
45005
+ }
45006
+ },
44803
45007
  {
44804
45008
  type: "object",
44805
45009
  required: ["@context", "type", "actor", "target", "object"],
@@ -44825,7 +45029,14 @@ class CardDav {
44825
45029
  discovery = new DiscoveryCache;
44826
45030
  config = {
44827
45031
  persist: false,
44828
- requireCredentials: ["fetch", "query", "create", "update", "delete"],
45032
+ requireCredentials: [
45033
+ "fetch",
45034
+ "query",
45035
+ "sync",
45036
+ "create",
45037
+ "update",
45038
+ "delete"
45039
+ ],
44829
45040
  connectTimeoutMs: 15000,
44830
45041
  allowPrivateAddresses: false,
44831
45042
  allowInsecureHttp: false,
@@ -44866,6 +45077,18 @@ class CardDav {
44866
45077
  items
44867
45078
  })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44868
45079
  }
45080
+ sync(job, credentials, done) {
45081
+ const client = this.client(credentials);
45082
+ const input = job.object ?? {};
45083
+ this.book(client, credentials, job.target?.id ?? "").then((book) => client.sync(book, input.syncToken)).then((result) => done(null, {
45084
+ "@context": CONTEXT,
45085
+ ...job.id ? { id: job.id } : {},
45086
+ type: "collection",
45087
+ summary: "CardDAV changes",
45088
+ totalItems: result.items.length,
45089
+ ...result
45090
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
45091
+ }
44869
45092
  create(job, credentials, done) {
44870
45093
  const client = this.client(credentials);
44871
45094
  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 +45169,4 @@ export {
44946
45169
  parseVCard
44947
45170
  };
44948
45171
 
44949
- //# debugId=D20292E281EC330764756E2164756E21
45172
+ //# debugId=A470017A427BECB364756E2164756E21