@sockethub/platform-caldav 1.0.0-beta.4 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -13
- package/dist/dav.d.ts +12 -2
- package/dist/ical.d.ts +8 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +479 -69
- package/dist/index.js.map +9 -8
- package/dist/patch.d.ts +13 -0
- package/dist/schema.d.ts +672 -0
- package/dist/types.d.ts +14 -0
- package/package.json +6 -6
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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
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
|
-
...
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
68527
|
-
"@sockethub/schemas": "^3.0.0-beta.
|
|
68528
|
-
"@sockethub/util": "^1.0.0-beta.
|
|
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.
|
|
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: "
|
|
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
|
|
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: [
|
|
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: [
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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=
|
|
69641
|
+
//# debugId=0BA1D0264BEB87B464756E2164756E21
|