@sockethub/platform-caldav 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/dist/index.js CHANGED
@@ -66075,9 +66075,10 @@ function xmlText(value) {
66075
66075
  return predefined[entity.toLowerCase()] ?? reference;
66076
66076
  });
66077
66077
  }
66078
- function parseDavXml(xml) {
66078
+ function parseDavXml(xml, options = {}) {
66079
66079
  return new XMLParser({
66080
66080
  ignoreAttributes: false,
66081
+ parseTagValue: options.parseTagValue ?? true,
66081
66082
  removeNSPrefix: true,
66082
66083
  processEntities: false,
66083
66084
  tagValueProcessor: (_tagName, value) => xmlText(value),
@@ -66106,6 +66107,39 @@ function propertyHref(value) {
66106
66107
  return davHref(value.href);
66107
66108
  return davHref(value);
66108
66109
  }
66110
+ function safePathname(pathname) {
66111
+ if (pathname.includes("\\") || /%(?:2f|5c)/i.test(pathname))
66112
+ return false;
66113
+ for (const segment of pathname.split("/")) {
66114
+ let decoded = segment;
66115
+ try {
66116
+ for (let pass = 0;pass < 3; pass += 1) {
66117
+ const next = decodeURIComponent(decoded);
66118
+ if (next === decoded)
66119
+ break;
66120
+ decoded = next;
66121
+ }
66122
+ } catch {
66123
+ return false;
66124
+ }
66125
+ if (decoded === "." || decoded === ".." || /[\\/]/.test(decoded))
66126
+ return false;
66127
+ }
66128
+ return true;
66129
+ }
66130
+ function isDavCollectionChild(collectionId, resourceId) {
66131
+ try {
66132
+ const collection = new URL(collectionId);
66133
+ const resource = new URL(resourceId);
66134
+ if (collection.search || collection.hash || resource.search || resource.hash || !safePathname(collection.pathname) || !safePathname(resource.pathname))
66135
+ return false;
66136
+ const prefix = collection.pathname.endsWith("/") ? collection.pathname : `${collection.pathname}/`;
66137
+ return resource.origin === collection.origin && resource.pathname.startsWith(prefix) && resource.pathname !== prefix;
66138
+ } catch {
66139
+ return false;
66140
+ }
66141
+ }
66142
+
66109
66143
  class DiscoveryCache {
66110
66144
  maxEntries;
66111
66145
  secret = randomBytes(32);
@@ -66223,6 +66257,16 @@ function digestChallenge(header) {
66223
66257
  return;
66224
66258
  }
66225
66259
  var quote = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
66260
+ var isStatus = (value, code) => new RegExp(`\\s${code}(?:\\s|$)`).test(String(value ?? ""));
66261
+ function davPreconditions(xml) {
66262
+ try {
66263
+ const error = parseDavXml(xml).error;
66264
+ return error && typeof error === "object" ? Object.keys(error) : [];
66265
+ } catch {
66266
+ return [];
66267
+ }
66268
+ }
66269
+ var xmlEscape = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
66226
66270
 
66227
66271
  class DavClient {
66228
66272
  authentication;
@@ -66267,9 +66311,10 @@ class DavClient {
66267
66311
  this.assertAllowed(url);
66268
66312
  let response;
66269
66313
  try {
66314
+ const { passForbidden: _, ...fetchInit } = init;
66270
66315
  const authorization = this.authorization(url, init);
66271
66316
  response = await this.fetchImpl(url, {
66272
- ...init,
66317
+ ...fetchInit,
66273
66318
  redirect: "manual",
66274
66319
  signal,
66275
66320
  headers: {
@@ -66313,7 +66358,7 @@ class DavClient {
66313
66358
  }
66314
66359
  throw new DavFailure(`${this.errorPrefix}:${authAttempts > 0 ? "authentication-failed" : "unsupported-authentication"}`);
66315
66360
  }
66316
- if (response.status === 401 || response.status === 403) {
66361
+ if (response.status === 401 || response.status === 403 && !init.passForbidden) {
66317
66362
  await response.body?.cancel().catch(() => {});
66318
66363
  throw new DavFailure(`${this.errorPrefix}:authentication-failed`);
66319
66364
  }
@@ -66334,6 +66379,146 @@ class DavClient {
66334
66379
  await response.body?.cancel().catch(() => {});
66335
66380
  throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66336
66381
  }
66382
+ return this.readMultistatus(response);
66383
+ }
66384
+ async syncCollection(collection, prop, syncToken, namespaces = "") {
66385
+ const response = await this.request(collection, {
66386
+ method: "REPORT",
66387
+ passForbidden: true,
66388
+ headers: {
66389
+ depth: "0",
66390
+ "content-type": "application/xml; charset=utf-8"
66391
+ },
66392
+ body: `<?xml version="1.0" encoding="utf-8"?>
66393
+ <d:sync-collection xmlns:d="DAV:"${namespaces ? ` ${namespaces}` : ""}>
66394
+ ${syncToken ? `<d:sync-token>${xmlEscape(syncToken)}</d:sync-token>` : "<d:sync-token/>"}
66395
+ <d:sync-level>1</d:sync-level>
66396
+ <d:prop>${prop}</d:prop>
66397
+ </d:sync-collection>`
66398
+ });
66399
+ if (response.status !== 207) {
66400
+ let body = "";
66401
+ try {
66402
+ body = await response.text();
66403
+ } catch {}
66404
+ const preconditions = davPreconditions(body);
66405
+ const prefix = this.errorPrefix;
66406
+ if (preconditions.includes("valid-sync-token"))
66407
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
66408
+ if (preconditions.includes("supported-report"))
66409
+ throw new DavFailure(`${prefix}:sync-unsupported`);
66410
+ if (response.status === 404)
66411
+ throw new DavFailure(`${prefix}:not-found`);
66412
+ if ([400, 409, 412].includes(response.status) && syncToken)
66413
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
66414
+ if (response.status === 403)
66415
+ throw new DavFailure(`${prefix}:authentication-failed`);
66416
+ if ([400, 405, 415, 501].includes(response.status))
66417
+ throw new DavFailure(`${prefix}:sync-unsupported`);
66418
+ throw new DavFailure(`${prefix}:sync-failed`);
66419
+ }
66420
+ let multistatus;
66421
+ try {
66422
+ const parsed = parseDavXml(await response.text(), {
66423
+ parseTagValue: false
66424
+ }).multistatus;
66425
+ if (!parsed || typeof parsed !== "object")
66426
+ throw new Error;
66427
+ multistatus = parsed;
66428
+ } catch {
66429
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66430
+ }
66431
+ const token = multistatus["sync-token"];
66432
+ if (typeof token !== "string" || token.trim() === "")
66433
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66434
+ const result = {
66435
+ changed: [],
66436
+ removed: [],
66437
+ syncToken: token.trim(),
66438
+ truncated: false
66439
+ };
66440
+ for (const item of asArray(multistatus.response)) {
66441
+ const href = davHref(item.href);
66442
+ if (!href)
66443
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66444
+ const id = new URL(href, collection).href;
66445
+ const status = item.status;
66446
+ if (id === collection.href || `${id}/` === collection.href) {
66447
+ if (isStatus(status, 507))
66448
+ result.truncated = true;
66449
+ continue;
66450
+ }
66451
+ if (!isDavCollectionChild(collection.href, id))
66452
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66453
+ if (isStatus(status, 404))
66454
+ result.removed.push(id);
66455
+ else
66456
+ result.changed.push({ id, props: successfulProps(item) });
66457
+ }
66458
+ return result;
66459
+ }
66460
+ async syncResources(collection, options, syncToken) {
66461
+ const property = options.data.slice(options.data.indexOf(":") + 1);
66462
+ const sync = await this.syncCollection(collection, `<d:getetag/><${options.data}/>`, syncToken, options.namespaces);
66463
+ const resources = [];
66464
+ const missing = new Map;
66465
+ const add = (id, value, etag) => {
66466
+ const data = value && typeof value === "object" ? value["#text"] : value;
66467
+ if (typeof data !== "string")
66468
+ return false;
66469
+ resources.push({
66470
+ id,
66471
+ data,
66472
+ ...typeof etag === "string" ? { etag } : {}
66473
+ });
66474
+ return true;
66475
+ };
66476
+ for (const member of sync.changed)
66477
+ if (!add(member.id, member.props[property], member.props.getetag))
66478
+ missing.set(member.id, member.props.getetag);
66479
+ if (missing.size) {
66480
+ const hrefs = [...missing.keys()].map((id) => `<d:href>${xmlEscape(new URL(id).pathname)}</d:href>`).join("");
66481
+ const response = await this.request(collection, {
66482
+ method: "REPORT",
66483
+ headers: {
66484
+ depth: "1",
66485
+ "content-type": "application/xml; charset=utf-8"
66486
+ },
66487
+ body: `<?xml version="1.0" encoding="utf-8"?>
66488
+ <${options.multiget} xmlns:d="DAV:" ${options.namespaces}>
66489
+ <d:prop><d:getetag/><${options.data}/></d:prop>
66490
+ ${hrefs}
66491
+ </${options.multiget}>`
66492
+ });
66493
+ if (response.status !== 207)
66494
+ return rejectDavResponse(response, `${this.errorPrefix}:sync-failed`);
66495
+ for (const item of await this.readMultistatus(response)) {
66496
+ const href = davHref(item.href);
66497
+ if (!href)
66498
+ continue;
66499
+ const id = new URL(href, collection).href;
66500
+ if (!missing.has(id))
66501
+ continue;
66502
+ if (isStatus(item.status, 404)) {
66503
+ sync.removed.push(id);
66504
+ missing.delete(id);
66505
+ continue;
66506
+ }
66507
+ const props = successfulProps(item);
66508
+ if (add(id, props[property], props.getetag ?? missing.get(id)))
66509
+ missing.delete(id);
66510
+ }
66511
+ if (missing.size)
66512
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66513
+ }
66514
+ return {
66515
+ resources,
66516
+ removed: sync.removed,
66517
+ syncToken: sync.syncToken,
66518
+ truncated: sync.truncated
66519
+ };
66520
+ }
66521
+ async readMultistatus(response) {
66337
66522
  try {
66338
66523
  const parsed = parseDavXml(await response.text());
66339
66524
  return asArray(parsed.multistatus?.response);
@@ -66634,7 +66819,7 @@ var ActivityStreamSchema = {
66634
66819
  var package_default = {
66635
66820
  name: "@sockethub/schemas",
66636
66821
  description: "JSON schema files for validating Sockethub Activity Streams",
66637
- version: "3.0.0-beta.3",
66822
+ version: "3.0.0-beta.5",
66638
66823
  type: "module",
66639
66824
  private: false,
66640
66825
  author: "Nick Jennings <nick@silverbucket.net>",
@@ -66704,7 +66889,7 @@ var package_default = {
66704
66889
  "ajv-formats": "^2.1.1",
66705
66890
  "ajv-formats-draft2019": "^1.6.1"
66706
66891
  },
66707
- gitHead: "bc04522d60d1e2fa1b3098b339897de62c3b0549",
66892
+ gitHead: "cc4ffdd9714ebbd8372a88c4f2d795974e04029e",
66708
66893
  types: "./dist/index.d.ts"
66709
66894
  };
66710
66895
 
@@ -68124,7 +68309,15 @@ var queryBody = (query) => `<?xml version="1.0" encoding="utf-8"?>
68124
68309
  <d:prop><d:getetag/><c:calendar-data/></d:prop>
68125
68310
  <c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="${query.type === "task" ? "VTODO" : "VEVENT"}">${query.startTime || query.endTime ? `<c:time-range${query.startTime ? ` start="${calDavTime(query.startTime)}"` : ""}${query.endTime ? ` end="${calDavTime(query.endTime)}"` : ""}/>` : ""}</c:comp-filter></c:comp-filter></c:filter>
68126
68311
  </c:calendar-query>`;
68312
+ var HAS_EVENT_OR_TASK = /^BEGIN:(?:VEVENT|VTODO)\r?$/m;
68127
68313
  var CalDavFailure = DavFailure;
68314
+ function calendarItem(data, id, etag) {
68315
+ try {
68316
+ return parseICalendar(data, id, typeof etag === "string" ? etag : undefined);
68317
+ } catch {
68318
+ throw new CalDavFailure("caldav:invalid-response");
68319
+ }
68320
+ }
68128
68321
 
68129
68322
  class CalDavClient extends DavClient {
68130
68323
  constructor(url, authentication, timeoutMs = 15000, networkOptions = {}) {
@@ -68232,28 +68425,28 @@ class CalDavClient extends DavClient {
68232
68425
  await response.body?.cancel().catch(() => {});
68233
68426
  throw new CalDavFailure("caldav:query-failed");
68234
68427
  }
68235
- let parsed;
68236
- try {
68237
- parsed = parseDavXml(await response.text());
68238
- } catch {
68239
- throw new CalDavFailure("caldav:invalid-response");
68240
- }
68241
- const responses = asArray(parsed.multistatus?.response);
68242
- for (const item of responses) {
68428
+ for (const item of await this.readMultistatus(response)) {
68243
68429
  const props = successfulProps(item);
68244
68430
  const itemHref = davHref(item.href);
68245
68431
  const data = props["calendar-data"];
68246
68432
  if (!itemHref || typeof data !== "string")
68247
68433
  continue;
68248
- try {
68249
- items.push(parseICalendar(data, new URL(itemHref, calendar.id).href, typeof props.getetag === "string" ? props.getetag : undefined));
68250
- } catch {
68251
- throw new CalDavFailure("caldav:invalid-response");
68252
- }
68434
+ items.push(calendarItem(data, new URL(itemHref, calendar.id).href, props.getetag));
68253
68435
  }
68254
68436
  }
68255
68437
  return items;
68256
68438
  }
68439
+ async sync(calendar, syncToken) {
68440
+ const { resources, ...result } = await this.syncResources(new URL(calendar.id), {
68441
+ namespaces: 'xmlns:c="urn:ietf:params:xml:ns:caldav"',
68442
+ data: "c:calendar-data",
68443
+ multiget: "c:calendar-multiget"
68444
+ }, syncToken);
68445
+ return {
68446
+ ...result,
68447
+ items: resources.filter((resource) => HAS_EVENT_OR_TASK.test(resource.data)).map((resource) => calendarItem(resource.data, resource.id, resource.etag))
68448
+ };
68449
+ }
68257
68450
  async update(id, etag, body) {
68258
68451
  const resource = new URL(id);
68259
68452
  this.assertAllowed(resource);
@@ -68489,7 +68682,7 @@ END:VCALENDAR\r
68489
68682
  var package_default2 = {
68490
68683
  name: "@sockethub/platform-caldav",
68491
68684
  description: "A Sockethub platform for synchronizing CalDAV calendars, events, and tasks",
68492
- version: "1.0.0-beta.3",
68685
+ version: "1.0.0-beta.5",
68493
68686
  type: "module",
68494
68687
  private: false,
68495
68688
  license: "LGPL-3.0+",
@@ -68523,16 +68716,16 @@ var package_default2 = {
68523
68716
  test: "bun test src/"
68524
68717
  },
68525
68718
  dependencies: {
68526
- "@sockethub/dav": "^1.0.0-beta.3",
68527
- "@sockethub/schemas": "^3.0.0-beta.3",
68528
- "@sockethub/util": "^1.0.0-beta.3",
68719
+ "@sockethub/dav": "^1.0.0-beta.5",
68720
+ "@sockethub/schemas": "^3.0.0-beta.5",
68721
+ "@sockethub/util": "^1.0.0-beta.5",
68529
68722
  undici: "^7.30.0"
68530
68723
  },
68531
68724
  devDependencies: {
68532
68725
  "@types/bun": "latest"
68533
68726
  },
68534
68727
  peerDependencies: {
68535
- "@sockethub/server": "^5.0.0-beta.3"
68728
+ "@sockethub/server": "^5.0.0-beta.5"
68536
68729
  },
68537
68730
  peerDependenciesMeta: {
68538
68731
  "@sockethub/server": {
@@ -68540,7 +68733,7 @@ var package_default2 = {
68540
68733
  }
68541
68734
  },
68542
68735
  types: "./dist/index.d.ts",
68543
- gitHead: "bc04522d60d1e2fa1b3098b339897de62c3b0549"
68736
+ gitHead: "cc4ffdd9714ebbd8372a88c4f2d795974e04029e"
68544
68737
  };
68545
68738
 
68546
68739
  // src/schema.ts
@@ -68737,7 +68930,13 @@ var queryObject = {
68737
68930
  endTime: { type: "string", format: "date-time" }
68738
68931
  }
68739
68932
  };
68740
- var calendarItem = { oneOf: [eventObject, taskObject] };
68933
+ var syncToken = { type: "string", minLength: 1, maxLength: 4096 };
68934
+ var syncObject = {
68935
+ type: "object",
68936
+ additionalProperties: false,
68937
+ properties: { syncToken }
68938
+ };
68939
+ var calendarItem2 = { oneOf: [eventObject, taskObject] };
68741
68940
  var mutationObject = {
68742
68941
  type: "object",
68743
68942
  required: ["id", "type"],
@@ -68760,12 +68959,26 @@ var PlatformCalDavSchema = {
68760
68959
  properties: {
68761
68960
  type: {
68762
68961
  type: "string",
68763
- enum: ["fetch", "read", "query", "create", "update", "delete"]
68962
+ enum: [
68963
+ "fetch",
68964
+ "read",
68965
+ "query",
68966
+ "sync",
68967
+ "create",
68968
+ "update",
68969
+ "delete"
68970
+ ]
68764
68971
  },
68765
68972
  actor,
68766
68973
  target: { oneOf: [target, feedTarget] },
68767
68974
  object: {
68768
- oneOf: [eventObject, taskObject, deleteObject, queryObject]
68975
+ oneOf: [
68976
+ eventObject,
68977
+ taskObject,
68978
+ deleteObject,
68979
+ queryObject,
68980
+ { ...syncObject, required: ["syncToken"] }
68981
+ ]
68769
68982
  }
68770
68983
  }
68771
68984
  },
@@ -68794,11 +69007,19 @@ var PlatformCalDavSchema = {
68794
69007
  },
68795
69008
  required: ["type", "actor", "target"]
68796
69009
  },
69010
+ {
69011
+ properties: {
69012
+ type: { const: "sync" },
69013
+ target,
69014
+ object: syncObject
69015
+ },
69016
+ required: ["type", "actor", "target"]
69017
+ },
68797
69018
  {
68798
69019
  properties: {
68799
69020
  type: { const: "create" },
68800
69021
  target,
68801
- object: calendarItem
69022
+ object: calendarItem2
68802
69023
  },
68803
69024
  required: ["type", "actor", "target", "object"]
68804
69025
  },
@@ -68909,7 +69130,7 @@ var PlatformCalDavSchema = {
68909
69130
  type: "array",
68910
69131
  items: {
68911
69132
  oneOf: [
68912
- calendarItem,
69133
+ calendarItem2,
68913
69134
  {
68914
69135
  type: "object",
68915
69136
  required: [
@@ -68939,6 +69160,30 @@ var PlatformCalDavSchema = {
68939
69160
  }
68940
69161
  }
68941
69162
  },
69163
+ {
69164
+ type: "object",
69165
+ required: [
69166
+ "@context",
69167
+ "type",
69168
+ "totalItems",
69169
+ "items",
69170
+ "removed",
69171
+ "syncToken",
69172
+ "truncated"
69173
+ ],
69174
+ additionalProperties: false,
69175
+ properties: {
69176
+ "@context": { type: "array", items: { type: "string" } },
69177
+ id: { type: ["string", "null"] },
69178
+ type: { const: "collection" },
69179
+ summary: { type: "string" },
69180
+ totalItems: { type: "integer", minimum: 0 },
69181
+ items: { type: "array", items: calendarItem2 },
69182
+ removed: { type: "array", items: shared.id },
69183
+ syncToken,
69184
+ truncated: { type: "boolean" }
69185
+ }
69186
+ },
68942
69187
  {
68943
69188
  type: "object",
68944
69189
  required: ["@context", "type", "actor", "target", "object"],
@@ -68960,39 +69205,8 @@ var PlatformCalDavSchema = {
68960
69205
  var CONTEXT = buildCanonicalContext(PlatformCalDavSchema.contextUrl);
68961
69206
  var MAX_FEED_BYTES = 10 * 1024 * 1024;
68962
69207
  var MAX_FEED_REDIRECTS = 5;
68963
- function safePathname(pathname) {
68964
- if (pathname.includes("\\") || /%(?:2f|5c)/i.test(pathname))
68965
- return false;
68966
- for (const segment of pathname.split("/")) {
68967
- let decoded = segment;
68968
- try {
68969
- for (let pass = 0;pass < 3; pass += 1) {
68970
- const next = decodeURIComponent(decoded);
68971
- if (next === decoded)
68972
- break;
68973
- decoded = next;
68974
- }
68975
- } catch {
68976
- return false;
68977
- }
68978
- if (decoded === "." || decoded === ".." || /[\\/]/.test(decoded))
68979
- return false;
68980
- }
68981
- return true;
68982
- }
68983
69208
  function assertCalendarResource(calendarId, resourceId) {
68984
- let calendar;
68985
- let resource;
68986
- try {
68987
- calendar = new URL(calendarId);
68988
- resource = new URL(resourceId);
68989
- } catch {
68990
- throw new CalDavFailure("caldav:invalid-resource");
68991
- }
68992
- if (calendar.search || calendar.hash || resource.search || resource.hash || !safePathname(calendar.pathname) || !safePathname(resource.pathname))
68993
- throw new CalDavFailure("caldav:invalid-resource");
68994
- const prefix = calendar.pathname.endsWith("/") ? calendar.pathname : `${calendar.pathname}/`;
68995
- if (resource.origin !== calendar.origin || !resource.pathname.startsWith(prefix) || resource.pathname === prefix)
69209
+ if (!isDavCollectionChild(calendarId, resourceId))
68996
69210
  throw new CalDavFailure("caldav:invalid-resource");
68997
69211
  }
68998
69212
 
@@ -69003,7 +69217,14 @@ class CalDav {
69003
69217
  discovery = new DiscoveryCache;
69004
69218
  config = {
69005
69219
  persist: false,
69006
- requireCredentials: ["fetch", "query", "create", "update", "delete"],
69220
+ requireCredentials: [
69221
+ "fetch",
69222
+ "query",
69223
+ "sync",
69224
+ "create",
69225
+ "update",
69226
+ "delete"
69227
+ ],
69007
69228
  connectTimeoutMs: 15000,
69008
69229
  allowPrivateAddresses: false,
69009
69230
  allowInsecureHttp: false,
@@ -69093,6 +69314,18 @@ class CalDav {
69093
69314
  items
69094
69315
  })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
69095
69316
  }
69317
+ sync(job, credentials, done) {
69318
+ const client = this.client(credentials);
69319
+ const input = job.object ?? {};
69320
+ this.calendar(client, credentials, job.target?.id ?? "").then((calendar) => client.sync(calendar, input.syncToken)).then((result) => done(null, {
69321
+ "@context": CONTEXT,
69322
+ ...job.id ? { id: job.id } : {},
69323
+ type: "collection",
69324
+ summary: "CalDAV changes",
69325
+ totalItems: result.items.length,
69326
+ ...result
69327
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
69328
+ }
69096
69329
  update(job, credentials, done) {
69097
69330
  const client = this.client(credentials);
69098
69331
  const input = job.object;
@@ -69228,4 +69461,4 @@ export {
69228
69461
  foldLine
69229
69462
  };
69230
69463
 
69231
- //# debugId=DEF47195F0892FA064756E2164756E21
69464
+ //# debugId=16184EE35BE0342464756E2164756E21