@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/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,175 @@ 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 currentSyncToken(collection) {
66385
+ const response = await this.request(collection, {
66386
+ method: "PROPFIND",
66387
+ headers: {
66388
+ depth: "0",
66389
+ "content-type": "application/xml; charset=utf-8"
66390
+ },
66391
+ body: `<?xml version="1.0" encoding="utf-8"?>
66392
+ <d:propfind xmlns:d="DAV:"><d:prop><d:sync-token/></d:prop></d:propfind>`
66393
+ });
66394
+ if (response.status === 404)
66395
+ return rejectDavResponse(response, `${this.errorPrefix}:not-found`);
66396
+ if (response.status !== 207) {
66397
+ await response.body?.cancel().catch(() => {});
66398
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66399
+ }
66400
+ let responses;
66401
+ try {
66402
+ responses = asArray(parseDavXml(await response.text(), {
66403
+ parseTagValue: false
66404
+ }).multistatus?.response);
66405
+ } catch {
66406
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66407
+ }
66408
+ const token = successfulProps(responses[0] ?? {})["sync-token"];
66409
+ if (typeof token !== "string" || token.trim() === "")
66410
+ throw new DavFailure(`${this.errorPrefix}:sync-unsupported`);
66411
+ return token.trim();
66412
+ }
66413
+ async syncCollection(collection, prop, syncToken, namespaces = "") {
66414
+ const response = await this.request(collection, {
66415
+ method: "REPORT",
66416
+ passForbidden: true,
66417
+ headers: {
66418
+ depth: "0",
66419
+ "content-type": "application/xml; charset=utf-8"
66420
+ },
66421
+ body: `<?xml version="1.0" encoding="utf-8"?>
66422
+ <d:sync-collection xmlns:d="DAV:"${namespaces ? ` ${namespaces}` : ""}>
66423
+ ${syncToken ? `<d:sync-token>${xmlEscape(syncToken)}</d:sync-token>` : "<d:sync-token/>"}
66424
+ <d:sync-level>1</d:sync-level>
66425
+ <d:prop>${prop}</d:prop>
66426
+ </d:sync-collection>`
66427
+ });
66428
+ if (response.status !== 207) {
66429
+ let body = "";
66430
+ try {
66431
+ body = await response.text();
66432
+ } catch {}
66433
+ const preconditions = davPreconditions(body);
66434
+ const prefix = this.errorPrefix;
66435
+ if (preconditions.includes("valid-sync-token"))
66436
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
66437
+ if (preconditions.includes("supported-report"))
66438
+ throw new DavFailure(`${prefix}:sync-unsupported`);
66439
+ if (response.status === 404)
66440
+ throw new DavFailure(`${prefix}:not-found`);
66441
+ if ([400, 409, 412].includes(response.status) && syncToken)
66442
+ throw new DavFailure(`${prefix}:sync-token-invalid`);
66443
+ if (response.status === 403)
66444
+ throw new DavFailure(`${prefix}:authentication-failed`);
66445
+ if ([400, 405, 415, 501].includes(response.status))
66446
+ throw new DavFailure(`${prefix}:sync-unsupported`);
66447
+ throw new DavFailure(`${prefix}:sync-failed`);
66448
+ }
66449
+ let multistatus;
66450
+ try {
66451
+ const parsed = parseDavXml(await response.text(), {
66452
+ parseTagValue: false
66453
+ }).multistatus;
66454
+ if (!parsed || typeof parsed !== "object")
66455
+ throw new Error;
66456
+ multistatus = parsed;
66457
+ } catch {
66458
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66459
+ }
66460
+ const token = multistatus["sync-token"];
66461
+ if (typeof token !== "string" || token.trim() === "")
66462
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66463
+ const result = {
66464
+ changed: [],
66465
+ removed: [],
66466
+ syncToken: token.trim(),
66467
+ truncated: false
66468
+ };
66469
+ for (const item of asArray(multistatus.response)) {
66470
+ const href = davHref(item.href);
66471
+ if (!href)
66472
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66473
+ const id = new URL(href, collection).href;
66474
+ const status = item.status;
66475
+ if (id === collection.href || `${id}/` === collection.href) {
66476
+ if (isStatus(status, 507))
66477
+ result.truncated = true;
66478
+ continue;
66479
+ }
66480
+ if (!isDavCollectionChild(collection.href, id))
66481
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66482
+ if (isStatus(status, 404))
66483
+ result.removed.push(id);
66484
+ else
66485
+ result.changed.push({ id, props: successfulProps(item) });
66486
+ }
66487
+ return result;
66488
+ }
66489
+ async syncResources(collection, options, syncToken) {
66490
+ const property = options.data.slice(options.data.indexOf(":") + 1);
66491
+ const sync = await this.syncCollection(collection, `<d:getetag/><${options.data}/>`, syncToken, options.namespaces);
66492
+ const resources = [];
66493
+ const missing = new Map;
66494
+ const add = (id, value, etag) => {
66495
+ const data = value && typeof value === "object" ? value["#text"] : value;
66496
+ if (typeof data !== "string")
66497
+ return false;
66498
+ resources.push({
66499
+ id,
66500
+ data,
66501
+ ...typeof etag === "string" ? { etag } : {}
66502
+ });
66503
+ return true;
66504
+ };
66505
+ for (const member of sync.changed)
66506
+ if (!add(member.id, member.props[property], member.props.getetag))
66507
+ missing.set(member.id, member.props.getetag);
66508
+ if (missing.size) {
66509
+ const hrefs = [...missing.keys()].map((id) => `<d:href>${xmlEscape(new URL(id).pathname)}</d:href>`).join("");
66510
+ const response = await this.request(collection, {
66511
+ method: "REPORT",
66512
+ headers: {
66513
+ depth: "1",
66514
+ "content-type": "application/xml; charset=utf-8"
66515
+ },
66516
+ body: `<?xml version="1.0" encoding="utf-8"?>
66517
+ <${options.multiget} xmlns:d="DAV:" ${options.namespaces}>
66518
+ <d:prop><d:getetag/><${options.data}/></d:prop>
66519
+ ${hrefs}
66520
+ </${options.multiget}>`
66521
+ });
66522
+ if (response.status !== 207)
66523
+ return rejectDavResponse(response, `${this.errorPrefix}:sync-failed`);
66524
+ for (const item of await this.readMultistatus(response)) {
66525
+ const href = davHref(item.href);
66526
+ if (!href)
66527
+ continue;
66528
+ const id = new URL(href, collection).href;
66529
+ if (!missing.has(id))
66530
+ continue;
66531
+ if (isStatus(item.status, 404)) {
66532
+ sync.removed.push(id);
66533
+ missing.delete(id);
66534
+ continue;
66535
+ }
66536
+ const props = successfulProps(item);
66537
+ if (add(id, props[property], props.getetag ?? missing.get(id)))
66538
+ missing.delete(id);
66539
+ }
66540
+ if (missing.size)
66541
+ throw new DavFailure(`${this.errorPrefix}:invalid-response`);
66542
+ }
66543
+ return {
66544
+ resources,
66545
+ removed: sync.removed,
66546
+ syncToken: sync.syncToken,
66547
+ truncated: sync.truncated
66548
+ };
66549
+ }
66550
+ async readMultistatus(response) {
66337
66551
  try {
66338
66552
  const parsed = parseDavXml(await response.text());
66339
66553
  return asArray(parsed.multistatus?.response);
@@ -66634,7 +66848,7 @@ var ActivityStreamSchema = {
66634
66848
  var package_default = {
66635
66849
  name: "@sockethub/schemas",
66636
66850
  description: "JSON schema files for validating Sockethub Activity Streams",
66637
- version: "3.0.0-beta.4",
66851
+ version: "3.0.0-beta.6",
66638
66852
  type: "module",
66639
66853
  private: false,
66640
66854
  author: "Nick Jennings <nick@silverbucket.net>",
@@ -66704,7 +66918,7 @@ var package_default = {
66704
66918
  "ajv-formats": "^2.1.1",
66705
66919
  "ajv-formats-draft2019": "^1.6.1"
66706
66920
  },
66707
- gitHead: "639c296a7332f192a1b3f7e86ef6603b5b67bbaa",
66921
+ gitHead: "604f3da8850a19a4fd5f5e5dcec10bf283e9085e",
66708
66922
  types: "./dist/index.d.ts"
66709
66923
  };
66710
66924
 
@@ -67851,7 +68065,7 @@ function timeZoneRange(input, fallbackYear) {
67851
68065
  const last = years.length ? Math.max(...years) : fallbackYear;
67852
68066
  return [first - 1, Math.max(last + 1, first + 30)];
67853
68067
  }
67854
- function buildICalendar(input, now = new Date) {
68068
+ function buildICalendar(input, now = new Date, { created = false } = {}) {
67855
68069
  const uid = input.uid ?? `${randomUUID()}@sockethub`;
67856
68070
  if (/[\r\n%/\\]/.test(uid))
67857
68071
  throw new Error("invalid uid");
@@ -67870,6 +68084,8 @@ function buildICalendar(input, now = new Date) {
67870
68084
  `BEGIN:${component}`,
67871
68085
  `UID:${escapeText(uid)}`,
67872
68086
  `DTSTAMP:${utcDateTime(now.toISOString(), "now")}`,
68087
+ ...created ? [`CREATED:${utcDateTime(now.toISOString(), "now")}`] : [],
68088
+ `LAST-MODIFIED:${utcDateTime(now.toISOString(), "now")}`,
67873
68089
  `SUMMARY:${escapeText(input.name)}`
67874
68090
  ];
67875
68091
  if (input.content)
@@ -67916,6 +68132,13 @@ function parseDate(value) {
67916
68132
  return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}T${value.slice(9, 11)}:${value.slice(11, 13)}:${value.slice(13, 15)}`;
67917
68133
  return value;
67918
68134
  }
68135
+ function utcTimestamp(value) {
68136
+ if (!value || !/^\d{8}T\d{6}Z$/.test(value))
68137
+ return;
68138
+ const iso = parseDate(value);
68139
+ const parsed = new Date(iso);
68140
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().replace(".000Z", "Z") === iso ? iso : undefined;
68141
+ }
67919
68142
  var STATUSES = {
67920
68143
  event: ["tentative", "confirmed", "cancelled"],
67921
68144
  task: ["needs-action", "in-process", "completed", "cancelled"]
@@ -67991,6 +68214,13 @@ function parseICalendar(body, id, etag) {
67991
68214
  name: unescapeText(summary),
67992
68215
  updateSupported: isUpdateSupported(body)
67993
68216
  };
68217
+ const created = utcTimestamp(first("CREATED")?.value);
68218
+ const hasMethod = lines.slice(0, begin).some((line) => /^METHOD[;:]/i.test(line));
68219
+ const lastModified = utcTimestamp(first("LAST-MODIFIED")?.value) ?? (hasMethod ? undefined : utcTimestamp(first("DTSTAMP")?.value));
68220
+ if (created)
68221
+ item.created = created;
68222
+ if (lastModified)
68223
+ item.lastModified = lastModified;
67994
68224
  if (first("DESCRIPTION"))
67995
68225
  item.content = unescapeText(first("DESCRIPTION")?.value ?? "");
67996
68226
  if (first("LOCATION"))
@@ -68124,7 +68354,15 @@ var queryBody = (query) => `<?xml version="1.0" encoding="utf-8"?>
68124
68354
  <d:prop><d:getetag/><c:calendar-data/></d:prop>
68125
68355
  <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
68356
  </c:calendar-query>`;
68357
+ var HAS_EVENT_OR_TASK = /^BEGIN:(?:VEVENT|VTODO)\r?$/m;
68127
68358
  var CalDavFailure = DavFailure;
68359
+ function calendarItem(data, id, etag) {
68360
+ try {
68361
+ return parseICalendar(data, id, typeof etag === "string" ? etag : undefined);
68362
+ } catch {
68363
+ throw new CalDavFailure("caldav:invalid-response");
68364
+ }
68365
+ }
68128
68366
 
68129
68367
  class CalDavClient extends DavClient {
68130
68368
  constructor(url, authentication, timeoutMs = 15000, networkOptions = {}) {
@@ -68232,28 +68470,28 @@ class CalDavClient extends DavClient {
68232
68470
  await response.body?.cancel().catch(() => {});
68233
68471
  throw new CalDavFailure("caldav:query-failed");
68234
68472
  }
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) {
68473
+ for (const item of await this.readMultistatus(response)) {
68243
68474
  const props = successfulProps(item);
68244
68475
  const itemHref = davHref(item.href);
68245
68476
  const data = props["calendar-data"];
68246
68477
  if (!itemHref || typeof data !== "string")
68247
68478
  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
- }
68479
+ items.push(calendarItem(data, new URL(itemHref, calendar.id).href, props.getetag));
68253
68480
  }
68254
68481
  }
68255
68482
  return items;
68256
68483
  }
68484
+ async sync(calendar, syncToken) {
68485
+ const { resources, ...result } = await this.syncResources(new URL(calendar.id), {
68486
+ namespaces: 'xmlns:c="urn:ietf:params:xml:ns:caldav"',
68487
+ data: "c:calendar-data",
68488
+ multiget: "c:calendar-multiget"
68489
+ }, syncToken);
68490
+ return {
68491
+ ...result,
68492
+ items: resources.filter((resource) => HAS_EVENT_OR_TASK.test(resource.data)).map((resource) => calendarItem(resource.data, resource.id, resource.etag))
68493
+ };
68494
+ }
68257
68495
  async update(id, etag, body) {
68258
68496
  const resource = new URL(id);
68259
68497
  this.assertAllowed(resource);
@@ -68267,7 +68505,8 @@ class CalDavClient extends DavClient {
68267
68505
  return rejectDavResponse(current, "caldav:not-found");
68268
68506
  if (!current.ok)
68269
68507
  return rejectDavResponse(current, "caldav:update-failed");
68270
- if (!isUpdateSupported(await current.text()))
68508
+ const stored = await current.text();
68509
+ if (!isUpdateSupported(stored))
68271
68510
  throw new CalDavFailure("caldav:unsupported-update");
68272
68511
  const response = await this.request(resource, {
68273
68512
  method: "PUT",
@@ -68275,7 +68514,7 @@ class CalDavClient extends DavClient {
68275
68514
  "content-type": "text/calendar; charset=utf-8",
68276
68515
  "if-match": etag
68277
68516
  },
68278
- body
68517
+ body: typeof body === "function" ? body(stored) : body
68279
68518
  });
68280
68519
  if (response.status === 412)
68281
68520
  return rejectDavResponse(response, "caldav:conflict");
@@ -68485,11 +68724,126 @@ END:VCALENDAR\r
68485
68724
  skipped
68486
68725
  };
68487
68726
  }
68727
+
68728
+ // src/patch.ts
68729
+ var GROUPS = [
68730
+ { fields: ["name"], names: ["SUMMARY"] },
68731
+ { fields: ["content"], names: ["DESCRIPTION"] },
68732
+ { fields: ["location"], names: ["LOCATION"] },
68733
+ { fields: ["url"], names: ["URL"] },
68734
+ {
68735
+ fields: ["startTime", "endTime", "due", "allDay", "timeZone"],
68736
+ names: ["DTSTART", "DTEND", "DUE"]
68737
+ },
68738
+ { fields: ["status"], names: ["STATUS"] },
68739
+ { fields: ["completedTime"], names: ["COMPLETED"] },
68740
+ { fields: ["percentComplete"], names: ["PERCENT-COMPLETE"] },
68741
+ { fields: ["sequence"], names: ["SEQUENCE"] },
68742
+ { fields: ["recurrence"], names: ["RRULE"] },
68743
+ { fields: ["organizer"], names: ["ORGANIZER"] },
68744
+ { fields: ["attendees"], names: ["ATTENDEE"] },
68745
+ { fields: ["attachments"], names: ["ATTACH"] },
68746
+ { fields: ["reminders"], names: ["VALARM"] },
68747
+ { fields: [], names: ["DTSTAMP", "LAST-MODIFIED"] }
68748
+ ];
68749
+ function logicalLines(body) {
68750
+ const lines = [];
68751
+ for (const line of body.split(/\r?\n/)) {
68752
+ if (/^[ \t]/.test(line) && lines.length)
68753
+ lines[lines.length - 1].push(line);
68754
+ else
68755
+ lines.push([line]);
68756
+ }
68757
+ while (lines.length && lines[lines.length - 1].join("") === "")
68758
+ lines.pop();
68759
+ return lines;
68760
+ }
68761
+ var unfolded = (raw) => raw.map((line, index) => index ? line.slice(1) : line).join("");
68762
+ var propertyName = (raw) => (unfolded(raw).match(/^[^;:]*/)?.[0] ?? "").toUpperCase();
68763
+ function componentBounds(lines) {
68764
+ const begin = lines.findIndex((raw) => /^BEGIN:(?:VEVENT|VTODO)$/.test(unfolded(raw)));
68765
+ if (begin === -1)
68766
+ return;
68767
+ const name = unfolded(lines[begin]).slice("BEGIN:".length);
68768
+ const end = lines.findIndex((raw, index) => index > begin && unfolded(raw) === `END:${name}`);
68769
+ return end === -1 ? undefined : { begin, end, name };
68770
+ }
68771
+ function units(lines) {
68772
+ const result = [];
68773
+ let alarm;
68774
+ for (const raw of lines) {
68775
+ const text = unfolded(raw);
68776
+ if (alarm) {
68777
+ alarm.push(...raw);
68778
+ if (text === "END:VALARM") {
68779
+ result.push({ name: "VALARM", raw: alarm });
68780
+ alarm = undefined;
68781
+ }
68782
+ } else if (text === "BEGIN:VALARM")
68783
+ alarm = [...raw];
68784
+ else
68785
+ result.push({ name: propertyName(raw), raw });
68786
+ }
68787
+ if (alarm)
68788
+ result.push({ name: "", raw: alarm });
68789
+ return result;
68790
+ }
68791
+ function canonical(value) {
68792
+ return JSON.stringify(value, (_key, entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? Object.fromEntries(Object.keys(entry).sort().map((key) => [key, entry[key]])) : entry);
68793
+ }
68794
+ function sameField(current, next, field) {
68795
+ if (field === "allDay")
68796
+ return current.allDay === true === (next.allDay === true);
68797
+ return canonical(current[field]) === canonical(next[field]);
68798
+ }
68799
+ function patchICalendar(stored, item, generated) {
68800
+ const current = parseICalendar(stored, "");
68801
+ if (current.type !== item.type)
68802
+ return { uid: item.uid ?? current.uid, body: generated };
68803
+ const lines = logicalLines(stored);
68804
+ const bounds = componentBounds(lines);
68805
+ const fresh = logicalLines(generated);
68806
+ const freshBounds = componentBounds(fresh);
68807
+ if (!bounds || !freshBounds)
68808
+ throw new Error("invalid calendar data");
68809
+ const freshUnits = units(fresh.slice(freshBounds.begin + 1, freshBounds.end));
68810
+ const replaced = new Set;
68811
+ for (const group of GROUPS)
68812
+ if (!group.fields.length || !group.fields.every((field) => sameField(current, item, field)))
68813
+ for (const name of group.names)
68814
+ replaced.add(name);
68815
+ const freshNames = new Set(freshUnits.map((unit) => unit.name));
68816
+ if (replaced.has("DTSTART") && (freshNames.has("DTEND") || freshNames.has("DUE")))
68817
+ replaced.add("DURATION");
68818
+ const kept = [];
68819
+ let insertAt;
68820
+ for (const unit of units(lines.slice(bounds.begin + 1, bounds.end))) {
68821
+ if (replaced.has(unit.name))
68822
+ insertAt ??= kept.length;
68823
+ else
68824
+ kept.push(unit);
68825
+ }
68826
+ const added = freshUnits.filter((unit) => replaced.has(unit.name));
68827
+ kept.splice(insertAt ?? kept.length, 0, ...added);
68828
+ const zone = item.timeZone;
68829
+ const freshZone = fresh.findIndex((raw) => unfolded(raw) === "BEGIN:VTIMEZONE");
68830
+ const zones = zone && replaced.has("DTSTART") && freshZone !== -1 && freshZone < freshBounds.begin && !lines.slice(0, bounds.begin).concat(lines.slice(bounds.end + 1)).some((raw) => unfolded(raw) === `TZID:${zone}`) ? fresh.slice(freshZone, freshBounds.begin) : [];
68831
+ const output = [
68832
+ ...lines.slice(0, bounds.begin),
68833
+ ...zones,
68834
+ lines[bounds.begin],
68835
+ ...kept.map((unit) => unit.raw),
68836
+ ...lines.slice(bounds.end)
68837
+ ].flat();
68838
+ return { uid: current.uid, body: `${output.join(`\r
68839
+ `)}\r
68840
+ ` };
68841
+ }
68488
68842
  // package.json
68489
68843
  var package_default2 = {
68490
68844
  name: "@sockethub/platform-caldav",
68491
68845
  description: "A Sockethub platform for synchronizing CalDAV calendars, events, and tasks",
68492
- version: "1.0.0-beta.4",
68846
+ version: "1.0.0-beta.6",
68493
68847
  type: "module",
68494
68848
  private: false,
68495
68849
  license: "LGPL-3.0+",
@@ -68523,16 +68877,16 @@ var package_default2 = {
68523
68877
  test: "bun test src/"
68524
68878
  },
68525
68879
  dependencies: {
68526
- "@sockethub/dav": "^1.0.0-beta.4",
68527
- "@sockethub/schemas": "^3.0.0-beta.4",
68528
- "@sockethub/util": "^1.0.0-beta.4",
68880
+ "@sockethub/dav": "^1.0.0-beta.6",
68881
+ "@sockethub/schemas": "^3.0.0-beta.6",
68882
+ "@sockethub/util": "^1.0.0-beta.6",
68529
68883
  undici: "^7.30.0"
68530
68884
  },
68531
68885
  devDependencies: {
68532
68886
  "@types/bun": "latest"
68533
68887
  },
68534
68888
  peerDependencies: {
68535
- "@sockethub/server": "^5.0.0-beta.4"
68889
+ "@sockethub/server": "^5.0.0-beta.6"
68536
68890
  },
68537
68891
  peerDependenciesMeta: {
68538
68892
  "@sockethub/server": {
@@ -68540,7 +68894,7 @@ var package_default2 = {
68540
68894
  }
68541
68895
  },
68542
68896
  types: "./dist/index.d.ts",
68543
- gitHead: "639c296a7332f192a1b3f7e86ef6603b5b67bbaa"
68897
+ gitHead: "604f3da8850a19a4fd5f5e5dcec10bf283e9085e"
68544
68898
  };
68545
68899
 
68546
68900
  // src/schema.ts
@@ -68670,7 +69024,9 @@ var shared = {
68670
69024
  attendees: { type: "array", maxItems: 1000, items: person },
68671
69025
  reminders: { type: "array", maxItems: 100, items: reminder },
68672
69026
  attachments: { type: "array", maxItems: 100, items: attachment },
68673
- updateSupported: { type: "boolean" }
69027
+ updateSupported: { type: "boolean" },
69028
+ created: { type: "string", format: "date-time" },
69029
+ lastModified: { type: "string", format: "date-time" }
68674
69030
  };
68675
69031
  var eventObject = {
68676
69032
  type: "object",
@@ -68737,7 +69093,14 @@ var queryObject = {
68737
69093
  endTime: { type: "string", format: "date-time" }
68738
69094
  }
68739
69095
  };
68740
- var calendarItem = { oneOf: [eventObject, taskObject] };
69096
+ var syncToken = { type: "string", minLength: 1, maxLength: 4096 };
69097
+ var syncObject = {
69098
+ type: "object",
69099
+ additionalProperties: false,
69100
+ properties: { syncToken, tokenOnly: { const: true } },
69101
+ not: { required: ["syncToken", "tokenOnly"] }
69102
+ };
69103
+ var calendarItem2 = { oneOf: [eventObject, taskObject] };
68741
69104
  var mutationObject = {
68742
69105
  type: "object",
68743
69106
  required: ["id", "type"],
@@ -68760,12 +69123,27 @@ var PlatformCalDavSchema = {
68760
69123
  properties: {
68761
69124
  type: {
68762
69125
  type: "string",
68763
- enum: ["fetch", "read", "query", "create", "update", "delete"]
69126
+ enum: [
69127
+ "fetch",
69128
+ "read",
69129
+ "query",
69130
+ "sync",
69131
+ "create",
69132
+ "update",
69133
+ "delete"
69134
+ ]
68764
69135
  },
68765
69136
  actor,
68766
69137
  target: { oneOf: [target, feedTarget] },
68767
69138
  object: {
68768
- oneOf: [eventObject, taskObject, deleteObject, queryObject]
69139
+ oneOf: [
69140
+ eventObject,
69141
+ taskObject,
69142
+ deleteObject,
69143
+ queryObject,
69144
+ { ...syncObject, required: ["syncToken"] },
69145
+ { ...syncObject, required: ["tokenOnly"] }
69146
+ ]
68769
69147
  }
68770
69148
  }
68771
69149
  },
@@ -68794,11 +69172,19 @@ var PlatformCalDavSchema = {
68794
69172
  },
68795
69173
  required: ["type", "actor", "target"]
68796
69174
  },
69175
+ {
69176
+ properties: {
69177
+ type: { const: "sync" },
69178
+ target,
69179
+ object: syncObject
69180
+ },
69181
+ required: ["type", "actor", "target"]
69182
+ },
68797
69183
  {
68798
69184
  properties: {
68799
69185
  type: { const: "create" },
68800
69186
  target,
68801
- object: calendarItem
69187
+ object: calendarItem2
68802
69188
  },
68803
69189
  required: ["type", "actor", "target", "object"]
68804
69190
  },
@@ -68909,7 +69295,7 @@ var PlatformCalDavSchema = {
68909
69295
  type: "array",
68910
69296
  items: {
68911
69297
  oneOf: [
68912
- calendarItem,
69298
+ calendarItem2,
68913
69299
  {
68914
69300
  type: "object",
68915
69301
  required: [
@@ -68939,6 +69325,30 @@ var PlatformCalDavSchema = {
68939
69325
  }
68940
69326
  }
68941
69327
  },
69328
+ {
69329
+ type: "object",
69330
+ required: [
69331
+ "@context",
69332
+ "type",
69333
+ "totalItems",
69334
+ "items",
69335
+ "removed",
69336
+ "syncToken",
69337
+ "truncated"
69338
+ ],
69339
+ additionalProperties: false,
69340
+ properties: {
69341
+ "@context": { type: "array", items: { type: "string" } },
69342
+ id: { type: ["string", "null"] },
69343
+ type: { const: "collection" },
69344
+ summary: { type: "string" },
69345
+ totalItems: { type: "integer", minimum: 0 },
69346
+ items: { type: "array", items: calendarItem2 },
69347
+ removed: { type: "array", items: shared.id },
69348
+ syncToken,
69349
+ truncated: { type: "boolean" }
69350
+ }
69351
+ },
68942
69352
  {
68943
69353
  type: "object",
68944
69354
  required: ["@context", "type", "actor", "target", "object"],
@@ -68960,39 +69370,8 @@ var PlatformCalDavSchema = {
68960
69370
  var CONTEXT = buildCanonicalContext(PlatformCalDavSchema.contextUrl);
68961
69371
  var MAX_FEED_BYTES = 10 * 1024 * 1024;
68962
69372
  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
69373
  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)
69374
+ if (!isDavCollectionChild(calendarId, resourceId))
68996
69375
  throw new CalDavFailure("caldav:invalid-resource");
68997
69376
  }
68998
69377
 
@@ -69003,7 +69382,14 @@ class CalDav {
69003
69382
  discovery = new DiscoveryCache;
69004
69383
  config = {
69005
69384
  persist: false,
69006
- requireCredentials: ["fetch", "query", "create", "update", "delete"],
69385
+ requireCredentials: [
69386
+ "fetch",
69387
+ "query",
69388
+ "sync",
69389
+ "create",
69390
+ "update",
69391
+ "delete"
69392
+ ],
69007
69393
  connectTimeoutMs: 15000,
69008
69394
  allowPrivateAddresses: false,
69009
69395
  allowInsecureHttp: false,
@@ -69065,7 +69451,9 @@ class CalDav {
69065
69451
  if (!target.components.includes(input.type)) {
69066
69452
  throw new CalDavFailure("caldav:unsupported-component");
69067
69453
  }
69068
- const calendarData = buildICalendar(input);
69454
+ const calendarData = buildICalendar(input, new Date, {
69455
+ created: true
69456
+ });
69069
69457
  const created = await client.create(target, calendarData.uid, calendarData.body);
69070
69458
  done(null, {
69071
69459
  "@context": CONTEXT,
@@ -69093,13 +69481,35 @@ class CalDav {
69093
69481
  items
69094
69482
  })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
69095
69483
  }
69484
+ sync(job, credentials, done) {
69485
+ const client = this.client(credentials);
69486
+ const input = job.object ?? {};
69487
+ this.calendar(client, credentials, job.target?.id ?? "").then((calendar) => input.tokenOnly ? client.currentSyncToken(new URL(calendar.id)).then((syncToken) => ({
69488
+ items: [],
69489
+ removed: [],
69490
+ syncToken,
69491
+ truncated: false
69492
+ })) : client.sync(calendar, input.syncToken)).then((result) => done(null, {
69493
+ "@context": CONTEXT,
69494
+ ...job.id ? { id: job.id } : {},
69495
+ type: "collection",
69496
+ summary: "CalDAV changes",
69497
+ totalItems: result.items.length,
69498
+ ...result
69499
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
69500
+ }
69096
69501
  update(job, credentials, done) {
69097
69502
  const client = this.client(credentials);
69098
69503
  const input = job.object;
69099
69504
  this.calendar(client, credentials, job.target?.id ?? "").then((calendar) => {
69100
69505
  this.assertResource(calendar.id, input.id ?? "");
69101
69506
  const generated = buildICalendar(input);
69102
- return client.update(input.id ?? "", input.etag ?? "", generated.body).then((updated) => ({ updated, uid: generated.uid }));
69507
+ let uid = generated.uid;
69508
+ return client.update(input.id ?? "", input.etag ?? "", (stored) => {
69509
+ const patched = patchICalendar(stored, input, generated.body);
69510
+ uid = patched.uid;
69511
+ return patched.body;
69512
+ }).then((updated) => ({ updated, uid }));
69103
69513
  }).then(({ updated, uid }) => done(null, this.mutationResponse(job, "update", updated.id, input.type, uid, updated.etag))).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
69104
69514
  }
69105
69515
  delete(job, credentials, done) {
@@ -69228,4 +69638,4 @@ export {
69228
69638
  foldLine
69229
69639
  };
69230
69640
 
69231
- //# debugId=911786856EA8A5A164756E2164756E21
69641
+ //# debugId=0BA1D0264BEB87B464756E2164756E21