diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50b05eab..94495314 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,10 @@ jobs: if: matrix.node-version >= 22 run: pnpm lint + - name: Build package for entry tests + if: matrix.node-version >= 22 + run: pnpm build + - name: Run `test` if: matrix.node-version >= 22 run: pnpm test @@ -59,6 +63,13 @@ jobs: CREDENTIAL_NEXTCLOUD_PASSWORD: ${{secrets.CREDENTIAL_NEXTCLOUD_PASSWORD}} CREDENTIAL_NEXTCLOUD_SERVER_URL: ${{secrets.CREDENTIAL_NEXTCLOUD_SERVER_URL}} + - name: Check public consumers and examples + if: matrix.node-version >= 22 + run: | + pnpm test:consumer + pnpm test:examples + pnpm exec knip --dependencies --workspace . + - name: Build package for runtime smoke test if: matrix.node-version < 22 run: | @@ -232,6 +243,12 @@ jobs: cache: 'pnpm' cache-dependency-path: docs/pnpm-lock.yaml - run: pnpm install --frozen-lockfile + - name: Install shared-source verification tools + working-directory: . + run: pnpm install --frozen-lockfile + - name: Check docs dependencies + working-directory: . + run: pnpm exec knip --dependencies --workspace docs - run: pnpm test - - run: pnpm exec tsc --noEmit + - run: pnpm typecheck - run: pnpm build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7c5c6ee..f4122f2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,8 +37,11 @@ jobs: - name: check types run: pnpm typecheck - # - name: lint - # run: pnpm lint + - name: lint + run: pnpm lint + + - name: build package for verification + run: pnpm build - name: test run: pnpm test @@ -57,6 +60,12 @@ jobs: CREDENTIAL_NEXTCLOUD_PASSWORD: ${{secrets.CREDENTIAL_NEXTCLOUD_PASSWORD}} CREDENTIAL_NEXTCLOUD_SERVER_URL: ${{secrets.CREDENTIAL_NEXTCLOUD_SERVER_URL}} + - name: check public consumers and examples + run: | + pnpm test:consumer + pnpm test:examples + pnpm exec knip --dependencies --workspace . + # Publish to npm with provenance statements # Requires NPM_TOKEN secret to be configured in repository secrets # or in the Production environment secrets diff --git a/.gitignore b/.gitignore index 52889a96..5e77ce17 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ typings/ .DS_Store .vercel .vscode/ + +/tests/.examples-*/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 100cb875..f093e398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## v2.3.5 + +##### bug fixes + +- resolve relative discovery redirects against the request URL; thanks to [@bensynapse](https://github.com/bensynapse) for [#281](https://github.com/natelindev/tsdav/pull/281) +- preserve redirect origins and explicit ports, and remove request bodies from discovery's GET fallback +- preserve opaque DAV XML values as strings, mixed text/CDATA ordering, namespaces, and property status details while retaining `xml-js` +- preserve complete successful raw responses, including large free/busy payloads +- reject failed or incomplete discovery, object retrieval, and sync responses before calculating deletions or advancing sync state +- include tasks, journals, and extensionless resources in collection synchronization, preserve REPORT sync tokens, and retain unchanged local objects +- refresh expired OAuth credentials before requests through both client APIs, deduplicate concurrent refreshes, and stop DAV requests when refresh fails +- apply header exclusions after merging all request headers and respect CardDAV object URL filters when multi-get is disabled +- require a time range when expanding calendar recurrences, correct client sync return types, and bind multi-get callbacks to the client +- restrict package cleanup to the `dist` directory + +##### improvements + +- export `makeCollection` and the `DAVPropStat` type, and replace repeated sync URL scans with indexed comparisons +- correct browser, synchronization, iCalendar import, authentication, and response documentation; preserve recurrence exceptions, timezones, all-day events, and escaping in the feed-import example +- replace executable input in the docs XML converter with JSON parsing and share the library's XML normalizer +- derive the documentation version from the package and verify executable examples, public consumer types, dependency usage, and fresh package artifacts in CI and release checks + +##### security + +- remove unused direct dependencies and constrain vulnerable documentation transitive dependencies to fixed versions; root and documentation dependency audits report no known vulnerabilities at release verification + ## v2.3.4 ##### bug fixes diff --git a/biome.jsonc b/biome.jsonc index a2fcdf53..cf978dc2 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.12/schema.json", "vcs": { "enabled": false, "clientKind": "git", diff --git a/dist/client.d.ts b/dist/client.d.ts index 7fa57039..a4003f28 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -3,23 +3,16 @@ import { calendarMultiGet as rawCalendarMultiGet, calendarQuery as rawCalendarQu import { collectionQuery as rawCollectionQuery, isCollectionDirty as rawIsCollectionDirty, makeCollection as rawMakeCollection, supportedReportSet as rawSupportedReportSet, syncCollection as rawSyncCollection } from './collection'; import { createObject as rawCreateObject, deleteObject as rawDeleteObject, propfind as rawPropfind, updateObject as rawUpdateObject } from './request'; import { DAVRequest, DAVResponse } from './types/DAVTypes'; -import { SmartCollectionSync, SmartCollectionSyncDetailed, SmartCollectionSyncDetailedResult, SyncCalendars, SyncCalendarsDetailed, SyncCalendarsDetailedResult } from './types/functionsOverloads'; +import { SmartCollectionSyncDetailedResult, SyncCalendars, SyncCalendarsDetailed, SyncCalendarsDetailedResult } from './types/functionsOverloads'; import { DAVAccount, DAVAddressBook, DAVCalendar, DAVCalendarObject, DAVCollection, DAVCredentials, DAVVCard } from './types/models'; import { Optional } from './util/typeHelpers'; -export declare const createDAVClient: (params: { - serverUrl: string; - credentials: DAVCredentials; - authMethod?: 'Basic' | 'Oauth' | 'Digest' | 'Custom' | 'Bearer'; - authFunction?: (credentials: DAVCredentials) => Promise>; - defaultAccountType?: DAVAccount['accountType'] | undefined; - fetchOptions?: RequestInit; - fetch?: typeof globalThis.fetch; -}) => Promise<{ +export declare const createDAVClient: (params: ConstructorParameters[0]) => Promise<{ davRequest: (params0: { url: string; init: DAVRequest; convertIncoming?: boolean; parseOutgoing?: boolean; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof globalThis.fetch; }) => Promise; @@ -32,14 +25,7 @@ export declare const createDAVClient: (params: { fetchOptions?: RequestInit; fetch?: typeof import("./util/fetch").fetch; }) => Promise; - createAccount: (params0: { - account: Optional; - headers?: Record; - loadCollections?: boolean; - loadObjects?: boolean; - fetchOptions?: RequestInit; - fetch?: typeof globalThis.fetch; - }) => Promise; + createAccount: (...args: Parameters) => Promise; createObject: (params: { url: string; data: BodyInit; @@ -163,10 +149,52 @@ export declare const createDAVClient: (params: { fetch?: typeof fetch; }) => Promise<{ isDirty: boolean; - newCtag: string; + newCtag: string | undefined; }>; - smartCollectionSync: SmartCollectionSync; - smartCollectionSyncDetailed: SmartCollectionSyncDetailed; + smartCollectionSync: { + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: DAVAccount; + /** @deprecated Use smartCollectionSyncDetailed instead. */ + detailedResult?: false; + }): Promise; + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: DAVAccount; + /** @deprecated Use smartCollectionSyncDetailed instead. */ + detailedResult: true; + }): Promise>; + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: DAVAccount; + /** @deprecated Use smartCollectionSyncDetailed instead. */ + detailedResult?: boolean; + }): Promise>; + }; + smartCollectionSyncDetailed: (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: DAVAccount; + }) => Promise>; fetchCalendars: (params?: { account?: DAVAccount; props?: import("xml-js").ElementCompact; @@ -198,7 +226,7 @@ export declare const createDAVClient: (params: { useMultiGet?: boolean; fetchOptions?: RequestInit; fetch?: typeof fetch; - }) => Promise; + }) => Promise; createCalendarObject: (params: { calendar: DAVCalendar; iCalString: string; @@ -222,8 +250,23 @@ export declare const createDAVClient: (params: { fetchOptions?: RequestInit; fetch?: typeof fetch; }) => Promise; - syncCalendars: SyncCalendars; - syncCalendarsDetailed: SyncCalendarsDetailed; + syncCalendars: { + (params: Parameters[0] & { + detailedResult: true; + }): Promise; + (params: Parameters[0] & { + detailedResult?: false; + }): Promise; + (params: Parameters[0]): Promise; + }; + syncCalendarsDetailed: (params: { + oldCalendars: DAVCalendar[]; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof fetch; + account?: DAVAccount; + }) => Promise; fetchAddressBooks: (params?: { account?: DAVAccount; props?: import("xml-js").ElementCompact; @@ -231,7 +274,7 @@ export declare const createDAVClient: (params: { headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof fetch; - } | undefined) => Promise; + } | undefined) => Promise; addressBookMultiGet: (params: { url: string; props: import("xml-js").ElementCompact; @@ -251,7 +294,7 @@ export declare const createDAVClient: (params: { headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof fetch; - }) => Promise; + }) => Promise; createVCard: (params: { addressBook: DAVAddressBook; vCardString: string; @@ -295,6 +338,10 @@ export declare class DAVClient { fetchOptions?: RequestInit; fetch?: typeof globalThis.fetch; }); + private authentication?; + private authenticate; + private requestDefaults; + private invoke; login(options?: { loadCollections?: boolean; loadObjects?: boolean; @@ -304,6 +351,7 @@ export declare class DAVClient { init: DAVRequest; convertIncoming?: boolean; parseOutgoing?: boolean; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof globalThis.fetch; }): Promise; @@ -314,6 +362,7 @@ export declare class DAVClient { createAccount(params0: { account: Optional; headers?: Record; + headersToExclude?: string[]; loadCollections?: boolean; loadObjects?: boolean; fetchOptions?: RequestInit; @@ -325,7 +374,7 @@ export declare class DAVClient { supportedReportSet(...params: Parameters): Promise; isCollectionDirty(...params: Parameters): Promise<{ isDirty: boolean; - newCtag: string; + newCtag: string | undefined; }>; smartCollectionSync(param: { collection: T; @@ -379,7 +428,13 @@ export declare class DAVClient { createCalendarObject(...params: Parameters): Promise; updateCalendarObject(...params: Parameters): Promise; deleteCalendarObject(...params: Parameters): Promise; - syncCalendars(...params: Parameters): Promise>; + syncCalendars(params: Parameters[0] & { + detailedResult: true; + }): Promise; + syncCalendars(params: Parameters[0] & { + detailedResult?: false; + }): Promise; + syncCalendars(params: Parameters[0]): Promise; syncCalendarsDetailed(...params: Parameters): Promise; addressBookQuery(...params: Parameters): Promise; addressBookMultiGet(...params: Parameters): Promise; diff --git a/dist/collection.d.ts b/dist/collection.d.ts index 770e384c..aacb9352 100644 --- a/dist/collection.d.ts +++ b/dist/collection.d.ts @@ -37,7 +37,7 @@ export declare const isCollectionDirty: (params: { fetch?: typeof fetch; }) => Promise<{ isDirty: boolean; - newCtag: string; + newCtag: string | undefined; }>; /** * This is for webdav sync-collection only diff --git a/dist/index.d.ts b/dist/index.d.ts index 65f617ce..238da19c 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,13 +1,13 @@ import * as client from './client'; import { DAVNamespace, DAVNamespaceShort, ICALObjects } from './consts'; -export type { DAVDepth, DAVMethods, DAVRequest, DAVResponse, DAVTokens } from './types/DAVTypes'; +export type { DAVDepth, DAVMethods, DAVPropStat, DAVRequest, DAVResponse, DAVTokens, } from './types/DAVTypes'; export type { DAVAccount, DAVAddressBook, DAVCalendar, DAVCalendarObject, DAVCollection, DAVCredentials, DAVObject, DAVVCard, } from './types/models'; export type { SmartCollectionSync, SmartCollectionSyncDetailed, SmartCollectionSyncDetailedResult, SyncCalendars, SyncCalendarsDetailed, SyncCalendarsDetailedResult, } from './types/functionsOverloads'; export { DAVClient } from './client'; export { createDAVClient } from './client'; export { createAccount, serviceDiscovery, fetchPrincipalUrl, fetchHomeUrl, } from './account'; export { davRequest, propfind, createObject, updateObject, deleteObject } from './request'; -export { collectionQuery, supportedReportSet, isCollectionDirty, syncCollection, smartCollectionSync, smartCollectionSyncDetailed, } from './collection'; +export { collectionQuery, makeCollection, supportedReportSet, isCollectionDirty, syncCollection, smartCollectionSync, smartCollectionSyncDetailed, } from './collection'; export { calendarQuery, calendarMultiGet, makeCalendar, fetchCalendars, fetchCalendarUserAddresses, fetchCalendarObjects, createCalendarObject, updateCalendarObject, deleteCalendarObject, syncCalendars, syncCalendarsDetailed, freeBusyQuery, } from './calendar'; export { addressBookQuery, addressBookMultiGet, fetchAddressBooks, fetchVCards, createVCard, updateVCard, deleteVCard, } from './addressBook'; export { getBasicAuthHeaders, getBearerAuthHeaders, getOauthHeaders, fetchOauthTokens, refreshAccessToken, } from './util/authHelpers'; @@ -32,6 +32,7 @@ declare const _default: { init: import(".").DAVRequest; convertIncoming?: boolean; parseOutgoing?: boolean; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof import("./util/fetch").fetch; }) => Promise; @@ -103,7 +104,7 @@ declare const _default: { fetch?: typeof fetch; }) => Promise<{ isDirty: boolean; - newCtag: string; + newCtag: string | undefined; }>; syncCollection: (params: { url: string; @@ -324,20 +325,13 @@ declare const _default: { authorization?: string; }; }>; - createDAVClient: (params: { - serverUrl: string; - credentials: import(".").DAVCredentials; - authMethod?: 'Basic' | 'Oauth' | 'Digest' | 'Custom' | 'Bearer'; - authFunction?: (credentials: import(".").DAVCredentials) => Promise>; - defaultAccountType?: import(".").DAVAccount['accountType'] | undefined; - fetchOptions?: RequestInit; - fetch?: typeof globalThis.fetch; - }) => Promise<{ + createDAVClient: (params: ConstructorParameters[0]) => Promise<{ davRequest: (params0: { url: string; init: import(".").DAVRequest; convertIncoming?: boolean; parseOutgoing?: boolean; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof globalThis.fetch; }) => Promise; @@ -350,14 +344,7 @@ declare const _default: { fetchOptions?: RequestInit; fetch?: typeof import("./util/fetch").fetch; }) => Promise; - createAccount: (params0: { - account: import("./util/typeHelpers").Optional; - headers?: Record; - loadCollections?: boolean; - loadObjects?: boolean; - fetchOptions?: RequestInit; - fetch?: typeof globalThis.fetch; - }) => Promise; + createAccount: (...args: Parameters) => Promise; createObject: (params: { url: string; data: BodyInit; @@ -481,10 +468,49 @@ declare const _default: { fetch?: typeof fetch; }) => Promise<{ isDirty: boolean; - newCtag: string; + newCtag: string | undefined; }>; - smartCollectionSync: import(".").SmartCollectionSync; - smartCollectionSyncDetailed: import(".").SmartCollectionSyncDetailed; + smartCollectionSync: { + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + detailedResult?: false; + }): Promise; + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + detailedResult: true; + }): Promise>; + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + detailedResult?: boolean; + }): Promise>; + }; + smartCollectionSyncDetailed: (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + }) => Promise>; fetchCalendars: (params?: { account?: import(".").DAVAccount; props?: import("xml-js").ElementCompact; @@ -516,7 +542,7 @@ declare const _default: { useMultiGet?: boolean; fetchOptions?: RequestInit; fetch?: typeof fetch; - }) => Promise; + }) => Promise; createCalendarObject: (params: { calendar: import(".").DAVCalendar; iCalString: string; @@ -540,8 +566,23 @@ declare const _default: { fetchOptions?: RequestInit; fetch?: typeof fetch; }) => Promise; - syncCalendars: import(".").SyncCalendars; - syncCalendarsDetailed: import(".").SyncCalendarsDetailed; + syncCalendars: { + (params: Parameters[0] & { + detailedResult: true; + }): Promise; + (params: Parameters[0] & { + detailedResult?: false; + }): Promise; + (params: Parameters[0]): Promise; + }; + syncCalendarsDetailed: (params: { + oldCalendars: import(".").DAVCalendar[]; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof fetch; + account?: import(".").DAVAccount; + }) => Promise; fetchAddressBooks: (params?: { account?: import(".").DAVAccount; props?: import("xml-js").ElementCompact; @@ -549,7 +590,7 @@ declare const _default: { headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof fetch; - } | undefined) => Promise; + } | undefined) => Promise; addressBookMultiGet: (params: { url: string; props: import("xml-js").ElementCompact; @@ -569,7 +610,7 @@ declare const _default: { headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof fetch; - }) => Promise; + }) => Promise; createVCard: (params: { addressBook: import(".").DAVAddressBook; vCardString: string; diff --git a/dist/package.json b/dist/package.json index 3a1ba0cd..6a547b06 100644 --- a/dist/package.json +++ b/dist/package.json @@ -1,6 +1,6 @@ { "name": "tsdav", - "version": "2.3.4", + "version": "2.3.5", "description": "WebDAV, CALDAV, and CARDDAV client for Nodejs and the Browser", "keywords": [ "dav", @@ -61,7 +61,10 @@ "test:unit": "vitest run src/__tests__/unit src/util/__tests__", "test:zoho": "vitest run src/__tests__/integration/zoho --no-file-parallelism", "typecheck": "tsc --noEmit", - "watch": "tsc --watch --outDir ./dist" + "watch": "tsc --watch --outDir ./dist", + "check:dependencies": "knip --dependencies", + "test:consumer": "tsc -p tests/consumer/tsconfig.json --noEmit", + "test:examples": "node scripts/check-doc-examples.cjs && vitest run tests/docs-examples.test.ts" }, "dependencies": { "debug": "4.4.3", @@ -72,10 +75,10 @@ "@types/debug": "4.1.13", "@types/node": "26.4.1", "@vitest/coverage-v8": "5.0.0", - "cross-env": "10.1.0", "dotenv": "17.4.2", + "ical.js": "^2.2.1", + "knip": "^6.39.0", "rolldown": "1.2.7", - "sort-package-json": "4.0.0", "typescript": "7.0.2", "vitest": "^5.0.0" }, diff --git a/dist/request.d.ts b/dist/request.d.ts index 432ed840..885c8ffd 100644 --- a/dist/request.d.ts +++ b/dist/request.d.ts @@ -6,6 +6,7 @@ export declare const davRequest: (params: { init: DAVRequest; convertIncoming?: boolean; parseOutgoing?: boolean; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof fetch; }) => Promise; diff --git a/dist/tsdav.cjs b/dist/tsdav.cjs index ca70e181..18d9009b 100644 --- a/dist/tsdav.cjs +++ b/dist/tsdav.cjs @@ -71,9 +71,6 @@ let ICALObjects = /* @__PURE__ */ function(ICALObjects) { return ICALObjects; }({}); //#endregion -//#region src/util/camelCase.ts -const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); -//#endregion //#region src/util/fetch.ts /** * Resolve the runtime `fetch` implementation. @@ -99,18 +96,175 @@ const resolveFetch = () => { }; const fetch = resolveFetch(); //#endregion -//#region src/util/nativeType.ts -const NUMERIC_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; -const nativeType = (value) => { - if (typeof value !== "string") return value; - if (NUMERIC_RE.test(value)) { - const nValue = Number(value); - if (!Number.isNaN(nValue) && Number.isFinite(nValue) && (!Number.isInteger(nValue) || Number.isSafeInteger(nValue))) return nValue; - } - const bValue = value.toLowerCase(); - if (bValue === "true") return true; - if (bValue === "false") return false; - return value; +//#region src/util/camelCase.ts +const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); +//#endregion +//#region src/util/typeHelpers.ts +function hasFields(obj, fields) { + if (!obj) return false; + const inObj = (object) => object != null && fields.every((f) => object[f]); + if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); + return inObj(obj); +} +const findMissingFieldNames = (obj, fields) => { + if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); + return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +}; +const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key); +//#endregion +//#region src/util/xml.ts +const davNamespaces = new Set(Object.values(DAVNamespace)); +const standardNamespaces = new Map([ + ...[ + "multistatus", + "response", + "propstat", + "prop", + "href", + "status", + "error", + "responsedescription", + "resourcetype", + "collection", + "getetag", + "displayname", + "syncToken", + "supportedReportSet", + "supportedReport", + "report", + "currentUserPrincipal" + ].map((name) => [name, "DAV:"]), + ...[ + "calendarData", + "calendar", + "calendarDescription", + "calendarTimezone", + "calendarHomeSet", + "calendarUserAddressSet", + "supportedCalendarComponentSet", + "comp" + ].map((name) => [name, "urn:ietf:params:xml:ns:caldav"]), + ...[ + "addressData", + "addressbook", + "addressbookHomeSet" + ].map((name) => [name, "urn:ietf:params:xml:ns:carddav"]), + ["getctag", "http://calendarserver.org/ns/"], + ["calendarColor", "http://apple.com/ns/ical/"] +]); +const normalizedKey = (name, namespace) => { + const localName = camelCase(name.replace(/^.*:/, "")); + const standardNamespace = standardNamespaces.get(localName); + return namespace && standardNamespace && namespace !== standardNamespace ? `{${namespace}}${localName}` : localName; +}; +const normalizeElement = (element, inheritedNamespaces) => { + const namespaceContext = Object.assign(Object.create(null), inheritedNamespaces); + const attributes = Object.create(null); + for (const [name, value] of Object.entries(element.attributes ?? {})) { + if (name === "xmlns" || name.startsWith("xmlns:")) namespaceContext[name === "xmlns" ? "" : name.slice(6)] = String(value); + if (name !== "xmlns") attributes[name] = value; + } + const name = element.name ?? ""; + const separator = name.indexOf(":"); + const namespace = namespaceContext[separator === -1 ? "" : name.slice(0, separator)] ?? ""; + const children = element.elements ?? []; + const elements = children.filter((child) => child.type === "element"); + const text = children.filter((child) => child.type === "text" || child.type === "cdata").map((child) => String(child.text ?? child.cdata ?? "")).join(""); + const value = {}; + const namespaces = Object.create(null); + if (Object.keys(attributes).length) value._attributes = attributes; + if (!elements.length) { + const hasText = children.some((child) => child.type === "text"); + const hasCdata = children.some((child) => child.type === "cdata"); + if (hasText) return { + value: text, + namespace, + namespaces + }; + if (hasCdata) value._cdata = text; + return { + value, + namespace, + namespaces + }; + } + if (text.trim()) value._text = text; + for (const child of elements) { + const normalized = normalizeElement(child, namespaceContext); + const localName = camelCase((child.name ?? "").replace(/^.*:/, "")); + let key = normalizedKey(child.name ?? "", normalized.namespace); + const previousNamespace = namespaces[key]; + if (hasOwn(value, key) && previousNamespace !== normalized.namespace) { + if (davNamespaces.has(normalized.namespace) && !davNamespaces.has(previousNamespace)) { + const previousKey = `{${previousNamespace}}${localName}`; + Object.defineProperty(value, previousKey, { + value: value[key], + enumerable: true, + configurable: true, + writable: true + }); + namespaces[previousKey] = previousNamespace; + delete value[key]; + } else key = `{${normalized.namespace}}${localName}`; + } + if (hasOwn(value, key)) { + if (Array.isArray(value[key])) value[key].push(normalized.value); + else value[key] = [value[key], normalized.value]; + } else Object.defineProperty(value, key, { + value: normalized.value, + enumerable: true, + configurable: true, + writable: true + }); + namespaces[key] = normalized.namespace; + if (localName === "prop" && normalized.namespace === "DAV:") value.propNamespaces = normalized.namespaces; + } + return { + value, + namespace, + namespaces + }; +}; +/** Preserve XML text order before normalizing DAV names to the existing compact shape. */ +const parseDAVXML = (xml) => { + const document = xml_js.default.xml2js(xml, { + compact: false, + ignoreDeclaration: true + }); + const result = {}; + for (const element of document.elements ?? []) { + if (element.type !== "element") continue; + const normalized = normalizeElement(element, {}); + const key = normalizedKey(element.name ?? "", normalized.namespace); + Object.defineProperty(result, key, { + value: normalized.value, + enumerable: true + }); + } + return result; +}; +/** Keep same-name properties from distinct namespaces across propstat groups. */ +const mergeDAVProps = (propStats) => { + const groups = /* @__PURE__ */ new Map(); + for (const stat of propStats) { + if (!stat.ok) continue; + for (const [name, value] of Object.entries(stat.props)) { + const namespaces = groups.get(name) ?? /* @__PURE__ */ new Map(); + namespaces.set(stat.namespaces?.[name] ?? "", value); + groups.set(name, namespaces); + } + } + const props = {}; + for (const [name, namespaces] of groups) { + const primary = [...namespaces.keys()].find((namespace) => davNamespaces.has(namespace)) ?? namespaces.keys().next().value; + for (const [namespace, value] of namespaces) Object.defineProperty(props, namespace === primary ? name : `{${namespace}}${name}`, { + value, + enumerable: true, + configurable: true, + writable: true + }); + } + return props; }; //#endregion //#region src/util/requestHelpers.ts @@ -230,7 +384,7 @@ var request_exports = /* @__PURE__ */ __exportAll({ }); const debug$6 = (0, debug.default)("tsdav:request"); const parseStatusLine = (statusLine) => { - const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine ?? ""); + const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine?.trim() ?? ""); const status = match?.groups?.status; const statusText = match?.groups?.statusText; return status ? { @@ -239,7 +393,7 @@ const parseStatusLine = (statusLine) => { } : void 0; }; const davRequest = async (params) => { - const { url, init, convertIncoming = true, parseOutgoing = true, fetchOptions = {}, fetch: fetchOverride } = params; + const { url, init, convertIncoming = true, parseOutgoing = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const requestFetch = fetchOverride ?? fetch; const { headers = {}, body, namespace, method, attributes } = init; let processedBody = body; @@ -272,7 +426,7 @@ const davRequest = async (params) => { }) : body; const fetchOptionsWithoutHeaders = { ...fetchOptions }; delete fetchOptionsWithoutHeaders.headers; - const mergedHeaders = mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers); + const mergedHeaders = excludeHeaders(mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers), headersToExclude); const davResponse = await requestFetch(url, { ...fetchOptionsWithoutHeaders, headers: mergedHeaders, @@ -280,54 +434,25 @@ const davRequest = async (params) => { method }); const resText = await davResponse.text(); - if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) { - const MAX_RAW = 4096; - const raw = resText.length > MAX_RAW ? `${resText.slice(0, MAX_RAW)}…` : resText; - return [{ - href: davResponse.url, - ok: davResponse.ok, - status: davResponse.status, - statusText: davResponse.statusText, - raw - }]; - } + if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) return [{ + href: davResponse.url, + ok: davResponse.ok, + status: davResponse.status, + statusText: davResponse.statusText, + raw: resText + }]; let result; try { - result = xml_js.default.xml2js(resText, { - compact: true, - trim: true, - textFn: (value, parentElement) => { - try { - const parentOfParent = parentElement._parent; - const pOpKeys = Object.keys(parentOfParent); - const keyName = pOpKeys[pOpKeys.length - 1]; - if (!keyName) return; - const arrOfKey = parentOfParent[keyName]; - if (arrOfKey.length > 0) { - const arr = arrOfKey; - const arrIndex = arrOfKey.length - 1; - arr[arrIndex] = nativeType(value); - } else parentOfParent[keyName] = nativeType(value); - } catch (e) { - debug$6(e.stack); - } - }, - elementNameFn: (attributeName) => camelCase(attributeName.replace(/^.+:/, "")), - attributesFn: (value) => { - const newVal = { ...value }; - delete newVal.xmlns; - return newVal; - }, - ignoreDeclaration: true - }); + result = parseDAVXML(resText); } catch (e) { debug$6(`Failed to parse DAV response XML: ${e.message}`); return [{ href: davResponse.url, - ok: davResponse.ok, + ok: false, status: davResponse.status, statusText: davResponse.statusText, - raw: resText + raw: resText, + parseError: e.message }]; } if (!result?.multistatus) return [{ @@ -344,7 +469,21 @@ const davRequest = async (params) => { statusText: davResponse.statusText, ok: davResponse.ok }; - const parsedStatus = parseStatusLine(responseBody.status); + const propStats = (Array.isArray(responseBody.propstat) ? responseBody.propstat : responseBody.propstat ? [responseBody.propstat] : []).map((stat) => { + const parsed = parseStatusLine(stat.status); + const status = parsed?.status ?? 0; + return { + props: stat.prop ?? {}, + namespaces: stat.propNamespaces, + status, + statusText: parsed?.statusText ?? "Invalid DAV property status", + ok: status >= 200 && status < 300, + error: stat.error, + responsedescription: stat.responsedescription + }; + }); + const failedStatus = propStats.length > 0 && propStats.every((stat) => !stat.ok) ? propStats[0] : void 0; + const parsedStatus = parseStatusLine(responseBody.status) ?? failedStatus; const status = parsedStatus?.status ?? davResponse.status; return { raw: result, @@ -354,14 +493,8 @@ const davRequest = async (params) => { ok: status >= 200 && status < 300, error: responseBody.error, responsedescription: responseBody.responsedescription, - props: (Array.isArray(responseBody.propstat) ? responseBody.propstat : [responseBody.propstat]).reduce((prev, curr) => { - const propstatStatus = parseStatusLine(curr?.status)?.status; - if (propstatStatus && (propstatStatus < 200 || propstatStatus >= 300)) return prev; - return { - ...prev, - ...curr?.prop - }; - }, {}) + propStats, + props: mergeDAVProps(propStats) }; }); }; @@ -387,6 +520,7 @@ const propfind = async (params) => { prop: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -430,16 +564,76 @@ const deleteObject = async (params) => { }); }; //#endregion -//#region src/util/typeHelpers.ts -function hasFields(obj, fields) { - if (!obj) return false; - const inObj = (object) => object != null && fields.every((f) => object[f]); - if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); - return inObj(obj); -} -const findMissingFieldNames = (obj, fields) => { - if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); - return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +//#region src/util/syncHelpers.ts +/** Stable keys for linear-time DAV resource comparisons. */ +const getDAVUrlKey = (url, baseUrl) => { + let resolved; + try { + resolved = new URL(url, ensureTrailingSlash(baseUrl)).href; + } catch { + resolved = url.trim(); + } + return resolved.endsWith("/") ? resolved.slice(0, -1) : resolved; +}; +const diffDAVObjects = (local, remote, baseUrl, incremental = false, deletedObjects = []) => { + const localByUrl = new Map(local.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const remoteByUrl = new Map(remote.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const deleted = incremental ? deletedObjects : local.filter((object) => !remoteByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const deletedUrls = new Set(deleted.map((object) => getDAVUrlKey(object.url, baseUrl))); + const created = remote.filter((object) => !localByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const updated = []; + const unchanged = []; + for (const object of local) { + const key = getDAVUrlKey(object.url, baseUrl); + if (deletedUrls.has(key)) continue; + const found = remoteByUrl.get(key); + if (found && found.etag !== object.etag) updated.push(found); + else if (found || incremental) unchanged.push(object); + } + return { + created, + updated, + deleted, + unchanged + }; +}; +//#endregion +//#region src/util/responseHelpers.ts +const assertDAVResponses = (responses, context) => { + const failed = responses.find((response) => !response.ok || response.status >= 400); + if (failed) throw new Error(`${context}: ${failed.status} ${failed.statusText}`); +}; +const assertDAVDiscovery = (responses, context) => { + assertDAVResponses(responses, context); + for (const response of responses) { + if (response.raw?.multistatus && !response.raw.multistatus.response) continue; + assertDAVProperty(response, "resourcetype", context); + if (!response.props || !hasOwn(response.props, "resourcetype") || typeof response.href !== "string" || !response.href) throw new Error(`${context}: missing resourcetype or href in DAV multistatus response`); + } +}; +const assertDAVProperty = (response, name, context) => { + const failed = response.propStats?.find((stat) => !stat.ok && hasOwn(stat.props, name)); + if (failed && !hasOwn(response.props ?? {}, name)) throw new Error(`${context}: ${name} returned ${failed.status} ${failed.statusText}`); +}; +const assertDAVObjectResponses = (responses, property, objectUrls, baseUrl, context) => { + assertDAVResponses(responses, context); + const fetchedUrls = /* @__PURE__ */ new Set(); + for (const response of responses) { + assertDAVProperty(response, property, context); + if (!response.href || typeof (response.props?.[property]?._cdata ?? response.props?.[property]) !== "string") throw new Error(`${context}: missing ${property} or href`); + fetchedUrls.add(getDAVUrlKey(response.href, baseUrl)); + } + const withoutQuery = (url) => getDAVUrlKey(url.replace(/\?.*$/, ""), baseUrl); + const countsByPath = /* @__PURE__ */ new Map(); + for (const url of objectUrls) { + const key = withoutQuery(url); + countsByPath.set(key, (countsByPath.get(key) ?? 0) + 1); + } + if (objectUrls.some((url) => !fetchedUrls.has(getDAVUrlKey(url, baseUrl)) && !(countsByPath.get(withoutQuery(url)) === 1 && fetchedUrls.has(withoutQuery(url))))) throw new Error(`${context}: incomplete response`); +}; +const getDAVText = (value) => { + const text = typeof value === "string" || typeof value === "number" ? String(value) : value?._cdata ?? value?._text; + return typeof text === "string" ? text : void 0; }; //#endregion //#region src/collection.ts @@ -460,13 +654,6 @@ const resolveDAVHref = (href, baseUrl) => { return href; } }; -const hrefHasExtension = (href, extension, baseUrl) => { - try { - return new URL(href, ensureTrailingSlash(baseUrl)).pathname.toLowerCase().endsWith(extension); - } catch { - return (href.split(/[?#]/, 1)[0] ?? "").toLowerCase().endsWith(extension); - } -}; const collectionQuery = async (params) => { const { url, body, depth, defaultNamespace = "d", headers, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const queryResults = await davRequest({ @@ -480,13 +667,16 @@ const collectionQuery = async (params) => { namespace: defaultNamespace, body }, + headersToExclude, fetchOptions, fetch: fetchOverride }); const emptyNotFound = queryResults[0]; if (defaultNamespace === "c" && body?.["calendar-query"] != null && queryResults.length === 1 && emptyNotFound && emptyNotFound.status === 404 && urlMatches(url, emptyNotFound.href, url) && !emptyNotFound.error && Object.keys(emptyNotFound.props ?? {}).length === 0 && typeof emptyNotFound.raw === "object" && emptyNotFound.raw !== null && emptyNotFound.raw.multistatus?.response?.propstat == null) return []; const errorResponse = queryResults.find((res) => !res.ok || res.status && res.status >= 400); - if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${errorResponse.raw ? `Raw response: ${errorResponse.raw}` : ""}`); + if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${typeof errorResponse.raw === "string" ? `Raw response: ${errorResponse.raw.slice(0, 4096)}` : ""}`); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => !response.raw?.multistatus)) throw new Error("Collection query failed: expected a DAV multistatus response"); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection query failed: missing href in DAV response"); const firstQueryResult = queryResults[0]; if (queryResults.length === 1 && firstQueryResult && (!firstQueryResult.raw || firstQueryResult.raw.multistatus && !firstQueryResult.raw.multistatus.response) && firstQueryResult.status && firstQueryResult.status < 300) return []; return queryResults; @@ -513,6 +703,7 @@ const makeCollection = async (params) => { set: { prop: props } } } : void 0 }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -524,6 +715,7 @@ const supportedReportSet = async (params) => { props: { [`d:supported-report-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]?.props?.supportedReportSet?.supportedReport; @@ -537,15 +729,17 @@ const isCollectionDirty = async (params) => { props: { [`cs:getctag`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(collection.url, r.href, collection.url)); if (!res) throw new Error("Collection does not exist on server"); - if (!res.ok) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); - const remoteCtag = res.props?.getctag; + const unavailableCtag = res.propStats?.length && res.propStats.every((stat) => stat.status === 404 && hasOwn(stat.props, "getctag")); + if (!res.ok && !unavailableCtag) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); + const remoteCtag = getDAVText(res.props?.getctag); return { isDirty: collection.ctag == null || remoteCtag == null || `${collection.ctag}` !== `${remoteCtag}`, - newCtag: remoteCtag?.toString() + newCtag: remoteCtag }; }; /** @@ -570,6 +764,7 @@ const syncCollection = (params) => { [`d:prop`]: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -595,15 +790,17 @@ const smartCollectionSync = async (params) => { syncLevel: 1, syncToken: collection.syncToken, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); const isObjectResponse = (r) => { - const extName = account.accountType === "caldav" ? ".ics" : ".vcf"; - return typeof r.href === "string" && hrefHasExtension(r.href, extName, collection.url); + return typeof r.href === "string" && getDAVUrlKey(r.href, collection.url) !== getDAVUrlKey(collection.url, collection.url) && !r.props?.resourcetype?.collection; }; - const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && isObjectResponse(r))); + const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && !r.propStats?.length && isObjectResponse(r))); if (errorResponse) throw new Error(`Collection sync failed: ${errorResponse.status} ${errorResponse.statusText}`); + if (result.some((response) => response.raw && !response.raw.multistatus)) throw new Error("Collection sync failed: expected a DAV multistatus response"); + if (result.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection sync failed: missing href in DAV response"); const objectResponses = result.filter(isObjectResponse); const changedObjectUrls = objectResponses.filter((o) => o.status !== 404).map((r) => r.href); const deletedObjectUrls = objectResponses.filter((o) => o.status === 404).map((r) => r.href); @@ -618,30 +815,24 @@ const smartCollectionSync = async (params) => { objectUrls: changedObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? [] : []; - const multiGetError = multiGetObjectResponse.find((r) => !r.ok || r.status >= 400); - if (multiGetError) throw new Error(`Collection sync multi-get failed: ${multiGetError.status} ${multiGetError.statusText}`); + assertDAVObjectResponses(multiGetObjectResponse, account.accountType === "caldav" ? "calendarData" : "addressData", changedObjectUrls, collection.url, "Collection sync multi-get failed"); const remoteObjects = multiGetObjectResponse.map((res) => { return { url: resolveDAVHref(res.href ?? "", collection.url), - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: account?.accountType === "caldav" ? res.props?.calendarData?._cdata ?? res.props?.calendarData : res.props?.addressData?._cdata ?? res.props?.addressData }; }); const localObjects = collection.objects ?? []; - const created = remoteObjects.filter((o) => localObjects.every((lo) => !urlMatches(lo.url, o.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = deletedObjectUrls.map((o) => ({ - url: resolveDAVHref(o, collection.url), + const deletedObjects = deletedObjectUrls.map((url) => ({ + url: resolveDAVHref(url, collection.url), etag: "" })); - const unchanged = localObjects.filter((localObject) => deleted.every((deletedObject) => !urlMatches(localObject.url, deletedObject.url, collection.url)) && updated.every((updatedObject) => !urlMatches(localObject.url, updatedObject.url, collection.url))); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url, true, deletedObjects); return { ...collection, objects: detailedResult ? { @@ -653,13 +844,14 @@ const smartCollectionSync = async (params) => { ...created, ...updated ], - syncToken: result[0]?.raw?.multistatus?.syncToken ?? collection.syncToken + syncToken: getDAVText(result[0]?.raw?.multistatus?.syncToken) ?? collection.syncToken }; } if (syncMethod === "basic") { const { isDirty, newCtag } = await isCollectionDirty({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -676,17 +868,11 @@ const smartCollectionSync = async (params) => { const remoteObjects = await collection.fetchObjects({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? []; - const created = remoteObjects.filter((ro) => localObjects.every((lo) => !urlMatches(lo.url, ro.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = localObjects.filter((cal) => remoteObjects.every((ro) => !urlMatches(ro.url, cal.url, collection.url))); - const unchanged = localObjects.filter((lo) => remoteObjects.some((ro) => urlMatches(lo.url, ro.url, collection.url) && ro.etag === lo.etag)); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url); return { ...collection, objects: detailedResult ? { @@ -738,6 +924,7 @@ const addressBookQuery = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -754,6 +941,7 @@ const addressBookMultiGet = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -767,33 +955,39 @@ const fetchAddressBooks = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`d:displayname`]: {}, - [`cs:getctag`]: {}, - [`d:resourcetype`]: {}, - [`d:sync-token`]: {} + props: { + ...customProps ?? { + [`d:displayname`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`d:sync-token`]: {} + }, + [`d:resourcetype`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Address book discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("addressbook")).map((rs) => { const displayName = rs.props?.displayname?._cdata ?? rs.props?.displayname; debug$4(`Found address book named ${typeof displayName === "string" ? displayName : ""}, props: ${JSON.stringify(rs.props)}`); return { url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), displayName: typeof displayName === "string" ? displayName : "", resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken + syncToken: getDAVText(rs.props?.syncToken) }; }).map(async (addr) => ({ ...addr, reports: await supportedReportSet({ collection: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -812,12 +1006,14 @@ const fetchVCards = async (params) => { props: { [`d:getetag`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).map((res) => res.href ?? "")).filter((url) => typeof url === "string" && url.trim().length > 0).map((url) => url.startsWith("http") ? url : new URL(url, ensureTrailingSlash(addressBook.url)).href).filter((url) => !urlEquals(url, addressBook.url)).filter(urlFilter).map((url) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(vcardUrls.map((url) => getDAVUrlKey(url, addressBook.url))); let vCardResults = []; if (vcardUrls.length > 0) { if (useMultiGet) vCardResults = await addressBookMultiGet({ @@ -829,24 +1025,30 @@ const fetchVCards = async (params) => { objectUrls: vcardUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - else vCardResults = await addressBookQuery({ - url: addressBook.url, - props: { - [`d:getetag`]: {}, - [`card:address-data`]: {} - }, - depth: "1", - headers: excludeHeaders(headers, headersToExclude), - fetchOptions, - fetch: fetchOverride - }); + else { + vCardResults = await addressBookQuery({ + url: addressBook.url, + props: { + [`d:getetag`]: {}, + [`card:address-data`]: {} + }, + depth: "1", + headers: excludeHeaders(headers, headersToExclude), + headersToExclude, + fetchOptions, + fetch: fetchOverride + }); + vCardResults = vCardResults.filter((res) => !!res.href && targetUrls.has(getDAVUrlKey(res.href, addressBook.url))); + } } + assertDAVObjectResponses(vCardResults, "addressData", vcardUrls, addressBook.url, "VCard fetch failed"); return vCardResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(addressBook.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.addressData?._cdata ?? res.props?.addressData })); }; @@ -860,6 +1062,7 @@ const createVCard = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -874,6 +1077,7 @@ const updateVCard = async (params) => { "content-type": "text/vcard; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -884,6 +1088,7 @@ const deleteVCard = async (params) => { url: vCard.url, etag: vCard.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -933,6 +1138,7 @@ const fetchCalendarUserAddresses = async (params) => { props: { [`c:calendar-user-address-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(account.principalUrl, r.href, account.rootUrl)); @@ -963,6 +1169,7 @@ const calendarQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -981,6 +1188,7 @@ const calendarMultiGet = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1005,6 +1213,7 @@ const makeCalendar = async (params) => { set: { prop: props } } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1018,21 +1227,27 @@ const fetchCalendars = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`c:calendar-description`]: {}, - [`c:calendar-timezone`]: {}, - [`d:displayname`]: {}, - [`ca:calendar-color`]: {}, - [`cs:getctag`]: {}, + props: { + ...customProps ?? { + [`c:calendar-description`]: {}, + [`c:calendar-timezone`]: {}, + [`d:displayname`]: {}, + [`ca:calendar-color`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`c:supported-calendar-component-set`]: {}, + [`d:sync-token`]: {} + }, [`d:resourcetype`]: {}, - [`c:supported-calendar-component-set`]: {}, - [`d:sync-token`]: {} + [`c:supported-calendar-component-set`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Calendar discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("calendar")).filter((rc) => { const components = extractComponentNames(rc.props?.supportedCalendarComponentSet?.comp); return components.length === 0 || components.some((c) => Object.values(ICALObjects).includes(c)); @@ -1045,12 +1260,12 @@ const fetchCalendars = async (params) => { description: typeof description === "string" ? description : "", timezone: typeof timezone === "string" ? timezone : "", url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), calendarColor: rs.props?.calendarColor, - displayName: rs.props?.displayname?._cdata ?? rs.props?.displayname, + displayName: getDAVText(rs.props?.displayname), components: extractComponentNames(compSet), resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken, + syncToken: getDAVText(rs.props?.syncToken), ...projectedProps && projectedEntries.length > 0 ? { projectedProps: Object.fromEntries(projectedEntries) } : {} }; }).map(async (cal) => ({ @@ -1058,6 +1273,7 @@ const fetchCalendars = async (params) => { reports: await supportedReportSet({ collection: cal, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1065,6 +1281,7 @@ const fetchCalendars = async (params) => { }; const fetchCalendarObjects = async (params) => { const { calendar, objectUrls, filters: customFilters, timeRange, headers, expand, urlFilter = (url) => Boolean(url?.includes(".ics")), useMultiGet = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; + if (expand && !timeRange) throw new Error("timeRange is required when expand is true"); if (timeRange) validateTimeRange(timeRange); debug$3(`Fetching calendar objects from ${calendar?.url}`); const requiredFields = ["url"]; @@ -1095,6 +1312,7 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1102,6 +1320,7 @@ const fetchCalendarObjects = async (params) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(calendarObjectUrls.map((url) => getDAVUrlKey(url, calendar.url))); let calendarObjectResults = []; if (calendarObjectUrls.length > 0) { if (expand && !objectUrls) calendarObjectResults = initialResponses.filter((res) => { @@ -1121,12 +1340,13 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - if (objectUrls && objectUrls.length > 0) calendarObjectResults = calendarObjectResults.filter((res) => { + calendarObjectResults = calendarObjectResults.filter((res) => { const fullUrl = (res.href ?? "").startsWith("http") ? res.href ?? "" : new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href; - return calendarObjectUrls.some((target) => urlMatches(fullUrl, target, calendar.url)); + return targetUrls.has(getDAVUrlKey(fullUrl, calendar.url)); }); } else calendarObjectResults = await calendarMultiGet({ url: calendar.url, @@ -1140,13 +1360,15 @@ const fetchCalendarObjects = async (params) => { objectUrls: calendarObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); } + assertDAVObjectResponses(calendarObjectResults, "calendarData", calendarObjectUrls, calendar.url, "Calendar object fetch failed"); return calendarObjectResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.calendarData?._cdata ?? res.props?.calendarData })); }; @@ -1160,6 +1382,7 @@ const createCalendarObject = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1174,6 +1397,7 @@ const updateCalendarObject = async (params) => { "content-type": "text/calendar; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1184,6 +1408,7 @@ const deleteCalendarObject = async (params) => { url: calendarObject.url, etag: calendarObject.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1198,19 +1423,26 @@ const syncCalendars = async (params) => { const remoteCalendars = await fetchCalendars({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - const created = remoteCalendars.filter((rc) => localCalendars.every((lc) => !urlMatches(lc.url, rc.url, account.rootUrl))); - debug$3(`new calendars: ${created.map((cc) => cc.displayName)}`); - const updated = localCalendars.reduce((prev, curr) => { - const found = remoteCalendars.find((rc) => urlMatches(rc.url, curr.url, account.rootUrl)); - if (found && (found.syncToken && `${found.syncToken}` !== `${curr.syncToken}` || found.ctag && `${found.ctag}` !== `${curr.ctag}`)) return [...prev, { - local: curr, - remote: found - }]; - return prev; - }, []); + const baseUrl = account.rootUrl ?? account.homeUrl ?? account.serverUrl; + const localByUrl = new Map(localCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const remoteByUrl = new Map(remoteCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const created = remoteCalendars.filter((cal) => !localByUrl.has(getDAVUrlKey(cal.url, baseUrl))); + const updated = []; + const unchanged = []; + const deleted = []; + for (const local of localCalendars) { + const remote = remoteByUrl.get(getDAVUrlKey(local.url, baseUrl)); + if (!remote) deleted.push(local); + else if (!remote.syncToken && !remote.ctag || remote.syncToken && remote.syncToken !== local.syncToken || remote.ctag && remote.ctag !== local.ctag) updated.push({ + local, + remote + }); + else unchanged.push(local); + } debug$3(`updated calendars: ${updated.map(({ remote }) => remote.displayName)}`); const updatedWithObjects = await Promise.all(updated.map(async ({ local, remote }) => { const fetchObjects = async (fetchParams) => { @@ -1218,7 +1450,9 @@ const syncCalendars = async (params) => { const { collection, ...requestParams } = fetchParams; return fetchCalendarObjects({ ...requestParams, - calendar: collection + calendar: collection, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => getDAVUrlKey(url, collection.url) !== getDAVUrlKey(collection.url, collection.url) }); }; const collection = { @@ -1233,25 +1467,17 @@ const syncCalendars = async (params) => { collection, detailedResult: false, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account, fetchOptions, fetch: fetchOverride }); return { ...result, - ctag: remote.ctag ?? result.ctag, - syncToken: remote.syncToken ?? result.syncToken + ctag: remote.reports?.includes("syncCollection") ? remote.ctag ?? result.ctag : result.ctag ?? remote.ctag, + syncToken: remote.reports?.includes("syncCollection") ? result.syncToken : remote.syncToken ?? result.syncToken }; })); - const deleted = localCalendars.filter((cal) => remoteCalendars.every((rc) => !urlMatches(rc.url, cal.url, account.rootUrl))); - debug$3(`deleted calendars: ${deleted.map((cc) => cc.displayName)}`); - const unchanged = localCalendars.filter((cal) => remoteCalendars.some((rc) => { - if (!urlMatches(rc.url, cal.url, account.rootUrl)) return false; - const syncTokenMatches = !rc.syncToken || `${rc.syncToken}` === `${cal.syncToken}`; - const ctagMatches = !rc.ctag || `${rc.ctag}` === `${cal.ctag}`; - return syncTokenMatches && ctagMatches; - })); - debug$3(`unchanged calendars: ${unchanged.map((cc) => cc.displayName)}`); return detailedResult ? { created, updated: updatedWithObjects, @@ -1282,6 +1508,7 @@ const freeBusyQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]; @@ -1318,11 +1545,7 @@ const serviceDiscovery = async (params) => { const location = response.headers.get("Location"); if (typeof location === "string" && location.length) { debug$2(`Service discovery redirected to ${location}`); - const hasExplicitScheme = /^[a-z][a-z0-9+.-]*:/i.test(location); - const serviceURL = new URL(location, endpoint); - if (serviceURL.hostname === uri.hostname && uri.port && !serviceURL.port) serviceURL.port = uri.port; - if (!hasExplicitScheme) serviceURL.protocol = endpoint.protocol ?? "http"; - return serviceURL.href; + return new URL(location, uri).href; } } }; @@ -1347,6 +1570,7 @@ const serviceDiscovery = async (params) => { const redirectUrl = extractRedirect(await requestFetch(uri.href, { ...fetchOptionsWithoutHeaders, method: "GET", + body: void 0, headers: excludeHeaders(mergeHeaders(headers, fetchHeaders), headersToExclude), redirect: "manual" })); @@ -1386,6 +1610,7 @@ const fetchPrincipalUrl = async (params) => { props: { [`d:current-user-principal`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1412,6 +1637,7 @@ const fetchHomeUrl = async (params) => { props: account.accountType === "caldav" ? { [`c:calendar-home-set`]: {} } : { [`card:addressbook-home-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1435,6 +1661,7 @@ const createAccount = async (params) => { const discoveredRootUrl = account.rootUrl ?? await serviceDiscovery({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1453,6 +1680,7 @@ const createAccount = async (params) => { rootUrl }, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1468,24 +1696,28 @@ const createAccount = async (params) => { newAccount.principalUrl = account.principalUrl ?? newAccount.principalUrl ?? await fetchPrincipalUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); newAccount.homeUrl = account.homeUrl ?? await fetchHomeUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); if (loadCollections || loadObjects) { if (account.accountType === "caldav") newAccount.calendars = await fetchCalendars({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride }); else if (account.accountType === "carddav") newAccount.addressBooks = await fetchAddressBooks({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride @@ -1496,7 +1728,10 @@ const createAccount = async (params) => { ...cal, objects: await fetchCalendarObjects({ calendar: cal, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => !urlEquals(url, cal.url), headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1506,6 +1741,7 @@ const createAccount = async (params) => { objects: await fetchVCards({ addressBook: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1656,20 +1892,18 @@ const getOauthHeaders = async (credentials, fetchOptions, fetchOverride) => { debug$1("Fetching oauth headers"); let tokens = {}; let didRefresh = false; - if (!credentials.refreshToken) { - tokens = await fetchOauthTokens(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else if (credentials.refreshToken && !credentials.accessToken || Date.now() > (credentials.expiration ?? 0)) { - tokens = await refreshAccessToken(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else tokens = { + if (credentials.accessToken && (credentials.expiration == null && !credentials.refreshToken || credentials.expiration != null && Date.now() < credentials.expiration)) tokens = { access_token: credentials.accessToken, refresh_token: credentials.refreshToken }; + else { + tokens = credentials.refreshToken ? await refreshAccessToken(credentials, fetchOptions, fetchOverride) : await fetchOauthTokens(credentials, fetchOptions, fetchOverride); + didRefresh = true; + } if (didRefresh) { if (tokens.access_token) credentials.accessToken = tokens.access_token; if (tokens.refresh_token) credentials.refreshToken = tokens.refresh_token; - if (typeof tokens.expires_in === "number") credentials.expiration = Date.now() + tokens.expires_in * 1e3; + if (tokens.access_token) credentials.expiration = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0; } debug$1("Oauth tokens obtained"); return { @@ -1683,145 +1917,71 @@ var client_exports = /* @__PURE__ */ __exportAll({ DAVClient: () => DAVClient, createDAVClient: () => createDAVClient }); -const createDAVClient = async (params) => { - const { serverUrl, credentials, authMethod = "Basic", defaultAccountType, authFunction, fetchOptions: defaultFetchOptions, fetch: fetchOverride } = params; - let authHeaders = {}; - switch (authMethod) { - case "Basic": - authHeaders = getBasicAuthHeaders(credentials); - break; - case "Bearer": - authHeaders = getBearerAuthHeaders(credentials); - break; - case "Oauth": - authHeaders = (await getOauthHeaders(credentials, defaultFetchOptions, fetchOverride)).headers; - break; - case "Digest": - authHeaders = { Authorization: `Digest ${credentials.digestString}` }; - break; +const resolveAuthHeaders = async (client, fetchOptions = client.fetchOptions, fetchOverride = client.fetchOverride) => { + switch (client.authMethod) { + case "Basic": return getBasicAuthHeaders(client.credentials); + case "Bearer": return getBearerAuthHeaders(client.credentials); + case "Oauth": { + const { headers } = await getOauthHeaders(client.credentials, fetchOptions, fetchOverride); + if (!headers.authorization) throw new Error("OAuth authentication failed: token endpoint returned no access token"); + return headers; + } + case "Digest": return { Authorization: `Digest ${client.credentials.digestString}` }; case "Custom": - if (!authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - authHeaders = await authFunction(credentials) ?? {}; - break; + if (!client.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); + return await client.authFunction(client.credentials) ?? {}; default: throw new Error("Invalid auth method"); } - const defaultAccount = defaultAccountType ? await createAccount({ +}; +const createDAVClient = async (params) => { + const client = new DAVClient(params); + client.authHeaders = await resolveAuthHeaders(client); + client.account = params.defaultAccountType ? await createAccount({ account: { - serverUrl, - credentials, - accountType: defaultAccountType + serverUrl: params.serverUrl, + credentials: params.credentials, + accountType: params.defaultAccountType }, - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride + headers: client.authHeaders, + fetchOptions: client.fetchOptions, + fetch: client.fetchOverride }) : void 0; - const davRequest$1 = async (params0) => { - const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; - const { headers, ...restInit } = init; - return davRequest({ - ...rest, - init: { - ...restInit, - headers: { - ...authHeaders, - ...headers - } - }, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const commonDefaults = { - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride - }; - const commonDefaultsWithUrl = { - url: serverUrl, - ...commonDefaults - }; - const commonDefaultsWithAccount = { - account: defaultAccount, - ...commonDefaults - }; - const createObject$1 = defaultParam(createObject, commonDefaultsWithUrl); - const updateObject$1 = defaultParam(updateObject, commonDefaultsWithUrl); - const deleteObject$1 = defaultParam(deleteObject, commonDefaultsWithUrl); - const propfind$1 = defaultParam(propfind, commonDefaults); - const createAccount$1 = async (params0) => { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch: fetchOverride2 } = params0; - const merged = { - serverUrl, - credentials, - ...account - }; - if (!merged.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client."); - return createAccount({ - account: merged, - headers: { - ...authHeaders, - ...headers - }, - loadCollections, - loadObjects, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const collectionQuery$1 = defaultParam(collectionQuery, commonDefaults); - const makeCollection$1 = defaultParam(makeCollection, commonDefaults); - const syncCollection$1 = defaultParam(syncCollection, commonDefaults); - const supportedReportSet$1 = defaultParam(supportedReportSet, commonDefaults); - const isCollectionDirty$1 = defaultParam(isCollectionDirty, commonDefaults); - const smartCollectionSync$1 = defaultParam(smartCollectionSync, commonDefaultsWithAccount); - const smartCollectionSyncDetailed$1 = defaultParam(smartCollectionSyncDetailed, commonDefaultsWithAccount); - const calendarQuery$1 = defaultParam(calendarQuery, commonDefaults); - const calendarMultiGet$1 = defaultParam(calendarMultiGet, commonDefaults); - const makeCalendar$1 = defaultParam(makeCalendar, commonDefaults); - const fetchCalendars$1 = defaultParam(fetchCalendars, commonDefaultsWithAccount); - const fetchCalendarUserAddresses$1 = defaultParam(fetchCalendarUserAddresses, commonDefaultsWithAccount); - const fetchCalendarObjects$1 = defaultParam(fetchCalendarObjects, commonDefaults); - const createCalendarObject$1 = defaultParam(createCalendarObject, commonDefaults); - const updateCalendarObject$1 = defaultParam(updateCalendarObject, commonDefaults); - const deleteCalendarObject$1 = defaultParam(deleteCalendarObject, commonDefaults); - const syncCalendars$1 = defaultParam(syncCalendars, commonDefaultsWithAccount); - const syncCalendarsDetailed$1 = defaultParam(syncCalendarsDetailed, commonDefaultsWithAccount); - const freeBusyQuery$1 = defaultParam(freeBusyQuery, commonDefaults); - const addressBookQuery$1 = defaultParam(addressBookQuery, commonDefaults); - const addressBookMultiGet$1 = defaultParam(addressBookMultiGet, commonDefaults); return { - davRequest: davRequest$1, - propfind: propfind$1, - createAccount: createAccount$1, - createObject: createObject$1, - updateObject: updateObject$1, - deleteObject: deleteObject$1, - calendarQuery: calendarQuery$1, - addressBookQuery: addressBookQuery$1, - collectionQuery: collectionQuery$1, - makeCollection: makeCollection$1, - calendarMultiGet: calendarMultiGet$1, - makeCalendar: makeCalendar$1, - freeBusyQuery: freeBusyQuery$1, - syncCollection: syncCollection$1, - supportedReportSet: supportedReportSet$1, - isCollectionDirty: isCollectionDirty$1, - smartCollectionSync: smartCollectionSync$1, - smartCollectionSyncDetailed: smartCollectionSyncDetailed$1, - fetchCalendars: fetchCalendars$1, - fetchCalendarUserAddresses: fetchCalendarUserAddresses$1, - fetchCalendarObjects: fetchCalendarObjects$1, - createCalendarObject: createCalendarObject$1, - updateCalendarObject: updateCalendarObject$1, - deleteCalendarObject: deleteCalendarObject$1, - syncCalendars: syncCalendars$1, - syncCalendarsDetailed: syncCalendarsDetailed$1, - fetchAddressBooks: defaultParam(fetchAddressBooks, commonDefaultsWithAccount), - addressBookMultiGet: addressBookMultiGet$1, - fetchVCards: defaultParam(fetchVCards, commonDefaults), - createVCard: defaultParam(createVCard, commonDefaults), - updateVCard: defaultParam(updateVCard, commonDefaults), - deleteVCard: defaultParam(deleteVCard, commonDefaults) + davRequest: client.davRequest.bind(client), + propfind: client.propfind.bind(client), + createAccount: async (...args) => { + if (!args[0].account.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType`."); + return client.createAccount(...args); + }, + createObject: client.createObject.bind(client), + updateObject: client.updateObject.bind(client), + deleteObject: client.deleteObject.bind(client), + calendarQuery: client.calendarQuery.bind(client), + addressBookQuery: client.addressBookQuery.bind(client), + collectionQuery: client.collectionQuery.bind(client), + makeCollection: client.makeCollection.bind(client), + calendarMultiGet: client.calendarMultiGet.bind(client), + makeCalendar: client.makeCalendar.bind(client), + freeBusyQuery: client.freeBusyQuery.bind(client), + syncCollection: client.syncCollection.bind(client), + supportedReportSet: client.supportedReportSet.bind(client), + isCollectionDirty: client.isCollectionDirty.bind(client), + smartCollectionSync: client.smartCollectionSync.bind(client), + smartCollectionSyncDetailed: client.smartCollectionSyncDetailed.bind(client), + fetchCalendars: client.fetchCalendars.bind(client), + fetchCalendarUserAddresses: client.fetchCalendarUserAddresses.bind(client), + fetchCalendarObjects: client.fetchCalendarObjects.bind(client), + createCalendarObject: client.createCalendarObject.bind(client), + updateCalendarObject: client.updateCalendarObject.bind(client), + deleteCalendarObject: client.deleteCalendarObject.bind(client), + syncCalendars: client.syncCalendars.bind(client), + syncCalendarsDetailed: client.syncCalendarsDetailed.bind(client), + fetchAddressBooks: client.fetchAddressBooks.bind(client), + addressBookMultiGet: client.addressBookMultiGet.bind(client), + fetchVCards: client.fetchVCards.bind(client), + createVCard: client.createVCard.bind(client), + updateVCard: client.updateVCard.bind(client), + deleteVCard: client.deleteVCard.bind(client) }; }; var DAVClient = class { @@ -1833,27 +1993,42 @@ var DAVClient = class { this.authFunction = params.authFunction; this.fetchOptions = params.fetchOptions ?? {}; this.fetchOverride = params.fetch; + this.calendarMultiGet = this.calendarMultiGet.bind(this); + this.addressBookMultiGet = this.addressBookMultiGet.bind(this); + } + async authenticate(force = false, fetchOptions = this.fetchOptions, fetchOverride = this.fetchOverride) { + if (!force && this.authMethod !== "Oauth") return; + if (!force && this.authHeaders && this.credentials.accessToken && (this.credentials.expiration == null || Date.now() < this.credentials.expiration)) { + this.authHeaders = { authorization: `Bearer ${this.credentials.accessToken}` }; + return; + } + if (this.authentication) return this.authentication; + const authenticate = async () => { + this.authHeaders = await resolveAuthHeaders(this, fetchOptions, fetchOverride); + }; + this.authentication = authenticate(); + try { + await this.authentication; + } finally { + this.authentication = void 0; + } + } + async requestDefaults(params) { + await this.authenticate(false, params?.fetchOptions ?? this.fetchOptions, params?.fetch ?? this.fetchOverride); + return { + url: this.serverUrl, + headers: this.authHeaders, + account: this.account, + fetchOptions: this.fetchOptions, + fetch: this.fetchOverride + }; + } + async invoke(fn, params) { + const defaults = await this.requestDefaults(params); + return await defaultParam(fn, defaults)(...[params]); } async login(options) { - switch (this.authMethod) { - case "Basic": - this.authHeaders = getBasicAuthHeaders(this.credentials); - break; - case "Bearer": - this.authHeaders = getBearerAuthHeaders(this.credentials); - break; - case "Oauth": - this.authHeaders = (await getOauthHeaders(this.credentials, this.fetchOptions, this.fetchOverride)).headers; - break; - case "Digest": - this.authHeaders = { Authorization: `Digest ${this.credentials.digestString}` }; - break; - case "Custom": - if (!this.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - this.authHeaders = await this.authFunction(this.credentials); - break; - default: throw new Error("Invalid auth method"); - } + await this.authenticate(true); this.account = this.accountType ? await createAccount({ account: { serverUrl: this.serverUrl, @@ -1870,52 +2045,32 @@ var DAVClient = class { async davRequest(params0) { const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; const { headers, ...restInit } = init; + const defaults = await this.requestDefaults(params0); return davRequest({ ...rest, init: { ...restInit, - headers: { - ...this.authHeaders, - ...headers - } + headers: mergeHeaders(defaults.headers, headers) }, fetchOptions: fetchOptions ?? this.fetchOptions, fetch: fetchOverride2 ?? this.fetchOverride }); } async createObject(...params) { - return defaultParam(createObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createObject, params[0]); } async updateObject(...params) { - return defaultParam(updateObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateObject, params[0]); } async deleteObject(...params) { - return defaultParam(deleteObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteObject, params[0]); } async propfind(...params) { - return defaultParam(propfind, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(propfind, params[0]); } async createAccount(params0) { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const { account, headers, headersToExclude, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const defaults = await this.requestDefaults(params0); const accountType = account.accountType ?? this.accountType; if (!accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient."); return createAccount({ @@ -1925,10 +2080,8 @@ var DAVClient = class { ...account, accountType }, - headers: { - ...this.authHeaders, - ...headers - }, + headers: mergeHeaders(defaults.headers, headers), + headersToExclude, loadCollections, loadObjects, fetchOptions: fetchOptions ?? this.fetchOptions, @@ -1936,193 +2089,82 @@ var DAVClient = class { }); } async collectionQuery(...params) { - return defaultParam(collectionQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(collectionQuery, params[0]); } async makeCollection(...params) { - return defaultParam(makeCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCollection, params[0]); } async syncCollection(...params) { - return defaultParam(syncCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCollection, params[0]); } async supportedReportSet(...params) { - return defaultParam(supportedReportSet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(supportedReportSet, params[0]); } async isCollectionDirty(...params) { - return defaultParam(isCollectionDirty, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(isCollectionDirty, params[0]); } async smartCollectionSync(...params) { - return defaultParam(smartCollectionSync, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(params[0]); + return this.invoke(smartCollectionSync, params[0]); } async smartCollectionSyncDetailed(param) { - return defaultParam(smartCollectionSyncDetailed, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(param); + return this.invoke(smartCollectionSyncDetailed, param); } async calendarQuery(...params) { - return defaultParam(calendarQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarQuery, params[0]); } async makeCalendar(...params) { - return defaultParam(makeCalendar, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCalendar, params[0]); } async freeBusyQuery(...params) { - return defaultParam(freeBusyQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(freeBusyQuery, params[0]); } async calendarMultiGet(...params) { - return defaultParam(calendarMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarMultiGet, params[0]); } async fetchCalendars(...params) { - return defaultParam(fetchCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendars, params[0]); } async fetchCalendarUserAddresses(...params) { - return defaultParam(fetchCalendarUserAddresses, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendarUserAddresses, params[0]); } async fetchCalendarObjects(...params) { - return defaultParam(fetchCalendarObjects, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchCalendarObjects, params[0]); } async createCalendarObject(...params) { - return defaultParam(createCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createCalendarObject, params[0]); } async updateCalendarObject(...params) { - return defaultParam(updateCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateCalendarObject, params[0]); } async deleteCalendarObject(...params) { - return defaultParam(deleteCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteCalendarObject, params[0]); } async syncCalendars(...params) { - return defaultParam(syncCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendars, params[0]); } async syncCalendarsDetailed(...params) { - return defaultParam(syncCalendarsDetailed, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendarsDetailed, params[0]); } async addressBookQuery(...params) { - return defaultParam(addressBookQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookQuery, params[0]); } async addressBookMultiGet(...params) { - return defaultParam(addressBookMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookMultiGet, params[0]); } async fetchAddressBooks(...params) { - return defaultParam(fetchAddressBooks, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchAddressBooks, params[0]); } async fetchVCards(...params) { - return defaultParam(fetchVCards, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchVCards, params[0]); } async createVCard(...params) { - return defaultParam(createVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createVCard, params[0]); } async updateVCard(...params) { - return defaultParam(updateVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateVCard, params[0]); } async deleteVCard(...params) { - return defaultParam(deleteVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteVCard, params[0]); } }; //#endregion @@ -2180,6 +2222,7 @@ exports.getDAVAttribute = getDAVAttribute; exports.getOauthHeaders = getOauthHeaders; exports.isCollectionDirty = isCollectionDirty; exports.makeCalendar = makeCalendar; +exports.makeCollection = makeCollection; exports.mergeHeaders = mergeHeaders; exports.propfind = propfind; exports.refreshAccessToken = refreshAccessToken; diff --git a/dist/tsdav.cjs.js b/dist/tsdav.cjs.js index ca70e181..18d9009b 100644 --- a/dist/tsdav.cjs.js +++ b/dist/tsdav.cjs.js @@ -71,9 +71,6 @@ let ICALObjects = /* @__PURE__ */ function(ICALObjects) { return ICALObjects; }({}); //#endregion -//#region src/util/camelCase.ts -const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); -//#endregion //#region src/util/fetch.ts /** * Resolve the runtime `fetch` implementation. @@ -99,18 +96,175 @@ const resolveFetch = () => { }; const fetch = resolveFetch(); //#endregion -//#region src/util/nativeType.ts -const NUMERIC_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; -const nativeType = (value) => { - if (typeof value !== "string") return value; - if (NUMERIC_RE.test(value)) { - const nValue = Number(value); - if (!Number.isNaN(nValue) && Number.isFinite(nValue) && (!Number.isInteger(nValue) || Number.isSafeInteger(nValue))) return nValue; - } - const bValue = value.toLowerCase(); - if (bValue === "true") return true; - if (bValue === "false") return false; - return value; +//#region src/util/camelCase.ts +const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); +//#endregion +//#region src/util/typeHelpers.ts +function hasFields(obj, fields) { + if (!obj) return false; + const inObj = (object) => object != null && fields.every((f) => object[f]); + if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); + return inObj(obj); +} +const findMissingFieldNames = (obj, fields) => { + if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); + return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +}; +const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key); +//#endregion +//#region src/util/xml.ts +const davNamespaces = new Set(Object.values(DAVNamespace)); +const standardNamespaces = new Map([ + ...[ + "multistatus", + "response", + "propstat", + "prop", + "href", + "status", + "error", + "responsedescription", + "resourcetype", + "collection", + "getetag", + "displayname", + "syncToken", + "supportedReportSet", + "supportedReport", + "report", + "currentUserPrincipal" + ].map((name) => [name, "DAV:"]), + ...[ + "calendarData", + "calendar", + "calendarDescription", + "calendarTimezone", + "calendarHomeSet", + "calendarUserAddressSet", + "supportedCalendarComponentSet", + "comp" + ].map((name) => [name, "urn:ietf:params:xml:ns:caldav"]), + ...[ + "addressData", + "addressbook", + "addressbookHomeSet" + ].map((name) => [name, "urn:ietf:params:xml:ns:carddav"]), + ["getctag", "http://calendarserver.org/ns/"], + ["calendarColor", "http://apple.com/ns/ical/"] +]); +const normalizedKey = (name, namespace) => { + const localName = camelCase(name.replace(/^.*:/, "")); + const standardNamespace = standardNamespaces.get(localName); + return namespace && standardNamespace && namespace !== standardNamespace ? `{${namespace}}${localName}` : localName; +}; +const normalizeElement = (element, inheritedNamespaces) => { + const namespaceContext = Object.assign(Object.create(null), inheritedNamespaces); + const attributes = Object.create(null); + for (const [name, value] of Object.entries(element.attributes ?? {})) { + if (name === "xmlns" || name.startsWith("xmlns:")) namespaceContext[name === "xmlns" ? "" : name.slice(6)] = String(value); + if (name !== "xmlns") attributes[name] = value; + } + const name = element.name ?? ""; + const separator = name.indexOf(":"); + const namespace = namespaceContext[separator === -1 ? "" : name.slice(0, separator)] ?? ""; + const children = element.elements ?? []; + const elements = children.filter((child) => child.type === "element"); + const text = children.filter((child) => child.type === "text" || child.type === "cdata").map((child) => String(child.text ?? child.cdata ?? "")).join(""); + const value = {}; + const namespaces = Object.create(null); + if (Object.keys(attributes).length) value._attributes = attributes; + if (!elements.length) { + const hasText = children.some((child) => child.type === "text"); + const hasCdata = children.some((child) => child.type === "cdata"); + if (hasText) return { + value: text, + namespace, + namespaces + }; + if (hasCdata) value._cdata = text; + return { + value, + namespace, + namespaces + }; + } + if (text.trim()) value._text = text; + for (const child of elements) { + const normalized = normalizeElement(child, namespaceContext); + const localName = camelCase((child.name ?? "").replace(/^.*:/, "")); + let key = normalizedKey(child.name ?? "", normalized.namespace); + const previousNamespace = namespaces[key]; + if (hasOwn(value, key) && previousNamespace !== normalized.namespace) { + if (davNamespaces.has(normalized.namespace) && !davNamespaces.has(previousNamespace)) { + const previousKey = `{${previousNamespace}}${localName}`; + Object.defineProperty(value, previousKey, { + value: value[key], + enumerable: true, + configurable: true, + writable: true + }); + namespaces[previousKey] = previousNamespace; + delete value[key]; + } else key = `{${normalized.namespace}}${localName}`; + } + if (hasOwn(value, key)) { + if (Array.isArray(value[key])) value[key].push(normalized.value); + else value[key] = [value[key], normalized.value]; + } else Object.defineProperty(value, key, { + value: normalized.value, + enumerable: true, + configurable: true, + writable: true + }); + namespaces[key] = normalized.namespace; + if (localName === "prop" && normalized.namespace === "DAV:") value.propNamespaces = normalized.namespaces; + } + return { + value, + namespace, + namespaces + }; +}; +/** Preserve XML text order before normalizing DAV names to the existing compact shape. */ +const parseDAVXML = (xml) => { + const document = xml_js.default.xml2js(xml, { + compact: false, + ignoreDeclaration: true + }); + const result = {}; + for (const element of document.elements ?? []) { + if (element.type !== "element") continue; + const normalized = normalizeElement(element, {}); + const key = normalizedKey(element.name ?? "", normalized.namespace); + Object.defineProperty(result, key, { + value: normalized.value, + enumerable: true + }); + } + return result; +}; +/** Keep same-name properties from distinct namespaces across propstat groups. */ +const mergeDAVProps = (propStats) => { + const groups = /* @__PURE__ */ new Map(); + for (const stat of propStats) { + if (!stat.ok) continue; + for (const [name, value] of Object.entries(stat.props)) { + const namespaces = groups.get(name) ?? /* @__PURE__ */ new Map(); + namespaces.set(stat.namespaces?.[name] ?? "", value); + groups.set(name, namespaces); + } + } + const props = {}; + for (const [name, namespaces] of groups) { + const primary = [...namespaces.keys()].find((namespace) => davNamespaces.has(namespace)) ?? namespaces.keys().next().value; + for (const [namespace, value] of namespaces) Object.defineProperty(props, namespace === primary ? name : `{${namespace}}${name}`, { + value, + enumerable: true, + configurable: true, + writable: true + }); + } + return props; }; //#endregion //#region src/util/requestHelpers.ts @@ -230,7 +384,7 @@ var request_exports = /* @__PURE__ */ __exportAll({ }); const debug$6 = (0, debug.default)("tsdav:request"); const parseStatusLine = (statusLine) => { - const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine ?? ""); + const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine?.trim() ?? ""); const status = match?.groups?.status; const statusText = match?.groups?.statusText; return status ? { @@ -239,7 +393,7 @@ const parseStatusLine = (statusLine) => { } : void 0; }; const davRequest = async (params) => { - const { url, init, convertIncoming = true, parseOutgoing = true, fetchOptions = {}, fetch: fetchOverride } = params; + const { url, init, convertIncoming = true, parseOutgoing = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const requestFetch = fetchOverride ?? fetch; const { headers = {}, body, namespace, method, attributes } = init; let processedBody = body; @@ -272,7 +426,7 @@ const davRequest = async (params) => { }) : body; const fetchOptionsWithoutHeaders = { ...fetchOptions }; delete fetchOptionsWithoutHeaders.headers; - const mergedHeaders = mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers); + const mergedHeaders = excludeHeaders(mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers), headersToExclude); const davResponse = await requestFetch(url, { ...fetchOptionsWithoutHeaders, headers: mergedHeaders, @@ -280,54 +434,25 @@ const davRequest = async (params) => { method }); const resText = await davResponse.text(); - if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) { - const MAX_RAW = 4096; - const raw = resText.length > MAX_RAW ? `${resText.slice(0, MAX_RAW)}…` : resText; - return [{ - href: davResponse.url, - ok: davResponse.ok, - status: davResponse.status, - statusText: davResponse.statusText, - raw - }]; - } + if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) return [{ + href: davResponse.url, + ok: davResponse.ok, + status: davResponse.status, + statusText: davResponse.statusText, + raw: resText + }]; let result; try { - result = xml_js.default.xml2js(resText, { - compact: true, - trim: true, - textFn: (value, parentElement) => { - try { - const parentOfParent = parentElement._parent; - const pOpKeys = Object.keys(parentOfParent); - const keyName = pOpKeys[pOpKeys.length - 1]; - if (!keyName) return; - const arrOfKey = parentOfParent[keyName]; - if (arrOfKey.length > 0) { - const arr = arrOfKey; - const arrIndex = arrOfKey.length - 1; - arr[arrIndex] = nativeType(value); - } else parentOfParent[keyName] = nativeType(value); - } catch (e) { - debug$6(e.stack); - } - }, - elementNameFn: (attributeName) => camelCase(attributeName.replace(/^.+:/, "")), - attributesFn: (value) => { - const newVal = { ...value }; - delete newVal.xmlns; - return newVal; - }, - ignoreDeclaration: true - }); + result = parseDAVXML(resText); } catch (e) { debug$6(`Failed to parse DAV response XML: ${e.message}`); return [{ href: davResponse.url, - ok: davResponse.ok, + ok: false, status: davResponse.status, statusText: davResponse.statusText, - raw: resText + raw: resText, + parseError: e.message }]; } if (!result?.multistatus) return [{ @@ -344,7 +469,21 @@ const davRequest = async (params) => { statusText: davResponse.statusText, ok: davResponse.ok }; - const parsedStatus = parseStatusLine(responseBody.status); + const propStats = (Array.isArray(responseBody.propstat) ? responseBody.propstat : responseBody.propstat ? [responseBody.propstat] : []).map((stat) => { + const parsed = parseStatusLine(stat.status); + const status = parsed?.status ?? 0; + return { + props: stat.prop ?? {}, + namespaces: stat.propNamespaces, + status, + statusText: parsed?.statusText ?? "Invalid DAV property status", + ok: status >= 200 && status < 300, + error: stat.error, + responsedescription: stat.responsedescription + }; + }); + const failedStatus = propStats.length > 0 && propStats.every((stat) => !stat.ok) ? propStats[0] : void 0; + const parsedStatus = parseStatusLine(responseBody.status) ?? failedStatus; const status = parsedStatus?.status ?? davResponse.status; return { raw: result, @@ -354,14 +493,8 @@ const davRequest = async (params) => { ok: status >= 200 && status < 300, error: responseBody.error, responsedescription: responseBody.responsedescription, - props: (Array.isArray(responseBody.propstat) ? responseBody.propstat : [responseBody.propstat]).reduce((prev, curr) => { - const propstatStatus = parseStatusLine(curr?.status)?.status; - if (propstatStatus && (propstatStatus < 200 || propstatStatus >= 300)) return prev; - return { - ...prev, - ...curr?.prop - }; - }, {}) + propStats, + props: mergeDAVProps(propStats) }; }); }; @@ -387,6 +520,7 @@ const propfind = async (params) => { prop: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -430,16 +564,76 @@ const deleteObject = async (params) => { }); }; //#endregion -//#region src/util/typeHelpers.ts -function hasFields(obj, fields) { - if (!obj) return false; - const inObj = (object) => object != null && fields.every((f) => object[f]); - if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); - return inObj(obj); -} -const findMissingFieldNames = (obj, fields) => { - if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); - return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +//#region src/util/syncHelpers.ts +/** Stable keys for linear-time DAV resource comparisons. */ +const getDAVUrlKey = (url, baseUrl) => { + let resolved; + try { + resolved = new URL(url, ensureTrailingSlash(baseUrl)).href; + } catch { + resolved = url.trim(); + } + return resolved.endsWith("/") ? resolved.slice(0, -1) : resolved; +}; +const diffDAVObjects = (local, remote, baseUrl, incremental = false, deletedObjects = []) => { + const localByUrl = new Map(local.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const remoteByUrl = new Map(remote.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const deleted = incremental ? deletedObjects : local.filter((object) => !remoteByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const deletedUrls = new Set(deleted.map((object) => getDAVUrlKey(object.url, baseUrl))); + const created = remote.filter((object) => !localByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const updated = []; + const unchanged = []; + for (const object of local) { + const key = getDAVUrlKey(object.url, baseUrl); + if (deletedUrls.has(key)) continue; + const found = remoteByUrl.get(key); + if (found && found.etag !== object.etag) updated.push(found); + else if (found || incremental) unchanged.push(object); + } + return { + created, + updated, + deleted, + unchanged + }; +}; +//#endregion +//#region src/util/responseHelpers.ts +const assertDAVResponses = (responses, context) => { + const failed = responses.find((response) => !response.ok || response.status >= 400); + if (failed) throw new Error(`${context}: ${failed.status} ${failed.statusText}`); +}; +const assertDAVDiscovery = (responses, context) => { + assertDAVResponses(responses, context); + for (const response of responses) { + if (response.raw?.multistatus && !response.raw.multistatus.response) continue; + assertDAVProperty(response, "resourcetype", context); + if (!response.props || !hasOwn(response.props, "resourcetype") || typeof response.href !== "string" || !response.href) throw new Error(`${context}: missing resourcetype or href in DAV multistatus response`); + } +}; +const assertDAVProperty = (response, name, context) => { + const failed = response.propStats?.find((stat) => !stat.ok && hasOwn(stat.props, name)); + if (failed && !hasOwn(response.props ?? {}, name)) throw new Error(`${context}: ${name} returned ${failed.status} ${failed.statusText}`); +}; +const assertDAVObjectResponses = (responses, property, objectUrls, baseUrl, context) => { + assertDAVResponses(responses, context); + const fetchedUrls = /* @__PURE__ */ new Set(); + for (const response of responses) { + assertDAVProperty(response, property, context); + if (!response.href || typeof (response.props?.[property]?._cdata ?? response.props?.[property]) !== "string") throw new Error(`${context}: missing ${property} or href`); + fetchedUrls.add(getDAVUrlKey(response.href, baseUrl)); + } + const withoutQuery = (url) => getDAVUrlKey(url.replace(/\?.*$/, ""), baseUrl); + const countsByPath = /* @__PURE__ */ new Map(); + for (const url of objectUrls) { + const key = withoutQuery(url); + countsByPath.set(key, (countsByPath.get(key) ?? 0) + 1); + } + if (objectUrls.some((url) => !fetchedUrls.has(getDAVUrlKey(url, baseUrl)) && !(countsByPath.get(withoutQuery(url)) === 1 && fetchedUrls.has(withoutQuery(url))))) throw new Error(`${context}: incomplete response`); +}; +const getDAVText = (value) => { + const text = typeof value === "string" || typeof value === "number" ? String(value) : value?._cdata ?? value?._text; + return typeof text === "string" ? text : void 0; }; //#endregion //#region src/collection.ts @@ -460,13 +654,6 @@ const resolveDAVHref = (href, baseUrl) => { return href; } }; -const hrefHasExtension = (href, extension, baseUrl) => { - try { - return new URL(href, ensureTrailingSlash(baseUrl)).pathname.toLowerCase().endsWith(extension); - } catch { - return (href.split(/[?#]/, 1)[0] ?? "").toLowerCase().endsWith(extension); - } -}; const collectionQuery = async (params) => { const { url, body, depth, defaultNamespace = "d", headers, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const queryResults = await davRequest({ @@ -480,13 +667,16 @@ const collectionQuery = async (params) => { namespace: defaultNamespace, body }, + headersToExclude, fetchOptions, fetch: fetchOverride }); const emptyNotFound = queryResults[0]; if (defaultNamespace === "c" && body?.["calendar-query"] != null && queryResults.length === 1 && emptyNotFound && emptyNotFound.status === 404 && urlMatches(url, emptyNotFound.href, url) && !emptyNotFound.error && Object.keys(emptyNotFound.props ?? {}).length === 0 && typeof emptyNotFound.raw === "object" && emptyNotFound.raw !== null && emptyNotFound.raw.multistatus?.response?.propstat == null) return []; const errorResponse = queryResults.find((res) => !res.ok || res.status && res.status >= 400); - if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${errorResponse.raw ? `Raw response: ${errorResponse.raw}` : ""}`); + if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${typeof errorResponse.raw === "string" ? `Raw response: ${errorResponse.raw.slice(0, 4096)}` : ""}`); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => !response.raw?.multistatus)) throw new Error("Collection query failed: expected a DAV multistatus response"); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection query failed: missing href in DAV response"); const firstQueryResult = queryResults[0]; if (queryResults.length === 1 && firstQueryResult && (!firstQueryResult.raw || firstQueryResult.raw.multistatus && !firstQueryResult.raw.multistatus.response) && firstQueryResult.status && firstQueryResult.status < 300) return []; return queryResults; @@ -513,6 +703,7 @@ const makeCollection = async (params) => { set: { prop: props } } } : void 0 }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -524,6 +715,7 @@ const supportedReportSet = async (params) => { props: { [`d:supported-report-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]?.props?.supportedReportSet?.supportedReport; @@ -537,15 +729,17 @@ const isCollectionDirty = async (params) => { props: { [`cs:getctag`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(collection.url, r.href, collection.url)); if (!res) throw new Error("Collection does not exist on server"); - if (!res.ok) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); - const remoteCtag = res.props?.getctag; + const unavailableCtag = res.propStats?.length && res.propStats.every((stat) => stat.status === 404 && hasOwn(stat.props, "getctag")); + if (!res.ok && !unavailableCtag) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); + const remoteCtag = getDAVText(res.props?.getctag); return { isDirty: collection.ctag == null || remoteCtag == null || `${collection.ctag}` !== `${remoteCtag}`, - newCtag: remoteCtag?.toString() + newCtag: remoteCtag }; }; /** @@ -570,6 +764,7 @@ const syncCollection = (params) => { [`d:prop`]: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -595,15 +790,17 @@ const smartCollectionSync = async (params) => { syncLevel: 1, syncToken: collection.syncToken, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); const isObjectResponse = (r) => { - const extName = account.accountType === "caldav" ? ".ics" : ".vcf"; - return typeof r.href === "string" && hrefHasExtension(r.href, extName, collection.url); + return typeof r.href === "string" && getDAVUrlKey(r.href, collection.url) !== getDAVUrlKey(collection.url, collection.url) && !r.props?.resourcetype?.collection; }; - const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && isObjectResponse(r))); + const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && !r.propStats?.length && isObjectResponse(r))); if (errorResponse) throw new Error(`Collection sync failed: ${errorResponse.status} ${errorResponse.statusText}`); + if (result.some((response) => response.raw && !response.raw.multistatus)) throw new Error("Collection sync failed: expected a DAV multistatus response"); + if (result.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection sync failed: missing href in DAV response"); const objectResponses = result.filter(isObjectResponse); const changedObjectUrls = objectResponses.filter((o) => o.status !== 404).map((r) => r.href); const deletedObjectUrls = objectResponses.filter((o) => o.status === 404).map((r) => r.href); @@ -618,30 +815,24 @@ const smartCollectionSync = async (params) => { objectUrls: changedObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? [] : []; - const multiGetError = multiGetObjectResponse.find((r) => !r.ok || r.status >= 400); - if (multiGetError) throw new Error(`Collection sync multi-get failed: ${multiGetError.status} ${multiGetError.statusText}`); + assertDAVObjectResponses(multiGetObjectResponse, account.accountType === "caldav" ? "calendarData" : "addressData", changedObjectUrls, collection.url, "Collection sync multi-get failed"); const remoteObjects = multiGetObjectResponse.map((res) => { return { url: resolveDAVHref(res.href ?? "", collection.url), - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: account?.accountType === "caldav" ? res.props?.calendarData?._cdata ?? res.props?.calendarData : res.props?.addressData?._cdata ?? res.props?.addressData }; }); const localObjects = collection.objects ?? []; - const created = remoteObjects.filter((o) => localObjects.every((lo) => !urlMatches(lo.url, o.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = deletedObjectUrls.map((o) => ({ - url: resolveDAVHref(o, collection.url), + const deletedObjects = deletedObjectUrls.map((url) => ({ + url: resolveDAVHref(url, collection.url), etag: "" })); - const unchanged = localObjects.filter((localObject) => deleted.every((deletedObject) => !urlMatches(localObject.url, deletedObject.url, collection.url)) && updated.every((updatedObject) => !urlMatches(localObject.url, updatedObject.url, collection.url))); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url, true, deletedObjects); return { ...collection, objects: detailedResult ? { @@ -653,13 +844,14 @@ const smartCollectionSync = async (params) => { ...created, ...updated ], - syncToken: result[0]?.raw?.multistatus?.syncToken ?? collection.syncToken + syncToken: getDAVText(result[0]?.raw?.multistatus?.syncToken) ?? collection.syncToken }; } if (syncMethod === "basic") { const { isDirty, newCtag } = await isCollectionDirty({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -676,17 +868,11 @@ const smartCollectionSync = async (params) => { const remoteObjects = await collection.fetchObjects({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? []; - const created = remoteObjects.filter((ro) => localObjects.every((lo) => !urlMatches(lo.url, ro.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = localObjects.filter((cal) => remoteObjects.every((ro) => !urlMatches(ro.url, cal.url, collection.url))); - const unchanged = localObjects.filter((lo) => remoteObjects.some((ro) => urlMatches(lo.url, ro.url, collection.url) && ro.etag === lo.etag)); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url); return { ...collection, objects: detailedResult ? { @@ -738,6 +924,7 @@ const addressBookQuery = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -754,6 +941,7 @@ const addressBookMultiGet = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -767,33 +955,39 @@ const fetchAddressBooks = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`d:displayname`]: {}, - [`cs:getctag`]: {}, - [`d:resourcetype`]: {}, - [`d:sync-token`]: {} + props: { + ...customProps ?? { + [`d:displayname`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`d:sync-token`]: {} + }, + [`d:resourcetype`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Address book discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("addressbook")).map((rs) => { const displayName = rs.props?.displayname?._cdata ?? rs.props?.displayname; debug$4(`Found address book named ${typeof displayName === "string" ? displayName : ""}, props: ${JSON.stringify(rs.props)}`); return { url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), displayName: typeof displayName === "string" ? displayName : "", resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken + syncToken: getDAVText(rs.props?.syncToken) }; }).map(async (addr) => ({ ...addr, reports: await supportedReportSet({ collection: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -812,12 +1006,14 @@ const fetchVCards = async (params) => { props: { [`d:getetag`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).map((res) => res.href ?? "")).filter((url) => typeof url === "string" && url.trim().length > 0).map((url) => url.startsWith("http") ? url : new URL(url, ensureTrailingSlash(addressBook.url)).href).filter((url) => !urlEquals(url, addressBook.url)).filter(urlFilter).map((url) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(vcardUrls.map((url) => getDAVUrlKey(url, addressBook.url))); let vCardResults = []; if (vcardUrls.length > 0) { if (useMultiGet) vCardResults = await addressBookMultiGet({ @@ -829,24 +1025,30 @@ const fetchVCards = async (params) => { objectUrls: vcardUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - else vCardResults = await addressBookQuery({ - url: addressBook.url, - props: { - [`d:getetag`]: {}, - [`card:address-data`]: {} - }, - depth: "1", - headers: excludeHeaders(headers, headersToExclude), - fetchOptions, - fetch: fetchOverride - }); + else { + vCardResults = await addressBookQuery({ + url: addressBook.url, + props: { + [`d:getetag`]: {}, + [`card:address-data`]: {} + }, + depth: "1", + headers: excludeHeaders(headers, headersToExclude), + headersToExclude, + fetchOptions, + fetch: fetchOverride + }); + vCardResults = vCardResults.filter((res) => !!res.href && targetUrls.has(getDAVUrlKey(res.href, addressBook.url))); + } } + assertDAVObjectResponses(vCardResults, "addressData", vcardUrls, addressBook.url, "VCard fetch failed"); return vCardResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(addressBook.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.addressData?._cdata ?? res.props?.addressData })); }; @@ -860,6 +1062,7 @@ const createVCard = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -874,6 +1077,7 @@ const updateVCard = async (params) => { "content-type": "text/vcard; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -884,6 +1088,7 @@ const deleteVCard = async (params) => { url: vCard.url, etag: vCard.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -933,6 +1138,7 @@ const fetchCalendarUserAddresses = async (params) => { props: { [`c:calendar-user-address-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(account.principalUrl, r.href, account.rootUrl)); @@ -963,6 +1169,7 @@ const calendarQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -981,6 +1188,7 @@ const calendarMultiGet = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1005,6 +1213,7 @@ const makeCalendar = async (params) => { set: { prop: props } } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1018,21 +1227,27 @@ const fetchCalendars = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`c:calendar-description`]: {}, - [`c:calendar-timezone`]: {}, - [`d:displayname`]: {}, - [`ca:calendar-color`]: {}, - [`cs:getctag`]: {}, + props: { + ...customProps ?? { + [`c:calendar-description`]: {}, + [`c:calendar-timezone`]: {}, + [`d:displayname`]: {}, + [`ca:calendar-color`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`c:supported-calendar-component-set`]: {}, + [`d:sync-token`]: {} + }, [`d:resourcetype`]: {}, - [`c:supported-calendar-component-set`]: {}, - [`d:sync-token`]: {} + [`c:supported-calendar-component-set`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Calendar discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("calendar")).filter((rc) => { const components = extractComponentNames(rc.props?.supportedCalendarComponentSet?.comp); return components.length === 0 || components.some((c) => Object.values(ICALObjects).includes(c)); @@ -1045,12 +1260,12 @@ const fetchCalendars = async (params) => { description: typeof description === "string" ? description : "", timezone: typeof timezone === "string" ? timezone : "", url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), calendarColor: rs.props?.calendarColor, - displayName: rs.props?.displayname?._cdata ?? rs.props?.displayname, + displayName: getDAVText(rs.props?.displayname), components: extractComponentNames(compSet), resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken, + syncToken: getDAVText(rs.props?.syncToken), ...projectedProps && projectedEntries.length > 0 ? { projectedProps: Object.fromEntries(projectedEntries) } : {} }; }).map(async (cal) => ({ @@ -1058,6 +1273,7 @@ const fetchCalendars = async (params) => { reports: await supportedReportSet({ collection: cal, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1065,6 +1281,7 @@ const fetchCalendars = async (params) => { }; const fetchCalendarObjects = async (params) => { const { calendar, objectUrls, filters: customFilters, timeRange, headers, expand, urlFilter = (url) => Boolean(url?.includes(".ics")), useMultiGet = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; + if (expand && !timeRange) throw new Error("timeRange is required when expand is true"); if (timeRange) validateTimeRange(timeRange); debug$3(`Fetching calendar objects from ${calendar?.url}`); const requiredFields = ["url"]; @@ -1095,6 +1312,7 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1102,6 +1320,7 @@ const fetchCalendarObjects = async (params) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(calendarObjectUrls.map((url) => getDAVUrlKey(url, calendar.url))); let calendarObjectResults = []; if (calendarObjectUrls.length > 0) { if (expand && !objectUrls) calendarObjectResults = initialResponses.filter((res) => { @@ -1121,12 +1340,13 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - if (objectUrls && objectUrls.length > 0) calendarObjectResults = calendarObjectResults.filter((res) => { + calendarObjectResults = calendarObjectResults.filter((res) => { const fullUrl = (res.href ?? "").startsWith("http") ? res.href ?? "" : new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href; - return calendarObjectUrls.some((target) => urlMatches(fullUrl, target, calendar.url)); + return targetUrls.has(getDAVUrlKey(fullUrl, calendar.url)); }); } else calendarObjectResults = await calendarMultiGet({ url: calendar.url, @@ -1140,13 +1360,15 @@ const fetchCalendarObjects = async (params) => { objectUrls: calendarObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); } + assertDAVObjectResponses(calendarObjectResults, "calendarData", calendarObjectUrls, calendar.url, "Calendar object fetch failed"); return calendarObjectResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.calendarData?._cdata ?? res.props?.calendarData })); }; @@ -1160,6 +1382,7 @@ const createCalendarObject = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1174,6 +1397,7 @@ const updateCalendarObject = async (params) => { "content-type": "text/calendar; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1184,6 +1408,7 @@ const deleteCalendarObject = async (params) => { url: calendarObject.url, etag: calendarObject.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1198,19 +1423,26 @@ const syncCalendars = async (params) => { const remoteCalendars = await fetchCalendars({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - const created = remoteCalendars.filter((rc) => localCalendars.every((lc) => !urlMatches(lc.url, rc.url, account.rootUrl))); - debug$3(`new calendars: ${created.map((cc) => cc.displayName)}`); - const updated = localCalendars.reduce((prev, curr) => { - const found = remoteCalendars.find((rc) => urlMatches(rc.url, curr.url, account.rootUrl)); - if (found && (found.syncToken && `${found.syncToken}` !== `${curr.syncToken}` || found.ctag && `${found.ctag}` !== `${curr.ctag}`)) return [...prev, { - local: curr, - remote: found - }]; - return prev; - }, []); + const baseUrl = account.rootUrl ?? account.homeUrl ?? account.serverUrl; + const localByUrl = new Map(localCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const remoteByUrl = new Map(remoteCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const created = remoteCalendars.filter((cal) => !localByUrl.has(getDAVUrlKey(cal.url, baseUrl))); + const updated = []; + const unchanged = []; + const deleted = []; + for (const local of localCalendars) { + const remote = remoteByUrl.get(getDAVUrlKey(local.url, baseUrl)); + if (!remote) deleted.push(local); + else if (!remote.syncToken && !remote.ctag || remote.syncToken && remote.syncToken !== local.syncToken || remote.ctag && remote.ctag !== local.ctag) updated.push({ + local, + remote + }); + else unchanged.push(local); + } debug$3(`updated calendars: ${updated.map(({ remote }) => remote.displayName)}`); const updatedWithObjects = await Promise.all(updated.map(async ({ local, remote }) => { const fetchObjects = async (fetchParams) => { @@ -1218,7 +1450,9 @@ const syncCalendars = async (params) => { const { collection, ...requestParams } = fetchParams; return fetchCalendarObjects({ ...requestParams, - calendar: collection + calendar: collection, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => getDAVUrlKey(url, collection.url) !== getDAVUrlKey(collection.url, collection.url) }); }; const collection = { @@ -1233,25 +1467,17 @@ const syncCalendars = async (params) => { collection, detailedResult: false, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account, fetchOptions, fetch: fetchOverride }); return { ...result, - ctag: remote.ctag ?? result.ctag, - syncToken: remote.syncToken ?? result.syncToken + ctag: remote.reports?.includes("syncCollection") ? remote.ctag ?? result.ctag : result.ctag ?? remote.ctag, + syncToken: remote.reports?.includes("syncCollection") ? result.syncToken : remote.syncToken ?? result.syncToken }; })); - const deleted = localCalendars.filter((cal) => remoteCalendars.every((rc) => !urlMatches(rc.url, cal.url, account.rootUrl))); - debug$3(`deleted calendars: ${deleted.map((cc) => cc.displayName)}`); - const unchanged = localCalendars.filter((cal) => remoteCalendars.some((rc) => { - if (!urlMatches(rc.url, cal.url, account.rootUrl)) return false; - const syncTokenMatches = !rc.syncToken || `${rc.syncToken}` === `${cal.syncToken}`; - const ctagMatches = !rc.ctag || `${rc.ctag}` === `${cal.ctag}`; - return syncTokenMatches && ctagMatches; - })); - debug$3(`unchanged calendars: ${unchanged.map((cc) => cc.displayName)}`); return detailedResult ? { created, updated: updatedWithObjects, @@ -1282,6 +1508,7 @@ const freeBusyQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]; @@ -1318,11 +1545,7 @@ const serviceDiscovery = async (params) => { const location = response.headers.get("Location"); if (typeof location === "string" && location.length) { debug$2(`Service discovery redirected to ${location}`); - const hasExplicitScheme = /^[a-z][a-z0-9+.-]*:/i.test(location); - const serviceURL = new URL(location, endpoint); - if (serviceURL.hostname === uri.hostname && uri.port && !serviceURL.port) serviceURL.port = uri.port; - if (!hasExplicitScheme) serviceURL.protocol = endpoint.protocol ?? "http"; - return serviceURL.href; + return new URL(location, uri).href; } } }; @@ -1347,6 +1570,7 @@ const serviceDiscovery = async (params) => { const redirectUrl = extractRedirect(await requestFetch(uri.href, { ...fetchOptionsWithoutHeaders, method: "GET", + body: void 0, headers: excludeHeaders(mergeHeaders(headers, fetchHeaders), headersToExclude), redirect: "manual" })); @@ -1386,6 +1610,7 @@ const fetchPrincipalUrl = async (params) => { props: { [`d:current-user-principal`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1412,6 +1637,7 @@ const fetchHomeUrl = async (params) => { props: account.accountType === "caldav" ? { [`c:calendar-home-set`]: {} } : { [`card:addressbook-home-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1435,6 +1661,7 @@ const createAccount = async (params) => { const discoveredRootUrl = account.rootUrl ?? await serviceDiscovery({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1453,6 +1680,7 @@ const createAccount = async (params) => { rootUrl }, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1468,24 +1696,28 @@ const createAccount = async (params) => { newAccount.principalUrl = account.principalUrl ?? newAccount.principalUrl ?? await fetchPrincipalUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); newAccount.homeUrl = account.homeUrl ?? await fetchHomeUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); if (loadCollections || loadObjects) { if (account.accountType === "caldav") newAccount.calendars = await fetchCalendars({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride }); else if (account.accountType === "carddav") newAccount.addressBooks = await fetchAddressBooks({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride @@ -1496,7 +1728,10 @@ const createAccount = async (params) => { ...cal, objects: await fetchCalendarObjects({ calendar: cal, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => !urlEquals(url, cal.url), headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1506,6 +1741,7 @@ const createAccount = async (params) => { objects: await fetchVCards({ addressBook: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1656,20 +1892,18 @@ const getOauthHeaders = async (credentials, fetchOptions, fetchOverride) => { debug$1("Fetching oauth headers"); let tokens = {}; let didRefresh = false; - if (!credentials.refreshToken) { - tokens = await fetchOauthTokens(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else if (credentials.refreshToken && !credentials.accessToken || Date.now() > (credentials.expiration ?? 0)) { - tokens = await refreshAccessToken(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else tokens = { + if (credentials.accessToken && (credentials.expiration == null && !credentials.refreshToken || credentials.expiration != null && Date.now() < credentials.expiration)) tokens = { access_token: credentials.accessToken, refresh_token: credentials.refreshToken }; + else { + tokens = credentials.refreshToken ? await refreshAccessToken(credentials, fetchOptions, fetchOverride) : await fetchOauthTokens(credentials, fetchOptions, fetchOverride); + didRefresh = true; + } if (didRefresh) { if (tokens.access_token) credentials.accessToken = tokens.access_token; if (tokens.refresh_token) credentials.refreshToken = tokens.refresh_token; - if (typeof tokens.expires_in === "number") credentials.expiration = Date.now() + tokens.expires_in * 1e3; + if (tokens.access_token) credentials.expiration = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0; } debug$1("Oauth tokens obtained"); return { @@ -1683,145 +1917,71 @@ var client_exports = /* @__PURE__ */ __exportAll({ DAVClient: () => DAVClient, createDAVClient: () => createDAVClient }); -const createDAVClient = async (params) => { - const { serverUrl, credentials, authMethod = "Basic", defaultAccountType, authFunction, fetchOptions: defaultFetchOptions, fetch: fetchOverride } = params; - let authHeaders = {}; - switch (authMethod) { - case "Basic": - authHeaders = getBasicAuthHeaders(credentials); - break; - case "Bearer": - authHeaders = getBearerAuthHeaders(credentials); - break; - case "Oauth": - authHeaders = (await getOauthHeaders(credentials, defaultFetchOptions, fetchOverride)).headers; - break; - case "Digest": - authHeaders = { Authorization: `Digest ${credentials.digestString}` }; - break; +const resolveAuthHeaders = async (client, fetchOptions = client.fetchOptions, fetchOverride = client.fetchOverride) => { + switch (client.authMethod) { + case "Basic": return getBasicAuthHeaders(client.credentials); + case "Bearer": return getBearerAuthHeaders(client.credentials); + case "Oauth": { + const { headers } = await getOauthHeaders(client.credentials, fetchOptions, fetchOverride); + if (!headers.authorization) throw new Error("OAuth authentication failed: token endpoint returned no access token"); + return headers; + } + case "Digest": return { Authorization: `Digest ${client.credentials.digestString}` }; case "Custom": - if (!authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - authHeaders = await authFunction(credentials) ?? {}; - break; + if (!client.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); + return await client.authFunction(client.credentials) ?? {}; default: throw new Error("Invalid auth method"); } - const defaultAccount = defaultAccountType ? await createAccount({ +}; +const createDAVClient = async (params) => { + const client = new DAVClient(params); + client.authHeaders = await resolveAuthHeaders(client); + client.account = params.defaultAccountType ? await createAccount({ account: { - serverUrl, - credentials, - accountType: defaultAccountType + serverUrl: params.serverUrl, + credentials: params.credentials, + accountType: params.defaultAccountType }, - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride + headers: client.authHeaders, + fetchOptions: client.fetchOptions, + fetch: client.fetchOverride }) : void 0; - const davRequest$1 = async (params0) => { - const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; - const { headers, ...restInit } = init; - return davRequest({ - ...rest, - init: { - ...restInit, - headers: { - ...authHeaders, - ...headers - } - }, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const commonDefaults = { - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride - }; - const commonDefaultsWithUrl = { - url: serverUrl, - ...commonDefaults - }; - const commonDefaultsWithAccount = { - account: defaultAccount, - ...commonDefaults - }; - const createObject$1 = defaultParam(createObject, commonDefaultsWithUrl); - const updateObject$1 = defaultParam(updateObject, commonDefaultsWithUrl); - const deleteObject$1 = defaultParam(deleteObject, commonDefaultsWithUrl); - const propfind$1 = defaultParam(propfind, commonDefaults); - const createAccount$1 = async (params0) => { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch: fetchOverride2 } = params0; - const merged = { - serverUrl, - credentials, - ...account - }; - if (!merged.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client."); - return createAccount({ - account: merged, - headers: { - ...authHeaders, - ...headers - }, - loadCollections, - loadObjects, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const collectionQuery$1 = defaultParam(collectionQuery, commonDefaults); - const makeCollection$1 = defaultParam(makeCollection, commonDefaults); - const syncCollection$1 = defaultParam(syncCollection, commonDefaults); - const supportedReportSet$1 = defaultParam(supportedReportSet, commonDefaults); - const isCollectionDirty$1 = defaultParam(isCollectionDirty, commonDefaults); - const smartCollectionSync$1 = defaultParam(smartCollectionSync, commonDefaultsWithAccount); - const smartCollectionSyncDetailed$1 = defaultParam(smartCollectionSyncDetailed, commonDefaultsWithAccount); - const calendarQuery$1 = defaultParam(calendarQuery, commonDefaults); - const calendarMultiGet$1 = defaultParam(calendarMultiGet, commonDefaults); - const makeCalendar$1 = defaultParam(makeCalendar, commonDefaults); - const fetchCalendars$1 = defaultParam(fetchCalendars, commonDefaultsWithAccount); - const fetchCalendarUserAddresses$1 = defaultParam(fetchCalendarUserAddresses, commonDefaultsWithAccount); - const fetchCalendarObjects$1 = defaultParam(fetchCalendarObjects, commonDefaults); - const createCalendarObject$1 = defaultParam(createCalendarObject, commonDefaults); - const updateCalendarObject$1 = defaultParam(updateCalendarObject, commonDefaults); - const deleteCalendarObject$1 = defaultParam(deleteCalendarObject, commonDefaults); - const syncCalendars$1 = defaultParam(syncCalendars, commonDefaultsWithAccount); - const syncCalendarsDetailed$1 = defaultParam(syncCalendarsDetailed, commonDefaultsWithAccount); - const freeBusyQuery$1 = defaultParam(freeBusyQuery, commonDefaults); - const addressBookQuery$1 = defaultParam(addressBookQuery, commonDefaults); - const addressBookMultiGet$1 = defaultParam(addressBookMultiGet, commonDefaults); return { - davRequest: davRequest$1, - propfind: propfind$1, - createAccount: createAccount$1, - createObject: createObject$1, - updateObject: updateObject$1, - deleteObject: deleteObject$1, - calendarQuery: calendarQuery$1, - addressBookQuery: addressBookQuery$1, - collectionQuery: collectionQuery$1, - makeCollection: makeCollection$1, - calendarMultiGet: calendarMultiGet$1, - makeCalendar: makeCalendar$1, - freeBusyQuery: freeBusyQuery$1, - syncCollection: syncCollection$1, - supportedReportSet: supportedReportSet$1, - isCollectionDirty: isCollectionDirty$1, - smartCollectionSync: smartCollectionSync$1, - smartCollectionSyncDetailed: smartCollectionSyncDetailed$1, - fetchCalendars: fetchCalendars$1, - fetchCalendarUserAddresses: fetchCalendarUserAddresses$1, - fetchCalendarObjects: fetchCalendarObjects$1, - createCalendarObject: createCalendarObject$1, - updateCalendarObject: updateCalendarObject$1, - deleteCalendarObject: deleteCalendarObject$1, - syncCalendars: syncCalendars$1, - syncCalendarsDetailed: syncCalendarsDetailed$1, - fetchAddressBooks: defaultParam(fetchAddressBooks, commonDefaultsWithAccount), - addressBookMultiGet: addressBookMultiGet$1, - fetchVCards: defaultParam(fetchVCards, commonDefaults), - createVCard: defaultParam(createVCard, commonDefaults), - updateVCard: defaultParam(updateVCard, commonDefaults), - deleteVCard: defaultParam(deleteVCard, commonDefaults) + davRequest: client.davRequest.bind(client), + propfind: client.propfind.bind(client), + createAccount: async (...args) => { + if (!args[0].account.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType`."); + return client.createAccount(...args); + }, + createObject: client.createObject.bind(client), + updateObject: client.updateObject.bind(client), + deleteObject: client.deleteObject.bind(client), + calendarQuery: client.calendarQuery.bind(client), + addressBookQuery: client.addressBookQuery.bind(client), + collectionQuery: client.collectionQuery.bind(client), + makeCollection: client.makeCollection.bind(client), + calendarMultiGet: client.calendarMultiGet.bind(client), + makeCalendar: client.makeCalendar.bind(client), + freeBusyQuery: client.freeBusyQuery.bind(client), + syncCollection: client.syncCollection.bind(client), + supportedReportSet: client.supportedReportSet.bind(client), + isCollectionDirty: client.isCollectionDirty.bind(client), + smartCollectionSync: client.smartCollectionSync.bind(client), + smartCollectionSyncDetailed: client.smartCollectionSyncDetailed.bind(client), + fetchCalendars: client.fetchCalendars.bind(client), + fetchCalendarUserAddresses: client.fetchCalendarUserAddresses.bind(client), + fetchCalendarObjects: client.fetchCalendarObjects.bind(client), + createCalendarObject: client.createCalendarObject.bind(client), + updateCalendarObject: client.updateCalendarObject.bind(client), + deleteCalendarObject: client.deleteCalendarObject.bind(client), + syncCalendars: client.syncCalendars.bind(client), + syncCalendarsDetailed: client.syncCalendarsDetailed.bind(client), + fetchAddressBooks: client.fetchAddressBooks.bind(client), + addressBookMultiGet: client.addressBookMultiGet.bind(client), + fetchVCards: client.fetchVCards.bind(client), + createVCard: client.createVCard.bind(client), + updateVCard: client.updateVCard.bind(client), + deleteVCard: client.deleteVCard.bind(client) }; }; var DAVClient = class { @@ -1833,27 +1993,42 @@ var DAVClient = class { this.authFunction = params.authFunction; this.fetchOptions = params.fetchOptions ?? {}; this.fetchOverride = params.fetch; + this.calendarMultiGet = this.calendarMultiGet.bind(this); + this.addressBookMultiGet = this.addressBookMultiGet.bind(this); + } + async authenticate(force = false, fetchOptions = this.fetchOptions, fetchOverride = this.fetchOverride) { + if (!force && this.authMethod !== "Oauth") return; + if (!force && this.authHeaders && this.credentials.accessToken && (this.credentials.expiration == null || Date.now() < this.credentials.expiration)) { + this.authHeaders = { authorization: `Bearer ${this.credentials.accessToken}` }; + return; + } + if (this.authentication) return this.authentication; + const authenticate = async () => { + this.authHeaders = await resolveAuthHeaders(this, fetchOptions, fetchOverride); + }; + this.authentication = authenticate(); + try { + await this.authentication; + } finally { + this.authentication = void 0; + } + } + async requestDefaults(params) { + await this.authenticate(false, params?.fetchOptions ?? this.fetchOptions, params?.fetch ?? this.fetchOverride); + return { + url: this.serverUrl, + headers: this.authHeaders, + account: this.account, + fetchOptions: this.fetchOptions, + fetch: this.fetchOverride + }; + } + async invoke(fn, params) { + const defaults = await this.requestDefaults(params); + return await defaultParam(fn, defaults)(...[params]); } async login(options) { - switch (this.authMethod) { - case "Basic": - this.authHeaders = getBasicAuthHeaders(this.credentials); - break; - case "Bearer": - this.authHeaders = getBearerAuthHeaders(this.credentials); - break; - case "Oauth": - this.authHeaders = (await getOauthHeaders(this.credentials, this.fetchOptions, this.fetchOverride)).headers; - break; - case "Digest": - this.authHeaders = { Authorization: `Digest ${this.credentials.digestString}` }; - break; - case "Custom": - if (!this.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - this.authHeaders = await this.authFunction(this.credentials); - break; - default: throw new Error("Invalid auth method"); - } + await this.authenticate(true); this.account = this.accountType ? await createAccount({ account: { serverUrl: this.serverUrl, @@ -1870,52 +2045,32 @@ var DAVClient = class { async davRequest(params0) { const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; const { headers, ...restInit } = init; + const defaults = await this.requestDefaults(params0); return davRequest({ ...rest, init: { ...restInit, - headers: { - ...this.authHeaders, - ...headers - } + headers: mergeHeaders(defaults.headers, headers) }, fetchOptions: fetchOptions ?? this.fetchOptions, fetch: fetchOverride2 ?? this.fetchOverride }); } async createObject(...params) { - return defaultParam(createObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createObject, params[0]); } async updateObject(...params) { - return defaultParam(updateObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateObject, params[0]); } async deleteObject(...params) { - return defaultParam(deleteObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteObject, params[0]); } async propfind(...params) { - return defaultParam(propfind, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(propfind, params[0]); } async createAccount(params0) { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const { account, headers, headersToExclude, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const defaults = await this.requestDefaults(params0); const accountType = account.accountType ?? this.accountType; if (!accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient."); return createAccount({ @@ -1925,10 +2080,8 @@ var DAVClient = class { ...account, accountType }, - headers: { - ...this.authHeaders, - ...headers - }, + headers: mergeHeaders(defaults.headers, headers), + headersToExclude, loadCollections, loadObjects, fetchOptions: fetchOptions ?? this.fetchOptions, @@ -1936,193 +2089,82 @@ var DAVClient = class { }); } async collectionQuery(...params) { - return defaultParam(collectionQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(collectionQuery, params[0]); } async makeCollection(...params) { - return defaultParam(makeCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCollection, params[0]); } async syncCollection(...params) { - return defaultParam(syncCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCollection, params[0]); } async supportedReportSet(...params) { - return defaultParam(supportedReportSet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(supportedReportSet, params[0]); } async isCollectionDirty(...params) { - return defaultParam(isCollectionDirty, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(isCollectionDirty, params[0]); } async smartCollectionSync(...params) { - return defaultParam(smartCollectionSync, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(params[0]); + return this.invoke(smartCollectionSync, params[0]); } async smartCollectionSyncDetailed(param) { - return defaultParam(smartCollectionSyncDetailed, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(param); + return this.invoke(smartCollectionSyncDetailed, param); } async calendarQuery(...params) { - return defaultParam(calendarQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarQuery, params[0]); } async makeCalendar(...params) { - return defaultParam(makeCalendar, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCalendar, params[0]); } async freeBusyQuery(...params) { - return defaultParam(freeBusyQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(freeBusyQuery, params[0]); } async calendarMultiGet(...params) { - return defaultParam(calendarMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarMultiGet, params[0]); } async fetchCalendars(...params) { - return defaultParam(fetchCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendars, params[0]); } async fetchCalendarUserAddresses(...params) { - return defaultParam(fetchCalendarUserAddresses, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendarUserAddresses, params[0]); } async fetchCalendarObjects(...params) { - return defaultParam(fetchCalendarObjects, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchCalendarObjects, params[0]); } async createCalendarObject(...params) { - return defaultParam(createCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createCalendarObject, params[0]); } async updateCalendarObject(...params) { - return defaultParam(updateCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateCalendarObject, params[0]); } async deleteCalendarObject(...params) { - return defaultParam(deleteCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteCalendarObject, params[0]); } async syncCalendars(...params) { - return defaultParam(syncCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendars, params[0]); } async syncCalendarsDetailed(...params) { - return defaultParam(syncCalendarsDetailed, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendarsDetailed, params[0]); } async addressBookQuery(...params) { - return defaultParam(addressBookQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookQuery, params[0]); } async addressBookMultiGet(...params) { - return defaultParam(addressBookMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookMultiGet, params[0]); } async fetchAddressBooks(...params) { - return defaultParam(fetchAddressBooks, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchAddressBooks, params[0]); } async fetchVCards(...params) { - return defaultParam(fetchVCards, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchVCards, params[0]); } async createVCard(...params) { - return defaultParam(createVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createVCard, params[0]); } async updateVCard(...params) { - return defaultParam(updateVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateVCard, params[0]); } async deleteVCard(...params) { - return defaultParam(deleteVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteVCard, params[0]); } }; //#endregion @@ -2180,6 +2222,7 @@ exports.getDAVAttribute = getDAVAttribute; exports.getOauthHeaders = getOauthHeaders; exports.isCollectionDirty = isCollectionDirty; exports.makeCalendar = makeCalendar; +exports.makeCollection = makeCollection; exports.mergeHeaders = mergeHeaders; exports.propfind = propfind; exports.refreshAccessToken = refreshAccessToken; diff --git a/dist/tsdav.d.ts b/dist/tsdav.d.ts index 65f617ce..238da19c 100644 --- a/dist/tsdav.d.ts +++ b/dist/tsdav.d.ts @@ -1,13 +1,13 @@ import * as client from './client'; import { DAVNamespace, DAVNamespaceShort, ICALObjects } from './consts'; -export type { DAVDepth, DAVMethods, DAVRequest, DAVResponse, DAVTokens } from './types/DAVTypes'; +export type { DAVDepth, DAVMethods, DAVPropStat, DAVRequest, DAVResponse, DAVTokens, } from './types/DAVTypes'; export type { DAVAccount, DAVAddressBook, DAVCalendar, DAVCalendarObject, DAVCollection, DAVCredentials, DAVObject, DAVVCard, } from './types/models'; export type { SmartCollectionSync, SmartCollectionSyncDetailed, SmartCollectionSyncDetailedResult, SyncCalendars, SyncCalendarsDetailed, SyncCalendarsDetailedResult, } from './types/functionsOverloads'; export { DAVClient } from './client'; export { createDAVClient } from './client'; export { createAccount, serviceDiscovery, fetchPrincipalUrl, fetchHomeUrl, } from './account'; export { davRequest, propfind, createObject, updateObject, deleteObject } from './request'; -export { collectionQuery, supportedReportSet, isCollectionDirty, syncCollection, smartCollectionSync, smartCollectionSyncDetailed, } from './collection'; +export { collectionQuery, makeCollection, supportedReportSet, isCollectionDirty, syncCollection, smartCollectionSync, smartCollectionSyncDetailed, } from './collection'; export { calendarQuery, calendarMultiGet, makeCalendar, fetchCalendars, fetchCalendarUserAddresses, fetchCalendarObjects, createCalendarObject, updateCalendarObject, deleteCalendarObject, syncCalendars, syncCalendarsDetailed, freeBusyQuery, } from './calendar'; export { addressBookQuery, addressBookMultiGet, fetchAddressBooks, fetchVCards, createVCard, updateVCard, deleteVCard, } from './addressBook'; export { getBasicAuthHeaders, getBearerAuthHeaders, getOauthHeaders, fetchOauthTokens, refreshAccessToken, } from './util/authHelpers'; @@ -32,6 +32,7 @@ declare const _default: { init: import(".").DAVRequest; convertIncoming?: boolean; parseOutgoing?: boolean; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof import("./util/fetch").fetch; }) => Promise; @@ -103,7 +104,7 @@ declare const _default: { fetch?: typeof fetch; }) => Promise<{ isDirty: boolean; - newCtag: string; + newCtag: string | undefined; }>; syncCollection: (params: { url: string; @@ -324,20 +325,13 @@ declare const _default: { authorization?: string; }; }>; - createDAVClient: (params: { - serverUrl: string; - credentials: import(".").DAVCredentials; - authMethod?: 'Basic' | 'Oauth' | 'Digest' | 'Custom' | 'Bearer'; - authFunction?: (credentials: import(".").DAVCredentials) => Promise>; - defaultAccountType?: import(".").DAVAccount['accountType'] | undefined; - fetchOptions?: RequestInit; - fetch?: typeof globalThis.fetch; - }) => Promise<{ + createDAVClient: (params: ConstructorParameters[0]) => Promise<{ davRequest: (params0: { url: string; init: import(".").DAVRequest; convertIncoming?: boolean; parseOutgoing?: boolean; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof globalThis.fetch; }) => Promise; @@ -350,14 +344,7 @@ declare const _default: { fetchOptions?: RequestInit; fetch?: typeof import("./util/fetch").fetch; }) => Promise; - createAccount: (params0: { - account: import("./util/typeHelpers").Optional; - headers?: Record; - loadCollections?: boolean; - loadObjects?: boolean; - fetchOptions?: RequestInit; - fetch?: typeof globalThis.fetch; - }) => Promise; + createAccount: (...args: Parameters) => Promise; createObject: (params: { url: string; data: BodyInit; @@ -481,10 +468,49 @@ declare const _default: { fetch?: typeof fetch; }) => Promise<{ isDirty: boolean; - newCtag: string; + newCtag: string | undefined; }>; - smartCollectionSync: import(".").SmartCollectionSync; - smartCollectionSyncDetailed: import(".").SmartCollectionSyncDetailed; + smartCollectionSync: { + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + detailedResult?: false; + }): Promise; + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + detailedResult: true; + }): Promise>; + (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + detailedResult?: boolean; + }): Promise>; + }; + smartCollectionSyncDetailed: (param: { + collection: T; + method?: 'basic' | 'webdav'; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof globalThis.fetch; + account?: import(".").DAVAccount; + }) => Promise>; fetchCalendars: (params?: { account?: import(".").DAVAccount; props?: import("xml-js").ElementCompact; @@ -516,7 +542,7 @@ declare const _default: { useMultiGet?: boolean; fetchOptions?: RequestInit; fetch?: typeof fetch; - }) => Promise; + }) => Promise; createCalendarObject: (params: { calendar: import(".").DAVCalendar; iCalString: string; @@ -540,8 +566,23 @@ declare const _default: { fetchOptions?: RequestInit; fetch?: typeof fetch; }) => Promise; - syncCalendars: import(".").SyncCalendars; - syncCalendarsDetailed: import(".").SyncCalendarsDetailed; + syncCalendars: { + (params: Parameters[0] & { + detailedResult: true; + }): Promise; + (params: Parameters[0] & { + detailedResult?: false; + }): Promise; + (params: Parameters[0]): Promise; + }; + syncCalendarsDetailed: (params: { + oldCalendars: import(".").DAVCalendar[]; + headers?: Record; + headersToExclude?: string[]; + fetchOptions?: RequestInit; + fetch?: typeof fetch; + account?: import(".").DAVAccount; + }) => Promise; fetchAddressBooks: (params?: { account?: import(".").DAVAccount; props?: import("xml-js").ElementCompact; @@ -549,7 +590,7 @@ declare const _default: { headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof fetch; - } | undefined) => Promise; + } | undefined) => Promise; addressBookMultiGet: (params: { url: string; props: import("xml-js").ElementCompact; @@ -569,7 +610,7 @@ declare const _default: { headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof fetch; - }) => Promise; + }) => Promise; createVCard: (params: { addressBook: import(".").DAVAddressBook; vCardString: string; diff --git a/dist/tsdav.esm.js b/dist/tsdav.esm.js index 11e8c341..1011a86d 100644 --- a/dist/tsdav.esm.js +++ b/dist/tsdav.esm.js @@ -46,9 +46,6 @@ let ICALObjects = /* @__PURE__ */ function(ICALObjects) { return ICALObjects; }({}); //#endregion -//#region src/util/camelCase.ts -const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); -//#endregion //#region src/util/fetch.ts /** * Resolve the runtime `fetch` implementation. @@ -74,18 +71,175 @@ const resolveFetch = () => { }; const fetch = resolveFetch(); //#endregion -//#region src/util/nativeType.ts -const NUMERIC_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; -const nativeType = (value) => { - if (typeof value !== "string") return value; - if (NUMERIC_RE.test(value)) { - const nValue = Number(value); - if (!Number.isNaN(nValue) && Number.isFinite(nValue) && (!Number.isInteger(nValue) || Number.isSafeInteger(nValue))) return nValue; - } - const bValue = value.toLowerCase(); - if (bValue === "true") return true; - if (bValue === "false") return false; - return value; +//#region src/util/camelCase.ts +const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); +//#endregion +//#region src/util/typeHelpers.ts +function hasFields(obj, fields) { + if (!obj) return false; + const inObj = (object) => object != null && fields.every((f) => object[f]); + if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); + return inObj(obj); +} +const findMissingFieldNames = (obj, fields) => { + if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); + return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +}; +const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key); +//#endregion +//#region src/util/xml.ts +const davNamespaces = new Set(Object.values(DAVNamespace)); +const standardNamespaces = new Map([ + ...[ + "multistatus", + "response", + "propstat", + "prop", + "href", + "status", + "error", + "responsedescription", + "resourcetype", + "collection", + "getetag", + "displayname", + "syncToken", + "supportedReportSet", + "supportedReport", + "report", + "currentUserPrincipal" + ].map((name) => [name, "DAV:"]), + ...[ + "calendarData", + "calendar", + "calendarDescription", + "calendarTimezone", + "calendarHomeSet", + "calendarUserAddressSet", + "supportedCalendarComponentSet", + "comp" + ].map((name) => [name, "urn:ietf:params:xml:ns:caldav"]), + ...[ + "addressData", + "addressbook", + "addressbookHomeSet" + ].map((name) => [name, "urn:ietf:params:xml:ns:carddav"]), + ["getctag", "http://calendarserver.org/ns/"], + ["calendarColor", "http://apple.com/ns/ical/"] +]); +const normalizedKey = (name, namespace) => { + const localName = camelCase(name.replace(/^.*:/, "")); + const standardNamespace = standardNamespaces.get(localName); + return namespace && standardNamespace && namespace !== standardNamespace ? `{${namespace}}${localName}` : localName; +}; +const normalizeElement = (element, inheritedNamespaces) => { + const namespaceContext = Object.assign(Object.create(null), inheritedNamespaces); + const attributes = Object.create(null); + for (const [name, value] of Object.entries(element.attributes ?? {})) { + if (name === "xmlns" || name.startsWith("xmlns:")) namespaceContext[name === "xmlns" ? "" : name.slice(6)] = String(value); + if (name !== "xmlns") attributes[name] = value; + } + const name = element.name ?? ""; + const separator = name.indexOf(":"); + const namespace = namespaceContext[separator === -1 ? "" : name.slice(0, separator)] ?? ""; + const children = element.elements ?? []; + const elements = children.filter((child) => child.type === "element"); + const text = children.filter((child) => child.type === "text" || child.type === "cdata").map((child) => String(child.text ?? child.cdata ?? "")).join(""); + const value = {}; + const namespaces = Object.create(null); + if (Object.keys(attributes).length) value._attributes = attributes; + if (!elements.length) { + const hasText = children.some((child) => child.type === "text"); + const hasCdata = children.some((child) => child.type === "cdata"); + if (hasText) return { + value: text, + namespace, + namespaces + }; + if (hasCdata) value._cdata = text; + return { + value, + namespace, + namespaces + }; + } + if (text.trim()) value._text = text; + for (const child of elements) { + const normalized = normalizeElement(child, namespaceContext); + const localName = camelCase((child.name ?? "").replace(/^.*:/, "")); + let key = normalizedKey(child.name ?? "", normalized.namespace); + const previousNamespace = namespaces[key]; + if (hasOwn(value, key) && previousNamespace !== normalized.namespace) { + if (davNamespaces.has(normalized.namespace) && !davNamespaces.has(previousNamespace)) { + const previousKey = `{${previousNamespace}}${localName}`; + Object.defineProperty(value, previousKey, { + value: value[key], + enumerable: true, + configurable: true, + writable: true + }); + namespaces[previousKey] = previousNamespace; + delete value[key]; + } else key = `{${normalized.namespace}}${localName}`; + } + if (hasOwn(value, key)) { + if (Array.isArray(value[key])) value[key].push(normalized.value); + else value[key] = [value[key], normalized.value]; + } else Object.defineProperty(value, key, { + value: normalized.value, + enumerable: true, + configurable: true, + writable: true + }); + namespaces[key] = normalized.namespace; + if (localName === "prop" && normalized.namespace === "DAV:") value.propNamespaces = normalized.namespaces; + } + return { + value, + namespace, + namespaces + }; +}; +/** Preserve XML text order before normalizing DAV names to the existing compact shape. */ +const parseDAVXML = (xml) => { + const document = convert.xml2js(xml, { + compact: false, + ignoreDeclaration: true + }); + const result = {}; + for (const element of document.elements ?? []) { + if (element.type !== "element") continue; + const normalized = normalizeElement(element, {}); + const key = normalizedKey(element.name ?? "", normalized.namespace); + Object.defineProperty(result, key, { + value: normalized.value, + enumerable: true + }); + } + return result; +}; +/** Keep same-name properties from distinct namespaces across propstat groups. */ +const mergeDAVProps = (propStats) => { + const groups = /* @__PURE__ */ new Map(); + for (const stat of propStats) { + if (!stat.ok) continue; + for (const [name, value] of Object.entries(stat.props)) { + const namespaces = groups.get(name) ?? /* @__PURE__ */ new Map(); + namespaces.set(stat.namespaces?.[name] ?? "", value); + groups.set(name, namespaces); + } + } + const props = {}; + for (const [name, namespaces] of groups) { + const primary = [...namespaces.keys()].find((namespace) => davNamespaces.has(namespace)) ?? namespaces.keys().next().value; + for (const [namespace, value] of namespaces) Object.defineProperty(props, namespace === primary ? name : `{${namespace}}${name}`, { + value, + enumerable: true, + configurable: true, + writable: true + }); + } + return props; }; //#endregion //#region src/util/requestHelpers.ts @@ -205,7 +359,7 @@ var request_exports = /* @__PURE__ */ __exportAll({ }); const debug$5 = getLogger("tsdav:request"); const parseStatusLine = (statusLine) => { - const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine ?? ""); + const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine?.trim() ?? ""); const status = match?.groups?.status; const statusText = match?.groups?.statusText; return status ? { @@ -214,7 +368,7 @@ const parseStatusLine = (statusLine) => { } : void 0; }; const davRequest = async (params) => { - const { url, init, convertIncoming = true, parseOutgoing = true, fetchOptions = {}, fetch: fetchOverride } = params; + const { url, init, convertIncoming = true, parseOutgoing = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const requestFetch = fetchOverride ?? fetch; const { headers = {}, body, namespace, method, attributes } = init; let processedBody = body; @@ -247,7 +401,7 @@ const davRequest = async (params) => { }) : body; const fetchOptionsWithoutHeaders = { ...fetchOptions }; delete fetchOptionsWithoutHeaders.headers; - const mergedHeaders = mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers); + const mergedHeaders = excludeHeaders(mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers), headersToExclude); const davResponse = await requestFetch(url, { ...fetchOptionsWithoutHeaders, headers: mergedHeaders, @@ -255,54 +409,25 @@ const davRequest = async (params) => { method }); const resText = await davResponse.text(); - if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) { - const MAX_RAW = 4096; - const raw = resText.length > MAX_RAW ? `${resText.slice(0, MAX_RAW)}…` : resText; - return [{ - href: davResponse.url, - ok: davResponse.ok, - status: davResponse.status, - statusText: davResponse.statusText, - raw - }]; - } + if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) return [{ + href: davResponse.url, + ok: davResponse.ok, + status: davResponse.status, + statusText: davResponse.statusText, + raw: resText + }]; let result; try { - result = convert.xml2js(resText, { - compact: true, - trim: true, - textFn: (value, parentElement) => { - try { - const parentOfParent = parentElement._parent; - const pOpKeys = Object.keys(parentOfParent); - const keyName = pOpKeys[pOpKeys.length - 1]; - if (!keyName) return; - const arrOfKey = parentOfParent[keyName]; - if (arrOfKey.length > 0) { - const arr = arrOfKey; - const arrIndex = arrOfKey.length - 1; - arr[arrIndex] = nativeType(value); - } else parentOfParent[keyName] = nativeType(value); - } catch (e) { - debug$5(e.stack); - } - }, - elementNameFn: (attributeName) => camelCase(attributeName.replace(/^.+:/, "")), - attributesFn: (value) => { - const newVal = { ...value }; - delete newVal.xmlns; - return newVal; - }, - ignoreDeclaration: true - }); + result = parseDAVXML(resText); } catch (e) { debug$5(`Failed to parse DAV response XML: ${e.message}`); return [{ href: davResponse.url, - ok: davResponse.ok, + ok: false, status: davResponse.status, statusText: davResponse.statusText, - raw: resText + raw: resText, + parseError: e.message }]; } if (!result?.multistatus) return [{ @@ -319,7 +444,21 @@ const davRequest = async (params) => { statusText: davResponse.statusText, ok: davResponse.ok }; - const parsedStatus = parseStatusLine(responseBody.status); + const propStats = (Array.isArray(responseBody.propstat) ? responseBody.propstat : responseBody.propstat ? [responseBody.propstat] : []).map((stat) => { + const parsed = parseStatusLine(stat.status); + const status = parsed?.status ?? 0; + return { + props: stat.prop ?? {}, + namespaces: stat.propNamespaces, + status, + statusText: parsed?.statusText ?? "Invalid DAV property status", + ok: status >= 200 && status < 300, + error: stat.error, + responsedescription: stat.responsedescription + }; + }); + const failedStatus = propStats.length > 0 && propStats.every((stat) => !stat.ok) ? propStats[0] : void 0; + const parsedStatus = parseStatusLine(responseBody.status) ?? failedStatus; const status = parsedStatus?.status ?? davResponse.status; return { raw: result, @@ -329,14 +468,8 @@ const davRequest = async (params) => { ok: status >= 200 && status < 300, error: responseBody.error, responsedescription: responseBody.responsedescription, - props: (Array.isArray(responseBody.propstat) ? responseBody.propstat : [responseBody.propstat]).reduce((prev, curr) => { - const propstatStatus = parseStatusLine(curr?.status)?.status; - if (propstatStatus && (propstatStatus < 200 || propstatStatus >= 300)) return prev; - return { - ...prev, - ...curr?.prop - }; - }, {}) + propStats, + props: mergeDAVProps(propStats) }; }); }; @@ -362,6 +495,7 @@ const propfind = async (params) => { prop: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -405,16 +539,76 @@ const deleteObject = async (params) => { }); }; //#endregion -//#region src/util/typeHelpers.ts -function hasFields(obj, fields) { - if (!obj) return false; - const inObj = (object) => object != null && fields.every((f) => object[f]); - if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); - return inObj(obj); -} -const findMissingFieldNames = (obj, fields) => { - if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); - return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +//#region src/util/syncHelpers.ts +/** Stable keys for linear-time DAV resource comparisons. */ +const getDAVUrlKey = (url, baseUrl) => { + let resolved; + try { + resolved = new URL(url, ensureTrailingSlash(baseUrl)).href; + } catch { + resolved = url.trim(); + } + return resolved.endsWith("/") ? resolved.slice(0, -1) : resolved; +}; +const diffDAVObjects = (local, remote, baseUrl, incremental = false, deletedObjects = []) => { + const localByUrl = new Map(local.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const remoteByUrl = new Map(remote.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const deleted = incremental ? deletedObjects : local.filter((object) => !remoteByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const deletedUrls = new Set(deleted.map((object) => getDAVUrlKey(object.url, baseUrl))); + const created = remote.filter((object) => !localByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const updated = []; + const unchanged = []; + for (const object of local) { + const key = getDAVUrlKey(object.url, baseUrl); + if (deletedUrls.has(key)) continue; + const found = remoteByUrl.get(key); + if (found && found.etag !== object.etag) updated.push(found); + else if (found || incremental) unchanged.push(object); + } + return { + created, + updated, + deleted, + unchanged + }; +}; +//#endregion +//#region src/util/responseHelpers.ts +const assertDAVResponses = (responses, context) => { + const failed = responses.find((response) => !response.ok || response.status >= 400); + if (failed) throw new Error(`${context}: ${failed.status} ${failed.statusText}`); +}; +const assertDAVDiscovery = (responses, context) => { + assertDAVResponses(responses, context); + for (const response of responses) { + if (response.raw?.multistatus && !response.raw.multistatus.response) continue; + assertDAVProperty(response, "resourcetype", context); + if (!response.props || !hasOwn(response.props, "resourcetype") || typeof response.href !== "string" || !response.href) throw new Error(`${context}: missing resourcetype or href in DAV multistatus response`); + } +}; +const assertDAVProperty = (response, name, context) => { + const failed = response.propStats?.find((stat) => !stat.ok && hasOwn(stat.props, name)); + if (failed && !hasOwn(response.props ?? {}, name)) throw new Error(`${context}: ${name} returned ${failed.status} ${failed.statusText}`); +}; +const assertDAVObjectResponses = (responses, property, objectUrls, baseUrl, context) => { + assertDAVResponses(responses, context); + const fetchedUrls = /* @__PURE__ */ new Set(); + for (const response of responses) { + assertDAVProperty(response, property, context); + if (!response.href || typeof (response.props?.[property]?._cdata ?? response.props?.[property]) !== "string") throw new Error(`${context}: missing ${property} or href`); + fetchedUrls.add(getDAVUrlKey(response.href, baseUrl)); + } + const withoutQuery = (url) => getDAVUrlKey(url.replace(/\?.*$/, ""), baseUrl); + const countsByPath = /* @__PURE__ */ new Map(); + for (const url of objectUrls) { + const key = withoutQuery(url); + countsByPath.set(key, (countsByPath.get(key) ?? 0) + 1); + } + if (objectUrls.some((url) => !fetchedUrls.has(getDAVUrlKey(url, baseUrl)) && !(countsByPath.get(withoutQuery(url)) === 1 && fetchedUrls.has(withoutQuery(url))))) throw new Error(`${context}: incomplete response`); +}; +const getDAVText = (value) => { + const text = typeof value === "string" || typeof value === "number" ? String(value) : value?._cdata ?? value?._text; + return typeof text === "string" ? text : void 0; }; //#endregion //#region src/collection.ts @@ -435,13 +629,6 @@ const resolveDAVHref = (href, baseUrl) => { return href; } }; -const hrefHasExtension = (href, extension, baseUrl) => { - try { - return new URL(href, ensureTrailingSlash(baseUrl)).pathname.toLowerCase().endsWith(extension); - } catch { - return (href.split(/[?#]/, 1)[0] ?? "").toLowerCase().endsWith(extension); - } -}; const collectionQuery = async (params) => { const { url, body, depth, defaultNamespace = "d", headers, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const queryResults = await davRequest({ @@ -455,13 +642,16 @@ const collectionQuery = async (params) => { namespace: defaultNamespace, body }, + headersToExclude, fetchOptions, fetch: fetchOverride }); const emptyNotFound = queryResults[0]; if (defaultNamespace === "c" && body?.["calendar-query"] != null && queryResults.length === 1 && emptyNotFound && emptyNotFound.status === 404 && urlMatches(url, emptyNotFound.href, url) && !emptyNotFound.error && Object.keys(emptyNotFound.props ?? {}).length === 0 && typeof emptyNotFound.raw === "object" && emptyNotFound.raw !== null && emptyNotFound.raw.multistatus?.response?.propstat == null) return []; const errorResponse = queryResults.find((res) => !res.ok || res.status && res.status >= 400); - if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${errorResponse.raw ? `Raw response: ${errorResponse.raw}` : ""}`); + if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${typeof errorResponse.raw === "string" ? `Raw response: ${errorResponse.raw.slice(0, 4096)}` : ""}`); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => !response.raw?.multistatus)) throw new Error("Collection query failed: expected a DAV multistatus response"); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection query failed: missing href in DAV response"); const firstQueryResult = queryResults[0]; if (queryResults.length === 1 && firstQueryResult && (!firstQueryResult.raw || firstQueryResult.raw.multistatus && !firstQueryResult.raw.multistatus.response) && firstQueryResult.status && firstQueryResult.status < 300) return []; return queryResults; @@ -488,6 +678,7 @@ const makeCollection = async (params) => { set: { prop: props } } } : void 0 }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -499,6 +690,7 @@ const supportedReportSet = async (params) => { props: { [`d:supported-report-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]?.props?.supportedReportSet?.supportedReport; @@ -512,15 +704,17 @@ const isCollectionDirty = async (params) => { props: { [`cs:getctag`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(collection.url, r.href, collection.url)); if (!res) throw new Error("Collection does not exist on server"); - if (!res.ok) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); - const remoteCtag = res.props?.getctag; + const unavailableCtag = res.propStats?.length && res.propStats.every((stat) => stat.status === 404 && hasOwn(stat.props, "getctag")); + if (!res.ok && !unavailableCtag) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); + const remoteCtag = getDAVText(res.props?.getctag); return { isDirty: collection.ctag == null || remoteCtag == null || `${collection.ctag}` !== `${remoteCtag}`, - newCtag: remoteCtag?.toString() + newCtag: remoteCtag }; }; /** @@ -545,6 +739,7 @@ const syncCollection = (params) => { [`d:prop`]: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -570,15 +765,17 @@ const smartCollectionSync = async (params) => { syncLevel: 1, syncToken: collection.syncToken, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); const isObjectResponse = (r) => { - const extName = account.accountType === "caldav" ? ".ics" : ".vcf"; - return typeof r.href === "string" && hrefHasExtension(r.href, extName, collection.url); + return typeof r.href === "string" && getDAVUrlKey(r.href, collection.url) !== getDAVUrlKey(collection.url, collection.url) && !r.props?.resourcetype?.collection; }; - const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && isObjectResponse(r))); + const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && !r.propStats?.length && isObjectResponse(r))); if (errorResponse) throw new Error(`Collection sync failed: ${errorResponse.status} ${errorResponse.statusText}`); + if (result.some((response) => response.raw && !response.raw.multistatus)) throw new Error("Collection sync failed: expected a DAV multistatus response"); + if (result.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection sync failed: missing href in DAV response"); const objectResponses = result.filter(isObjectResponse); const changedObjectUrls = objectResponses.filter((o) => o.status !== 404).map((r) => r.href); const deletedObjectUrls = objectResponses.filter((o) => o.status === 404).map((r) => r.href); @@ -593,30 +790,24 @@ const smartCollectionSync = async (params) => { objectUrls: changedObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? [] : []; - const multiGetError = multiGetObjectResponse.find((r) => !r.ok || r.status >= 400); - if (multiGetError) throw new Error(`Collection sync multi-get failed: ${multiGetError.status} ${multiGetError.statusText}`); + assertDAVObjectResponses(multiGetObjectResponse, account.accountType === "caldav" ? "calendarData" : "addressData", changedObjectUrls, collection.url, "Collection sync multi-get failed"); const remoteObjects = multiGetObjectResponse.map((res) => { return { url: resolveDAVHref(res.href ?? "", collection.url), - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: account?.accountType === "caldav" ? res.props?.calendarData?._cdata ?? res.props?.calendarData : res.props?.addressData?._cdata ?? res.props?.addressData }; }); const localObjects = collection.objects ?? []; - const created = remoteObjects.filter((o) => localObjects.every((lo) => !urlMatches(lo.url, o.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = deletedObjectUrls.map((o) => ({ - url: resolveDAVHref(o, collection.url), + const deletedObjects = deletedObjectUrls.map((url) => ({ + url: resolveDAVHref(url, collection.url), etag: "" })); - const unchanged = localObjects.filter((localObject) => deleted.every((deletedObject) => !urlMatches(localObject.url, deletedObject.url, collection.url)) && updated.every((updatedObject) => !urlMatches(localObject.url, updatedObject.url, collection.url))); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url, true, deletedObjects); return { ...collection, objects: detailedResult ? { @@ -628,13 +819,14 @@ const smartCollectionSync = async (params) => { ...created, ...updated ], - syncToken: result[0]?.raw?.multistatus?.syncToken ?? collection.syncToken + syncToken: getDAVText(result[0]?.raw?.multistatus?.syncToken) ?? collection.syncToken }; } if (syncMethod === "basic") { const { isDirty, newCtag } = await isCollectionDirty({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -651,17 +843,11 @@ const smartCollectionSync = async (params) => { const remoteObjects = await collection.fetchObjects({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? []; - const created = remoteObjects.filter((ro) => localObjects.every((lo) => !urlMatches(lo.url, ro.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = localObjects.filter((cal) => remoteObjects.every((ro) => !urlMatches(ro.url, cal.url, collection.url))); - const unchanged = localObjects.filter((lo) => remoteObjects.some((ro) => urlMatches(lo.url, ro.url, collection.url) && ro.etag === lo.etag)); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url); return { ...collection, objects: detailedResult ? { @@ -713,6 +899,7 @@ const addressBookQuery = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -729,6 +916,7 @@ const addressBookMultiGet = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -742,33 +930,39 @@ const fetchAddressBooks = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`d:displayname`]: {}, - [`cs:getctag`]: {}, - [`d:resourcetype`]: {}, - [`d:sync-token`]: {} + props: { + ...customProps ?? { + [`d:displayname`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`d:sync-token`]: {} + }, + [`d:resourcetype`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Address book discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("addressbook")).map((rs) => { const displayName = rs.props?.displayname?._cdata ?? rs.props?.displayname; debug$3(`Found address book named ${typeof displayName === "string" ? displayName : ""}, props: ${JSON.stringify(rs.props)}`); return { url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), displayName: typeof displayName === "string" ? displayName : "", resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken + syncToken: getDAVText(rs.props?.syncToken) }; }).map(async (addr) => ({ ...addr, reports: await supportedReportSet({ collection: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -787,12 +981,14 @@ const fetchVCards = async (params) => { props: { [`d:getetag`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).map((res) => res.href ?? "")).filter((url) => typeof url === "string" && url.trim().length > 0).map((url) => url.startsWith("http") ? url : new URL(url, ensureTrailingSlash(addressBook.url)).href).filter((url) => !urlEquals(url, addressBook.url)).filter(urlFilter).map((url) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(vcardUrls.map((url) => getDAVUrlKey(url, addressBook.url))); let vCardResults = []; if (vcardUrls.length > 0) { if (useMultiGet) vCardResults = await addressBookMultiGet({ @@ -804,24 +1000,30 @@ const fetchVCards = async (params) => { objectUrls: vcardUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - else vCardResults = await addressBookQuery({ - url: addressBook.url, - props: { - [`d:getetag`]: {}, - [`card:address-data`]: {} - }, - depth: "1", - headers: excludeHeaders(headers, headersToExclude), - fetchOptions, - fetch: fetchOverride - }); + else { + vCardResults = await addressBookQuery({ + url: addressBook.url, + props: { + [`d:getetag`]: {}, + [`card:address-data`]: {} + }, + depth: "1", + headers: excludeHeaders(headers, headersToExclude), + headersToExclude, + fetchOptions, + fetch: fetchOverride + }); + vCardResults = vCardResults.filter((res) => !!res.href && targetUrls.has(getDAVUrlKey(res.href, addressBook.url))); + } } + assertDAVObjectResponses(vCardResults, "addressData", vcardUrls, addressBook.url, "VCard fetch failed"); return vCardResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(addressBook.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.addressData?._cdata ?? res.props?.addressData })); }; @@ -835,6 +1037,7 @@ const createVCard = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -849,6 +1052,7 @@ const updateVCard = async (params) => { "content-type": "text/vcard; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -859,6 +1063,7 @@ const deleteVCard = async (params) => { url: vCard.url, etag: vCard.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -908,6 +1113,7 @@ const fetchCalendarUserAddresses = async (params) => { props: { [`c:calendar-user-address-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(account.principalUrl, r.href, account.rootUrl)); @@ -938,6 +1144,7 @@ const calendarQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -956,6 +1163,7 @@ const calendarMultiGet = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -980,6 +1188,7 @@ const makeCalendar = async (params) => { set: { prop: props } } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -993,21 +1202,27 @@ const fetchCalendars = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`c:calendar-description`]: {}, - [`c:calendar-timezone`]: {}, - [`d:displayname`]: {}, - [`ca:calendar-color`]: {}, - [`cs:getctag`]: {}, + props: { + ...customProps ?? { + [`c:calendar-description`]: {}, + [`c:calendar-timezone`]: {}, + [`d:displayname`]: {}, + [`ca:calendar-color`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`c:supported-calendar-component-set`]: {}, + [`d:sync-token`]: {} + }, [`d:resourcetype`]: {}, - [`c:supported-calendar-component-set`]: {}, - [`d:sync-token`]: {} + [`c:supported-calendar-component-set`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Calendar discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("calendar")).filter((rc) => { const components = extractComponentNames(rc.props?.supportedCalendarComponentSet?.comp); return components.length === 0 || components.some((c) => Object.values(ICALObjects).includes(c)); @@ -1020,12 +1235,12 @@ const fetchCalendars = async (params) => { description: typeof description === "string" ? description : "", timezone: typeof timezone === "string" ? timezone : "", url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), calendarColor: rs.props?.calendarColor, - displayName: rs.props?.displayname?._cdata ?? rs.props?.displayname, + displayName: getDAVText(rs.props?.displayname), components: extractComponentNames(compSet), resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken, + syncToken: getDAVText(rs.props?.syncToken), ...projectedProps && projectedEntries.length > 0 ? { projectedProps: Object.fromEntries(projectedEntries) } : {} }; }).map(async (cal) => ({ @@ -1033,6 +1248,7 @@ const fetchCalendars = async (params) => { reports: await supportedReportSet({ collection: cal, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1040,6 +1256,7 @@ const fetchCalendars = async (params) => { }; const fetchCalendarObjects = async (params) => { const { calendar, objectUrls, filters: customFilters, timeRange, headers, expand, urlFilter = (url) => Boolean(url?.includes(".ics")), useMultiGet = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; + if (expand && !timeRange) throw new Error("timeRange is required when expand is true"); if (timeRange) validateTimeRange(timeRange); debug$2(`Fetching calendar objects from ${calendar?.url}`); const requiredFields = ["url"]; @@ -1070,6 +1287,7 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1077,6 +1295,7 @@ const fetchCalendarObjects = async (params) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(calendarObjectUrls.map((url) => getDAVUrlKey(url, calendar.url))); let calendarObjectResults = []; if (calendarObjectUrls.length > 0) { if (expand && !objectUrls) calendarObjectResults = initialResponses.filter((res) => { @@ -1096,12 +1315,13 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - if (objectUrls && objectUrls.length > 0) calendarObjectResults = calendarObjectResults.filter((res) => { + calendarObjectResults = calendarObjectResults.filter((res) => { const fullUrl = (res.href ?? "").startsWith("http") ? res.href ?? "" : new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href; - return calendarObjectUrls.some((target) => urlMatches(fullUrl, target, calendar.url)); + return targetUrls.has(getDAVUrlKey(fullUrl, calendar.url)); }); } else calendarObjectResults = await calendarMultiGet({ url: calendar.url, @@ -1115,13 +1335,15 @@ const fetchCalendarObjects = async (params) => { objectUrls: calendarObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); } + assertDAVObjectResponses(calendarObjectResults, "calendarData", calendarObjectUrls, calendar.url, "Calendar object fetch failed"); return calendarObjectResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.calendarData?._cdata ?? res.props?.calendarData })); }; @@ -1135,6 +1357,7 @@ const createCalendarObject = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1149,6 +1372,7 @@ const updateCalendarObject = async (params) => { "content-type": "text/calendar; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1159,6 +1383,7 @@ const deleteCalendarObject = async (params) => { url: calendarObject.url, etag: calendarObject.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1173,19 +1398,26 @@ const syncCalendars = async (params) => { const remoteCalendars = await fetchCalendars({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - const created = remoteCalendars.filter((rc) => localCalendars.every((lc) => !urlMatches(lc.url, rc.url, account.rootUrl))); - debug$2(`new calendars: ${created.map((cc) => cc.displayName)}`); - const updated = localCalendars.reduce((prev, curr) => { - const found = remoteCalendars.find((rc) => urlMatches(rc.url, curr.url, account.rootUrl)); - if (found && (found.syncToken && `${found.syncToken}` !== `${curr.syncToken}` || found.ctag && `${found.ctag}` !== `${curr.ctag}`)) return [...prev, { - local: curr, - remote: found - }]; - return prev; - }, []); + const baseUrl = account.rootUrl ?? account.homeUrl ?? account.serverUrl; + const localByUrl = new Map(localCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const remoteByUrl = new Map(remoteCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const created = remoteCalendars.filter((cal) => !localByUrl.has(getDAVUrlKey(cal.url, baseUrl))); + const updated = []; + const unchanged = []; + const deleted = []; + for (const local of localCalendars) { + const remote = remoteByUrl.get(getDAVUrlKey(local.url, baseUrl)); + if (!remote) deleted.push(local); + else if (!remote.syncToken && !remote.ctag || remote.syncToken && remote.syncToken !== local.syncToken || remote.ctag && remote.ctag !== local.ctag) updated.push({ + local, + remote + }); + else unchanged.push(local); + } debug$2(`updated calendars: ${updated.map(({ remote }) => remote.displayName)}`); const updatedWithObjects = await Promise.all(updated.map(async ({ local, remote }) => { const fetchObjects = async (fetchParams) => { @@ -1193,7 +1425,9 @@ const syncCalendars = async (params) => { const { collection, ...requestParams } = fetchParams; return fetchCalendarObjects({ ...requestParams, - calendar: collection + calendar: collection, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => getDAVUrlKey(url, collection.url) !== getDAVUrlKey(collection.url, collection.url) }); }; const collection = { @@ -1208,25 +1442,17 @@ const syncCalendars = async (params) => { collection, detailedResult: false, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account, fetchOptions, fetch: fetchOverride }); return { ...result, - ctag: remote.ctag ?? result.ctag, - syncToken: remote.syncToken ?? result.syncToken + ctag: remote.reports?.includes("syncCollection") ? remote.ctag ?? result.ctag : result.ctag ?? remote.ctag, + syncToken: remote.reports?.includes("syncCollection") ? result.syncToken : remote.syncToken ?? result.syncToken }; })); - const deleted = localCalendars.filter((cal) => remoteCalendars.every((rc) => !urlMatches(rc.url, cal.url, account.rootUrl))); - debug$2(`deleted calendars: ${deleted.map((cc) => cc.displayName)}`); - const unchanged = localCalendars.filter((cal) => remoteCalendars.some((rc) => { - if (!urlMatches(rc.url, cal.url, account.rootUrl)) return false; - const syncTokenMatches = !rc.syncToken || `${rc.syncToken}` === `${cal.syncToken}`; - const ctagMatches = !rc.ctag || `${rc.ctag}` === `${cal.ctag}`; - return syncTokenMatches && ctagMatches; - })); - debug$2(`unchanged calendars: ${unchanged.map((cc) => cc.displayName)}`); return detailedResult ? { created, updated: updatedWithObjects, @@ -1257,6 +1483,7 @@ const freeBusyQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]; @@ -1293,11 +1520,7 @@ const serviceDiscovery = async (params) => { const location = response.headers.get("Location"); if (typeof location === "string" && location.length) { debug$1(`Service discovery redirected to ${location}`); - const hasExplicitScheme = /^[a-z][a-z0-9+.-]*:/i.test(location); - const serviceURL = new URL(location, endpoint); - if (serviceURL.hostname === uri.hostname && uri.port && !serviceURL.port) serviceURL.port = uri.port; - if (!hasExplicitScheme) serviceURL.protocol = endpoint.protocol ?? "http"; - return serviceURL.href; + return new URL(location, uri).href; } } }; @@ -1322,6 +1545,7 @@ const serviceDiscovery = async (params) => { const redirectUrl = extractRedirect(await requestFetch(uri.href, { ...fetchOptionsWithoutHeaders, method: "GET", + body: void 0, headers: excludeHeaders(mergeHeaders(headers, fetchHeaders), headersToExclude), redirect: "manual" })); @@ -1361,6 +1585,7 @@ const fetchPrincipalUrl = async (params) => { props: { [`d:current-user-principal`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1387,6 +1612,7 @@ const fetchHomeUrl = async (params) => { props: account.accountType === "caldav" ? { [`c:calendar-home-set`]: {} } : { [`card:addressbook-home-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1410,6 +1636,7 @@ const createAccount = async (params) => { const discoveredRootUrl = account.rootUrl ?? await serviceDiscovery({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1428,6 +1655,7 @@ const createAccount = async (params) => { rootUrl }, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1443,24 +1671,28 @@ const createAccount = async (params) => { newAccount.principalUrl = account.principalUrl ?? newAccount.principalUrl ?? await fetchPrincipalUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); newAccount.homeUrl = account.homeUrl ?? await fetchHomeUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); if (loadCollections || loadObjects) { if (account.accountType === "caldav") newAccount.calendars = await fetchCalendars({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride }); else if (account.accountType === "carddav") newAccount.addressBooks = await fetchAddressBooks({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride @@ -1471,7 +1703,10 @@ const createAccount = async (params) => { ...cal, objects: await fetchCalendarObjects({ calendar: cal, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => !urlEquals(url, cal.url), headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1481,6 +1716,7 @@ const createAccount = async (params) => { objects: await fetchVCards({ addressBook: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1631,20 +1867,18 @@ const getOauthHeaders = async (credentials, fetchOptions, fetchOverride) => { debug("Fetching oauth headers"); let tokens = {}; let didRefresh = false; - if (!credentials.refreshToken) { - tokens = await fetchOauthTokens(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else if (credentials.refreshToken && !credentials.accessToken || Date.now() > (credentials.expiration ?? 0)) { - tokens = await refreshAccessToken(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else tokens = { + if (credentials.accessToken && (credentials.expiration == null && !credentials.refreshToken || credentials.expiration != null && Date.now() < credentials.expiration)) tokens = { access_token: credentials.accessToken, refresh_token: credentials.refreshToken }; + else { + tokens = credentials.refreshToken ? await refreshAccessToken(credentials, fetchOptions, fetchOverride) : await fetchOauthTokens(credentials, fetchOptions, fetchOverride); + didRefresh = true; + } if (didRefresh) { if (tokens.access_token) credentials.accessToken = tokens.access_token; if (tokens.refresh_token) credentials.refreshToken = tokens.refresh_token; - if (typeof tokens.expires_in === "number") credentials.expiration = Date.now() + tokens.expires_in * 1e3; + if (tokens.access_token) credentials.expiration = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0; } debug("Oauth tokens obtained"); return { @@ -1658,145 +1892,71 @@ var client_exports = /* @__PURE__ */ __exportAll({ DAVClient: () => DAVClient, createDAVClient: () => createDAVClient }); -const createDAVClient = async (params) => { - const { serverUrl, credentials, authMethod = "Basic", defaultAccountType, authFunction, fetchOptions: defaultFetchOptions, fetch: fetchOverride } = params; - let authHeaders = {}; - switch (authMethod) { - case "Basic": - authHeaders = getBasicAuthHeaders(credentials); - break; - case "Bearer": - authHeaders = getBearerAuthHeaders(credentials); - break; - case "Oauth": - authHeaders = (await getOauthHeaders(credentials, defaultFetchOptions, fetchOverride)).headers; - break; - case "Digest": - authHeaders = { Authorization: `Digest ${credentials.digestString}` }; - break; +const resolveAuthHeaders = async (client, fetchOptions = client.fetchOptions, fetchOverride = client.fetchOverride) => { + switch (client.authMethod) { + case "Basic": return getBasicAuthHeaders(client.credentials); + case "Bearer": return getBearerAuthHeaders(client.credentials); + case "Oauth": { + const { headers } = await getOauthHeaders(client.credentials, fetchOptions, fetchOverride); + if (!headers.authorization) throw new Error("OAuth authentication failed: token endpoint returned no access token"); + return headers; + } + case "Digest": return { Authorization: `Digest ${client.credentials.digestString}` }; case "Custom": - if (!authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - authHeaders = await authFunction(credentials) ?? {}; - break; + if (!client.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); + return await client.authFunction(client.credentials) ?? {}; default: throw new Error("Invalid auth method"); } - const defaultAccount = defaultAccountType ? await createAccount({ +}; +const createDAVClient = async (params) => { + const client = new DAVClient(params); + client.authHeaders = await resolveAuthHeaders(client); + client.account = params.defaultAccountType ? await createAccount({ account: { - serverUrl, - credentials, - accountType: defaultAccountType + serverUrl: params.serverUrl, + credentials: params.credentials, + accountType: params.defaultAccountType }, - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride + headers: client.authHeaders, + fetchOptions: client.fetchOptions, + fetch: client.fetchOverride }) : void 0; - const davRequest$1 = async (params0) => { - const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; - const { headers, ...restInit } = init; - return davRequest({ - ...rest, - init: { - ...restInit, - headers: { - ...authHeaders, - ...headers - } - }, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const commonDefaults = { - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride - }; - const commonDefaultsWithUrl = { - url: serverUrl, - ...commonDefaults - }; - const commonDefaultsWithAccount = { - account: defaultAccount, - ...commonDefaults - }; - const createObject$1 = defaultParam(createObject, commonDefaultsWithUrl); - const updateObject$1 = defaultParam(updateObject, commonDefaultsWithUrl); - const deleteObject$1 = defaultParam(deleteObject, commonDefaultsWithUrl); - const propfind$1 = defaultParam(propfind, commonDefaults); - const createAccount$1 = async (params0) => { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch: fetchOverride2 } = params0; - const merged = { - serverUrl, - credentials, - ...account - }; - if (!merged.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client."); - return createAccount({ - account: merged, - headers: { - ...authHeaders, - ...headers - }, - loadCollections, - loadObjects, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const collectionQuery$1 = defaultParam(collectionQuery, commonDefaults); - const makeCollection$1 = defaultParam(makeCollection, commonDefaults); - const syncCollection$1 = defaultParam(syncCollection, commonDefaults); - const supportedReportSet$1 = defaultParam(supportedReportSet, commonDefaults); - const isCollectionDirty$1 = defaultParam(isCollectionDirty, commonDefaults); - const smartCollectionSync$1 = defaultParam(smartCollectionSync, commonDefaultsWithAccount); - const smartCollectionSyncDetailed$1 = defaultParam(smartCollectionSyncDetailed, commonDefaultsWithAccount); - const calendarQuery$1 = defaultParam(calendarQuery, commonDefaults); - const calendarMultiGet$1 = defaultParam(calendarMultiGet, commonDefaults); - const makeCalendar$1 = defaultParam(makeCalendar, commonDefaults); - const fetchCalendars$1 = defaultParam(fetchCalendars, commonDefaultsWithAccount); - const fetchCalendarUserAddresses$1 = defaultParam(fetchCalendarUserAddresses, commonDefaultsWithAccount); - const fetchCalendarObjects$1 = defaultParam(fetchCalendarObjects, commonDefaults); - const createCalendarObject$1 = defaultParam(createCalendarObject, commonDefaults); - const updateCalendarObject$1 = defaultParam(updateCalendarObject, commonDefaults); - const deleteCalendarObject$1 = defaultParam(deleteCalendarObject, commonDefaults); - const syncCalendars$1 = defaultParam(syncCalendars, commonDefaultsWithAccount); - const syncCalendarsDetailed$1 = defaultParam(syncCalendarsDetailed, commonDefaultsWithAccount); - const freeBusyQuery$1 = defaultParam(freeBusyQuery, commonDefaults); - const addressBookQuery$1 = defaultParam(addressBookQuery, commonDefaults); - const addressBookMultiGet$1 = defaultParam(addressBookMultiGet, commonDefaults); return { - davRequest: davRequest$1, - propfind: propfind$1, - createAccount: createAccount$1, - createObject: createObject$1, - updateObject: updateObject$1, - deleteObject: deleteObject$1, - calendarQuery: calendarQuery$1, - addressBookQuery: addressBookQuery$1, - collectionQuery: collectionQuery$1, - makeCollection: makeCollection$1, - calendarMultiGet: calendarMultiGet$1, - makeCalendar: makeCalendar$1, - freeBusyQuery: freeBusyQuery$1, - syncCollection: syncCollection$1, - supportedReportSet: supportedReportSet$1, - isCollectionDirty: isCollectionDirty$1, - smartCollectionSync: smartCollectionSync$1, - smartCollectionSyncDetailed: smartCollectionSyncDetailed$1, - fetchCalendars: fetchCalendars$1, - fetchCalendarUserAddresses: fetchCalendarUserAddresses$1, - fetchCalendarObjects: fetchCalendarObjects$1, - createCalendarObject: createCalendarObject$1, - updateCalendarObject: updateCalendarObject$1, - deleteCalendarObject: deleteCalendarObject$1, - syncCalendars: syncCalendars$1, - syncCalendarsDetailed: syncCalendarsDetailed$1, - fetchAddressBooks: defaultParam(fetchAddressBooks, commonDefaultsWithAccount), - addressBookMultiGet: addressBookMultiGet$1, - fetchVCards: defaultParam(fetchVCards, commonDefaults), - createVCard: defaultParam(createVCard, commonDefaults), - updateVCard: defaultParam(updateVCard, commonDefaults), - deleteVCard: defaultParam(deleteVCard, commonDefaults) + davRequest: client.davRequest.bind(client), + propfind: client.propfind.bind(client), + createAccount: async (...args) => { + if (!args[0].account.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType`."); + return client.createAccount(...args); + }, + createObject: client.createObject.bind(client), + updateObject: client.updateObject.bind(client), + deleteObject: client.deleteObject.bind(client), + calendarQuery: client.calendarQuery.bind(client), + addressBookQuery: client.addressBookQuery.bind(client), + collectionQuery: client.collectionQuery.bind(client), + makeCollection: client.makeCollection.bind(client), + calendarMultiGet: client.calendarMultiGet.bind(client), + makeCalendar: client.makeCalendar.bind(client), + freeBusyQuery: client.freeBusyQuery.bind(client), + syncCollection: client.syncCollection.bind(client), + supportedReportSet: client.supportedReportSet.bind(client), + isCollectionDirty: client.isCollectionDirty.bind(client), + smartCollectionSync: client.smartCollectionSync.bind(client), + smartCollectionSyncDetailed: client.smartCollectionSyncDetailed.bind(client), + fetchCalendars: client.fetchCalendars.bind(client), + fetchCalendarUserAddresses: client.fetchCalendarUserAddresses.bind(client), + fetchCalendarObjects: client.fetchCalendarObjects.bind(client), + createCalendarObject: client.createCalendarObject.bind(client), + updateCalendarObject: client.updateCalendarObject.bind(client), + deleteCalendarObject: client.deleteCalendarObject.bind(client), + syncCalendars: client.syncCalendars.bind(client), + syncCalendarsDetailed: client.syncCalendarsDetailed.bind(client), + fetchAddressBooks: client.fetchAddressBooks.bind(client), + addressBookMultiGet: client.addressBookMultiGet.bind(client), + fetchVCards: client.fetchVCards.bind(client), + createVCard: client.createVCard.bind(client), + updateVCard: client.updateVCard.bind(client), + deleteVCard: client.deleteVCard.bind(client) }; }; var DAVClient = class { @@ -1808,27 +1968,42 @@ var DAVClient = class { this.authFunction = params.authFunction; this.fetchOptions = params.fetchOptions ?? {}; this.fetchOverride = params.fetch; + this.calendarMultiGet = this.calendarMultiGet.bind(this); + this.addressBookMultiGet = this.addressBookMultiGet.bind(this); + } + async authenticate(force = false, fetchOptions = this.fetchOptions, fetchOverride = this.fetchOverride) { + if (!force && this.authMethod !== "Oauth") return; + if (!force && this.authHeaders && this.credentials.accessToken && (this.credentials.expiration == null || Date.now() < this.credentials.expiration)) { + this.authHeaders = { authorization: `Bearer ${this.credentials.accessToken}` }; + return; + } + if (this.authentication) return this.authentication; + const authenticate = async () => { + this.authHeaders = await resolveAuthHeaders(this, fetchOptions, fetchOverride); + }; + this.authentication = authenticate(); + try { + await this.authentication; + } finally { + this.authentication = void 0; + } + } + async requestDefaults(params) { + await this.authenticate(false, params?.fetchOptions ?? this.fetchOptions, params?.fetch ?? this.fetchOverride); + return { + url: this.serverUrl, + headers: this.authHeaders, + account: this.account, + fetchOptions: this.fetchOptions, + fetch: this.fetchOverride + }; + } + async invoke(fn, params) { + const defaults = await this.requestDefaults(params); + return await defaultParam(fn, defaults)(...[params]); } async login(options) { - switch (this.authMethod) { - case "Basic": - this.authHeaders = getBasicAuthHeaders(this.credentials); - break; - case "Bearer": - this.authHeaders = getBearerAuthHeaders(this.credentials); - break; - case "Oauth": - this.authHeaders = (await getOauthHeaders(this.credentials, this.fetchOptions, this.fetchOverride)).headers; - break; - case "Digest": - this.authHeaders = { Authorization: `Digest ${this.credentials.digestString}` }; - break; - case "Custom": - if (!this.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - this.authHeaders = await this.authFunction(this.credentials); - break; - default: throw new Error("Invalid auth method"); - } + await this.authenticate(true); this.account = this.accountType ? await createAccount({ account: { serverUrl: this.serverUrl, @@ -1845,52 +2020,32 @@ var DAVClient = class { async davRequest(params0) { const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; const { headers, ...restInit } = init; + const defaults = await this.requestDefaults(params0); return davRequest({ ...rest, init: { ...restInit, - headers: { - ...this.authHeaders, - ...headers - } + headers: mergeHeaders(defaults.headers, headers) }, fetchOptions: fetchOptions ?? this.fetchOptions, fetch: fetchOverride2 ?? this.fetchOverride }); } async createObject(...params) { - return defaultParam(createObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createObject, params[0]); } async updateObject(...params) { - return defaultParam(updateObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateObject, params[0]); } async deleteObject(...params) { - return defaultParam(deleteObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteObject, params[0]); } async propfind(...params) { - return defaultParam(propfind, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(propfind, params[0]); } async createAccount(params0) { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const { account, headers, headersToExclude, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const defaults = await this.requestDefaults(params0); const accountType = account.accountType ?? this.accountType; if (!accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient."); return createAccount({ @@ -1900,10 +2055,8 @@ var DAVClient = class { ...account, accountType }, - headers: { - ...this.authHeaders, - ...headers - }, + headers: mergeHeaders(defaults.headers, headers), + headersToExclude, loadCollections, loadObjects, fetchOptions: fetchOptions ?? this.fetchOptions, @@ -1911,193 +2064,82 @@ var DAVClient = class { }); } async collectionQuery(...params) { - return defaultParam(collectionQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(collectionQuery, params[0]); } async makeCollection(...params) { - return defaultParam(makeCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCollection, params[0]); } async syncCollection(...params) { - return defaultParam(syncCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCollection, params[0]); } async supportedReportSet(...params) { - return defaultParam(supportedReportSet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(supportedReportSet, params[0]); } async isCollectionDirty(...params) { - return defaultParam(isCollectionDirty, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(isCollectionDirty, params[0]); } async smartCollectionSync(...params) { - return defaultParam(smartCollectionSync, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(params[0]); + return this.invoke(smartCollectionSync, params[0]); } async smartCollectionSyncDetailed(param) { - return defaultParam(smartCollectionSyncDetailed, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(param); + return this.invoke(smartCollectionSyncDetailed, param); } async calendarQuery(...params) { - return defaultParam(calendarQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarQuery, params[0]); } async makeCalendar(...params) { - return defaultParam(makeCalendar, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCalendar, params[0]); } async freeBusyQuery(...params) { - return defaultParam(freeBusyQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(freeBusyQuery, params[0]); } async calendarMultiGet(...params) { - return defaultParam(calendarMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarMultiGet, params[0]); } async fetchCalendars(...params) { - return defaultParam(fetchCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendars, params[0]); } async fetchCalendarUserAddresses(...params) { - return defaultParam(fetchCalendarUserAddresses, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendarUserAddresses, params[0]); } async fetchCalendarObjects(...params) { - return defaultParam(fetchCalendarObjects, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchCalendarObjects, params[0]); } async createCalendarObject(...params) { - return defaultParam(createCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createCalendarObject, params[0]); } async updateCalendarObject(...params) { - return defaultParam(updateCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateCalendarObject, params[0]); } async deleteCalendarObject(...params) { - return defaultParam(deleteCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteCalendarObject, params[0]); } async syncCalendars(...params) { - return defaultParam(syncCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendars, params[0]); } async syncCalendarsDetailed(...params) { - return defaultParam(syncCalendarsDetailed, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendarsDetailed, params[0]); } async addressBookQuery(...params) { - return defaultParam(addressBookQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookQuery, params[0]); } async addressBookMultiGet(...params) { - return defaultParam(addressBookMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookMultiGet, params[0]); } async fetchAddressBooks(...params) { - return defaultParam(fetchAddressBooks, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchAddressBooks, params[0]); } async fetchVCards(...params) { - return defaultParam(fetchVCards, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchVCards, params[0]); } async createVCard(...params) { - return defaultParam(createVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createVCard, params[0]); } async updateVCard(...params) { - return defaultParam(updateVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateVCard, params[0]); } async deleteVCard(...params) { - return defaultParam(deleteVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteVCard, params[0]); } }; //#endregion @@ -2117,4 +2159,4 @@ var src_default = { ...requestHelpers_exports }; //#endregion -export { DAVAttributeMap, DAVClient, DAVNamespace, DAVNamespaceShort, ICALObjects, addressBookMultiGet, addressBookQuery, calendarMultiGet, calendarQuery, cleanupFalsy, collectionQuery, createAccount, createCalendarObject, createDAVClient, createObject, createVCard, davRequest, src_default as default, deleteCalendarObject, deleteObject, deleteVCard, ensureTrailingSlash, excludeHeaders, fetchAddressBooks, fetchCalendarObjects, fetchCalendarUserAddresses, fetchCalendars, fetchHomeUrl, fetchOauthTokens, fetchPrincipalUrl, fetchVCards, freeBusyQuery, getBasicAuthHeaders, getBearerAuthHeaders, getDAVAttribute, getOauthHeaders, isCollectionDirty, makeCalendar, mergeHeaders, propfind, refreshAccessToken, serviceDiscovery, smartCollectionSync, smartCollectionSyncDetailed, supportedReportSet, syncCalendars, syncCalendarsDetailed, syncCollection, updateCalendarObject, updateObject, updateVCard, urlContains, urlEquals, urlMatches }; +export { DAVAttributeMap, DAVClient, DAVNamespace, DAVNamespaceShort, ICALObjects, addressBookMultiGet, addressBookQuery, calendarMultiGet, calendarQuery, cleanupFalsy, collectionQuery, createAccount, createCalendarObject, createDAVClient, createObject, createVCard, davRequest, src_default as default, deleteCalendarObject, deleteObject, deleteVCard, ensureTrailingSlash, excludeHeaders, fetchAddressBooks, fetchCalendarObjects, fetchCalendarUserAddresses, fetchCalendars, fetchHomeUrl, fetchOauthTokens, fetchPrincipalUrl, fetchVCards, freeBusyQuery, getBasicAuthHeaders, getBearerAuthHeaders, getDAVAttribute, getOauthHeaders, isCollectionDirty, makeCalendar, makeCollection, mergeHeaders, propfind, refreshAccessToken, serviceDiscovery, smartCollectionSync, smartCollectionSyncDetailed, supportedReportSet, syncCalendars, syncCalendarsDetailed, syncCollection, updateCalendarObject, updateObject, updateVCard, urlContains, urlEquals, urlMatches }; diff --git a/dist/tsdav.js b/dist/tsdav.js index 8c9b3340..209707a1 100644 --- a/dist/tsdav.js +++ b/dist/tsdav.js @@ -2464,7 +2464,7 @@ var require_json2xml = /* @__PURE__ */ __commonJSMin(((exports, module) => { }; })); //#endregion -//#region src/util/camelCase.ts +//#region src/util/fetch.ts var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => { module.exports = { xml2js: require_xml2js(), @@ -2473,9 +2473,6 @@ var import_lib = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export json2xml: require_json2xml() }; })))()); -const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); -//#endregion -//#region src/util/fetch.ts /** * Resolve the runtime `fetch` implementation. * @@ -2500,18 +2497,175 @@ const resolveFetch = () => { }; const fetch = resolveFetch(); //#endregion -//#region src/util/nativeType.ts -const NUMERIC_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; -const nativeType = (value) => { - if (typeof value !== "string") return value; - if (NUMERIC_RE.test(value)) { - const nValue = Number(value); - if (!Number.isNaN(nValue) && Number.isFinite(nValue) && (!Number.isInteger(nValue) || Number.isSafeInteger(nValue))) return nValue; +//#region src/util/camelCase.ts +const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); +//#endregion +//#region src/util/typeHelpers.ts +function hasFields(obj, fields) { + if (!obj) return false; + const inObj = (object) => object != null && fields.every((f) => object[f]); + if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); + return inObj(obj); +} +const findMissingFieldNames = (obj, fields) => { + if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); + return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +}; +const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key); +//#endregion +//#region src/util/xml.ts +const davNamespaces = new Set(Object.values(DAVNamespace)); +const standardNamespaces = new Map([ + ...[ + "multistatus", + "response", + "propstat", + "prop", + "href", + "status", + "error", + "responsedescription", + "resourcetype", + "collection", + "getetag", + "displayname", + "syncToken", + "supportedReportSet", + "supportedReport", + "report", + "currentUserPrincipal" + ].map((name) => [name, "DAV:"]), + ...[ + "calendarData", + "calendar", + "calendarDescription", + "calendarTimezone", + "calendarHomeSet", + "calendarUserAddressSet", + "supportedCalendarComponentSet", + "comp" + ].map((name) => [name, "urn:ietf:params:xml:ns:caldav"]), + ...[ + "addressData", + "addressbook", + "addressbookHomeSet" + ].map((name) => [name, "urn:ietf:params:xml:ns:carddav"]), + ["getctag", "http://calendarserver.org/ns/"], + ["calendarColor", "http://apple.com/ns/ical/"] +]); +const normalizedKey = (name, namespace) => { + const localName = camelCase(name.replace(/^.*:/, "")); + const standardNamespace = standardNamespaces.get(localName); + return namespace && standardNamespace && namespace !== standardNamespace ? `{${namespace}}${localName}` : localName; +}; +const normalizeElement = (element, inheritedNamespaces) => { + const namespaceContext = Object.assign(Object.create(null), inheritedNamespaces); + const attributes = Object.create(null); + for (const [name, value] of Object.entries(element.attributes ?? {})) { + if (name === "xmlns" || name.startsWith("xmlns:")) namespaceContext[name === "xmlns" ? "" : name.slice(6)] = String(value); + if (name !== "xmlns") attributes[name] = value; + } + const name = element.name ?? ""; + const separator = name.indexOf(":"); + const namespace = namespaceContext[separator === -1 ? "" : name.slice(0, separator)] ?? ""; + const children = element.elements ?? []; + const elements = children.filter((child) => child.type === "element"); + const text = children.filter((child) => child.type === "text" || child.type === "cdata").map((child) => String(child.text ?? child.cdata ?? "")).join(""); + const value = {}; + const namespaces = Object.create(null); + if (Object.keys(attributes).length) value._attributes = attributes; + if (!elements.length) { + const hasText = children.some((child) => child.type === "text"); + const hasCdata = children.some((child) => child.type === "cdata"); + if (hasText) return { + value: text, + namespace, + namespaces + }; + if (hasCdata) value._cdata = text; + return { + value, + namespace, + namespaces + }; + } + if (text.trim()) value._text = text; + for (const child of elements) { + const normalized = normalizeElement(child, namespaceContext); + const localName = camelCase((child.name ?? "").replace(/^.*:/, "")); + let key = normalizedKey(child.name ?? "", normalized.namespace); + const previousNamespace = namespaces[key]; + if (hasOwn(value, key) && previousNamespace !== normalized.namespace) { + if (davNamespaces.has(normalized.namespace) && !davNamespaces.has(previousNamespace)) { + const previousKey = `{${previousNamespace}}${localName}`; + Object.defineProperty(value, previousKey, { + value: value[key], + enumerable: true, + configurable: true, + writable: true + }); + namespaces[previousKey] = previousNamespace; + delete value[key]; + } else key = `{${normalized.namespace}}${localName}`; + } + if (hasOwn(value, key)) { + if (Array.isArray(value[key])) value[key].push(normalized.value); + else value[key] = [value[key], normalized.value]; + } else Object.defineProperty(value, key, { + value: normalized.value, + enumerable: true, + configurable: true, + writable: true + }); + namespaces[key] = normalized.namespace; + if (localName === "prop" && normalized.namespace === "DAV:") value.propNamespaces = normalized.namespaces; + } + return { + value, + namespace, + namespaces + }; +}; +/** Preserve XML text order before normalizing DAV names to the existing compact shape. */ +const parseDAVXML = (xml) => { + const document = import_lib.default.xml2js(xml, { + compact: false, + ignoreDeclaration: true + }); + const result = {}; + for (const element of document.elements ?? []) { + if (element.type !== "element") continue; + const normalized = normalizeElement(element, {}); + const key = normalizedKey(element.name ?? "", normalized.namespace); + Object.defineProperty(result, key, { + value: normalized.value, + enumerable: true + }); } - const bValue = value.toLowerCase(); - if (bValue === "true") return true; - if (bValue === "false") return false; - return value; + return result; +}; +/** Keep same-name properties from distinct namespaces across propstat groups. */ +const mergeDAVProps = (propStats) => { + const groups = /* @__PURE__ */ new Map(); + for (const stat of propStats) { + if (!stat.ok) continue; + for (const [name, value] of Object.entries(stat.props)) { + const namespaces = groups.get(name) ?? /* @__PURE__ */ new Map(); + namespaces.set(stat.namespaces?.[name] ?? "", value); + groups.set(name, namespaces); + } + } + const props = {}; + for (const [name, namespaces] of groups) { + const primary = [...namespaces.keys()].find((namespace) => davNamespaces.has(namespace)) ?? namespaces.keys().next().value; + for (const [namespace, value] of namespaces) Object.defineProperty(props, namespace === primary ? name : `{${namespace}}${name}`, { + value, + enumerable: true, + configurable: true, + writable: true + }); + } + return props; }; //#endregion //#region src/util/requestHelpers.ts @@ -2631,7 +2785,7 @@ var request_exports = /* @__PURE__ */ __exportAll({ }); const debug$5 = (0, import_browser.default)("tsdav:request"); const parseStatusLine = (statusLine) => { - const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine ?? ""); + const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine?.trim() ?? ""); const status = match?.groups?.status; const statusText = match?.groups?.statusText; return status ? { @@ -2640,7 +2794,7 @@ const parseStatusLine = (statusLine) => { } : void 0; }; const davRequest = async (params) => { - const { url, init, convertIncoming = true, parseOutgoing = true, fetchOptions = {}, fetch: fetchOverride } = params; + const { url, init, convertIncoming = true, parseOutgoing = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const requestFetch = fetchOverride ?? fetch; const { headers = {}, body, namespace, method, attributes } = init; let processedBody = body; @@ -2673,7 +2827,7 @@ const davRequest = async (params) => { }) : body; const fetchOptionsWithoutHeaders = { ...fetchOptions }; delete fetchOptionsWithoutHeaders.headers; - const mergedHeaders = mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers); + const mergedHeaders = excludeHeaders(mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers), headersToExclude); const davResponse = await requestFetch(url, { ...fetchOptionsWithoutHeaders, headers: mergedHeaders, @@ -2681,54 +2835,25 @@ const davRequest = async (params) => { method }); const resText = await davResponse.text(); - if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) { - const MAX_RAW = 4096; - const raw = resText.length > MAX_RAW ? `${resText.slice(0, MAX_RAW)}…` : resText; - return [{ - href: davResponse.url, - ok: davResponse.ok, - status: davResponse.status, - statusText: davResponse.statusText, - raw - }]; - } + if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) return [{ + href: davResponse.url, + ok: davResponse.ok, + status: davResponse.status, + statusText: davResponse.statusText, + raw: resText + }]; let result; try { - result = import_lib.default.xml2js(resText, { - compact: true, - trim: true, - textFn: (value, parentElement) => { - try { - const parentOfParent = parentElement._parent; - const pOpKeys = Object.keys(parentOfParent); - const keyName = pOpKeys[pOpKeys.length - 1]; - if (!keyName) return; - const arrOfKey = parentOfParent[keyName]; - if (arrOfKey.length > 0) { - const arr = arrOfKey; - const arrIndex = arrOfKey.length - 1; - arr[arrIndex] = nativeType(value); - } else parentOfParent[keyName] = nativeType(value); - } catch (e) { - debug$5(e.stack); - } - }, - elementNameFn: (attributeName) => camelCase(attributeName.replace(/^.+:/, "")), - attributesFn: (value) => { - const newVal = { ...value }; - delete newVal.xmlns; - return newVal; - }, - ignoreDeclaration: true - }); + result = parseDAVXML(resText); } catch (e) { debug$5(`Failed to parse DAV response XML: ${e.message}`); return [{ href: davResponse.url, - ok: davResponse.ok, + ok: false, status: davResponse.status, statusText: davResponse.statusText, - raw: resText + raw: resText, + parseError: e.message }]; } if (!result?.multistatus) return [{ @@ -2745,7 +2870,21 @@ const davRequest = async (params) => { statusText: davResponse.statusText, ok: davResponse.ok }; - const parsedStatus = parseStatusLine(responseBody.status); + const propStats = (Array.isArray(responseBody.propstat) ? responseBody.propstat : responseBody.propstat ? [responseBody.propstat] : []).map((stat) => { + const parsed = parseStatusLine(stat.status); + const status = parsed?.status ?? 0; + return { + props: stat.prop ?? {}, + namespaces: stat.propNamespaces, + status, + statusText: parsed?.statusText ?? "Invalid DAV property status", + ok: status >= 200 && status < 300, + error: stat.error, + responsedescription: stat.responsedescription + }; + }); + const failedStatus = propStats.length > 0 && propStats.every((stat) => !stat.ok) ? propStats[0] : void 0; + const parsedStatus = parseStatusLine(responseBody.status) ?? failedStatus; const status = parsedStatus?.status ?? davResponse.status; return { raw: result, @@ -2755,14 +2894,8 @@ const davRequest = async (params) => { ok: status >= 200 && status < 300, error: responseBody.error, responsedescription: responseBody.responsedescription, - props: (Array.isArray(responseBody.propstat) ? responseBody.propstat : [responseBody.propstat]).reduce((prev, curr) => { - const propstatStatus = parseStatusLine(curr?.status)?.status; - if (propstatStatus && (propstatStatus < 200 || propstatStatus >= 300)) return prev; - return { - ...prev, - ...curr?.prop - }; - }, {}) + propStats, + props: mergeDAVProps(propStats) }; }); }; @@ -2788,6 +2921,7 @@ const propfind = async (params) => { prop: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -2831,16 +2965,76 @@ const deleteObject = async (params) => { }); }; //#endregion -//#region src/util/typeHelpers.ts -function hasFields(obj, fields) { - if (!obj) return false; - const inObj = (object) => object != null && fields.every((f) => object[f]); - if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); - return inObj(obj); -} -const findMissingFieldNames = (obj, fields) => { - if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); - return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +//#region src/util/syncHelpers.ts +/** Stable keys for linear-time DAV resource comparisons. */ +const getDAVUrlKey = (url, baseUrl) => { + let resolved; + try { + resolved = new URL(url, ensureTrailingSlash(baseUrl)).href; + } catch { + resolved = url.trim(); + } + return resolved.endsWith("/") ? resolved.slice(0, -1) : resolved; +}; +const diffDAVObjects = (local, remote, baseUrl, incremental = false, deletedObjects = []) => { + const localByUrl = new Map(local.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const remoteByUrl = new Map(remote.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const deleted = incremental ? deletedObjects : local.filter((object) => !remoteByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const deletedUrls = new Set(deleted.map((object) => getDAVUrlKey(object.url, baseUrl))); + const created = remote.filter((object) => !localByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const updated = []; + const unchanged = []; + for (const object of local) { + const key = getDAVUrlKey(object.url, baseUrl); + if (deletedUrls.has(key)) continue; + const found = remoteByUrl.get(key); + if (found && found.etag !== object.etag) updated.push(found); + else if (found || incremental) unchanged.push(object); + } + return { + created, + updated, + deleted, + unchanged + }; +}; +//#endregion +//#region src/util/responseHelpers.ts +const assertDAVResponses = (responses, context) => { + const failed = responses.find((response) => !response.ok || response.status >= 400); + if (failed) throw new Error(`${context}: ${failed.status} ${failed.statusText}`); +}; +const assertDAVDiscovery = (responses, context) => { + assertDAVResponses(responses, context); + for (const response of responses) { + if (response.raw?.multistatus && !response.raw.multistatus.response) continue; + assertDAVProperty(response, "resourcetype", context); + if (!response.props || !hasOwn(response.props, "resourcetype") || typeof response.href !== "string" || !response.href) throw new Error(`${context}: missing resourcetype or href in DAV multistatus response`); + } +}; +const assertDAVProperty = (response, name, context) => { + const failed = response.propStats?.find((stat) => !stat.ok && hasOwn(stat.props, name)); + if (failed && !hasOwn(response.props ?? {}, name)) throw new Error(`${context}: ${name} returned ${failed.status} ${failed.statusText}`); +}; +const assertDAVObjectResponses = (responses, property, objectUrls, baseUrl, context) => { + assertDAVResponses(responses, context); + const fetchedUrls = /* @__PURE__ */ new Set(); + for (const response of responses) { + assertDAVProperty(response, property, context); + if (!response.href || typeof (response.props?.[property]?._cdata ?? response.props?.[property]) !== "string") throw new Error(`${context}: missing ${property} or href`); + fetchedUrls.add(getDAVUrlKey(response.href, baseUrl)); + } + const withoutQuery = (url) => getDAVUrlKey(url.replace(/\?.*$/, ""), baseUrl); + const countsByPath = /* @__PURE__ */ new Map(); + for (const url of objectUrls) { + const key = withoutQuery(url); + countsByPath.set(key, (countsByPath.get(key) ?? 0) + 1); + } + if (objectUrls.some((url) => !fetchedUrls.has(getDAVUrlKey(url, baseUrl)) && !(countsByPath.get(withoutQuery(url)) === 1 && fetchedUrls.has(withoutQuery(url))))) throw new Error(`${context}: incomplete response`); +}; +const getDAVText = (value) => { + const text = typeof value === "string" || typeof value === "number" ? String(value) : value?._cdata ?? value?._text; + return typeof text === "string" ? text : void 0; }; //#endregion //#region src/collection.ts @@ -2861,13 +3055,6 @@ const resolveDAVHref = (href, baseUrl) => { return href; } }; -const hrefHasExtension = (href, extension, baseUrl) => { - try { - return new URL(href, ensureTrailingSlash(baseUrl)).pathname.toLowerCase().endsWith(extension); - } catch { - return (href.split(/[?#]/, 1)[0] ?? "").toLowerCase().endsWith(extension); - } -}; const collectionQuery = async (params) => { const { url, body, depth, defaultNamespace = "d", headers, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const queryResults = await davRequest({ @@ -2881,13 +3068,16 @@ const collectionQuery = async (params) => { namespace: defaultNamespace, body }, + headersToExclude, fetchOptions, fetch: fetchOverride }); const emptyNotFound = queryResults[0]; if (defaultNamespace === "c" && body?.["calendar-query"] != null && queryResults.length === 1 && emptyNotFound && emptyNotFound.status === 404 && urlMatches(url, emptyNotFound.href, url) && !emptyNotFound.error && Object.keys(emptyNotFound.props ?? {}).length === 0 && typeof emptyNotFound.raw === "object" && emptyNotFound.raw !== null && emptyNotFound.raw.multistatus?.response?.propstat == null) return []; const errorResponse = queryResults.find((res) => !res.ok || res.status && res.status >= 400); - if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${errorResponse.raw ? `Raw response: ${errorResponse.raw}` : ""}`); + if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${typeof errorResponse.raw === "string" ? `Raw response: ${errorResponse.raw.slice(0, 4096)}` : ""}`); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => !response.raw?.multistatus)) throw new Error("Collection query failed: expected a DAV multistatus response"); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection query failed: missing href in DAV response"); const firstQueryResult = queryResults[0]; if (queryResults.length === 1 && firstQueryResult && (!firstQueryResult.raw || firstQueryResult.raw.multistatus && !firstQueryResult.raw.multistatus.response) && firstQueryResult.status && firstQueryResult.status < 300) return []; return queryResults; @@ -2914,6 +3104,7 @@ const makeCollection = async (params) => { set: { prop: props } } } : void 0 }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -2925,6 +3116,7 @@ const supportedReportSet = async (params) => { props: { [`d:supported-report-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]?.props?.supportedReportSet?.supportedReport; @@ -2938,15 +3130,17 @@ const isCollectionDirty = async (params) => { props: { [`cs:getctag`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(collection.url, r.href, collection.url)); if (!res) throw new Error("Collection does not exist on server"); - if (!res.ok) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); - const remoteCtag = res.props?.getctag; + const unavailableCtag = res.propStats?.length && res.propStats.every((stat) => stat.status === 404 && hasOwn(stat.props, "getctag")); + if (!res.ok && !unavailableCtag) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); + const remoteCtag = getDAVText(res.props?.getctag); return { isDirty: collection.ctag == null || remoteCtag == null || `${collection.ctag}` !== `${remoteCtag}`, - newCtag: remoteCtag?.toString() + newCtag: remoteCtag }; }; /** @@ -2971,6 +3165,7 @@ const syncCollection = (params) => { [`d:prop`]: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -2996,15 +3191,17 @@ const smartCollectionSync = async (params) => { syncLevel: 1, syncToken: collection.syncToken, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); const isObjectResponse = (r) => { - const extName = account.accountType === "caldav" ? ".ics" : ".vcf"; - return typeof r.href === "string" && hrefHasExtension(r.href, extName, collection.url); + return typeof r.href === "string" && getDAVUrlKey(r.href, collection.url) !== getDAVUrlKey(collection.url, collection.url) && !r.props?.resourcetype?.collection; }; - const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && isObjectResponse(r))); + const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && !r.propStats?.length && isObjectResponse(r))); if (errorResponse) throw new Error(`Collection sync failed: ${errorResponse.status} ${errorResponse.statusText}`); + if (result.some((response) => response.raw && !response.raw.multistatus)) throw new Error("Collection sync failed: expected a DAV multistatus response"); + if (result.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection sync failed: missing href in DAV response"); const objectResponses = result.filter(isObjectResponse); const changedObjectUrls = objectResponses.filter((o) => o.status !== 404).map((r) => r.href); const deletedObjectUrls = objectResponses.filter((o) => o.status === 404).map((r) => r.href); @@ -3019,30 +3216,24 @@ const smartCollectionSync = async (params) => { objectUrls: changedObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? [] : []; - const multiGetError = multiGetObjectResponse.find((r) => !r.ok || r.status >= 400); - if (multiGetError) throw new Error(`Collection sync multi-get failed: ${multiGetError.status} ${multiGetError.statusText}`); + assertDAVObjectResponses(multiGetObjectResponse, account.accountType === "caldav" ? "calendarData" : "addressData", changedObjectUrls, collection.url, "Collection sync multi-get failed"); const remoteObjects = multiGetObjectResponse.map((res) => { return { url: resolveDAVHref(res.href ?? "", collection.url), - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: account?.accountType === "caldav" ? res.props?.calendarData?._cdata ?? res.props?.calendarData : res.props?.addressData?._cdata ?? res.props?.addressData }; }); const localObjects = collection.objects ?? []; - const created = remoteObjects.filter((o) => localObjects.every((lo) => !urlMatches(lo.url, o.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = deletedObjectUrls.map((o) => ({ - url: resolveDAVHref(o, collection.url), + const deletedObjects = deletedObjectUrls.map((url) => ({ + url: resolveDAVHref(url, collection.url), etag: "" })); - const unchanged = localObjects.filter((localObject) => deleted.every((deletedObject) => !urlMatches(localObject.url, deletedObject.url, collection.url)) && updated.every((updatedObject) => !urlMatches(localObject.url, updatedObject.url, collection.url))); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url, true, deletedObjects); return { ...collection, objects: detailedResult ? { @@ -3054,13 +3245,14 @@ const smartCollectionSync = async (params) => { ...created, ...updated ], - syncToken: result[0]?.raw?.multistatus?.syncToken ?? collection.syncToken + syncToken: getDAVText(result[0]?.raw?.multistatus?.syncToken) ?? collection.syncToken }; } if (syncMethod === "basic") { const { isDirty, newCtag } = await isCollectionDirty({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3077,17 +3269,11 @@ const smartCollectionSync = async (params) => { const remoteObjects = await collection.fetchObjects({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? []; - const created = remoteObjects.filter((ro) => localObjects.every((lo) => !urlMatches(lo.url, ro.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = localObjects.filter((cal) => remoteObjects.every((ro) => !urlMatches(ro.url, cal.url, collection.url))); - const unchanged = localObjects.filter((lo) => remoteObjects.some((ro) => urlMatches(lo.url, ro.url, collection.url) && ro.etag === lo.etag)); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url); return { ...collection, objects: detailedResult ? { @@ -3139,6 +3325,7 @@ const addressBookQuery = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3155,6 +3342,7 @@ const addressBookMultiGet = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3168,33 +3356,39 @@ const fetchAddressBooks = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`d:displayname`]: {}, - [`cs:getctag`]: {}, - [`d:resourcetype`]: {}, - [`d:sync-token`]: {} + props: { + ...customProps ?? { + [`d:displayname`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`d:sync-token`]: {} + }, + [`d:resourcetype`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Address book discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("addressbook")).map((rs) => { const displayName = rs.props?.displayname?._cdata ?? rs.props?.displayname; debug$3(`Found address book named ${typeof displayName === "string" ? displayName : ""}, props: ${JSON.stringify(rs.props)}`); return { url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), displayName: typeof displayName === "string" ? displayName : "", resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken + syncToken: getDAVText(rs.props?.syncToken) }; }).map(async (addr) => ({ ...addr, reports: await supportedReportSet({ collection: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -3213,12 +3407,14 @@ const fetchVCards = async (params) => { props: { [`d:getetag`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).map((res) => res.href ?? "")).filter((url) => typeof url === "string" && url.trim().length > 0).map((url) => url.startsWith("http") ? url : new URL(url, ensureTrailingSlash(addressBook.url)).href).filter((url) => !urlEquals(url, addressBook.url)).filter(urlFilter).map((url) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(vcardUrls.map((url) => getDAVUrlKey(url, addressBook.url))); let vCardResults = []; if (vcardUrls.length > 0) { if (useMultiGet) vCardResults = await addressBookMultiGet({ @@ -3230,24 +3426,30 @@ const fetchVCards = async (params) => { objectUrls: vcardUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - else vCardResults = await addressBookQuery({ - url: addressBook.url, - props: { - [`d:getetag`]: {}, - [`card:address-data`]: {} - }, - depth: "1", - headers: excludeHeaders(headers, headersToExclude), - fetchOptions, - fetch: fetchOverride - }); + else { + vCardResults = await addressBookQuery({ + url: addressBook.url, + props: { + [`d:getetag`]: {}, + [`card:address-data`]: {} + }, + depth: "1", + headers: excludeHeaders(headers, headersToExclude), + headersToExclude, + fetchOptions, + fetch: fetchOverride + }); + vCardResults = vCardResults.filter((res) => !!res.href && targetUrls.has(getDAVUrlKey(res.href, addressBook.url))); + } } + assertDAVObjectResponses(vCardResults, "addressData", vcardUrls, addressBook.url, "VCard fetch failed"); return vCardResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(addressBook.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.addressData?._cdata ?? res.props?.addressData })); }; @@ -3261,6 +3463,7 @@ const createVCard = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3275,6 +3478,7 @@ const updateVCard = async (params) => { "content-type": "text/vcard; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3285,6 +3489,7 @@ const deleteVCard = async (params) => { url: vCard.url, etag: vCard.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3334,6 +3539,7 @@ const fetchCalendarUserAddresses = async (params) => { props: { [`c:calendar-user-address-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(account.principalUrl, r.href, account.rootUrl)); @@ -3364,6 +3570,7 @@ const calendarQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3382,6 +3589,7 @@ const calendarMultiGet = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3406,6 +3614,7 @@ const makeCalendar = async (params) => { set: { prop: props } } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3419,21 +3628,27 @@ const fetchCalendars = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`c:calendar-description`]: {}, - [`c:calendar-timezone`]: {}, - [`d:displayname`]: {}, - [`ca:calendar-color`]: {}, - [`cs:getctag`]: {}, + props: { + ...customProps ?? { + [`c:calendar-description`]: {}, + [`c:calendar-timezone`]: {}, + [`d:displayname`]: {}, + [`ca:calendar-color`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`c:supported-calendar-component-set`]: {}, + [`d:sync-token`]: {} + }, [`d:resourcetype`]: {}, - [`c:supported-calendar-component-set`]: {}, - [`d:sync-token`]: {} + [`c:supported-calendar-component-set`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Calendar discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("calendar")).filter((rc) => { const components = extractComponentNames(rc.props?.supportedCalendarComponentSet?.comp); return components.length === 0 || components.some((c) => Object.values(ICALObjects).includes(c)); @@ -3446,12 +3661,12 @@ const fetchCalendars = async (params) => { description: typeof description === "string" ? description : "", timezone: typeof timezone === "string" ? timezone : "", url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), calendarColor: rs.props?.calendarColor, - displayName: rs.props?.displayname?._cdata ?? rs.props?.displayname, + displayName: getDAVText(rs.props?.displayname), components: extractComponentNames(compSet), resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken, + syncToken: getDAVText(rs.props?.syncToken), ...projectedProps && projectedEntries.length > 0 ? { projectedProps: Object.fromEntries(projectedEntries) } : {} }; }).map(async (cal) => ({ @@ -3459,6 +3674,7 @@ const fetchCalendars = async (params) => { reports: await supportedReportSet({ collection: cal, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -3466,6 +3682,7 @@ const fetchCalendars = async (params) => { }; const fetchCalendarObjects = async (params) => { const { calendar, objectUrls, filters: customFilters, timeRange, headers, expand, urlFilter = (url) => Boolean(url?.includes(".ics")), useMultiGet = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; + if (expand && !timeRange) throw new Error("timeRange is required when expand is true"); if (timeRange) validateTimeRange(timeRange); debug$2(`Fetching calendar objects from ${calendar?.url}`); const requiredFields = ["url"]; @@ -3496,6 +3713,7 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3503,6 +3721,7 @@ const fetchCalendarObjects = async (params) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(calendarObjectUrls.map((url) => getDAVUrlKey(url, calendar.url))); let calendarObjectResults = []; if (calendarObjectUrls.length > 0) { if (expand && !objectUrls) calendarObjectResults = initialResponses.filter((res) => { @@ -3522,12 +3741,13 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - if (objectUrls && objectUrls.length > 0) calendarObjectResults = calendarObjectResults.filter((res) => { + calendarObjectResults = calendarObjectResults.filter((res) => { const fullUrl = (res.href ?? "").startsWith("http") ? res.href ?? "" : new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href; - return calendarObjectUrls.some((target) => urlMatches(fullUrl, target, calendar.url)); + return targetUrls.has(getDAVUrlKey(fullUrl, calendar.url)); }); } else calendarObjectResults = await calendarMultiGet({ url: calendar.url, @@ -3541,13 +3761,15 @@ const fetchCalendarObjects = async (params) => { objectUrls: calendarObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); } + assertDAVObjectResponses(calendarObjectResults, "calendarData", calendarObjectUrls, calendar.url, "Calendar object fetch failed"); return calendarObjectResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.calendarData?._cdata ?? res.props?.calendarData })); }; @@ -3561,6 +3783,7 @@ const createCalendarObject = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3575,6 +3798,7 @@ const updateCalendarObject = async (params) => { "content-type": "text/calendar; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3585,6 +3809,7 @@ const deleteCalendarObject = async (params) => { url: calendarObject.url, etag: calendarObject.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3599,19 +3824,26 @@ const syncCalendars = async (params) => { const remoteCalendars = await fetchCalendars({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - const created = remoteCalendars.filter((rc) => localCalendars.every((lc) => !urlMatches(lc.url, rc.url, account.rootUrl))); - debug$2(`new calendars: ${created.map((cc) => cc.displayName)}`); - const updated = localCalendars.reduce((prev, curr) => { - const found = remoteCalendars.find((rc) => urlMatches(rc.url, curr.url, account.rootUrl)); - if (found && (found.syncToken && `${found.syncToken}` !== `${curr.syncToken}` || found.ctag && `${found.ctag}` !== `${curr.ctag}`)) return [...prev, { - local: curr, - remote: found - }]; - return prev; - }, []); + const baseUrl = account.rootUrl ?? account.homeUrl ?? account.serverUrl; + const localByUrl = new Map(localCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const remoteByUrl = new Map(remoteCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const created = remoteCalendars.filter((cal) => !localByUrl.has(getDAVUrlKey(cal.url, baseUrl))); + const updated = []; + const unchanged = []; + const deleted = []; + for (const local of localCalendars) { + const remote = remoteByUrl.get(getDAVUrlKey(local.url, baseUrl)); + if (!remote) deleted.push(local); + else if (!remote.syncToken && !remote.ctag || remote.syncToken && remote.syncToken !== local.syncToken || remote.ctag && remote.ctag !== local.ctag) updated.push({ + local, + remote + }); + else unchanged.push(local); + } debug$2(`updated calendars: ${updated.map(({ remote }) => remote.displayName)}`); const updatedWithObjects = await Promise.all(updated.map(async ({ local, remote }) => { const fetchObjects = async (fetchParams) => { @@ -3619,7 +3851,9 @@ const syncCalendars = async (params) => { const { collection, ...requestParams } = fetchParams; return fetchCalendarObjects({ ...requestParams, - calendar: collection + calendar: collection, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => getDAVUrlKey(url, collection.url) !== getDAVUrlKey(collection.url, collection.url) }); }; const collection = { @@ -3634,25 +3868,17 @@ const syncCalendars = async (params) => { collection, detailedResult: false, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account, fetchOptions, fetch: fetchOverride }); return { ...result, - ctag: remote.ctag ?? result.ctag, - syncToken: remote.syncToken ?? result.syncToken + ctag: remote.reports?.includes("syncCollection") ? remote.ctag ?? result.ctag : result.ctag ?? remote.ctag, + syncToken: remote.reports?.includes("syncCollection") ? result.syncToken : remote.syncToken ?? result.syncToken }; })); - const deleted = localCalendars.filter((cal) => remoteCalendars.every((rc) => !urlMatches(rc.url, cal.url, account.rootUrl))); - debug$2(`deleted calendars: ${deleted.map((cc) => cc.displayName)}`); - const unchanged = localCalendars.filter((cal) => remoteCalendars.some((rc) => { - if (!urlMatches(rc.url, cal.url, account.rootUrl)) return false; - const syncTokenMatches = !rc.syncToken || `${rc.syncToken}` === `${cal.syncToken}`; - const ctagMatches = !rc.ctag || `${rc.ctag}` === `${cal.ctag}`; - return syncTokenMatches && ctagMatches; - })); - debug$2(`unchanged calendars: ${unchanged.map((cc) => cc.displayName)}`); return detailedResult ? { created, updated: updatedWithObjects, @@ -3683,6 +3909,7 @@ const freeBusyQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]; @@ -3719,11 +3946,7 @@ const serviceDiscovery = async (params) => { const location = response.headers.get("Location"); if (typeof location === "string" && location.length) { debug$1(`Service discovery redirected to ${location}`); - const hasExplicitScheme = /^[a-z][a-z0-9+.-]*:/i.test(location); - const serviceURL = new URL(location, endpoint); - if (serviceURL.hostname === uri.hostname && uri.port && !serviceURL.port) serviceURL.port = uri.port; - if (!hasExplicitScheme) serviceURL.protocol = endpoint.protocol ?? "http"; - return serviceURL.href; + return new URL(location, uri).href; } } }; @@ -3748,6 +3971,7 @@ const serviceDiscovery = async (params) => { const redirectUrl = extractRedirect(await requestFetch(uri.href, { ...fetchOptionsWithoutHeaders, method: "GET", + body: void 0, headers: excludeHeaders(mergeHeaders(headers, fetchHeaders), headersToExclude), redirect: "manual" })); @@ -3787,6 +4011,7 @@ const fetchPrincipalUrl = async (params) => { props: { [`d:current-user-principal`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3813,6 +4038,7 @@ const fetchHomeUrl = async (params) => { props: account.accountType === "caldav" ? { [`c:calendar-home-set`]: {} } : { [`card:addressbook-home-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3836,6 +4062,7 @@ const createAccount = async (params) => { const discoveredRootUrl = account.rootUrl ?? await serviceDiscovery({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -3854,6 +4081,7 @@ const createAccount = async (params) => { rootUrl }, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -3869,24 +4097,28 @@ const createAccount = async (params) => { newAccount.principalUrl = account.principalUrl ?? newAccount.principalUrl ?? await fetchPrincipalUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); newAccount.homeUrl = account.homeUrl ?? await fetchHomeUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); if (loadCollections || loadObjects) { if (account.accountType === "caldav") newAccount.calendars = await fetchCalendars({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride }); else if (account.accountType === "carddav") newAccount.addressBooks = await fetchAddressBooks({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride @@ -3897,7 +4129,10 @@ const createAccount = async (params) => { ...cal, objects: await fetchCalendarObjects({ calendar: cal, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => !urlEquals(url, cal.url), headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -3907,6 +4142,7 @@ const createAccount = async (params) => { objects: await fetchVCards({ addressBook: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -4057,20 +4293,18 @@ const getOauthHeaders = async (credentials, fetchOptions, fetchOverride) => { debug("Fetching oauth headers"); let tokens = {}; let didRefresh = false; - if (!credentials.refreshToken) { - tokens = await fetchOauthTokens(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else if (credentials.refreshToken && !credentials.accessToken || Date.now() > (credentials.expiration ?? 0)) { - tokens = await refreshAccessToken(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else tokens = { + if (credentials.accessToken && (credentials.expiration == null && !credentials.refreshToken || credentials.expiration != null && Date.now() < credentials.expiration)) tokens = { access_token: credentials.accessToken, refresh_token: credentials.refreshToken }; + else { + tokens = credentials.refreshToken ? await refreshAccessToken(credentials, fetchOptions, fetchOverride) : await fetchOauthTokens(credentials, fetchOptions, fetchOverride); + didRefresh = true; + } if (didRefresh) { if (tokens.access_token) credentials.accessToken = tokens.access_token; if (tokens.refresh_token) credentials.refreshToken = tokens.refresh_token; - if (typeof tokens.expires_in === "number") credentials.expiration = Date.now() + tokens.expires_in * 1e3; + if (tokens.access_token) credentials.expiration = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0; } debug("Oauth tokens obtained"); return { @@ -4084,145 +4318,71 @@ var client_exports = /* @__PURE__ */ __exportAll({ DAVClient: () => DAVClient, createDAVClient: () => createDAVClient }); -const createDAVClient = async (params) => { - const { serverUrl, credentials, authMethod = "Basic", defaultAccountType, authFunction, fetchOptions: defaultFetchOptions, fetch: fetchOverride } = params; - let authHeaders = {}; - switch (authMethod) { - case "Basic": - authHeaders = getBasicAuthHeaders(credentials); - break; - case "Bearer": - authHeaders = getBearerAuthHeaders(credentials); - break; - case "Oauth": - authHeaders = (await getOauthHeaders(credentials, defaultFetchOptions, fetchOverride)).headers; - break; - case "Digest": - authHeaders = { Authorization: `Digest ${credentials.digestString}` }; - break; +const resolveAuthHeaders = async (client, fetchOptions = client.fetchOptions, fetchOverride = client.fetchOverride) => { + switch (client.authMethod) { + case "Basic": return getBasicAuthHeaders(client.credentials); + case "Bearer": return getBearerAuthHeaders(client.credentials); + case "Oauth": { + const { headers } = await getOauthHeaders(client.credentials, fetchOptions, fetchOverride); + if (!headers.authorization) throw new Error("OAuth authentication failed: token endpoint returned no access token"); + return headers; + } + case "Digest": return { Authorization: `Digest ${client.credentials.digestString}` }; case "Custom": - if (!authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - authHeaders = await authFunction(credentials) ?? {}; - break; + if (!client.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); + return await client.authFunction(client.credentials) ?? {}; default: throw new Error("Invalid auth method"); } - const defaultAccount = defaultAccountType ? await createAccount({ +}; +const createDAVClient = async (params) => { + const client = new DAVClient(params); + client.authHeaders = await resolveAuthHeaders(client); + client.account = params.defaultAccountType ? await createAccount({ account: { - serverUrl, - credentials, - accountType: defaultAccountType + serverUrl: params.serverUrl, + credentials: params.credentials, + accountType: params.defaultAccountType }, - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride + headers: client.authHeaders, + fetchOptions: client.fetchOptions, + fetch: client.fetchOverride }) : void 0; - const davRequest$1 = async (params0) => { - const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; - const { headers, ...restInit } = init; - return davRequest({ - ...rest, - init: { - ...restInit, - headers: { - ...authHeaders, - ...headers - } - }, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const commonDefaults = { - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride - }; - const commonDefaultsWithUrl = { - url: serverUrl, - ...commonDefaults - }; - const commonDefaultsWithAccount = { - account: defaultAccount, - ...commonDefaults - }; - const createObject$1 = defaultParam(createObject, commonDefaultsWithUrl); - const updateObject$1 = defaultParam(updateObject, commonDefaultsWithUrl); - const deleteObject$1 = defaultParam(deleteObject, commonDefaultsWithUrl); - const propfind$1 = defaultParam(propfind, commonDefaults); - const createAccount$1 = async (params0) => { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch: fetchOverride2 } = params0; - const merged = { - serverUrl, - credentials, - ...account - }; - if (!merged.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client."); - return createAccount({ - account: merged, - headers: { - ...authHeaders, - ...headers - }, - loadCollections, - loadObjects, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const collectionQuery$1 = defaultParam(collectionQuery, commonDefaults); - const makeCollection$1 = defaultParam(makeCollection, commonDefaults); - const syncCollection$1 = defaultParam(syncCollection, commonDefaults); - const supportedReportSet$1 = defaultParam(supportedReportSet, commonDefaults); - const isCollectionDirty$1 = defaultParam(isCollectionDirty, commonDefaults); - const smartCollectionSync$1 = defaultParam(smartCollectionSync, commonDefaultsWithAccount); - const smartCollectionSyncDetailed$1 = defaultParam(smartCollectionSyncDetailed, commonDefaultsWithAccount); - const calendarQuery$1 = defaultParam(calendarQuery, commonDefaults); - const calendarMultiGet$1 = defaultParam(calendarMultiGet, commonDefaults); - const makeCalendar$1 = defaultParam(makeCalendar, commonDefaults); - const fetchCalendars$1 = defaultParam(fetchCalendars, commonDefaultsWithAccount); - const fetchCalendarUserAddresses$1 = defaultParam(fetchCalendarUserAddresses, commonDefaultsWithAccount); - const fetchCalendarObjects$1 = defaultParam(fetchCalendarObjects, commonDefaults); - const createCalendarObject$1 = defaultParam(createCalendarObject, commonDefaults); - const updateCalendarObject$1 = defaultParam(updateCalendarObject, commonDefaults); - const deleteCalendarObject$1 = defaultParam(deleteCalendarObject, commonDefaults); - const syncCalendars$1 = defaultParam(syncCalendars, commonDefaultsWithAccount); - const syncCalendarsDetailed$1 = defaultParam(syncCalendarsDetailed, commonDefaultsWithAccount); - const freeBusyQuery$1 = defaultParam(freeBusyQuery, commonDefaults); - const addressBookQuery$1 = defaultParam(addressBookQuery, commonDefaults); - const addressBookMultiGet$1 = defaultParam(addressBookMultiGet, commonDefaults); return { - davRequest: davRequest$1, - propfind: propfind$1, - createAccount: createAccount$1, - createObject: createObject$1, - updateObject: updateObject$1, - deleteObject: deleteObject$1, - calendarQuery: calendarQuery$1, - addressBookQuery: addressBookQuery$1, - collectionQuery: collectionQuery$1, - makeCollection: makeCollection$1, - calendarMultiGet: calendarMultiGet$1, - makeCalendar: makeCalendar$1, - freeBusyQuery: freeBusyQuery$1, - syncCollection: syncCollection$1, - supportedReportSet: supportedReportSet$1, - isCollectionDirty: isCollectionDirty$1, - smartCollectionSync: smartCollectionSync$1, - smartCollectionSyncDetailed: smartCollectionSyncDetailed$1, - fetchCalendars: fetchCalendars$1, - fetchCalendarUserAddresses: fetchCalendarUserAddresses$1, - fetchCalendarObjects: fetchCalendarObjects$1, - createCalendarObject: createCalendarObject$1, - updateCalendarObject: updateCalendarObject$1, - deleteCalendarObject: deleteCalendarObject$1, - syncCalendars: syncCalendars$1, - syncCalendarsDetailed: syncCalendarsDetailed$1, - fetchAddressBooks: defaultParam(fetchAddressBooks, commonDefaultsWithAccount), - addressBookMultiGet: addressBookMultiGet$1, - fetchVCards: defaultParam(fetchVCards, commonDefaults), - createVCard: defaultParam(createVCard, commonDefaults), - updateVCard: defaultParam(updateVCard, commonDefaults), - deleteVCard: defaultParam(deleteVCard, commonDefaults) + davRequest: client.davRequest.bind(client), + propfind: client.propfind.bind(client), + createAccount: async (...args) => { + if (!args[0].account.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType`."); + return client.createAccount(...args); + }, + createObject: client.createObject.bind(client), + updateObject: client.updateObject.bind(client), + deleteObject: client.deleteObject.bind(client), + calendarQuery: client.calendarQuery.bind(client), + addressBookQuery: client.addressBookQuery.bind(client), + collectionQuery: client.collectionQuery.bind(client), + makeCollection: client.makeCollection.bind(client), + calendarMultiGet: client.calendarMultiGet.bind(client), + makeCalendar: client.makeCalendar.bind(client), + freeBusyQuery: client.freeBusyQuery.bind(client), + syncCollection: client.syncCollection.bind(client), + supportedReportSet: client.supportedReportSet.bind(client), + isCollectionDirty: client.isCollectionDirty.bind(client), + smartCollectionSync: client.smartCollectionSync.bind(client), + smartCollectionSyncDetailed: client.smartCollectionSyncDetailed.bind(client), + fetchCalendars: client.fetchCalendars.bind(client), + fetchCalendarUserAddresses: client.fetchCalendarUserAddresses.bind(client), + fetchCalendarObjects: client.fetchCalendarObjects.bind(client), + createCalendarObject: client.createCalendarObject.bind(client), + updateCalendarObject: client.updateCalendarObject.bind(client), + deleteCalendarObject: client.deleteCalendarObject.bind(client), + syncCalendars: client.syncCalendars.bind(client), + syncCalendarsDetailed: client.syncCalendarsDetailed.bind(client), + fetchAddressBooks: client.fetchAddressBooks.bind(client), + addressBookMultiGet: client.addressBookMultiGet.bind(client), + fetchVCards: client.fetchVCards.bind(client), + createVCard: client.createVCard.bind(client), + updateVCard: client.updateVCard.bind(client), + deleteVCard: client.deleteVCard.bind(client) }; }; var DAVClient = class { @@ -4234,27 +4394,42 @@ var DAVClient = class { this.authFunction = params.authFunction; this.fetchOptions = params.fetchOptions ?? {}; this.fetchOverride = params.fetch; + this.calendarMultiGet = this.calendarMultiGet.bind(this); + this.addressBookMultiGet = this.addressBookMultiGet.bind(this); } - async login(options) { - switch (this.authMethod) { - case "Basic": - this.authHeaders = getBasicAuthHeaders(this.credentials); - break; - case "Bearer": - this.authHeaders = getBearerAuthHeaders(this.credentials); - break; - case "Oauth": - this.authHeaders = (await getOauthHeaders(this.credentials, this.fetchOptions, this.fetchOverride)).headers; - break; - case "Digest": - this.authHeaders = { Authorization: `Digest ${this.credentials.digestString}` }; - break; - case "Custom": - if (!this.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - this.authHeaders = await this.authFunction(this.credentials); - break; - default: throw new Error("Invalid auth method"); + async authenticate(force = false, fetchOptions = this.fetchOptions, fetchOverride = this.fetchOverride) { + if (!force && this.authMethod !== "Oauth") return; + if (!force && this.authHeaders && this.credentials.accessToken && (this.credentials.expiration == null || Date.now() < this.credentials.expiration)) { + this.authHeaders = { authorization: `Bearer ${this.credentials.accessToken}` }; + return; } + if (this.authentication) return this.authentication; + const authenticate = async () => { + this.authHeaders = await resolveAuthHeaders(this, fetchOptions, fetchOverride); + }; + this.authentication = authenticate(); + try { + await this.authentication; + } finally { + this.authentication = void 0; + } + } + async requestDefaults(params) { + await this.authenticate(false, params?.fetchOptions ?? this.fetchOptions, params?.fetch ?? this.fetchOverride); + return { + url: this.serverUrl, + headers: this.authHeaders, + account: this.account, + fetchOptions: this.fetchOptions, + fetch: this.fetchOverride + }; + } + async invoke(fn, params) { + const defaults = await this.requestDefaults(params); + return await defaultParam(fn, defaults)(...[params]); + } + async login(options) { + await this.authenticate(true); this.account = this.accountType ? await createAccount({ account: { serverUrl: this.serverUrl, @@ -4271,52 +4446,32 @@ var DAVClient = class { async davRequest(params0) { const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; const { headers, ...restInit } = init; + const defaults = await this.requestDefaults(params0); return davRequest({ ...rest, init: { ...restInit, - headers: { - ...this.authHeaders, - ...headers - } + headers: mergeHeaders(defaults.headers, headers) }, fetchOptions: fetchOptions ?? this.fetchOptions, fetch: fetchOverride2 ?? this.fetchOverride }); } async createObject(...params) { - return defaultParam(createObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createObject, params[0]); } async updateObject(...params) { - return defaultParam(updateObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateObject, params[0]); } async deleteObject(...params) { - return defaultParam(deleteObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteObject, params[0]); } async propfind(...params) { - return defaultParam(propfind, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(propfind, params[0]); } async createAccount(params0) { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const { account, headers, headersToExclude, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const defaults = await this.requestDefaults(params0); const accountType = account.accountType ?? this.accountType; if (!accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient."); return createAccount({ @@ -4326,10 +4481,8 @@ var DAVClient = class { ...account, accountType }, - headers: { - ...this.authHeaders, - ...headers - }, + headers: mergeHeaders(defaults.headers, headers), + headersToExclude, loadCollections, loadObjects, fetchOptions: fetchOptions ?? this.fetchOptions, @@ -4337,193 +4490,82 @@ var DAVClient = class { }); } async collectionQuery(...params) { - return defaultParam(collectionQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(collectionQuery, params[0]); } async makeCollection(...params) { - return defaultParam(makeCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCollection, params[0]); } async syncCollection(...params) { - return defaultParam(syncCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCollection, params[0]); } async supportedReportSet(...params) { - return defaultParam(supportedReportSet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(supportedReportSet, params[0]); } async isCollectionDirty(...params) { - return defaultParam(isCollectionDirty, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(isCollectionDirty, params[0]); } async smartCollectionSync(...params) { - return defaultParam(smartCollectionSync, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(params[0]); + return this.invoke(smartCollectionSync, params[0]); } async smartCollectionSyncDetailed(param) { - return defaultParam(smartCollectionSyncDetailed, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(param); + return this.invoke(smartCollectionSyncDetailed, param); } async calendarQuery(...params) { - return defaultParam(calendarQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarQuery, params[0]); } async makeCalendar(...params) { - return defaultParam(makeCalendar, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCalendar, params[0]); } async freeBusyQuery(...params) { - return defaultParam(freeBusyQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(freeBusyQuery, params[0]); } async calendarMultiGet(...params) { - return defaultParam(calendarMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarMultiGet, params[0]); } async fetchCalendars(...params) { - return defaultParam(fetchCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendars, params[0]); } async fetchCalendarUserAddresses(...params) { - return defaultParam(fetchCalendarUserAddresses, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendarUserAddresses, params[0]); } async fetchCalendarObjects(...params) { - return defaultParam(fetchCalendarObjects, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchCalendarObjects, params[0]); } async createCalendarObject(...params) { - return defaultParam(createCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createCalendarObject, params[0]); } async updateCalendarObject(...params) { - return defaultParam(updateCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateCalendarObject, params[0]); } async deleteCalendarObject(...params) { - return defaultParam(deleteCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteCalendarObject, params[0]); } async syncCalendars(...params) { - return defaultParam(syncCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendars, params[0]); } async syncCalendarsDetailed(...params) { - return defaultParam(syncCalendarsDetailed, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendarsDetailed, params[0]); } async addressBookQuery(...params) { - return defaultParam(addressBookQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookQuery, params[0]); } async addressBookMultiGet(...params) { - return defaultParam(addressBookMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookMultiGet, params[0]); } async fetchAddressBooks(...params) { - return defaultParam(fetchAddressBooks, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchAddressBooks, params[0]); } async fetchVCards(...params) { - return defaultParam(fetchVCards, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchVCards, params[0]); } async createVCard(...params) { - return defaultParam(createVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createVCard, params[0]); } async updateVCard(...params) { - return defaultParam(updateVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateVCard, params[0]); } async deleteVCard(...params) { - return defaultParam(deleteVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteVCard, params[0]); } }; //#endregion @@ -4543,4 +4585,4 @@ var src_default = { ...requestHelpers_exports }; //#endregion -export { DAVAttributeMap, DAVClient, DAVNamespace, DAVNamespaceShort, ICALObjects, addressBookMultiGet, addressBookQuery, calendarMultiGet, calendarQuery, cleanupFalsy, collectionQuery, createAccount, createCalendarObject, createDAVClient, createObject, createVCard, davRequest, src_default as default, deleteCalendarObject, deleteObject, deleteVCard, ensureTrailingSlash, excludeHeaders, fetchAddressBooks, fetchCalendarObjects, fetchCalendarUserAddresses, fetchCalendars, fetchHomeUrl, fetchOauthTokens, fetchPrincipalUrl, fetchVCards, freeBusyQuery, getBasicAuthHeaders, getBearerAuthHeaders, getDAVAttribute, getOauthHeaders, isCollectionDirty, makeCalendar, mergeHeaders, propfind, refreshAccessToken, serviceDiscovery, smartCollectionSync, smartCollectionSyncDetailed, supportedReportSet, syncCalendars, syncCalendarsDetailed, syncCollection, updateCalendarObject, updateObject, updateVCard, urlContains, urlEquals, urlMatches }; +export { DAVAttributeMap, DAVClient, DAVNamespace, DAVNamespaceShort, ICALObjects, addressBookMultiGet, addressBookQuery, calendarMultiGet, calendarQuery, cleanupFalsy, collectionQuery, createAccount, createCalendarObject, createDAVClient, createObject, createVCard, davRequest, src_default as default, deleteCalendarObject, deleteObject, deleteVCard, ensureTrailingSlash, excludeHeaders, fetchAddressBooks, fetchCalendarObjects, fetchCalendarUserAddresses, fetchCalendars, fetchHomeUrl, fetchOauthTokens, fetchPrincipalUrl, fetchVCards, freeBusyQuery, getBasicAuthHeaders, getBearerAuthHeaders, getDAVAttribute, getOauthHeaders, isCollectionDirty, makeCalendar, makeCollection, mergeHeaders, propfind, refreshAccessToken, serviceDiscovery, smartCollectionSync, smartCollectionSyncDetailed, supportedReportSet, syncCalendars, syncCalendarsDetailed, syncCollection, updateCalendarObject, updateObject, updateVCard, urlContains, urlEquals, urlMatches }; diff --git a/dist/tsdav.min.cjs b/dist/tsdav.min.cjs index 591016d5..e6424cb8 100644 --- a/dist/tsdav.min.cjs +++ b/dist/tsdav.min.cjs @@ -1,7 +1,7 @@ -Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));let l=require("debug");l=c(l);let u=require("xml-js");u=c(u);let d=function(e){return e.CALENDAR_SERVER=`http://calendarserver.org/ns/`,e.CALDAV_APPLE=`http://apple.com/ns/ical/`,e.CALDAV=`urn:ietf:params:xml:ns:caldav`,e.CARDDAV=`urn:ietf:params:xml:ns:carddav`,e.DAV=`DAV:`,e}({});const f={"urn:ietf:params:xml:ns:caldav":`xmlns:c`,"urn:ietf:params:xml:ns:carddav":`xmlns:card`,"http://calendarserver.org/ns/":`xmlns:cs`,"http://apple.com/ns/ical/":`xmlns:ca`,"DAV:":`xmlns:d`};let p=function(e){return e.CALDAV=`c`,e.CARDDAV=`card`,e.CALENDAR_SERVER=`cs`,e.CALDAV_APPLE=`ca`,e.DAV=`d`,e}({}),m=function(e){return e.VEVENT=`VEVENT`,e.VTODO=`VTODO`,e.VJOURNAL=`VJOURNAL`,e.VFREEBUSY=`VFREEBUSY`,e.VTIMEZONE=`VTIMEZONE`,e.VALARM=`VALARM`,e}({});const h=e=>e.replace(/[-_]+(\w?)/g,(e,t)=>t?t.toUpperCase():``),g=typeof globalThis<`u`&&typeof globalThis.fetch==`function`?globalThis.fetch.bind(globalThis):(()=>{throw Error("tsdav: global fetch is not available in this runtime. Upgrade to Node.js >= 18, run under a browser/Bun/Deno, or install a fetch polyfill on globalThis before importing tsdav. You can also pass a custom `fetch` implementation to `createDAVClient`, `DAVClient`, or individual request helpers.")}),_=/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/,v=e=>{if(typeof e!=`string`)return e;if(_.test(e)){let t=Number(e);if(!Number.isNaN(t)&&Number.isFinite(t)&&(!Number.isInteger(t)||Number.isSafeInteger(t)))return t}let t=e.toLowerCase();return t===`true`||t!==`false`&&e};var y=o({cleanupFalsy:()=>E,conditionalParam:()=>ee,ensureTrailingSlash:()=>x,excludeHeaders:()=>D,getDAVAttribute:()=>T,mergeHeaders:()=>O,urlContains:()=>C,urlEquals:()=>S,urlMatches:()=>w});const b=e=>{let t=e.trim();return t.endsWith(`/`)?t.slice(0,-1):t},x=e=>{let t=e.trim(),n=t.search(/[?#]/),r=n===-1?t:t.slice(0,n),i=n===-1?``:t.slice(n);return`${r.endsWith(`/`)?r:`${r}/`}${i}`},S=(e,t)=>!e&&!t?!0:!e||!t?!1:b(e)===b(t),C=(e,t)=>{if(!e&&!t)return!0;if(!e||!t)return!1;let n=b(e),r=b(t);return n.includes(r)||r.includes(n)},w=(e,t,n)=>{if(!e||!t||!n)return S(e,t);try{return S(new URL(e,n).href,new URL(t,n).href)}catch{return S(e,t)}},T=e=>e.reduce((e,t)=>({...e,[f[t]]:t}),{}),E=e=>Object.entries(e).reduce((e,[t,n])=>n?{...e,[t]:n}:e,{}),ee=(e,t)=>t?{[e]:t}:{},D=(e,t)=>{if(!e)return{};if(!t||t.length===0)return e;let n=new Set(t.map(e=>e.toLowerCase()));return Object.fromEntries(Object.entries(e).filter(([e])=>!n.has(e.toLowerCase())))},O=(...e)=>{let t=new Map,n=(e,n)=>{t.set(e.toLowerCase(),[e,n])};for(let t of e)if(t){if(Array.isArray(t)){for(let[e,r]of t)n(e,r);continue}if(typeof t.forEach==`function`){t.forEach((e,t)=>{n(t,e)});continue}for(let[e,r]of Object.entries(t))n(e,r)}return Object.fromEntries(t.values())};var te=o({createObject:()=>j,davRequest:()=>k,deleteObject:()=>N,propfind:()=>A,updateObject:()=>M});const ne=(0,l.default)(`tsdav:request`),re=e=>{let t=/^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(e??``),n=t?.groups?.status,r=t?.groups?.statusText;return n?{status:Number.parseInt(n,10),statusText:r??``}:void 0},k=async e=>{let{url:t,init:n,convertIncoming:r=!0,parseOutgoing:i=!0,fetchOptions:a={},fetch:o}=e,s=o??g,{headers:c={},body:l,namespace:d,method:f,attributes:p}=n,m=l;p&&typeof l==`object`&&l&&!Array.isArray(l)&&(m=Object.fromEntries(Object.entries(l).map(([e,t])=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let n=t;return[e,{...n,_attributes:{...p,...n._attributes}}]}return[e,t]})));let _=r&&l!=null?u.default.js2xml({_declaration:{_attributes:{version:`1.0`,encoding:`utf-8`}},...m},{compact:!0,spaces:2,elementNameFn:e=>d&&!/^.+:.+/.test(e)?`${d}:${e}`:e}):l,y={...a};delete y.headers;let b=O({"Content-Type":`text/xml;charset=UTF-8`},E(c),a.headers),x=await s(t,{...y,headers:b,body:_,method:f}),S=await x.text();if(!x.ok||!x.headers.get(`content-type`)?.toLowerCase().includes(`xml`)||!i||!S){let e=4096,t=S.length>e?`${S.slice(0,e)}…`:S;return[{href:x.url,ok:x.ok,status:x.status,statusText:x.statusText,raw:t}]}let C;try{C=u.default.xml2js(S,{compact:!0,trim:!0,textFn:(e,t)=>{try{let n=t._parent,r=Object.keys(n),i=r[r.length-1];if(!i)return;let a=n[i];if(a.length>0){let t=a,n=a.length-1;t[n]=v(e)}else n[i]=v(e)}catch(e){ne(e.stack)}},elementNameFn:e=>h(e.replace(/^.+:/,``)),attributesFn:e=>{let t={...e};return delete t.xmlns,t},ignoreDeclaration:!0})}catch(e){return ne(`Failed to parse DAV response XML: ${e.message}`),[{href:x.url,ok:x.ok,status:x.status,statusText:x.statusText,raw:S}]}return C?.multistatus?(Array.isArray(C.multistatus.response)?C.multistatus.response:[C.multistatus.response]).map(e=>{if(!e)return{raw:C,status:x.status,statusText:x.statusText,ok:x.ok};let t=re(e.status),n=t?.status??x.status;return{raw:C,href:e.href,status:n,statusText:t?.statusText??x.statusText,ok:n>=200&&n<300,error:e.error,responsedescription:e.responsedescription,props:(Array.isArray(e.propstat)?e.propstat:[e.propstat]).reduce((e,t)=>{let n=re(t?.status)?.status;return n&&(n<200||n>=300)?e:{...e,...t?.prop}},{})}}):[{href:x.url,ok:x.ok,status:x.status,statusText:x.statusText,raw:C}]},A=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return k({url:t,init:{method:`PROPFIND`,headers:D(E({depth:r,...i}),a),namespace:`d`,body:{propfind:{_attributes:T([`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`,`http://calendarserver.org/ns/`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),prop:n}}},fetchOptions:o,fetch:s})},j=async e=>{let{url:t,data:n,headers:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??g,{headers:c,...l}=a;return s(t,{...l,method:`PUT`,body:n,headers:D(O(r,c),i)})},M=async e=>{let{url:t,data:n,etag:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c=s??g,{headers:l,...u}=o;return c(t,{...u,method:`PUT`,body:n,headers:D(O(E({"If-Match":r,...i}),l),a)})},N=async e=>{let{url:t,headers:n,etag:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??g,{headers:c,...l}=a;return s(t,{...l,method:`DELETE`,headers:D(O(E({"If-Match":r,...n}),c),i)})};function P(e,t){if(!e)return!1;let n=e=>e!=null&&t.every(t=>e[t]);return Array.isArray(e)?e.length>0&&e.every(e=>n(e)):n(e)}const F=(e,t)=>!e||typeof e!=`object`?t.map(e=>e.toString()).join(`,`):t.reduce((t,n)=>e[n]?t:`${t.length?`${t},`:``}${n.toString()}`,``);var ie=o({collectionQuery:()=>I,isCollectionDirty:()=>le,makeCollection:()=>ce,smartCollectionSync:()=>z,smartCollectionSyncDetailed:()=>ue,supportedReportSet:()=>L,syncCollection:()=>R});const ae=(0,l.default)(`tsdav:collection`),oe=(e,t)=>{try{return new URL(e,x(t)).href}catch{return e}},se=(e,t,n)=>{try{return new URL(e,x(n)).pathname.toLowerCase().endsWith(t)}catch{return(e.split(/[?#]/,1)[0]??``).toLowerCase().endsWith(t)}},I=async e=>{let{url:t,body:n,depth:r,defaultNamespace:i=`d`,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e,l=await k({url:t,init:{method:`REPORT`,headers:D(E({depth:r,...a}),o),namespace:i,body:n},fetchOptions:s,fetch:c}),u=l[0];if(i===`c`&&n?.[`calendar-query`]!=null&&l.length===1&&u&&u.status===404&&w(t,u.href,t)&&!u.error&&Object.keys(u.props??{}).length===0&&typeof u.raw==`object`&&u.raw!==null&&u.raw.multistatus?.response?.propstat==null)return[];let d=l.find(e=>!e.ok||e.status&&e.status>=400);if(d)throw Error(`Collection query failed: ${d.status} ${d.statusText}. ${d.raw?`Raw response: ${d.raw}`:``}`);let f=l[0];return l.length===1&&f&&(!f.raw||f.raw.multistatus&&!f.raw.multistatus.response)&&f.status&&f.status<300?[]:l},ce=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return k({url:t,init:{method:`MKCOL`,headers:D(E({depth:r,...i}),a),namespace:`d`,body:n?{mkcol:{_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`]),set:{prop:n}}}:void 0},fetchOptions:o,fetch:s})},L=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await A({url:t.url,props:{"d:supported-report-set":{}},depth:`0`,headers:D(n,r),fetchOptions:i,fetch:a}))[0]?.props?.supportedReportSet?.supportedReport;return o?(Array.isArray(o)?o:[o]).map(e=>e?.report?Object.keys(e.report)[0]:void 0).filter(e=>typeof e==`string`&&e.length>0):[]},le=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await A({url:t.url,props:{"cs:getctag":{}},depth:`0`,headers:D(n,r),fetchOptions:i,fetch:a})).find(e=>w(t.url,e.href,t.url));if(!o)throw Error(`Collection does not exist on server`);if(!o.ok)throw Error(`Collection status check failed: ${o.status} ${o.statusText}`);let s=o.props?.getctag;return{isDirty:t.ctag==null||s==null||`${t.ctag}`!=`${s}`,newCtag:s?.toString()}},R=e=>{let{url:t,props:n,headers:r,syncLevel:i,syncToken:a,headersToExclude:o,fetchOptions:s,fetch:c}=e;return k({url:t,init:{method:`REPORT`,namespace:`d`,headers:D({...r},o),body:{"sync-collection":{_attributes:T([`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"sync-level":i,"sync-token":a,"d:prop":n}}},fetchOptions:s,fetch:c})},z=async e=>{let{collection:t,method:n,headers:r,headersToExclude:i,account:a,detailedResult:o,fetchOptions:s={},fetch:c}=e,l=[`accountType`,`homeUrl`];if(!a||!P(a,l))throw Error(a?`account must have ${F(a,l)} before smartCollectionSync`:`no account for smartCollectionSync`);let u=n??(t.reports?.includes(`syncCollection`)?`webdav`:`basic`);if(ae(`smart collection sync with type ${a.accountType} and method ${u}`),u===`webdav`){let e=await R({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{},"d:displayname":{}},syncLevel:1,syncToken:t.syncToken,headers:D(r,i),fetchOptions:s,fetch:c}),n=e=>{let n=a.accountType===`caldav`?`.ics`:`.vcf`;return typeof e.href==`string`&&se(e.href,n,t.url)},l=e.find(e=>(!e.ok||e.status>=400)&&!(e.status===404&&n(e)));if(l)throw Error(`Collection sync failed: ${l.status} ${l.statusText}`);let u=e.filter(n),d=u.filter(e=>e.status!==404).map(e=>e.href),f=u.filter(e=>e.status===404).map(e=>e.href),p=t.objectMultiGet;if(d.length>0&&!p)throw Error(`collection.objectMultiGet is required for webdav sync changes`);let m=d.length?await p?.({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{}},objectUrls:d,depth:`1`,headers:D(r,i),fetchOptions:s,fetch:c})??[]:[],h=m.find(e=>!e.ok||e.status>=400);if(h)throw Error(`Collection sync multi-get failed: ${h.status} ${h.statusText}`);let g=m.map(e=>({url:oe(e.href??``,t.url),etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:a?.accountType===`caldav`?e.props?.calendarData?._cdata??e.props?.calendarData:e.props?.addressData?._cdata??e.props?.addressData})),_=t.objects??[],v=g.filter(e=>_.every(n=>!w(n.url,e.url,t.url))),y=_.reduce((e,n)=>{let r=g.find(e=>w(e.url,n.url,t.url));return r&&r.etag&&r.etag!==n.etag?[...e,r]:e},[]),b=f.map(e=>({url:oe(e,t.url),etag:``})),x=_.filter(e=>b.every(n=>!w(e.url,n.url,t.url))&&y.every(n=>!w(e.url,n.url,t.url)));return{...t,objects:o?{created:v,updated:y,deleted:b}:[...x,...v,...y],syncToken:e[0]?.raw?.multistatus?.syncToken??t.syncToken}}if(u===`basic`){let{isDirty:e,newCtag:n}=await le({collection:t,headers:D(r,i),fetchOptions:s,fetch:c});if(!e)return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t;let a=t.objects??[];if(!t.fetchObjects)throw Error(`collection.fetchObjects is required for basic sync changes`);let l=await t.fetchObjects({collection:t,headers:D(r,i),fetchOptions:s,fetch:c})??[],u=l.filter(e=>a.every(n=>!w(n.url,e.url,t.url))),d=a.reduce((e,n)=>{let r=l.find(e=>w(e.url,n.url,t.url));return r&&r.etag&&r.etag!==n.etag?[...e,r]:e},[]),f=a.filter(e=>l.every(n=>!w(n.url,e.url,t.url))),p=a.filter(e=>l.some(n=>w(e.url,n.url,t.url)&&n.etag===e.etag));return{...t,objects:o?{created:u,updated:d,deleted:f}:[...p,...u,...d],ctag:n}}return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t},ue=async e=>z({...e,detailedResult:!0});var de=o({addressBookMultiGet:()=>V,addressBookQuery:()=>B,createVCard:()=>pe,deleteVCard:()=>he,fetchAddressBooks:()=>H,fetchVCards:()=>U,updateVCard:()=>me});const fe=(0,l.default)(`tsdav:addressBook`),B=async e=>{let{url:t,props:n,filters:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return I({url:t,body:{"addressbook-query":E({_attributes:T([`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"d:prop":n,filter:r??{"prop-filter":{_attributes:{name:`FN`}}}})},defaultNamespace:`card`,depth:i,headers:D(a,o),fetchOptions:s,fetch:c})},V=async e=>{let{url:t,props:n,objectUrls:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return I({url:t,body:{"addressbook-multiget":E({_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:carddav`]),"d:prop":n,"d:href":r})},defaultNamespace:`card`,depth:i,headers:D(a,o),fetchOptions:s,fetch:c})},H=async e=>{let{account:t,headers:n,props:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e??{},s=[`homeUrl`,`rootUrl`];if(!t||!P(t,s))throw Error(t?`account must have ${F(t,s)} before fetchAddressBooks`:`no account for fetchAddressBooks`);let c=await A({url:t.homeUrl,props:r??{"d:displayname":{},"cs:getctag":{},"d:resourcetype":{},"d:sync-token":{}},depth:`1`,headers:D(n,i),fetchOptions:a,fetch:o});return Promise.all(c.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`addressbook`)).map(e=>{let n=e.props?.displayname?._cdata??e.props?.displayname;return fe(`Found address book named ${typeof n==`string`?n:``}, - props: ${JSON.stringify(e.props)}`),{url:new URL(e.href??``,x(t.rootUrl??``)).href,ctag:e.props?.getctag,displayName:typeof n==`string`?n:``,resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:e.props?.syncToken}}).map(async e=>({...e,reports:await L({collection:e,headers:D(n,i),fetchOptions:a,fetch:o})})))},U=async e=>{let{addressBook:t,headers:n,objectUrls:r,headersToExclude:i,urlFilter:a=e=>!!e,useMultiGet:o=!0,fetchOptions:s={},fetch:c}=e;fe(`Fetching vcards from ${t?.url}`);let l=[`url`];if(!t||!P(t,l))throw Error(t?`addressBook must have ${F(t,l)} before fetchVCards`:`cannot fetchVCards for undefined addressBook`);let u=(r??(await B({url:t.url,props:{"d:getetag":{}},depth:`1`,headers:D(n,i),fetchOptions:s,fetch:c})).map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,x(t.url)).href).filter(e=>!S(e,t.url)).filter(a).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),d=[];return u.length>0&&(d=o?await V({url:t.url,props:{"d:getetag":{},"card:address-data":{}},objectUrls:u,depth:`1`,headers:D(n,i),fetchOptions:s,fetch:c}):await B({url:t.url,props:{"d:getetag":{},"card:address-data":{}},depth:`1`,headers:D(n,i),fetchOptions:s,fetch:c})),d.map(e=>({url:new URL(e.href??``,x(t.url)).href,etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:e.props?.addressData?._cdata??e.props?.addressData}))},pe=async e=>{let{addressBook:t,vCardString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return j({url:new URL(r,x(t.url)).href,data:n,headers:D({"content-type":`text/vcard; charset=utf-8`,"If-None-Match":`*`,...i},a),fetchOptions:o,fetch:s})},me=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return M({url:t.url,data:t.data,etag:t.etag,headers:D({"content-type":`text/vcard; charset=utf-8`,...n},r),fetchOptions:i,fetch:a})},he=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return N({url:t.url,etag:t.etag,headers:D(n,r),fetchOptions:i,fetch:a})};var ge=o({calendarMultiGet:()=>K,calendarQuery:()=>G,createCalendarObject:()=>Ce,deleteCalendarObject:()=>Te,fetchCalendarObjects:()=>J,fetchCalendarUserAddresses:()=>xe,fetchCalendars:()=>q,freeBusyQuery:()=>Oe,makeCalendar:()=>Se,syncCalendars:()=>Ee,syncCalendarsDetailed:()=>De,updateCalendarObject:()=>we});const W=(0,l.default)(`tsdav:calendar`),_e=/^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i,ve=/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i,ye=e=>{let{start:t,end:n}=e;if(!(_e.test(t)&&_e.test(n)||ve.test(t)&&ve.test(n)))throw Error(`invalid timeRange format, not in ISO8601`);if(Number.isNaN(new Date(t).getTime())||Number.isNaN(new Date(n).getTime()))throw Error(`invalid timeRange: start or end is not a valid date`);if(new Date(t).getTime()>=new Date(n).getTime())throw Error(`invalid timeRange: start must be before end`)},be=e=>{let t=[];return Array.isArray(e)?t=e.map(e=>e?._attributes?.name):e&&typeof e==`object`&&(t=[e._attributes?.name]),t.filter(e=>typeof e==`string`&&e.length>0)},xe=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!P(t,o))throw Error(`account must have ${F(t,o)} before fetchUserAddresses`);W(`Fetch user addresses from ${t.principalUrl}`);let s=(await A({url:t.principalUrl,props:{"c:calendar-user-address-set":{}},depth:`0`,headers:D(n,r),fetchOptions:i,fetch:a})).find(e=>w(t.principalUrl,e.href,t.rootUrl));if(!s||!s.ok)throw Error(`cannot find calendarUserAddresses`);let c=s?.props?.calendarUserAddressSet?.href,l=[];Array.isArray(c)?l=c:c&&(l=[c]);let u=l.filter(e=>typeof e==`string`&&e.length>0);return W(`Fetched calendar user addresses ${u}`),u},G=async e=>{let{url:t,props:n,filters:r,timezone:i,depth:a,headers:o,headersToExclude:s,fetchOptions:c={},fetch:l}=e;return I({url:t,body:{"calendar-query":E({_attributes:T([`urn:ietf:params:xml:ns:caldav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`,`DAV:`]),"d:prop":n,filter:r,timezone:i})},defaultNamespace:`c`,depth:a,headers:D(o,s),fetchOptions:c,fetch:l})},K=async e=>{let{url:t,props:n,objectUrls:r,filters:i,timezone:a,depth:o,headers:s,headersToExclude:c,fetchOptions:l={},fetch:u}=e;return I({url:t,body:{"calendar-multiget":E({_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:caldav`]),"d:prop":n,"d:href":r,filter:i,timezone:a})},defaultNamespace:`c`,depth:o,headers:D(s,c),fetchOptions:l,fetch:u})},Se=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return k({url:t,init:{method:`MKCALENDAR`,headers:D(E({depth:r,...i}),a),namespace:`d`,body:{"c:mkcalendar":{_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`]),set:{prop:n}}}},fetchOptions:o,fetch:s})},q=async e=>{let{headers:t,account:n,props:r,projectedProps:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e??{},c=[`homeUrl`,`rootUrl`];if(!n||!P(n,c))throw Error(n?`account must have ${F(n,c)} before fetchCalendars`:`no account for fetchCalendars`);let l=await A({url:n.homeUrl,props:r??{"c:calendar-description":{},"c:calendar-timezone":{},"d:displayname":{},"ca:calendar-color":{},"cs:getctag":{},"d:resourcetype":{},"c:supported-calendar-component-set":{},"d:sync-token":{}},depth:`1`,headers:D(t,a),fetchOptions:o,fetch:s});return Promise.all(l.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`calendar`)).filter(e=>{let t=be(e.props?.supportedCalendarComponentSet?.comp);return t.length===0||t.some(e=>Object.values(m).includes(e))}).map(e=>{let t=e.props?.calendarDescription,r=e.props?.calendarTimezone,a=e.props?.supportedCalendarComponentSet?.comp,o=Object.entries(e.props??{}).filter(([e])=>i?.[e]);return{description:typeof t==`string`?t:``,timezone:typeof r==`string`?r:``,url:new URL(e.href??``,x(n.rootUrl??``)).href,ctag:e.props?.getctag,calendarColor:e.props?.calendarColor,displayName:e.props?.displayname?._cdata??e.props?.displayname,components:be(a),resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:e.props?.syncToken,...i&&o.length>0?{projectedProps:Object.fromEntries(o)}:{}}}).map(async e=>({...e,reports:await L({collection:e,headers:D(t,a),fetchOptions:o,fetch:s})})))},J=async e=>{let{calendar:t,objectUrls:n,filters:r,timeRange:i,headers:a,expand:o,urlFilter:s=e=>!!e?.includes(`.ics`),useMultiGet:c=!0,headersToExclude:l,fetchOptions:u={},fetch:d}=e;i&&ye(i),W(`Fetching calendar objects from ${t?.url}`);let f=[`url`];if(!t||!P(t,f))throw Error(t?`calendar must have ${F(t,f)} before fetchCalendarObjects`:`cannot fetchCalendarObjects for undefined calendar`);let p=r??[{"comp-filter":{_attributes:{name:`VCALENDAR`},"comp-filter":{_attributes:{name:`VEVENT`},...i?{"time-range":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}}}],m=[];n||(m=await G({url:t.url,props:{"d:getetag":{},...o&&i?{"c:calendar-data":{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}}:{}},filters:p,depth:`1`,headers:D(a,l),fetchOptions:u,fetch:d}));let h=(n??m.map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,x(t.url)).href).filter(s).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),g=[];return h.length>0&&(o&&!n?g=m.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href:new URL(e.href??``,x(t.url)).href;return s(n??``)}):c?g=await K({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},objectUrls:h,depth:`1`,headers:D(a,l),fetchOptions:u,fetch:d}):(g=await G({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},filters:p,depth:`1`,headers:D(a,l),fetchOptions:u,fetch:d}),n&&n.length>0&&(g=g.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href??``:new URL(e.href??``,x(t.url)).href;return h.some(e=>w(n,e,t.url))})))),g.map(e=>({url:new URL(e.href??``,x(t.url)).href,etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:e.props?.calendarData?._cdata??e.props?.calendarData}))},Ce=async e=>{let{calendar:t,iCalString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return j({url:new URL(r,x(t.url)).href,data:n,headers:D({"content-type":`text/calendar; charset=utf-8`,"If-None-Match":`*`,...i},a),fetchOptions:o,fetch:s})},we=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return M({url:t.url,data:t.data,etag:t.etag,headers:D({"content-type":`text/calendar; charset=utf-8`,...n},r),fetchOptions:i,fetch:a})},Te=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return N({url:t.url,etag:t.etag,headers:D(n,r),fetchOptions:i,fetch:a})},Ee=async e=>{let{oldCalendars:t,account:n,detailedResult:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`Must have account before syncCalendars`);let c=t??n.calendars??[],l=await q({account:n,headers:D(i,a),fetchOptions:o,fetch:s}),u=l.filter(e=>c.every(t=>!w(t.url,e.url,n.rootUrl)));W(`new calendars: ${u.map(e=>e.displayName)}`);let d=c.reduce((e,t)=>{let r=l.find(e=>w(e.url,t.url,n.rootUrl));return r&&(r.syncToken&&`${r.syncToken}`!=`${t.syncToken}`||r.ctag&&`${r.ctag}`!=`${t.ctag}`)?[...e,{local:t,remote:r}]:e},[]);W(`updated calendars: ${d.map(({remote:e})=>e.displayName)}`);let f=await Promise.all(d.map(async({local:e,remote:t})=>{let r=async e=>{if(!e)return[];let{collection:t,...n}=e;return J({...n,calendar:t})},c={...t,ctag:e.ctag,syncToken:e.syncToken,objects:e.objects,objectMultiGet:K,fetchObjects:r},l=await z({collection:c,detailedResult:!1,headers:D(i,a),account:n,fetchOptions:o,fetch:s});return{...l,ctag:t.ctag??l.ctag,syncToken:t.syncToken??l.syncToken}})),p=c.filter(e=>l.every(t=>!w(t.url,e.url,n.rootUrl)));W(`deleted calendars: ${p.map(e=>e.displayName)}`);let m=c.filter(e=>l.some(t=>{if(!w(t.url,e.url,n.rootUrl))return!1;let r=!t.syncToken||`${t.syncToken}`==`${e.syncToken}`,i=!t.ctag||`${t.ctag}`==`${e.ctag}`;return r&&i}));return W(`unchanged calendars: ${m.map(e=>e.displayName)}`),r?{created:u,updated:f,deleted:p}:[...m,...u,...f]},De=async e=>Ee({...e,detailedResult:!0}),Oe=async e=>{let{url:t,timeRange:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`timeRange is required`);ye(n);let c=(await I({url:t,body:{"free-busy-query":E({_attributes:T([`urn:ietf:params:xml:ns:caldav`]),"c:time-range":{_attributes:{start:`${new Date(n.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(n.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}})},defaultNamespace:`c`,depth:r,headers:D(i,a),fetchOptions:o,fetch:s}))[0];if(!c)throw Error(`freeBusyQuery returned no response`);return c};var ke=o({createAccount:()=>X,fetchHomeUrl:()=>Pe,fetchPrincipalUrl:()=>Ne,serviceDiscovery:()=>je});const Y=(0,l.default)(`tsdav:account`),Ae=(e,t)=>{let n=[t,e,new URL(`/`,e).href];return n.filter((e,t)=>n.indexOf(e)===t)},je=async e=>{Y(`Service discovery...`);let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=a??g,s=new URL(t.serverUrl),{headers:c,...l}=i,u=new URL(`/.well-known/${t.accountType}`,s);u.protocol=s.protocol??`http`;let d=e=>{if(e.status>=300&&e.status<400){let t=e.headers.get(`Location`);if(typeof t==`string`&&t.length){Y(`Service discovery redirected to ${t}`);let e=/^[a-z][a-z0-9+.-]*:/i.test(t),n=new URL(t,s);return n.hostname===u.hostname&&u.port&&!n.port&&(n.port=u.port),e||(n.protocol=s.protocol??`http`),n.href}}};try{let e=d(await o(u.href,{...l,method:`PROPFIND`,headers:D(O({"Content-Type":`text/xml;charset=UTF-8`},n,c),r),body:` +Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));let l=require("debug");l=c(l);let u=require("xml-js");u=c(u);let d=function(e){return e.CALENDAR_SERVER=`http://calendarserver.org/ns/`,e.CALDAV_APPLE=`http://apple.com/ns/ical/`,e.CALDAV=`urn:ietf:params:xml:ns:caldav`,e.CARDDAV=`urn:ietf:params:xml:ns:carddav`,e.DAV=`DAV:`,e}({});const f={"urn:ietf:params:xml:ns:caldav":`xmlns:c`,"urn:ietf:params:xml:ns:carddav":`xmlns:card`,"http://calendarserver.org/ns/":`xmlns:cs`,"http://apple.com/ns/ical/":`xmlns:ca`,"DAV:":`xmlns:d`};let p=function(e){return e.CALDAV=`c`,e.CARDDAV=`card`,e.CALENDAR_SERVER=`cs`,e.CALDAV_APPLE=`ca`,e.DAV=`d`,e}({}),m=function(e){return e.VEVENT=`VEVENT`,e.VTODO=`VTODO`,e.VJOURNAL=`VJOURNAL`,e.VFREEBUSY=`VFREEBUSY`,e.VTIMEZONE=`VTIMEZONE`,e.VALARM=`VALARM`,e}({});const h=typeof globalThis<`u`&&typeof globalThis.fetch==`function`?globalThis.fetch.bind(globalThis):(()=>{throw Error("tsdav: global fetch is not available in this runtime. Upgrade to Node.js >= 18, run under a browser/Bun/Deno, or install a fetch polyfill on globalThis before importing tsdav. You can also pass a custom `fetch` implementation to `createDAVClient`, `DAVClient`, or individual request helpers.")}),g=e=>e.replace(/[-_]+(\w?)/g,(e,t)=>t?t.toUpperCase():``);function _(e,t){if(!e)return!1;let n=e=>e!=null&&t.every(t=>e[t]);return Array.isArray(e)?e.length>0&&e.every(e=>n(e)):n(e)}const v=(e,t)=>!e||typeof e!=`object`?t.map(e=>e.toString()).join(`,`):t.reduce((t,n)=>e[n]?t:`${t.length?`${t},`:``}${n.toString()}`,``),y=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),b=new Set(Object.values(d)),x=new Map([...[`multistatus`,`response`,`propstat`,`prop`,`href`,`status`,`error`,`responsedescription`,`resourcetype`,`collection`,`getetag`,`displayname`,`syncToken`,`supportedReportSet`,`supportedReport`,`report`,`currentUserPrincipal`].map(e=>[e,`DAV:`]),...[`calendarData`,`calendar`,`calendarDescription`,`calendarTimezone`,`calendarHomeSet`,`calendarUserAddressSet`,`supportedCalendarComponentSet`,`comp`].map(e=>[e,`urn:ietf:params:xml:ns:caldav`]),...[`addressData`,`addressbook`,`addressbookHomeSet`].map(e=>[e,`urn:ietf:params:xml:ns:carddav`]),[`getctag`,`http://calendarserver.org/ns/`],[`calendarColor`,`http://apple.com/ns/ical/`]]),S=(e,t)=>{let n=g(e.replace(/^.*:/,``)),r=x.get(n);return t&&r&&t!==r?`{${t}}${n}`:n},ee=(e,t)=>{let n=Object.assign(Object.create(null),t),r=Object.create(null);for(let[t,i]of Object.entries(e.attributes??{}))(t===`xmlns`||t.startsWith(`xmlns:`))&&(n[t===`xmlns`?``:t.slice(6)]=String(i)),t!==`xmlns`&&(r[t]=i);let i=e.name??``,a=i.indexOf(`:`),o=n[a===-1?``:i.slice(0,a)]??``,s=e.elements??[],c=s.filter(e=>e.type===`element`),l=s.filter(e=>e.type===`text`||e.type===`cdata`).map(e=>String(e.text??e.cdata??``)).join(``),u={},d=Object.create(null);if(Object.keys(r).length&&(u._attributes=r),!c.length){let e=s.some(e=>e.type===`text`),t=s.some(e=>e.type===`cdata`);return e?{value:l,namespace:o,namespaces:d}:(t&&(u._cdata=l),{value:u,namespace:o,namespaces:d})}l.trim()&&(u._text=l);for(let e of c){let t=ee(e,n),r=g((e.name??``).replace(/^.*:/,``)),i=S(e.name??``,t.namespace),a=d[i];if(y(u,i)&&a!==t.namespace){if(b.has(t.namespace)&&!b.has(a)){let e=`{${a}}${r}`;Object.defineProperty(u,e,{value:u[i],enumerable:!0,configurable:!0,writable:!0}),d[e]=a,delete u[i]}else i=`{${t.namespace}}${r}`}y(u,i)?Array.isArray(u[i])?u[i].push(t.value):u[i]=[u[i],t.value]:Object.defineProperty(u,i,{value:t.value,enumerable:!0,configurable:!0,writable:!0}),d[i]=t.namespace,r===`prop`&&t.namespace===`DAV:`&&(u.propNamespaces=t.namespaces)}return{value:u,namespace:o,namespaces:d}},te=e=>{let t=u.default.xml2js(e,{compact:!1,ignoreDeclaration:!0}),n={};for(let e of t.elements??[]){if(e.type!==`element`)continue;let t=ee(e,{}),r=S(e.name??``,t.namespace);Object.defineProperty(n,r,{value:t.value,enumerable:!0})}return n},ne=e=>{let t=new Map;for(let n of e)if(n.ok)for(let[e,r]of Object.entries(n.props)){let i=t.get(e)??new Map;i.set(n.namespaces?.[e]??``,r),t.set(e,i)}let n={};for(let[e,r]of t){let t=[...r.keys()].find(e=>b.has(e))??r.keys().next().value;for(let[i,a]of r)Object.defineProperty(n,i===t?e:`{${i}}${e}`,{value:a,enumerable:!0,configurable:!0,writable:!0})}return n};var re=o({cleanupFalsy:()=>D,conditionalParam:()=>oe,ensureTrailingSlash:()=>C,excludeHeaders:()=>O,getDAVAttribute:()=>E,mergeHeaders:()=>k,urlContains:()=>ae,urlEquals:()=>w,urlMatches:()=>T});const ie=e=>{let t=e.trim();return t.endsWith(`/`)?t.slice(0,-1):t},C=e=>{let t=e.trim(),n=t.search(/[?#]/),r=n===-1?t:t.slice(0,n),i=n===-1?``:t.slice(n);return`${r.endsWith(`/`)?r:`${r}/`}${i}`},w=(e,t)=>!e&&!t?!0:!e||!t?!1:ie(e)===ie(t),ae=(e,t)=>{if(!e&&!t)return!0;if(!e||!t)return!1;let n=ie(e),r=ie(t);return n.includes(r)||r.includes(n)},T=(e,t,n)=>{if(!e||!t||!n)return w(e,t);try{return w(new URL(e,n).href,new URL(t,n).href)}catch{return w(e,t)}},E=e=>e.reduce((e,t)=>({...e,[f[t]]:t}),{}),D=e=>Object.entries(e).reduce((e,[t,n])=>n?{...e,[t]:n}:e,{}),oe=(e,t)=>t?{[e]:t}:{},O=(e,t)=>{if(!e)return{};if(!t||t.length===0)return e;let n=new Set(t.map(e=>e.toLowerCase()));return Object.fromEntries(Object.entries(e).filter(([e])=>!n.has(e.toLowerCase())))},k=(...e)=>{let t=new Map,n=(e,n)=>{t.set(e.toLowerCase(),[e,n])};for(let t of e)if(t){if(Array.isArray(t)){for(let[e,r]of t)n(e,r);continue}if(typeof t.forEach==`function`){t.forEach((e,t)=>{n(t,e)});continue}for(let[e,r]of Object.entries(t))n(e,r)}return Object.fromEntries(t.values())};var se=o({createObject:()=>M,davRequest:()=>A,deleteObject:()=>P,propfind:()=>j,updateObject:()=>N});const ce=(0,l.default)(`tsdav:request`),le=e=>{let t=/^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(e?.trim()??``),n=t?.groups?.status,r=t?.groups?.statusText;return n?{status:Number.parseInt(n,10),statusText:r??``}:void 0},A=async e=>{let{url:t,init:n,convertIncoming:r=!0,parseOutgoing:i=!0,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c=s??h,{headers:l={},body:d,namespace:f,method:p,attributes:m}=n,g=d;m&&typeof d==`object`&&d&&!Array.isArray(d)&&(g=Object.fromEntries(Object.entries(d).map(([e,t])=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let n=t;return[e,{...n,_attributes:{...m,...n._attributes}}]}return[e,t]})));let _=r&&d!=null?u.default.js2xml({_declaration:{_attributes:{version:`1.0`,encoding:`utf-8`}},...g},{compact:!0,spaces:2,elementNameFn:e=>f&&!/^.+:.+/.test(e)?`${f}:${e}`:e}):d,v={...o};delete v.headers;let y=O(k({"Content-Type":`text/xml;charset=UTF-8`},D(l),o.headers),a),b=await c(t,{...v,headers:y,body:_,method:p}),x=await b.text();if(!b.ok||!b.headers.get(`content-type`)?.toLowerCase().includes(`xml`)||!i||!x)return[{href:b.url,ok:b.ok,status:b.status,statusText:b.statusText,raw:x}];let S;try{S=te(x)}catch(e){return ce(`Failed to parse DAV response XML: ${e.message}`),[{href:b.url,ok:!1,status:b.status,statusText:b.statusText,raw:x,parseError:e.message}]}return S?.multistatus?(Array.isArray(S.multistatus.response)?S.multistatus.response:[S.multistatus.response]).map(e=>{if(!e)return{raw:S,status:b.status,statusText:b.statusText,ok:b.ok};let t=(Array.isArray(e.propstat)?e.propstat:e.propstat?[e.propstat]:[]).map(e=>{let t=le(e.status),n=t?.status??0;return{props:e.prop??{},namespaces:e.propNamespaces,status:n,statusText:t?.statusText??`Invalid DAV property status`,ok:n>=200&&n<300,error:e.error,responsedescription:e.responsedescription}}),n=t.length>0&&t.every(e=>!e.ok)?t[0]:void 0,r=le(e.status)??n,i=r?.status??b.status;return{raw:S,href:e.href,status:i,statusText:r?.statusText??b.statusText,ok:i>=200&&i<300,error:e.error,responsedescription:e.responsedescription,propStats:t,props:ne(t)}}):[{href:b.url,ok:b.ok,status:b.status,statusText:b.statusText,raw:S}]},j=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return A({url:t,init:{method:`PROPFIND`,headers:O(D({depth:r,...i}),a),namespace:`d`,body:{propfind:{_attributes:E([`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`,`http://calendarserver.org/ns/`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),prop:n}}},headersToExclude:a,fetchOptions:o,fetch:s})},M=async e=>{let{url:t,data:n,headers:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??h,{headers:c,...l}=a;return s(t,{...l,method:`PUT`,body:n,headers:O(k(r,c),i)})},N=async e=>{let{url:t,data:n,etag:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c=s??h,{headers:l,...u}=o;return c(t,{...u,method:`PUT`,body:n,headers:O(k(D({"If-Match":r,...i}),l),a)})},P=async e=>{let{url:t,headers:n,etag:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??h,{headers:c,...l}=a;return s(t,{...l,method:`DELETE`,headers:O(k(D({"If-Match":r,...n}),c),i)})},F=(e,t)=>{let n;try{n=new URL(e,C(t)).href}catch{n=e.trim()}return n.endsWith(`/`)?n.slice(0,-1):n},ue=(e,t,n,r=!1,i=[])=>{let a=new Map(e.map(e=>[F(e.url,n),e])),o=new Map(t.map(e=>[F(e.url,n),e])),s=r?i:e.filter(e=>!o.has(F(e.url,n))),c=new Set(s.map(e=>F(e.url,n))),l=t.filter(e=>!a.has(F(e.url,n))),u=[],d=[];for(let t of e){let e=F(t.url,n);if(c.has(e))continue;let i=o.get(e);i&&i.etag!==t.etag?u.push(i):(i||r)&&d.push(t)}return{created:l,updated:u,deleted:s,unchanged:d}},de=(e,t)=>{let n=e.find(e=>!e.ok||e.status>=400);if(n)throw Error(`${t}: ${n.status} ${n.statusText}`)},fe=(e,t)=>{de(e,t);for(let n of e)if((!n.raw?.multistatus||n.raw.multistatus.response)&&(pe(n,`resourcetype`,t),!n.props||!y(n.props,`resourcetype`)||typeof n.href!=`string`||!n.href))throw Error(`${t}: missing resourcetype or href in DAV multistatus response`)},pe=(e,t,n)=>{let r=e.propStats?.find(e=>!e.ok&&y(e.props,t));if(r&&!y(e.props??{},t))throw Error(`${n}: ${t} returned ${r.status} ${r.statusText}`)},me=(e,t,n,r,i)=>{de(e,i);let a=new Set;for(let n of e){if(pe(n,t,i),!n.href||typeof(n.props?.[t]?._cdata??n.props?.[t])!=`string`)throw Error(`${i}: missing ${t} or href`);a.add(F(n.href,r))}let o=e=>F(e.replace(/\?.*$/,``),r),s=new Map;for(let e of n){let t=o(e);s.set(t,(s.get(t)??0)+1)}if(n.some(e=>!a.has(F(e,r))&&!(s.get(o(e))===1&&a.has(o(e)))))throw Error(`${i}: incomplete response`)},I=e=>{let t=typeof e==`string`||typeof e==`number`?String(e):e?._cdata??e?._text;return typeof t==`string`?t:void 0};var he=o({collectionQuery:()=>L,isCollectionDirty:()=>z,makeCollection:()=>ve,smartCollectionSync:()=>V,smartCollectionSyncDetailed:()=>ye,supportedReportSet:()=>R,syncCollection:()=>B});const ge=(0,l.default)(`tsdav:collection`),_e=(e,t)=>{try{return new URL(e,C(t)).href}catch{return e}},L=async e=>{let{url:t,body:n,depth:r,defaultNamespace:i=`d`,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e,l=await A({url:t,init:{method:`REPORT`,headers:O(D({depth:r,...a}),o),namespace:i,body:n},headersToExclude:o,fetchOptions:s,fetch:c}),u=l[0];if(i===`c`&&n?.[`calendar-query`]!=null&&l.length===1&&u&&u.status===404&&T(t,u.href,t)&&!u.error&&Object.keys(u.props??{}).length===0&&typeof u.raw==`object`&&u.raw!==null&&u.raw.multistatus?.response?.propstat==null)return[];let d=l.find(e=>!e.ok||e.status&&e.status>=400);if(d)throw Error(`Collection query failed: ${d.status} ${d.statusText}. ${typeof d.raw==`string`?`Raw response: ${d.raw.slice(0,4096)}`:``}`);if((n?.[`calendar-query`]||n?.[`calendar-multiget`]||n?.[`addressbook-query`]||n?.[`addressbook-multiget`])&&l.some(e=>!e.raw?.multistatus))throw Error(`Collection query failed: expected a DAV multistatus response`);if((n?.[`calendar-query`]||n?.[`calendar-multiget`]||n?.[`addressbook-query`]||n?.[`addressbook-multiget`])&&l.some(e=>e.raw?.multistatus?.response&&(typeof e.href!=`string`||!e.href)))throw Error(`Collection query failed: missing href in DAV response`);let f=l[0];return l.length===1&&f&&(!f.raw||f.raw.multistatus&&!f.raw.multistatus.response)&&f.status&&f.status<300?[]:l},ve=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return A({url:t,init:{method:`MKCOL`,headers:O(D({depth:r,...i}),a),namespace:`d`,body:n?{mkcol:{_attributes:E([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`]),set:{prop:n}}}:void 0},headersToExclude:a,fetchOptions:o,fetch:s})},R=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await j({url:t.url,props:{"d:supported-report-set":{}},depth:`0`,headers:O(n,r),headersToExclude:r,fetchOptions:i,fetch:a}))[0]?.props?.supportedReportSet?.supportedReport;return o?(Array.isArray(o)?o:[o]).map(e=>e?.report?Object.keys(e.report)[0]:void 0).filter(e=>typeof e==`string`&&e.length>0):[]},z=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await j({url:t.url,props:{"cs:getctag":{}},depth:`0`,headers:O(n,r),headersToExclude:r,fetchOptions:i,fetch:a})).find(e=>T(t.url,e.href,t.url));if(!o)throw Error(`Collection does not exist on server`);let s=o.propStats?.length&&o.propStats.every(e=>e.status===404&&y(e.props,`getctag`));if(!o.ok&&!s)throw Error(`Collection status check failed: ${o.status} ${o.statusText}`);let c=I(o.props?.getctag);return{isDirty:t.ctag==null||c==null||`${t.ctag}`!=`${c}`,newCtag:c}},B=e=>{let{url:t,props:n,headers:r,syncLevel:i,syncToken:a,headersToExclude:o,fetchOptions:s,fetch:c}=e;return A({url:t,init:{method:`REPORT`,namespace:`d`,headers:O({...r},o),body:{"sync-collection":{_attributes:E([`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"sync-level":i,"sync-token":a,"d:prop":n}}},headersToExclude:o,fetchOptions:s,fetch:c})},V=async e=>{let{collection:t,method:n,headers:r,headersToExclude:i,account:a,detailedResult:o,fetchOptions:s={},fetch:c}=e,l=[`accountType`,`homeUrl`];if(!a||!_(a,l))throw Error(a?`account must have ${v(a,l)} before smartCollectionSync`:`no account for smartCollectionSync`);let u=n??(t.reports?.includes(`syncCollection`)?`webdav`:`basic`);if(ge(`smart collection sync with type ${a.accountType} and method ${u}`),u===`webdav`){let e=await B({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{},"d:displayname":{}},syncLevel:1,syncToken:t.syncToken,headers:O(r,i),headersToExclude:i,fetchOptions:s,fetch:c}),n=e=>typeof e.href==`string`&&F(e.href,t.url)!==F(t.url,t.url)&&!e.props?.resourcetype?.collection,l=e.find(e=>(!e.ok||e.status>=400)&&!(e.status===404&&!e.propStats?.length&&n(e)));if(l)throw Error(`Collection sync failed: ${l.status} ${l.statusText}`);if(e.some(e=>e.raw&&!e.raw.multistatus))throw Error(`Collection sync failed: expected a DAV multistatus response`);if(e.some(e=>e.raw?.multistatus?.response&&(typeof e.href!=`string`||!e.href)))throw Error(`Collection sync failed: missing href in DAV response`);let u=e.filter(n),d=u.filter(e=>e.status!==404).map(e=>e.href),f=u.filter(e=>e.status===404).map(e=>e.href),p=t.objectMultiGet;if(d.length>0&&!p)throw Error(`collection.objectMultiGet is required for webdav sync changes`);let m=d.length?await p?.({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{}},objectUrls:d,depth:`1`,headers:O(r,i),headersToExclude:i,fetchOptions:s,fetch:c})??[]:[];me(m,a.accountType===`caldav`?`calendarData`:`addressData`,d,t.url,`Collection sync multi-get failed`);let h=m.map(e=>({url:_e(e.href??``,t.url),etag:I(e.props?.getetag),data:a?.accountType===`caldav`?e.props?.calendarData?._cdata??e.props?.calendarData:e.props?.addressData?._cdata??e.props?.addressData})),g=t.objects??[],_=f.map(e=>({url:_e(e,t.url),etag:``})),{created:v,updated:y,deleted:b,unchanged:x}=ue(g,h,t.url,!0,_);return{...t,objects:o?{created:v,updated:y,deleted:b}:[...x,...v,...y],syncToken:I(e[0]?.raw?.multistatus?.syncToken)??t.syncToken}}if(u===`basic`){let{isDirty:e,newCtag:n}=await z({collection:t,headers:O(r,i),headersToExclude:i,fetchOptions:s,fetch:c});if(!e)return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t;let a=t.objects??[];if(!t.fetchObjects)throw Error(`collection.fetchObjects is required for basic sync changes`);let l=await t.fetchObjects({collection:t,headers:O(r,i),headersToExclude:i,fetchOptions:s,fetch:c})??[],{created:u,updated:d,deleted:f,unchanged:p}=ue(a,l,t.url);return{...t,objects:o?{created:u,updated:d,deleted:f}:[...p,...u,...d],ctag:n}}return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t},ye=async e=>V({...e,detailedResult:!0});var be=o({addressBookMultiGet:()=>U,addressBookQuery:()=>H,createVCard:()=>Se,deleteVCard:()=>we,fetchAddressBooks:()=>W,fetchVCards:()=>G,updateVCard:()=>Ce});const xe=(0,l.default)(`tsdav:addressBook`),H=async e=>{let{url:t,props:n,filters:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return L({url:t,body:{"addressbook-query":D({_attributes:E([`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"d:prop":n,filter:r??{"prop-filter":{_attributes:{name:`FN`}}}})},defaultNamespace:`card`,depth:i,headers:O(a,o),headersToExclude:o,fetchOptions:s,fetch:c})},U=async e=>{let{url:t,props:n,objectUrls:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return L({url:t,body:{"addressbook-multiget":D({_attributes:E([`DAV:`,`urn:ietf:params:xml:ns:carddav`]),"d:prop":n,"d:href":r})},defaultNamespace:`card`,depth:i,headers:O(a,o),headersToExclude:o,fetchOptions:s,fetch:c})},W=async e=>{let{account:t,headers:n,props:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e??{},s=[`homeUrl`,`rootUrl`];if(!t||!_(t,s))throw Error(t?`account must have ${v(t,s)} before fetchAddressBooks`:`no account for fetchAddressBooks`);let c=await j({url:t.homeUrl,props:{...r??{"d:displayname":{},"cs:getctag":{},"d:resourcetype":{},"d:sync-token":{}},"d:resourcetype":{}},depth:`1`,headers:O(n,i),headersToExclude:i,fetchOptions:a,fetch:o});return fe(c,`Address book discovery failed`),Promise.all(c.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`addressbook`)).map(e=>{let n=e.props?.displayname?._cdata??e.props?.displayname;return xe(`Found address book named ${typeof n==`string`?n:``}, + props: ${JSON.stringify(e.props)}`),{url:new URL(e.href??``,C(t.rootUrl??``)).href,ctag:I(e.props?.getctag),displayName:typeof n==`string`?n:``,resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:I(e.props?.syncToken)}}).map(async e=>({...e,reports:await R({collection:e,headers:O(n,i),headersToExclude:i,fetchOptions:a,fetch:o})})))},G=async e=>{let{addressBook:t,headers:n,objectUrls:r,headersToExclude:i,urlFilter:a=e=>!!e,useMultiGet:o=!0,fetchOptions:s={},fetch:c}=e;xe(`Fetching vcards from ${t?.url}`);let l=[`url`];if(!t||!_(t,l))throw Error(t?`addressBook must have ${v(t,l)} before fetchVCards`:`cannot fetchVCards for undefined addressBook`);let u=(r??(await H({url:t.url,props:{"d:getetag":{}},depth:`1`,headers:O(n,i),headersToExclude:i,fetchOptions:s,fetch:c})).map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,C(t.url)).href).filter(e=>!w(e,t.url)).filter(a).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),d=new Set(u.map(e=>F(e,t.url))),f=[];return u.length>0&&(o?f=await U({url:t.url,props:{"d:getetag":{},"card:address-data":{}},objectUrls:u,depth:`1`,headers:O(n,i),headersToExclude:i,fetchOptions:s,fetch:c}):(f=await H({url:t.url,props:{"d:getetag":{},"card:address-data":{}},depth:`1`,headers:O(n,i),headersToExclude:i,fetchOptions:s,fetch:c}),f=f.filter(e=>!!e.href&&d.has(F(e.href,t.url))))),me(f,`addressData`,u,t.url,`VCard fetch failed`),f.map(e=>({url:new URL(e.href??``,C(t.url)).href,etag:I(e.props?.getetag),data:e.props?.addressData?._cdata??e.props?.addressData}))},Se=async e=>{let{addressBook:t,vCardString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return M({url:new URL(r,C(t.url)).href,data:n,headers:O({"content-type":`text/vcard; charset=utf-8`,"If-None-Match":`*`,...i},a),headersToExclude:a,fetchOptions:o,fetch:s})},Ce=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return N({url:t.url,data:t.data,etag:t.etag,headers:O({"content-type":`text/vcard; charset=utf-8`,...n},r),headersToExclude:r,fetchOptions:i,fetch:a})},we=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return P({url:t.url,etag:t.etag,headers:O(n,r),headersToExclude:r,fetchOptions:i,fetch:a})};var Te=o({calendarMultiGet:()=>q,calendarQuery:()=>K,createCalendarObject:()=>Ne,deleteCalendarObject:()=>Fe,fetchCalendarObjects:()=>Y,fetchCalendarUserAddresses:()=>je,fetchCalendars:()=>J,freeBusyQuery:()=>Re,makeCalendar:()=>Me,syncCalendars:()=>Ie,syncCalendarsDetailed:()=>Le,updateCalendarObject:()=>Pe});const Ee=(0,l.default)(`tsdav:calendar`),De=/^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i,Oe=/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i,ke=e=>{let{start:t,end:n}=e;if(!(De.test(t)&&De.test(n)||Oe.test(t)&&Oe.test(n)))throw Error(`invalid timeRange format, not in ISO8601`);if(Number.isNaN(new Date(t).getTime())||Number.isNaN(new Date(n).getTime()))throw Error(`invalid timeRange: start or end is not a valid date`);if(new Date(t).getTime()>=new Date(n).getTime())throw Error(`invalid timeRange: start must be before end`)},Ae=e=>{let t=[];return Array.isArray(e)?t=e.map(e=>e?._attributes?.name):e&&typeof e==`object`&&(t=[e._attributes?.name]),t.filter(e=>typeof e==`string`&&e.length>0)},je=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!_(t,o))throw Error(`account must have ${v(t,o)} before fetchUserAddresses`);Ee(`Fetch user addresses from ${t.principalUrl}`);let s=(await j({url:t.principalUrl,props:{"c:calendar-user-address-set":{}},depth:`0`,headers:O(n,r),headersToExclude:r,fetchOptions:i,fetch:a})).find(e=>T(t.principalUrl,e.href,t.rootUrl));if(!s||!s.ok)throw Error(`cannot find calendarUserAddresses`);let c=s?.props?.calendarUserAddressSet?.href,l=[];Array.isArray(c)?l=c:c&&(l=[c]);let u=l.filter(e=>typeof e==`string`&&e.length>0);return Ee(`Fetched calendar user addresses ${u}`),u},K=async e=>{let{url:t,props:n,filters:r,timezone:i,depth:a,headers:o,headersToExclude:s,fetchOptions:c={},fetch:l}=e;return L({url:t,body:{"calendar-query":D({_attributes:E([`urn:ietf:params:xml:ns:caldav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`,`DAV:`]),"d:prop":n,filter:r,timezone:i})},defaultNamespace:`c`,depth:a,headers:O(o,s),headersToExclude:s,fetchOptions:c,fetch:l})},q=async e=>{let{url:t,props:n,objectUrls:r,filters:i,timezone:a,depth:o,headers:s,headersToExclude:c,fetchOptions:l={},fetch:u}=e;return L({url:t,body:{"calendar-multiget":D({_attributes:E([`DAV:`,`urn:ietf:params:xml:ns:caldav`]),"d:prop":n,"d:href":r,filter:i,timezone:a})},defaultNamespace:`c`,depth:o,headers:O(s,c),headersToExclude:c,fetchOptions:l,fetch:u})},Me=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return A({url:t,init:{method:`MKCALENDAR`,headers:O(D({depth:r,...i}),a),namespace:`d`,body:{"c:mkcalendar":{_attributes:E([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`]),set:{prop:n}}}},headersToExclude:a,fetchOptions:o,fetch:s})},J=async e=>{let{headers:t,account:n,props:r,projectedProps:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e??{},c=[`homeUrl`,`rootUrl`];if(!n||!_(n,c))throw Error(n?`account must have ${v(n,c)} before fetchCalendars`:`no account for fetchCalendars`);let l=await j({url:n.homeUrl,props:{...r??{"c:calendar-description":{},"c:calendar-timezone":{},"d:displayname":{},"ca:calendar-color":{},"cs:getctag":{},"d:resourcetype":{},"c:supported-calendar-component-set":{},"d:sync-token":{}},"d:resourcetype":{},"c:supported-calendar-component-set":{}},depth:`1`,headers:O(t,a),headersToExclude:a,fetchOptions:o,fetch:s});return fe(l,`Calendar discovery failed`),Promise.all(l.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`calendar`)).filter(e=>{let t=Ae(e.props?.supportedCalendarComponentSet?.comp);return t.length===0||t.some(e=>Object.values(m).includes(e))}).map(e=>{let t=e.props?.calendarDescription,r=e.props?.calendarTimezone,a=e.props?.supportedCalendarComponentSet?.comp,o=Object.entries(e.props??{}).filter(([e])=>i?.[e]);return{description:typeof t==`string`?t:``,timezone:typeof r==`string`?r:``,url:new URL(e.href??``,C(n.rootUrl??``)).href,ctag:I(e.props?.getctag),calendarColor:e.props?.calendarColor,displayName:I(e.props?.displayname),components:Ae(a),resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:I(e.props?.syncToken),...i&&o.length>0?{projectedProps:Object.fromEntries(o)}:{}}}).map(async e=>({...e,reports:await R({collection:e,headers:O(t,a),headersToExclude:a,fetchOptions:o,fetch:s})})))},Y=async e=>{let{calendar:t,objectUrls:n,filters:r,timeRange:i,headers:a,expand:o,urlFilter:s=e=>!!e?.includes(`.ics`),useMultiGet:c=!0,headersToExclude:l,fetchOptions:u={},fetch:d}=e;if(o&&!i)throw Error(`timeRange is required when expand is true`);i&&ke(i),Ee(`Fetching calendar objects from ${t?.url}`);let f=[`url`];if(!t||!_(t,f))throw Error(t?`calendar must have ${v(t,f)} before fetchCalendarObjects`:`cannot fetchCalendarObjects for undefined calendar`);let p=r??[{"comp-filter":{_attributes:{name:`VCALENDAR`},"comp-filter":{_attributes:{name:`VEVENT`},...i?{"time-range":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}}}],m=[];n||(m=await K({url:t.url,props:{"d:getetag":{},...o&&i?{"c:calendar-data":{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}}:{}},filters:p,depth:`1`,headers:O(a,l),headersToExclude:l,fetchOptions:u,fetch:d}));let h=(n??m.map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,C(t.url)).href).filter(s).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),g=new Set(h.map(e=>F(e,t.url))),y=[];return h.length>0&&(o&&!n?y=m.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href:new URL(e.href??``,C(t.url)).href;return s(n??``)}):c?y=await q({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},objectUrls:h,depth:`1`,headers:O(a,l),headersToExclude:l,fetchOptions:u,fetch:d}):(y=await K({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},filters:p,depth:`1`,headers:O(a,l),headersToExclude:l,fetchOptions:u,fetch:d}),y=y.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href??``:new URL(e.href??``,C(t.url)).href;return g.has(F(n,t.url))}))),me(y,`calendarData`,h,t.url,`Calendar object fetch failed`),y.map(e=>({url:new URL(e.href??``,C(t.url)).href,etag:I(e.props?.getetag),data:e.props?.calendarData?._cdata??e.props?.calendarData}))},Ne=async e=>{let{calendar:t,iCalString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return M({url:new URL(r,C(t.url)).href,data:n,headers:O({"content-type":`text/calendar; charset=utf-8`,"If-None-Match":`*`,...i},a),headersToExclude:a,fetchOptions:o,fetch:s})},Pe=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return N({url:t.url,data:t.data,etag:t.etag,headers:O({"content-type":`text/calendar; charset=utf-8`,...n},r),headersToExclude:r,fetchOptions:i,fetch:a})},Fe=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return P({url:t.url,etag:t.etag,headers:O(n,r),headersToExclude:r,fetchOptions:i,fetch:a})},Ie=async e=>{let{oldCalendars:t,account:n,detailedResult:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`Must have account before syncCalendars`);let c=t??n.calendars??[],l=await J({account:n,headers:O(i,a),headersToExclude:a,fetchOptions:o,fetch:s}),u=n.rootUrl??n.homeUrl??n.serverUrl,d=new Map(c.map(e=>[F(e.url,u),e])),f=new Map(l.map(e=>[F(e.url,u),e])),p=l.filter(e=>!d.has(F(e.url,u))),m=[],h=[],g=[];for(let e of c){let t=f.get(F(e.url,u));t?!t.syncToken&&!t.ctag||t.syncToken&&t.syncToken!==e.syncToken||t.ctag&&t.ctag!==e.ctag?m.push({local:e,remote:t}):h.push(e):g.push(e)}Ee(`updated calendars: ${m.map(({remote:e})=>e.displayName)}`);let _=await Promise.all(m.map(async({local:e,remote:t})=>{let r=async e=>{if(!e)return[];let{collection:t,...n}=e;return Y({...n,calendar:t,filters:{"comp-filter":{_attributes:{name:`VCALENDAR`}}},urlFilter:e=>F(e,t.url)!==F(t.url,t.url)})},c={...t,ctag:e.ctag,syncToken:e.syncToken,objects:e.objects,objectMultiGet:q,fetchObjects:r},l=await V({collection:c,detailedResult:!1,headers:O(i,a),headersToExclude:a,account:n,fetchOptions:o,fetch:s});return{...l,ctag:t.reports?.includes(`syncCollection`)?t.ctag??l.ctag:l.ctag??t.ctag,syncToken:t.reports?.includes(`syncCollection`)?l.syncToken:t.syncToken??l.syncToken}}));return r?{created:p,updated:_,deleted:g}:[...h,...p,..._]},Le=async e=>Ie({...e,detailedResult:!0}),Re=async e=>{let{url:t,timeRange:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`timeRange is required`);ke(n);let c=(await L({url:t,body:{"free-busy-query":D({_attributes:E([`urn:ietf:params:xml:ns:caldav`]),"c:time-range":{_attributes:{start:`${new Date(n.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(n.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}})},defaultNamespace:`c`,depth:r,headers:O(i,a),headersToExclude:a,fetchOptions:o,fetch:s}))[0];if(!c)throw Error(`freeBusyQuery returned no response`);return c};var ze=o({createAccount:()=>Z,fetchHomeUrl:()=>We,fetchPrincipalUrl:()=>Ue,serviceDiscovery:()=>Ve});const X=(0,l.default)(`tsdav:account`),Be=(e,t)=>{let n=[t,e,new URL(`/`,e).href];return n.filter((e,t)=>n.indexOf(e)===t)},Ve=async e=>{X(`Service discovery...`);let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=a??h,s=new URL(t.serverUrl),{headers:c,...l}=i,u=new URL(`/.well-known/${t.accountType}`,s);u.protocol=s.protocol??`http`;let d=e=>{if(e.status>=300&&e.status<400){let t=e.headers.get(`Location`);if(typeof t==`string`&&t.length)return X(`Service discovery redirected to ${t}`),new URL(t,u).href}};try{let e=d(await o(u.href,{...l,method:`PROPFIND`,headers:O(k({"Content-Type":`text/xml;charset=UTF-8`},n,c),r),body:` -`,redirect:`manual`}));if(e)return e}catch(e){Y(`Service discovery PROPFIND failed: ${e.stack}`)}try{let e=d(await o(u.href,{...l,method:`GET`,headers:D(O(n,c),r),redirect:`manual`}));if(e)return e}catch(e){Y(`Service discovery GET failed: ${e.stack}`)}return s.href},Me=e=>{if(typeof e==`string`&&e.trim().length>0)return e.trim();if(Array.isArray(e)){for(let t of e){let e=Me(t);if(e)return e}return}if(e&&typeof e==`object`){if(`_cdata`in e&&typeof e._cdata==`string`){let t=e._cdata.trim();if(t.length>0)return t}if(`_text`in e&&typeof e._text==`string`){let t=e._text.trim();if(t.length>0)return t}}},Ne=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`rootUrl`];if(!P(t,o))throw Error(`account must have ${F(t,o)} before fetchPrincipalUrl`);Y(`Fetching principal url from path ${t.rootUrl}`);let[s]=await A({url:t.rootUrl,props:{"d:current-user-principal":{}},depth:`0`,headers:D(n,r),fetchOptions:i,fetch:a});if(!s?.ok)throw Y(`Fetch principal url failed: ${s?.statusText??`empty response`}`),s?.status===401?Error(`Invalid credentials: PROPFIND ${t.rootUrl} returned 401 Unauthorized`):Error(`cannot find principalUrl`);let c=Me(s.props?.currentUserPrincipal?.href);if(!c)throw Y(`Fetch principal url failed: missing current-user-principal href`),Error(`cannot find principalUrl`);return Y(`Fetched principal url ${c}`),new URL(c,x(t.rootUrl)).href},Pe=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!P(t,o))throw Error(`account must have ${F(t,o)} before fetchHomeUrl`);Y(`Fetch home url from ${t.principalUrl}`);let s=await A({url:t.principalUrl,props:t.accountType===`caldav`?{"c:calendar-home-set":{}}:{"card:addressbook-home-set":{}},depth:`0`,headers:D(n,r),fetchOptions:i,fetch:a}),c=s.find(e=>w(t.principalUrl,e.href,t.rootUrl));if(!c||!c.ok)throw Y(`Fetch home url failed with status ${c?.statusText} and error ${JSON.stringify(s.map(e=>e.error))}`),Error(`cannot find homeUrl`);let l=Me(t.accountType===`caldav`?c.props?.calendarHomeSet?.href:c.props?.addressbookHomeSet?.href);if(!l)throw Y(`Fetch home url failed: server did not return a ${t.accountType===`caldav`?`calendar-home-set`:`addressbook-home-set`} href`),Error(`cannot find homeUrl`);let u=new URL(l,x(t.rootUrl)).href;return Y(`Fetched home url ${u}`),u},X=async e=>{let{account:t,headers:n,loadCollections:r=!1,loadObjects:i=!1,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c={...t},l=t.rootUrl??await je({account:t,headers:D(n,a),fetchOptions:o,fetch:s});if(t.rootUrl)c.rootUrl=t.rootUrl;else if(t.principalUrl)c.rootUrl=l;else{let e=async(t,r=0,i)=>{let l=t[r];if(!l)throw i??Error(`cannot find principalUrl`);try{return{rootUrl:l,principalUrl:await Ne({account:{...c,rootUrl:l},headers:D(n,a),fetchOptions:o,fetch:s})}}catch(n){return e(t,r+1,n)}},{rootUrl:r,principalUrl:i}=await e(Ae(t.serverUrl,l));c.rootUrl=r,c.principalUrl=i}return c.principalUrl=t.principalUrl??c.principalUrl??await Ne({account:c,headers:D(n,a),fetchOptions:o,fetch:s}),c.homeUrl=t.homeUrl??await Pe({account:c,headers:D(n,a),fetchOptions:o,fetch:s}),(r||i)&&(t.accountType===`caldav`?c.calendars=await q({headers:D(n,a),account:c,fetchOptions:o,fetch:s}):t.accountType===`carddav`&&(c.addressBooks=await H({headers:D(n,a),account:c,fetchOptions:o,fetch:s}))),i&&(t.accountType===`caldav`&&c.calendars?c.calendars=await Promise.all(c.calendars.map(async e=>({...e,objects:await J({calendar:e,headers:D(n,a),fetchOptions:o,fetch:s})}))):t.accountType===`carddav`&&c.addressBooks&&(c.addressBooks=await Promise.all(c.addressBooks.map(async e=>({...e,objects:await U({addressBook:e,headers:D(n,a),fetchOptions:o,fetch:s})}))))),c};var Fe=o({defaultParam:()=>$,fetchOauthTokens:()=>Ve,getBasicAuthHeaders:()=>ze,getBearerAuthHeaders:()=>Be,getOauthHeaders:()=>Ue,refreshAccessToken:()=>He});const Z=(0,l.default)(`tsdav:authHelper`),Q=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;var Ie=class extends Error{constructor(e){super(e),this.name=`InvalidCharacterError`}};const Le=e=>{if(e>255)throw new Ie(`The string to be encoded contains characters outside of the Latin1 range.`)},Re=e=>{let t=``,n=0;for(;n(...n)=>{let r=n[0],i={...t,...r};return(t.headers||r?.headers)&&(i.headers=O(t.headers,r?.headers)),e(i)},ze=e=>(Z(`Basic auth token generated for user "${e.username??``}"`),{authorization:`Basic ${Re(`${e.username}:${e.password}`)}`}),Be=e=>({authorization:`Bearer ${e.accessToken}`}),Ve=async(e,t,n)=>{let r=[`authorizationCode`,`redirectUrl`,`clientId`,`clientSecret`,`tokenUrl`];if(!P(e,r))throw Error(`Oauth credentials missing: ${F(e,r)}`);let i=new URLSearchParams({grant_type:`authorization_code`,code:e.authorizationCode,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret});Z(`Fetching oauth tokens from ${e.tokenUrl}`);let a=n??g,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:O({"content-type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Z(`Fetch Oauth tokens failed with status ${c.status}`),{})},He=async(e,t,n)=>{let r=[`refreshToken`,`clientId`,`clientSecret`,`tokenUrl`];if(!P(e,r))throw Error(`Oauth credentials missing: ${F(e,r)}`);let i=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,refresh_token:e.refreshToken,grant_type:`refresh_token`}),a=n??g,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:O({"Content-Type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Z(`Refresh access token failed with status ${c.status}`),{})},Ue=async(e,t,n)=>{Z(`Fetching oauth headers`);let r={},i=!1;return e.refreshToken?e.refreshToken&&!e.accessToken||Date.now()>(e.expiration??0)?(r=await He(e,t,n),i=!0):r={access_token:e.accessToken,refresh_token:e.refreshToken}:(r=await Ve(e,t,n),i=!0),i&&(r.access_token&&(e.accessToken=r.access_token),r.refresh_token&&(e.refreshToken=r.refresh_token),typeof r.expires_in==`number`&&(e.expiration=Date.now()+r.expires_in*1e3)),Z(`Oauth tokens obtained`),{tokens:r,headers:r.access_token?{authorization:`Bearer ${r.access_token}`}:{}}};var We=o({DAVClient:()=>Ke,createDAVClient:()=>Ge});const Ge=async e=>{let{serverUrl:t,credentials:n,authMethod:r=`Basic`,defaultAccountType:i,authFunction:a,fetchOptions:o,fetch:s}=e,c={};switch(r){case`Basic`:c=ze(n);break;case`Bearer`:c=Be(n);break;case`Oauth`:c=(await Ue(n,o,s)).headers;break;case`Digest`:c={Authorization:`Digest ${n.digestString}`};break;case`Custom`:if(!a)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);c=await a(n)??{};break;default:throw Error(`Invalid auth method`)}let l=i?await X({account:{serverUrl:t,credentials:n,accountType:i},headers:c,fetchOptions:o,fetch:s}):void 0,u=async e=>{let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...l}=t;return k({...i,init:{...l,headers:{...c,...a}},fetchOptions:n??o,fetch:r??s})},d={headers:c,fetchOptions:o,fetch:s},f={url:t,...d},p={account:l,...d},m=$(j,f),h=$(M,f),g=$(N,f),_=$(A,d),v=async e=>{let{account:r,headers:i,loadCollections:a,loadObjects:l,fetchOptions:u,fetch:d}=e,f={serverUrl:t,credentials:n,...r};if(!f.accountType)throw Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client.");return X({account:f,headers:{...c,...i},loadCollections:a,loadObjects:l,fetchOptions:u??o,fetch:d??s})},y=$(I,d),b=$(ce,d),x=$(R,d),S=$(L,d),C=$(le,d),w=$(z,p),T=$(ue,p),E=$(G,d),ee=$(K,d),D=$(Se,d),O=$(q,p),te=$(xe,p),ne=$(J,d),re=$(Ce,d),P=$(we,d),F=$(Te,d),ie=$(Ee,p),ae=$(De,p),oe=$(Oe,d),se=$(B,d),de=$(V,d);return{davRequest:u,propfind:_,createAccount:v,createObject:m,updateObject:h,deleteObject:g,calendarQuery:E,addressBookQuery:se,collectionQuery:y,makeCollection:b,calendarMultiGet:ee,makeCalendar:D,freeBusyQuery:oe,syncCollection:x,supportedReportSet:S,isCollectionDirty:C,smartCollectionSync:w,smartCollectionSyncDetailed:T,fetchCalendars:O,fetchCalendarUserAddresses:te,fetchCalendarObjects:ne,createCalendarObject:re,updateCalendarObject:P,deleteCalendarObject:F,syncCalendars:ie,syncCalendarsDetailed:ae,fetchAddressBooks:$(H,p),addressBookMultiGet:de,fetchVCards:$(U,d),createVCard:$(pe,d),updateVCard:$(me,d),deleteVCard:$(he,d)}};var Ke=class{constructor(e){this.serverUrl=e.serverUrl,this.credentials=e.credentials,this.authMethod=e.authMethod??`Basic`,this.accountType=e.defaultAccountType??`caldav`,this.authFunction=e.authFunction,this.fetchOptions=e.fetchOptions??{},this.fetchOverride=e.fetch}async login(e){switch(this.authMethod){case`Basic`:this.authHeaders=ze(this.credentials);break;case`Bearer`:this.authHeaders=Be(this.credentials);break;case`Oauth`:this.authHeaders=(await Ue(this.credentials,this.fetchOptions,this.fetchOverride)).headers;break;case`Digest`:this.authHeaders={Authorization:`Digest ${this.credentials.digestString}`};break;case`Custom`:if(!this.authFunction)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);this.authHeaders=await this.authFunction(this.credentials);break;default:throw Error(`Invalid auth method`)}this.account=this.accountType?await X({account:{serverUrl:this.serverUrl,credentials:this.credentials,accountType:this.accountType},headers:this.authHeaders,loadCollections:e?.loadCollections,loadObjects:e?.loadObjects,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}):void 0}async davRequest(e){let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...o}=t;return k({...i,init:{...o,headers:{...this.authHeaders,...a}},fetchOptions:n??this.fetchOptions,fetch:r??this.fetchOverride})}async createObject(...e){return $(j,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateObject(...e){return $(M,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteObject(...e){return $(N,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async propfind(...e){return $(A,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createAccount(e){let{account:t,headers:n,loadCollections:r,loadObjects:i,fetchOptions:a,fetch:o}=e,s=t.accountType??this.accountType;if(!s)throw Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient.");return X({account:{serverUrl:this.serverUrl,credentials:this.credentials,...t,accountType:s},headers:{...this.authHeaders,...n},loadCollections:r,loadObjects:i,fetchOptions:a??this.fetchOptions,fetch:o??this.fetchOverride})}async collectionQuery(...e){return $(I,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async makeCollection(...e){return $(ce,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCollection(...e){return $(R,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async supportedReportSet(...e){return $(L,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async isCollectionDirty(...e){return $(le,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async smartCollectionSync(...e){return $(z,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride,account:this.account})(e[0])}async smartCollectionSyncDetailed(e){return $(ue,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride,account:this.account})(e)}async calendarQuery(...e){return $(G,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async makeCalendar(...e){return $(Se,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async freeBusyQuery(...e){return $(Oe,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async calendarMultiGet(...e){return $(K,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async fetchCalendars(...e){return $(q,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchCalendarUserAddresses(...e){return $(xe,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchCalendarObjects(...e){return $(J,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createCalendarObject(...e){return $(Ce,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateCalendarObject(...e){return $(we,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteCalendarObject(...e){return $(Te,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCalendars(...e){return $(Ee,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCalendarsDetailed(...e){return $(De,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async addressBookQuery(...e){return $(B,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async addressBookMultiGet(...e){return $(V,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async fetchAddressBooks(...e){return $(H,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchVCards(...e){return $(U,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createVCard(...e){return $(pe,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateVCard(...e){return $(me,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteVCard(...e){return $(he,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}},qe={DAVNamespace:d,DAVNamespaceShort:p,DAVAttributeMap:f,ICALObjects:m,...We,...te,...ie,...ke,...de,...ge,...Fe,...y};exports.DAVAttributeMap=f,exports.DAVClient=Ke,exports.DAVNamespace=d,exports.DAVNamespaceShort=p,exports.ICALObjects=m,exports.addressBookMultiGet=V,exports.addressBookQuery=B,exports.calendarMultiGet=K,exports.calendarQuery=G,exports.cleanupFalsy=E,exports.collectionQuery=I,exports.createAccount=X,exports.createCalendarObject=Ce,exports.createDAVClient=Ge,exports.createObject=j,exports.createVCard=pe,exports.davRequest=k,exports.default=qe,exports.deleteCalendarObject=Te,exports.deleteObject=N,exports.deleteVCard=he,exports.ensureTrailingSlash=x,exports.excludeHeaders=D,exports.fetchAddressBooks=H,exports.fetchCalendarObjects=J,exports.fetchCalendarUserAddresses=xe,exports.fetchCalendars=q,exports.fetchHomeUrl=Pe,exports.fetchOauthTokens=Ve,exports.fetchPrincipalUrl=Ne,exports.fetchVCards=U,exports.freeBusyQuery=Oe,exports.getBasicAuthHeaders=ze,exports.getBearerAuthHeaders=Be,exports.getDAVAttribute=T,exports.getOauthHeaders=Ue,exports.isCollectionDirty=le,exports.makeCalendar=Se,exports.mergeHeaders=O,exports.propfind=A,exports.refreshAccessToken=He,exports.serviceDiscovery=je,exports.smartCollectionSync=z,exports.smartCollectionSyncDetailed=ue,exports.supportedReportSet=L,exports.syncCalendars=Ee,exports.syncCalendarsDetailed=De,exports.syncCollection=R,exports.updateCalendarObject=we,exports.updateObject=M,exports.updateVCard=me,exports.urlContains=C,exports.urlEquals=S,exports.urlMatches=w; \ No newline at end of file +`,redirect:`manual`}));if(e)return e}catch(e){X(`Service discovery PROPFIND failed: ${e.stack}`)}try{let e=d(await o(u.href,{...l,method:`GET`,body:void 0,headers:O(k(n,c),r),redirect:`manual`}));if(e)return e}catch(e){X(`Service discovery GET failed: ${e.stack}`)}return s.href},He=e=>{if(typeof e==`string`&&e.trim().length>0)return e.trim();if(Array.isArray(e)){for(let t of e){let e=He(t);if(e)return e}return}if(e&&typeof e==`object`){if(`_cdata`in e&&typeof e._cdata==`string`){let t=e._cdata.trim();if(t.length>0)return t}if(`_text`in e&&typeof e._text==`string`){let t=e._text.trim();if(t.length>0)return t}}},Ue=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`rootUrl`];if(!_(t,o))throw Error(`account must have ${v(t,o)} before fetchPrincipalUrl`);X(`Fetching principal url from path ${t.rootUrl}`);let[s]=await j({url:t.rootUrl,props:{"d:current-user-principal":{}},depth:`0`,headers:O(n,r),headersToExclude:r,fetchOptions:i,fetch:a});if(!s?.ok)throw X(`Fetch principal url failed: ${s?.statusText??`empty response`}`),s?.status===401?Error(`Invalid credentials: PROPFIND ${t.rootUrl} returned 401 Unauthorized`):Error(`cannot find principalUrl`);let c=He(s.props?.currentUserPrincipal?.href);if(!c)throw X(`Fetch principal url failed: missing current-user-principal href`),Error(`cannot find principalUrl`);return X(`Fetched principal url ${c}`),new URL(c,C(t.rootUrl)).href},We=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!_(t,o))throw Error(`account must have ${v(t,o)} before fetchHomeUrl`);X(`Fetch home url from ${t.principalUrl}`);let s=await j({url:t.principalUrl,props:t.accountType===`caldav`?{"c:calendar-home-set":{}}:{"card:addressbook-home-set":{}},depth:`0`,headers:O(n,r),headersToExclude:r,fetchOptions:i,fetch:a}),c=s.find(e=>T(t.principalUrl,e.href,t.rootUrl));if(!c||!c.ok)throw X(`Fetch home url failed with status ${c?.statusText} and error ${JSON.stringify(s.map(e=>e.error))}`),Error(`cannot find homeUrl`);let l=He(t.accountType===`caldav`?c.props?.calendarHomeSet?.href:c.props?.addressbookHomeSet?.href);if(!l)throw X(`Fetch home url failed: server did not return a ${t.accountType===`caldav`?`calendar-home-set`:`addressbook-home-set`} href`),Error(`cannot find homeUrl`);let u=new URL(l,C(t.rootUrl)).href;return X(`Fetched home url ${u}`),u},Z=async e=>{let{account:t,headers:n,loadCollections:r=!1,loadObjects:i=!1,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c={...t},l=t.rootUrl??await Ve({account:t,headers:O(n,a),headersToExclude:a,fetchOptions:o,fetch:s});if(t.rootUrl)c.rootUrl=t.rootUrl;else if(t.principalUrl)c.rootUrl=l;else{let e=async(t,r=0,i)=>{let l=t[r];if(!l)throw i??Error(`cannot find principalUrl`);try{return{rootUrl:l,principalUrl:await Ue({account:{...c,rootUrl:l},headers:O(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}}catch(n){return e(t,r+1,n)}},{rootUrl:r,principalUrl:i}=await e(Be(t.serverUrl,l));c.rootUrl=r,c.principalUrl=i}return c.principalUrl=t.principalUrl??c.principalUrl??await Ue({account:c,headers:O(n,a),headersToExclude:a,fetchOptions:o,fetch:s}),c.homeUrl=t.homeUrl??await We({account:c,headers:O(n,a),headersToExclude:a,fetchOptions:o,fetch:s}),(r||i)&&(t.accountType===`caldav`?c.calendars=await J({headers:O(n,a),headersToExclude:a,account:c,fetchOptions:o,fetch:s}):t.accountType===`carddav`&&(c.addressBooks=await W({headers:O(n,a),headersToExclude:a,account:c,fetchOptions:o,fetch:s}))),i&&(t.accountType===`caldav`&&c.calendars?c.calendars=await Promise.all(c.calendars.map(async e=>({...e,objects:await Y({calendar:e,filters:{"comp-filter":{_attributes:{name:`VCALENDAR`}}},urlFilter:t=>!w(t,e.url),headers:O(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}))):t.accountType===`carddav`&&c.addressBooks&&(c.addressBooks=await Promise.all(c.addressBooks.map(async e=>({...e,objects:await G({addressBook:e,headers:O(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}))))),c};var Ge=o({defaultParam:()=>Ye,fetchOauthTokens:()=>Qe,getBasicAuthHeaders:()=>Xe,getBearerAuthHeaders:()=>Ze,getOauthHeaders:()=>et,refreshAccessToken:()=>$e});const Q=(0,l.default)(`tsdav:authHelper`),$=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;var Ke=class extends Error{constructor(e){super(e),this.name=`InvalidCharacterError`}};const qe=e=>{if(e>255)throw new Ke(`The string to be encoded contains characters outside of the Latin1 range.`)},Je=e=>{let t=``,n=0;for(;n(...n)=>{let r=n[0],i={...t,...r};return(t.headers||r?.headers)&&(i.headers=k(t.headers,r?.headers)),e(i)},Xe=e=>(Q(`Basic auth token generated for user "${e.username??``}"`),{authorization:`Basic ${Je(`${e.username}:${e.password}`)}`}),Ze=e=>({authorization:`Bearer ${e.accessToken}`}),Qe=async(e,t,n)=>{let r=[`authorizationCode`,`redirectUrl`,`clientId`,`clientSecret`,`tokenUrl`];if(!_(e,r))throw Error(`Oauth credentials missing: ${v(e,r)}`);let i=new URLSearchParams({grant_type:`authorization_code`,code:e.authorizationCode,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret});Q(`Fetching oauth tokens from ${e.tokenUrl}`);let a=n??h,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:k({"content-type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Q(`Fetch Oauth tokens failed with status ${c.status}`),{})},$e=async(e,t,n)=>{let r=[`refreshToken`,`clientId`,`clientSecret`,`tokenUrl`];if(!_(e,r))throw Error(`Oauth credentials missing: ${v(e,r)}`);let i=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,refresh_token:e.refreshToken,grant_type:`refresh_token`}),a=n??h,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:k({"Content-Type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Q(`Refresh access token failed with status ${c.status}`),{})},et=async(e,t,n)=>{Q(`Fetching oauth headers`);let r={},i=!1;return e.accessToken&&(e.expiration==null&&!e.refreshToken||e.expiration!=null&&Date.now()it,createDAVClient:()=>rt});const nt=async(e,t=e.fetchOptions,n=e.fetchOverride)=>{switch(e.authMethod){case`Basic`:return Xe(e.credentials);case`Bearer`:return Ze(e.credentials);case`Oauth`:{let{headers:r}=await et(e.credentials,t,n);if(!r.authorization)throw Error(`OAuth authentication failed: token endpoint returned no access token`);return r}case`Digest`:return{Authorization:`Digest ${e.credentials.digestString}`};case`Custom`:if(!e.authFunction)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);return await e.authFunction(e.credentials)??{};default:throw Error(`Invalid auth method`)}},rt=async e=>{let t=new it(e);return t.authHeaders=await nt(t),t.account=e.defaultAccountType?await Z({account:{serverUrl:e.serverUrl,credentials:e.credentials,accountType:e.defaultAccountType},headers:t.authHeaders,fetchOptions:t.fetchOptions,fetch:t.fetchOverride}):void 0,{davRequest:t.davRequest.bind(t),propfind:t.propfind.bind(t),createAccount:async(...e)=>{if(!e[0].account.accountType)throw Error("createAccount requires an accountType; pass one via `account.accountType`.");return t.createAccount(...e)},createObject:t.createObject.bind(t),updateObject:t.updateObject.bind(t),deleteObject:t.deleteObject.bind(t),calendarQuery:t.calendarQuery.bind(t),addressBookQuery:t.addressBookQuery.bind(t),collectionQuery:t.collectionQuery.bind(t),makeCollection:t.makeCollection.bind(t),calendarMultiGet:t.calendarMultiGet.bind(t),makeCalendar:t.makeCalendar.bind(t),freeBusyQuery:t.freeBusyQuery.bind(t),syncCollection:t.syncCollection.bind(t),supportedReportSet:t.supportedReportSet.bind(t),isCollectionDirty:t.isCollectionDirty.bind(t),smartCollectionSync:t.smartCollectionSync.bind(t),smartCollectionSyncDetailed:t.smartCollectionSyncDetailed.bind(t),fetchCalendars:t.fetchCalendars.bind(t),fetchCalendarUserAddresses:t.fetchCalendarUserAddresses.bind(t),fetchCalendarObjects:t.fetchCalendarObjects.bind(t),createCalendarObject:t.createCalendarObject.bind(t),updateCalendarObject:t.updateCalendarObject.bind(t),deleteCalendarObject:t.deleteCalendarObject.bind(t),syncCalendars:t.syncCalendars.bind(t),syncCalendarsDetailed:t.syncCalendarsDetailed.bind(t),fetchAddressBooks:t.fetchAddressBooks.bind(t),addressBookMultiGet:t.addressBookMultiGet.bind(t),fetchVCards:t.fetchVCards.bind(t),createVCard:t.createVCard.bind(t),updateVCard:t.updateVCard.bind(t),deleteVCard:t.deleteVCard.bind(t)}};var it=class{constructor(e){this.serverUrl=e.serverUrl,this.credentials=e.credentials,this.authMethod=e.authMethod??`Basic`,this.accountType=e.defaultAccountType??`caldav`,this.authFunction=e.authFunction,this.fetchOptions=e.fetchOptions??{},this.fetchOverride=e.fetch,this.calendarMultiGet=this.calendarMultiGet.bind(this),this.addressBookMultiGet=this.addressBookMultiGet.bind(this)}async authenticate(e=!1,t=this.fetchOptions,n=this.fetchOverride){if(!e&&this.authMethod!==`Oauth`)return;if(!e&&this.authHeaders&&this.credentials.accessToken&&(this.credentials.expiration==null||Date.now(){this.authHeaders=await nt(this,t,n)};this.authentication=r();try{await this.authentication}finally{this.authentication=void 0}}async requestDefaults(e){return await this.authenticate(!1,e?.fetchOptions??this.fetchOptions,e?.fetch??this.fetchOverride),{url:this.serverUrl,headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}}async invoke(e,t){let n=await this.requestDefaults(t);return await Ye(e,n)(t)}async login(e){await this.authenticate(!0),this.account=this.accountType?await Z({account:{serverUrl:this.serverUrl,credentials:this.credentials,accountType:this.accountType},headers:this.authHeaders,loadCollections:e?.loadCollections,loadObjects:e?.loadObjects,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}):void 0}async davRequest(e){let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...o}=t,s=await this.requestDefaults(e);return A({...i,init:{...o,headers:k(s.headers,a)},fetchOptions:n??this.fetchOptions,fetch:r??this.fetchOverride})}async createObject(...e){return this.invoke(M,e[0])}async updateObject(...e){return this.invoke(N,e[0])}async deleteObject(...e){return this.invoke(P,e[0])}async propfind(...e){return this.invoke(j,e[0])}async createAccount(e){let{account:t,headers:n,headersToExclude:r,loadCollections:i,loadObjects:a,fetchOptions:o,fetch:s}=e,c=await this.requestDefaults(e),l=t.accountType??this.accountType;if(!l)throw Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient.");return Z({account:{serverUrl:this.serverUrl,credentials:this.credentials,...t,accountType:l},headers:k(c.headers,n),headersToExclude:r,loadCollections:i,loadObjects:a,fetchOptions:o??this.fetchOptions,fetch:s??this.fetchOverride})}async collectionQuery(...e){return this.invoke(L,e[0])}async makeCollection(...e){return this.invoke(ve,e[0])}async syncCollection(...e){return this.invoke(B,e[0])}async supportedReportSet(...e){return this.invoke(R,e[0])}async isCollectionDirty(...e){return this.invoke(z,e[0])}async smartCollectionSync(...e){return this.invoke(V,e[0])}async smartCollectionSyncDetailed(e){return this.invoke(ye,e)}async calendarQuery(...e){return this.invoke(K,e[0])}async makeCalendar(...e){return this.invoke(Me,e[0])}async freeBusyQuery(...e){return this.invoke(Re,e[0])}async calendarMultiGet(...e){return this.invoke(q,e[0])}async fetchCalendars(...e){return this.invoke(J,e[0])}async fetchCalendarUserAddresses(...e){return this.invoke(je,e[0])}async fetchCalendarObjects(...e){return this.invoke(Y,e[0])}async createCalendarObject(...e){return this.invoke(Ne,e[0])}async updateCalendarObject(...e){return this.invoke(Pe,e[0])}async deleteCalendarObject(...e){return this.invoke(Fe,e[0])}async syncCalendars(...e){return this.invoke(Ie,e[0])}async syncCalendarsDetailed(...e){return this.invoke(Le,e[0])}async addressBookQuery(...e){return this.invoke(H,e[0])}async addressBookMultiGet(...e){return this.invoke(U,e[0])}async fetchAddressBooks(...e){return this.invoke(W,e[0])}async fetchVCards(...e){return this.invoke(G,e[0])}async createVCard(...e){return this.invoke(Se,e[0])}async updateVCard(...e){return this.invoke(Ce,e[0])}async deleteVCard(...e){return this.invoke(we,e[0])}},at={DAVNamespace:d,DAVNamespaceShort:p,DAVAttributeMap:f,ICALObjects:m,...tt,...se,...he,...ze,...be,...Te,...Ge,...re};exports.DAVAttributeMap=f,exports.DAVClient=it,exports.DAVNamespace=d,exports.DAVNamespaceShort=p,exports.ICALObjects=m,exports.addressBookMultiGet=U,exports.addressBookQuery=H,exports.calendarMultiGet=q,exports.calendarQuery=K,exports.cleanupFalsy=D,exports.collectionQuery=L,exports.createAccount=Z,exports.createCalendarObject=Ne,exports.createDAVClient=rt,exports.createObject=M,exports.createVCard=Se,exports.davRequest=A,exports.default=at,exports.deleteCalendarObject=Fe,exports.deleteObject=P,exports.deleteVCard=we,exports.ensureTrailingSlash=C,exports.excludeHeaders=O,exports.fetchAddressBooks=W,exports.fetchCalendarObjects=Y,exports.fetchCalendarUserAddresses=je,exports.fetchCalendars=J,exports.fetchHomeUrl=We,exports.fetchOauthTokens=Qe,exports.fetchPrincipalUrl=Ue,exports.fetchVCards=G,exports.freeBusyQuery=Re,exports.getBasicAuthHeaders=Xe,exports.getBearerAuthHeaders=Ze,exports.getDAVAttribute=E,exports.getOauthHeaders=et,exports.isCollectionDirty=z,exports.makeCalendar=Me,exports.makeCollection=ve,exports.mergeHeaders=k,exports.propfind=j,exports.refreshAccessToken=$e,exports.serviceDiscovery=Ve,exports.smartCollectionSync=V,exports.smartCollectionSyncDetailed=ye,exports.supportedReportSet=R,exports.syncCalendars=Ie,exports.syncCalendarsDetailed=Le,exports.syncCollection=B,exports.updateCalendarObject=Pe,exports.updateObject=N,exports.updateVCard=Ce,exports.urlContains=ae,exports.urlEquals=w,exports.urlMatches=T; \ No newline at end of file diff --git a/dist/tsdav.min.js b/dist/tsdav.min.js index c9462173..7ea63a9e 100644 --- a/dist/tsdav.min.js +++ b/dist/tsdav.min.js @@ -1,20 +1,20 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,o)=>(o=n==null?{}:e(i(n)),c(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n)),u=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")}),d=o(((e,t)=>{var n=1e3,r=n*60,i=r*60,a=i*24,o=a*7,s=a*365.25;t.exports=function(e,t){t||={};var n=typeof e;if(n===`string`&&e.length>0)return c(e);if(n===`number`&&isFinite(e))return t.long?u(e):l(e);throw Error(`val is not a non-empty string or a valid number. val=`+JSON.stringify(e))};function c(e){if(e=String(e),!(e.length>100)){var t=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(t){var c=parseFloat(t[1]);switch((t[2]||`ms`).toLowerCase()){case`years`:case`year`:case`yrs`:case`yr`:case`y`:return c*s;case`weeks`:case`week`:case`w`:return c*o;case`days`:case`day`:case`d`:return c*a;case`hours`:case`hour`:case`hrs`:case`hr`:case`h`:return c*i;case`minutes`:case`minute`:case`mins`:case`min`:case`m`:return c*r;case`seconds`:case`second`:case`secs`:case`sec`:case`s`:return c*n;case`milliseconds`:case`millisecond`:case`msecs`:case`msec`:case`ms`:return c;default:return}}}}function l(e){var t=Math.abs(e);return t>=a?Math.round(e/a)+`d`:t>=i?Math.round(e/i)+`h`:t>=r?Math.round(e/r)+`m`:t>=n?Math.round(e/n)+`s`:e+`ms`}function u(e){var t=Math.abs(e);return t>=a?d(e,t,a,`day`):t>=i?d(e,t,i,`hour`):t>=r?d(e,t,r,`minute`):t>=n?d(e,t,n,`second`):e+` ms`}function d(e,t,n,r){var i=t>=n*1.5;return Math.round(e/n)+` `+r+(i?`s`:``)}})),f=o(((e,t)=>{function n(e){n.debug=n,n.default=n,n.coerce=c,n.disable=o,n.enable=i,n.enabled=s,n.humanize=d(),n.destroy=l,Object.keys(e).forEach(t=>{n[t]=e[t]}),n.names=[],n.skips=[],n.formatters={};function t(e){let t=0;for(let n=0;n{if(t===`%%`)return`%`;a++;let o=n.formatters[i];if(typeof o==`function`){let n=e[a];t=o.call(r,n),e.splice(a,1),a--}return t}),n.formatArgs.call(r,e),(r.log||n.log).apply(r,e)}return s.namespace=e,s.useColors=n.useColors(),s.color=n.selectColor(e),s.extend=r,s.destroy=n.destroy,Object.defineProperty(s,"enabled",{enumerable:!0,configurable:!1,get:()=>i===null?(a!==n.namespaces&&(a=n.namespaces,o=n.enabled(e)),o):i,set:e=>{i=e}}),typeof n.init==`function`&&n.init(s),s}function r(e,t){let r=n(this.namespace+(t===void 0?`:`:t)+e);return r.log=this.log,r}function i(e){n.save(e),n.namespaces=e,n.names=[],n.skips=[];let t=(typeof e==`string`?e:``).trim().replace(/\s+/g,`,`).split(`,`).filter(Boolean);for(let e of t)e[0]===`-`?n.skips.push(e.slice(1)):n.names.push(e)}function a(e,t){let n=0,r=0,i=-1,a=0;for(;n`-`+e)].join(`,`);return n.enable(``),e}function s(e){for(let t of n.skips)if(a(e,t))return!1;for(let t of n.names)if(a(e,t))return!0;return!1}function c(e){return e instanceof Error?e.stack||e.message:e}function l(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")}return n.enable(n.load()),n}t.exports=n})),p=l(o(((e,t)=>{e.formatArgs=r,e.save=i,e.load=a,e.useColors=n,e.storage=o(),e.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),e.colors=`#0000CC.#0000FF.#0033CC.#0033FF.#0066CC.#0066FF.#0099CC.#0099FF.#00CC00.#00CC33.#00CC66.#00CC99.#00CCCC.#00CCFF.#3300CC.#3300FF.#3333CC.#3333FF.#3366CC.#3366FF.#3399CC.#3399FF.#33CC00.#33CC33.#33CC66.#33CC99.#33CCCC.#33CCFF.#6600CC.#6600FF.#6633CC.#6633FF.#66CC00.#66CC33.#9900CC.#9900FF.#9933CC.#9933FF.#99CC00.#99CC33.#CC0000.#CC0033.#CC0066.#CC0099.#CC00CC.#CC00FF.#CC3300.#CC3333.#CC3366.#CC3399.#CC33CC.#CC33FF.#CC6600.#CC6633.#CC9900.#CC9933.#CCCC00.#CCCC33.#FF0000.#FF0033.#FF0066.#FF0099.#FF00CC.#FF00FF.#FF3300.#FF3333.#FF3366.#FF3399.#FF33CC.#FF33FF.#FF6600.#FF6633.#FF9900.#FF9933.#FFCC00.#FFCC33`.split(`.`);function n(){if(typeof window<`u`&&window.process&&(window.process.type===`renderer`||window.process.__nwjs))return!0;if(typeof navigator<`u`&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let e;return typeof document<`u`&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<`u`&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<`u`&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(e[1],10)>=31||typeof navigator<`u`&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}function r(e){if(e[0]=(this.useColors?`%c`:``)+this.namespace+(this.useColors?` %c`:` `)+e[0]+(this.useColors?`%c `:` `)+`+`+t.exports.humanize(this.diff),!this.useColors)return;let n=`color: `+this.color;e.splice(1,0,n,`color: inherit`);let r=0,i=0;e[0].replace(/%[a-zA-Z%]/g,e=>{e!==`%%`&&(r++,e===`%c`&&(i=r))}),e.splice(i,0,n)}e.log=console.debug||console.log||(()=>{});function i(t){try{t?e.storage.setItem(`debug`,t):e.storage.removeItem(`debug`)}catch{}}function a(){let t;try{t=e.storage.getItem(`debug`)||e.storage.getItem(`DEBUG`)}catch{}return!t&&typeof process<`u`&&`env`in process&&(t=process.env.DEBUG),t}function o(){try{return localStorage}catch{}}t.exports=f()(e);let{formatters:s}=t.exports;s.j=function(e){try{return JSON.stringify(e)}catch(e){return`[UnexpectedJSONParseError]: `+e.message}}}))());let m=function(e){return e.CALENDAR_SERVER=`http://calendarserver.org/ns/`,e.CALDAV_APPLE=`http://apple.com/ns/ical/`,e.CALDAV=`urn:ietf:params:xml:ns:caldav`,e.CARDDAV=`urn:ietf:params:xml:ns:carddav`,e.DAV=`DAV:`,e}({});const h={"urn:ietf:params:xml:ns:caldav":`xmlns:c`,"urn:ietf:params:xml:ns:carddav":`xmlns:card`,"http://calendarserver.org/ns/":`xmlns:cs`,"http://apple.com/ns/ical/":`xmlns:ca`,"DAV:":`xmlns:d`};let g=function(e){return e.CALDAV=`c`,e.CARDDAV=`card`,e.CALENDAR_SERVER=`cs`,e.CALDAV_APPLE=`ca`,e.DAV=`d`,e}({}),_=function(e){return e.VEVENT=`VEVENT`,e.VTODO=`VTODO`,e.VJOURNAL=`VJOURNAL`,e.VFREEBUSY=`VFREEBUSY`,e.VTIMEZONE=`VTIMEZONE`,e.VALARM=`VALARM`,e}({});var v=o((e=>{(function(e){e.parser=function(e,t){return new n(e,t)},e.SAXParser=n,e.SAXStream=d,e.createStream=c,e.MAX_BUFFER_LENGTH=65536;var t=[`comment`,`sgmlDecl`,`textNode`,`tagName`,`doctype`,`procInstName`,`procInstBody`,`entity`,`attribName`,`attribValue`,`cdata`,`script`];e.EVENTS=[`text`,`processinginstruction`,`sgmldeclaration`,`doctype`,`comment`,`opentagstart`,`attribute`,`opentag`,`closetag`,`opencdata`,`cdata`,`closecdata`,`error`,`end`,`ready`,`script`,`opennamespace`,`closenamespace`];function n(t,r){if(!(this instanceof n))return new n(t,r);var a=this;i(a),a.q=a.c=``,a.bufferCheckPosition=e.MAX_BUFFER_LENGTH,a.encoding=null,a.opt=r||{},a.opt.lowercase=a.opt.lowercase||a.opt.lowercasetags,a.looseCase=a.opt.lowercase?`toLowerCase`:`toUpperCase`,a.opt.maxEntityCount=a.opt.maxEntityCount||512,a.opt.maxEntityDepth=a.opt.maxEntityDepth||4,a.entityCount=a.entityDepth=0,a.tags=[],a.closed=a.closedRoot=a.sawRoot=!1,a.tag=a.error=null,a.strict=!!t,a.noscript=!!(t||a.opt.noscript),a.state=w.BEGIN,a.strictEntities=a.opt.strictEntities,a.ENTITIES=a.strictEntities?Object.create(e.XML_ENTITIES):Object.create(e.ENTITIES),a.attribList=[],a.opt.xmlns&&(a.ns=Object.create(g)),a.opt.unquotedAttributeValues===void 0&&(a.opt.unquotedAttributeValues=!t),a.trackPosition=a.opt.position!==!1,a.trackPosition&&(a.position=a.line=a.column=0),E(a,`onready`)}Object.create||(Object.create=function(e){function t(){}return t.prototype=e,new t}),Object.keys||(Object.keys=function(e){var t=[];for(var n in e)e.hasOwnProperty(n)&&t.push(n);return t});function r(n){for(var r=Math.max(e.MAX_BUFFER_LENGTH,10),i=0,a=0,o=t.length;ar)switch(t[a]){case`textNode`:A(n);break;case`cdata`:k(n,`oncdata`,n.cdata),n.cdata=``;break;case`script`:k(n,`onscript`,n.script),n.script=``;break;default:M(n,`Max buffer length exceeded: `+t[a])}i=Math.max(i,s)}n.bufferCheckPosition=e.MAX_BUFFER_LENGTH-i+n.position}function i(e){for(var n=0,r=t.length;n=2){if(e[0]===255&&e[1]===254)return`utf-16le`;if(e[0]===254&&e[1]===255)return`utf-16be`}return e.length>=3&&e[0]===239&&e[1]===187&&e[2]===191?`utf8`:e.length>=4?e[0]===60&&e[1]===0&&e[2]===63&&e[3]===0?`utf-16le`:e[0]===0&&e[1]===60&&e[2]===0&&e[3]===63?`utf-16be`:`utf8`:t?`utf8`:null}function d(e,t){if(!(this instanceof d))return new d(e,t);o.apply(this),this._parser=new n(e,t),this.writable=!0,this.readable=!0;var r=this;this._parser.onend=function(){r.emit(`end`)},this._parser.onerror=function(e){r.emit(`error`,e),r._parser.error=null},this._decoder=null,this._decoderBuffer=null,s.forEach(function(e){Object.defineProperty(r,`on`+e,{get:function(){return r._parser[`on`+e]},set:function(t){if(!t)return r.removeAllListeners(e),r._parser[`on`+e]=t,t;r.on(e,t)},enumerable:!0,configurable:!1})})}d.prototype=Object.create(o.prototype,{constructor:{value:d}}),d.prototype._decodeBuffer=function(e,t){if(this._decoderBuffer&&=(e=Buffer.concat([this._decoderBuffer,e]),null),!this._decoder){var n=l(e,t);if(!n)return this._decoderBuffer=e,``;this._parser.encoding=n,this._decoder=new TextDecoder(n)}return this._decoder.decode(e,{stream:!t})},d.prototype.write=function(e){if(typeof Buffer==`function`&&typeof Buffer.isBuffer==`function`&&Buffer.isBuffer(e))e=this._decodeBuffer(e,!1);else if(this._decoderBuffer){var t=this._decodeBuffer(Buffer.alloc(0),!0);t&&(this._parser.write(t),this.emit(`data`,t))}return this._parser.write(e.toString()),this.emit(`data`,e),!0},d.prototype.end=function(e){if(e&&e.length&&this.write(e),this._decoderBuffer){var t=this._decodeBuffer(Buffer.alloc(0),!0);t&&(this._parser.write(t),this.emit(`data`,t))}else if(this._decoder){var n=this._decoder.decode();n&&(this._parser.write(n),this.emit(`data`,n))}return this._parser.end(),!0},d.prototype.on=function(e,t){var n=this;return!n._parser[`on`+e]&&s.indexOf(e)!==-1&&(n._parser[`on`+e]=function(){var t=arguments.length===1?[arguments[0]]:Array.apply(null,arguments);t.splice(0,0,e),n.emit.apply(n,t)}),o.prototype.on.call(n,e,t)};var f=/^\[CDATA\[$/i,p=/^DOCTYPE$/i,m=`http://www.w3.org/XML/1998/namespace`,h=`http://www.w3.org/2000/xmlns/`,g={xml:m,xmlns:h},_=/[:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/,v=/[:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u00B7\u0300-\u036F\u203F-\u2040.\d-]/,y=/[#:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/,b=/[#:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u00B7\u0300-\u036F\u203F-\u2040.\d-]/;function x(e){return e===` `||e===` -`||e===`\r`||e===` `}function S(e){return e===`"`||e===`'`}function ee(e){return e===`>`||x(e)}function C(e,t){return e.test(t)}function te(e,t){return!C(e,t)}var w=0;for(var T in e.STATE={BEGIN:w++,BEGIN_WHITESPACE:w++,TEXT:w++,TEXT_ENTITY:w++,OPEN_WAKA:w++,SGML_DECL:w++,SGML_DECL_QUOTED:w++,DOCTYPE:w++,DOCTYPE_QUOTED:w++,DOCTYPE_DTD:w++,DOCTYPE_DTD_QUOTED:w++,COMMENT_STARTING:w++,COMMENT:w++,COMMENT_ENDING:w++,COMMENT_ENDED:w++,CDATA:w++,CDATA_ENDING:w++,CDATA_ENDING_2:w++,PROC_INST:w++,PROC_INST_BODY:w++,PROC_INST_ENDING:w++,OPEN_TAG:w++,OPEN_TAG_SLASH:w++,ATTRIB:w++,ATTRIB_NAME:w++,ATTRIB_NAME_SAW_WHITE:w++,ATTRIB_VALUE:w++,ATTRIB_VALUE_QUOTED:w++,ATTRIB_VALUE_CLOSED:w++,ATTRIB_VALUE_UNQUOTED:w++,ATTRIB_VALUE_ENTITY_Q:w++,ATTRIB_VALUE_ENTITY_U:w++,CLOSE_TAG:w++,CLOSE_TAG_SAW_WHITE:w++,SCRIPT:w++,SCRIPT_ENDING:w++},e.XML_ENTITIES=Object.assign(Object.create(null),{amp:`&`,gt:`>`,lt:`<`,quot:`"`,apos:`'`}),e.ENTITIES=Object.assign(Object.create(null),{amp:`&`,gt:`>`,lt:`<`,quot:`"`,apos:`'`,AElig:198,Aacute:193,Acirc:194,Agrave:192,Aring:197,Atilde:195,Auml:196,Ccedil:199,ETH:208,Eacute:201,Ecirc:202,Egrave:200,Euml:203,Iacute:205,Icirc:206,Igrave:204,Iuml:207,Ntilde:209,Oacute:211,Ocirc:212,Ograve:210,Oslash:216,Otilde:213,Ouml:214,THORN:222,Uacute:218,Ucirc:219,Ugrave:217,Uuml:220,Yacute:221,aacute:225,acirc:226,aelig:230,agrave:224,aring:229,atilde:227,auml:228,ccedil:231,eacute:233,ecirc:234,egrave:232,eth:240,euml:235,iacute:237,icirc:238,igrave:236,iuml:239,ntilde:241,oacute:243,ocirc:244,ograve:242,oslash:248,otilde:245,ouml:246,szlig:223,thorn:254,uacute:250,ucirc:251,ugrave:249,uuml:252,yacute:253,yuml:255,copy:169,reg:174,nbsp:160,iexcl:161,cent:162,pound:163,curren:164,yen:165,brvbar:166,sect:167,uml:168,ordf:170,laquo:171,not:172,shy:173,macr:175,deg:176,plusmn:177,sup1:185,sup2:178,sup3:179,acute:180,micro:181,para:182,middot:183,cedil:184,ordm:186,raquo:187,frac14:188,frac12:189,frac34:190,iquest:191,times:215,divide:247,OElig:338,oelig:339,Scaron:352,scaron:353,Yuml:376,fnof:402,circ:710,tilde:732,Alpha:913,Beta:914,Gamma:915,Delta:916,Epsilon:917,Zeta:918,Eta:919,Theta:920,Iota:921,Kappa:922,Lambda:923,Mu:924,Nu:925,Xi:926,Omicron:927,Pi:928,Rho:929,Sigma:931,Tau:932,Upsilon:933,Phi:934,Chi:935,Psi:936,Omega:937,alpha:945,beta:946,gamma:947,delta:948,epsilon:949,zeta:950,eta:951,theta:952,iota:953,kappa:954,lambda:955,mu:956,nu:957,xi:958,omicron:959,pi:960,rho:961,sigmaf:962,sigma:963,tau:964,upsilon:965,phi:966,chi:967,psi:968,omega:969,thetasym:977,upsih:978,piv:982,ensp:8194,emsp:8195,thinsp:8201,zwnj:8204,zwj:8205,lrm:8206,rlm:8207,ndash:8211,mdash:8212,lsquo:8216,rsquo:8217,sbquo:8218,ldquo:8220,rdquo:8221,bdquo:8222,dagger:8224,Dagger:8225,bull:8226,hellip:8230,permil:8240,prime:8242,Prime:8243,lsaquo:8249,rsaquo:8250,oline:8254,frasl:8260,euro:8364,image:8465,weierp:8472,real:8476,trade:8482,alefsym:8501,larr:8592,uarr:8593,rarr:8594,darr:8595,harr:8596,crarr:8629,lArr:8656,uArr:8657,rArr:8658,dArr:8659,hArr:8660,forall:8704,part:8706,exist:8707,empty:8709,nabla:8711,isin:8712,notin:8713,ni:8715,prod:8719,sum:8721,minus:8722,lowast:8727,radic:8730,prop:8733,infin:8734,ang:8736,and:8743,or:8744,cap:8745,cup:8746,int:8747,there4:8756,sim:8764,cong:8773,asymp:8776,ne:8800,equiv:8801,le:8804,ge:8805,sub:8834,sup:8835,nsub:8836,sube:8838,supe:8839,oplus:8853,otimes:8855,perp:8869,sdot:8901,lceil:8968,rceil:8969,lfloor:8970,rfloor:8971,lang:9001,rang:9002,loz:9674,spades:9824,clubs:9827,hearts:9829,diams:9830}),Object.keys(e.ENTITIES).forEach(function(t){var n=e.ENTITIES[t],r=typeof n==`number`?String.fromCharCode(n):n;e.ENTITIES[t]=r}),e.STATE)e.STATE[e.STATE[T]]=T;w=e.STATE;function E(e,t,n){e[t]&&e[t](n)}function ne(e){var t=e&&e.match(/(?:^|\s)encoding\s*=\s*(['"])([^'"]+)\1/i);return t?t[2]:null}function re(e){return e?e.toLowerCase().replace(/[^a-z0-9]/g,``):null}function D(e,t){let n=re(e),r=re(t);return!n||!r?!0:r===`utf16`?n===`utf16le`||n===`utf16be`:n===r}function O(e,t){if(e.strict&&e.encoding&&t&&t.name===`xml`){var n=ne(t.body);n&&!D(e.encoding,n)&&P(e,`XML declaration encoding `+n+` does not match detected stream encoding `+e.encoding.toUpperCase())}}function k(e,t,n){e.textNode&&A(e),E(e,t,n)}function A(e){e.textNode=j(e.opt,e.textNode),e.textNode&&E(e,`ontext`,e.textNode),e.textNode=``}function j(e,t){return e.trim&&(t=t.trim()),e.normalize&&(t=t.replace(/\s+/g,` `)),t}function M(e,t){return A(e),e.trackPosition&&(t+=` +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,o)=>(o=n==null?{}:e(i(n)),c(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n)),u=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")}),d=o(((e,t)=>{var n=1e3,r=n*60,i=r*60,a=i*24,o=a*7,s=a*365.25;t.exports=function(e,t){t||={};var n=typeof e;if(n===`string`&&e.length>0)return c(e);if(n===`number`&&isFinite(e))return t.long?u(e):l(e);throw Error(`val is not a non-empty string or a valid number. val=`+JSON.stringify(e))};function c(e){if(e=String(e),!(e.length>100)){var t=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(t){var c=parseFloat(t[1]);switch((t[2]||`ms`).toLowerCase()){case`years`:case`year`:case`yrs`:case`yr`:case`y`:return c*s;case`weeks`:case`week`:case`w`:return c*o;case`days`:case`day`:case`d`:return c*a;case`hours`:case`hour`:case`hrs`:case`hr`:case`h`:return c*i;case`minutes`:case`minute`:case`mins`:case`min`:case`m`:return c*r;case`seconds`:case`second`:case`secs`:case`sec`:case`s`:return c*n;case`milliseconds`:case`millisecond`:case`msecs`:case`msec`:case`ms`:return c;default:return}}}}function l(e){var t=Math.abs(e);return t>=a?Math.round(e/a)+`d`:t>=i?Math.round(e/i)+`h`:t>=r?Math.round(e/r)+`m`:t>=n?Math.round(e/n)+`s`:e+`ms`}function u(e){var t=Math.abs(e);return t>=a?d(e,t,a,`day`):t>=i?d(e,t,i,`hour`):t>=r?d(e,t,r,`minute`):t>=n?d(e,t,n,`second`):e+` ms`}function d(e,t,n,r){var i=t>=n*1.5;return Math.round(e/n)+` `+r+(i?`s`:``)}})),f=o(((e,t)=>{function n(e){n.debug=n,n.default=n,n.coerce=c,n.disable=o,n.enable=i,n.enabled=s,n.humanize=d(),n.destroy=l,Object.keys(e).forEach(t=>{n[t]=e[t]}),n.names=[],n.skips=[],n.formatters={};function t(e){let t=0;for(let n=0;n{if(t===`%%`)return`%`;a++;let o=n.formatters[i];if(typeof o==`function`){let n=e[a];t=o.call(r,n),e.splice(a,1),a--}return t}),n.formatArgs.call(r,e),(r.log||n.log).apply(r,e)}return s.namespace=e,s.useColors=n.useColors(),s.color=n.selectColor(e),s.extend=r,s.destroy=n.destroy,Object.defineProperty(s,"enabled",{enumerable:!0,configurable:!1,get:()=>i===null?(a!==n.namespaces&&(a=n.namespaces,o=n.enabled(e)),o):i,set:e=>{i=e}}),typeof n.init==`function`&&n.init(s),s}function r(e,t){let r=n(this.namespace+(t===void 0?`:`:t)+e);return r.log=this.log,r}function i(e){n.save(e),n.namespaces=e,n.names=[],n.skips=[];let t=(typeof e==`string`?e:``).trim().replace(/\s+/g,`,`).split(`,`).filter(Boolean);for(let e of t)e[0]===`-`?n.skips.push(e.slice(1)):n.names.push(e)}function a(e,t){let n=0,r=0,i=-1,a=0;for(;n`-`+e)].join(`,`);return n.enable(``),e}function s(e){for(let t of n.skips)if(a(e,t))return!1;for(let t of n.names)if(a(e,t))return!0;return!1}function c(e){return e instanceof Error?e.stack||e.message:e}function l(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")}return n.enable(n.load()),n}t.exports=n})),p=l(o(((e,t)=>{e.formatArgs=r,e.save=i,e.load=a,e.useColors=n,e.storage=o(),e.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),e.colors=`#0000CC.#0000FF.#0033CC.#0033FF.#0066CC.#0066FF.#0099CC.#0099FF.#00CC00.#00CC33.#00CC66.#00CC99.#00CCCC.#00CCFF.#3300CC.#3300FF.#3333CC.#3333FF.#3366CC.#3366FF.#3399CC.#3399FF.#33CC00.#33CC33.#33CC66.#33CC99.#33CCCC.#33CCFF.#6600CC.#6600FF.#6633CC.#6633FF.#66CC00.#66CC33.#9900CC.#9900FF.#9933CC.#9933FF.#99CC00.#99CC33.#CC0000.#CC0033.#CC0066.#CC0099.#CC00CC.#CC00FF.#CC3300.#CC3333.#CC3366.#CC3399.#CC33CC.#CC33FF.#CC6600.#CC6633.#CC9900.#CC9933.#CCCC00.#CCCC33.#FF0000.#FF0033.#FF0066.#FF0099.#FF00CC.#FF00FF.#FF3300.#FF3333.#FF3366.#FF3399.#FF33CC.#FF33FF.#FF6600.#FF6633.#FF9900.#FF9933.#FFCC00.#FFCC33`.split(`.`);function n(){if(typeof window<`u`&&window.process&&(window.process.type===`renderer`||window.process.__nwjs))return!0;if(typeof navigator<`u`&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let e;return typeof document<`u`&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<`u`&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<`u`&&navigator.userAgent&&(e=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(e[1],10)>=31||typeof navigator<`u`&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}function r(e){if(e[0]=(this.useColors?`%c`:``)+this.namespace+(this.useColors?` %c`:` `)+e[0]+(this.useColors?`%c `:` `)+`+`+t.exports.humanize(this.diff),!this.useColors)return;let n=`color: `+this.color;e.splice(1,0,n,`color: inherit`);let r=0,i=0;e[0].replace(/%[a-zA-Z%]/g,e=>{e!==`%%`&&(r++,e===`%c`&&(i=r))}),e.splice(i,0,n)}e.log=console.debug||console.log||(()=>{});function i(t){try{t?e.storage.setItem(`debug`,t):e.storage.removeItem(`debug`)}catch{}}function a(){let t;try{t=e.storage.getItem(`debug`)||e.storage.getItem(`DEBUG`)}catch{}return!t&&typeof process<`u`&&`env`in process&&(t=process.env.DEBUG),t}function o(){try{return localStorage}catch{}}t.exports=f()(e);let{formatters:s}=t.exports;s.j=function(e){try{return JSON.stringify(e)}catch(e){return`[UnexpectedJSONParseError]: `+e.message}}}))());let m=function(e){return e.CALENDAR_SERVER=`http://calendarserver.org/ns/`,e.CALDAV_APPLE=`http://apple.com/ns/ical/`,e.CALDAV=`urn:ietf:params:xml:ns:caldav`,e.CARDDAV=`urn:ietf:params:xml:ns:carddav`,e.DAV=`DAV:`,e}({});const h={"urn:ietf:params:xml:ns:caldav":`xmlns:c`,"urn:ietf:params:xml:ns:carddav":`xmlns:card`,"http://calendarserver.org/ns/":`xmlns:cs`,"http://apple.com/ns/ical/":`xmlns:ca`,"DAV:":`xmlns:d`};let g=function(e){return e.CALDAV=`c`,e.CARDDAV=`card`,e.CALENDAR_SERVER=`cs`,e.CALDAV_APPLE=`ca`,e.DAV=`d`,e}({}),_=function(e){return e.VEVENT=`VEVENT`,e.VTODO=`VTODO`,e.VJOURNAL=`VJOURNAL`,e.VFREEBUSY=`VFREEBUSY`,e.VTIMEZONE=`VTIMEZONE`,e.VALARM=`VALARM`,e}({});var v=o((e=>{(function(e){e.parser=function(e,t){return new n(e,t)},e.SAXParser=n,e.SAXStream=d,e.createStream=c,e.MAX_BUFFER_LENGTH=65536;var t=[`comment`,`sgmlDecl`,`textNode`,`tagName`,`doctype`,`procInstName`,`procInstBody`,`entity`,`attribName`,`attribValue`,`cdata`,`script`];e.EVENTS=[`text`,`processinginstruction`,`sgmldeclaration`,`doctype`,`comment`,`opentagstart`,`attribute`,`opentag`,`closetag`,`opencdata`,`cdata`,`closecdata`,`error`,`end`,`ready`,`script`,`opennamespace`,`closenamespace`];function n(t,r){if(!(this instanceof n))return new n(t,r);var a=this;i(a),a.q=a.c=``,a.bufferCheckPosition=e.MAX_BUFFER_LENGTH,a.encoding=null,a.opt=r||{},a.opt.lowercase=a.opt.lowercase||a.opt.lowercasetags,a.looseCase=a.opt.lowercase?`toLowerCase`:`toUpperCase`,a.opt.maxEntityCount=a.opt.maxEntityCount||512,a.opt.maxEntityDepth=a.opt.maxEntityDepth||4,a.entityCount=a.entityDepth=0,a.tags=[],a.closed=a.closedRoot=a.sawRoot=!1,a.tag=a.error=null,a.strict=!!t,a.noscript=!!(t||a.opt.noscript),a.state=T.BEGIN,a.strictEntities=a.opt.strictEntities,a.ENTITIES=a.strictEntities?Object.create(e.XML_ENTITIES):Object.create(e.ENTITIES),a.attribList=[],a.opt.xmlns&&(a.ns=Object.create(g)),a.opt.unquotedAttributeValues===void 0&&(a.opt.unquotedAttributeValues=!t),a.trackPosition=a.opt.position!==!1,a.trackPosition&&(a.position=a.line=a.column=0),E(a,`onready`)}Object.create||(Object.create=function(e){function t(){}return t.prototype=e,new t}),Object.keys||(Object.keys=function(e){var t=[];for(var n in e)e.hasOwnProperty(n)&&t.push(n);return t});function r(n){for(var r=Math.max(e.MAX_BUFFER_LENGTH,10),i=0,a=0,o=t.length;ar)switch(t[a]){case`textNode`:A(n);break;case`cdata`:k(n,`oncdata`,n.cdata),n.cdata=``;break;case`script`:k(n,`onscript`,n.script),n.script=``;break;default:j(n,`Max buffer length exceeded: `+t[a])}i=Math.max(i,s)}n.bufferCheckPosition=e.MAX_BUFFER_LENGTH-i+n.position}function i(e){for(var n=0,r=t.length;n=2){if(e[0]===255&&e[1]===254)return`utf-16le`;if(e[0]===254&&e[1]===255)return`utf-16be`}return e.length>=3&&e[0]===239&&e[1]===187&&e[2]===191?`utf8`:e.length>=4?e[0]===60&&e[1]===0&&e[2]===63&&e[3]===0?`utf-16le`:e[0]===0&&e[1]===60&&e[2]===0&&e[3]===63?`utf-16be`:`utf8`:t?`utf8`:null}function d(e,t){if(!(this instanceof d))return new d(e,t);o.apply(this),this._parser=new n(e,t),this.writable=!0,this.readable=!0;var r=this;this._parser.onend=function(){r.emit(`end`)},this._parser.onerror=function(e){r.emit(`error`,e),r._parser.error=null},this._decoder=null,this._decoderBuffer=null,s.forEach(function(e){Object.defineProperty(r,`on`+e,{get:function(){return r._parser[`on`+e]},set:function(t){if(!t)return r.removeAllListeners(e),r._parser[`on`+e]=t,t;r.on(e,t)},enumerable:!0,configurable:!1})})}d.prototype=Object.create(o.prototype,{constructor:{value:d}}),d.prototype._decodeBuffer=function(e,t){if(this._decoderBuffer&&=(e=Buffer.concat([this._decoderBuffer,e]),null),!this._decoder){var n=l(e,t);if(!n)return this._decoderBuffer=e,``;this._parser.encoding=n,this._decoder=new TextDecoder(n)}return this._decoder.decode(e,{stream:!t})},d.prototype.write=function(e){if(typeof Buffer==`function`&&typeof Buffer.isBuffer==`function`&&Buffer.isBuffer(e))e=this._decodeBuffer(e,!1);else if(this._decoderBuffer){var t=this._decodeBuffer(Buffer.alloc(0),!0);t&&(this._parser.write(t),this.emit(`data`,t))}return this._parser.write(e.toString()),this.emit(`data`,e),!0},d.prototype.end=function(e){if(e&&e.length&&this.write(e),this._decoderBuffer){var t=this._decodeBuffer(Buffer.alloc(0),!0);t&&(this._parser.write(t),this.emit(`data`,t))}else if(this._decoder){var n=this._decoder.decode();n&&(this._parser.write(n),this.emit(`data`,n))}return this._parser.end(),!0},d.prototype.on=function(e,t){var n=this;return!n._parser[`on`+e]&&s.indexOf(e)!==-1&&(n._parser[`on`+e]=function(){var t=arguments.length===1?[arguments[0]]:Array.apply(null,arguments);t.splice(0,0,e),n.emit.apply(n,t)}),o.prototype.on.call(n,e,t)};var f=/^\[CDATA\[$/i,p=/^DOCTYPE$/i,m=`http://www.w3.org/XML/1998/namespace`,h=`http://www.w3.org/2000/xmlns/`,g={xml:m,xmlns:h},_=/[:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/,v=/[:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u00B7\u0300-\u036F\u203F-\u2040.\d-]/,y=/[#:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/,b=/[#:_A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u00B7\u0300-\u036F\u203F-\u2040.\d-]/;function x(e){return e===` `||e===` +`||e===`\r`||e===` `}function S(e){return e===`"`||e===`'`}function C(e){return e===`>`||x(e)}function w(e,t){return e.test(t)}function ee(e,t){return!w(e,t)}var T=0;for(var te in e.STATE={BEGIN:T++,BEGIN_WHITESPACE:T++,TEXT:T++,TEXT_ENTITY:T++,OPEN_WAKA:T++,SGML_DECL:T++,SGML_DECL_QUOTED:T++,DOCTYPE:T++,DOCTYPE_QUOTED:T++,DOCTYPE_DTD:T++,DOCTYPE_DTD_QUOTED:T++,COMMENT_STARTING:T++,COMMENT:T++,COMMENT_ENDING:T++,COMMENT_ENDED:T++,CDATA:T++,CDATA_ENDING:T++,CDATA_ENDING_2:T++,PROC_INST:T++,PROC_INST_BODY:T++,PROC_INST_ENDING:T++,OPEN_TAG:T++,OPEN_TAG_SLASH:T++,ATTRIB:T++,ATTRIB_NAME:T++,ATTRIB_NAME_SAW_WHITE:T++,ATTRIB_VALUE:T++,ATTRIB_VALUE_QUOTED:T++,ATTRIB_VALUE_CLOSED:T++,ATTRIB_VALUE_UNQUOTED:T++,ATTRIB_VALUE_ENTITY_Q:T++,ATTRIB_VALUE_ENTITY_U:T++,CLOSE_TAG:T++,CLOSE_TAG_SAW_WHITE:T++,SCRIPT:T++,SCRIPT_ENDING:T++},e.XML_ENTITIES=Object.assign(Object.create(null),{amp:`&`,gt:`>`,lt:`<`,quot:`"`,apos:`'`}),e.ENTITIES=Object.assign(Object.create(null),{amp:`&`,gt:`>`,lt:`<`,quot:`"`,apos:`'`,AElig:198,Aacute:193,Acirc:194,Agrave:192,Aring:197,Atilde:195,Auml:196,Ccedil:199,ETH:208,Eacute:201,Ecirc:202,Egrave:200,Euml:203,Iacute:205,Icirc:206,Igrave:204,Iuml:207,Ntilde:209,Oacute:211,Ocirc:212,Ograve:210,Oslash:216,Otilde:213,Ouml:214,THORN:222,Uacute:218,Ucirc:219,Ugrave:217,Uuml:220,Yacute:221,aacute:225,acirc:226,aelig:230,agrave:224,aring:229,atilde:227,auml:228,ccedil:231,eacute:233,ecirc:234,egrave:232,eth:240,euml:235,iacute:237,icirc:238,igrave:236,iuml:239,ntilde:241,oacute:243,ocirc:244,ograve:242,oslash:248,otilde:245,ouml:246,szlig:223,thorn:254,uacute:250,ucirc:251,ugrave:249,uuml:252,yacute:253,yuml:255,copy:169,reg:174,nbsp:160,iexcl:161,cent:162,pound:163,curren:164,yen:165,brvbar:166,sect:167,uml:168,ordf:170,laquo:171,not:172,shy:173,macr:175,deg:176,plusmn:177,sup1:185,sup2:178,sup3:179,acute:180,micro:181,para:182,middot:183,cedil:184,ordm:186,raquo:187,frac14:188,frac12:189,frac34:190,iquest:191,times:215,divide:247,OElig:338,oelig:339,Scaron:352,scaron:353,Yuml:376,fnof:402,circ:710,tilde:732,Alpha:913,Beta:914,Gamma:915,Delta:916,Epsilon:917,Zeta:918,Eta:919,Theta:920,Iota:921,Kappa:922,Lambda:923,Mu:924,Nu:925,Xi:926,Omicron:927,Pi:928,Rho:929,Sigma:931,Tau:932,Upsilon:933,Phi:934,Chi:935,Psi:936,Omega:937,alpha:945,beta:946,gamma:947,delta:948,epsilon:949,zeta:950,eta:951,theta:952,iota:953,kappa:954,lambda:955,mu:956,nu:957,xi:958,omicron:959,pi:960,rho:961,sigmaf:962,sigma:963,tau:964,upsilon:965,phi:966,chi:967,psi:968,omega:969,thetasym:977,upsih:978,piv:982,ensp:8194,emsp:8195,thinsp:8201,zwnj:8204,zwj:8205,lrm:8206,rlm:8207,ndash:8211,mdash:8212,lsquo:8216,rsquo:8217,sbquo:8218,ldquo:8220,rdquo:8221,bdquo:8222,dagger:8224,Dagger:8225,bull:8226,hellip:8230,permil:8240,prime:8242,Prime:8243,lsaquo:8249,rsaquo:8250,oline:8254,frasl:8260,euro:8364,image:8465,weierp:8472,real:8476,trade:8482,alefsym:8501,larr:8592,uarr:8593,rarr:8594,darr:8595,harr:8596,crarr:8629,lArr:8656,uArr:8657,rArr:8658,dArr:8659,hArr:8660,forall:8704,part:8706,exist:8707,empty:8709,nabla:8711,isin:8712,notin:8713,ni:8715,prod:8719,sum:8721,minus:8722,lowast:8727,radic:8730,prop:8733,infin:8734,ang:8736,and:8743,or:8744,cap:8745,cup:8746,int:8747,there4:8756,sim:8764,cong:8773,asymp:8776,ne:8800,equiv:8801,le:8804,ge:8805,sub:8834,sup:8835,nsub:8836,sube:8838,supe:8839,oplus:8853,otimes:8855,perp:8869,sdot:8901,lceil:8968,rceil:8969,lfloor:8970,rfloor:8971,lang:9001,rang:9002,loz:9674,spades:9824,clubs:9827,hearts:9829,diams:9830}),Object.keys(e.ENTITIES).forEach(function(t){var n=e.ENTITIES[t],r=typeof n==`number`?String.fromCharCode(n):n;e.ENTITIES[t]=r}),e.STATE)e.STATE[e.STATE[te]]=te;T=e.STATE;function E(e,t,n){e[t]&&e[t](n)}function D(e){var t=e&&e.match(/(?:^|\s)encoding\s*=\s*(['"])([^'"]+)\1/i);return t?t[2]:null}function O(e){return e?e.toLowerCase().replace(/[^a-z0-9]/g,``):null}function ne(e,t){let n=O(e),r=O(t);return!n||!r?!0:r===`utf16`?n===`utf16le`||n===`utf16be`:n===r}function re(e,t){if(e.strict&&e.encoding&&t&&t.name===`xml`){var n=D(t.body);n&&!ne(e.encoding,n)&&M(e,`XML declaration encoding `+n+` does not match detected stream encoding `+e.encoding.toUpperCase())}}function k(e,t,n){e.textNode&&A(e),E(e,t,n)}function A(e){e.textNode=ie(e.opt,e.textNode),e.textNode&&E(e,`ontext`,e.textNode),e.textNode=``}function ie(e,t){return e.trim&&(t=t.trim()),e.normalize&&(t=t.replace(/\s+/g,` `)),t}function j(e,t){return A(e),e.trackPosition&&(t+=` Line: `+e.line+` Column: `+e.column+` -Char: `+e.c),t=Error(t),e.error=t,E(e,`onerror`,t),e}function N(e){return e.sawRoot&&!e.closedRoot&&P(e,`Unclosed root tag`),e.state!==w.BEGIN&&e.state!==w.BEGIN_WHITESPACE&&e.state!==w.TEXT&&M(e,`Unexpected end`),A(e),e.c=``,e.closed=!0,E(e,`onend`),n.call(e,e.strict,e.opt),e}function P(e,t){if(typeof e!=`object`||!(e instanceof n))throw Error(`bad call to strictFail`);e.strict&&M(e,t)}function F(e){e.strict||(e.tagName=e.tagName[e.looseCase]());var t=e.tags[e.tags.length-1]||e,n=e.tag={name:e.tagName,attributes:{}};e.opt.xmlns&&(n.ns=t.ns),e.attribList.length=0,k(e,`onopentagstart`,n)}function I(e,t){var n=e.indexOf(`:`)<0?[``,e]:e.split(`:`),r=n[0],i=n[1];return t&&e===`xmlns`&&(r=`xmlns`,i=``),{prefix:r,local:i}}function ie(e){if(e.strict||(e.attribName=e.attribName[e.looseCase]()),e.attribList.indexOf(e.attribName)!==-1||e.tag.attributes.hasOwnProperty(e.attribName)){e.attribName=e.attribValue=``;return}if(e.opt.xmlns){var t=I(e.attribName,!0),n=t.prefix,r=t.local;if(n===`xmlns`){if(r===`xml`&&e.attribValue!==m)P(e,`xml: prefix must be bound to `+m+` -Actual: `+e.attribValue);else if(r===`xmlns`&&e.attribValue!==h)P(e,`xmlns: prefix must be bound to `+h+` -Actual: `+e.attribValue);else{var i=e.tag,a=e.tags[e.tags.length-1]||e;i.ns===a.ns&&(i.ns=Object.create(a.ns)),i.ns[r]=e.attribValue}}e.attribList.push([e.attribName,e.attribValue])}else e.tag.attributes[e.attribName]=e.attribValue,k(e,`onattribute`,{name:e.attribName,value:e.attribValue});e.attribName=e.attribValue=``}function L(e,t){if(e.opt.xmlns){var n=e.tag,r=I(e.tagName);n.prefix=r.prefix,n.local=r.local,n.uri=n.ns[r.prefix]||``,n.prefix&&!n.uri&&(P(e,`Unbound namespace prefix: `+JSON.stringify(e.tagName)),n.uri=r.prefix);var i=e.tags[e.tags.length-1]||e;n.ns&&i.ns!==n.ns&&Object.keys(n.ns).forEach(function(t){k(e,`onopennamespace`,{prefix:t,uri:n.ns[t]})});for(var a=0,o=e.attribList.length;a`,e.state=w.TEXT;return}if(e.script){if(e.tagName!==`script`){e.script+=``,e.tagName=``,e.state=w.SCRIPT;return}k(e,`onscript`,e.script),e.script=``}var t=e.tags.length,n=e.tagName;e.strict||(n=n[e.looseCase]());for(var r=n;t--&&e.tags[t].name!==r;)P(e,`Unexpected close tag`);if(t<0){P(e,`Unmatched closing tag: `+e.tagName),e.textNode+=``,e.state=w.TEXT;return}e.tagName=n;for(var i=e.tags.length;i-->t;){var a=e.tag=e.tags.pop();e.tagName=e.tag.name,k(e,`onclosetag`,e.tagName);var o={};for(var s in a.ns)o[s]=a.ns[s];var c=e.tags[e.tags.length-1]||e;e.opt.xmlns&&a.ns!==c.ns&&Object.keys(a.ns).forEach(function(t){var n=a.ns[t];k(e,`onclosenamespace`,{prefix:t,uri:n})})}t===0&&(e.closedRoot=!0),e.tagName=e.attribValue=e.attribName=``,e.attribList.length=0,e.state=w.TEXT}function R(e){var t=e.entity,n=t.toLowerCase(),r,i=``;return e.ENTITIES[t]?e.ENTITIES[t]:e.ENTITIES[n]?e.ENTITIES[n]:(t=n,t.charAt(0)===`#`&&(t.charAt(1)===`x`?(t=t.slice(2),r=parseInt(t,16),i=r.toString(16)):(t=t.slice(1),r=parseInt(t,10),i=r.toString(10))),t=t.replace(/^0+/,``),isNaN(r)||i.toLowerCase()!==t||r<0||r>1114111||!z(r)?(P(e,`Invalid character entity`),`&`+e.entity+`;`):String.fromCodePoint(r))}function z(e){return e===9||e===10||e===13||e>=32&&e<=55295||e>=57344&&e<=65533||e>=65536&&e<=1114111}function B(e,t){t===`<`?(e.state=w.OPEN_WAKA,e.startTagPosition=e.position):x(t)||(P(e,`Non-whitespace before first tag.`),e.textNode=t,e.state=w.TEXT)}function V(e,t){var n=``;return t`?(k(n,`onsgmldeclaration`,n.sgmlDecl),n.sgmlDecl=``,n.state=w.TEXT):(S(a)&&(n.state=w.SGML_DECL_QUOTED),n.sgmlDecl+=a);continue;case w.SGML_DECL_QUOTED:a===n.q&&(n.state=w.SGML_DECL,n.q=``),n.sgmlDecl+=a;continue;case w.DOCTYPE:a===`>`?(n.state=w.TEXT,k(n,`ondoctype`,n.doctype),n.doctype=!0):(n.doctype+=a,a===`[`?n.state=w.DOCTYPE_DTD:S(a)&&(n.state=w.DOCTYPE_QUOTED,n.q=a));continue;case w.DOCTYPE_QUOTED:n.doctype+=a,a===n.q&&(n.q=``,n.state=w.DOCTYPE);continue;case w.DOCTYPE_DTD:a===`]`?(n.doctype+=a,n.state=w.DOCTYPE):a===`<`?(n.state=w.OPEN_WAKA,n.startTagPosition=n.position):S(a)?(n.doctype+=a,n.state=w.DOCTYPE_DTD_QUOTED,n.q=a):n.doctype+=a;continue;case w.DOCTYPE_DTD_QUOTED:n.doctype+=a,a===n.q&&(n.state=w.DOCTYPE_DTD,n.q=``);continue;case w.COMMENT:a===`-`?n.state=w.COMMENT_ENDING:n.comment+=a;continue;case w.COMMENT_ENDING:a===`-`?(n.state=w.COMMENT_ENDED,n.comment=j(n.opt,n.comment),n.comment&&k(n,`oncomment`,n.comment),n.comment=``):(n.comment+=`-`+a,n.state=w.COMMENT);continue;case w.COMMENT_ENDED:a===`>`?n.state=n.doctype&&n.doctype!==!0?w.DOCTYPE_DTD:w.TEXT:(P(n,`Malformed comment`),n.comment+=`--`+a,n.state=w.COMMENT);continue;case w.CDATA:for(var o=i-1;a&&a!==`]`;)a=V(t,i++),a&&n.trackPosition&&(n.position++,a===` -`?(n.line++,n.column=0):n.column++);n.cdata+=t.substring(o,i-1),a===`]`&&(n.state=w.CDATA_ENDING);continue;case w.CDATA_ENDING:a===`]`?n.state=w.CDATA_ENDING_2:(n.cdata+=`]`+a,n.state=w.CDATA);continue;case w.CDATA_ENDING_2:a===`>`?(n.cdata&&k(n,`oncdata`,n.cdata),k(n,`onclosecdata`),n.cdata=``,n.state=w.TEXT):a===`]`?n.cdata+=`]`:(n.cdata+=`]]`+a,n.state=w.CDATA);continue;case w.PROC_INST:a===`?`?n.state=w.PROC_INST_ENDING:x(a)?n.state=w.PROC_INST_BODY:n.procInstName+=a;continue;case w.PROC_INST_BODY:if(!n.procInstBody&&x(a))continue;a===`?`?n.state=w.PROC_INST_ENDING:n.procInstBody+=a;continue;case w.PROC_INST_ENDING:if(a===`>`){let e={name:n.procInstName,body:n.procInstBody};O(n,e),k(n,`onprocessinginstruction`,e),n.procInstName=n.procInstBody=``,n.state=w.TEXT}else n.procInstBody+=`?`+a,n.state=w.PROC_INST_BODY;continue;case w.OPEN_TAG:C(v,a)?n.tagName+=a:(F(n),a===`>`?L(n):a===`/`?n.state=w.OPEN_TAG_SLASH:(x(a)||P(n,`Invalid character in tag name`),n.state=w.ATTRIB));continue;case w.OPEN_TAG_SLASH:a===`>`?(L(n,!0),ae(n)):(P(n,`Forward-slash in opening tag not followed by >`),n.state=w.ATTRIB);continue;case w.ATTRIB:if(x(a))continue;a===`>`?L(n):a===`/`?n.state=w.OPEN_TAG_SLASH:C(_,a)?(n.attribName=a,n.attribValue=``,n.state=w.ATTRIB_NAME):P(n,`Invalid attribute name`);continue;case w.ATTRIB_NAME:a===`=`?n.state=w.ATTRIB_VALUE:a===`>`?(P(n,`Attribute without value`),n.attribValue=n.attribName,ie(n),L(n)):x(a)?n.state=w.ATTRIB_NAME_SAW_WHITE:C(v,a)?n.attribName+=a:P(n,`Invalid attribute name`);continue;case w.ATTRIB_NAME_SAW_WHITE:if(a===`=`)n.state=w.ATTRIB_VALUE;else if(x(a))continue;else P(n,`Attribute without value`),n.tag.attributes[n.attribName]=``,n.attribValue=``,k(n,`onattribute`,{name:n.attribName,value:``}),n.attribName=``,a===`>`?L(n):C(_,a)?(n.attribName=a,n.state=w.ATTRIB_NAME):(P(n,`Invalid attribute name`),n.state=w.ATTRIB);continue;case w.ATTRIB_VALUE:if(x(a))continue;S(a)?(n.q=a,n.state=w.ATTRIB_VALUE_QUOTED):(n.opt.unquotedAttributeValues||M(n,`Unquoted attribute value`),n.state=w.ATTRIB_VALUE_UNQUOTED,n.attribValue=a);continue;case w.ATTRIB_VALUE_QUOTED:if(a!==n.q){a===`&`?n.state=w.ATTRIB_VALUE_ENTITY_Q:n.attribValue+=a;continue}ie(n),n.q=``,n.state=w.ATTRIB_VALUE_CLOSED;continue;case w.ATTRIB_VALUE_CLOSED:x(a)?n.state=w.ATTRIB:a===`>`?L(n):a===`/`?n.state=w.OPEN_TAG_SLASH:C(_,a)?(P(n,`No whitespace between attributes`),n.attribName=a,n.attribValue=``,n.state=w.ATTRIB_NAME):P(n,`Invalid attribute name`);continue;case w.ATTRIB_VALUE_UNQUOTED:if(!ee(a)){a===`&`?n.state=w.ATTRIB_VALUE_ENTITY_U:n.attribValue+=a;continue}ie(n),a===`>`?L(n):n.state=w.ATTRIB;continue;case w.CLOSE_TAG:if(n.tagName)a===`>`?ae(n):C(v,a)?n.tagName+=a:n.script?(n.script+=``?ae(n):P(n,`Invalid characters in closing tag`);continue;case w.TEXT_ENTITY:case w.ATTRIB_VALUE_ENTITY_Q:case w.ATTRIB_VALUE_ENTITY_U:var c,l;switch(n.state){case w.TEXT_ENTITY:c=w.TEXT,l=`textNode`;break;case w.ATTRIB_VALUE_ENTITY_Q:c=w.ATTRIB_VALUE_QUOTED,l=`attribValue`;break;case w.ATTRIB_VALUE_ENTITY_U:c=w.ATTRIB_VALUE_UNQUOTED,l=`attribValue`}if(a===`;`){var u=R(n);n.opt.unparsedEntities&&!Object.values(e.XML_ENTITIES).includes(u)?((n.entityCount+=1)>n.opt.maxEntityCount&&M(n,`Parsed entity count exceeds max entity count`),(n.entityDepth+=1)>n.opt.maxEntityDepth&&M(n,`Parsed entity depth exceeds max entity depth`),n.entity=``,n.state=c,n.write(u),--n.entityDepth):(n[l]+=u,n.entity=``,n.state=c)}else C(n.entity.length?b:y,a)?n.entity+=a:(P(n,`Invalid character in entity name`),n[l]+=`&`+n.entity+a,n.entity=``,n.state=c);continue;default:throw Error(n,`Unknown state: `+n.state)}return n.position>=n.bufferCheckPosition&&r(n),n} +Char: `+e.c),t=Error(t),e.error=t,E(e,`onerror`,t),e}function ae(e){return e.sawRoot&&!e.closedRoot&&M(e,`Unclosed root tag`),e.state!==T.BEGIN&&e.state!==T.BEGIN_WHITESPACE&&e.state!==T.TEXT&&j(e,`Unexpected end`),A(e),e.c=``,e.closed=!0,E(e,`onend`),n.call(e,e.strict,e.opt),e}function M(e,t){if(typeof e!=`object`||!(e instanceof n))throw Error(`bad call to strictFail`);e.strict&&j(e,t)}function N(e){e.strict||(e.tagName=e.tagName[e.looseCase]());var t=e.tags[e.tags.length-1]||e,n=e.tag={name:e.tagName,attributes:{}};e.opt.xmlns&&(n.ns=t.ns),e.attribList.length=0,k(e,`onopentagstart`,n)}function P(e,t){var n=e.indexOf(`:`)<0?[``,e]:e.split(`:`),r=n[0],i=n[1];return t&&e===`xmlns`&&(r=`xmlns`,i=``),{prefix:r,local:i}}function F(e){if(e.strict||(e.attribName=e.attribName[e.looseCase]()),e.attribList.indexOf(e.attribName)!==-1||e.tag.attributes.hasOwnProperty(e.attribName)){e.attribName=e.attribValue=``;return}if(e.opt.xmlns){var t=P(e.attribName,!0),n=t.prefix,r=t.local;if(n===`xmlns`){if(r===`xml`&&e.attribValue!==m)M(e,`xml: prefix must be bound to `+m+` +Actual: `+e.attribValue);else if(r===`xmlns`&&e.attribValue!==h)M(e,`xmlns: prefix must be bound to `+h+` +Actual: `+e.attribValue);else{var i=e.tag,a=e.tags[e.tags.length-1]||e;i.ns===a.ns&&(i.ns=Object.create(a.ns)),i.ns[r]=e.attribValue}}e.attribList.push([e.attribName,e.attribValue])}else e.tag.attributes[e.attribName]=e.attribValue,k(e,`onattribute`,{name:e.attribName,value:e.attribValue});e.attribName=e.attribValue=``}function I(e,t){if(e.opt.xmlns){var n=e.tag,r=P(e.tagName);n.prefix=r.prefix,n.local=r.local,n.uri=n.ns[r.prefix]||``,n.prefix&&!n.uri&&(M(e,`Unbound namespace prefix: `+JSON.stringify(e.tagName)),n.uri=r.prefix);var i=e.tags[e.tags.length-1]||e;n.ns&&i.ns!==n.ns&&Object.keys(n.ns).forEach(function(t){k(e,`onopennamespace`,{prefix:t,uri:n.ns[t]})});for(var a=0,o=e.attribList.length;a`,e.state=T.TEXT;return}if(e.script){if(e.tagName!==`script`){e.script+=``,e.tagName=``,e.state=T.SCRIPT;return}k(e,`onscript`,e.script),e.script=``}var t=e.tags.length,n=e.tagName;e.strict||(n=n[e.looseCase]());for(var r=n;t--&&e.tags[t].name!==r;)M(e,`Unexpected close tag`);if(t<0){M(e,`Unmatched closing tag: `+e.tagName),e.textNode+=``,e.state=T.TEXT;return}e.tagName=n;for(var i=e.tags.length;i-->t;){var a=e.tag=e.tags.pop();e.tagName=e.tag.name,k(e,`onclosetag`,e.tagName);var o={};for(var s in a.ns)o[s]=a.ns[s];var c=e.tags[e.tags.length-1]||e;e.opt.xmlns&&a.ns!==c.ns&&Object.keys(a.ns).forEach(function(t){var n=a.ns[t];k(e,`onclosenamespace`,{prefix:t,uri:n})})}t===0&&(e.closedRoot=!0),e.tagName=e.attribValue=e.attribName=``,e.attribList.length=0,e.state=T.TEXT}function R(e){var t=e.entity,n=t.toLowerCase(),r,i=``;return e.ENTITIES[t]?e.ENTITIES[t]:e.ENTITIES[n]?e.ENTITIES[n]:(t=n,t.charAt(0)===`#`&&(t.charAt(1)===`x`?(t=t.slice(2),r=parseInt(t,16),i=r.toString(16)):(t=t.slice(1),r=parseInt(t,10),i=r.toString(10))),t=t.replace(/^0+/,``),isNaN(r)||i.toLowerCase()!==t||r<0||r>1114111||!oe(r)?(M(e,`Invalid character entity`),`&`+e.entity+`;`):String.fromCodePoint(r))}function oe(e){return e===9||e===10||e===13||e>=32&&e<=55295||e>=57344&&e<=65533||e>=65536&&e<=1114111}function z(e,t){t===`<`?(e.state=T.OPEN_WAKA,e.startTagPosition=e.position):x(t)||(M(e,`Non-whitespace before first tag.`),e.textNode=t,e.state=T.TEXT)}function B(e,t){var n=``;return t`?(k(n,`onsgmldeclaration`,n.sgmlDecl),n.sgmlDecl=``,n.state=T.TEXT):(S(a)&&(n.state=T.SGML_DECL_QUOTED),n.sgmlDecl+=a);continue;case T.SGML_DECL_QUOTED:a===n.q&&(n.state=T.SGML_DECL,n.q=``),n.sgmlDecl+=a;continue;case T.DOCTYPE:a===`>`?(n.state=T.TEXT,k(n,`ondoctype`,n.doctype),n.doctype=!0):(n.doctype+=a,a===`[`?n.state=T.DOCTYPE_DTD:S(a)&&(n.state=T.DOCTYPE_QUOTED,n.q=a));continue;case T.DOCTYPE_QUOTED:n.doctype+=a,a===n.q&&(n.q=``,n.state=T.DOCTYPE);continue;case T.DOCTYPE_DTD:a===`]`?(n.doctype+=a,n.state=T.DOCTYPE):a===`<`?(n.state=T.OPEN_WAKA,n.startTagPosition=n.position):S(a)?(n.doctype+=a,n.state=T.DOCTYPE_DTD_QUOTED,n.q=a):n.doctype+=a;continue;case T.DOCTYPE_DTD_QUOTED:n.doctype+=a,a===n.q&&(n.state=T.DOCTYPE_DTD,n.q=``);continue;case T.COMMENT:a===`-`?n.state=T.COMMENT_ENDING:n.comment+=a;continue;case T.COMMENT_ENDING:a===`-`?(n.state=T.COMMENT_ENDED,n.comment=ie(n.opt,n.comment),n.comment&&k(n,`oncomment`,n.comment),n.comment=``):(n.comment+=`-`+a,n.state=T.COMMENT);continue;case T.COMMENT_ENDED:a===`>`?n.state=n.doctype&&n.doctype!==!0?T.DOCTYPE_DTD:T.TEXT:(M(n,`Malformed comment`),n.comment+=`--`+a,n.state=T.COMMENT);continue;case T.CDATA:for(var o=i-1;a&&a!==`]`;)a=B(t,i++),a&&n.trackPosition&&(n.position++,a===` +`?(n.line++,n.column=0):n.column++);n.cdata+=t.substring(o,i-1),a===`]`&&(n.state=T.CDATA_ENDING);continue;case T.CDATA_ENDING:a===`]`?n.state=T.CDATA_ENDING_2:(n.cdata+=`]`+a,n.state=T.CDATA);continue;case T.CDATA_ENDING_2:a===`>`?(n.cdata&&k(n,`oncdata`,n.cdata),k(n,`onclosecdata`),n.cdata=``,n.state=T.TEXT):a===`]`?n.cdata+=`]`:(n.cdata+=`]]`+a,n.state=T.CDATA);continue;case T.PROC_INST:a===`?`?n.state=T.PROC_INST_ENDING:x(a)?n.state=T.PROC_INST_BODY:n.procInstName+=a;continue;case T.PROC_INST_BODY:if(!n.procInstBody&&x(a))continue;a===`?`?n.state=T.PROC_INST_ENDING:n.procInstBody+=a;continue;case T.PROC_INST_ENDING:if(a===`>`){let e={name:n.procInstName,body:n.procInstBody};re(n,e),k(n,`onprocessinginstruction`,e),n.procInstName=n.procInstBody=``,n.state=T.TEXT}else n.procInstBody+=`?`+a,n.state=T.PROC_INST_BODY;continue;case T.OPEN_TAG:w(v,a)?n.tagName+=a:(N(n),a===`>`?I(n):a===`/`?n.state=T.OPEN_TAG_SLASH:(x(a)||M(n,`Invalid character in tag name`),n.state=T.ATTRIB));continue;case T.OPEN_TAG_SLASH:a===`>`?(I(n,!0),L(n)):(M(n,`Forward-slash in opening tag not followed by >`),n.state=T.ATTRIB);continue;case T.ATTRIB:if(x(a))continue;a===`>`?I(n):a===`/`?n.state=T.OPEN_TAG_SLASH:w(_,a)?(n.attribName=a,n.attribValue=``,n.state=T.ATTRIB_NAME):M(n,`Invalid attribute name`);continue;case T.ATTRIB_NAME:a===`=`?n.state=T.ATTRIB_VALUE:a===`>`?(M(n,`Attribute without value`),n.attribValue=n.attribName,F(n),I(n)):x(a)?n.state=T.ATTRIB_NAME_SAW_WHITE:w(v,a)?n.attribName+=a:M(n,`Invalid attribute name`);continue;case T.ATTRIB_NAME_SAW_WHITE:if(a===`=`)n.state=T.ATTRIB_VALUE;else if(x(a))continue;else M(n,`Attribute without value`),n.tag.attributes[n.attribName]=``,n.attribValue=``,k(n,`onattribute`,{name:n.attribName,value:``}),n.attribName=``,a===`>`?I(n):w(_,a)?(n.attribName=a,n.state=T.ATTRIB_NAME):(M(n,`Invalid attribute name`),n.state=T.ATTRIB);continue;case T.ATTRIB_VALUE:if(x(a))continue;S(a)?(n.q=a,n.state=T.ATTRIB_VALUE_QUOTED):(n.opt.unquotedAttributeValues||j(n,`Unquoted attribute value`),n.state=T.ATTRIB_VALUE_UNQUOTED,n.attribValue=a);continue;case T.ATTRIB_VALUE_QUOTED:if(a!==n.q){a===`&`?n.state=T.ATTRIB_VALUE_ENTITY_Q:n.attribValue+=a;continue}F(n),n.q=``,n.state=T.ATTRIB_VALUE_CLOSED;continue;case T.ATTRIB_VALUE_CLOSED:x(a)?n.state=T.ATTRIB:a===`>`?I(n):a===`/`?n.state=T.OPEN_TAG_SLASH:w(_,a)?(M(n,`No whitespace between attributes`),n.attribName=a,n.attribValue=``,n.state=T.ATTRIB_NAME):M(n,`Invalid attribute name`);continue;case T.ATTRIB_VALUE_UNQUOTED:if(!C(a)){a===`&`?n.state=T.ATTRIB_VALUE_ENTITY_U:n.attribValue+=a;continue}F(n),a===`>`?I(n):n.state=T.ATTRIB;continue;case T.CLOSE_TAG:if(n.tagName)a===`>`?L(n):w(v,a)?n.tagName+=a:n.script?(n.script+=``?L(n):M(n,`Invalid characters in closing tag`);continue;case T.TEXT_ENTITY:case T.ATTRIB_VALUE_ENTITY_Q:case T.ATTRIB_VALUE_ENTITY_U:var c,l;switch(n.state){case T.TEXT_ENTITY:c=T.TEXT,l=`textNode`;break;case T.ATTRIB_VALUE_ENTITY_Q:c=T.ATTRIB_VALUE_QUOTED,l=`attribValue`;break;case T.ATTRIB_VALUE_ENTITY_U:c=T.ATTRIB_VALUE_UNQUOTED,l=`attribValue`}if(a===`;`){var u=R(n);n.opt.unparsedEntities&&!Object.values(e.XML_ENTITIES).includes(u)?((n.entityCount+=1)>n.opt.maxEntityCount&&j(n,`Parsed entity count exceeds max entity count`),(n.entityDepth+=1)>n.opt.maxEntityDepth&&j(n,`Parsed entity depth exceeds max entity depth`),n.entity=``,n.state=c,n.write(u),--n.entityDepth):(n[l]+=u,n.entity=``,n.state=c)}else w(n.entity.length?b:y,a)?n.entity+=a:(M(n,`Invalid character in entity name`),n[l]+=`&`+n.entity+a,n.entity=``,n.state=c);continue;default:throw Error(n,`Unknown state: `+n.state)}return n.position>=n.bufferCheckPosition&&r(n),n} /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ -String.fromCodePoint||(function(){var e=String.fromCharCode,t=Math.floor,n=function(){var n=16384,r=[],i,a,o=-1,s=arguments.length;if(!s)return``;for(var c=``;++o1114111||t(l)!==l)throw RangeError(`Invalid code point: `+l);l<=65535?r.push(l):(l-=65536,i=(l>>10)+55296,a=l%1024+56320,r.push(i,a)),(o+1===s||r.length>n)&&(c+=e.apply(null,r),r.length=0)}return c};Object.defineProperty?Object.defineProperty(String,"fromCodePoint",{value:n,configurable:!0,writable:!0}):String.fromCodePoint=n})()})(e===void 0?e.sax={}:e)})),y=o(((e,t)=>{t.exports={isArray:function(e){return Array.isArray?Array.isArray(e):Object.prototype.toString.call(e)===`[object Array]`}}})),b=o(((e,t)=>{var n=y().isArray;t.exports={copyOptions:function(e){var t,n={};for(t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);return n},ensureFlagExists:function(e,t){(!(e in t)||typeof t[e]!=`boolean`)&&(t[e]=!1)},ensureSpacesExists:function(e){(!(`spaces`in e)||typeof e.spaces!=`number`&&typeof e.spaces!=`string`)&&(e.spaces=0)},ensureAlwaysArrayExists:function(e){(!(`alwaysArray`in e)||typeof e.alwaysArray!=`boolean`&&!n(e.alwaysArray))&&(e.alwaysArray=!1)},ensureKeyExists:function(e,t){(!(e+`Key`in t)||typeof t[e+`Key`]!=`string`)&&(t[e+`Key`]=t.compact?`_`+e:e)},checkFnExists:function(e,t){return e+`Fn`in t}}})),x=o(((e,t)=>{var n=v(),r={on:function(){},parse:function(){}},i=b(),a=y().isArray,o,s=!0,c;function l(e){return o=i.copyOptions(e),i.ensureFlagExists(`ignoreDeclaration`,o),i.ensureFlagExists(`ignoreInstruction`,o),i.ensureFlagExists(`ignoreAttributes`,o),i.ensureFlagExists(`ignoreText`,o),i.ensureFlagExists(`ignoreComment`,o),i.ensureFlagExists(`ignoreCdata`,o),i.ensureFlagExists(`ignoreDoctype`,o),i.ensureFlagExists(`compact`,o),i.ensureFlagExists(`alwaysChildren`,o),i.ensureFlagExists(`addParent`,o),i.ensureFlagExists(`trim`,o),i.ensureFlagExists(`nativeType`,o),i.ensureFlagExists(`nativeTypeAttributes`,o),i.ensureFlagExists(`sanitize`,o),i.ensureFlagExists(`instructionHasAttributes`,o),i.ensureFlagExists(`captureSpacesBetweenElements`,o),i.ensureAlwaysArrayExists(o),i.ensureKeyExists(`declaration`,o),i.ensureKeyExists(`instruction`,o),i.ensureKeyExists(`attributes`,o),i.ensureKeyExists(`text`,o),i.ensureKeyExists(`comment`,o),i.ensureKeyExists(`cdata`,o),i.ensureKeyExists(`doctype`,o),i.ensureKeyExists(`type`,o),i.ensureKeyExists(`name`,o),i.ensureKeyExists(`elements`,o),i.ensureKeyExists(`parent`,o),i.checkFnExists(`doctype`,o),i.checkFnExists(`instruction`,o),i.checkFnExists(`cdata`,o),i.checkFnExists(`comment`,o),i.checkFnExists(`text`,o),i.checkFnExists(`instructionName`,o),i.checkFnExists(`elementName`,o),i.checkFnExists(`attributeName`,o),i.checkFnExists(`attributeValue`,o),i.checkFnExists(`attributes`,o),o}function u(e){var t=Number(e);if(!isNaN(t))return t;var n=e.toLowerCase();return n===`true`||n!==`false`&&e}function d(e,t){var n;if(o.compact){if(!c[o[e+`Key`]]&&(a(o.alwaysArray)?o.alwaysArray.indexOf(o[e+`Key`])!==-1:o.alwaysArray)&&(c[o[e+`Key`]]=[]),c[o[e+`Key`]]&&!a(c[o[e+`Key`]])&&(c[o[e+`Key`]]=[c[o[e+`Key`]]]),e+`Fn`in o&&typeof t==`string`&&(t=o[e+`Fn`](t,c)),e===`instruction`&&(`instructionFn`in o||`instructionNameFn`in o)){for(n in t)if(t.hasOwnProperty(n)){if(`instructionFn`in o)t[n]=o.instructionFn(t[n],n,c);else{var r=t[n];delete t[n],t[o.instructionNameFn(n,r,c)]=r}}}a(c[o[e+`Key`]])?c[o[e+`Key`]].push(t):c[o[e+`Key`]]=t}else{c[o.elementsKey]||(c[o.elementsKey]=[]);var i={};if(i[o.typeKey]=e,e===`instruction`){for(n in t)if(t.hasOwnProperty(n))break;i[o.nameKey]=`instructionNameFn`in o?o.instructionNameFn(n,t,c):n,o.instructionHasAttributes?(i[o.attributesKey]=t[n][o.attributesKey],`instructionFn`in o&&(i[o.attributesKey]=o.instructionFn(i[o.attributesKey],n,c))):(`instructionFn`in o&&(t[n]=o.instructionFn(t[n],n,c)),i[o.instructionKey]=t[n])}else e+`Fn`in o&&(t=o[e+`Fn`](t,c)),i[o[e+`Key`]]=t;o.addParent&&(i[o.parentKey]=c),c[o.elementsKey].push(i)}}function f(e){if(`attributesFn`in o&&e&&(e=o.attributesFn(e,c)),(o.trim||`attributeValueFn`in o||`attributeNameFn`in o||o.nativeTypeAttributes)&&e){for(var t in e)if(e.hasOwnProperty(t)&&(o.trim&&(e[t]=e[t].trim()),o.nativeTypeAttributes&&(e[t]=u(e[t])),`attributeValueFn`in o&&(e[t]=o.attributeValueFn(e[t],t,c)),`attributeNameFn`in o)){var n=e[t];delete e[t],e[o.attributeNameFn(t,e[t],c)]=n}}return e}function p(e){var t={};if(e.body&&(e.name.toLowerCase()===`xml`||o.instructionHasAttributes)){for(var n=/([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\w+))\s*/g,r;(r=n.exec(e.body))!==null;)t[r[1]]=r[2]||r[3]||r[4];t=f(t)}if(e.name.toLowerCase()===`xml`){if(o.ignoreDeclaration)return;c[o.declarationKey]={},Object.keys(t).length&&(c[o.declarationKey][o.attributesKey]=t),o.addParent&&(c[o.declarationKey][o.parentKey]=c)}else{if(o.ignoreInstruction)return;o.trim&&(e.body=e.body.trim());var i={};o.instructionHasAttributes&&Object.keys(t).length?(i[e.name]={},i[e.name][o.attributesKey]=t):i[e.name]=e.body,d(`instruction`,i)}}function m(e,t){var n;if(typeof e==`object`&&(t=e.attributes,e=e.name),t=f(t),`elementNameFn`in o&&(e=o.elementNameFn(e,c)),o.compact){if(n={},!o.ignoreAttributes&&t&&Object.keys(t).length)for(var r in n[o.attributesKey]={},t)t.hasOwnProperty(r)&&(n[o.attributesKey][r]=t[r]);!(e in c)&&(a(o.alwaysArray)?o.alwaysArray.indexOf(e)!==-1:o.alwaysArray)&&(c[e]=[]),c[e]&&!a(c[e])&&(c[e]=[c[e]]),a(c[e])?c[e].push(n):c[e]=n}else c[o.elementsKey]||(c[o.elementsKey]=[]),n={},n[o.typeKey]=`element`,n[o.nameKey]=e,!o.ignoreAttributes&&t&&Object.keys(t).length&&(n[o.attributesKey]=t),o.alwaysChildren&&(n[o.elementsKey]=[]),c[o.elementsKey].push(n);n[o.parentKey]=c,c=n}function h(e){o.ignoreText||(e.trim()||o.captureSpacesBetweenElements)&&(o.trim&&(e=e.trim()),o.nativeType&&(e=u(e)),o.sanitize&&(e=e.replace(/&/g,`&`).replace(//g,`>`)),d(`text`,e))}function g(e){o.ignoreComment||(o.trim&&(e=e.trim()),d(`comment`,e))}function _(e){var t=c[o.parentKey];o.addParent||delete c[o.parentKey],c=t}function x(e){o.ignoreCdata||(o.trim&&(e=e.trim()),d(`cdata`,e))}function S(e){o.ignoreDoctype||(e=e.replace(/^ /,``),o.trim&&(e=e.trim()),d(`doctype`,e))}function ee(e){e.note=e}t.exports=function(e,t){var i=s?n.parser(!0,{}):i=new r.Parser(`UTF-8`),a={};if(c=a,o=l(t),s?(i.opt={strictEntities:!0},i.onopentag=m,i.ontext=h,i.oncomment=g,i.onclosetag=_,i.onerror=ee,i.oncdata=x,i.ondoctype=S,i.onprocessinginstruction=p):(i.on(`startElement`,m),i.on(`text`,h),i.on(`comment`,g),i.on(`endElement`,_),i.on(`error`,ee)),s)i.write(e).close();else if(!i.parse(e))throw Error(`XML parsing error: `+i.getError());if(a[o.elementsKey]){var u=a[o.elementsKey];delete a[o.elementsKey],a[o.elementsKey]=u,delete a.text}return a}})),S=o(((e,t)=>{var n=b(),r=x();function i(e){var t=n.copyOptions(e);return n.ensureSpacesExists(t),t}t.exports=function(e,t){var n=i(t),a=r(e,n),o,s=`compact`in n&&n.compact?`_parent`:`parent`;return o=`addParent`in n&&n.addParent?JSON.stringify(a,function(e,t){return e===s?`_`:t},n.spaces):JSON.stringify(a,null,n.spaces),o.replace(/\u2028/g,`\\u2028`).replace(/\u2029/g,`\\u2029`)}})),ee=o(((e,t)=>{var n=b(),r=y().isArray,i,a;function o(e){var t=n.copyOptions(e);return n.ensureFlagExists(`ignoreDeclaration`,t),n.ensureFlagExists(`ignoreInstruction`,t),n.ensureFlagExists(`ignoreAttributes`,t),n.ensureFlagExists(`ignoreText`,t),n.ensureFlagExists(`ignoreComment`,t),n.ensureFlagExists(`ignoreCdata`,t),n.ensureFlagExists(`ignoreDoctype`,t),n.ensureFlagExists(`compact`,t),n.ensureFlagExists(`indentText`,t),n.ensureFlagExists(`indentCdata`,t),n.ensureFlagExists(`indentAttributes`,t),n.ensureFlagExists(`indentInstruction`,t),n.ensureFlagExists(`fullTagEmptyElement`,t),n.ensureFlagExists(`noQuotesForNativeAttributes`,t),n.ensureSpacesExists(t),typeof t.spaces==`number`&&(t.spaces=Array(t.spaces+1).join(` `)),n.ensureKeyExists(`declaration`,t),n.ensureKeyExists(`instruction`,t),n.ensureKeyExists(`attributes`,t),n.ensureKeyExists(`text`,t),n.ensureKeyExists(`comment`,t),n.ensureKeyExists(`cdata`,t),n.ensureKeyExists(`doctype`,t),n.ensureKeyExists(`type`,t),n.ensureKeyExists(`name`,t),n.ensureKeyExists(`elements`,t),n.checkFnExists(`doctype`,t),n.checkFnExists(`instruction`,t),n.checkFnExists(`cdata`,t),n.checkFnExists(`comment`,t),n.checkFnExists(`text`,t),n.checkFnExists(`instructionName`,t),n.checkFnExists(`elementName`,t),n.checkFnExists(`attributeName`,t),n.checkFnExists(`attributeValue`,t),n.checkFnExists(`attributes`,t),n.checkFnExists(`fullTagEmptyElement`,t),t}function s(e,t,n){return(!n&&e.spaces?` +String.fromCodePoint||(function(){var e=String.fromCharCode,t=Math.floor,n=function(){var n=16384,r=[],i,a,o=-1,s=arguments.length;if(!s)return``;for(var c=``;++o1114111||t(l)!==l)throw RangeError(`Invalid code point: `+l);l<=65535?r.push(l):(l-=65536,i=(l>>10)+55296,a=l%1024+56320,r.push(i,a)),(o+1===s||r.length>n)&&(c+=e.apply(null,r),r.length=0)}return c};Object.defineProperty?Object.defineProperty(String,"fromCodePoint",{value:n,configurable:!0,writable:!0}):String.fromCodePoint=n})()})(e===void 0?e.sax={}:e)})),y=o(((e,t)=>{t.exports={isArray:function(e){return Array.isArray?Array.isArray(e):Object.prototype.toString.call(e)===`[object Array]`}}})),b=o(((e,t)=>{var n=y().isArray;t.exports={copyOptions:function(e){var t,n={};for(t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);return n},ensureFlagExists:function(e,t){(!(e in t)||typeof t[e]!=`boolean`)&&(t[e]=!1)},ensureSpacesExists:function(e){(!(`spaces`in e)||typeof e.spaces!=`number`&&typeof e.spaces!=`string`)&&(e.spaces=0)},ensureAlwaysArrayExists:function(e){(!(`alwaysArray`in e)||typeof e.alwaysArray!=`boolean`&&!n(e.alwaysArray))&&(e.alwaysArray=!1)},ensureKeyExists:function(e,t){(!(e+`Key`in t)||typeof t[e+`Key`]!=`string`)&&(t[e+`Key`]=t.compact?`_`+e:e)},checkFnExists:function(e,t){return e+`Fn`in t}}})),x=o(((e,t)=>{var n=v(),r={on:function(){},parse:function(){}},i=b(),a=y().isArray,o,s=!0,c;function l(e){return o=i.copyOptions(e),i.ensureFlagExists(`ignoreDeclaration`,o),i.ensureFlagExists(`ignoreInstruction`,o),i.ensureFlagExists(`ignoreAttributes`,o),i.ensureFlagExists(`ignoreText`,o),i.ensureFlagExists(`ignoreComment`,o),i.ensureFlagExists(`ignoreCdata`,o),i.ensureFlagExists(`ignoreDoctype`,o),i.ensureFlagExists(`compact`,o),i.ensureFlagExists(`alwaysChildren`,o),i.ensureFlagExists(`addParent`,o),i.ensureFlagExists(`trim`,o),i.ensureFlagExists(`nativeType`,o),i.ensureFlagExists(`nativeTypeAttributes`,o),i.ensureFlagExists(`sanitize`,o),i.ensureFlagExists(`instructionHasAttributes`,o),i.ensureFlagExists(`captureSpacesBetweenElements`,o),i.ensureAlwaysArrayExists(o),i.ensureKeyExists(`declaration`,o),i.ensureKeyExists(`instruction`,o),i.ensureKeyExists(`attributes`,o),i.ensureKeyExists(`text`,o),i.ensureKeyExists(`comment`,o),i.ensureKeyExists(`cdata`,o),i.ensureKeyExists(`doctype`,o),i.ensureKeyExists(`type`,o),i.ensureKeyExists(`name`,o),i.ensureKeyExists(`elements`,o),i.ensureKeyExists(`parent`,o),i.checkFnExists(`doctype`,o),i.checkFnExists(`instruction`,o),i.checkFnExists(`cdata`,o),i.checkFnExists(`comment`,o),i.checkFnExists(`text`,o),i.checkFnExists(`instructionName`,o),i.checkFnExists(`elementName`,o),i.checkFnExists(`attributeName`,o),i.checkFnExists(`attributeValue`,o),i.checkFnExists(`attributes`,o),o}function u(e){var t=Number(e);if(!isNaN(t))return t;var n=e.toLowerCase();return n===`true`||n!==`false`&&e}function d(e,t){var n;if(o.compact){if(!c[o[e+`Key`]]&&(a(o.alwaysArray)?o.alwaysArray.indexOf(o[e+`Key`])!==-1:o.alwaysArray)&&(c[o[e+`Key`]]=[]),c[o[e+`Key`]]&&!a(c[o[e+`Key`]])&&(c[o[e+`Key`]]=[c[o[e+`Key`]]]),e+`Fn`in o&&typeof t==`string`&&(t=o[e+`Fn`](t,c)),e===`instruction`&&(`instructionFn`in o||`instructionNameFn`in o)){for(n in t)if(t.hasOwnProperty(n)){if(`instructionFn`in o)t[n]=o.instructionFn(t[n],n,c);else{var r=t[n];delete t[n],t[o.instructionNameFn(n,r,c)]=r}}}a(c[o[e+`Key`]])?c[o[e+`Key`]].push(t):c[o[e+`Key`]]=t}else{c[o.elementsKey]||(c[o.elementsKey]=[]);var i={};if(i[o.typeKey]=e,e===`instruction`){for(n in t)if(t.hasOwnProperty(n))break;i[o.nameKey]=`instructionNameFn`in o?o.instructionNameFn(n,t,c):n,o.instructionHasAttributes?(i[o.attributesKey]=t[n][o.attributesKey],`instructionFn`in o&&(i[o.attributesKey]=o.instructionFn(i[o.attributesKey],n,c))):(`instructionFn`in o&&(t[n]=o.instructionFn(t[n],n,c)),i[o.instructionKey]=t[n])}else e+`Fn`in o&&(t=o[e+`Fn`](t,c)),i[o[e+`Key`]]=t;o.addParent&&(i[o.parentKey]=c),c[o.elementsKey].push(i)}}function f(e){if(`attributesFn`in o&&e&&(e=o.attributesFn(e,c)),(o.trim||`attributeValueFn`in o||`attributeNameFn`in o||o.nativeTypeAttributes)&&e){for(var t in e)if(e.hasOwnProperty(t)&&(o.trim&&(e[t]=e[t].trim()),o.nativeTypeAttributes&&(e[t]=u(e[t])),`attributeValueFn`in o&&(e[t]=o.attributeValueFn(e[t],t,c)),`attributeNameFn`in o)){var n=e[t];delete e[t],e[o.attributeNameFn(t,e[t],c)]=n}}return e}function p(e){var t={};if(e.body&&(e.name.toLowerCase()===`xml`||o.instructionHasAttributes)){for(var n=/([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\w+))\s*/g,r;(r=n.exec(e.body))!==null;)t[r[1]]=r[2]||r[3]||r[4];t=f(t)}if(e.name.toLowerCase()===`xml`){if(o.ignoreDeclaration)return;c[o.declarationKey]={},Object.keys(t).length&&(c[o.declarationKey][o.attributesKey]=t),o.addParent&&(c[o.declarationKey][o.parentKey]=c)}else{if(o.ignoreInstruction)return;o.trim&&(e.body=e.body.trim());var i={};o.instructionHasAttributes&&Object.keys(t).length?(i[e.name]={},i[e.name][o.attributesKey]=t):i[e.name]=e.body,d(`instruction`,i)}}function m(e,t){var n;if(typeof e==`object`&&(t=e.attributes,e=e.name),t=f(t),`elementNameFn`in o&&(e=o.elementNameFn(e,c)),o.compact){if(n={},!o.ignoreAttributes&&t&&Object.keys(t).length)for(var r in n[o.attributesKey]={},t)t.hasOwnProperty(r)&&(n[o.attributesKey][r]=t[r]);!(e in c)&&(a(o.alwaysArray)?o.alwaysArray.indexOf(e)!==-1:o.alwaysArray)&&(c[e]=[]),c[e]&&!a(c[e])&&(c[e]=[c[e]]),a(c[e])?c[e].push(n):c[e]=n}else c[o.elementsKey]||(c[o.elementsKey]=[]),n={},n[o.typeKey]=`element`,n[o.nameKey]=e,!o.ignoreAttributes&&t&&Object.keys(t).length&&(n[o.attributesKey]=t),o.alwaysChildren&&(n[o.elementsKey]=[]),c[o.elementsKey].push(n);n[o.parentKey]=c,c=n}function h(e){o.ignoreText||(e.trim()||o.captureSpacesBetweenElements)&&(o.trim&&(e=e.trim()),o.nativeType&&(e=u(e)),o.sanitize&&(e=e.replace(/&/g,`&`).replace(//g,`>`)),d(`text`,e))}function g(e){o.ignoreComment||(o.trim&&(e=e.trim()),d(`comment`,e))}function _(e){var t=c[o.parentKey];o.addParent||delete c[o.parentKey],c=t}function x(e){o.ignoreCdata||(o.trim&&(e=e.trim()),d(`cdata`,e))}function S(e){o.ignoreDoctype||(e=e.replace(/^ /,``),o.trim&&(e=e.trim()),d(`doctype`,e))}function C(e){e.note=e}t.exports=function(e,t){var i=s?n.parser(!0,{}):i=new r.Parser(`UTF-8`),a={};if(c=a,o=l(t),s?(i.opt={strictEntities:!0},i.onopentag=m,i.ontext=h,i.oncomment=g,i.onclosetag=_,i.onerror=C,i.oncdata=x,i.ondoctype=S,i.onprocessinginstruction=p):(i.on(`startElement`,m),i.on(`text`,h),i.on(`comment`,g),i.on(`endElement`,_),i.on(`error`,C)),s)i.write(e).close();else if(!i.parse(e))throw Error(`XML parsing error: `+i.getError());if(a[o.elementsKey]){var u=a[o.elementsKey];delete a[o.elementsKey],a[o.elementsKey]=u,delete a.text}return a}})),S=o(((e,t)=>{var n=b(),r=x();function i(e){var t=n.copyOptions(e);return n.ensureSpacesExists(t),t}t.exports=function(e,t){var n=i(t),a=r(e,n),o,s=`compact`in n&&n.compact?`_parent`:`parent`;return o=`addParent`in n&&n.addParent?JSON.stringify(a,function(e,t){return e===s?`_`:t},n.spaces):JSON.stringify(a,null,n.spaces),o.replace(/\u2028/g,`\\u2028`).replace(/\u2029/g,`\\u2029`)}})),C=o(((e,t)=>{var n=b(),r=y().isArray,i,a;function o(e){var t=n.copyOptions(e);return n.ensureFlagExists(`ignoreDeclaration`,t),n.ensureFlagExists(`ignoreInstruction`,t),n.ensureFlagExists(`ignoreAttributes`,t),n.ensureFlagExists(`ignoreText`,t),n.ensureFlagExists(`ignoreComment`,t),n.ensureFlagExists(`ignoreCdata`,t),n.ensureFlagExists(`ignoreDoctype`,t),n.ensureFlagExists(`compact`,t),n.ensureFlagExists(`indentText`,t),n.ensureFlagExists(`indentCdata`,t),n.ensureFlagExists(`indentAttributes`,t),n.ensureFlagExists(`indentInstruction`,t),n.ensureFlagExists(`fullTagEmptyElement`,t),n.ensureFlagExists(`noQuotesForNativeAttributes`,t),n.ensureSpacesExists(t),typeof t.spaces==`number`&&(t.spaces=Array(t.spaces+1).join(` `)),n.ensureKeyExists(`declaration`,t),n.ensureKeyExists(`instruction`,t),n.ensureKeyExists(`attributes`,t),n.ensureKeyExists(`text`,t),n.ensureKeyExists(`comment`,t),n.ensureKeyExists(`cdata`,t),n.ensureKeyExists(`doctype`,t),n.ensureKeyExists(`type`,t),n.ensureKeyExists(`name`,t),n.ensureKeyExists(`elements`,t),n.checkFnExists(`doctype`,t),n.checkFnExists(`instruction`,t),n.checkFnExists(`cdata`,t),n.checkFnExists(`comment`,t),n.checkFnExists(`text`,t),n.checkFnExists(`instructionName`,t),n.checkFnExists(`elementName`,t),n.checkFnExists(`attributeName`,t),n.checkFnExists(`attributeValue`,t),n.checkFnExists(`attributes`,t),n.checkFnExists(`fullTagEmptyElement`,t),t}function s(e,t,n){return(!n&&e.spaces?` `:``)+Array(t+1).join(e.spaces)}function c(e,t,n){if(t.ignoreAttributes)return``;`attributesFn`in t&&(e=t.attributesFn(e,a,i));var r,o,c,l,u=[];for(r in e)e.hasOwnProperty(r)&&e[r]!==null&&e[r]!==void 0&&(l=t.noQuotesForNativeAttributes&&typeof e[r]!=`string`?``:`"`,o=``+e[r],o=o.replace(/"/g,`"`),c=`attributeNameFn`in t?t.attributeNameFn(r,o,a,i):r,u.push(t.spaces&&t.indentAttributes?s(t,n+1,!1):` `),u.push(c+`=`+l+(`attributeValueFn`in t?t.attributeValueFn(o,r,a,i):o)+l));return e&&Object.keys(e).length&&t.spaces&&t.indentAttributes&&u.push(s(t,n,!1)),u.join(``)}function l(e,t,n){return i=e,a=`xml`,t.ignoreDeclaration?``:``}function u(e,t,n){if(t.ignoreInstruction)return``;for(var r in e)if(e.hasOwnProperty(r))break;var o=`instructionNameFn`in t?t.instructionNameFn(r,e[r],a,i):r;if(typeof e[r]==`object`)return i=e,a=o,``;var s=e[r]?e[r]:``;return`instructionFn`in t&&(s=t.instructionFn(s,r,a,i)),``}function d(e,t){return t.ignoreComment?``:``}function f(e,t){return t.ignoreCdata?``:``,`]]]]>`))+`]]>`}function p(e,t){return t.ignoreDoctype?``:``}function m(e,t){return t.ignoreText?``:(e=``+e,e=e.replace(/&/g,`&`),e=e.replace(/&/g,`&`).replace(//g,`>`),`textFn`in t?t.textFn(e,a,i):e)}function h(e,t){var n;if(e.elements&&e.elements.length)for(n=0;n`),e[t.elementsKey]&&e[t.elementsKey].length&&(r.push(_(e[t.elementsKey],t,n+1)),i=e,a=e.name),r.push(t.spaces&&h(e,t)?` -`+Array(n+1).join(t.spaces):``),r.push(``)):r.push(`/>`),r.join(``)}function _(e,t,n,r){return e.reduce(function(e,i){var a=s(t,n,r&&!e);switch(i.type){case`element`:return e+a+g(i,t,n);case`comment`:return e+a+d(i[t.commentKey],t);case`doctype`:return e+a+p(i[t.doctypeKey],t);case`cdata`:return e+(t.indentCdata?a:``)+f(i[t.cdataKey],t);case`text`:return e+(t.indentText?a:``)+m(i[t.textKey],t);case`instruction`:var o={};return o[i[t.nameKey]]=i[t.attributesKey]?i:i[t.instructionKey],e+(t.indentInstruction?a:``)+u(o,t,n)}},``)}function v(e,t,n){for(var r in e)if(e.hasOwnProperty(r))switch(r){case t.parentKey:case t.attributesKey:break;case t.textKey:if(t.indentText||n)return!0;break;case t.cdataKey:if(t.indentCdata||n)return!0;break;case t.instructionKey:if(t.indentInstruction||n)return!0;break;case t.doctypeKey:case t.commentKey:return!0;default:return!0}return!1}function x(e,t,n,r,o){i=e,a=t;var l=`elementNameFn`in n?n.elementNameFn(t,e):t;if(e==null||e===``)return`fullTagEmptyElementFn`in n&&n.fullTagEmptyElementFn(t,e)||n.fullTagEmptyElement?`<`+l+`>`:`<`+l+`/>`;var u=[];if(t){if(u.push(`<`+l),typeof e!=`object`)return u.push(`>`+m(e,n)+``),u.join(``);e[n.attributesKey]&&u.push(c(e[n.attributesKey],n,r));var d=v(e,n,!0)||e[n.attributesKey]&&e[n.attributesKey][`xml:space`]===`preserve`;if(d||=`fullTagEmptyElementFn`in n?n.fullTagEmptyElementFn(t,e):n.fullTagEmptyElement,d)u.push(`>`);else return u.push(`/>`),u.join(``)}return u.push(S(e,n,r+1,!1)),i=e,a=t,t&&u.push((o?s(n,r,!1):``)+``),u.join(``)}function S(e,t,n,i){var a,o,c,h=[];for(o in e)if(e.hasOwnProperty(o))for(c=r(e[o])?e[o]:[e[o]],a=0;a{var n=ee();t.exports=function(e,t){e instanceof Buffer&&(e=e.toString());var r=null;if(typeof e==`string`)try{r=JSON.parse(e)}catch{throw Error(`The JSON structure is invalid`)}else r=e;return n(r,t)}})),te=l(o(((e,t)=>{t.exports={xml2js:x(),xml2json:S(),js2xml:ee(),json2xml:C()}}))());const w=e=>e.replace(/[-_]+(\w?)/g,(e,t)=>t?t.toUpperCase():``),T=typeof globalThis<`u`&&typeof globalThis.fetch==`function`?globalThis.fetch.bind(globalThis):(()=>{throw Error("tsdav: global fetch is not available in this runtime. Upgrade to Node.js >= 18, run under a browser/Bun/Deno, or install a fetch polyfill on globalThis before importing tsdav. You can also pass a custom `fetch` implementation to `createDAVClient`, `DAVClient`, or individual request helpers.")}),E=/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/,ne=e=>{if(typeof e!=`string`)return e;if(E.test(e)){let t=Number(e);if(!Number.isNaN(t)&&Number.isFinite(t)&&(!Number.isInteger(t)||Number.isSafeInteger(t)))return t}let t=e.toLowerCase();return t===`true`||t!==`false`&&e};var re=s({cleanupFalsy:()=>N,conditionalParam:()=>P,ensureTrailingSlash:()=>O,excludeHeaders:()=>F,getDAVAttribute:()=>M,mergeHeaders:()=>I,urlContains:()=>A,urlEquals:()=>k,urlMatches:()=>j});const D=e=>{let t=e.trim();return t.endsWith(`/`)?t.slice(0,-1):t},O=e=>{let t=e.trim(),n=t.search(/[?#]/),r=n===-1?t:t.slice(0,n),i=n===-1?``:t.slice(n);return`${r.endsWith(`/`)?r:`${r}/`}${i}`},k=(e,t)=>!e&&!t?!0:!e||!t?!1:D(e)===D(t),A=(e,t)=>{if(!e&&!t)return!0;if(!e||!t)return!1;let n=D(e),r=D(t);return n.includes(r)||r.includes(n)},j=(e,t,n)=>{if(!e||!t||!n)return k(e,t);try{return k(new URL(e,n).href,new URL(t,n).href)}catch{return k(e,t)}},M=e=>e.reduce((e,t)=>({...e,[h[t]]:t}),{}),N=e=>Object.entries(e).reduce((e,[t,n])=>n?{...e,[t]:n}:e,{}),P=(e,t)=>t?{[e]:t}:{},F=(e,t)=>{if(!e)return{};if(!t||t.length===0)return e;let n=new Set(t.map(e=>e.toLowerCase()));return Object.fromEntries(Object.entries(e).filter(([e])=>!n.has(e.toLowerCase())))},I=(...e)=>{let t=new Map,n=(e,n)=>{t.set(e.toLowerCase(),[e,n])};for(let t of e)if(t){if(Array.isArray(t)){for(let[e,r]of t)n(e,r);continue}if(typeof t.forEach==`function`){t.forEach((e,t)=>{n(t,e)});continue}for(let[e,r]of Object.entries(t))n(e,r)}return Object.fromEntries(t.values())};var ie=s({createObject:()=>B,davRequest:()=>R,deleteObject:()=>H,propfind:()=>z,updateObject:()=>V});const L=(0,p.default)(`tsdav:request`),ae=e=>{let t=/^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(e??``),n=t?.groups?.status,r=t?.groups?.statusText;return n?{status:Number.parseInt(n,10),statusText:r??``}:void 0},R=async e=>{let{url:t,init:n,convertIncoming:r=!0,parseOutgoing:i=!0,fetchOptions:a={},fetch:o}=e,s=o??T,{headers:c={},body:l,namespace:u,method:d,attributes:f}=n,p=l;f&&typeof l==`object`&&l&&!Array.isArray(l)&&(p=Object.fromEntries(Object.entries(l).map(([e,t])=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let n=t;return[e,{...n,_attributes:{...f,...n._attributes}}]}return[e,t]})));let m=r&&l!=null?te.default.js2xml({_declaration:{_attributes:{version:`1.0`,encoding:`utf-8`}},...p},{compact:!0,spaces:2,elementNameFn:e=>u&&!/^.+:.+/.test(e)?`${u}:${e}`:e}):l,h={...a};delete h.headers;let g=I({"Content-Type":`text/xml;charset=UTF-8`},N(c),a.headers),_=await s(t,{...h,headers:g,body:m,method:d}),v=await _.text();if(!_.ok||!_.headers.get(`content-type`)?.toLowerCase().includes(`xml`)||!i||!v){let e=4096,t=v.length>e?`${v.slice(0,e)}…`:v;return[{href:_.url,ok:_.ok,status:_.status,statusText:_.statusText,raw:t}]}let y;try{y=te.default.xml2js(v,{compact:!0,trim:!0,textFn:(e,t)=>{try{let n=t._parent,r=Object.keys(n),i=r[r.length-1];if(!i)return;let a=n[i];if(a.length>0){let t=a,n=a.length-1;t[n]=ne(e)}else n[i]=ne(e)}catch(e){L(e.stack)}},elementNameFn:e=>w(e.replace(/^.+:/,``)),attributesFn:e=>{let t={...e};return delete t.xmlns,t},ignoreDeclaration:!0})}catch(e){return L(`Failed to parse DAV response XML: ${e.message}`),[{href:_.url,ok:_.ok,status:_.status,statusText:_.statusText,raw:v}]}return y?.multistatus?(Array.isArray(y.multistatus.response)?y.multistatus.response:[y.multistatus.response]).map(e=>{if(!e)return{raw:y,status:_.status,statusText:_.statusText,ok:_.ok};let t=ae(e.status),n=t?.status??_.status;return{raw:y,href:e.href,status:n,statusText:t?.statusText??_.statusText,ok:n>=200&&n<300,error:e.error,responsedescription:e.responsedescription,props:(Array.isArray(e.propstat)?e.propstat:[e.propstat]).reduce((e,t)=>{let n=ae(t?.status)?.status;return n&&(n<200||n>=300)?e:{...e,...t?.prop}},{})}}):[{href:_.url,ok:_.ok,status:_.status,statusText:_.statusText,raw:y}]},z=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return R({url:t,init:{method:`PROPFIND`,headers:F(N({depth:r,...i}),a),namespace:`d`,body:{propfind:{_attributes:M([`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`,`http://calendarserver.org/ns/`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),prop:n}}},fetchOptions:o,fetch:s})},B=async e=>{let{url:t,data:n,headers:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??T,{headers:c,...l}=a;return s(t,{...l,method:`PUT`,body:n,headers:F(I(r,c),i)})},V=async e=>{let{url:t,data:n,etag:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c=s??T,{headers:l,...u}=o;return c(t,{...u,method:`PUT`,body:n,headers:F(I(N({"If-Match":r,...i}),l),a)})},H=async e=>{let{url:t,headers:n,etag:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??T,{headers:c,...l}=a;return s(t,{...l,method:`DELETE`,headers:F(I(N({"If-Match":r,...n}),c),i)})};function U(e,t){if(!e)return!1;let n=e=>e!=null&&t.every(t=>e[t]);return Array.isArray(e)?e.length>0&&e.every(e=>n(e)):n(e)}const W=(e,t)=>!e||typeof e!=`object`?t.map(e=>e.toString()).join(`,`):t.reduce((t,n)=>e[n]?t:`${t.length?`${t},`:``}${n.toString()}`,``);var oe=s({collectionQuery:()=>G,isCollectionDirty:()=>fe,makeCollection:()=>ue,smartCollectionSync:()=>K,smartCollectionSyncDetailed:()=>me,supportedReportSet:()=>de,syncCollection:()=>pe});const se=(0,p.default)(`tsdav:collection`),ce=(e,t)=>{try{return new URL(e,O(t)).href}catch{return e}},le=(e,t,n)=>{try{return new URL(e,O(n)).pathname.toLowerCase().endsWith(t)}catch{return(e.split(/[?#]/,1)[0]??``).toLowerCase().endsWith(t)}},G=async e=>{let{url:t,body:n,depth:r,defaultNamespace:i=`d`,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e,l=await R({url:t,init:{method:`REPORT`,headers:F(N({depth:r,...a}),o),namespace:i,body:n},fetchOptions:s,fetch:c}),u=l[0];if(i===`c`&&n?.[`calendar-query`]!=null&&l.length===1&&u&&u.status===404&&j(t,u.href,t)&&!u.error&&Object.keys(u.props??{}).length===0&&typeof u.raw==`object`&&u.raw!==null&&u.raw.multistatus?.response?.propstat==null)return[];let d=l.find(e=>!e.ok||e.status&&e.status>=400);if(d)throw Error(`Collection query failed: ${d.status} ${d.statusText}. ${d.raw?`Raw response: ${d.raw}`:``}`);let f=l[0];return l.length===1&&f&&(!f.raw||f.raw.multistatus&&!f.raw.multistatus.response)&&f.status&&f.status<300?[]:l},ue=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return R({url:t,init:{method:`MKCOL`,headers:F(N({depth:r,...i}),a),namespace:`d`,body:n?{mkcol:{_attributes:M([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`]),set:{prop:n}}}:void 0},fetchOptions:o,fetch:s})},de=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await z({url:t.url,props:{"d:supported-report-set":{}},depth:`0`,headers:F(n,r),fetchOptions:i,fetch:a}))[0]?.props?.supportedReportSet?.supportedReport;return o?(Array.isArray(o)?o:[o]).map(e=>e?.report?Object.keys(e.report)[0]:void 0).filter(e=>typeof e==`string`&&e.length>0):[]},fe=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await z({url:t.url,props:{"cs:getctag":{}},depth:`0`,headers:F(n,r),fetchOptions:i,fetch:a})).find(e=>j(t.url,e.href,t.url));if(!o)throw Error(`Collection does not exist on server`);if(!o.ok)throw Error(`Collection status check failed: ${o.status} ${o.statusText}`);let s=o.props?.getctag;return{isDirty:t.ctag==null||s==null||`${t.ctag}`!=`${s}`,newCtag:s?.toString()}},pe=e=>{let{url:t,props:n,headers:r,syncLevel:i,syncToken:a,headersToExclude:o,fetchOptions:s,fetch:c}=e;return R({url:t,init:{method:`REPORT`,namespace:`d`,headers:F({...r},o),body:{"sync-collection":{_attributes:M([`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"sync-level":i,"sync-token":a,"d:prop":n}}},fetchOptions:s,fetch:c})},K=async e=>{let{collection:t,method:n,headers:r,headersToExclude:i,account:a,detailedResult:o,fetchOptions:s={},fetch:c}=e,l=[`accountType`,`homeUrl`];if(!a||!U(a,l))throw Error(a?`account must have ${W(a,l)} before smartCollectionSync`:`no account for smartCollectionSync`);let u=n??(t.reports?.includes(`syncCollection`)?`webdav`:`basic`);if(se(`smart collection sync with type ${a.accountType} and method ${u}`),u===`webdav`){let e=await pe({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{},"d:displayname":{}},syncLevel:1,syncToken:t.syncToken,headers:F(r,i),fetchOptions:s,fetch:c}),n=e=>{let n=a.accountType===`caldav`?`.ics`:`.vcf`;return typeof e.href==`string`&&le(e.href,n,t.url)},l=e.find(e=>(!e.ok||e.status>=400)&&!(e.status===404&&n(e)));if(l)throw Error(`Collection sync failed: ${l.status} ${l.statusText}`);let u=e.filter(n),d=u.filter(e=>e.status!==404).map(e=>e.href),f=u.filter(e=>e.status===404).map(e=>e.href),p=t.objectMultiGet;if(d.length>0&&!p)throw Error(`collection.objectMultiGet is required for webdav sync changes`);let m=d.length?await p?.({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{}},objectUrls:d,depth:`1`,headers:F(r,i),fetchOptions:s,fetch:c})??[]:[],h=m.find(e=>!e.ok||e.status>=400);if(h)throw Error(`Collection sync multi-get failed: ${h.status} ${h.statusText}`);let g=m.map(e=>({url:ce(e.href??``,t.url),etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:a?.accountType===`caldav`?e.props?.calendarData?._cdata??e.props?.calendarData:e.props?.addressData?._cdata??e.props?.addressData})),_=t.objects??[],v=g.filter(e=>_.every(n=>!j(n.url,e.url,t.url))),y=_.reduce((e,n)=>{let r=g.find(e=>j(e.url,n.url,t.url));return r&&r.etag&&r.etag!==n.etag?[...e,r]:e},[]),b=f.map(e=>({url:ce(e,t.url),etag:``})),x=_.filter(e=>b.every(n=>!j(e.url,n.url,t.url))&&y.every(n=>!j(e.url,n.url,t.url)));return{...t,objects:o?{created:v,updated:y,deleted:b}:[...x,...v,...y],syncToken:e[0]?.raw?.multistatus?.syncToken??t.syncToken}}if(u===`basic`){let{isDirty:e,newCtag:n}=await fe({collection:t,headers:F(r,i),fetchOptions:s,fetch:c});if(!e)return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t;let a=t.objects??[];if(!t.fetchObjects)throw Error(`collection.fetchObjects is required for basic sync changes`);let l=await t.fetchObjects({collection:t,headers:F(r,i),fetchOptions:s,fetch:c})??[],u=l.filter(e=>a.every(n=>!j(n.url,e.url,t.url))),d=a.reduce((e,n)=>{let r=l.find(e=>j(e.url,n.url,t.url));return r&&r.etag&&r.etag!==n.etag?[...e,r]:e},[]),f=a.filter(e=>l.every(n=>!j(n.url,e.url,t.url))),p=a.filter(e=>l.some(n=>j(e.url,n.url,t.url)&&n.etag===e.etag));return{...t,objects:o?{created:u,updated:d,deleted:f}:[...p,...u,...d],ctag:n}}return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t},me=async e=>K({...e,detailedResult:!0});var he=s({addressBookMultiGet:()=>_e,addressBookQuery:()=>q,createVCard:()=>be,deleteVCard:()=>Se,fetchAddressBooks:()=>ve,fetchVCards:()=>ye,updateVCard:()=>xe});const ge=(0,p.default)(`tsdav:addressBook`),q=async e=>{let{url:t,props:n,filters:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return G({url:t,body:{"addressbook-query":N({_attributes:M([`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"d:prop":n,filter:r??{"prop-filter":{_attributes:{name:`FN`}}}})},defaultNamespace:`card`,depth:i,headers:F(a,o),fetchOptions:s,fetch:c})},_e=async e=>{let{url:t,props:n,objectUrls:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return G({url:t,body:{"addressbook-multiget":N({_attributes:M([`DAV:`,`urn:ietf:params:xml:ns:carddav`]),"d:prop":n,"d:href":r})},defaultNamespace:`card`,depth:i,headers:F(a,o),fetchOptions:s,fetch:c})},ve=async e=>{let{account:t,headers:n,props:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e??{},s=[`homeUrl`,`rootUrl`];if(!t||!U(t,s))throw Error(t?`account must have ${W(t,s)} before fetchAddressBooks`:`no account for fetchAddressBooks`);let c=await z({url:t.homeUrl,props:r??{"d:displayname":{},"cs:getctag":{},"d:resourcetype":{},"d:sync-token":{}},depth:`1`,headers:F(n,i),fetchOptions:a,fetch:o});return Promise.all(c.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`addressbook`)).map(e=>{let n=e.props?.displayname?._cdata??e.props?.displayname;return ge(`Found address book named ${typeof n==`string`?n:``}, - props: ${JSON.stringify(e.props)}`),{url:new URL(e.href??``,O(t.rootUrl??``)).href,ctag:e.props?.getctag,displayName:typeof n==`string`?n:``,resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:e.props?.syncToken}}).map(async e=>({...e,reports:await de({collection:e,headers:F(n,i),fetchOptions:a,fetch:o})})))},ye=async e=>{let{addressBook:t,headers:n,objectUrls:r,headersToExclude:i,urlFilter:a=e=>!!e,useMultiGet:o=!0,fetchOptions:s={},fetch:c}=e;ge(`Fetching vcards from ${t?.url}`);let l=[`url`];if(!t||!U(t,l))throw Error(t?`addressBook must have ${W(t,l)} before fetchVCards`:`cannot fetchVCards for undefined addressBook`);let u=(r??(await q({url:t.url,props:{"d:getetag":{}},depth:`1`,headers:F(n,i),fetchOptions:s,fetch:c})).map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,O(t.url)).href).filter(e=>!k(e,t.url)).filter(a).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),d=[];return u.length>0&&(d=o?await _e({url:t.url,props:{"d:getetag":{},"card:address-data":{}},objectUrls:u,depth:`1`,headers:F(n,i),fetchOptions:s,fetch:c}):await q({url:t.url,props:{"d:getetag":{},"card:address-data":{}},depth:`1`,headers:F(n,i),fetchOptions:s,fetch:c})),d.map(e=>({url:new URL(e.href??``,O(t.url)).href,etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:e.props?.addressData?._cdata??e.props?.addressData}))},be=async e=>{let{addressBook:t,vCardString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return B({url:new URL(r,O(t.url)).href,data:n,headers:F({"content-type":`text/vcard; charset=utf-8`,"If-None-Match":`*`,...i},a),fetchOptions:o,fetch:s})},xe=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return V({url:t.url,data:t.data,etag:t.etag,headers:F({"content-type":`text/vcard; charset=utf-8`,...n},r),fetchOptions:i,fetch:a})},Se=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return H({url:t.url,etag:t.etag,headers:F(n,r),fetchOptions:i,fetch:a})};var Ce=s({calendarMultiGet:()=>ke,calendarQuery:()=>Y,createCalendarObject:()=>Me,deleteCalendarObject:()=>Pe,fetchCalendarObjects:()=>X,fetchCalendarUserAddresses:()=>Oe,fetchCalendars:()=>je,freeBusyQuery:()=>Le,makeCalendar:()=>Ae,syncCalendars:()=>Fe,syncCalendarsDetailed:()=>Ie,updateCalendarObject:()=>Ne});const J=(0,p.default)(`tsdav:calendar`),we=/^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i,Te=/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i,Ee=e=>{let{start:t,end:n}=e;if(!(we.test(t)&&we.test(n)||Te.test(t)&&Te.test(n)))throw Error(`invalid timeRange format, not in ISO8601`);if(Number.isNaN(new Date(t).getTime())||Number.isNaN(new Date(n).getTime()))throw Error(`invalid timeRange: start or end is not a valid date`);if(new Date(t).getTime()>=new Date(n).getTime())throw Error(`invalid timeRange: start must be before end`)},De=e=>{let t=[];return Array.isArray(e)?t=e.map(e=>e?._attributes?.name):e&&typeof e==`object`&&(t=[e._attributes?.name]),t.filter(e=>typeof e==`string`&&e.length>0)},Oe=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!U(t,o))throw Error(`account must have ${W(t,o)} before fetchUserAddresses`);J(`Fetch user addresses from ${t.principalUrl}`);let s=(await z({url:t.principalUrl,props:{"c:calendar-user-address-set":{}},depth:`0`,headers:F(n,r),fetchOptions:i,fetch:a})).find(e=>j(t.principalUrl,e.href,t.rootUrl));if(!s||!s.ok)throw Error(`cannot find calendarUserAddresses`);let c=s?.props?.calendarUserAddressSet?.href,l=[];Array.isArray(c)?l=c:c&&(l=[c]);let u=l.filter(e=>typeof e==`string`&&e.length>0);return J(`Fetched calendar user addresses ${u}`),u},Y=async e=>{let{url:t,props:n,filters:r,timezone:i,depth:a,headers:o,headersToExclude:s,fetchOptions:c={},fetch:l}=e;return G({url:t,body:{"calendar-query":N({_attributes:M([`urn:ietf:params:xml:ns:caldav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`,`DAV:`]),"d:prop":n,filter:r,timezone:i})},defaultNamespace:`c`,depth:a,headers:F(o,s),fetchOptions:c,fetch:l})},ke=async e=>{let{url:t,props:n,objectUrls:r,filters:i,timezone:a,depth:o,headers:s,headersToExclude:c,fetchOptions:l={},fetch:u}=e;return G({url:t,body:{"calendar-multiget":N({_attributes:M([`DAV:`,`urn:ietf:params:xml:ns:caldav`]),"d:prop":n,"d:href":r,filter:i,timezone:a})},defaultNamespace:`c`,depth:o,headers:F(s,c),fetchOptions:l,fetch:u})},Ae=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return R({url:t,init:{method:`MKCALENDAR`,headers:F(N({depth:r,...i}),a),namespace:`d`,body:{"c:mkcalendar":{_attributes:M([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`]),set:{prop:n}}}},fetchOptions:o,fetch:s})},je=async e=>{let{headers:t,account:n,props:r,projectedProps:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e??{},c=[`homeUrl`,`rootUrl`];if(!n||!U(n,c))throw Error(n?`account must have ${W(n,c)} before fetchCalendars`:`no account for fetchCalendars`);let l=await z({url:n.homeUrl,props:r??{"c:calendar-description":{},"c:calendar-timezone":{},"d:displayname":{},"ca:calendar-color":{},"cs:getctag":{},"d:resourcetype":{},"c:supported-calendar-component-set":{},"d:sync-token":{}},depth:`1`,headers:F(t,a),fetchOptions:o,fetch:s});return Promise.all(l.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`calendar`)).filter(e=>{let t=De(e.props?.supportedCalendarComponentSet?.comp);return t.length===0||t.some(e=>Object.values(_).includes(e))}).map(e=>{let t=e.props?.calendarDescription,r=e.props?.calendarTimezone,a=e.props?.supportedCalendarComponentSet?.comp,o=Object.entries(e.props??{}).filter(([e])=>i?.[e]);return{description:typeof t==`string`?t:``,timezone:typeof r==`string`?r:``,url:new URL(e.href??``,O(n.rootUrl??``)).href,ctag:e.props?.getctag,calendarColor:e.props?.calendarColor,displayName:e.props?.displayname?._cdata??e.props?.displayname,components:De(a),resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:e.props?.syncToken,...i&&o.length>0?{projectedProps:Object.fromEntries(o)}:{}}}).map(async e=>({...e,reports:await de({collection:e,headers:F(t,a),fetchOptions:o,fetch:s})})))},X=async e=>{let{calendar:t,objectUrls:n,filters:r,timeRange:i,headers:a,expand:o,urlFilter:s=e=>!!e?.includes(`.ics`),useMultiGet:c=!0,headersToExclude:l,fetchOptions:u={},fetch:d}=e;i&&Ee(i),J(`Fetching calendar objects from ${t?.url}`);let f=[`url`];if(!t||!U(t,f))throw Error(t?`calendar must have ${W(t,f)} before fetchCalendarObjects`:`cannot fetchCalendarObjects for undefined calendar`);let p=r??[{"comp-filter":{_attributes:{name:`VCALENDAR`},"comp-filter":{_attributes:{name:`VEVENT`},...i?{"time-range":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}}}],m=[];n||(m=await Y({url:t.url,props:{"d:getetag":{},...o&&i?{"c:calendar-data":{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}}:{}},filters:p,depth:`1`,headers:F(a,l),fetchOptions:u,fetch:d}));let h=(n??m.map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,O(t.url)).href).filter(s).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),g=[];return h.length>0&&(o&&!n?g=m.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href:new URL(e.href??``,O(t.url)).href;return s(n??``)}):c?g=await ke({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},objectUrls:h,depth:`1`,headers:F(a,l),fetchOptions:u,fetch:d}):(g=await Y({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},filters:p,depth:`1`,headers:F(a,l),fetchOptions:u,fetch:d}),n&&n.length>0&&(g=g.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href??``:new URL(e.href??``,O(t.url)).href;return h.some(e=>j(n,e,t.url))})))),g.map(e=>({url:new URL(e.href??``,O(t.url)).href,etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:e.props?.calendarData?._cdata??e.props?.calendarData}))},Me=async e=>{let{calendar:t,iCalString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return B({url:new URL(r,O(t.url)).href,data:n,headers:F({"content-type":`text/calendar; charset=utf-8`,"If-None-Match":`*`,...i},a),fetchOptions:o,fetch:s})},Ne=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return V({url:t.url,data:t.data,etag:t.etag,headers:F({"content-type":`text/calendar; charset=utf-8`,...n},r),fetchOptions:i,fetch:a})},Pe=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return H({url:t.url,etag:t.etag,headers:F(n,r),fetchOptions:i,fetch:a})},Fe=async e=>{let{oldCalendars:t,account:n,detailedResult:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`Must have account before syncCalendars`);let c=t??n.calendars??[],l=await je({account:n,headers:F(i,a),fetchOptions:o,fetch:s}),u=l.filter(e=>c.every(t=>!j(t.url,e.url,n.rootUrl)));J(`new calendars: ${u.map(e=>e.displayName)}`);let d=c.reduce((e,t)=>{let r=l.find(e=>j(e.url,t.url,n.rootUrl));return r&&(r.syncToken&&`${r.syncToken}`!=`${t.syncToken}`||r.ctag&&`${r.ctag}`!=`${t.ctag}`)?[...e,{local:t,remote:r}]:e},[]);J(`updated calendars: ${d.map(({remote:e})=>e.displayName)}`);let f=await Promise.all(d.map(async({local:e,remote:t})=>{let r=async e=>{if(!e)return[];let{collection:t,...n}=e;return X({...n,calendar:t})},c={...t,ctag:e.ctag,syncToken:e.syncToken,objects:e.objects,objectMultiGet:ke,fetchObjects:r},l=await K({collection:c,detailedResult:!1,headers:F(i,a),account:n,fetchOptions:o,fetch:s});return{...l,ctag:t.ctag??l.ctag,syncToken:t.syncToken??l.syncToken}})),p=c.filter(e=>l.every(t=>!j(t.url,e.url,n.rootUrl)));J(`deleted calendars: ${p.map(e=>e.displayName)}`);let m=c.filter(e=>l.some(t=>{if(!j(t.url,e.url,n.rootUrl))return!1;let r=!t.syncToken||`${t.syncToken}`==`${e.syncToken}`,i=!t.ctag||`${t.ctag}`==`${e.ctag}`;return r&&i}));return J(`unchanged calendars: ${m.map(e=>e.displayName)}`),r?{created:u,updated:f,deleted:p}:[...m,...u,...f]},Ie=async e=>Fe({...e,detailedResult:!0}),Le=async e=>{let{url:t,timeRange:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`timeRange is required`);Ee(n);let c=(await G({url:t,body:{"free-busy-query":N({_attributes:M([`urn:ietf:params:xml:ns:caldav`]),"c:time-range":{_attributes:{start:`${new Date(n.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(n.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}})},defaultNamespace:`c`,depth:r,headers:F(i,a),fetchOptions:o,fetch:s}))[0];if(!c)throw Error(`freeBusyQuery returned no response`);return c};var Re=s({createAccount:()=>We,fetchHomeUrl:()=>Ue,fetchPrincipalUrl:()=>He,serviceDiscovery:()=>Be});const Z=(0,p.default)(`tsdav:account`),ze=(e,t)=>{let n=[t,e,new URL(`/`,e).href];return n.filter((e,t)=>n.indexOf(e)===t)},Be=async e=>{Z(`Service discovery...`);let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=a??T,s=new URL(t.serverUrl),{headers:c,...l}=i,u=new URL(`/.well-known/${t.accountType}`,s);u.protocol=s.protocol??`http`;let d=e=>{if(e.status>=300&&e.status<400){let t=e.headers.get(`Location`);if(typeof t==`string`&&t.length){Z(`Service discovery redirected to ${t}`);let e=/^[a-z][a-z0-9+.-]*:/i.test(t),n=new URL(t,s);return n.hostname===u.hostname&&u.port&&!n.port&&(n.port=u.port),e||(n.protocol=s.protocol??`http`),n.href}}};try{let e=d(await o(u.href,{...l,method:`PROPFIND`,headers:F(I({"Content-Type":`text/xml;charset=UTF-8`},n,c),r),body:` +`+Array(n+1).join(t.spaces):``),r.push(``)):r.push(`/>`),r.join(``)}function _(e,t,n,r){return e.reduce(function(e,i){var a=s(t,n,r&&!e);switch(i.type){case`element`:return e+a+g(i,t,n);case`comment`:return e+a+d(i[t.commentKey],t);case`doctype`:return e+a+p(i[t.doctypeKey],t);case`cdata`:return e+(t.indentCdata?a:``)+f(i[t.cdataKey],t);case`text`:return e+(t.indentText?a:``)+m(i[t.textKey],t);case`instruction`:var o={};return o[i[t.nameKey]]=i[t.attributesKey]?i:i[t.instructionKey],e+(t.indentInstruction?a:``)+u(o,t,n)}},``)}function v(e,t,n){for(var r in e)if(e.hasOwnProperty(r))switch(r){case t.parentKey:case t.attributesKey:break;case t.textKey:if(t.indentText||n)return!0;break;case t.cdataKey:if(t.indentCdata||n)return!0;break;case t.instructionKey:if(t.indentInstruction||n)return!0;break;case t.doctypeKey:case t.commentKey:return!0;default:return!0}return!1}function x(e,t,n,r,o){i=e,a=t;var l=`elementNameFn`in n?n.elementNameFn(t,e):t;if(e==null||e===``)return`fullTagEmptyElementFn`in n&&n.fullTagEmptyElementFn(t,e)||n.fullTagEmptyElement?`<`+l+`>`:`<`+l+`/>`;var u=[];if(t){if(u.push(`<`+l),typeof e!=`object`)return u.push(`>`+m(e,n)+``),u.join(``);e[n.attributesKey]&&u.push(c(e[n.attributesKey],n,r));var d=v(e,n,!0)||e[n.attributesKey]&&e[n.attributesKey][`xml:space`]===`preserve`;if(d||=`fullTagEmptyElementFn`in n?n.fullTagEmptyElementFn(t,e):n.fullTagEmptyElement,d)u.push(`>`);else return u.push(`/>`),u.join(``)}return u.push(S(e,n,r+1,!1)),i=e,a=t,t&&u.push((o?s(n,r,!1):``)+``),u.join(``)}function S(e,t,n,i){var a,o,c,h=[];for(o in e)if(e.hasOwnProperty(o))for(c=r(e[o])?e[o]:[e[o]],a=0;a{var n=C();t.exports=function(e,t){e instanceof Buffer&&(e=e.toString());var r=null;if(typeof e==`string`)try{r=JSON.parse(e)}catch{throw Error(`The JSON structure is invalid`)}else r=e;return n(r,t)}})),ee=l(o(((e,t)=>{t.exports={xml2js:x(),xml2json:S(),js2xml:C(),json2xml:w()}}))());const T=typeof globalThis<`u`&&typeof globalThis.fetch==`function`?globalThis.fetch.bind(globalThis):(()=>{throw Error("tsdav: global fetch is not available in this runtime. Upgrade to Node.js >= 18, run under a browser/Bun/Deno, or install a fetch polyfill on globalThis before importing tsdav. You can also pass a custom `fetch` implementation to `createDAVClient`, `DAVClient`, or individual request helpers.")}),te=e=>e.replace(/[-_]+(\w?)/g,(e,t)=>t?t.toUpperCase():``);function E(e,t){if(!e)return!1;let n=e=>e!=null&&t.every(t=>e[t]);return Array.isArray(e)?e.length>0&&e.every(e=>n(e)):n(e)}const D=(e,t)=>!e||typeof e!=`object`?t.map(e=>e.toString()).join(`,`):t.reduce((t,n)=>e[n]?t:`${t.length?`${t},`:``}${n.toString()}`,``),O=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),ne=new Set(Object.values(m)),re=new Map([...[`multistatus`,`response`,`propstat`,`prop`,`href`,`status`,`error`,`responsedescription`,`resourcetype`,`collection`,`getetag`,`displayname`,`syncToken`,`supportedReportSet`,`supportedReport`,`report`,`currentUserPrincipal`].map(e=>[e,`DAV:`]),...[`calendarData`,`calendar`,`calendarDescription`,`calendarTimezone`,`calendarHomeSet`,`calendarUserAddressSet`,`supportedCalendarComponentSet`,`comp`].map(e=>[e,`urn:ietf:params:xml:ns:caldav`]),...[`addressData`,`addressbook`,`addressbookHomeSet`].map(e=>[e,`urn:ietf:params:xml:ns:carddav`]),[`getctag`,`http://calendarserver.org/ns/`],[`calendarColor`,`http://apple.com/ns/ical/`]]),k=(e,t)=>{let n=te(e.replace(/^.*:/,``)),r=re.get(n);return t&&r&&t!==r?`{${t}}${n}`:n},A=(e,t)=>{let n=Object.assign(Object.create(null),t),r=Object.create(null);for(let[t,i]of Object.entries(e.attributes??{}))(t===`xmlns`||t.startsWith(`xmlns:`))&&(n[t===`xmlns`?``:t.slice(6)]=String(i)),t!==`xmlns`&&(r[t]=i);let i=e.name??``,a=i.indexOf(`:`),o=n[a===-1?``:i.slice(0,a)]??``,s=e.elements??[],c=s.filter(e=>e.type===`element`),l=s.filter(e=>e.type===`text`||e.type===`cdata`).map(e=>String(e.text??e.cdata??``)).join(``),u={},d=Object.create(null);if(Object.keys(r).length&&(u._attributes=r),!c.length){let e=s.some(e=>e.type===`text`),t=s.some(e=>e.type===`cdata`);return e?{value:l,namespace:o,namespaces:d}:(t&&(u._cdata=l),{value:u,namespace:o,namespaces:d})}l.trim()&&(u._text=l);for(let e of c){let t=A(e,n),r=te((e.name??``).replace(/^.*:/,``)),i=k(e.name??``,t.namespace),a=d[i];if(O(u,i)&&a!==t.namespace){if(ne.has(t.namespace)&&!ne.has(a)){let e=`{${a}}${r}`;Object.defineProperty(u,e,{value:u[i],enumerable:!0,configurable:!0,writable:!0}),d[e]=a,delete u[i]}else i=`{${t.namespace}}${r}`}O(u,i)?Array.isArray(u[i])?u[i].push(t.value):u[i]=[u[i],t.value]:Object.defineProperty(u,i,{value:t.value,enumerable:!0,configurable:!0,writable:!0}),d[i]=t.namespace,r===`prop`&&t.namespace===`DAV:`&&(u.propNamespaces=t.namespaces)}return{value:u,namespace:o,namespaces:d}},ie=e=>{let t=ee.default.xml2js(e,{compact:!1,ignoreDeclaration:!0}),n={};for(let e of t.elements??[]){if(e.type!==`element`)continue;let t=A(e,{}),r=k(e.name??``,t.namespace);Object.defineProperty(n,r,{value:t.value,enumerable:!0})}return n},j=e=>{let t=new Map;for(let n of e)if(n.ok)for(let[e,r]of Object.entries(n.props)){let i=t.get(e)??new Map;i.set(n.namespaces?.[e]??``,r),t.set(e,i)}let n={};for(let[e,r]of t){let t=[...r.keys()].find(e=>ne.has(e))??r.keys().next().value;for(let[i,a]of r)Object.defineProperty(n,i===t?e:`{${i}}${e}`,{value:a,enumerable:!0,configurable:!0,writable:!0})}return n};var ae=s({cleanupFalsy:()=>R,conditionalParam:()=>oe,ensureTrailingSlash:()=>N,excludeHeaders:()=>z,getDAVAttribute:()=>L,mergeHeaders:()=>B,urlContains:()=>F,urlEquals:()=>P,urlMatches:()=>I});const M=e=>{let t=e.trim();return t.endsWith(`/`)?t.slice(0,-1):t},N=e=>{let t=e.trim(),n=t.search(/[?#]/),r=n===-1?t:t.slice(0,n),i=n===-1?``:t.slice(n);return`${r.endsWith(`/`)?r:`${r}/`}${i}`},P=(e,t)=>!e&&!t?!0:!e||!t?!1:M(e)===M(t),F=(e,t)=>{if(!e&&!t)return!0;if(!e||!t)return!1;let n=M(e),r=M(t);return n.includes(r)||r.includes(n)},I=(e,t,n)=>{if(!e||!t||!n)return P(e,t);try{return P(new URL(e,n).href,new URL(t,n).href)}catch{return P(e,t)}},L=e=>e.reduce((e,t)=>({...e,[h[t]]:t}),{}),R=e=>Object.entries(e).reduce((e,[t,n])=>n?{...e,[t]:n}:e,{}),oe=(e,t)=>t?{[e]:t}:{},z=(e,t)=>{if(!e)return{};if(!t||t.length===0)return e;let n=new Set(t.map(e=>e.toLowerCase()));return Object.fromEntries(Object.entries(e).filter(([e])=>!n.has(e.toLowerCase())))},B=(...e)=>{let t=new Map,n=(e,n)=>{t.set(e.toLowerCase(),[e,n])};for(let t of e)if(t){if(Array.isArray(t)){for(let[e,r]of t)n(e,r);continue}if(typeof t.forEach==`function`){t.forEach((e,t)=>{n(t,e)});continue}for(let[e,r]of Object.entries(t))n(e,r)}return Object.fromEntries(t.values())};var se=s({createObject:()=>U,davRequest:()=>V,deleteObject:()=>G,propfind:()=>H,updateObject:()=>W});const ce=(0,p.default)(`tsdav:request`),le=e=>{let t=/^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(e?.trim()??``),n=t?.groups?.status,r=t?.groups?.statusText;return n?{status:Number.parseInt(n,10),statusText:r??``}:void 0},V=async e=>{let{url:t,init:n,convertIncoming:r=!0,parseOutgoing:i=!0,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c=s??T,{headers:l={},body:u,namespace:d,method:f,attributes:p}=n,m=u;p&&typeof u==`object`&&u&&!Array.isArray(u)&&(m=Object.fromEntries(Object.entries(u).map(([e,t])=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let n=t;return[e,{...n,_attributes:{...p,...n._attributes}}]}return[e,t]})));let h=r&&u!=null?ee.default.js2xml({_declaration:{_attributes:{version:`1.0`,encoding:`utf-8`}},...m},{compact:!0,spaces:2,elementNameFn:e=>d&&!/^.+:.+/.test(e)?`${d}:${e}`:e}):u,g={...o};delete g.headers;let _=z(B({"Content-Type":`text/xml;charset=UTF-8`},R(l),o.headers),a),v=await c(t,{...g,headers:_,body:h,method:f}),y=await v.text();if(!v.ok||!v.headers.get(`content-type`)?.toLowerCase().includes(`xml`)||!i||!y)return[{href:v.url,ok:v.ok,status:v.status,statusText:v.statusText,raw:y}];let b;try{b=ie(y)}catch(e){return ce(`Failed to parse DAV response XML: ${e.message}`),[{href:v.url,ok:!1,status:v.status,statusText:v.statusText,raw:y,parseError:e.message}]}return b?.multistatus?(Array.isArray(b.multistatus.response)?b.multistatus.response:[b.multistatus.response]).map(e=>{if(!e)return{raw:b,status:v.status,statusText:v.statusText,ok:v.ok};let t=(Array.isArray(e.propstat)?e.propstat:e.propstat?[e.propstat]:[]).map(e=>{let t=le(e.status),n=t?.status??0;return{props:e.prop??{},namespaces:e.propNamespaces,status:n,statusText:t?.statusText??`Invalid DAV property status`,ok:n>=200&&n<300,error:e.error,responsedescription:e.responsedescription}}),n=t.length>0&&t.every(e=>!e.ok)?t[0]:void 0,r=le(e.status)??n,i=r?.status??v.status;return{raw:b,href:e.href,status:i,statusText:r?.statusText??v.statusText,ok:i>=200&&i<300,error:e.error,responsedescription:e.responsedescription,propStats:t,props:j(t)}}):[{href:v.url,ok:v.ok,status:v.status,statusText:v.statusText,raw:b}]},H=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return V({url:t,init:{method:`PROPFIND`,headers:z(R({depth:r,...i}),a),namespace:`d`,body:{propfind:{_attributes:L([`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`,`http://calendarserver.org/ns/`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),prop:n}}},headersToExclude:a,fetchOptions:o,fetch:s})},U=async e=>{let{url:t,data:n,headers:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??T,{headers:c,...l}=a;return s(t,{...l,method:`PUT`,body:n,headers:z(B(r,c),i)})},W=async e=>{let{url:t,data:n,etag:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c=s??T,{headers:l,...u}=o;return c(t,{...u,method:`PUT`,body:n,headers:z(B(R({"If-Match":r,...i}),l),a)})},G=async e=>{let{url:t,headers:n,etag:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??T,{headers:c,...l}=a;return s(t,{...l,method:`DELETE`,headers:z(B(R({"If-Match":r,...n}),c),i)})},K=(e,t)=>{let n;try{n=new URL(e,N(t)).href}catch{n=e.trim()}return n.endsWith(`/`)?n.slice(0,-1):n},ue=(e,t,n,r=!1,i=[])=>{let a=new Map(e.map(e=>[K(e.url,n),e])),o=new Map(t.map(e=>[K(e.url,n),e])),s=r?i:e.filter(e=>!o.has(K(e.url,n))),c=new Set(s.map(e=>K(e.url,n))),l=t.filter(e=>!a.has(K(e.url,n))),u=[],d=[];for(let t of e){let e=K(t.url,n);if(c.has(e))continue;let i=o.get(e);i&&i.etag!==t.etag?u.push(i):(i||r)&&d.push(t)}return{created:l,updated:u,deleted:s,unchanged:d}},de=(e,t)=>{let n=e.find(e=>!e.ok||e.status>=400);if(n)throw Error(`${t}: ${n.status} ${n.statusText}`)},fe=(e,t)=>{de(e,t);for(let n of e)if((!n.raw?.multistatus||n.raw.multistatus.response)&&(pe(n,`resourcetype`,t),!n.props||!O(n.props,`resourcetype`)||typeof n.href!=`string`||!n.href))throw Error(`${t}: missing resourcetype or href in DAV multistatus response`)},pe=(e,t,n)=>{let r=e.propStats?.find(e=>!e.ok&&O(e.props,t));if(r&&!O(e.props??{},t))throw Error(`${n}: ${t} returned ${r.status} ${r.statusText}`)},me=(e,t,n,r,i)=>{de(e,i);let a=new Set;for(let n of e){if(pe(n,t,i),!n.href||typeof(n.props?.[t]?._cdata??n.props?.[t])!=`string`)throw Error(`${i}: missing ${t} or href`);a.add(K(n.href,r))}let o=e=>K(e.replace(/\?.*$/,``),r),s=new Map;for(let e of n){let t=o(e);s.set(t,(s.get(t)??0)+1)}if(n.some(e=>!a.has(K(e,r))&&!(s.get(o(e))===1&&a.has(o(e)))))throw Error(`${i}: incomplete response`)},q=e=>{let t=typeof e==`string`||typeof e==`number`?String(e):e?._cdata??e?._text;return typeof t==`string`?t:void 0};var he=s({collectionQuery:()=>J,isCollectionDirty:()=>ye,makeCollection:()=>ve,smartCollectionSync:()=>X,smartCollectionSyncDetailed:()=>xe,supportedReportSet:()=>Y,syncCollection:()=>be});const ge=(0,p.default)(`tsdav:collection`),_e=(e,t)=>{try{return new URL(e,N(t)).href}catch{return e}},J=async e=>{let{url:t,body:n,depth:r,defaultNamespace:i=`d`,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e,l=await V({url:t,init:{method:`REPORT`,headers:z(R({depth:r,...a}),o),namespace:i,body:n},headersToExclude:o,fetchOptions:s,fetch:c}),u=l[0];if(i===`c`&&n?.[`calendar-query`]!=null&&l.length===1&&u&&u.status===404&&I(t,u.href,t)&&!u.error&&Object.keys(u.props??{}).length===0&&typeof u.raw==`object`&&u.raw!==null&&u.raw.multistatus?.response?.propstat==null)return[];let d=l.find(e=>!e.ok||e.status&&e.status>=400);if(d)throw Error(`Collection query failed: ${d.status} ${d.statusText}. ${typeof d.raw==`string`?`Raw response: ${d.raw.slice(0,4096)}`:``}`);if((n?.[`calendar-query`]||n?.[`calendar-multiget`]||n?.[`addressbook-query`]||n?.[`addressbook-multiget`])&&l.some(e=>!e.raw?.multistatus))throw Error(`Collection query failed: expected a DAV multistatus response`);if((n?.[`calendar-query`]||n?.[`calendar-multiget`]||n?.[`addressbook-query`]||n?.[`addressbook-multiget`])&&l.some(e=>e.raw?.multistatus?.response&&(typeof e.href!=`string`||!e.href)))throw Error(`Collection query failed: missing href in DAV response`);let f=l[0];return l.length===1&&f&&(!f.raw||f.raw.multistatus&&!f.raw.multistatus.response)&&f.status&&f.status<300?[]:l},ve=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return V({url:t,init:{method:`MKCOL`,headers:z(R({depth:r,...i}),a),namespace:`d`,body:n?{mkcol:{_attributes:L([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`]),set:{prop:n}}}:void 0},headersToExclude:a,fetchOptions:o,fetch:s})},Y=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await H({url:t.url,props:{"d:supported-report-set":{}},depth:`0`,headers:z(n,r),headersToExclude:r,fetchOptions:i,fetch:a}))[0]?.props?.supportedReportSet?.supportedReport;return o?(Array.isArray(o)?o:[o]).map(e=>e?.report?Object.keys(e.report)[0]:void 0).filter(e=>typeof e==`string`&&e.length>0):[]},ye=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await H({url:t.url,props:{"cs:getctag":{}},depth:`0`,headers:z(n,r),headersToExclude:r,fetchOptions:i,fetch:a})).find(e=>I(t.url,e.href,t.url));if(!o)throw Error(`Collection does not exist on server`);let s=o.propStats?.length&&o.propStats.every(e=>e.status===404&&O(e.props,`getctag`));if(!o.ok&&!s)throw Error(`Collection status check failed: ${o.status} ${o.statusText}`);let c=q(o.props?.getctag);return{isDirty:t.ctag==null||c==null||`${t.ctag}`!=`${c}`,newCtag:c}},be=e=>{let{url:t,props:n,headers:r,syncLevel:i,syncToken:a,headersToExclude:o,fetchOptions:s,fetch:c}=e;return V({url:t,init:{method:`REPORT`,namespace:`d`,headers:z({...r},o),body:{"sync-collection":{_attributes:L([`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"sync-level":i,"sync-token":a,"d:prop":n}}},headersToExclude:o,fetchOptions:s,fetch:c})},X=async e=>{let{collection:t,method:n,headers:r,headersToExclude:i,account:a,detailedResult:o,fetchOptions:s={},fetch:c}=e,l=[`accountType`,`homeUrl`];if(!a||!E(a,l))throw Error(a?`account must have ${D(a,l)} before smartCollectionSync`:`no account for smartCollectionSync`);let u=n??(t.reports?.includes(`syncCollection`)?`webdav`:`basic`);if(ge(`smart collection sync with type ${a.accountType} and method ${u}`),u===`webdav`){let e=await be({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{},"d:displayname":{}},syncLevel:1,syncToken:t.syncToken,headers:z(r,i),headersToExclude:i,fetchOptions:s,fetch:c}),n=e=>typeof e.href==`string`&&K(e.href,t.url)!==K(t.url,t.url)&&!e.props?.resourcetype?.collection,l=e.find(e=>(!e.ok||e.status>=400)&&!(e.status===404&&!e.propStats?.length&&n(e)));if(l)throw Error(`Collection sync failed: ${l.status} ${l.statusText}`);if(e.some(e=>e.raw&&!e.raw.multistatus))throw Error(`Collection sync failed: expected a DAV multistatus response`);if(e.some(e=>e.raw?.multistatus?.response&&(typeof e.href!=`string`||!e.href)))throw Error(`Collection sync failed: missing href in DAV response`);let u=e.filter(n),d=u.filter(e=>e.status!==404).map(e=>e.href),f=u.filter(e=>e.status===404).map(e=>e.href),p=t.objectMultiGet;if(d.length>0&&!p)throw Error(`collection.objectMultiGet is required for webdav sync changes`);let m=d.length?await p?.({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{}},objectUrls:d,depth:`1`,headers:z(r,i),headersToExclude:i,fetchOptions:s,fetch:c})??[]:[];me(m,a.accountType===`caldav`?`calendarData`:`addressData`,d,t.url,`Collection sync multi-get failed`);let h=m.map(e=>({url:_e(e.href??``,t.url),etag:q(e.props?.getetag),data:a?.accountType===`caldav`?e.props?.calendarData?._cdata??e.props?.calendarData:e.props?.addressData?._cdata??e.props?.addressData})),g=t.objects??[],_=f.map(e=>({url:_e(e,t.url),etag:``})),{created:v,updated:y,deleted:b,unchanged:x}=ue(g,h,t.url,!0,_);return{...t,objects:o?{created:v,updated:y,deleted:b}:[...x,...v,...y],syncToken:q(e[0]?.raw?.multistatus?.syncToken)??t.syncToken}}if(u===`basic`){let{isDirty:e,newCtag:n}=await ye({collection:t,headers:z(r,i),headersToExclude:i,fetchOptions:s,fetch:c});if(!e)return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t;let a=t.objects??[];if(!t.fetchObjects)throw Error(`collection.fetchObjects is required for basic sync changes`);let l=await t.fetchObjects({collection:t,headers:z(r,i),headersToExclude:i,fetchOptions:s,fetch:c})??[],{created:u,updated:d,deleted:f,unchanged:p}=ue(a,l,t.url);return{...t,objects:o?{created:u,updated:d,deleted:f}:[...p,...u,...d],ctag:n}}return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t},xe=async e=>X({...e,detailedResult:!0});var Se=s({addressBookMultiGet:()=>Te,addressBookQuery:()=>we,createVCard:()=>Oe,deleteVCard:()=>Ae,fetchAddressBooks:()=>Ee,fetchVCards:()=>De,updateVCard:()=>ke});const Ce=(0,p.default)(`tsdav:addressBook`),we=async e=>{let{url:t,props:n,filters:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return J({url:t,body:{"addressbook-query":R({_attributes:L([`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"d:prop":n,filter:r??{"prop-filter":{_attributes:{name:`FN`}}}})},defaultNamespace:`card`,depth:i,headers:z(a,o),headersToExclude:o,fetchOptions:s,fetch:c})},Te=async e=>{let{url:t,props:n,objectUrls:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return J({url:t,body:{"addressbook-multiget":R({_attributes:L([`DAV:`,`urn:ietf:params:xml:ns:carddav`]),"d:prop":n,"d:href":r})},defaultNamespace:`card`,depth:i,headers:z(a,o),headersToExclude:o,fetchOptions:s,fetch:c})},Ee=async e=>{let{account:t,headers:n,props:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e??{},s=[`homeUrl`,`rootUrl`];if(!t||!E(t,s))throw Error(t?`account must have ${D(t,s)} before fetchAddressBooks`:`no account for fetchAddressBooks`);let c=await H({url:t.homeUrl,props:{...r??{"d:displayname":{},"cs:getctag":{},"d:resourcetype":{},"d:sync-token":{}},"d:resourcetype":{}},depth:`1`,headers:z(n,i),headersToExclude:i,fetchOptions:a,fetch:o});return fe(c,`Address book discovery failed`),Promise.all(c.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`addressbook`)).map(e=>{let n=e.props?.displayname?._cdata??e.props?.displayname;return Ce(`Found address book named ${typeof n==`string`?n:``}, + props: ${JSON.stringify(e.props)}`),{url:new URL(e.href??``,N(t.rootUrl??``)).href,ctag:q(e.props?.getctag),displayName:typeof n==`string`?n:``,resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:q(e.props?.syncToken)}}).map(async e=>({...e,reports:await Y({collection:e,headers:z(n,i),headersToExclude:i,fetchOptions:a,fetch:o})})))},De=async e=>{let{addressBook:t,headers:n,objectUrls:r,headersToExclude:i,urlFilter:a=e=>!!e,useMultiGet:o=!0,fetchOptions:s={},fetch:c}=e;Ce(`Fetching vcards from ${t?.url}`);let l=[`url`];if(!t||!E(t,l))throw Error(t?`addressBook must have ${D(t,l)} before fetchVCards`:`cannot fetchVCards for undefined addressBook`);let u=(r??(await we({url:t.url,props:{"d:getetag":{}},depth:`1`,headers:z(n,i),headersToExclude:i,fetchOptions:s,fetch:c})).map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,N(t.url)).href).filter(e=>!P(e,t.url)).filter(a).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),d=new Set(u.map(e=>K(e,t.url))),f=[];return u.length>0&&(o?f=await Te({url:t.url,props:{"d:getetag":{},"card:address-data":{}},objectUrls:u,depth:`1`,headers:z(n,i),headersToExclude:i,fetchOptions:s,fetch:c}):(f=await we({url:t.url,props:{"d:getetag":{},"card:address-data":{}},depth:`1`,headers:z(n,i),headersToExclude:i,fetchOptions:s,fetch:c}),f=f.filter(e=>!!e.href&&d.has(K(e.href,t.url))))),me(f,`addressData`,u,t.url,`VCard fetch failed`),f.map(e=>({url:new URL(e.href??``,N(t.url)).href,etag:q(e.props?.getetag),data:e.props?.addressData?._cdata??e.props?.addressData}))},Oe=async e=>{let{addressBook:t,vCardString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return U({url:new URL(r,N(t.url)).href,data:n,headers:z({"content-type":`text/vcard; charset=utf-8`,"If-None-Match":`*`,...i},a),headersToExclude:a,fetchOptions:o,fetch:s})},ke=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return W({url:t.url,data:t.data,etag:t.etag,headers:z({"content-type":`text/vcard; charset=utf-8`,...n},r),headersToExclude:r,fetchOptions:i,fetch:a})},Ae=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return G({url:t.url,etag:t.etag,headers:z(n,r),headersToExclude:r,fetchOptions:i,fetch:a})};var je=s({calendarMultiGet:()=>ze,calendarQuery:()=>Re,createCalendarObject:()=>Ue,deleteCalendarObject:()=>Ge,fetchCalendarObjects:()=>He,fetchCalendarUserAddresses:()=>Le,fetchCalendars:()=>Ve,freeBusyQuery:()=>Je,makeCalendar:()=>Be,syncCalendars:()=>Ke,syncCalendarsDetailed:()=>qe,updateCalendarObject:()=>We});const Me=(0,p.default)(`tsdav:calendar`),Ne=/^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i,Pe=/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i,Fe=e=>{let{start:t,end:n}=e;if(!(Ne.test(t)&&Ne.test(n)||Pe.test(t)&&Pe.test(n)))throw Error(`invalid timeRange format, not in ISO8601`);if(Number.isNaN(new Date(t).getTime())||Number.isNaN(new Date(n).getTime()))throw Error(`invalid timeRange: start or end is not a valid date`);if(new Date(t).getTime()>=new Date(n).getTime())throw Error(`invalid timeRange: start must be before end`)},Ie=e=>{let t=[];return Array.isArray(e)?t=e.map(e=>e?._attributes?.name):e&&typeof e==`object`&&(t=[e._attributes?.name]),t.filter(e=>typeof e==`string`&&e.length>0)},Le=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!E(t,o))throw Error(`account must have ${D(t,o)} before fetchUserAddresses`);Me(`Fetch user addresses from ${t.principalUrl}`);let s=(await H({url:t.principalUrl,props:{"c:calendar-user-address-set":{}},depth:`0`,headers:z(n,r),headersToExclude:r,fetchOptions:i,fetch:a})).find(e=>I(t.principalUrl,e.href,t.rootUrl));if(!s||!s.ok)throw Error(`cannot find calendarUserAddresses`);let c=s?.props?.calendarUserAddressSet?.href,l=[];Array.isArray(c)?l=c:c&&(l=[c]);let u=l.filter(e=>typeof e==`string`&&e.length>0);return Me(`Fetched calendar user addresses ${u}`),u},Re=async e=>{let{url:t,props:n,filters:r,timezone:i,depth:a,headers:o,headersToExclude:s,fetchOptions:c={},fetch:l}=e;return J({url:t,body:{"calendar-query":R({_attributes:L([`urn:ietf:params:xml:ns:caldav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`,`DAV:`]),"d:prop":n,filter:r,timezone:i})},defaultNamespace:`c`,depth:a,headers:z(o,s),headersToExclude:s,fetchOptions:c,fetch:l})},ze=async e=>{let{url:t,props:n,objectUrls:r,filters:i,timezone:a,depth:o,headers:s,headersToExclude:c,fetchOptions:l={},fetch:u}=e;return J({url:t,body:{"calendar-multiget":R({_attributes:L([`DAV:`,`urn:ietf:params:xml:ns:caldav`]),"d:prop":n,"d:href":r,filter:i,timezone:a})},defaultNamespace:`c`,depth:o,headers:z(s,c),headersToExclude:c,fetchOptions:l,fetch:u})},Be=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return V({url:t,init:{method:`MKCALENDAR`,headers:z(R({depth:r,...i}),a),namespace:`d`,body:{"c:mkcalendar":{_attributes:L([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`]),set:{prop:n}}}},headersToExclude:a,fetchOptions:o,fetch:s})},Ve=async e=>{let{headers:t,account:n,props:r,projectedProps:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e??{},c=[`homeUrl`,`rootUrl`];if(!n||!E(n,c))throw Error(n?`account must have ${D(n,c)} before fetchCalendars`:`no account for fetchCalendars`);let l=await H({url:n.homeUrl,props:{...r??{"c:calendar-description":{},"c:calendar-timezone":{},"d:displayname":{},"ca:calendar-color":{},"cs:getctag":{},"d:resourcetype":{},"c:supported-calendar-component-set":{},"d:sync-token":{}},"d:resourcetype":{},"c:supported-calendar-component-set":{}},depth:`1`,headers:z(t,a),headersToExclude:a,fetchOptions:o,fetch:s});return fe(l,`Calendar discovery failed`),Promise.all(l.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`calendar`)).filter(e=>{let t=Ie(e.props?.supportedCalendarComponentSet?.comp);return t.length===0||t.some(e=>Object.values(_).includes(e))}).map(e=>{let t=e.props?.calendarDescription,r=e.props?.calendarTimezone,a=e.props?.supportedCalendarComponentSet?.comp,o=Object.entries(e.props??{}).filter(([e])=>i?.[e]);return{description:typeof t==`string`?t:``,timezone:typeof r==`string`?r:``,url:new URL(e.href??``,N(n.rootUrl??``)).href,ctag:q(e.props?.getctag),calendarColor:e.props?.calendarColor,displayName:q(e.props?.displayname),components:Ie(a),resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:q(e.props?.syncToken),...i&&o.length>0?{projectedProps:Object.fromEntries(o)}:{}}}).map(async e=>({...e,reports:await Y({collection:e,headers:z(t,a),headersToExclude:a,fetchOptions:o,fetch:s})})))},He=async e=>{let{calendar:t,objectUrls:n,filters:r,timeRange:i,headers:a,expand:o,urlFilter:s=e=>!!e?.includes(`.ics`),useMultiGet:c=!0,headersToExclude:l,fetchOptions:u={},fetch:d}=e;if(o&&!i)throw Error(`timeRange is required when expand is true`);i&&Fe(i),Me(`Fetching calendar objects from ${t?.url}`);let f=[`url`];if(!t||!E(t,f))throw Error(t?`calendar must have ${D(t,f)} before fetchCalendarObjects`:`cannot fetchCalendarObjects for undefined calendar`);let p=r??[{"comp-filter":{_attributes:{name:`VCALENDAR`},"comp-filter":{_attributes:{name:`VEVENT`},...i?{"time-range":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}}}],m=[];n||(m=await Re({url:t.url,props:{"d:getetag":{},...o&&i?{"c:calendar-data":{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}}:{}},filters:p,depth:`1`,headers:z(a,l),headersToExclude:l,fetchOptions:u,fetch:d}));let h=(n??m.map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,N(t.url)).href).filter(s).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),g=new Set(h.map(e=>K(e,t.url))),_=[];return h.length>0&&(o&&!n?_=m.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href:new URL(e.href??``,N(t.url)).href;return s(n??``)}):c?_=await ze({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},objectUrls:h,depth:`1`,headers:z(a,l),headersToExclude:l,fetchOptions:u,fetch:d}):(_=await Re({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},filters:p,depth:`1`,headers:z(a,l),headersToExclude:l,fetchOptions:u,fetch:d}),_=_.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href??``:new URL(e.href??``,N(t.url)).href;return g.has(K(n,t.url))}))),me(_,`calendarData`,h,t.url,`Calendar object fetch failed`),_.map(e=>({url:new URL(e.href??``,N(t.url)).href,etag:q(e.props?.getetag),data:e.props?.calendarData?._cdata??e.props?.calendarData}))},Ue=async e=>{let{calendar:t,iCalString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return U({url:new URL(r,N(t.url)).href,data:n,headers:z({"content-type":`text/calendar; charset=utf-8`,"If-None-Match":`*`,...i},a),headersToExclude:a,fetchOptions:o,fetch:s})},We=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return W({url:t.url,data:t.data,etag:t.etag,headers:z({"content-type":`text/calendar; charset=utf-8`,...n},r),headersToExclude:r,fetchOptions:i,fetch:a})},Ge=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return G({url:t.url,etag:t.etag,headers:z(n,r),headersToExclude:r,fetchOptions:i,fetch:a})},Ke=async e=>{let{oldCalendars:t,account:n,detailedResult:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`Must have account before syncCalendars`);let c=t??n.calendars??[],l=await Ve({account:n,headers:z(i,a),headersToExclude:a,fetchOptions:o,fetch:s}),u=n.rootUrl??n.homeUrl??n.serverUrl,d=new Map(c.map(e=>[K(e.url,u),e])),f=new Map(l.map(e=>[K(e.url,u),e])),p=l.filter(e=>!d.has(K(e.url,u))),m=[],h=[],g=[];for(let e of c){let t=f.get(K(e.url,u));t?!t.syncToken&&!t.ctag||t.syncToken&&t.syncToken!==e.syncToken||t.ctag&&t.ctag!==e.ctag?m.push({local:e,remote:t}):h.push(e):g.push(e)}Me(`updated calendars: ${m.map(({remote:e})=>e.displayName)}`);let _=await Promise.all(m.map(async({local:e,remote:t})=>{let r=async e=>{if(!e)return[];let{collection:t,...n}=e;return He({...n,calendar:t,filters:{"comp-filter":{_attributes:{name:`VCALENDAR`}}},urlFilter:e=>K(e,t.url)!==K(t.url,t.url)})},c={...t,ctag:e.ctag,syncToken:e.syncToken,objects:e.objects,objectMultiGet:ze,fetchObjects:r},l=await X({collection:c,detailedResult:!1,headers:z(i,a),headersToExclude:a,account:n,fetchOptions:o,fetch:s});return{...l,ctag:t.reports?.includes(`syncCollection`)?t.ctag??l.ctag:l.ctag??t.ctag,syncToken:t.reports?.includes(`syncCollection`)?l.syncToken:t.syncToken??l.syncToken}}));return r?{created:p,updated:_,deleted:g}:[...h,...p,..._]},qe=async e=>Ke({...e,detailedResult:!0}),Je=async e=>{let{url:t,timeRange:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`timeRange is required`);Fe(n);let c=(await J({url:t,body:{"free-busy-query":R({_attributes:L([`urn:ietf:params:xml:ns:caldav`]),"c:time-range":{_attributes:{start:`${new Date(n.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(n.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}})},defaultNamespace:`c`,depth:r,headers:z(i,a),headersToExclude:a,fetchOptions:o,fetch:s}))[0];if(!c)throw Error(`freeBusyQuery returned no response`);return c};var Ye=s({createAccount:()=>tt,fetchHomeUrl:()=>et,fetchPrincipalUrl:()=>$e,serviceDiscovery:()=>Ze});const Z=(0,p.default)(`tsdav:account`),Xe=(e,t)=>{let n=[t,e,new URL(`/`,e).href];return n.filter((e,t)=>n.indexOf(e)===t)},Ze=async e=>{Z(`Service discovery...`);let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=a??T,s=new URL(t.serverUrl),{headers:c,...l}=i,u=new URL(`/.well-known/${t.accountType}`,s);u.protocol=s.protocol??`http`;let d=e=>{if(e.status>=300&&e.status<400){let t=e.headers.get(`Location`);if(typeof t==`string`&&t.length)return Z(`Service discovery redirected to ${t}`),new URL(t,u).href}};try{let e=d(await o(u.href,{...l,method:`PROPFIND`,headers:z(B({"Content-Type":`text/xml;charset=UTF-8`},n,c),r),body:` -`,redirect:`manual`}));if(e)return e}catch(e){Z(`Service discovery PROPFIND failed: ${e.stack}`)}try{let e=d(await o(u.href,{...l,method:`GET`,headers:F(I(n,c),r),redirect:`manual`}));if(e)return e}catch(e){Z(`Service discovery GET failed: ${e.stack}`)}return s.href},Ve=e=>{if(typeof e==`string`&&e.trim().length>0)return e.trim();if(Array.isArray(e)){for(let t of e){let e=Ve(t);if(e)return e}return}if(e&&typeof e==`object`){if(`_cdata`in e&&typeof e._cdata==`string`){let t=e._cdata.trim();if(t.length>0)return t}if(`_text`in e&&typeof e._text==`string`){let t=e._text.trim();if(t.length>0)return t}}},He=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`rootUrl`];if(!U(t,o))throw Error(`account must have ${W(t,o)} before fetchPrincipalUrl`);Z(`Fetching principal url from path ${t.rootUrl}`);let[s]=await z({url:t.rootUrl,props:{"d:current-user-principal":{}},depth:`0`,headers:F(n,r),fetchOptions:i,fetch:a});if(!s?.ok)throw Z(`Fetch principal url failed: ${s?.statusText??`empty response`}`),s?.status===401?Error(`Invalid credentials: PROPFIND ${t.rootUrl} returned 401 Unauthorized`):Error(`cannot find principalUrl`);let c=Ve(s.props?.currentUserPrincipal?.href);if(!c)throw Z(`Fetch principal url failed: missing current-user-principal href`),Error(`cannot find principalUrl`);return Z(`Fetched principal url ${c}`),new URL(c,O(t.rootUrl)).href},Ue=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!U(t,o))throw Error(`account must have ${W(t,o)} before fetchHomeUrl`);Z(`Fetch home url from ${t.principalUrl}`);let s=await z({url:t.principalUrl,props:t.accountType===`caldav`?{"c:calendar-home-set":{}}:{"card:addressbook-home-set":{}},depth:`0`,headers:F(n,r),fetchOptions:i,fetch:a}),c=s.find(e=>j(t.principalUrl,e.href,t.rootUrl));if(!c||!c.ok)throw Z(`Fetch home url failed with status ${c?.statusText} and error ${JSON.stringify(s.map(e=>e.error))}`),Error(`cannot find homeUrl`);let l=Ve(t.accountType===`caldav`?c.props?.calendarHomeSet?.href:c.props?.addressbookHomeSet?.href);if(!l)throw Z(`Fetch home url failed: server did not return a ${t.accountType===`caldav`?`calendar-home-set`:`addressbook-home-set`} href`),Error(`cannot find homeUrl`);let u=new URL(l,O(t.rootUrl)).href;return Z(`Fetched home url ${u}`),u},We=async e=>{let{account:t,headers:n,loadCollections:r=!1,loadObjects:i=!1,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c={...t},l=t.rootUrl??await Be({account:t,headers:F(n,a),fetchOptions:o,fetch:s});if(t.rootUrl)c.rootUrl=t.rootUrl;else if(t.principalUrl)c.rootUrl=l;else{let e=async(t,r=0,i)=>{let l=t[r];if(!l)throw i??Error(`cannot find principalUrl`);try{return{rootUrl:l,principalUrl:await He({account:{...c,rootUrl:l},headers:F(n,a),fetchOptions:o,fetch:s})}}catch(n){return e(t,r+1,n)}},{rootUrl:r,principalUrl:i}=await e(ze(t.serverUrl,l));c.rootUrl=r,c.principalUrl=i}return c.principalUrl=t.principalUrl??c.principalUrl??await He({account:c,headers:F(n,a),fetchOptions:o,fetch:s}),c.homeUrl=t.homeUrl??await Ue({account:c,headers:F(n,a),fetchOptions:o,fetch:s}),(r||i)&&(t.accountType===`caldav`?c.calendars=await je({headers:F(n,a),account:c,fetchOptions:o,fetch:s}):t.accountType===`carddav`&&(c.addressBooks=await ve({headers:F(n,a),account:c,fetchOptions:o,fetch:s}))),i&&(t.accountType===`caldav`&&c.calendars?c.calendars=await Promise.all(c.calendars.map(async e=>({...e,objects:await X({calendar:e,headers:F(n,a),fetchOptions:o,fetch:s})}))):t.accountType===`carddav`&&c.addressBooks&&(c.addressBooks=await Promise.all(c.addressBooks.map(async e=>({...e,objects:await ye({addressBook:e,headers:F(n,a),fetchOptions:o,fetch:s})}))))),c};var Ge=s({defaultParam:()=>$,fetchOauthTokens:()=>Qe,getBasicAuthHeaders:()=>Xe,getBearerAuthHeaders:()=>Ze,getOauthHeaders:()=>et,refreshAccessToken:()=>$e});const Ke=(0,p.default)(`tsdav:authHelper`),Q=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;var qe=class extends Error{constructor(e){super(e),this.name=`InvalidCharacterError`}};const Je=e=>{if(e>255)throw new qe(`The string to be encoded contains characters outside of the Latin1 range.`)},Ye=e=>{let t=``,n=0;for(;n(...n)=>{let r=n[0],i={...t,...r};return(t.headers||r?.headers)&&(i.headers=I(t.headers,r?.headers)),e(i)},Xe=e=>(Ke(`Basic auth token generated for user "${e.username??``}"`),{authorization:`Basic ${Ye(`${e.username}:${e.password}`)}`}),Ze=e=>({authorization:`Bearer ${e.accessToken}`}),Qe=async(e,t,n)=>{let r=[`authorizationCode`,`redirectUrl`,`clientId`,`clientSecret`,`tokenUrl`];if(!U(e,r))throw Error(`Oauth credentials missing: ${W(e,r)}`);let i=new URLSearchParams({grant_type:`authorization_code`,code:e.authorizationCode,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret});Ke(`Fetching oauth tokens from ${e.tokenUrl}`);let a=n??T,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:I({"content-type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Ke(`Fetch Oauth tokens failed with status ${c.status}`),{})},$e=async(e,t,n)=>{let r=[`refreshToken`,`clientId`,`clientSecret`,`tokenUrl`];if(!U(e,r))throw Error(`Oauth credentials missing: ${W(e,r)}`);let i=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,refresh_token:e.refreshToken,grant_type:`refresh_token`}),a=n??T,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:I({"Content-Type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Ke(`Refresh access token failed with status ${c.status}`),{})},et=async(e,t,n)=>{Ke(`Fetching oauth headers`);let r={},i=!1;return e.refreshToken?e.refreshToken&&!e.accessToken||Date.now()>(e.expiration??0)?(r=await $e(e,t,n),i=!0):r={access_token:e.accessToken,refresh_token:e.refreshToken}:(r=await Qe(e,t,n),i=!0),i&&(r.access_token&&(e.accessToken=r.access_token),r.refresh_token&&(e.refreshToken=r.refresh_token),typeof r.expires_in==`number`&&(e.expiration=Date.now()+r.expires_in*1e3)),Ke(`Oauth tokens obtained`),{tokens:r,headers:r.access_token?{authorization:`Bearer ${r.access_token}`}:{}}};var tt=s({DAVClient:()=>rt,createDAVClient:()=>nt});const nt=async e=>{let{serverUrl:t,credentials:n,authMethod:r=`Basic`,defaultAccountType:i,authFunction:a,fetchOptions:o,fetch:s}=e,c={};switch(r){case`Basic`:c=Xe(n);break;case`Bearer`:c=Ze(n);break;case`Oauth`:c=(await et(n,o,s)).headers;break;case`Digest`:c={Authorization:`Digest ${n.digestString}`};break;case`Custom`:if(!a)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);c=await a(n)??{};break;default:throw Error(`Invalid auth method`)}let l=i?await We({account:{serverUrl:t,credentials:n,accountType:i},headers:c,fetchOptions:o,fetch:s}):void 0,u=async e=>{let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...l}=t;return R({...i,init:{...l,headers:{...c,...a}},fetchOptions:n??o,fetch:r??s})},d={headers:c,fetchOptions:o,fetch:s},f={url:t,...d},p={account:l,...d},m=$(B,f),h=$(V,f),g=$(H,f),_=$(z,d),v=async e=>{let{account:r,headers:i,loadCollections:a,loadObjects:l,fetchOptions:u,fetch:d}=e,f={serverUrl:t,credentials:n,...r};if(!f.accountType)throw Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client.");return We({account:f,headers:{...c,...i},loadCollections:a,loadObjects:l,fetchOptions:u??o,fetch:d??s})},y=$(G,d),b=$(ue,d),x=$(pe,d),S=$(de,d),ee=$(fe,d),C=$(K,p),te=$(me,p),w=$(Y,d),T=$(ke,d),E=$(Ae,d),ne=$(je,p),re=$(Oe,p),D=$(X,d),O=$(Me,d),k=$(Ne,d),A=$(Pe,d),j=$(Fe,p),M=$(Ie,p),N=$(Le,d),P=$(q,d),F=$(_e,d);return{davRequest:u,propfind:_,createAccount:v,createObject:m,updateObject:h,deleteObject:g,calendarQuery:w,addressBookQuery:P,collectionQuery:y,makeCollection:b,calendarMultiGet:T,makeCalendar:E,freeBusyQuery:N,syncCollection:x,supportedReportSet:S,isCollectionDirty:ee,smartCollectionSync:C,smartCollectionSyncDetailed:te,fetchCalendars:ne,fetchCalendarUserAddresses:re,fetchCalendarObjects:D,createCalendarObject:O,updateCalendarObject:k,deleteCalendarObject:A,syncCalendars:j,syncCalendarsDetailed:M,fetchAddressBooks:$(ve,p),addressBookMultiGet:F,fetchVCards:$(ye,d),createVCard:$(be,d),updateVCard:$(xe,d),deleteVCard:$(Se,d)}};var rt=class{constructor(e){this.serverUrl=e.serverUrl,this.credentials=e.credentials,this.authMethod=e.authMethod??`Basic`,this.accountType=e.defaultAccountType??`caldav`,this.authFunction=e.authFunction,this.fetchOptions=e.fetchOptions??{},this.fetchOverride=e.fetch}async login(e){switch(this.authMethod){case`Basic`:this.authHeaders=Xe(this.credentials);break;case`Bearer`:this.authHeaders=Ze(this.credentials);break;case`Oauth`:this.authHeaders=(await et(this.credentials,this.fetchOptions,this.fetchOverride)).headers;break;case`Digest`:this.authHeaders={Authorization:`Digest ${this.credentials.digestString}`};break;case`Custom`:if(!this.authFunction)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);this.authHeaders=await this.authFunction(this.credentials);break;default:throw Error(`Invalid auth method`)}this.account=this.accountType?await We({account:{serverUrl:this.serverUrl,credentials:this.credentials,accountType:this.accountType},headers:this.authHeaders,loadCollections:e?.loadCollections,loadObjects:e?.loadObjects,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}):void 0}async davRequest(e){let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...o}=t;return R({...i,init:{...o,headers:{...this.authHeaders,...a}},fetchOptions:n??this.fetchOptions,fetch:r??this.fetchOverride})}async createObject(...e){return $(B,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateObject(...e){return $(V,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteObject(...e){return $(H,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async propfind(...e){return $(z,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createAccount(e){let{account:t,headers:n,loadCollections:r,loadObjects:i,fetchOptions:a,fetch:o}=e,s=t.accountType??this.accountType;if(!s)throw Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient.");return We({account:{serverUrl:this.serverUrl,credentials:this.credentials,...t,accountType:s},headers:{...this.authHeaders,...n},loadCollections:r,loadObjects:i,fetchOptions:a??this.fetchOptions,fetch:o??this.fetchOverride})}async collectionQuery(...e){return $(G,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async makeCollection(...e){return $(ue,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCollection(...e){return $(pe,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async supportedReportSet(...e){return $(de,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async isCollectionDirty(...e){return $(fe,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async smartCollectionSync(...e){return $(K,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride,account:this.account})(e[0])}async smartCollectionSyncDetailed(e){return $(me,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride,account:this.account})(e)}async calendarQuery(...e){return $(Y,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async makeCalendar(...e){return $(Ae,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async freeBusyQuery(...e){return $(Le,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async calendarMultiGet(...e){return $(ke,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async fetchCalendars(...e){return $(je,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchCalendarUserAddresses(...e){return $(Oe,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchCalendarObjects(...e){return $(X,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createCalendarObject(...e){return $(Me,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateCalendarObject(...e){return $(Ne,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteCalendarObject(...e){return $(Pe,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCalendars(...e){return $(Fe,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCalendarsDetailed(...e){return $(Ie,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async addressBookQuery(...e){return $(q,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async addressBookMultiGet(...e){return $(_e,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async fetchAddressBooks(...e){return $(ve,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchVCards(...e){return $(ye,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createVCard(...e){return $(be,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateVCard(...e){return $(xe,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteVCard(...e){return $(Se,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}},it={DAVNamespace:m,DAVNamespaceShort:g,DAVAttributeMap:h,ICALObjects:_,...tt,...ie,...oe,...Re,...he,...Ce,...Ge,...re};export{h as DAVAttributeMap,rt as DAVClient,m as DAVNamespace,g as DAVNamespaceShort,_ as ICALObjects,_e as addressBookMultiGet,q as addressBookQuery,ke as calendarMultiGet,Y as calendarQuery,N as cleanupFalsy,G as collectionQuery,We as createAccount,Me as createCalendarObject,nt as createDAVClient,B as createObject,be as createVCard,R as davRequest,it as default,Pe as deleteCalendarObject,H as deleteObject,Se as deleteVCard,O as ensureTrailingSlash,F as excludeHeaders,ve as fetchAddressBooks,X as fetchCalendarObjects,Oe as fetchCalendarUserAddresses,je as fetchCalendars,Ue as fetchHomeUrl,Qe as fetchOauthTokens,He as fetchPrincipalUrl,ye as fetchVCards,Le as freeBusyQuery,Xe as getBasicAuthHeaders,Ze as getBearerAuthHeaders,M as getDAVAttribute,et as getOauthHeaders,fe as isCollectionDirty,Ae as makeCalendar,I as mergeHeaders,z as propfind,$e as refreshAccessToken,Be as serviceDiscovery,K as smartCollectionSync,me as smartCollectionSyncDetailed,de as supportedReportSet,Fe as syncCalendars,Ie as syncCalendarsDetailed,pe as syncCollection,Ne as updateCalendarObject,V as updateObject,xe as updateVCard,A as urlContains,k as urlEquals,j as urlMatches}; \ No newline at end of file +`,redirect:`manual`}));if(e)return e}catch(e){Z(`Service discovery PROPFIND failed: ${e.stack}`)}try{let e=d(await o(u.href,{...l,method:`GET`,body:void 0,headers:z(B(n,c),r),redirect:`manual`}));if(e)return e}catch(e){Z(`Service discovery GET failed: ${e.stack}`)}return s.href},Qe=e=>{if(typeof e==`string`&&e.trim().length>0)return e.trim();if(Array.isArray(e)){for(let t of e){let e=Qe(t);if(e)return e}return}if(e&&typeof e==`object`){if(`_cdata`in e&&typeof e._cdata==`string`){let t=e._cdata.trim();if(t.length>0)return t}if(`_text`in e&&typeof e._text==`string`){let t=e._text.trim();if(t.length>0)return t}}},$e=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`rootUrl`];if(!E(t,o))throw Error(`account must have ${D(t,o)} before fetchPrincipalUrl`);Z(`Fetching principal url from path ${t.rootUrl}`);let[s]=await H({url:t.rootUrl,props:{"d:current-user-principal":{}},depth:`0`,headers:z(n,r),headersToExclude:r,fetchOptions:i,fetch:a});if(!s?.ok)throw Z(`Fetch principal url failed: ${s?.statusText??`empty response`}`),s?.status===401?Error(`Invalid credentials: PROPFIND ${t.rootUrl} returned 401 Unauthorized`):Error(`cannot find principalUrl`);let c=Qe(s.props?.currentUserPrincipal?.href);if(!c)throw Z(`Fetch principal url failed: missing current-user-principal href`),Error(`cannot find principalUrl`);return Z(`Fetched principal url ${c}`),new URL(c,N(t.rootUrl)).href},et=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!E(t,o))throw Error(`account must have ${D(t,o)} before fetchHomeUrl`);Z(`Fetch home url from ${t.principalUrl}`);let s=await H({url:t.principalUrl,props:t.accountType===`caldav`?{"c:calendar-home-set":{}}:{"card:addressbook-home-set":{}},depth:`0`,headers:z(n,r),headersToExclude:r,fetchOptions:i,fetch:a}),c=s.find(e=>I(t.principalUrl,e.href,t.rootUrl));if(!c||!c.ok)throw Z(`Fetch home url failed with status ${c?.statusText} and error ${JSON.stringify(s.map(e=>e.error))}`),Error(`cannot find homeUrl`);let l=Qe(t.accountType===`caldav`?c.props?.calendarHomeSet?.href:c.props?.addressbookHomeSet?.href);if(!l)throw Z(`Fetch home url failed: server did not return a ${t.accountType===`caldav`?`calendar-home-set`:`addressbook-home-set`} href`),Error(`cannot find homeUrl`);let u=new URL(l,N(t.rootUrl)).href;return Z(`Fetched home url ${u}`),u},tt=async e=>{let{account:t,headers:n,loadCollections:r=!1,loadObjects:i=!1,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c={...t},l=t.rootUrl??await Ze({account:t,headers:z(n,a),headersToExclude:a,fetchOptions:o,fetch:s});if(t.rootUrl)c.rootUrl=t.rootUrl;else if(t.principalUrl)c.rootUrl=l;else{let e=async(t,r=0,i)=>{let l=t[r];if(!l)throw i??Error(`cannot find principalUrl`);try{return{rootUrl:l,principalUrl:await $e({account:{...c,rootUrl:l},headers:z(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}}catch(n){return e(t,r+1,n)}},{rootUrl:r,principalUrl:i}=await e(Xe(t.serverUrl,l));c.rootUrl=r,c.principalUrl=i}return c.principalUrl=t.principalUrl??c.principalUrl??await $e({account:c,headers:z(n,a),headersToExclude:a,fetchOptions:o,fetch:s}),c.homeUrl=t.homeUrl??await et({account:c,headers:z(n,a),headersToExclude:a,fetchOptions:o,fetch:s}),(r||i)&&(t.accountType===`caldav`?c.calendars=await Ve({headers:z(n,a),headersToExclude:a,account:c,fetchOptions:o,fetch:s}):t.accountType===`carddav`&&(c.addressBooks=await Ee({headers:z(n,a),headersToExclude:a,account:c,fetchOptions:o,fetch:s}))),i&&(t.accountType===`caldav`&&c.calendars?c.calendars=await Promise.all(c.calendars.map(async e=>({...e,objects:await He({calendar:e,filters:{"comp-filter":{_attributes:{name:`VCALENDAR`}}},urlFilter:t=>!P(t,e.url),headers:z(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}))):t.accountType===`carddav`&&c.addressBooks&&(c.addressBooks=await Promise.all(c.addressBooks.map(async e=>({...e,objects:await De({addressBook:e,headers:z(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}))))),c};var nt=s({defaultParam:()=>ot,fetchOauthTokens:()=>lt,getBasicAuthHeaders:()=>st,getBearerAuthHeaders:()=>ct,getOauthHeaders:()=>dt,refreshAccessToken:()=>ut});const Q=(0,p.default)(`tsdav:authHelper`),$=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;var rt=class extends Error{constructor(e){super(e),this.name=`InvalidCharacterError`}};const it=e=>{if(e>255)throw new rt(`The string to be encoded contains characters outside of the Latin1 range.`)},at=e=>{let t=``,n=0;for(;n(...n)=>{let r=n[0],i={...t,...r};return(t.headers||r?.headers)&&(i.headers=B(t.headers,r?.headers)),e(i)},st=e=>(Q(`Basic auth token generated for user "${e.username??``}"`),{authorization:`Basic ${at(`${e.username}:${e.password}`)}`}),ct=e=>({authorization:`Bearer ${e.accessToken}`}),lt=async(e,t,n)=>{let r=[`authorizationCode`,`redirectUrl`,`clientId`,`clientSecret`,`tokenUrl`];if(!E(e,r))throw Error(`Oauth credentials missing: ${D(e,r)}`);let i=new URLSearchParams({grant_type:`authorization_code`,code:e.authorizationCode,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret});Q(`Fetching oauth tokens from ${e.tokenUrl}`);let a=n??T,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:B({"content-type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Q(`Fetch Oauth tokens failed with status ${c.status}`),{})},ut=async(e,t,n)=>{let r=[`refreshToken`,`clientId`,`clientSecret`,`tokenUrl`];if(!E(e,r))throw Error(`Oauth credentials missing: ${D(e,r)}`);let i=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,refresh_token:e.refreshToken,grant_type:`refresh_token`}),a=n??T,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:B({"Content-Type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(Q(`Refresh access token failed with status ${c.status}`),{})},dt=async(e,t,n)=>{Q(`Fetching oauth headers`);let r={},i=!1;return e.accessToken&&(e.expiration==null&&!e.refreshToken||e.expiration!=null&&Date.now()ht,createDAVClient:()=>mt});const pt=async(e,t=e.fetchOptions,n=e.fetchOverride)=>{switch(e.authMethod){case`Basic`:return st(e.credentials);case`Bearer`:return ct(e.credentials);case`Oauth`:{let{headers:r}=await dt(e.credentials,t,n);if(!r.authorization)throw Error(`OAuth authentication failed: token endpoint returned no access token`);return r}case`Digest`:return{Authorization:`Digest ${e.credentials.digestString}`};case`Custom`:if(!e.authFunction)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);return await e.authFunction(e.credentials)??{};default:throw Error(`Invalid auth method`)}},mt=async e=>{let t=new ht(e);return t.authHeaders=await pt(t),t.account=e.defaultAccountType?await tt({account:{serverUrl:e.serverUrl,credentials:e.credentials,accountType:e.defaultAccountType},headers:t.authHeaders,fetchOptions:t.fetchOptions,fetch:t.fetchOverride}):void 0,{davRequest:t.davRequest.bind(t),propfind:t.propfind.bind(t),createAccount:async(...e)=>{if(!e[0].account.accountType)throw Error("createAccount requires an accountType; pass one via `account.accountType`.");return t.createAccount(...e)},createObject:t.createObject.bind(t),updateObject:t.updateObject.bind(t),deleteObject:t.deleteObject.bind(t),calendarQuery:t.calendarQuery.bind(t),addressBookQuery:t.addressBookQuery.bind(t),collectionQuery:t.collectionQuery.bind(t),makeCollection:t.makeCollection.bind(t),calendarMultiGet:t.calendarMultiGet.bind(t),makeCalendar:t.makeCalendar.bind(t),freeBusyQuery:t.freeBusyQuery.bind(t),syncCollection:t.syncCollection.bind(t),supportedReportSet:t.supportedReportSet.bind(t),isCollectionDirty:t.isCollectionDirty.bind(t),smartCollectionSync:t.smartCollectionSync.bind(t),smartCollectionSyncDetailed:t.smartCollectionSyncDetailed.bind(t),fetchCalendars:t.fetchCalendars.bind(t),fetchCalendarUserAddresses:t.fetchCalendarUserAddresses.bind(t),fetchCalendarObjects:t.fetchCalendarObjects.bind(t),createCalendarObject:t.createCalendarObject.bind(t),updateCalendarObject:t.updateCalendarObject.bind(t),deleteCalendarObject:t.deleteCalendarObject.bind(t),syncCalendars:t.syncCalendars.bind(t),syncCalendarsDetailed:t.syncCalendarsDetailed.bind(t),fetchAddressBooks:t.fetchAddressBooks.bind(t),addressBookMultiGet:t.addressBookMultiGet.bind(t),fetchVCards:t.fetchVCards.bind(t),createVCard:t.createVCard.bind(t),updateVCard:t.updateVCard.bind(t),deleteVCard:t.deleteVCard.bind(t)}};var ht=class{constructor(e){this.serverUrl=e.serverUrl,this.credentials=e.credentials,this.authMethod=e.authMethod??`Basic`,this.accountType=e.defaultAccountType??`caldav`,this.authFunction=e.authFunction,this.fetchOptions=e.fetchOptions??{},this.fetchOverride=e.fetch,this.calendarMultiGet=this.calendarMultiGet.bind(this),this.addressBookMultiGet=this.addressBookMultiGet.bind(this)}async authenticate(e=!1,t=this.fetchOptions,n=this.fetchOverride){if(!e&&this.authMethod!==`Oauth`)return;if(!e&&this.authHeaders&&this.credentials.accessToken&&(this.credentials.expiration==null||Date.now(){this.authHeaders=await pt(this,t,n)};this.authentication=r();try{await this.authentication}finally{this.authentication=void 0}}async requestDefaults(e){return await this.authenticate(!1,e?.fetchOptions??this.fetchOptions,e?.fetch??this.fetchOverride),{url:this.serverUrl,headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}}async invoke(e,t){let n=await this.requestDefaults(t);return await ot(e,n)(t)}async login(e){await this.authenticate(!0),this.account=this.accountType?await tt({account:{serverUrl:this.serverUrl,credentials:this.credentials,accountType:this.accountType},headers:this.authHeaders,loadCollections:e?.loadCollections,loadObjects:e?.loadObjects,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}):void 0}async davRequest(e){let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...o}=t,s=await this.requestDefaults(e);return V({...i,init:{...o,headers:B(s.headers,a)},fetchOptions:n??this.fetchOptions,fetch:r??this.fetchOverride})}async createObject(...e){return this.invoke(U,e[0])}async updateObject(...e){return this.invoke(W,e[0])}async deleteObject(...e){return this.invoke(G,e[0])}async propfind(...e){return this.invoke(H,e[0])}async createAccount(e){let{account:t,headers:n,headersToExclude:r,loadCollections:i,loadObjects:a,fetchOptions:o,fetch:s}=e,c=await this.requestDefaults(e),l=t.accountType??this.accountType;if(!l)throw Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient.");return tt({account:{serverUrl:this.serverUrl,credentials:this.credentials,...t,accountType:l},headers:B(c.headers,n),headersToExclude:r,loadCollections:i,loadObjects:a,fetchOptions:o??this.fetchOptions,fetch:s??this.fetchOverride})}async collectionQuery(...e){return this.invoke(J,e[0])}async makeCollection(...e){return this.invoke(ve,e[0])}async syncCollection(...e){return this.invoke(be,e[0])}async supportedReportSet(...e){return this.invoke(Y,e[0])}async isCollectionDirty(...e){return this.invoke(ye,e[0])}async smartCollectionSync(...e){return this.invoke(X,e[0])}async smartCollectionSyncDetailed(e){return this.invoke(xe,e)}async calendarQuery(...e){return this.invoke(Re,e[0])}async makeCalendar(...e){return this.invoke(Be,e[0])}async freeBusyQuery(...e){return this.invoke(Je,e[0])}async calendarMultiGet(...e){return this.invoke(ze,e[0])}async fetchCalendars(...e){return this.invoke(Ve,e[0])}async fetchCalendarUserAddresses(...e){return this.invoke(Le,e[0])}async fetchCalendarObjects(...e){return this.invoke(He,e[0])}async createCalendarObject(...e){return this.invoke(Ue,e[0])}async updateCalendarObject(...e){return this.invoke(We,e[0])}async deleteCalendarObject(...e){return this.invoke(Ge,e[0])}async syncCalendars(...e){return this.invoke(Ke,e[0])}async syncCalendarsDetailed(...e){return this.invoke(qe,e[0])}async addressBookQuery(...e){return this.invoke(we,e[0])}async addressBookMultiGet(...e){return this.invoke(Te,e[0])}async fetchAddressBooks(...e){return this.invoke(Ee,e[0])}async fetchVCards(...e){return this.invoke(De,e[0])}async createVCard(...e){return this.invoke(Oe,e[0])}async updateVCard(...e){return this.invoke(ke,e[0])}async deleteVCard(...e){return this.invoke(Ae,e[0])}},gt={DAVNamespace:m,DAVNamespaceShort:g,DAVAttributeMap:h,ICALObjects:_,...ft,...se,...he,...Ye,...Se,...je,...nt,...ae};export{h as DAVAttributeMap,ht as DAVClient,m as DAVNamespace,g as DAVNamespaceShort,_ as ICALObjects,Te as addressBookMultiGet,we as addressBookQuery,ze as calendarMultiGet,Re as calendarQuery,R as cleanupFalsy,J as collectionQuery,tt as createAccount,Ue as createCalendarObject,mt as createDAVClient,U as createObject,Oe as createVCard,V as davRequest,gt as default,Ge as deleteCalendarObject,G as deleteObject,Ae as deleteVCard,N as ensureTrailingSlash,z as excludeHeaders,Ee as fetchAddressBooks,He as fetchCalendarObjects,Le as fetchCalendarUserAddresses,Ve as fetchCalendars,et as fetchHomeUrl,lt as fetchOauthTokens,$e as fetchPrincipalUrl,De as fetchVCards,Je as freeBusyQuery,st as getBasicAuthHeaders,ct as getBearerAuthHeaders,L as getDAVAttribute,dt as getOauthHeaders,ye as isCollectionDirty,Be as makeCalendar,ve as makeCollection,B as mergeHeaders,H as propfind,ut as refreshAccessToken,Ze as serviceDiscovery,X as smartCollectionSync,xe as smartCollectionSyncDetailed,Y as supportedReportSet,Ke as syncCalendars,qe as syncCalendarsDetailed,be as syncCollection,We as updateCalendarObject,W as updateObject,ke as updateVCard,F as urlContains,P as urlEquals,I as urlMatches}; \ No newline at end of file diff --git a/dist/tsdav.min.mjs b/dist/tsdav.min.mjs index d291b31c..e443bc0e 100644 --- a/dist/tsdav.min.mjs +++ b/dist/tsdav.min.mjs @@ -1,7 +1,7 @@ -import e from"debug";import t from"xml-js";var n=Object.defineProperty,r=(e,t)=>{let r={};for(var i in e)n(r,i,{get:e[i],enumerable:!0});return t||n(r,Symbol.toStringTag,{value:`Module`}),r};let i=function(e){return e.CALENDAR_SERVER=`http://calendarserver.org/ns/`,e.CALDAV_APPLE=`http://apple.com/ns/ical/`,e.CALDAV=`urn:ietf:params:xml:ns:caldav`,e.CARDDAV=`urn:ietf:params:xml:ns:carddav`,e.DAV=`DAV:`,e}({});const a={"urn:ietf:params:xml:ns:caldav":`xmlns:c`,"urn:ietf:params:xml:ns:carddav":`xmlns:card`,"http://calendarserver.org/ns/":`xmlns:cs`,"http://apple.com/ns/ical/":`xmlns:ca`,"DAV:":`xmlns:d`};let o=function(e){return e.CALDAV=`c`,e.CARDDAV=`card`,e.CALENDAR_SERVER=`cs`,e.CALDAV_APPLE=`ca`,e.DAV=`d`,e}({}),s=function(e){return e.VEVENT=`VEVENT`,e.VTODO=`VTODO`,e.VJOURNAL=`VJOURNAL`,e.VFREEBUSY=`VFREEBUSY`,e.VTIMEZONE=`VTIMEZONE`,e.VALARM=`VALARM`,e}({});const c=e=>e.replace(/[-_]+(\w?)/g,(e,t)=>t?t.toUpperCase():``),l=typeof globalThis<`u`&&typeof globalThis.fetch==`function`?globalThis.fetch.bind(globalThis):(()=>{throw Error("tsdav: global fetch is not available in this runtime. Upgrade to Node.js >= 18, run under a browser/Bun/Deno, or install a fetch polyfill on globalThis before importing tsdav. You can also pass a custom `fetch` implementation to `createDAVClient`, `DAVClient`, or individual request helpers.")}),u=/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/,d=e=>{if(typeof e!=`string`)return e;if(u.test(e)){let t=Number(e);if(!Number.isNaN(t)&&Number.isFinite(t)&&(!Number.isInteger(t)||Number.isSafeInteger(t)))return t}let t=e.toLowerCase();return t===`true`||t!==`false`&&e};var f=r({cleanupFalsy:()=>y,conditionalParam:()=>b,ensureTrailingSlash:()=>m,excludeHeaders:()=>x,getDAVAttribute:()=>v,mergeHeaders:()=>S,urlContains:()=>g,urlEquals:()=>h,urlMatches:()=>_});const p=e=>{let t=e.trim();return t.endsWith(`/`)?t.slice(0,-1):t},m=e=>{let t=e.trim(),n=t.search(/[?#]/),r=n===-1?t:t.slice(0,n),i=n===-1?``:t.slice(n);return`${r.endsWith(`/`)?r:`${r}/`}${i}`},h=(e,t)=>!e&&!t?!0:!e||!t?!1:p(e)===p(t),g=(e,t)=>{if(!e&&!t)return!0;if(!e||!t)return!1;let n=p(e),r=p(t);return n.includes(r)||r.includes(n)},_=(e,t,n)=>{if(!e||!t||!n)return h(e,t);try{return h(new URL(e,n).href,new URL(t,n).href)}catch{return h(e,t)}},v=e=>e.reduce((e,t)=>({...e,[a[t]]:t}),{}),y=e=>Object.entries(e).reduce((e,[t,n])=>n?{...e,[t]:n}:e,{}),b=(e,t)=>t?{[e]:t}:{},x=(e,t)=>{if(!e)return{};if(!t||t.length===0)return e;let n=new Set(t.map(e=>e.toLowerCase()));return Object.fromEntries(Object.entries(e).filter(([e])=>!n.has(e.toLowerCase())))},S=(...e)=>{let t=new Map,n=(e,n)=>{t.set(e.toLowerCase(),[e,n])};for(let t of e)if(t){if(Array.isArray(t)){for(let[e,r]of t)n(e,r);continue}if(typeof t.forEach==`function`){t.forEach((e,t)=>{n(t,e)});continue}for(let[e,r]of Object.entries(t))n(e,r)}return Object.fromEntries(t.values())};var C=r({createObject:()=>E,davRequest:()=>w,deleteObject:()=>O,propfind:()=>T,updateObject:()=>D});const ee=e(`tsdav:request`),te=e=>{let t=/^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(e??``),n=t?.groups?.status,r=t?.groups?.statusText;return n?{status:Number.parseInt(n,10),statusText:r??``}:void 0},w=async e=>{let{url:n,init:r,convertIncoming:i=!0,parseOutgoing:a=!0,fetchOptions:o={},fetch:s}=e,u=s??l,{headers:f={},body:p,namespace:m,method:h,attributes:g}=r,_=p;g&&typeof p==`object`&&p&&!Array.isArray(p)&&(_=Object.fromEntries(Object.entries(p).map(([e,t])=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let n=t;return[e,{...n,_attributes:{...g,...n._attributes}}]}return[e,t]})));let v=i&&p!=null?t.js2xml({_declaration:{_attributes:{version:`1.0`,encoding:`utf-8`}},..._},{compact:!0,spaces:2,elementNameFn:e=>m&&!/^.+:.+/.test(e)?`${m}:${e}`:e}):p,b={...o};delete b.headers;let x=S({"Content-Type":`text/xml;charset=UTF-8`},y(f),o.headers),C=await u(n,{...b,headers:x,body:v,method:h}),w=await C.text();if(!C.ok||!C.headers.get(`content-type`)?.toLowerCase().includes(`xml`)||!a||!w){let e=4096,t=w.length>e?`${w.slice(0,e)}…`:w;return[{href:C.url,ok:C.ok,status:C.status,statusText:C.statusText,raw:t}]}let T;try{T=t.xml2js(w,{compact:!0,trim:!0,textFn:(e,t)=>{try{let n=t._parent,r=Object.keys(n),i=r[r.length-1];if(!i)return;let a=n[i];if(a.length>0){let t=a,n=a.length-1;t[n]=d(e)}else n[i]=d(e)}catch(e){ee(e.stack)}},elementNameFn:e=>c(e.replace(/^.+:/,``)),attributesFn:e=>{let t={...e};return delete t.xmlns,t},ignoreDeclaration:!0})}catch(e){return ee(`Failed to parse DAV response XML: ${e.message}`),[{href:C.url,ok:C.ok,status:C.status,statusText:C.statusText,raw:w}]}return T?.multistatus?(Array.isArray(T.multistatus.response)?T.multistatus.response:[T.multistatus.response]).map(e=>{if(!e)return{raw:T,status:C.status,statusText:C.statusText,ok:C.ok};let t=te(e.status),n=t?.status??C.status;return{raw:T,href:e.href,status:n,statusText:t?.statusText??C.statusText,ok:n>=200&&n<300,error:e.error,responsedescription:e.responsedescription,props:(Array.isArray(e.propstat)?e.propstat:[e.propstat]).reduce((e,t)=>{let n=te(t?.status)?.status;return n&&(n<200||n>=300)?e:{...e,...t?.prop}},{})}}):[{href:C.url,ok:C.ok,status:C.status,statusText:C.statusText,raw:T}]},T=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return w({url:t,init:{method:`PROPFIND`,headers:x(y({depth:r,...i}),a),namespace:`d`,body:{propfind:{_attributes:v([`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`,`http://calendarserver.org/ns/`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),prop:n}}},fetchOptions:o,fetch:s})},E=async e=>{let{url:t,data:n,headers:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??l,{headers:c,...u}=a;return s(t,{...u,method:`PUT`,body:n,headers:x(S(r,c),i)})},D=async e=>{let{url:t,data:n,etag:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c=s??l,{headers:u,...d}=o;return c(t,{...d,method:`PUT`,body:n,headers:x(S(y({"If-Match":r,...i}),u),a)})},O=async e=>{let{url:t,headers:n,etag:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??l,{headers:c,...u}=a;return s(t,{...u,method:`DELETE`,headers:x(S(y({"If-Match":r,...n}),c),i)})};function k(e,t){if(!e)return!1;let n=e=>e!=null&&t.every(t=>e[t]);return Array.isArray(e)?e.length>0&&e.every(e=>n(e)):n(e)}const A=(e,t)=>!e||typeof e!=`object`?t.map(e=>e.toString()).join(`,`):t.reduce((t,n)=>e[n]?t:`${t.length?`${t},`:``}${n.toString()}`,``);var ne=r({collectionQuery:()=>j,isCollectionDirty:()=>N,makeCollection:()=>oe,smartCollectionSync:()=>F,smartCollectionSyncDetailed:()=>se,supportedReportSet:()=>M,syncCollection:()=>P});const re=e(`tsdav:collection`),ie=(e,t)=>{try{return new URL(e,m(t)).href}catch{return e}},ae=(e,t,n)=>{try{return new URL(e,m(n)).pathname.toLowerCase().endsWith(t)}catch{return(e.split(/[?#]/,1)[0]??``).toLowerCase().endsWith(t)}},j=async e=>{let{url:t,body:n,depth:r,defaultNamespace:i=`d`,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e,l=await w({url:t,init:{method:`REPORT`,headers:x(y({depth:r,...a}),o),namespace:i,body:n},fetchOptions:s,fetch:c}),u=l[0];if(i===`c`&&n?.[`calendar-query`]!=null&&l.length===1&&u&&u.status===404&&_(t,u.href,t)&&!u.error&&Object.keys(u.props??{}).length===0&&typeof u.raw==`object`&&u.raw!==null&&u.raw.multistatus?.response?.propstat==null)return[];let d=l.find(e=>!e.ok||e.status&&e.status>=400);if(d)throw Error(`Collection query failed: ${d.status} ${d.statusText}. ${d.raw?`Raw response: ${d.raw}`:``}`);let f=l[0];return l.length===1&&f&&(!f.raw||f.raw.multistatus&&!f.raw.multistatus.response)&&f.status&&f.status<300?[]:l},oe=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return w({url:t,init:{method:`MKCOL`,headers:x(y({depth:r,...i}),a),namespace:`d`,body:n?{mkcol:{_attributes:v([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`]),set:{prop:n}}}:void 0},fetchOptions:o,fetch:s})},M=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await T({url:t.url,props:{"d:supported-report-set":{}},depth:`0`,headers:x(n,r),fetchOptions:i,fetch:a}))[0]?.props?.supportedReportSet?.supportedReport;return o?(Array.isArray(o)?o:[o]).map(e=>e?.report?Object.keys(e.report)[0]:void 0).filter(e=>typeof e==`string`&&e.length>0):[]},N=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await T({url:t.url,props:{"cs:getctag":{}},depth:`0`,headers:x(n,r),fetchOptions:i,fetch:a})).find(e=>_(t.url,e.href,t.url));if(!o)throw Error(`Collection does not exist on server`);if(!o.ok)throw Error(`Collection status check failed: ${o.status} ${o.statusText}`);let s=o.props?.getctag;return{isDirty:t.ctag==null||s==null||`${t.ctag}`!=`${s}`,newCtag:s?.toString()}},P=e=>{let{url:t,props:n,headers:r,syncLevel:i,syncToken:a,headersToExclude:o,fetchOptions:s,fetch:c}=e;return w({url:t,init:{method:`REPORT`,namespace:`d`,headers:x({...r},o),body:{"sync-collection":{_attributes:v([`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"sync-level":i,"sync-token":a,"d:prop":n}}},fetchOptions:s,fetch:c})},F=async e=>{let{collection:t,method:n,headers:r,headersToExclude:i,account:a,detailedResult:o,fetchOptions:s={},fetch:c}=e,l=[`accountType`,`homeUrl`];if(!a||!k(a,l))throw Error(a?`account must have ${A(a,l)} before smartCollectionSync`:`no account for smartCollectionSync`);let u=n??(t.reports?.includes(`syncCollection`)?`webdav`:`basic`);if(re(`smart collection sync with type ${a.accountType} and method ${u}`),u===`webdav`){let e=await P({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{},"d:displayname":{}},syncLevel:1,syncToken:t.syncToken,headers:x(r,i),fetchOptions:s,fetch:c}),n=e=>{let n=a.accountType===`caldav`?`.ics`:`.vcf`;return typeof e.href==`string`&&ae(e.href,n,t.url)},l=e.find(e=>(!e.ok||e.status>=400)&&!(e.status===404&&n(e)));if(l)throw Error(`Collection sync failed: ${l.status} ${l.statusText}`);let u=e.filter(n),d=u.filter(e=>e.status!==404).map(e=>e.href),f=u.filter(e=>e.status===404).map(e=>e.href),p=t.objectMultiGet;if(d.length>0&&!p)throw Error(`collection.objectMultiGet is required for webdav sync changes`);let m=d.length?await p?.({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{}},objectUrls:d,depth:`1`,headers:x(r,i),fetchOptions:s,fetch:c})??[]:[],h=m.find(e=>!e.ok||e.status>=400);if(h)throw Error(`Collection sync multi-get failed: ${h.status} ${h.statusText}`);let g=m.map(e=>({url:ie(e.href??``,t.url),etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:a?.accountType===`caldav`?e.props?.calendarData?._cdata??e.props?.calendarData:e.props?.addressData?._cdata??e.props?.addressData})),v=t.objects??[],y=g.filter(e=>v.every(n=>!_(n.url,e.url,t.url))),b=v.reduce((e,n)=>{let r=g.find(e=>_(e.url,n.url,t.url));return r&&r.etag&&r.etag!==n.etag?[...e,r]:e},[]),S=f.map(e=>({url:ie(e,t.url),etag:``})),C=v.filter(e=>S.every(n=>!_(e.url,n.url,t.url))&&b.every(n=>!_(e.url,n.url,t.url)));return{...t,objects:o?{created:y,updated:b,deleted:S}:[...C,...y,...b],syncToken:e[0]?.raw?.multistatus?.syncToken??t.syncToken}}if(u===`basic`){let{isDirty:e,newCtag:n}=await N({collection:t,headers:x(r,i),fetchOptions:s,fetch:c});if(!e)return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t;let a=t.objects??[];if(!t.fetchObjects)throw Error(`collection.fetchObjects is required for basic sync changes`);let l=await t.fetchObjects({collection:t,headers:x(r,i),fetchOptions:s,fetch:c})??[],u=l.filter(e=>a.every(n=>!_(n.url,e.url,t.url))),d=a.reduce((e,n)=>{let r=l.find(e=>_(e.url,n.url,t.url));return r&&r.etag&&r.etag!==n.etag?[...e,r]:e},[]),f=a.filter(e=>l.every(n=>!_(n.url,e.url,t.url))),p=a.filter(e=>l.some(n=>_(e.url,n.url,t.url)&&n.etag===e.etag));return{...t,objects:o?{created:u,updated:d,deleted:f}:[...p,...u,...d],ctag:n}}return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t},se=async e=>F({...e,detailedResult:!0});var ce=r({addressBookMultiGet:()=>L,addressBookQuery:()=>I,createVCard:()=>ue,deleteVCard:()=>de,fetchAddressBooks:()=>R,fetchVCards:()=>z,updateVCard:()=>B});const le=e(`tsdav:addressBook`),I=async e=>{let{url:t,props:n,filters:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return j({url:t,body:{"addressbook-query":y({_attributes:v([`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"d:prop":n,filter:r??{"prop-filter":{_attributes:{name:`FN`}}}})},defaultNamespace:`card`,depth:i,headers:x(a,o),fetchOptions:s,fetch:c})},L=async e=>{let{url:t,props:n,objectUrls:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return j({url:t,body:{"addressbook-multiget":y({_attributes:v([`DAV:`,`urn:ietf:params:xml:ns:carddav`]),"d:prop":n,"d:href":r})},defaultNamespace:`card`,depth:i,headers:x(a,o),fetchOptions:s,fetch:c})},R=async e=>{let{account:t,headers:n,props:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e??{},s=[`homeUrl`,`rootUrl`];if(!t||!k(t,s))throw Error(t?`account must have ${A(t,s)} before fetchAddressBooks`:`no account for fetchAddressBooks`);let c=await T({url:t.homeUrl,props:r??{"d:displayname":{},"cs:getctag":{},"d:resourcetype":{},"d:sync-token":{}},depth:`1`,headers:x(n,i),fetchOptions:a,fetch:o});return Promise.all(c.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`addressbook`)).map(e=>{let n=e.props?.displayname?._cdata??e.props?.displayname;return le(`Found address book named ${typeof n==`string`?n:``}, - props: ${JSON.stringify(e.props)}`),{url:new URL(e.href??``,m(t.rootUrl??``)).href,ctag:e.props?.getctag,displayName:typeof n==`string`?n:``,resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:e.props?.syncToken}}).map(async e=>({...e,reports:await M({collection:e,headers:x(n,i),fetchOptions:a,fetch:o})})))},z=async e=>{let{addressBook:t,headers:n,objectUrls:r,headersToExclude:i,urlFilter:a=e=>!!e,useMultiGet:o=!0,fetchOptions:s={},fetch:c}=e;le(`Fetching vcards from ${t?.url}`);let l=[`url`];if(!t||!k(t,l))throw Error(t?`addressBook must have ${A(t,l)} before fetchVCards`:`cannot fetchVCards for undefined addressBook`);let u=(r??(await I({url:t.url,props:{"d:getetag":{}},depth:`1`,headers:x(n,i),fetchOptions:s,fetch:c})).map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,m(t.url)).href).filter(e=>!h(e,t.url)).filter(a).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),d=[];return u.length>0&&(d=o?await L({url:t.url,props:{"d:getetag":{},"card:address-data":{}},objectUrls:u,depth:`1`,headers:x(n,i),fetchOptions:s,fetch:c}):await I({url:t.url,props:{"d:getetag":{},"card:address-data":{}},depth:`1`,headers:x(n,i),fetchOptions:s,fetch:c})),d.map(e=>({url:new URL(e.href??``,m(t.url)).href,etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:e.props?.addressData?._cdata??e.props?.addressData}))},ue=async e=>{let{addressBook:t,vCardString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return E({url:new URL(r,m(t.url)).href,data:n,headers:x({"content-type":`text/vcard; charset=utf-8`,"If-None-Match":`*`,...i},a),fetchOptions:o,fetch:s})},B=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return D({url:t.url,data:t.data,etag:t.etag,headers:x({"content-type":`text/vcard; charset=utf-8`,...n},r),fetchOptions:i,fetch:a})},de=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return O({url:t.url,etag:t.etag,headers:x(n,r),fetchOptions:i,fetch:a})};var fe=r({calendarMultiGet:()=>W,calendarQuery:()=>U,createCalendarObject:()=>ve,deleteCalendarObject:()=>be,fetchCalendarObjects:()=>K,fetchCalendarUserAddresses:()=>H,fetchCalendars:()=>G,freeBusyQuery:()=>Se,makeCalendar:()=>_e,syncCalendars:()=>q,syncCalendarsDetailed:()=>xe,updateCalendarObject:()=>ye});const V=e(`tsdav:calendar`),pe=/^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i,me=/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i,he=e=>{let{start:t,end:n}=e;if(!(pe.test(t)&&pe.test(n)||me.test(t)&&me.test(n)))throw Error(`invalid timeRange format, not in ISO8601`);if(Number.isNaN(new Date(t).getTime())||Number.isNaN(new Date(n).getTime()))throw Error(`invalid timeRange: start or end is not a valid date`);if(new Date(t).getTime()>=new Date(n).getTime())throw Error(`invalid timeRange: start must be before end`)},ge=e=>{let t=[];return Array.isArray(e)?t=e.map(e=>e?._attributes?.name):e&&typeof e==`object`&&(t=[e._attributes?.name]),t.filter(e=>typeof e==`string`&&e.length>0)},H=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!k(t,o))throw Error(`account must have ${A(t,o)} before fetchUserAddresses`);V(`Fetch user addresses from ${t.principalUrl}`);let s=(await T({url:t.principalUrl,props:{"c:calendar-user-address-set":{}},depth:`0`,headers:x(n,r),fetchOptions:i,fetch:a})).find(e=>_(t.principalUrl,e.href,t.rootUrl));if(!s||!s.ok)throw Error(`cannot find calendarUserAddresses`);let c=s?.props?.calendarUserAddressSet?.href,l=[];Array.isArray(c)?l=c:c&&(l=[c]);let u=l.filter(e=>typeof e==`string`&&e.length>0);return V(`Fetched calendar user addresses ${u}`),u},U=async e=>{let{url:t,props:n,filters:r,timezone:i,depth:a,headers:o,headersToExclude:s,fetchOptions:c={},fetch:l}=e;return j({url:t,body:{"calendar-query":y({_attributes:v([`urn:ietf:params:xml:ns:caldav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`,`DAV:`]),"d:prop":n,filter:r,timezone:i})},defaultNamespace:`c`,depth:a,headers:x(o,s),fetchOptions:c,fetch:l})},W=async e=>{let{url:t,props:n,objectUrls:r,filters:i,timezone:a,depth:o,headers:s,headersToExclude:c,fetchOptions:l={},fetch:u}=e;return j({url:t,body:{"calendar-multiget":y({_attributes:v([`DAV:`,`urn:ietf:params:xml:ns:caldav`]),"d:prop":n,"d:href":r,filter:i,timezone:a})},defaultNamespace:`c`,depth:o,headers:x(s,c),fetchOptions:l,fetch:u})},_e=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return w({url:t,init:{method:`MKCALENDAR`,headers:x(y({depth:r,...i}),a),namespace:`d`,body:{"c:mkcalendar":{_attributes:v([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`]),set:{prop:n}}}},fetchOptions:o,fetch:s})},G=async e=>{let{headers:t,account:n,props:r,projectedProps:i,headersToExclude:a,fetchOptions:o={},fetch:c}=e??{},l=[`homeUrl`,`rootUrl`];if(!n||!k(n,l))throw Error(n?`account must have ${A(n,l)} before fetchCalendars`:`no account for fetchCalendars`);let u=await T({url:n.homeUrl,props:r??{"c:calendar-description":{},"c:calendar-timezone":{},"d:displayname":{},"ca:calendar-color":{},"cs:getctag":{},"d:resourcetype":{},"c:supported-calendar-component-set":{},"d:sync-token":{}},depth:`1`,headers:x(t,a),fetchOptions:o,fetch:c});return Promise.all(u.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`calendar`)).filter(e=>{let t=ge(e.props?.supportedCalendarComponentSet?.comp);return t.length===0||t.some(e=>Object.values(s).includes(e))}).map(e=>{let t=e.props?.calendarDescription,r=e.props?.calendarTimezone,a=e.props?.supportedCalendarComponentSet?.comp,o=Object.entries(e.props??{}).filter(([e])=>i?.[e]);return{description:typeof t==`string`?t:``,timezone:typeof r==`string`?r:``,url:new URL(e.href??``,m(n.rootUrl??``)).href,ctag:e.props?.getctag,calendarColor:e.props?.calendarColor,displayName:e.props?.displayname?._cdata??e.props?.displayname,components:ge(a),resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:e.props?.syncToken,...i&&o.length>0?{projectedProps:Object.fromEntries(o)}:{}}}).map(async e=>({...e,reports:await M({collection:e,headers:x(t,a),fetchOptions:o,fetch:c})})))},K=async e=>{let{calendar:t,objectUrls:n,filters:r,timeRange:i,headers:a,expand:o,urlFilter:s=e=>!!e?.includes(`.ics`),useMultiGet:c=!0,headersToExclude:l,fetchOptions:u={},fetch:d}=e;i&&he(i),V(`Fetching calendar objects from ${t?.url}`);let f=[`url`];if(!t||!k(t,f))throw Error(t?`calendar must have ${A(t,f)} before fetchCalendarObjects`:`cannot fetchCalendarObjects for undefined calendar`);let p=r??[{"comp-filter":{_attributes:{name:`VCALENDAR`},"comp-filter":{_attributes:{name:`VEVENT`},...i?{"time-range":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}}}],h=[];n||(h=await U({url:t.url,props:{"d:getetag":{},...o&&i?{"c:calendar-data":{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}}:{}},filters:p,depth:`1`,headers:x(a,l),fetchOptions:u,fetch:d}));let g=(n??h.map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,m(t.url)).href).filter(s).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),v=[];return g.length>0&&(o&&!n?v=h.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href:new URL(e.href??``,m(t.url)).href;return s(n??``)}):c?v=await W({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},objectUrls:g,depth:`1`,headers:x(a,l),fetchOptions:u,fetch:d}):(v=await U({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},filters:p,depth:`1`,headers:x(a,l),fetchOptions:u,fetch:d}),n&&n.length>0&&(v=v.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href??``:new URL(e.href??``,m(t.url)).href;return g.some(e=>_(n,e,t.url))})))),v.map(e=>({url:new URL(e.href??``,m(t.url)).href,etag:e.props?.getetag==null?void 0:String(e.props.getetag),data:e.props?.calendarData?._cdata??e.props?.calendarData}))},ve=async e=>{let{calendar:t,iCalString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return E({url:new URL(r,m(t.url)).href,data:n,headers:x({"content-type":`text/calendar; charset=utf-8`,"If-None-Match":`*`,...i},a),fetchOptions:o,fetch:s})},ye=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return D({url:t.url,data:t.data,etag:t.etag,headers:x({"content-type":`text/calendar; charset=utf-8`,...n},r),fetchOptions:i,fetch:a})},be=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return O({url:t.url,etag:t.etag,headers:x(n,r),fetchOptions:i,fetch:a})},q=async e=>{let{oldCalendars:t,account:n,detailedResult:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`Must have account before syncCalendars`);let c=t??n.calendars??[],l=await G({account:n,headers:x(i,a),fetchOptions:o,fetch:s}),u=l.filter(e=>c.every(t=>!_(t.url,e.url,n.rootUrl)));V(`new calendars: ${u.map(e=>e.displayName)}`);let d=c.reduce((e,t)=>{let r=l.find(e=>_(e.url,t.url,n.rootUrl));return r&&(r.syncToken&&`${r.syncToken}`!=`${t.syncToken}`||r.ctag&&`${r.ctag}`!=`${t.ctag}`)?[...e,{local:t,remote:r}]:e},[]);V(`updated calendars: ${d.map(({remote:e})=>e.displayName)}`);let f=await Promise.all(d.map(async({local:e,remote:t})=>{let r=async e=>{if(!e)return[];let{collection:t,...n}=e;return K({...n,calendar:t})},c={...t,ctag:e.ctag,syncToken:e.syncToken,objects:e.objects,objectMultiGet:W,fetchObjects:r},l=await F({collection:c,detailedResult:!1,headers:x(i,a),account:n,fetchOptions:o,fetch:s});return{...l,ctag:t.ctag??l.ctag,syncToken:t.syncToken??l.syncToken}})),p=c.filter(e=>l.every(t=>!_(t.url,e.url,n.rootUrl)));V(`deleted calendars: ${p.map(e=>e.displayName)}`);let m=c.filter(e=>l.some(t=>{if(!_(t.url,e.url,n.rootUrl))return!1;let r=!t.syncToken||`${t.syncToken}`==`${e.syncToken}`,i=!t.ctag||`${t.ctag}`==`${e.ctag}`;return r&&i}));return V(`unchanged calendars: ${m.map(e=>e.displayName)}`),r?{created:u,updated:f,deleted:p}:[...m,...u,...f]},xe=async e=>q({...e,detailedResult:!0}),Se=async e=>{let{url:t,timeRange:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`timeRange is required`);he(n);let c=(await j({url:t,body:{"free-busy-query":y({_attributes:v([`urn:ietf:params:xml:ns:caldav`]),"c:time-range":{_attributes:{start:`${new Date(n.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(n.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}})},defaultNamespace:`c`,depth:r,headers:x(i,a),fetchOptions:o,fetch:s}))[0];if(!c)throw Error(`freeBusyQuery returned no response`);return c};var Ce=r({createAccount:()=>Y,fetchHomeUrl:()=>Oe,fetchPrincipalUrl:()=>De,serviceDiscovery:()=>Te});const J=e(`tsdav:account`),we=(e,t)=>{let n=[t,e,new URL(`/`,e).href];return n.filter((e,t)=>n.indexOf(e)===t)},Te=async e=>{J(`Service discovery...`);let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=a??l,s=new URL(t.serverUrl),{headers:c,...u}=i,d=new URL(`/.well-known/${t.accountType}`,s);d.protocol=s.protocol??`http`;let f=e=>{if(e.status>=300&&e.status<400){let t=e.headers.get(`Location`);if(typeof t==`string`&&t.length){J(`Service discovery redirected to ${t}`);let e=/^[a-z][a-z0-9+.-]*:/i.test(t),n=new URL(t,s);return n.hostname===d.hostname&&d.port&&!n.port&&(n.port=d.port),e||(n.protocol=s.protocol??`http`),n.href}}};try{let e=f(await o(d.href,{...u,method:`PROPFIND`,headers:x(S({"Content-Type":`text/xml;charset=UTF-8`},n,c),r),body:` +import e from"debug";import t from"xml-js";var n=Object.defineProperty,r=(e,t)=>{let r={};for(var i in e)n(r,i,{get:e[i],enumerable:!0});return t||n(r,Symbol.toStringTag,{value:`Module`}),r};let i=function(e){return e.CALENDAR_SERVER=`http://calendarserver.org/ns/`,e.CALDAV_APPLE=`http://apple.com/ns/ical/`,e.CALDAV=`urn:ietf:params:xml:ns:caldav`,e.CARDDAV=`urn:ietf:params:xml:ns:carddav`,e.DAV=`DAV:`,e}({});const a={"urn:ietf:params:xml:ns:caldav":`xmlns:c`,"urn:ietf:params:xml:ns:carddav":`xmlns:card`,"http://calendarserver.org/ns/":`xmlns:cs`,"http://apple.com/ns/ical/":`xmlns:ca`,"DAV:":`xmlns:d`};let o=function(e){return e.CALDAV=`c`,e.CARDDAV=`card`,e.CALENDAR_SERVER=`cs`,e.CALDAV_APPLE=`ca`,e.DAV=`d`,e}({}),s=function(e){return e.VEVENT=`VEVENT`,e.VTODO=`VTODO`,e.VJOURNAL=`VJOURNAL`,e.VFREEBUSY=`VFREEBUSY`,e.VTIMEZONE=`VTIMEZONE`,e.VALARM=`VALARM`,e}({});const c=typeof globalThis<`u`&&typeof globalThis.fetch==`function`?globalThis.fetch.bind(globalThis):(()=>{throw Error("tsdav: global fetch is not available in this runtime. Upgrade to Node.js >= 18, run under a browser/Bun/Deno, or install a fetch polyfill on globalThis before importing tsdav. You can also pass a custom `fetch` implementation to `createDAVClient`, `DAVClient`, or individual request helpers.")}),l=e=>e.replace(/[-_]+(\w?)/g,(e,t)=>t?t.toUpperCase():``);function u(e,t){if(!e)return!1;let n=e=>e!=null&&t.every(t=>e[t]);return Array.isArray(e)?e.length>0&&e.every(e=>n(e)):n(e)}const d=(e,t)=>!e||typeof e!=`object`?t.map(e=>e.toString()).join(`,`):t.reduce((t,n)=>e[n]?t:`${t.length?`${t},`:``}${n.toString()}`,``),f=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),p=new Set(Object.values(i)),m=new Map([...[`multistatus`,`response`,`propstat`,`prop`,`href`,`status`,`error`,`responsedescription`,`resourcetype`,`collection`,`getetag`,`displayname`,`syncToken`,`supportedReportSet`,`supportedReport`,`report`,`currentUserPrincipal`].map(e=>[e,`DAV:`]),...[`calendarData`,`calendar`,`calendarDescription`,`calendarTimezone`,`calendarHomeSet`,`calendarUserAddressSet`,`supportedCalendarComponentSet`,`comp`].map(e=>[e,`urn:ietf:params:xml:ns:caldav`]),...[`addressData`,`addressbook`,`addressbookHomeSet`].map(e=>[e,`urn:ietf:params:xml:ns:carddav`]),[`getctag`,`http://calendarserver.org/ns/`],[`calendarColor`,`http://apple.com/ns/ical/`]]),h=(e,t)=>{let n=l(e.replace(/^.*:/,``)),r=m.get(n);return t&&r&&t!==r?`{${t}}${n}`:n},g=(e,t)=>{let n=Object.assign(Object.create(null),t),r=Object.create(null);for(let[t,i]of Object.entries(e.attributes??{}))(t===`xmlns`||t.startsWith(`xmlns:`))&&(n[t===`xmlns`?``:t.slice(6)]=String(i)),t!==`xmlns`&&(r[t]=i);let i=e.name??``,a=i.indexOf(`:`),o=n[a===-1?``:i.slice(0,a)]??``,s=e.elements??[],c=s.filter(e=>e.type===`element`),u=s.filter(e=>e.type===`text`||e.type===`cdata`).map(e=>String(e.text??e.cdata??``)).join(``),d={},m=Object.create(null);if(Object.keys(r).length&&(d._attributes=r),!c.length){let e=s.some(e=>e.type===`text`),t=s.some(e=>e.type===`cdata`);return e?{value:u,namespace:o,namespaces:m}:(t&&(d._cdata=u),{value:d,namespace:o,namespaces:m})}u.trim()&&(d._text=u);for(let e of c){let t=g(e,n),r=l((e.name??``).replace(/^.*:/,``)),i=h(e.name??``,t.namespace),a=m[i];if(f(d,i)&&a!==t.namespace){if(p.has(t.namespace)&&!p.has(a)){let e=`{${a}}${r}`;Object.defineProperty(d,e,{value:d[i],enumerable:!0,configurable:!0,writable:!0}),m[e]=a,delete d[i]}else i=`{${t.namespace}}${r}`}f(d,i)?Array.isArray(d[i])?d[i].push(t.value):d[i]=[d[i],t.value]:Object.defineProperty(d,i,{value:t.value,enumerable:!0,configurable:!0,writable:!0}),m[i]=t.namespace,r===`prop`&&t.namespace===`DAV:`&&(d.propNamespaces=t.namespaces)}return{value:d,namespace:o,namespaces:m}},_=e=>{let n=t.xml2js(e,{compact:!1,ignoreDeclaration:!0}),r={};for(let e of n.elements??[]){if(e.type!==`element`)continue;let t=g(e,{}),n=h(e.name??``,t.namespace);Object.defineProperty(r,n,{value:t.value,enumerable:!0})}return r},v=e=>{let t=new Map;for(let n of e)if(n.ok)for(let[e,r]of Object.entries(n.props)){let i=t.get(e)??new Map;i.set(n.namespaces?.[e]??``,r),t.set(e,i)}let n={};for(let[e,r]of t){let t=[...r.keys()].find(e=>p.has(e))??r.keys().next().value;for(let[i,a]of r)Object.defineProperty(n,i===t?e:`{${i}}${e}`,{value:a,enumerable:!0,configurable:!0,writable:!0})}return n};var y=r({cleanupFalsy:()=>E,conditionalParam:()=>ee,ensureTrailingSlash:()=>x,excludeHeaders:()=>D,getDAVAttribute:()=>T,mergeHeaders:()=>O,urlContains:()=>C,urlEquals:()=>S,urlMatches:()=>w});const b=e=>{let t=e.trim();return t.endsWith(`/`)?t.slice(0,-1):t},x=e=>{let t=e.trim(),n=t.search(/[?#]/),r=n===-1?t:t.slice(0,n),i=n===-1?``:t.slice(n);return`${r.endsWith(`/`)?r:`${r}/`}${i}`},S=(e,t)=>!e&&!t?!0:!e||!t?!1:b(e)===b(t),C=(e,t)=>{if(!e&&!t)return!0;if(!e||!t)return!1;let n=b(e),r=b(t);return n.includes(r)||r.includes(n)},w=(e,t,n)=>{if(!e||!t||!n)return S(e,t);try{return S(new URL(e,n).href,new URL(t,n).href)}catch{return S(e,t)}},T=e=>e.reduce((e,t)=>({...e,[a[t]]:t}),{}),E=e=>Object.entries(e).reduce((e,[t,n])=>n?{...e,[t]:n}:e,{}),ee=(e,t)=>t?{[e]:t}:{},D=(e,t)=>{if(!e)return{};if(!t||t.length===0)return e;let n=new Set(t.map(e=>e.toLowerCase()));return Object.fromEntries(Object.entries(e).filter(([e])=>!n.has(e.toLowerCase())))},O=(...e)=>{let t=new Map,n=(e,n)=>{t.set(e.toLowerCase(),[e,n])};for(let t of e)if(t){if(Array.isArray(t)){for(let[e,r]of t)n(e,r);continue}if(typeof t.forEach==`function`){t.forEach((e,t)=>{n(t,e)});continue}for(let[e,r]of Object.entries(t))n(e,r)}return Object.fromEntries(t.values())};var te=r({createObject:()=>j,davRequest:()=>k,deleteObject:()=>N,propfind:()=>A,updateObject:()=>M});const ne=e(`tsdav:request`),re=e=>{let t=/^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(e?.trim()??``),n=t?.groups?.status,r=t?.groups?.statusText;return n?{status:Number.parseInt(n,10),statusText:r??``}:void 0},k=async e=>{let{url:n,init:r,convertIncoming:i=!0,parseOutgoing:a=!0,headersToExclude:o,fetchOptions:s={},fetch:l}=e,u=l??c,{headers:d={},body:f,namespace:p,method:m,attributes:h}=r,g=f;h&&typeof f==`object`&&f&&!Array.isArray(f)&&(g=Object.fromEntries(Object.entries(f).map(([e,t])=>{if(t&&typeof t==`object`&&!Array.isArray(t)){let n=t;return[e,{...n,_attributes:{...h,...n._attributes}}]}return[e,t]})));let y=i&&f!=null?t.js2xml({_declaration:{_attributes:{version:`1.0`,encoding:`utf-8`}},...g},{compact:!0,spaces:2,elementNameFn:e=>p&&!/^.+:.+/.test(e)?`${p}:${e}`:e}):f,b={...s};delete b.headers;let x=D(O({"Content-Type":`text/xml;charset=UTF-8`},E(d),s.headers),o),S=await u(n,{...b,headers:x,body:y,method:m}),C=await S.text();if(!S.ok||!S.headers.get(`content-type`)?.toLowerCase().includes(`xml`)||!a||!C)return[{href:S.url,ok:S.ok,status:S.status,statusText:S.statusText,raw:C}];let w;try{w=_(C)}catch(e){return ne(`Failed to parse DAV response XML: ${e.message}`),[{href:S.url,ok:!1,status:S.status,statusText:S.statusText,raw:C,parseError:e.message}]}return w?.multistatus?(Array.isArray(w.multistatus.response)?w.multistatus.response:[w.multistatus.response]).map(e=>{if(!e)return{raw:w,status:S.status,statusText:S.statusText,ok:S.ok};let t=(Array.isArray(e.propstat)?e.propstat:e.propstat?[e.propstat]:[]).map(e=>{let t=re(e.status),n=t?.status??0;return{props:e.prop??{},namespaces:e.propNamespaces,status:n,statusText:t?.statusText??`Invalid DAV property status`,ok:n>=200&&n<300,error:e.error,responsedescription:e.responsedescription}}),n=t.length>0&&t.every(e=>!e.ok)?t[0]:void 0,r=re(e.status)??n,i=r?.status??S.status;return{raw:w,href:e.href,status:i,statusText:r?.statusText??S.statusText,ok:i>=200&&i<300,error:e.error,responsedescription:e.responsedescription,propStats:t,props:v(t)}}):[{href:S.url,ok:S.ok,status:S.status,statusText:S.statusText,raw:w}]},A=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return k({url:t,init:{method:`PROPFIND`,headers:D(E({depth:r,...i}),a),namespace:`d`,body:{propfind:{_attributes:T([`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`,`http://calendarserver.org/ns/`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),prop:n}}},headersToExclude:a,fetchOptions:o,fetch:s})},j=async e=>{let{url:t,data:n,headers:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??c,{headers:l,...u}=a;return s(t,{...u,method:`PUT`,body:n,headers:D(O(r,l),i)})},M=async e=>{let{url:t,data:n,etag:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e,l=s??c,{headers:u,...d}=o;return l(t,{...d,method:`PUT`,body:n,headers:D(O(E({"If-Match":r,...i}),u),a)})},N=async e=>{let{url:t,headers:n,etag:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e,s=o??c,{headers:l,...u}=a;return s(t,{...u,method:`DELETE`,headers:D(O(E({"If-Match":r,...n}),l),i)})},P=(e,t)=>{let n;try{n=new URL(e,x(t)).href}catch{n=e.trim()}return n.endsWith(`/`)?n.slice(0,-1):n},ie=(e,t,n,r=!1,i=[])=>{let a=new Map(e.map(e=>[P(e.url,n),e])),o=new Map(t.map(e=>[P(e.url,n),e])),s=r?i:e.filter(e=>!o.has(P(e.url,n))),c=new Set(s.map(e=>P(e.url,n))),l=t.filter(e=>!a.has(P(e.url,n))),u=[],d=[];for(let t of e){let e=P(t.url,n);if(c.has(e))continue;let i=o.get(e);i&&i.etag!==t.etag?u.push(i):(i||r)&&d.push(t)}return{created:l,updated:u,deleted:s,unchanged:d}},ae=(e,t)=>{let n=e.find(e=>!e.ok||e.status>=400);if(n)throw Error(`${t}: ${n.status} ${n.statusText}`)},oe=(e,t)=>{ae(e,t);for(let n of e)if((!n.raw?.multistatus||n.raw.multistatus.response)&&(se(n,`resourcetype`,t),!n.props||!f(n.props,`resourcetype`)||typeof n.href!=`string`||!n.href))throw Error(`${t}: missing resourcetype or href in DAV multistatus response`)},se=(e,t,n)=>{let r=e.propStats?.find(e=>!e.ok&&f(e.props,t));if(r&&!f(e.props??{},t))throw Error(`${n}: ${t} returned ${r.status} ${r.statusText}`)},ce=(e,t,n,r,i)=>{ae(e,i);let a=new Set;for(let n of e){if(se(n,t,i),!n.href||typeof(n.props?.[t]?._cdata??n.props?.[t])!=`string`)throw Error(`${i}: missing ${t} or href`);a.add(P(n.href,r))}let o=e=>P(e.replace(/\?.*$/,``),r),s=new Map;for(let e of n){let t=o(e);s.set(t,(s.get(t)??0)+1)}if(n.some(e=>!a.has(P(e,r))&&!(s.get(o(e))===1&&a.has(o(e)))))throw Error(`${i}: incomplete response`)},F=e=>{let t=typeof e==`string`||typeof e==`number`?String(e):e?._cdata??e?._text;return typeof t==`string`?t:void 0};var le=r({collectionQuery:()=>I,isCollectionDirty:()=>R,makeCollection:()=>fe,smartCollectionSync:()=>B,smartCollectionSyncDetailed:()=>pe,supportedReportSet:()=>L,syncCollection:()=>z});const ue=e(`tsdav:collection`),de=(e,t)=>{try{return new URL(e,x(t)).href}catch{return e}},I=async e=>{let{url:t,body:n,depth:r,defaultNamespace:i=`d`,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e,l=await k({url:t,init:{method:`REPORT`,headers:D(E({depth:r,...a}),o),namespace:i,body:n},headersToExclude:o,fetchOptions:s,fetch:c}),u=l[0];if(i===`c`&&n?.[`calendar-query`]!=null&&l.length===1&&u&&u.status===404&&w(t,u.href,t)&&!u.error&&Object.keys(u.props??{}).length===0&&typeof u.raw==`object`&&u.raw!==null&&u.raw.multistatus?.response?.propstat==null)return[];let d=l.find(e=>!e.ok||e.status&&e.status>=400);if(d)throw Error(`Collection query failed: ${d.status} ${d.statusText}. ${typeof d.raw==`string`?`Raw response: ${d.raw.slice(0,4096)}`:``}`);if((n?.[`calendar-query`]||n?.[`calendar-multiget`]||n?.[`addressbook-query`]||n?.[`addressbook-multiget`])&&l.some(e=>!e.raw?.multistatus))throw Error(`Collection query failed: expected a DAV multistatus response`);if((n?.[`calendar-query`]||n?.[`calendar-multiget`]||n?.[`addressbook-query`]||n?.[`addressbook-multiget`])&&l.some(e=>e.raw?.multistatus?.response&&(typeof e.href!=`string`||!e.href)))throw Error(`Collection query failed: missing href in DAV response`);let f=l[0];return l.length===1&&f&&(!f.raw||f.raw.multistatus&&!f.raw.multistatus.response)&&f.status&&f.status<300?[]:l},fe=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return k({url:t,init:{method:`MKCOL`,headers:D(E({depth:r,...i}),a),namespace:`d`,body:n?{mkcol:{_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`]),set:{prop:n}}}:void 0},headersToExclude:a,fetchOptions:o,fetch:s})},L=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await A({url:t.url,props:{"d:supported-report-set":{}},depth:`0`,headers:D(n,r),headersToExclude:r,fetchOptions:i,fetch:a}))[0]?.props?.supportedReportSet?.supportedReport;return o?(Array.isArray(o)?o:[o]).map(e=>e?.report?Object.keys(e.report)[0]:void 0).filter(e=>typeof e==`string`&&e.length>0):[]},R=async e=>{let{collection:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=(await A({url:t.url,props:{"cs:getctag":{}},depth:`0`,headers:D(n,r),headersToExclude:r,fetchOptions:i,fetch:a})).find(e=>w(t.url,e.href,t.url));if(!o)throw Error(`Collection does not exist on server`);let s=o.propStats?.length&&o.propStats.every(e=>e.status===404&&f(e.props,`getctag`));if(!o.ok&&!s)throw Error(`Collection status check failed: ${o.status} ${o.statusText}`);let c=F(o.props?.getctag);return{isDirty:t.ctag==null||c==null||`${t.ctag}`!=`${c}`,newCtag:c}},z=e=>{let{url:t,props:n,headers:r,syncLevel:i,syncToken:a,headersToExclude:o,fetchOptions:s,fetch:c}=e;return k({url:t,init:{method:`REPORT`,namespace:`d`,headers:D({...r},o),body:{"sync-collection":{_attributes:T([`urn:ietf:params:xml:ns:caldav`,`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"sync-level":i,"sync-token":a,"d:prop":n}}},headersToExclude:o,fetchOptions:s,fetch:c})},B=async e=>{let{collection:t,method:n,headers:r,headersToExclude:i,account:a,detailedResult:o,fetchOptions:s={},fetch:c}=e,l=[`accountType`,`homeUrl`];if(!a||!u(a,l))throw Error(a?`account must have ${d(a,l)} before smartCollectionSync`:`no account for smartCollectionSync`);let f=n??(t.reports?.includes(`syncCollection`)?`webdav`:`basic`);if(ue(`smart collection sync with type ${a.accountType} and method ${f}`),f===`webdav`){let e=await z({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{},"d:displayname":{}},syncLevel:1,syncToken:t.syncToken,headers:D(r,i),headersToExclude:i,fetchOptions:s,fetch:c}),n=e=>typeof e.href==`string`&&P(e.href,t.url)!==P(t.url,t.url)&&!e.props?.resourcetype?.collection,l=e.find(e=>(!e.ok||e.status>=400)&&!(e.status===404&&!e.propStats?.length&&n(e)));if(l)throw Error(`Collection sync failed: ${l.status} ${l.statusText}`);if(e.some(e=>e.raw&&!e.raw.multistatus))throw Error(`Collection sync failed: expected a DAV multistatus response`);if(e.some(e=>e.raw?.multistatus?.response&&(typeof e.href!=`string`||!e.href)))throw Error(`Collection sync failed: missing href in DAV response`);let u=e.filter(n),d=u.filter(e=>e.status!==404).map(e=>e.href),f=u.filter(e=>e.status===404).map(e=>e.href),p=t.objectMultiGet;if(d.length>0&&!p)throw Error(`collection.objectMultiGet is required for webdav sync changes`);let m=d.length?await p?.({url:t.url,props:{"d:getetag":{},[`${a.accountType===`caldav`?`c`:`card`}:${a.accountType===`caldav`?`calendar-data`:`address-data`}`]:{}},objectUrls:d,depth:`1`,headers:D(r,i),headersToExclude:i,fetchOptions:s,fetch:c})??[]:[];ce(m,a.accountType===`caldav`?`calendarData`:`addressData`,d,t.url,`Collection sync multi-get failed`);let h=m.map(e=>({url:de(e.href??``,t.url),etag:F(e.props?.getetag),data:a?.accountType===`caldav`?e.props?.calendarData?._cdata??e.props?.calendarData:e.props?.addressData?._cdata??e.props?.addressData})),g=t.objects??[],_=f.map(e=>({url:de(e,t.url),etag:``})),{created:v,updated:y,deleted:b,unchanged:x}=ie(g,h,t.url,!0,_);return{...t,objects:o?{created:v,updated:y,deleted:b}:[...x,...v,...y],syncToken:F(e[0]?.raw?.multistatus?.syncToken)??t.syncToken}}if(f===`basic`){let{isDirty:e,newCtag:n}=await R({collection:t,headers:D(r,i),headersToExclude:i,fetchOptions:s,fetch:c});if(!e)return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t;let a=t.objects??[];if(!t.fetchObjects)throw Error(`collection.fetchObjects is required for basic sync changes`);let l=await t.fetchObjects({collection:t,headers:D(r,i),headersToExclude:i,fetchOptions:s,fetch:c})??[],{created:u,updated:d,deleted:f,unchanged:p}=ie(a,l,t.url);return{...t,objects:o?{created:u,updated:d,deleted:f}:[...p,...u,...d],ctag:n}}return o?{...t,objects:{created:[],updated:[],deleted:[]}}:t},pe=async e=>B({...e,detailedResult:!0});var me=r({addressBookMultiGet:()=>H,addressBookQuery:()=>V,createVCard:()=>ge,deleteVCard:()=>ve,fetchAddressBooks:()=>U,fetchVCards:()=>W,updateVCard:()=>_e});const he=e(`tsdav:addressBook`),V=async e=>{let{url:t,props:n,filters:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return I({url:t,body:{"addressbook-query":E({_attributes:T([`urn:ietf:params:xml:ns:carddav`,`DAV:`]),"d:prop":n,filter:r??{"prop-filter":{_attributes:{name:`FN`}}}})},defaultNamespace:`card`,depth:i,headers:D(a,o),headersToExclude:o,fetchOptions:s,fetch:c})},H=async e=>{let{url:t,props:n,objectUrls:r,depth:i,headers:a,headersToExclude:o,fetchOptions:s={},fetch:c}=e;return I({url:t,body:{"addressbook-multiget":E({_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:carddav`]),"d:prop":n,"d:href":r})},defaultNamespace:`card`,depth:i,headers:D(a,o),headersToExclude:o,fetchOptions:s,fetch:c})},U=async e=>{let{account:t,headers:n,props:r,headersToExclude:i,fetchOptions:a={},fetch:o}=e??{},s=[`homeUrl`,`rootUrl`];if(!t||!u(t,s))throw Error(t?`account must have ${d(t,s)} before fetchAddressBooks`:`no account for fetchAddressBooks`);let c=await A({url:t.homeUrl,props:{...r??{"d:displayname":{},"cs:getctag":{},"d:resourcetype":{},"d:sync-token":{}},"d:resourcetype":{}},depth:`1`,headers:D(n,i),headersToExclude:i,fetchOptions:a,fetch:o});return oe(c,`Address book discovery failed`),Promise.all(c.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`addressbook`)).map(e=>{let n=e.props?.displayname?._cdata??e.props?.displayname;return he(`Found address book named ${typeof n==`string`?n:``}, + props: ${JSON.stringify(e.props)}`),{url:new URL(e.href??``,x(t.rootUrl??``)).href,ctag:F(e.props?.getctag),displayName:typeof n==`string`?n:``,resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:F(e.props?.syncToken)}}).map(async e=>({...e,reports:await L({collection:e,headers:D(n,i),headersToExclude:i,fetchOptions:a,fetch:o})})))},W=async e=>{let{addressBook:t,headers:n,objectUrls:r,headersToExclude:i,urlFilter:a=e=>!!e,useMultiGet:o=!0,fetchOptions:s={},fetch:c}=e;he(`Fetching vcards from ${t?.url}`);let l=[`url`];if(!t||!u(t,l))throw Error(t?`addressBook must have ${d(t,l)} before fetchVCards`:`cannot fetchVCards for undefined addressBook`);let f=(r??(await V({url:t.url,props:{"d:getetag":{}},depth:`1`,headers:D(n,i),headersToExclude:i,fetchOptions:s,fetch:c})).map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,x(t.url)).href).filter(e=>!S(e,t.url)).filter(a).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),p=new Set(f.map(e=>P(e,t.url))),m=[];return f.length>0&&(o?m=await H({url:t.url,props:{"d:getetag":{},"card:address-data":{}},objectUrls:f,depth:`1`,headers:D(n,i),headersToExclude:i,fetchOptions:s,fetch:c}):(m=await V({url:t.url,props:{"d:getetag":{},"card:address-data":{}},depth:`1`,headers:D(n,i),headersToExclude:i,fetchOptions:s,fetch:c}),m=m.filter(e=>!!e.href&&p.has(P(e.href,t.url))))),ce(m,`addressData`,f,t.url,`VCard fetch failed`),m.map(e=>({url:new URL(e.href??``,x(t.url)).href,etag:F(e.props?.getetag),data:e.props?.addressData?._cdata??e.props?.addressData}))},ge=async e=>{let{addressBook:t,vCardString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return j({url:new URL(r,x(t.url)).href,data:n,headers:D({"content-type":`text/vcard; charset=utf-8`,"If-None-Match":`*`,...i},a),headersToExclude:a,fetchOptions:o,fetch:s})},_e=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return M({url:t.url,data:t.data,etag:t.etag,headers:D({"content-type":`text/vcard; charset=utf-8`,...n},r),headersToExclude:r,fetchOptions:i,fetch:a})},ve=async e=>{let{vCard:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return N({url:t.url,etag:t.etag,headers:D(n,r),headersToExclude:r,fetchOptions:i,fetch:a})};var ye=r({calendarMultiGet:()=>K,calendarQuery:()=>G,createCalendarObject:()=>De,deleteCalendarObject:()=>ke,fetchCalendarObjects:()=>J,fetchCalendarUserAddresses:()=>Te,fetchCalendars:()=>q,freeBusyQuery:()=>Me,makeCalendar:()=>Ee,syncCalendars:()=>Ae,syncCalendarsDetailed:()=>je,updateCalendarObject:()=>Oe});const be=e(`tsdav:calendar`),xe=/^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i,Se=/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i,Ce=e=>{let{start:t,end:n}=e;if(!(xe.test(t)&&xe.test(n)||Se.test(t)&&Se.test(n)))throw Error(`invalid timeRange format, not in ISO8601`);if(Number.isNaN(new Date(t).getTime())||Number.isNaN(new Date(n).getTime()))throw Error(`invalid timeRange: start or end is not a valid date`);if(new Date(t).getTime()>=new Date(n).getTime())throw Error(`invalid timeRange: start must be before end`)},we=e=>{let t=[];return Array.isArray(e)?t=e.map(e=>e?._attributes?.name):e&&typeof e==`object`&&(t=[e._attributes?.name]),t.filter(e=>typeof e==`string`&&e.length>0)},Te=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!u(t,o))throw Error(`account must have ${d(t,o)} before fetchUserAddresses`);be(`Fetch user addresses from ${t.principalUrl}`);let s=(await A({url:t.principalUrl,props:{"c:calendar-user-address-set":{}},depth:`0`,headers:D(n,r),headersToExclude:r,fetchOptions:i,fetch:a})).find(e=>w(t.principalUrl,e.href,t.rootUrl));if(!s||!s.ok)throw Error(`cannot find calendarUserAddresses`);let c=s?.props?.calendarUserAddressSet?.href,l=[];Array.isArray(c)?l=c:c&&(l=[c]);let f=l.filter(e=>typeof e==`string`&&e.length>0);return be(`Fetched calendar user addresses ${f}`),f},G=async e=>{let{url:t,props:n,filters:r,timezone:i,depth:a,headers:o,headersToExclude:s,fetchOptions:c={},fetch:l}=e;return I({url:t,body:{"calendar-query":E({_attributes:T([`urn:ietf:params:xml:ns:caldav`,`http://calendarserver.org/ns/`,`http://apple.com/ns/ical/`,`DAV:`]),"d:prop":n,filter:r,timezone:i})},defaultNamespace:`c`,depth:a,headers:D(o,s),headersToExclude:s,fetchOptions:c,fetch:l})},K=async e=>{let{url:t,props:n,objectUrls:r,filters:i,timezone:a,depth:o,headers:s,headersToExclude:c,fetchOptions:l={},fetch:u}=e;return I({url:t,body:{"calendar-multiget":E({_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:caldav`]),"d:prop":n,"d:href":r,filter:i,timezone:a})},defaultNamespace:`c`,depth:o,headers:D(s,c),headersToExclude:c,fetchOptions:l,fetch:u})},Ee=async e=>{let{url:t,props:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return k({url:t,init:{method:`MKCALENDAR`,headers:D(E({depth:r,...i}),a),namespace:`d`,body:{"c:mkcalendar":{_attributes:T([`DAV:`,`urn:ietf:params:xml:ns:caldav`,`http://apple.com/ns/ical/`]),set:{prop:n}}}},headersToExclude:a,fetchOptions:o,fetch:s})},q=async e=>{let{headers:t,account:n,props:r,projectedProps:i,headersToExclude:a,fetchOptions:o={},fetch:c}=e??{},l=[`homeUrl`,`rootUrl`];if(!n||!u(n,l))throw Error(n?`account must have ${d(n,l)} before fetchCalendars`:`no account for fetchCalendars`);let f=await A({url:n.homeUrl,props:{...r??{"c:calendar-description":{},"c:calendar-timezone":{},"d:displayname":{},"ca:calendar-color":{},"cs:getctag":{},"d:resourcetype":{},"c:supported-calendar-component-set":{},"d:sync-token":{}},"d:resourcetype":{},"c:supported-calendar-component-set":{}},depth:`1`,headers:D(t,a),headersToExclude:a,fetchOptions:o,fetch:c});return oe(f,`Calendar discovery failed`),Promise.all(f.filter(e=>Object.keys(e.props?.resourcetype??{}).includes(`calendar`)).filter(e=>{let t=we(e.props?.supportedCalendarComponentSet?.comp);return t.length===0||t.some(e=>Object.values(s).includes(e))}).map(e=>{let t=e.props?.calendarDescription,r=e.props?.calendarTimezone,a=e.props?.supportedCalendarComponentSet?.comp,o=Object.entries(e.props??{}).filter(([e])=>i?.[e]);return{description:typeof t==`string`?t:``,timezone:typeof r==`string`?r:``,url:new URL(e.href??``,x(n.rootUrl??``)).href,ctag:F(e.props?.getctag),calendarColor:e.props?.calendarColor,displayName:F(e.props?.displayname),components:we(a),resourcetype:Object.keys(e.props?.resourcetype??{}),syncToken:F(e.props?.syncToken),...i&&o.length>0?{projectedProps:Object.fromEntries(o)}:{}}}).map(async e=>({...e,reports:await L({collection:e,headers:D(t,a),headersToExclude:a,fetchOptions:o,fetch:c})})))},J=async e=>{let{calendar:t,objectUrls:n,filters:r,timeRange:i,headers:a,expand:o,urlFilter:s=e=>!!e?.includes(`.ics`),useMultiGet:c=!0,headersToExclude:l,fetchOptions:f={},fetch:p}=e;if(o&&!i)throw Error(`timeRange is required when expand is true`);i&&Ce(i),be(`Fetching calendar objects from ${t?.url}`);let m=[`url`];if(!t||!u(t,m))throw Error(t?`calendar must have ${d(t,m)} before fetchCalendarObjects`:`cannot fetchCalendarObjects for undefined calendar`);let h=r??[{"comp-filter":{_attributes:{name:`VCALENDAR`},"comp-filter":{_attributes:{name:`VEVENT`},...i?{"time-range":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}}}],g=[];n||(g=await G({url:t.url,props:{"d:getetag":{},...o&&i?{"c:calendar-data":{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}}:{}},filters:h,depth:`1`,headers:D(a,l),headersToExclude:l,fetchOptions:f,fetch:p}));let _=(n??g.map(e=>e.href??``)).filter(e=>typeof e==`string`&&e.trim().length>0).map(e=>e.startsWith(`http`)?e:new URL(e,x(t.url)).href).filter(s).map(e=>{let t=new URL(e);return`${t.pathname}${t.search}`}),v=new Set(_.map(e=>P(e,t.url))),y=[];return _.length>0&&(o&&!n?y=g.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href:new URL(e.href??``,x(t.url)).href;return s(n??``)}):c?y=await K({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},objectUrls:_,depth:`1`,headers:D(a,l),headersToExclude:l,fetchOptions:f,fetch:p}):(y=await G({url:t.url,props:{"d:getetag":{},"c:calendar-data":{...o&&i?{"c:expand":{_attributes:{start:`${new Date(i.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(i.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}}:{}}},filters:h,depth:`1`,headers:D(a,l),headersToExclude:l,fetchOptions:f,fetch:p}),y=y.filter(e=>{let n=(e.href??``).startsWith(`http`)?e.href??``:new URL(e.href??``,x(t.url)).href;return v.has(P(n,t.url))}))),ce(y,`calendarData`,_,t.url,`Calendar object fetch failed`),y.map(e=>({url:new URL(e.href??``,x(t.url)).href,etag:F(e.props?.getetag),data:e.props?.calendarData?._cdata??e.props?.calendarData}))},De=async e=>{let{calendar:t,iCalString:n,filename:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;return j({url:new URL(r,x(t.url)).href,data:n,headers:D({"content-type":`text/calendar; charset=utf-8`,"If-None-Match":`*`,...i},a),headersToExclude:a,fetchOptions:o,fetch:s})},Oe=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return M({url:t.url,data:t.data,etag:t.etag,headers:D({"content-type":`text/calendar; charset=utf-8`,...n},r),headersToExclude:r,fetchOptions:i,fetch:a})},ke=async e=>{let{calendarObject:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e;return N({url:t.url,etag:t.etag,headers:D(n,r),headersToExclude:r,fetchOptions:i,fetch:a})},Ae=async e=>{let{oldCalendars:t,account:n,detailedResult:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`Must have account before syncCalendars`);let c=t??n.calendars??[],l=await q({account:n,headers:D(i,a),headersToExclude:a,fetchOptions:o,fetch:s}),u=n.rootUrl??n.homeUrl??n.serverUrl,d=new Map(c.map(e=>[P(e.url,u),e])),f=new Map(l.map(e=>[P(e.url,u),e])),p=l.filter(e=>!d.has(P(e.url,u))),m=[],h=[],g=[];for(let e of c){let t=f.get(P(e.url,u));t?!t.syncToken&&!t.ctag||t.syncToken&&t.syncToken!==e.syncToken||t.ctag&&t.ctag!==e.ctag?m.push({local:e,remote:t}):h.push(e):g.push(e)}be(`updated calendars: ${m.map(({remote:e})=>e.displayName)}`);let _=await Promise.all(m.map(async({local:e,remote:t})=>{let r=async e=>{if(!e)return[];let{collection:t,...n}=e;return J({...n,calendar:t,filters:{"comp-filter":{_attributes:{name:`VCALENDAR`}}},urlFilter:e=>P(e,t.url)!==P(t.url,t.url)})},c={...t,ctag:e.ctag,syncToken:e.syncToken,objects:e.objects,objectMultiGet:K,fetchObjects:r},l=await B({collection:c,detailedResult:!1,headers:D(i,a),headersToExclude:a,account:n,fetchOptions:o,fetch:s});return{...l,ctag:t.reports?.includes(`syncCollection`)?t.ctag??l.ctag:l.ctag??t.ctag,syncToken:t.reports?.includes(`syncCollection`)?l.syncToken:t.syncToken??l.syncToken}}));return r?{created:p,updated:_,deleted:g}:[...h,...p,..._]},je=async e=>Ae({...e,detailedResult:!0}),Me=async e=>{let{url:t,timeRange:n,depth:r,headers:i,headersToExclude:a,fetchOptions:o={},fetch:s}=e;if(!n)throw Error(`timeRange is required`);Ce(n);let c=(await I({url:t,body:{"free-busy-query":E({_attributes:T([`urn:ietf:params:xml:ns:caldav`]),"c:time-range":{_attributes:{start:`${new Date(n.start).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`,end:`${new Date(n.end).toISOString().slice(0,19).replace(/[-:.]/g,``)}Z`}}})},defaultNamespace:`c`,depth:r,headers:D(i,a),headersToExclude:a,fetchOptions:o,fetch:s}))[0];if(!c)throw Error(`freeBusyQuery returned no response`);return c};var Ne=r({createAccount:()=>Z,fetchHomeUrl:()=>Le,fetchPrincipalUrl:()=>X,serviceDiscovery:()=>Fe});const Y=e(`tsdav:account`),Pe=(e,t)=>{let n=[t,e,new URL(`/`,e).href];return n.filter((e,t)=>n.indexOf(e)===t)},Fe=async e=>{Y(`Service discovery...`);let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=a??c,s=new URL(t.serverUrl),{headers:l,...u}=i,d=new URL(`/.well-known/${t.accountType}`,s);d.protocol=s.protocol??`http`;let f=e=>{if(e.status>=300&&e.status<400){let t=e.headers.get(`Location`);if(typeof t==`string`&&t.length)return Y(`Service discovery redirected to ${t}`),new URL(t,d).href}};try{let e=f(await o(d.href,{...u,method:`PROPFIND`,headers:D(O({"Content-Type":`text/xml;charset=UTF-8`},n,l),r),body:` -`,redirect:`manual`}));if(e)return e}catch(e){J(`Service discovery PROPFIND failed: ${e.stack}`)}try{let e=f(await o(d.href,{...u,method:`GET`,headers:x(S(n,c),r),redirect:`manual`}));if(e)return e}catch(e){J(`Service discovery GET failed: ${e.stack}`)}return s.href},Ee=e=>{if(typeof e==`string`&&e.trim().length>0)return e.trim();if(Array.isArray(e)){for(let t of e){let e=Ee(t);if(e)return e}return}if(e&&typeof e==`object`){if(`_cdata`in e&&typeof e._cdata==`string`){let t=e._cdata.trim();if(t.length>0)return t}if(`_text`in e&&typeof e._text==`string`){let t=e._text.trim();if(t.length>0)return t}}},De=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`rootUrl`];if(!k(t,o))throw Error(`account must have ${A(t,o)} before fetchPrincipalUrl`);J(`Fetching principal url from path ${t.rootUrl}`);let[s]=await T({url:t.rootUrl,props:{"d:current-user-principal":{}},depth:`0`,headers:x(n,r),fetchOptions:i,fetch:a});if(!s?.ok)throw J(`Fetch principal url failed: ${s?.statusText??`empty response`}`),s?.status===401?Error(`Invalid credentials: PROPFIND ${t.rootUrl} returned 401 Unauthorized`):Error(`cannot find principalUrl`);let c=Ee(s.props?.currentUserPrincipal?.href);if(!c)throw J(`Fetch principal url failed: missing current-user-principal href`),Error(`cannot find principalUrl`);return J(`Fetched principal url ${c}`),new URL(c,m(t.rootUrl)).href},Oe=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!k(t,o))throw Error(`account must have ${A(t,o)} before fetchHomeUrl`);J(`Fetch home url from ${t.principalUrl}`);let s=await T({url:t.principalUrl,props:t.accountType===`caldav`?{"c:calendar-home-set":{}}:{"card:addressbook-home-set":{}},depth:`0`,headers:x(n,r),fetchOptions:i,fetch:a}),c=s.find(e=>_(t.principalUrl,e.href,t.rootUrl));if(!c||!c.ok)throw J(`Fetch home url failed with status ${c?.statusText} and error ${JSON.stringify(s.map(e=>e.error))}`),Error(`cannot find homeUrl`);let l=Ee(t.accountType===`caldav`?c.props?.calendarHomeSet?.href:c.props?.addressbookHomeSet?.href);if(!l)throw J(`Fetch home url failed: server did not return a ${t.accountType===`caldav`?`calendar-home-set`:`addressbook-home-set`} href`),Error(`cannot find homeUrl`);let u=new URL(l,m(t.rootUrl)).href;return J(`Fetched home url ${u}`),u},Y=async e=>{let{account:t,headers:n,loadCollections:r=!1,loadObjects:i=!1,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c={...t},l=t.rootUrl??await Te({account:t,headers:x(n,a),fetchOptions:o,fetch:s});if(t.rootUrl)c.rootUrl=t.rootUrl;else if(t.principalUrl)c.rootUrl=l;else{let e=async(t,r=0,i)=>{let l=t[r];if(!l)throw i??Error(`cannot find principalUrl`);try{return{rootUrl:l,principalUrl:await De({account:{...c,rootUrl:l},headers:x(n,a),fetchOptions:o,fetch:s})}}catch(n){return e(t,r+1,n)}},{rootUrl:r,principalUrl:i}=await e(we(t.serverUrl,l));c.rootUrl=r,c.principalUrl=i}return c.principalUrl=t.principalUrl??c.principalUrl??await De({account:c,headers:x(n,a),fetchOptions:o,fetch:s}),c.homeUrl=t.homeUrl??await Oe({account:c,headers:x(n,a),fetchOptions:o,fetch:s}),(r||i)&&(t.accountType===`caldav`?c.calendars=await G({headers:x(n,a),account:c,fetchOptions:o,fetch:s}):t.accountType===`carddav`&&(c.addressBooks=await R({headers:x(n,a),account:c,fetchOptions:o,fetch:s}))),i&&(t.accountType===`caldav`&&c.calendars?c.calendars=await Promise.all(c.calendars.map(async e=>({...e,objects:await K({calendar:e,headers:x(n,a),fetchOptions:o,fetch:s})}))):t.accountType===`carddav`&&c.addressBooks&&(c.addressBooks=await Promise.all(c.addressBooks.map(async e=>({...e,objects:await z({addressBook:e,headers:x(n,a),fetchOptions:o,fetch:s})}))))),c};var ke=r({defaultParam:()=>Q,fetchOauthTokens:()=>Fe,getBasicAuthHeaders:()=>Ne,getBearerAuthHeaders:()=>Pe,getOauthHeaders:()=>$,refreshAccessToken:()=>Ie});const X=e(`tsdav:authHelper`),Z=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;var Ae=class extends Error{constructor(e){super(e),this.name=`InvalidCharacterError`}};const je=e=>{if(e>255)throw new Ae(`The string to be encoded contains characters outside of the Latin1 range.`)},Me=e=>{let t=``,n=0;for(;n(...n)=>{let r=n[0],i={...t,...r};return(t.headers||r?.headers)&&(i.headers=S(t.headers,r?.headers)),e(i)},Ne=e=>(X(`Basic auth token generated for user "${e.username??``}"`),{authorization:`Basic ${Me(`${e.username}:${e.password}`)}`}),Pe=e=>({authorization:`Bearer ${e.accessToken}`}),Fe=async(e,t,n)=>{let r=[`authorizationCode`,`redirectUrl`,`clientId`,`clientSecret`,`tokenUrl`];if(!k(e,r))throw Error(`Oauth credentials missing: ${A(e,r)}`);let i=new URLSearchParams({grant_type:`authorization_code`,code:e.authorizationCode,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret});X(`Fetching oauth tokens from ${e.tokenUrl}`);let a=n??l,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:S({"content-type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(X(`Fetch Oauth tokens failed with status ${c.status}`),{})},Ie=async(e,t,n)=>{let r=[`refreshToken`,`clientId`,`clientSecret`,`tokenUrl`];if(!k(e,r))throw Error(`Oauth credentials missing: ${A(e,r)}`);let i=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,refresh_token:e.refreshToken,grant_type:`refresh_token`}),a=n??l,{headers:o,...s}=t??{},c=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:S({"Content-Type":`application/x-www-form-urlencoded`},o)});return c.ok?await c.json():(X(`Refresh access token failed with status ${c.status}`),{})},$=async(e,t,n)=>{X(`Fetching oauth headers`);let r={},i=!1;return e.refreshToken?e.refreshToken&&!e.accessToken||Date.now()>(e.expiration??0)?(r=await Ie(e,t,n),i=!0):r={access_token:e.accessToken,refresh_token:e.refreshToken}:(r=await Fe(e,t,n),i=!0),i&&(r.access_token&&(e.accessToken=r.access_token),r.refresh_token&&(e.refreshToken=r.refresh_token),typeof r.expires_in==`number`&&(e.expiration=Date.now()+r.expires_in*1e3)),X(`Oauth tokens obtained`),{tokens:r,headers:r.access_token?{authorization:`Bearer ${r.access_token}`}:{}}};var Le=r({DAVClient:()=>ze,createDAVClient:()=>Re});const Re=async e=>{let{serverUrl:t,credentials:n,authMethod:r=`Basic`,defaultAccountType:i,authFunction:a,fetchOptions:o,fetch:s}=e,c={};switch(r){case`Basic`:c=Ne(n);break;case`Bearer`:c=Pe(n);break;case`Oauth`:c=(await $(n,o,s)).headers;break;case`Digest`:c={Authorization:`Digest ${n.digestString}`};break;case`Custom`:if(!a)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);c=await a(n)??{};break;default:throw Error(`Invalid auth method`)}let l=i?await Y({account:{serverUrl:t,credentials:n,accountType:i},headers:c,fetchOptions:o,fetch:s}):void 0,u=async e=>{let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...l}=t;return w({...i,init:{...l,headers:{...c,...a}},fetchOptions:n??o,fetch:r??s})},d={headers:c,fetchOptions:o,fetch:s},f={url:t,...d},p={account:l,...d},m=Q(E,f),h=Q(D,f),g=Q(O,f),_=Q(T,d),v=async e=>{let{account:r,headers:i,loadCollections:a,loadObjects:l,fetchOptions:u,fetch:d}=e,f={serverUrl:t,credentials:n,...r};if(!f.accountType)throw Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client.");return Y({account:f,headers:{...c,...i},loadCollections:a,loadObjects:l,fetchOptions:u??o,fetch:d??s})},y=Q(j,d),b=Q(oe,d),x=Q(P,d),S=Q(M,d),C=Q(N,d),ee=Q(F,p),te=Q(se,p),k=Q(U,d),A=Q(W,d),ne=Q(_e,d),re=Q(G,p),ie=Q(H,p),ae=Q(K,d),ce=Q(ve,d),le=Q(ye,d),fe=Q(be,d),V=Q(q,p),pe=Q(xe,p),me=Q(Se,d),he=Q(I,d),ge=Q(L,d);return{davRequest:u,propfind:_,createAccount:v,createObject:m,updateObject:h,deleteObject:g,calendarQuery:k,addressBookQuery:he,collectionQuery:y,makeCollection:b,calendarMultiGet:A,makeCalendar:ne,freeBusyQuery:me,syncCollection:x,supportedReportSet:S,isCollectionDirty:C,smartCollectionSync:ee,smartCollectionSyncDetailed:te,fetchCalendars:re,fetchCalendarUserAddresses:ie,fetchCalendarObjects:ae,createCalendarObject:ce,updateCalendarObject:le,deleteCalendarObject:fe,syncCalendars:V,syncCalendarsDetailed:pe,fetchAddressBooks:Q(R,p),addressBookMultiGet:ge,fetchVCards:Q(z,d),createVCard:Q(ue,d),updateVCard:Q(B,d),deleteVCard:Q(de,d)}};var ze=class{constructor(e){this.serverUrl=e.serverUrl,this.credentials=e.credentials,this.authMethod=e.authMethod??`Basic`,this.accountType=e.defaultAccountType??`caldav`,this.authFunction=e.authFunction,this.fetchOptions=e.fetchOptions??{},this.fetchOverride=e.fetch}async login(e){switch(this.authMethod){case`Basic`:this.authHeaders=Ne(this.credentials);break;case`Bearer`:this.authHeaders=Pe(this.credentials);break;case`Oauth`:this.authHeaders=(await $(this.credentials,this.fetchOptions,this.fetchOverride)).headers;break;case`Digest`:this.authHeaders={Authorization:`Digest ${this.credentials.digestString}`};break;case`Custom`:if(!this.authFunction)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);this.authHeaders=await this.authFunction(this.credentials);break;default:throw Error(`Invalid auth method`)}this.account=this.accountType?await Y({account:{serverUrl:this.serverUrl,credentials:this.credentials,accountType:this.accountType},headers:this.authHeaders,loadCollections:e?.loadCollections,loadObjects:e?.loadObjects,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}):void 0}async davRequest(e){let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...o}=t;return w({...i,init:{...o,headers:{...this.authHeaders,...a}},fetchOptions:n??this.fetchOptions,fetch:r??this.fetchOverride})}async createObject(...e){return Q(E,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateObject(...e){return Q(D,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteObject(...e){return Q(O,{url:this.serverUrl,headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async propfind(...e){return Q(T,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createAccount(e){let{account:t,headers:n,loadCollections:r,loadObjects:i,fetchOptions:a,fetch:o}=e,s=t.accountType??this.accountType;if(!s)throw Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient.");return Y({account:{serverUrl:this.serverUrl,credentials:this.credentials,...t,accountType:s},headers:{...this.authHeaders,...n},loadCollections:r,loadObjects:i,fetchOptions:a??this.fetchOptions,fetch:o??this.fetchOverride})}async collectionQuery(...e){return Q(j,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async makeCollection(...e){return Q(oe,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCollection(...e){return Q(P,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async supportedReportSet(...e){return Q(M,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async isCollectionDirty(...e){return Q(N,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async smartCollectionSync(...e){return Q(F,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride,account:this.account})(e[0])}async smartCollectionSyncDetailed(e){return Q(se,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride,account:this.account})(e)}async calendarQuery(...e){return Q(U,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async makeCalendar(...e){return Q(_e,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async freeBusyQuery(...e){return Q(Se,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async calendarMultiGet(...e){return Q(W,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async fetchCalendars(...e){return Q(G,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchCalendarUserAddresses(...e){return Q(H,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchCalendarObjects(...e){return Q(K,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createCalendarObject(...e){return Q(ve,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateCalendarObject(...e){return Q(ye,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteCalendarObject(...e){return Q(be,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCalendars(...e){return Q(q,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async syncCalendarsDetailed(...e){return Q(xe,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async addressBookQuery(...e){return Q(I,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async addressBookMultiGet(...e){return Q(L,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async fetchAddressBooks(...e){return Q(R,{headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e?.[0])}async fetchVCards(...e){return Q(z,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async createVCard(...e){return Q(ue,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async updateVCard(...e){return Q(B,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}async deleteVCard(...e){return Q(de,{headers:this.authHeaders,fetchOptions:this.fetchOptions,fetch:this.fetchOverride})(e[0])}},Be={DAVNamespace:i,DAVNamespaceShort:o,DAVAttributeMap:a,ICALObjects:s,...Le,...C,...ne,...Ce,...ce,...fe,...ke,...f};export{a as DAVAttributeMap,ze as DAVClient,i as DAVNamespace,o as DAVNamespaceShort,s as ICALObjects,L as addressBookMultiGet,I as addressBookQuery,W as calendarMultiGet,U as calendarQuery,y as cleanupFalsy,j as collectionQuery,Y as createAccount,ve as createCalendarObject,Re as createDAVClient,E as createObject,ue as createVCard,w as davRequest,Be as default,be as deleteCalendarObject,O as deleteObject,de as deleteVCard,m as ensureTrailingSlash,x as excludeHeaders,R as fetchAddressBooks,K as fetchCalendarObjects,H as fetchCalendarUserAddresses,G as fetchCalendars,Oe as fetchHomeUrl,Fe as fetchOauthTokens,De as fetchPrincipalUrl,z as fetchVCards,Se as freeBusyQuery,Ne as getBasicAuthHeaders,Pe as getBearerAuthHeaders,v as getDAVAttribute,$ as getOauthHeaders,N as isCollectionDirty,_e as makeCalendar,S as mergeHeaders,T as propfind,Ie as refreshAccessToken,Te as serviceDiscovery,F as smartCollectionSync,se as smartCollectionSyncDetailed,M as supportedReportSet,q as syncCalendars,xe as syncCalendarsDetailed,P as syncCollection,ye as updateCalendarObject,D as updateObject,B as updateVCard,g as urlContains,h as urlEquals,_ as urlMatches}; \ No newline at end of file +`,redirect:`manual`}));if(e)return e}catch(e){Y(`Service discovery PROPFIND failed: ${e.stack}`)}try{let e=f(await o(d.href,{...u,method:`GET`,body:void 0,headers:D(O(n,l),r),redirect:`manual`}));if(e)return e}catch(e){Y(`Service discovery GET failed: ${e.stack}`)}return s.href},Ie=e=>{if(typeof e==`string`&&e.trim().length>0)return e.trim();if(Array.isArray(e)){for(let t of e){let e=Ie(t);if(e)return e}return}if(e&&typeof e==`object`){if(`_cdata`in e&&typeof e._cdata==`string`){let t=e._cdata.trim();if(t.length>0)return t}if(`_text`in e&&typeof e._text==`string`){let t=e._text.trim();if(t.length>0)return t}}},X=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`rootUrl`];if(!u(t,o))throw Error(`account must have ${d(t,o)} before fetchPrincipalUrl`);Y(`Fetching principal url from path ${t.rootUrl}`);let[s]=await A({url:t.rootUrl,props:{"d:current-user-principal":{}},depth:`0`,headers:D(n,r),headersToExclude:r,fetchOptions:i,fetch:a});if(!s?.ok)throw Y(`Fetch principal url failed: ${s?.statusText??`empty response`}`),s?.status===401?Error(`Invalid credentials: PROPFIND ${t.rootUrl} returned 401 Unauthorized`):Error(`cannot find principalUrl`);let c=Ie(s.props?.currentUserPrincipal?.href);if(!c)throw Y(`Fetch principal url failed: missing current-user-principal href`),Error(`cannot find principalUrl`);return Y(`Fetched principal url ${c}`),new URL(c,x(t.rootUrl)).href},Le=async e=>{let{account:t,headers:n,headersToExclude:r,fetchOptions:i={},fetch:a}=e,o=[`principalUrl`,`rootUrl`];if(!u(t,o))throw Error(`account must have ${d(t,o)} before fetchHomeUrl`);Y(`Fetch home url from ${t.principalUrl}`);let s=await A({url:t.principalUrl,props:t.accountType===`caldav`?{"c:calendar-home-set":{}}:{"card:addressbook-home-set":{}},depth:`0`,headers:D(n,r),headersToExclude:r,fetchOptions:i,fetch:a}),c=s.find(e=>w(t.principalUrl,e.href,t.rootUrl));if(!c||!c.ok)throw Y(`Fetch home url failed with status ${c?.statusText} and error ${JSON.stringify(s.map(e=>e.error))}`),Error(`cannot find homeUrl`);let l=Ie(t.accountType===`caldav`?c.props?.calendarHomeSet?.href:c.props?.addressbookHomeSet?.href);if(!l)throw Y(`Fetch home url failed: server did not return a ${t.accountType===`caldav`?`calendar-home-set`:`addressbook-home-set`} href`),Error(`cannot find homeUrl`);let f=new URL(l,x(t.rootUrl)).href;return Y(`Fetched home url ${f}`),f},Z=async e=>{let{account:t,headers:n,loadCollections:r=!1,loadObjects:i=!1,headersToExclude:a,fetchOptions:o={},fetch:s}=e,c={...t},l=t.rootUrl??await Fe({account:t,headers:D(n,a),headersToExclude:a,fetchOptions:o,fetch:s});if(t.rootUrl)c.rootUrl=t.rootUrl;else if(t.principalUrl)c.rootUrl=l;else{let e=async(t,r=0,i)=>{let l=t[r];if(!l)throw i??Error(`cannot find principalUrl`);try{return{rootUrl:l,principalUrl:await X({account:{...c,rootUrl:l},headers:D(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}}catch(n){return e(t,r+1,n)}},{rootUrl:r,principalUrl:i}=await e(Pe(t.serverUrl,l));c.rootUrl=r,c.principalUrl=i}return c.principalUrl=t.principalUrl??c.principalUrl??await X({account:c,headers:D(n,a),headersToExclude:a,fetchOptions:o,fetch:s}),c.homeUrl=t.homeUrl??await Le({account:c,headers:D(n,a),headersToExclude:a,fetchOptions:o,fetch:s}),(r||i)&&(t.accountType===`caldav`?c.calendars=await q({headers:D(n,a),headersToExclude:a,account:c,fetchOptions:o,fetch:s}):t.accountType===`carddav`&&(c.addressBooks=await U({headers:D(n,a),headersToExclude:a,account:c,fetchOptions:o,fetch:s}))),i&&(t.accountType===`caldav`&&c.calendars?c.calendars=await Promise.all(c.calendars.map(async e=>({...e,objects:await J({calendar:e,filters:{"comp-filter":{_attributes:{name:`VCALENDAR`}}},urlFilter:t=>!S(t,e.url),headers:D(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}))):t.accountType===`carddav`&&c.addressBooks&&(c.addressBooks=await Promise.all(c.addressBooks.map(async e=>({...e,objects:await W({addressBook:e,headers:D(n,a),headersToExclude:a,fetchOptions:o,fetch:s})}))))),c};var Re=r({defaultParam:()=>He,fetchOauthTokens:()=>Ge,getBasicAuthHeaders:()=>Ue,getBearerAuthHeaders:()=>We,getOauthHeaders:()=>qe,refreshAccessToken:()=>Ke});const Q=e(`tsdav:authHelper`),$=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;var ze=class extends Error{constructor(e){super(e),this.name=`InvalidCharacterError`}};const Be=e=>{if(e>255)throw new ze(`The string to be encoded contains characters outside of the Latin1 range.`)},Ve=e=>{let t=``,n=0;for(;n(...n)=>{let r=n[0],i={...t,...r};return(t.headers||r?.headers)&&(i.headers=O(t.headers,r?.headers)),e(i)},Ue=e=>(Q(`Basic auth token generated for user "${e.username??``}"`),{authorization:`Basic ${Ve(`${e.username}:${e.password}`)}`}),We=e=>({authorization:`Bearer ${e.accessToken}`}),Ge=async(e,t,n)=>{let r=[`authorizationCode`,`redirectUrl`,`clientId`,`clientSecret`,`tokenUrl`];if(!u(e,r))throw Error(`Oauth credentials missing: ${d(e,r)}`);let i=new URLSearchParams({grant_type:`authorization_code`,code:e.authorizationCode,redirect_uri:e.redirectUrl,client_id:e.clientId,client_secret:e.clientSecret});Q(`Fetching oauth tokens from ${e.tokenUrl}`);let a=n??c,{headers:o,...s}=t??{},l=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:O({"content-type":`application/x-www-form-urlencoded`},o)});return l.ok?await l.json():(Q(`Fetch Oauth tokens failed with status ${l.status}`),{})},Ke=async(e,t,n)=>{let r=[`refreshToken`,`clientId`,`clientSecret`,`tokenUrl`];if(!u(e,r))throw Error(`Oauth credentials missing: ${d(e,r)}`);let i=new URLSearchParams({client_id:e.clientId,client_secret:e.clientSecret,refresh_token:e.refreshToken,grant_type:`refresh_token`}),a=n??c,{headers:o,...s}=t??{},l=await a(e.tokenUrl,{...s,method:`POST`,body:i.toString(),headers:O({"Content-Type":`application/x-www-form-urlencoded`},o)});return l.ok?await l.json():(Q(`Refresh access token failed with status ${l.status}`),{})},qe=async(e,t,n)=>{Q(`Fetching oauth headers`);let r={},i=!1;return e.accessToken&&(e.expiration==null&&!e.refreshToken||e.expiration!=null&&Date.now()Ze,createDAVClient:()=>Xe});const Ye=async(e,t=e.fetchOptions,n=e.fetchOverride)=>{switch(e.authMethod){case`Basic`:return Ue(e.credentials);case`Bearer`:return We(e.credentials);case`Oauth`:{let{headers:r}=await qe(e.credentials,t,n);if(!r.authorization)throw Error(`OAuth authentication failed: token endpoint returned no access token`);return r}case`Digest`:return{Authorization:`Digest ${e.credentials.digestString}`};case`Custom`:if(!e.authFunction)throw Error(`authMethod 'Custom' requires an authFunction to produce request headers`);return await e.authFunction(e.credentials)??{};default:throw Error(`Invalid auth method`)}},Xe=async e=>{let t=new Ze(e);return t.authHeaders=await Ye(t),t.account=e.defaultAccountType?await Z({account:{serverUrl:e.serverUrl,credentials:e.credentials,accountType:e.defaultAccountType},headers:t.authHeaders,fetchOptions:t.fetchOptions,fetch:t.fetchOverride}):void 0,{davRequest:t.davRequest.bind(t),propfind:t.propfind.bind(t),createAccount:async(...e)=>{if(!e[0].account.accountType)throw Error("createAccount requires an accountType; pass one via `account.accountType`.");return t.createAccount(...e)},createObject:t.createObject.bind(t),updateObject:t.updateObject.bind(t),deleteObject:t.deleteObject.bind(t),calendarQuery:t.calendarQuery.bind(t),addressBookQuery:t.addressBookQuery.bind(t),collectionQuery:t.collectionQuery.bind(t),makeCollection:t.makeCollection.bind(t),calendarMultiGet:t.calendarMultiGet.bind(t),makeCalendar:t.makeCalendar.bind(t),freeBusyQuery:t.freeBusyQuery.bind(t),syncCollection:t.syncCollection.bind(t),supportedReportSet:t.supportedReportSet.bind(t),isCollectionDirty:t.isCollectionDirty.bind(t),smartCollectionSync:t.smartCollectionSync.bind(t),smartCollectionSyncDetailed:t.smartCollectionSyncDetailed.bind(t),fetchCalendars:t.fetchCalendars.bind(t),fetchCalendarUserAddresses:t.fetchCalendarUserAddresses.bind(t),fetchCalendarObjects:t.fetchCalendarObjects.bind(t),createCalendarObject:t.createCalendarObject.bind(t),updateCalendarObject:t.updateCalendarObject.bind(t),deleteCalendarObject:t.deleteCalendarObject.bind(t),syncCalendars:t.syncCalendars.bind(t),syncCalendarsDetailed:t.syncCalendarsDetailed.bind(t),fetchAddressBooks:t.fetchAddressBooks.bind(t),addressBookMultiGet:t.addressBookMultiGet.bind(t),fetchVCards:t.fetchVCards.bind(t),createVCard:t.createVCard.bind(t),updateVCard:t.updateVCard.bind(t),deleteVCard:t.deleteVCard.bind(t)}};var Ze=class{constructor(e){this.serverUrl=e.serverUrl,this.credentials=e.credentials,this.authMethod=e.authMethod??`Basic`,this.accountType=e.defaultAccountType??`caldav`,this.authFunction=e.authFunction,this.fetchOptions=e.fetchOptions??{},this.fetchOverride=e.fetch,this.calendarMultiGet=this.calendarMultiGet.bind(this),this.addressBookMultiGet=this.addressBookMultiGet.bind(this)}async authenticate(e=!1,t=this.fetchOptions,n=this.fetchOverride){if(!e&&this.authMethod!==`Oauth`)return;if(!e&&this.authHeaders&&this.credentials.accessToken&&(this.credentials.expiration==null||Date.now(){this.authHeaders=await Ye(this,t,n)};this.authentication=r();try{await this.authentication}finally{this.authentication=void 0}}async requestDefaults(e){return await this.authenticate(!1,e?.fetchOptions??this.fetchOptions,e?.fetch??this.fetchOverride),{url:this.serverUrl,headers:this.authHeaders,account:this.account,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}}async invoke(e,t){let n=await this.requestDefaults(t);return await He(e,n)(t)}async login(e){await this.authenticate(!0),this.account=this.accountType?await Z({account:{serverUrl:this.serverUrl,credentials:this.credentials,accountType:this.accountType},headers:this.authHeaders,loadCollections:e?.loadCollections,loadObjects:e?.loadObjects,fetchOptions:this.fetchOptions,fetch:this.fetchOverride}):void 0}async davRequest(e){let{init:t,fetchOptions:n,fetch:r,...i}=e,{headers:a,...o}=t,s=await this.requestDefaults(e);return k({...i,init:{...o,headers:O(s.headers,a)},fetchOptions:n??this.fetchOptions,fetch:r??this.fetchOverride})}async createObject(...e){return this.invoke(j,e[0])}async updateObject(...e){return this.invoke(M,e[0])}async deleteObject(...e){return this.invoke(N,e[0])}async propfind(...e){return this.invoke(A,e[0])}async createAccount(e){let{account:t,headers:n,headersToExclude:r,loadCollections:i,loadObjects:a,fetchOptions:o,fetch:s}=e,c=await this.requestDefaults(e),l=t.accountType??this.accountType;if(!l)throw Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient.");return Z({account:{serverUrl:this.serverUrl,credentials:this.credentials,...t,accountType:l},headers:O(c.headers,n),headersToExclude:r,loadCollections:i,loadObjects:a,fetchOptions:o??this.fetchOptions,fetch:s??this.fetchOverride})}async collectionQuery(...e){return this.invoke(I,e[0])}async makeCollection(...e){return this.invoke(fe,e[0])}async syncCollection(...e){return this.invoke(z,e[0])}async supportedReportSet(...e){return this.invoke(L,e[0])}async isCollectionDirty(...e){return this.invoke(R,e[0])}async smartCollectionSync(...e){return this.invoke(B,e[0])}async smartCollectionSyncDetailed(e){return this.invoke(pe,e)}async calendarQuery(...e){return this.invoke(G,e[0])}async makeCalendar(...e){return this.invoke(Ee,e[0])}async freeBusyQuery(...e){return this.invoke(Me,e[0])}async calendarMultiGet(...e){return this.invoke(K,e[0])}async fetchCalendars(...e){return this.invoke(q,e[0])}async fetchCalendarUserAddresses(...e){return this.invoke(Te,e[0])}async fetchCalendarObjects(...e){return this.invoke(J,e[0])}async createCalendarObject(...e){return this.invoke(De,e[0])}async updateCalendarObject(...e){return this.invoke(Oe,e[0])}async deleteCalendarObject(...e){return this.invoke(ke,e[0])}async syncCalendars(...e){return this.invoke(Ae,e[0])}async syncCalendarsDetailed(...e){return this.invoke(je,e[0])}async addressBookQuery(...e){return this.invoke(V,e[0])}async addressBookMultiGet(...e){return this.invoke(H,e[0])}async fetchAddressBooks(...e){return this.invoke(U,e[0])}async fetchVCards(...e){return this.invoke(W,e[0])}async createVCard(...e){return this.invoke(ge,e[0])}async updateVCard(...e){return this.invoke(_e,e[0])}async deleteVCard(...e){return this.invoke(ve,e[0])}},Qe={DAVNamespace:i,DAVNamespaceShort:o,DAVAttributeMap:a,ICALObjects:s,...Je,...te,...le,...Ne,...me,...ye,...Re,...y};export{a as DAVAttributeMap,Ze as DAVClient,i as DAVNamespace,o as DAVNamespaceShort,s as ICALObjects,H as addressBookMultiGet,V as addressBookQuery,K as calendarMultiGet,G as calendarQuery,E as cleanupFalsy,I as collectionQuery,Z as createAccount,De as createCalendarObject,Xe as createDAVClient,j as createObject,ge as createVCard,k as davRequest,Qe as default,ke as deleteCalendarObject,N as deleteObject,ve as deleteVCard,x as ensureTrailingSlash,D as excludeHeaders,U as fetchAddressBooks,J as fetchCalendarObjects,Te as fetchCalendarUserAddresses,q as fetchCalendars,Le as fetchHomeUrl,Ge as fetchOauthTokens,X as fetchPrincipalUrl,W as fetchVCards,Me as freeBusyQuery,Ue as getBasicAuthHeaders,We as getBearerAuthHeaders,T as getDAVAttribute,qe as getOauthHeaders,R as isCollectionDirty,Ee as makeCalendar,fe as makeCollection,O as mergeHeaders,A as propfind,Ke as refreshAccessToken,Fe as serviceDiscovery,B as smartCollectionSync,pe as smartCollectionSyncDetailed,L as supportedReportSet,Ae as syncCalendars,je as syncCalendarsDetailed,z as syncCollection,Oe as updateCalendarObject,M as updateObject,_e as updateVCard,C as urlContains,S as urlEquals,w as urlMatches}; \ No newline at end of file diff --git a/dist/tsdav.mjs b/dist/tsdav.mjs index 11e8c341..1011a86d 100644 --- a/dist/tsdav.mjs +++ b/dist/tsdav.mjs @@ -46,9 +46,6 @@ let ICALObjects = /* @__PURE__ */ function(ICALObjects) { return ICALObjects; }({}); //#endregion -//#region src/util/camelCase.ts -const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); -//#endregion //#region src/util/fetch.ts /** * Resolve the runtime `fetch` implementation. @@ -74,18 +71,175 @@ const resolveFetch = () => { }; const fetch = resolveFetch(); //#endregion -//#region src/util/nativeType.ts -const NUMERIC_RE = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; -const nativeType = (value) => { - if (typeof value !== "string") return value; - if (NUMERIC_RE.test(value)) { - const nValue = Number(value); - if (!Number.isNaN(nValue) && Number.isFinite(nValue) && (!Number.isInteger(nValue) || Number.isSafeInteger(nValue))) return nValue; - } - const bValue = value.toLowerCase(); - if (bValue === "true") return true; - if (bValue === "false") return false; - return value; +//#region src/util/camelCase.ts +const camelCase = (str) => str.replace(/[-_]+(\w?)/g, (_m, c) => c ? c.toUpperCase() : ""); +//#endregion +//#region src/util/typeHelpers.ts +function hasFields(obj, fields) { + if (!obj) return false; + const inObj = (object) => object != null && fields.every((f) => object[f]); + if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); + return inObj(obj); +} +const findMissingFieldNames = (obj, fields) => { + if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); + return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +}; +const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key); +//#endregion +//#region src/util/xml.ts +const davNamespaces = new Set(Object.values(DAVNamespace)); +const standardNamespaces = new Map([ + ...[ + "multistatus", + "response", + "propstat", + "prop", + "href", + "status", + "error", + "responsedescription", + "resourcetype", + "collection", + "getetag", + "displayname", + "syncToken", + "supportedReportSet", + "supportedReport", + "report", + "currentUserPrincipal" + ].map((name) => [name, "DAV:"]), + ...[ + "calendarData", + "calendar", + "calendarDescription", + "calendarTimezone", + "calendarHomeSet", + "calendarUserAddressSet", + "supportedCalendarComponentSet", + "comp" + ].map((name) => [name, "urn:ietf:params:xml:ns:caldav"]), + ...[ + "addressData", + "addressbook", + "addressbookHomeSet" + ].map((name) => [name, "urn:ietf:params:xml:ns:carddav"]), + ["getctag", "http://calendarserver.org/ns/"], + ["calendarColor", "http://apple.com/ns/ical/"] +]); +const normalizedKey = (name, namespace) => { + const localName = camelCase(name.replace(/^.*:/, "")); + const standardNamespace = standardNamespaces.get(localName); + return namespace && standardNamespace && namespace !== standardNamespace ? `{${namespace}}${localName}` : localName; +}; +const normalizeElement = (element, inheritedNamespaces) => { + const namespaceContext = Object.assign(Object.create(null), inheritedNamespaces); + const attributes = Object.create(null); + for (const [name, value] of Object.entries(element.attributes ?? {})) { + if (name === "xmlns" || name.startsWith("xmlns:")) namespaceContext[name === "xmlns" ? "" : name.slice(6)] = String(value); + if (name !== "xmlns") attributes[name] = value; + } + const name = element.name ?? ""; + const separator = name.indexOf(":"); + const namespace = namespaceContext[separator === -1 ? "" : name.slice(0, separator)] ?? ""; + const children = element.elements ?? []; + const elements = children.filter((child) => child.type === "element"); + const text = children.filter((child) => child.type === "text" || child.type === "cdata").map((child) => String(child.text ?? child.cdata ?? "")).join(""); + const value = {}; + const namespaces = Object.create(null); + if (Object.keys(attributes).length) value._attributes = attributes; + if (!elements.length) { + const hasText = children.some((child) => child.type === "text"); + const hasCdata = children.some((child) => child.type === "cdata"); + if (hasText) return { + value: text, + namespace, + namespaces + }; + if (hasCdata) value._cdata = text; + return { + value, + namespace, + namespaces + }; + } + if (text.trim()) value._text = text; + for (const child of elements) { + const normalized = normalizeElement(child, namespaceContext); + const localName = camelCase((child.name ?? "").replace(/^.*:/, "")); + let key = normalizedKey(child.name ?? "", normalized.namespace); + const previousNamespace = namespaces[key]; + if (hasOwn(value, key) && previousNamespace !== normalized.namespace) { + if (davNamespaces.has(normalized.namespace) && !davNamespaces.has(previousNamespace)) { + const previousKey = `{${previousNamespace}}${localName}`; + Object.defineProperty(value, previousKey, { + value: value[key], + enumerable: true, + configurable: true, + writable: true + }); + namespaces[previousKey] = previousNamespace; + delete value[key]; + } else key = `{${normalized.namespace}}${localName}`; + } + if (hasOwn(value, key)) { + if (Array.isArray(value[key])) value[key].push(normalized.value); + else value[key] = [value[key], normalized.value]; + } else Object.defineProperty(value, key, { + value: normalized.value, + enumerable: true, + configurable: true, + writable: true + }); + namespaces[key] = normalized.namespace; + if (localName === "prop" && normalized.namespace === "DAV:") value.propNamespaces = normalized.namespaces; + } + return { + value, + namespace, + namespaces + }; +}; +/** Preserve XML text order before normalizing DAV names to the existing compact shape. */ +const parseDAVXML = (xml) => { + const document = convert.xml2js(xml, { + compact: false, + ignoreDeclaration: true + }); + const result = {}; + for (const element of document.elements ?? []) { + if (element.type !== "element") continue; + const normalized = normalizeElement(element, {}); + const key = normalizedKey(element.name ?? "", normalized.namespace); + Object.defineProperty(result, key, { + value: normalized.value, + enumerable: true + }); + } + return result; +}; +/** Keep same-name properties from distinct namespaces across propstat groups. */ +const mergeDAVProps = (propStats) => { + const groups = /* @__PURE__ */ new Map(); + for (const stat of propStats) { + if (!stat.ok) continue; + for (const [name, value] of Object.entries(stat.props)) { + const namespaces = groups.get(name) ?? /* @__PURE__ */ new Map(); + namespaces.set(stat.namespaces?.[name] ?? "", value); + groups.set(name, namespaces); + } + } + const props = {}; + for (const [name, namespaces] of groups) { + const primary = [...namespaces.keys()].find((namespace) => davNamespaces.has(namespace)) ?? namespaces.keys().next().value; + for (const [namespace, value] of namespaces) Object.defineProperty(props, namespace === primary ? name : `{${namespace}}${name}`, { + value, + enumerable: true, + configurable: true, + writable: true + }); + } + return props; }; //#endregion //#region src/util/requestHelpers.ts @@ -205,7 +359,7 @@ var request_exports = /* @__PURE__ */ __exportAll({ }); const debug$5 = getLogger("tsdav:request"); const parseStatusLine = (statusLine) => { - const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine ?? ""); + const match = /^\S+\s+(?\d{3})(?:\s+(?.*))?$/.exec(statusLine?.trim() ?? ""); const status = match?.groups?.status; const statusText = match?.groups?.statusText; return status ? { @@ -214,7 +368,7 @@ const parseStatusLine = (statusLine) => { } : void 0; }; const davRequest = async (params) => { - const { url, init, convertIncoming = true, parseOutgoing = true, fetchOptions = {}, fetch: fetchOverride } = params; + const { url, init, convertIncoming = true, parseOutgoing = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const requestFetch = fetchOverride ?? fetch; const { headers = {}, body, namespace, method, attributes } = init; let processedBody = body; @@ -247,7 +401,7 @@ const davRequest = async (params) => { }) : body; const fetchOptionsWithoutHeaders = { ...fetchOptions }; delete fetchOptionsWithoutHeaders.headers; - const mergedHeaders = mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers); + const mergedHeaders = excludeHeaders(mergeHeaders({ "Content-Type": "text/xml;charset=UTF-8" }, cleanupFalsy(headers), fetchOptions.headers), headersToExclude); const davResponse = await requestFetch(url, { ...fetchOptionsWithoutHeaders, headers: mergedHeaders, @@ -255,54 +409,25 @@ const davRequest = async (params) => { method }); const resText = await davResponse.text(); - if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) { - const MAX_RAW = 4096; - const raw = resText.length > MAX_RAW ? `${resText.slice(0, MAX_RAW)}…` : resText; - return [{ - href: davResponse.url, - ok: davResponse.ok, - status: davResponse.status, - statusText: davResponse.statusText, - raw - }]; - } + if (!davResponse.ok || !davResponse.headers.get("content-type")?.toLowerCase().includes("xml") || !parseOutgoing || !resText) return [{ + href: davResponse.url, + ok: davResponse.ok, + status: davResponse.status, + statusText: davResponse.statusText, + raw: resText + }]; let result; try { - result = convert.xml2js(resText, { - compact: true, - trim: true, - textFn: (value, parentElement) => { - try { - const parentOfParent = parentElement._parent; - const pOpKeys = Object.keys(parentOfParent); - const keyName = pOpKeys[pOpKeys.length - 1]; - if (!keyName) return; - const arrOfKey = parentOfParent[keyName]; - if (arrOfKey.length > 0) { - const arr = arrOfKey; - const arrIndex = arrOfKey.length - 1; - arr[arrIndex] = nativeType(value); - } else parentOfParent[keyName] = nativeType(value); - } catch (e) { - debug$5(e.stack); - } - }, - elementNameFn: (attributeName) => camelCase(attributeName.replace(/^.+:/, "")), - attributesFn: (value) => { - const newVal = { ...value }; - delete newVal.xmlns; - return newVal; - }, - ignoreDeclaration: true - }); + result = parseDAVXML(resText); } catch (e) { debug$5(`Failed to parse DAV response XML: ${e.message}`); return [{ href: davResponse.url, - ok: davResponse.ok, + ok: false, status: davResponse.status, statusText: davResponse.statusText, - raw: resText + raw: resText, + parseError: e.message }]; } if (!result?.multistatus) return [{ @@ -319,7 +444,21 @@ const davRequest = async (params) => { statusText: davResponse.statusText, ok: davResponse.ok }; - const parsedStatus = parseStatusLine(responseBody.status); + const propStats = (Array.isArray(responseBody.propstat) ? responseBody.propstat : responseBody.propstat ? [responseBody.propstat] : []).map((stat) => { + const parsed = parseStatusLine(stat.status); + const status = parsed?.status ?? 0; + return { + props: stat.prop ?? {}, + namespaces: stat.propNamespaces, + status, + statusText: parsed?.statusText ?? "Invalid DAV property status", + ok: status >= 200 && status < 300, + error: stat.error, + responsedescription: stat.responsedescription + }; + }); + const failedStatus = propStats.length > 0 && propStats.every((stat) => !stat.ok) ? propStats[0] : void 0; + const parsedStatus = parseStatusLine(responseBody.status) ?? failedStatus; const status = parsedStatus?.status ?? davResponse.status; return { raw: result, @@ -329,14 +468,8 @@ const davRequest = async (params) => { ok: status >= 200 && status < 300, error: responseBody.error, responsedescription: responseBody.responsedescription, - props: (Array.isArray(responseBody.propstat) ? responseBody.propstat : [responseBody.propstat]).reduce((prev, curr) => { - const propstatStatus = parseStatusLine(curr?.status)?.status; - if (propstatStatus && (propstatStatus < 200 || propstatStatus >= 300)) return prev; - return { - ...prev, - ...curr?.prop - }; - }, {}) + propStats, + props: mergeDAVProps(propStats) }; }); }; @@ -362,6 +495,7 @@ const propfind = async (params) => { prop: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -405,16 +539,76 @@ const deleteObject = async (params) => { }); }; //#endregion -//#region src/util/typeHelpers.ts -function hasFields(obj, fields) { - if (!obj) return false; - const inObj = (object) => object != null && fields.every((f) => object[f]); - if (Array.isArray(obj)) return obj.length > 0 && obj.every((o) => inObj(o)); - return inObj(obj); -} -const findMissingFieldNames = (obj, fields) => { - if (!obj || typeof obj !== "object") return fields.map((f) => f.toString()).join(","); - return fields.reduce((prev, curr) => obj[curr] ? prev : `${prev.length ? `${prev},` : ""}${curr.toString()}`, ""); +//#region src/util/syncHelpers.ts +/** Stable keys for linear-time DAV resource comparisons. */ +const getDAVUrlKey = (url, baseUrl) => { + let resolved; + try { + resolved = new URL(url, ensureTrailingSlash(baseUrl)).href; + } catch { + resolved = url.trim(); + } + return resolved.endsWith("/") ? resolved.slice(0, -1) : resolved; +}; +const diffDAVObjects = (local, remote, baseUrl, incremental = false, deletedObjects = []) => { + const localByUrl = new Map(local.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const remoteByUrl = new Map(remote.map((object) => [getDAVUrlKey(object.url, baseUrl), object])); + const deleted = incremental ? deletedObjects : local.filter((object) => !remoteByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const deletedUrls = new Set(deleted.map((object) => getDAVUrlKey(object.url, baseUrl))); + const created = remote.filter((object) => !localByUrl.has(getDAVUrlKey(object.url, baseUrl))); + const updated = []; + const unchanged = []; + for (const object of local) { + const key = getDAVUrlKey(object.url, baseUrl); + if (deletedUrls.has(key)) continue; + const found = remoteByUrl.get(key); + if (found && found.etag !== object.etag) updated.push(found); + else if (found || incremental) unchanged.push(object); + } + return { + created, + updated, + deleted, + unchanged + }; +}; +//#endregion +//#region src/util/responseHelpers.ts +const assertDAVResponses = (responses, context) => { + const failed = responses.find((response) => !response.ok || response.status >= 400); + if (failed) throw new Error(`${context}: ${failed.status} ${failed.statusText}`); +}; +const assertDAVDiscovery = (responses, context) => { + assertDAVResponses(responses, context); + for (const response of responses) { + if (response.raw?.multistatus && !response.raw.multistatus.response) continue; + assertDAVProperty(response, "resourcetype", context); + if (!response.props || !hasOwn(response.props, "resourcetype") || typeof response.href !== "string" || !response.href) throw new Error(`${context}: missing resourcetype or href in DAV multistatus response`); + } +}; +const assertDAVProperty = (response, name, context) => { + const failed = response.propStats?.find((stat) => !stat.ok && hasOwn(stat.props, name)); + if (failed && !hasOwn(response.props ?? {}, name)) throw new Error(`${context}: ${name} returned ${failed.status} ${failed.statusText}`); +}; +const assertDAVObjectResponses = (responses, property, objectUrls, baseUrl, context) => { + assertDAVResponses(responses, context); + const fetchedUrls = /* @__PURE__ */ new Set(); + for (const response of responses) { + assertDAVProperty(response, property, context); + if (!response.href || typeof (response.props?.[property]?._cdata ?? response.props?.[property]) !== "string") throw new Error(`${context}: missing ${property} or href`); + fetchedUrls.add(getDAVUrlKey(response.href, baseUrl)); + } + const withoutQuery = (url) => getDAVUrlKey(url.replace(/\?.*$/, ""), baseUrl); + const countsByPath = /* @__PURE__ */ new Map(); + for (const url of objectUrls) { + const key = withoutQuery(url); + countsByPath.set(key, (countsByPath.get(key) ?? 0) + 1); + } + if (objectUrls.some((url) => !fetchedUrls.has(getDAVUrlKey(url, baseUrl)) && !(countsByPath.get(withoutQuery(url)) === 1 && fetchedUrls.has(withoutQuery(url))))) throw new Error(`${context}: incomplete response`); +}; +const getDAVText = (value) => { + const text = typeof value === "string" || typeof value === "number" ? String(value) : value?._cdata ?? value?._text; + return typeof text === "string" ? text : void 0; }; //#endregion //#region src/collection.ts @@ -435,13 +629,6 @@ const resolveDAVHref = (href, baseUrl) => { return href; } }; -const hrefHasExtension = (href, extension, baseUrl) => { - try { - return new URL(href, ensureTrailingSlash(baseUrl)).pathname.toLowerCase().endsWith(extension); - } catch { - return (href.split(/[?#]/, 1)[0] ?? "").toLowerCase().endsWith(extension); - } -}; const collectionQuery = async (params) => { const { url, body, depth, defaultNamespace = "d", headers, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; const queryResults = await davRequest({ @@ -455,13 +642,16 @@ const collectionQuery = async (params) => { namespace: defaultNamespace, body }, + headersToExclude, fetchOptions, fetch: fetchOverride }); const emptyNotFound = queryResults[0]; if (defaultNamespace === "c" && body?.["calendar-query"] != null && queryResults.length === 1 && emptyNotFound && emptyNotFound.status === 404 && urlMatches(url, emptyNotFound.href, url) && !emptyNotFound.error && Object.keys(emptyNotFound.props ?? {}).length === 0 && typeof emptyNotFound.raw === "object" && emptyNotFound.raw !== null && emptyNotFound.raw.multistatus?.response?.propstat == null) return []; const errorResponse = queryResults.find((res) => !res.ok || res.status && res.status >= 400); - if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${errorResponse.raw ? `Raw response: ${errorResponse.raw}` : ""}`); + if (errorResponse) throw new Error(`Collection query failed: ${errorResponse.status} ${errorResponse.statusText}. ${typeof errorResponse.raw === "string" ? `Raw response: ${errorResponse.raw.slice(0, 4096)}` : ""}`); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => !response.raw?.multistatus)) throw new Error("Collection query failed: expected a DAV multistatus response"); + if ((body?.["calendar-query"] || body?.["calendar-multiget"] || body?.["addressbook-query"] || body?.["addressbook-multiget"]) && queryResults.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection query failed: missing href in DAV response"); const firstQueryResult = queryResults[0]; if (queryResults.length === 1 && firstQueryResult && (!firstQueryResult.raw || firstQueryResult.raw.multistatus && !firstQueryResult.raw.multistatus.response) && firstQueryResult.status && firstQueryResult.status < 300) return []; return queryResults; @@ -488,6 +678,7 @@ const makeCollection = async (params) => { set: { prop: props } } } : void 0 }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -499,6 +690,7 @@ const supportedReportSet = async (params) => { props: { [`d:supported-report-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]?.props?.supportedReportSet?.supportedReport; @@ -512,15 +704,17 @@ const isCollectionDirty = async (params) => { props: { [`cs:getctag`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(collection.url, r.href, collection.url)); if (!res) throw new Error("Collection does not exist on server"); - if (!res.ok) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); - const remoteCtag = res.props?.getctag; + const unavailableCtag = res.propStats?.length && res.propStats.every((stat) => stat.status === 404 && hasOwn(stat.props, "getctag")); + if (!res.ok && !unavailableCtag) throw new Error(`Collection status check failed: ${res.status} ${res.statusText}`); + const remoteCtag = getDAVText(res.props?.getctag); return { isDirty: collection.ctag == null || remoteCtag == null || `${collection.ctag}` !== `${remoteCtag}`, - newCtag: remoteCtag?.toString() + newCtag: remoteCtag }; }; /** @@ -545,6 +739,7 @@ const syncCollection = (params) => { [`d:prop`]: props } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -570,15 +765,17 @@ const smartCollectionSync = async (params) => { syncLevel: 1, syncToken: collection.syncToken, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); const isObjectResponse = (r) => { - const extName = account.accountType === "caldav" ? ".ics" : ".vcf"; - return typeof r.href === "string" && hrefHasExtension(r.href, extName, collection.url); + return typeof r.href === "string" && getDAVUrlKey(r.href, collection.url) !== getDAVUrlKey(collection.url, collection.url) && !r.props?.resourcetype?.collection; }; - const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && isObjectResponse(r))); + const errorResponse = result.find((r) => (!r.ok || r.status >= 400) && !(r.status === 404 && !r.propStats?.length && isObjectResponse(r))); if (errorResponse) throw new Error(`Collection sync failed: ${errorResponse.status} ${errorResponse.statusText}`); + if (result.some((response) => response.raw && !response.raw.multistatus)) throw new Error("Collection sync failed: expected a DAV multistatus response"); + if (result.some((response) => response.raw?.multistatus?.response && (typeof response.href !== "string" || !response.href))) throw new Error("Collection sync failed: missing href in DAV response"); const objectResponses = result.filter(isObjectResponse); const changedObjectUrls = objectResponses.filter((o) => o.status !== 404).map((r) => r.href); const deletedObjectUrls = objectResponses.filter((o) => o.status === 404).map((r) => r.href); @@ -593,30 +790,24 @@ const smartCollectionSync = async (params) => { objectUrls: changedObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? [] : []; - const multiGetError = multiGetObjectResponse.find((r) => !r.ok || r.status >= 400); - if (multiGetError) throw new Error(`Collection sync multi-get failed: ${multiGetError.status} ${multiGetError.statusText}`); + assertDAVObjectResponses(multiGetObjectResponse, account.accountType === "caldav" ? "calendarData" : "addressData", changedObjectUrls, collection.url, "Collection sync multi-get failed"); const remoteObjects = multiGetObjectResponse.map((res) => { return { url: resolveDAVHref(res.href ?? "", collection.url), - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: account?.accountType === "caldav" ? res.props?.calendarData?._cdata ?? res.props?.calendarData : res.props?.addressData?._cdata ?? res.props?.addressData }; }); const localObjects = collection.objects ?? []; - const created = remoteObjects.filter((o) => localObjects.every((lo) => !urlMatches(lo.url, o.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = deletedObjectUrls.map((o) => ({ - url: resolveDAVHref(o, collection.url), + const deletedObjects = deletedObjectUrls.map((url) => ({ + url: resolveDAVHref(url, collection.url), etag: "" })); - const unchanged = localObjects.filter((localObject) => deleted.every((deletedObject) => !urlMatches(localObject.url, deletedObject.url, collection.url)) && updated.every((updatedObject) => !urlMatches(localObject.url, updatedObject.url, collection.url))); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url, true, deletedObjects); return { ...collection, objects: detailedResult ? { @@ -628,13 +819,14 @@ const smartCollectionSync = async (params) => { ...created, ...updated ], - syncToken: result[0]?.raw?.multistatus?.syncToken ?? collection.syncToken + syncToken: getDAVText(result[0]?.raw?.multistatus?.syncToken) ?? collection.syncToken }; } if (syncMethod === "basic") { const { isDirty, newCtag } = await isCollectionDirty({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -651,17 +843,11 @@ const smartCollectionSync = async (params) => { const remoteObjects = await collection.fetchObjects({ collection, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) ?? []; - const created = remoteObjects.filter((ro) => localObjects.every((lo) => !urlMatches(lo.url, ro.url, collection.url))); - const updated = localObjects.reduce((prev, curr) => { - const found = remoteObjects.find((ro) => urlMatches(ro.url, curr.url, collection.url)); - if (found && found.etag && found.etag !== curr.etag) return [...prev, found]; - return prev; - }, []); - const deleted = localObjects.filter((cal) => remoteObjects.every((ro) => !urlMatches(ro.url, cal.url, collection.url))); - const unchanged = localObjects.filter((lo) => remoteObjects.some((ro) => urlMatches(lo.url, ro.url, collection.url) && ro.etag === lo.etag)); + const { created, updated, deleted, unchanged } = diffDAVObjects(localObjects, remoteObjects, collection.url); return { ...collection, objects: detailedResult ? { @@ -713,6 +899,7 @@ const addressBookQuery = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -729,6 +916,7 @@ const addressBookMultiGet = async (params) => { defaultNamespace: "card", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -742,33 +930,39 @@ const fetchAddressBooks = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`d:displayname`]: {}, - [`cs:getctag`]: {}, - [`d:resourcetype`]: {}, - [`d:sync-token`]: {} + props: { + ...customProps ?? { + [`d:displayname`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`d:sync-token`]: {} + }, + [`d:resourcetype`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Address book discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("addressbook")).map((rs) => { const displayName = rs.props?.displayname?._cdata ?? rs.props?.displayname; debug$3(`Found address book named ${typeof displayName === "string" ? displayName : ""}, props: ${JSON.stringify(rs.props)}`); return { url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), displayName: typeof displayName === "string" ? displayName : "", resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken + syncToken: getDAVText(rs.props?.syncToken) }; }).map(async (addr) => ({ ...addr, reports: await supportedReportSet({ collection: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -787,12 +981,14 @@ const fetchVCards = async (params) => { props: { [`d:getetag`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).map((res) => res.href ?? "")).filter((url) => typeof url === "string" && url.trim().length > 0).map((url) => url.startsWith("http") ? url : new URL(url, ensureTrailingSlash(addressBook.url)).href).filter((url) => !urlEquals(url, addressBook.url)).filter(urlFilter).map((url) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(vcardUrls.map((url) => getDAVUrlKey(url, addressBook.url))); let vCardResults = []; if (vcardUrls.length > 0) { if (useMultiGet) vCardResults = await addressBookMultiGet({ @@ -804,24 +1000,30 @@ const fetchVCards = async (params) => { objectUrls: vcardUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - else vCardResults = await addressBookQuery({ - url: addressBook.url, - props: { - [`d:getetag`]: {}, - [`card:address-data`]: {} - }, - depth: "1", - headers: excludeHeaders(headers, headersToExclude), - fetchOptions, - fetch: fetchOverride - }); + else { + vCardResults = await addressBookQuery({ + url: addressBook.url, + props: { + [`d:getetag`]: {}, + [`card:address-data`]: {} + }, + depth: "1", + headers: excludeHeaders(headers, headersToExclude), + headersToExclude, + fetchOptions, + fetch: fetchOverride + }); + vCardResults = vCardResults.filter((res) => !!res.href && targetUrls.has(getDAVUrlKey(res.href, addressBook.url))); + } } + assertDAVObjectResponses(vCardResults, "addressData", vcardUrls, addressBook.url, "VCard fetch failed"); return vCardResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(addressBook.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.addressData?._cdata ?? res.props?.addressData })); }; @@ -835,6 +1037,7 @@ const createVCard = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -849,6 +1052,7 @@ const updateVCard = async (params) => { "content-type": "text/vcard; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -859,6 +1063,7 @@ const deleteVCard = async (params) => { url: vCard.url, etag: vCard.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -908,6 +1113,7 @@ const fetchCalendarUserAddresses = async (params) => { props: { [`c:calendar-user-address-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride })).find((r) => urlMatches(account.principalUrl, r.href, account.rootUrl)); @@ -938,6 +1144,7 @@ const calendarQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -956,6 +1163,7 @@ const calendarMultiGet = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -980,6 +1188,7 @@ const makeCalendar = async (params) => { set: { prop: props } } } }, + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -993,21 +1202,27 @@ const fetchCalendars = async (params) => { } const res = await propfind({ url: account.homeUrl, - props: customProps ?? { - [`c:calendar-description`]: {}, - [`c:calendar-timezone`]: {}, - [`d:displayname`]: {}, - [`ca:calendar-color`]: {}, - [`cs:getctag`]: {}, + props: { + ...customProps ?? { + [`c:calendar-description`]: {}, + [`c:calendar-timezone`]: {}, + [`d:displayname`]: {}, + [`ca:calendar-color`]: {}, + [`cs:getctag`]: {}, + [`d:resourcetype`]: {}, + [`c:supported-calendar-component-set`]: {}, + [`d:sync-token`]: {} + }, [`d:resourcetype`]: {}, - [`c:supported-calendar-component-set`]: {}, - [`d:sync-token`]: {} + [`c:supported-calendar-component-set`]: {} }, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); + assertDAVDiscovery(res, "Calendar discovery failed"); return Promise.all(res.filter((r) => Object.keys(r.props?.resourcetype ?? {}).includes("calendar")).filter((rc) => { const components = extractComponentNames(rc.props?.supportedCalendarComponentSet?.comp); return components.length === 0 || components.some((c) => Object.values(ICALObjects).includes(c)); @@ -1020,12 +1235,12 @@ const fetchCalendars = async (params) => { description: typeof description === "string" ? description : "", timezone: typeof timezone === "string" ? timezone : "", url: new URL(rs.href ?? "", ensureTrailingSlash(account.rootUrl ?? "")).href, - ctag: rs.props?.getctag, + ctag: getDAVText(rs.props?.getctag), calendarColor: rs.props?.calendarColor, - displayName: rs.props?.displayname?._cdata ?? rs.props?.displayname, + displayName: getDAVText(rs.props?.displayname), components: extractComponentNames(compSet), resourcetype: Object.keys(rs.props?.resourcetype ?? {}), - syncToken: rs.props?.syncToken, + syncToken: getDAVText(rs.props?.syncToken), ...projectedProps && projectedEntries.length > 0 ? { projectedProps: Object.fromEntries(projectedEntries) } : {} }; }).map(async (cal) => ({ @@ -1033,6 +1248,7 @@ const fetchCalendars = async (params) => { reports: await supportedReportSet({ collection: cal, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1040,6 +1256,7 @@ const fetchCalendars = async (params) => { }; const fetchCalendarObjects = async (params) => { const { calendar, objectUrls, filters: customFilters, timeRange, headers, expand, urlFilter = (url) => Boolean(url?.includes(".ics")), useMultiGet = true, headersToExclude, fetchOptions = {}, fetch: fetchOverride } = params; + if (expand && !timeRange) throw new Error("timeRange is required when expand is true"); if (timeRange) validateTimeRange(timeRange); debug$2(`Fetching calendar objects from ${calendar?.url}`); const requiredFields = ["url"]; @@ -1070,6 +1287,7 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1077,6 +1295,7 @@ const fetchCalendarObjects = async (params) => { const parsedUrl = new URL(url); return `${parsedUrl.pathname}${parsedUrl.search}`; }); + const targetUrls = new Set(calendarObjectUrls.map((url) => getDAVUrlKey(url, calendar.url))); let calendarObjectResults = []; if (calendarObjectUrls.length > 0) { if (expand && !objectUrls) calendarObjectResults = initialResponses.filter((res) => { @@ -1096,12 +1315,13 @@ const fetchCalendarObjects = async (params) => { filters, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - if (objectUrls && objectUrls.length > 0) calendarObjectResults = calendarObjectResults.filter((res) => { + calendarObjectResults = calendarObjectResults.filter((res) => { const fullUrl = (res.href ?? "").startsWith("http") ? res.href ?? "" : new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href; - return calendarObjectUrls.some((target) => urlMatches(fullUrl, target, calendar.url)); + return targetUrls.has(getDAVUrlKey(fullUrl, calendar.url)); }); } else calendarObjectResults = await calendarMultiGet({ url: calendar.url, @@ -1115,13 +1335,15 @@ const fetchCalendarObjects = async (params) => { objectUrls: calendarObjectUrls, depth: "1", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); } + assertDAVObjectResponses(calendarObjectResults, "calendarData", calendarObjectUrls, calendar.url, "Calendar object fetch failed"); return calendarObjectResults.map((res) => ({ url: new URL(res.href ?? "", ensureTrailingSlash(calendar.url)).href, - etag: res.props?.getetag == null ? void 0 : String(res.props.getetag), + etag: getDAVText(res.props?.getetag), data: res.props?.calendarData?._cdata ?? res.props?.calendarData })); }; @@ -1135,6 +1357,7 @@ const createCalendarObject = async (params) => { "If-None-Match": "*", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1149,6 +1372,7 @@ const updateCalendarObject = async (params) => { "content-type": "text/calendar; charset=utf-8", ...headers }, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1159,6 +1383,7 @@ const deleteCalendarObject = async (params) => { url: calendarObject.url, etag: calendarObject.etag, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1173,19 +1398,26 @@ const syncCalendars = async (params) => { const remoteCalendars = await fetchCalendars({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); - const created = remoteCalendars.filter((rc) => localCalendars.every((lc) => !urlMatches(lc.url, rc.url, account.rootUrl))); - debug$2(`new calendars: ${created.map((cc) => cc.displayName)}`); - const updated = localCalendars.reduce((prev, curr) => { - const found = remoteCalendars.find((rc) => urlMatches(rc.url, curr.url, account.rootUrl)); - if (found && (found.syncToken && `${found.syncToken}` !== `${curr.syncToken}` || found.ctag && `${found.ctag}` !== `${curr.ctag}`)) return [...prev, { - local: curr, - remote: found - }]; - return prev; - }, []); + const baseUrl = account.rootUrl ?? account.homeUrl ?? account.serverUrl; + const localByUrl = new Map(localCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const remoteByUrl = new Map(remoteCalendars.map((cal) => [getDAVUrlKey(cal.url, baseUrl), cal])); + const created = remoteCalendars.filter((cal) => !localByUrl.has(getDAVUrlKey(cal.url, baseUrl))); + const updated = []; + const unchanged = []; + const deleted = []; + for (const local of localCalendars) { + const remote = remoteByUrl.get(getDAVUrlKey(local.url, baseUrl)); + if (!remote) deleted.push(local); + else if (!remote.syncToken && !remote.ctag || remote.syncToken && remote.syncToken !== local.syncToken || remote.ctag && remote.ctag !== local.ctag) updated.push({ + local, + remote + }); + else unchanged.push(local); + } debug$2(`updated calendars: ${updated.map(({ remote }) => remote.displayName)}`); const updatedWithObjects = await Promise.all(updated.map(async ({ local, remote }) => { const fetchObjects = async (fetchParams) => { @@ -1193,7 +1425,9 @@ const syncCalendars = async (params) => { const { collection, ...requestParams } = fetchParams; return fetchCalendarObjects({ ...requestParams, - calendar: collection + calendar: collection, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => getDAVUrlKey(url, collection.url) !== getDAVUrlKey(collection.url, collection.url) }); }; const collection = { @@ -1208,25 +1442,17 @@ const syncCalendars = async (params) => { collection, detailedResult: false, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account, fetchOptions, fetch: fetchOverride }); return { ...result, - ctag: remote.ctag ?? result.ctag, - syncToken: remote.syncToken ?? result.syncToken + ctag: remote.reports?.includes("syncCollection") ? remote.ctag ?? result.ctag : result.ctag ?? remote.ctag, + syncToken: remote.reports?.includes("syncCollection") ? result.syncToken : remote.syncToken ?? result.syncToken }; })); - const deleted = localCalendars.filter((cal) => remoteCalendars.every((rc) => !urlMatches(rc.url, cal.url, account.rootUrl))); - debug$2(`deleted calendars: ${deleted.map((cc) => cc.displayName)}`); - const unchanged = localCalendars.filter((cal) => remoteCalendars.some((rc) => { - if (!urlMatches(rc.url, cal.url, account.rootUrl)) return false; - const syncTokenMatches = !rc.syncToken || `${rc.syncToken}` === `${cal.syncToken}`; - const ctagMatches = !rc.ctag || `${rc.ctag}` === `${cal.ctag}`; - return syncTokenMatches && ctagMatches; - })); - debug$2(`unchanged calendars: ${unchanged.map((cc) => cc.displayName)}`); return detailedResult ? { created, updated: updatedWithObjects, @@ -1257,6 +1483,7 @@ const freeBusyQuery = async (params) => { defaultNamespace: "c", depth, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }))[0]; @@ -1293,11 +1520,7 @@ const serviceDiscovery = async (params) => { const location = response.headers.get("Location"); if (typeof location === "string" && location.length) { debug$1(`Service discovery redirected to ${location}`); - const hasExplicitScheme = /^[a-z][a-z0-9+.-]*:/i.test(location); - const serviceURL = new URL(location, endpoint); - if (serviceURL.hostname === uri.hostname && uri.port && !serviceURL.port) serviceURL.port = uri.port; - if (!hasExplicitScheme) serviceURL.protocol = endpoint.protocol ?? "http"; - return serviceURL.href; + return new URL(location, uri).href; } } }; @@ -1322,6 +1545,7 @@ const serviceDiscovery = async (params) => { const redirectUrl = extractRedirect(await requestFetch(uri.href, { ...fetchOptionsWithoutHeaders, method: "GET", + body: void 0, headers: excludeHeaders(mergeHeaders(headers, fetchHeaders), headersToExclude), redirect: "manual" })); @@ -1361,6 +1585,7 @@ const fetchPrincipalUrl = async (params) => { props: { [`d:current-user-principal`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1387,6 +1612,7 @@ const fetchHomeUrl = async (params) => { props: account.accountType === "caldav" ? { [`c:calendar-home-set`]: {} } : { [`card:addressbook-home-set`]: {} }, depth: "0", headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1410,6 +1636,7 @@ const createAccount = async (params) => { const discoveredRootUrl = account.rootUrl ?? await serviceDiscovery({ account, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); @@ -1428,6 +1655,7 @@ const createAccount = async (params) => { rootUrl }, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1443,24 +1671,28 @@ const createAccount = async (params) => { newAccount.principalUrl = account.principalUrl ?? newAccount.principalUrl ?? await fetchPrincipalUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); newAccount.homeUrl = account.homeUrl ?? await fetchHomeUrl({ account: newAccount, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }); if (loadCollections || loadObjects) { if (account.accountType === "caldav") newAccount.calendars = await fetchCalendars({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride }); else if (account.accountType === "carddav") newAccount.addressBooks = await fetchAddressBooks({ headers: excludeHeaders(headers, headersToExclude), + headersToExclude, account: newAccount, fetchOptions, fetch: fetchOverride @@ -1471,7 +1703,10 @@ const createAccount = async (params) => { ...cal, objects: await fetchCalendarObjects({ calendar: cal, + filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }, + urlFilter: (url) => !urlEquals(url, cal.url), headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1481,6 +1716,7 @@ const createAccount = async (params) => { objects: await fetchVCards({ addressBook: addr, headers: excludeHeaders(headers, headersToExclude), + headersToExclude, fetchOptions, fetch: fetchOverride }) @@ -1631,20 +1867,18 @@ const getOauthHeaders = async (credentials, fetchOptions, fetchOverride) => { debug("Fetching oauth headers"); let tokens = {}; let didRefresh = false; - if (!credentials.refreshToken) { - tokens = await fetchOauthTokens(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else if (credentials.refreshToken && !credentials.accessToken || Date.now() > (credentials.expiration ?? 0)) { - tokens = await refreshAccessToken(credentials, fetchOptions, fetchOverride); - didRefresh = true; - } else tokens = { + if (credentials.accessToken && (credentials.expiration == null && !credentials.refreshToken || credentials.expiration != null && Date.now() < credentials.expiration)) tokens = { access_token: credentials.accessToken, refresh_token: credentials.refreshToken }; + else { + tokens = credentials.refreshToken ? await refreshAccessToken(credentials, fetchOptions, fetchOverride) : await fetchOauthTokens(credentials, fetchOptions, fetchOverride); + didRefresh = true; + } if (didRefresh) { if (tokens.access_token) credentials.accessToken = tokens.access_token; if (tokens.refresh_token) credentials.refreshToken = tokens.refresh_token; - if (typeof tokens.expires_in === "number") credentials.expiration = Date.now() + tokens.expires_in * 1e3; + if (tokens.access_token) credentials.expiration = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0; } debug("Oauth tokens obtained"); return { @@ -1658,145 +1892,71 @@ var client_exports = /* @__PURE__ */ __exportAll({ DAVClient: () => DAVClient, createDAVClient: () => createDAVClient }); -const createDAVClient = async (params) => { - const { serverUrl, credentials, authMethod = "Basic", defaultAccountType, authFunction, fetchOptions: defaultFetchOptions, fetch: fetchOverride } = params; - let authHeaders = {}; - switch (authMethod) { - case "Basic": - authHeaders = getBasicAuthHeaders(credentials); - break; - case "Bearer": - authHeaders = getBearerAuthHeaders(credentials); - break; - case "Oauth": - authHeaders = (await getOauthHeaders(credentials, defaultFetchOptions, fetchOverride)).headers; - break; - case "Digest": - authHeaders = { Authorization: `Digest ${credentials.digestString}` }; - break; +const resolveAuthHeaders = async (client, fetchOptions = client.fetchOptions, fetchOverride = client.fetchOverride) => { + switch (client.authMethod) { + case "Basic": return getBasicAuthHeaders(client.credentials); + case "Bearer": return getBearerAuthHeaders(client.credentials); + case "Oauth": { + const { headers } = await getOauthHeaders(client.credentials, fetchOptions, fetchOverride); + if (!headers.authorization) throw new Error("OAuth authentication failed: token endpoint returned no access token"); + return headers; + } + case "Digest": return { Authorization: `Digest ${client.credentials.digestString}` }; case "Custom": - if (!authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - authHeaders = await authFunction(credentials) ?? {}; - break; + if (!client.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); + return await client.authFunction(client.credentials) ?? {}; default: throw new Error("Invalid auth method"); } - const defaultAccount = defaultAccountType ? await createAccount({ +}; +const createDAVClient = async (params) => { + const client = new DAVClient(params); + client.authHeaders = await resolveAuthHeaders(client); + client.account = params.defaultAccountType ? await createAccount({ account: { - serverUrl, - credentials, - accountType: defaultAccountType + serverUrl: params.serverUrl, + credentials: params.credentials, + accountType: params.defaultAccountType }, - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride + headers: client.authHeaders, + fetchOptions: client.fetchOptions, + fetch: client.fetchOverride }) : void 0; - const davRequest$1 = async (params0) => { - const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; - const { headers, ...restInit } = init; - return davRequest({ - ...rest, - init: { - ...restInit, - headers: { - ...authHeaders, - ...headers - } - }, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const commonDefaults = { - headers: authHeaders, - fetchOptions: defaultFetchOptions, - fetch: fetchOverride - }; - const commonDefaultsWithUrl = { - url: serverUrl, - ...commonDefaults - }; - const commonDefaultsWithAccount = { - account: defaultAccount, - ...commonDefaults - }; - const createObject$1 = defaultParam(createObject, commonDefaultsWithUrl); - const updateObject$1 = defaultParam(updateObject, commonDefaultsWithUrl); - const deleteObject$1 = defaultParam(deleteObject, commonDefaultsWithUrl); - const propfind$1 = defaultParam(propfind, commonDefaults); - const createAccount$1 = async (params0) => { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch: fetchOverride2 } = params0; - const merged = { - serverUrl, - credentials, - ...account - }; - if (!merged.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or set `defaultAccountType` on the client."); - return createAccount({ - account: merged, - headers: { - ...authHeaders, - ...headers - }, - loadCollections, - loadObjects, - fetchOptions: fetchOptions ?? defaultFetchOptions, - fetch: fetchOverride2 ?? fetchOverride - }); - }; - const collectionQuery$1 = defaultParam(collectionQuery, commonDefaults); - const makeCollection$1 = defaultParam(makeCollection, commonDefaults); - const syncCollection$1 = defaultParam(syncCollection, commonDefaults); - const supportedReportSet$1 = defaultParam(supportedReportSet, commonDefaults); - const isCollectionDirty$1 = defaultParam(isCollectionDirty, commonDefaults); - const smartCollectionSync$1 = defaultParam(smartCollectionSync, commonDefaultsWithAccount); - const smartCollectionSyncDetailed$1 = defaultParam(smartCollectionSyncDetailed, commonDefaultsWithAccount); - const calendarQuery$1 = defaultParam(calendarQuery, commonDefaults); - const calendarMultiGet$1 = defaultParam(calendarMultiGet, commonDefaults); - const makeCalendar$1 = defaultParam(makeCalendar, commonDefaults); - const fetchCalendars$1 = defaultParam(fetchCalendars, commonDefaultsWithAccount); - const fetchCalendarUserAddresses$1 = defaultParam(fetchCalendarUserAddresses, commonDefaultsWithAccount); - const fetchCalendarObjects$1 = defaultParam(fetchCalendarObjects, commonDefaults); - const createCalendarObject$1 = defaultParam(createCalendarObject, commonDefaults); - const updateCalendarObject$1 = defaultParam(updateCalendarObject, commonDefaults); - const deleteCalendarObject$1 = defaultParam(deleteCalendarObject, commonDefaults); - const syncCalendars$1 = defaultParam(syncCalendars, commonDefaultsWithAccount); - const syncCalendarsDetailed$1 = defaultParam(syncCalendarsDetailed, commonDefaultsWithAccount); - const freeBusyQuery$1 = defaultParam(freeBusyQuery, commonDefaults); - const addressBookQuery$1 = defaultParam(addressBookQuery, commonDefaults); - const addressBookMultiGet$1 = defaultParam(addressBookMultiGet, commonDefaults); return { - davRequest: davRequest$1, - propfind: propfind$1, - createAccount: createAccount$1, - createObject: createObject$1, - updateObject: updateObject$1, - deleteObject: deleteObject$1, - calendarQuery: calendarQuery$1, - addressBookQuery: addressBookQuery$1, - collectionQuery: collectionQuery$1, - makeCollection: makeCollection$1, - calendarMultiGet: calendarMultiGet$1, - makeCalendar: makeCalendar$1, - freeBusyQuery: freeBusyQuery$1, - syncCollection: syncCollection$1, - supportedReportSet: supportedReportSet$1, - isCollectionDirty: isCollectionDirty$1, - smartCollectionSync: smartCollectionSync$1, - smartCollectionSyncDetailed: smartCollectionSyncDetailed$1, - fetchCalendars: fetchCalendars$1, - fetchCalendarUserAddresses: fetchCalendarUserAddresses$1, - fetchCalendarObjects: fetchCalendarObjects$1, - createCalendarObject: createCalendarObject$1, - updateCalendarObject: updateCalendarObject$1, - deleteCalendarObject: deleteCalendarObject$1, - syncCalendars: syncCalendars$1, - syncCalendarsDetailed: syncCalendarsDetailed$1, - fetchAddressBooks: defaultParam(fetchAddressBooks, commonDefaultsWithAccount), - addressBookMultiGet: addressBookMultiGet$1, - fetchVCards: defaultParam(fetchVCards, commonDefaults), - createVCard: defaultParam(createVCard, commonDefaults), - updateVCard: defaultParam(updateVCard, commonDefaults), - deleteVCard: defaultParam(deleteVCard, commonDefaults) + davRequest: client.davRequest.bind(client), + propfind: client.propfind.bind(client), + createAccount: async (...args) => { + if (!args[0].account.accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType`."); + return client.createAccount(...args); + }, + createObject: client.createObject.bind(client), + updateObject: client.updateObject.bind(client), + deleteObject: client.deleteObject.bind(client), + calendarQuery: client.calendarQuery.bind(client), + addressBookQuery: client.addressBookQuery.bind(client), + collectionQuery: client.collectionQuery.bind(client), + makeCollection: client.makeCollection.bind(client), + calendarMultiGet: client.calendarMultiGet.bind(client), + makeCalendar: client.makeCalendar.bind(client), + freeBusyQuery: client.freeBusyQuery.bind(client), + syncCollection: client.syncCollection.bind(client), + supportedReportSet: client.supportedReportSet.bind(client), + isCollectionDirty: client.isCollectionDirty.bind(client), + smartCollectionSync: client.smartCollectionSync.bind(client), + smartCollectionSyncDetailed: client.smartCollectionSyncDetailed.bind(client), + fetchCalendars: client.fetchCalendars.bind(client), + fetchCalendarUserAddresses: client.fetchCalendarUserAddresses.bind(client), + fetchCalendarObjects: client.fetchCalendarObjects.bind(client), + createCalendarObject: client.createCalendarObject.bind(client), + updateCalendarObject: client.updateCalendarObject.bind(client), + deleteCalendarObject: client.deleteCalendarObject.bind(client), + syncCalendars: client.syncCalendars.bind(client), + syncCalendarsDetailed: client.syncCalendarsDetailed.bind(client), + fetchAddressBooks: client.fetchAddressBooks.bind(client), + addressBookMultiGet: client.addressBookMultiGet.bind(client), + fetchVCards: client.fetchVCards.bind(client), + createVCard: client.createVCard.bind(client), + updateVCard: client.updateVCard.bind(client), + deleteVCard: client.deleteVCard.bind(client) }; }; var DAVClient = class { @@ -1808,27 +1968,42 @@ var DAVClient = class { this.authFunction = params.authFunction; this.fetchOptions = params.fetchOptions ?? {}; this.fetchOverride = params.fetch; + this.calendarMultiGet = this.calendarMultiGet.bind(this); + this.addressBookMultiGet = this.addressBookMultiGet.bind(this); + } + async authenticate(force = false, fetchOptions = this.fetchOptions, fetchOverride = this.fetchOverride) { + if (!force && this.authMethod !== "Oauth") return; + if (!force && this.authHeaders && this.credentials.accessToken && (this.credentials.expiration == null || Date.now() < this.credentials.expiration)) { + this.authHeaders = { authorization: `Bearer ${this.credentials.accessToken}` }; + return; + } + if (this.authentication) return this.authentication; + const authenticate = async () => { + this.authHeaders = await resolveAuthHeaders(this, fetchOptions, fetchOverride); + }; + this.authentication = authenticate(); + try { + await this.authentication; + } finally { + this.authentication = void 0; + } + } + async requestDefaults(params) { + await this.authenticate(false, params?.fetchOptions ?? this.fetchOptions, params?.fetch ?? this.fetchOverride); + return { + url: this.serverUrl, + headers: this.authHeaders, + account: this.account, + fetchOptions: this.fetchOptions, + fetch: this.fetchOverride + }; + } + async invoke(fn, params) { + const defaults = await this.requestDefaults(params); + return await defaultParam(fn, defaults)(...[params]); } async login(options) { - switch (this.authMethod) { - case "Basic": - this.authHeaders = getBasicAuthHeaders(this.credentials); - break; - case "Bearer": - this.authHeaders = getBearerAuthHeaders(this.credentials); - break; - case "Oauth": - this.authHeaders = (await getOauthHeaders(this.credentials, this.fetchOptions, this.fetchOverride)).headers; - break; - case "Digest": - this.authHeaders = { Authorization: `Digest ${this.credentials.digestString}` }; - break; - case "Custom": - if (!this.authFunction) throw new Error("authMethod 'Custom' requires an authFunction to produce request headers"); - this.authHeaders = await this.authFunction(this.credentials); - break; - default: throw new Error("Invalid auth method"); - } + await this.authenticate(true); this.account = this.accountType ? await createAccount({ account: { serverUrl: this.serverUrl, @@ -1845,52 +2020,32 @@ var DAVClient = class { async davRequest(params0) { const { init, fetchOptions, fetch: fetchOverride2, ...rest } = params0; const { headers, ...restInit } = init; + const defaults = await this.requestDefaults(params0); return davRequest({ ...rest, init: { ...restInit, - headers: { - ...this.authHeaders, - ...headers - } + headers: mergeHeaders(defaults.headers, headers) }, fetchOptions: fetchOptions ?? this.fetchOptions, fetch: fetchOverride2 ?? this.fetchOverride }); } async createObject(...params) { - return defaultParam(createObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createObject, params[0]); } async updateObject(...params) { - return defaultParam(updateObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateObject, params[0]); } async deleteObject(...params) { - return defaultParam(deleteObject, { - url: this.serverUrl, - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteObject, params[0]); } async propfind(...params) { - return defaultParam(propfind, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(propfind, params[0]); } async createAccount(params0) { - const { account, headers, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const { account, headers, headersToExclude, loadCollections, loadObjects, fetchOptions, fetch } = params0; + const defaults = await this.requestDefaults(params0); const accountType = account.accountType ?? this.accountType; if (!accountType) throw new Error("createAccount requires an accountType; pass one via `account.accountType` or configure `defaultAccountType` on the DAVClient."); return createAccount({ @@ -1900,10 +2055,8 @@ var DAVClient = class { ...account, accountType }, - headers: { - ...this.authHeaders, - ...headers - }, + headers: mergeHeaders(defaults.headers, headers), + headersToExclude, loadCollections, loadObjects, fetchOptions: fetchOptions ?? this.fetchOptions, @@ -1911,193 +2064,82 @@ var DAVClient = class { }); } async collectionQuery(...params) { - return defaultParam(collectionQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(collectionQuery, params[0]); } async makeCollection(...params) { - return defaultParam(makeCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCollection, params[0]); } async syncCollection(...params) { - return defaultParam(syncCollection, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCollection, params[0]); } async supportedReportSet(...params) { - return defaultParam(supportedReportSet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(supportedReportSet, params[0]); } async isCollectionDirty(...params) { - return defaultParam(isCollectionDirty, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(isCollectionDirty, params[0]); } async smartCollectionSync(...params) { - return defaultParam(smartCollectionSync, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(params[0]); + return this.invoke(smartCollectionSync, params[0]); } async smartCollectionSyncDetailed(param) { - return defaultParam(smartCollectionSyncDetailed, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride, - account: this.account - })(param); + return this.invoke(smartCollectionSyncDetailed, param); } async calendarQuery(...params) { - return defaultParam(calendarQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarQuery, params[0]); } async makeCalendar(...params) { - return defaultParam(makeCalendar, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(makeCalendar, params[0]); } async freeBusyQuery(...params) { - return defaultParam(freeBusyQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(freeBusyQuery, params[0]); } async calendarMultiGet(...params) { - return defaultParam(calendarMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(calendarMultiGet, params[0]); } async fetchCalendars(...params) { - return defaultParam(fetchCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendars, params[0]); } async fetchCalendarUserAddresses(...params) { - return defaultParam(fetchCalendarUserAddresses, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchCalendarUserAddresses, params[0]); } async fetchCalendarObjects(...params) { - return defaultParam(fetchCalendarObjects, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchCalendarObjects, params[0]); } async createCalendarObject(...params) { - return defaultParam(createCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createCalendarObject, params[0]); } async updateCalendarObject(...params) { - return defaultParam(updateCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateCalendarObject, params[0]); } async deleteCalendarObject(...params) { - return defaultParam(deleteCalendarObject, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteCalendarObject, params[0]); } async syncCalendars(...params) { - return defaultParam(syncCalendars, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendars, params[0]); } async syncCalendarsDetailed(...params) { - return defaultParam(syncCalendarsDetailed, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(syncCalendarsDetailed, params[0]); } async addressBookQuery(...params) { - return defaultParam(addressBookQuery, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookQuery, params[0]); } async addressBookMultiGet(...params) { - return defaultParam(addressBookMultiGet, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(addressBookMultiGet, params[0]); } async fetchAddressBooks(...params) { - return defaultParam(fetchAddressBooks, { - headers: this.authHeaders, - account: this.account, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params?.[0]); + return this.invoke(fetchAddressBooks, params[0]); } async fetchVCards(...params) { - return defaultParam(fetchVCards, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(fetchVCards, params[0]); } async createVCard(...params) { - return defaultParam(createVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(createVCard, params[0]); } async updateVCard(...params) { - return defaultParam(updateVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(updateVCard, params[0]); } async deleteVCard(...params) { - return defaultParam(deleteVCard, { - headers: this.authHeaders, - fetchOptions: this.fetchOptions, - fetch: this.fetchOverride - })(params[0]); + return this.invoke(deleteVCard, params[0]); } }; //#endregion @@ -2117,4 +2159,4 @@ var src_default = { ...requestHelpers_exports }; //#endregion -export { DAVAttributeMap, DAVClient, DAVNamespace, DAVNamespaceShort, ICALObjects, addressBookMultiGet, addressBookQuery, calendarMultiGet, calendarQuery, cleanupFalsy, collectionQuery, createAccount, createCalendarObject, createDAVClient, createObject, createVCard, davRequest, src_default as default, deleteCalendarObject, deleteObject, deleteVCard, ensureTrailingSlash, excludeHeaders, fetchAddressBooks, fetchCalendarObjects, fetchCalendarUserAddresses, fetchCalendars, fetchHomeUrl, fetchOauthTokens, fetchPrincipalUrl, fetchVCards, freeBusyQuery, getBasicAuthHeaders, getBearerAuthHeaders, getDAVAttribute, getOauthHeaders, isCollectionDirty, makeCalendar, mergeHeaders, propfind, refreshAccessToken, serviceDiscovery, smartCollectionSync, smartCollectionSyncDetailed, supportedReportSet, syncCalendars, syncCalendarsDetailed, syncCollection, updateCalendarObject, updateObject, updateVCard, urlContains, urlEquals, urlMatches }; +export { DAVAttributeMap, DAVClient, DAVNamespace, DAVNamespaceShort, ICALObjects, addressBookMultiGet, addressBookQuery, calendarMultiGet, calendarQuery, cleanupFalsy, collectionQuery, createAccount, createCalendarObject, createDAVClient, createObject, createVCard, davRequest, src_default as default, deleteCalendarObject, deleteObject, deleteVCard, ensureTrailingSlash, excludeHeaders, fetchAddressBooks, fetchCalendarObjects, fetchCalendarUserAddresses, fetchCalendars, fetchHomeUrl, fetchOauthTokens, fetchPrincipalUrl, fetchVCards, freeBusyQuery, getBasicAuthHeaders, getBearerAuthHeaders, getDAVAttribute, getOauthHeaders, isCollectionDirty, makeCalendar, makeCollection, mergeHeaders, propfind, refreshAccessToken, serviceDiscovery, smartCollectionSync, smartCollectionSyncDetailed, supportedReportSet, syncCalendars, syncCalendarsDetailed, syncCollection, updateCalendarObject, updateObject, updateVCard, urlContains, urlEquals, urlMatches }; diff --git a/dist/types/DAVTypes.d.ts b/dist/types/DAVTypes.d.ts index e0deccc7..b26d70c7 100644 --- a/dist/types/DAVTypes.d.ts +++ b/dist/types/DAVTypes.d.ts @@ -19,6 +19,17 @@ export type DAVResponse = { value: any; } | any; }; + propStats?: DAVPropStat[]; + parseError?: string; +}; +export type DAVPropStat = { + props: Record; + namespaces?: Record; + status: number; + statusText: string; + ok: boolean; + error?: Record; + responsedescription?: string; }; export type DAVRequest = { headers?: Record; diff --git a/dist/types/models.d.ts b/dist/types/models.d.ts index fae5fdf8..1cc4b16d 100644 --- a/dist/types/models.d.ts +++ b/dist/types/models.d.ts @@ -13,11 +13,13 @@ export type DAVCollection = { fetchObjects?: ((params?: { collection: DAVCalendar; headers?: Record; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof globalThis.fetch; }) => Promise) | ((params?: { collection: DAVAddressBook; headers?: Record; + headersToExclude?: string[]; fetchOptions?: RequestInit; fetch?: typeof globalThis.fetch; }) => Promise); @@ -29,6 +31,7 @@ export type DAVCollection = { timezone?: string; depth: DAVDepth; fetchOptions?: RequestInit; + headersToExclude?: string[]; headers?: Record; fetch?: typeof globalThis.fetch; }) => Promise; diff --git a/dist/util/responseHelpers.d.ts b/dist/util/responseHelpers.d.ts new file mode 100644 index 00000000..61aa7bae --- /dev/null +++ b/dist/util/responseHelpers.d.ts @@ -0,0 +1,6 @@ +import { DAVResponse } from '../types/DAVTypes'; +export declare const assertDAVResponses: (responses: DAVResponse[], context: string) => void; +export declare const assertDAVDiscovery: (responses: DAVResponse[], context: string) => void; +export declare const assertDAVProperty: (response: DAVResponse, name: string, context: string) => void; +export declare const assertDAVObjectResponses: (responses: DAVResponse[], property: string, objectUrls: string[], baseUrl: string, context: string) => void; +export declare const getDAVText: (value: any) => string | undefined; diff --git a/dist/util/syncHelpers.d.ts b/dist/util/syncHelpers.d.ts new file mode 100644 index 00000000..a4060626 --- /dev/null +++ b/dist/util/syncHelpers.d.ts @@ -0,0 +1,9 @@ +import { DAVObject } from '../types/models'; +/** Stable keys for linear-time DAV resource comparisons. */ +export declare const getDAVUrlKey: (url: string, baseUrl: string) => string; +export declare const diffDAVObjects: (local: DAVObject[], remote: DAVObject[], baseUrl: string, incremental?: boolean, deletedObjects?: DAVObject[]) => { + created: DAVObject[]; + updated: DAVObject[]; + deleted: DAVObject[]; + unchanged: DAVObject[]; +}; diff --git a/dist/util/typeHelpers.d.ts b/dist/util/typeHelpers.d.ts index 53409697..e039d9c2 100644 --- a/dist/util/typeHelpers.d.ts +++ b/dist/util/typeHelpers.d.ts @@ -14,3 +14,4 @@ export type RequireAtLeastOne = Pick(obj: Array>, fields: K[]): obj is Array>; export declare function hasFields(obj: T | RequireAndNotNullSome, fields: K[]): obj is RequireAndNotNullSome; export declare const findMissingFieldNames: (obj: T, fields: Array) => string; +export declare const hasOwn: (value: object, key: PropertyKey) => boolean; diff --git a/dist/util/xml.d.ts b/dist/util/xml.d.ts new file mode 100644 index 00000000..7c31a7cf --- /dev/null +++ b/dist/util/xml.d.ts @@ -0,0 +1,6 @@ +import { ElementCompact } from 'xml-js'; +import { DAVPropStat } from '../types/DAVTypes'; +/** Preserve XML text order before normalizing DAV names to the existing compact shape. */ +export declare const parseDAVXML: (xml: string) => ElementCompact; +/** Keep same-name properties from distinct namespaces across propstat groups. */ +export declare const mergeDAVProps: (propStats: DAVPropStat[]) => Record; diff --git a/docs/README.md b/docs/README.md index 231a499c..8caa6974 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,17 +1,17 @@ # Website -This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. +This website is built using [Docusaurus 3](https://docusaurus.io/), a modern static website generator. ## Installation ```console -yarn install +pnpm install ``` ## Local Development ```console -yarn start +pnpm start ``` This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. @@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo ## Build ```console -yarn build +pnpm build ``` This command generates static content into the `build` directory and can be served using any static contents hosting service. @@ -27,7 +27,13 @@ This command generates static content into the `build` directory and can be serv ## Deployment ```console -GIT_USER= USE_SSH=true yarn deploy +GIT_USER= USE_SSH=true pnpm deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. + +Run `pnpm test` and `pnpm typecheck` here before building. The repository root also provides +`pnpm test:examples`, `pnpm test:consumer`, and `pnpm check:dependencies`. + +The Knip configuration retains Docusaurus type packages required by the inherited TypeScript +configuration, React Router types used by those aliases, and the virtual `@docusaurus/router` module. diff --git a/docs/agent-architecture-overview.md b/docs/agent-architecture-overview.md index 10b2abeb..bf4d4b56 100644 --- a/docs/agent-architecture-overview.md +++ b/docs/agent-architecture-overview.md @@ -16,6 +16,7 @@ workflow, project guardrails, and verification guidance. - `serviceDiscovery`, `fetchPrincipalUrl`, and `fetchHomeUrl` locate account roots via `/.well-known/` redirects (`docs/docs/webdav/account`). - `createAccount` enriches a `DAVAccount` with discovered URLs plus calendars/address books when supplied credentials. +- `src/util/xml.ts` normalizes XML after parsing with `xml-js`, preserving strings, CDATA order, and namespace collisions. `DAVResponse.propStats` retains individual property statuses and namespace maps. - `davRequest` is the low-level fetch wrapper shared by WebDAV, CalDAV, and CardDAV helpers (`docs/docs/webdav/davRequest.md`). - Object helpers map to HTTP verbs: `createObject`, `updateObject`, and `deleteObject` issue PUT/PATCH/DELETE with concurrency headers; `propfind` reads WebDAV metadata (`docs/docs/webdav`). @@ -24,7 +25,7 @@ workflow, project guardrails, and verification guidance. - Enumerate calendars with `fetchCalendars`; filter or retrieve objects via `fetchCalendarObjects`, `calendarMultiGet`, and `calendarQuery` (`docs/docs/caldav`). - Write data with `createCalendarObject`, `updateCalendarObject`, and `deleteCalendarObject`; the helpers enforce proper `If-Match`/`If-None-Match` usage. - `syncCalendars` and `smartCollectionSync` surface server-side `sync-token`/`ctag` deltas for incremental syncs. -- `smart calendar sync.md` documents end-to-end two-way sync: recommended database schema, handling created/updated/deleted objects, and when to fall back to multi-get. +- `smart calendar sync.md` documents transactional remote-to-local sync with explicit storage adapters, complete component queries, and checked local-to-remote writes. ## CardDAV Workflow Highlights @@ -33,7 +34,7 @@ workflow, project guardrails, and verification guidance. ## Helpers and Types -- `docs/docs/helper.mdx` exposes the XML ⇄ JS converter to assemble ElementCompact request bodies without manual XML string building. +- `docs/docs/helper.mdx` exposes a JSON/XML converter for compact request bodies and normalized response inspection. It uses the same XML normalization as the library. - Typed shapes such as `DAVAccount`, `DAVCalendar`, `DAVCalendarObject`, `DAVAddressBook`, `ElementCompact`, and credential tokens are documented in `docs/docs/types/`. - Most high-level functions accept `headers`, `headersToExclude`, and `fetchOptions` overrides—ensure custom auth headers align with provider requirements. - You can override the underlying `fetch` implementation for custom transports (Electron, KaiOS, Workers); see `docs/docs/intro.md` and `docs/docs/cloud providers.md`. @@ -45,7 +46,7 @@ workflow, project guardrails, and verification guidance. ## Field Tips for Agents -- Always normalize calendar object URLs using `URL.resolve` when combining parent collection URLs with relative paths (see smart sync example). +- Always normalize calendar object URLs using `new URL(href, collectionUrl).href` when combining parent collection URLs with relative paths (see smart sync example). - When overriding CalDAV/CardDAV `props`, keep required fields (`supported-calendar-component-set`, `resourcetype`) to prevent server errors. - Reuse helper predicates (`urlFilter`, `useMultiGet`) for providers that emit non-`.ics` or non-`.vcf` keys. - Integration tests (`pnpm test:`) hit live services—guard credentials via environment variables and skip in CI unless configured. diff --git a/docs/docs/caldav/fetchCalendarObjects.md b/docs/docs/caldav/fetchCalendarObjects.md index 7b3849b7..ec1e85e9 100644 --- a/docs/docs/caldav/fetchCalendarObjects.md +++ b/docs/docs/caldav/fetchCalendarObjects.md @@ -32,6 +32,7 @@ const objects = await fetchCalendarObjects({ in this case, you need to pass in your own calendar object name filter so that you can have results you need. ::: - `urlFilter` **default: function which only keep .ics objects** predicate function to filter urls from the calendar objects before fetching +- `expand` requires `timeRange`; requests without it reject. - `expand` whether to [expand](https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5) the calendar objects, forcing the server to expand recurring components into individual calendar objects. :::info some calendar providers may not support calendarMultiGet, then it's necessary to use calendarQuery to fetch calendar object data. @@ -45,3 +46,8 @@ array of [DAVCalendarObject](../types/DAVCalendarObject.md) ### Behavior a mix of [calendarMultiGet](calendarMultiGet.md) and [calendarQuery](calendarQuery.md), you can specify both filters and objectUrls here. + +The default query selects `VEVENT` resources. For a complete collection snapshot, pass +`filters: { "comp-filter": { _attributes: { name: "VCALENDAR" } } }` and a `urlFilter` that +accepts every object URL except the collection URL, including extensionless resources. Built-in +calendar sync and account loading use this complete query. diff --git a/docs/docs/caldav/freeBusyQuery.md b/docs/docs/caldav/freeBusyQuery.md index 90c4d0e3..faf10d54 100644 --- a/docs/docs/caldav/freeBusyQuery.md +++ b/docs/docs/caldav/freeBusyQuery.md @@ -7,12 +7,13 @@ sidebar_position: 10 query free busy data on calendar :::caution -a lot of caldav providers do not support this method like google, apple. -use with caution. +Support depends on the server. Check its supported reports and provider documentation before use. ::: ```ts -const freeBusyQuery = await freeBusyQuery({ +import { freeBusyQuery } from 'tsdav'; + +const result = await freeBusyQuery({ url: 'https://caldav.icloud.com/123456/calendars/A5639426-B73B-4F90-86AB-D70F7F603E75/', timeRange: { start: '2022-01-28T16:25:33.125Z', diff --git a/docs/docs/caldav/import-ical-feed.md b/docs/docs/caldav/import-ical-feed.md index 2f6207b0..c0ab6823 100644 --- a/docs/docs/caldav/import-ical-feed.md +++ b/docs/docs/caldav/import-ical-feed.md @@ -2,73 +2,62 @@ sidebar_position: 15 --- -# Importing iCal Feeds (Airbnb, Booking.com, etc.) +# Importing iCal feeds -`tsdav` is a CalDAV and CardDAV client, which means it is designed to interact with DAV servers. It does not natively support ingesting and syncing iCal subscription feeds (like those provided by Airbnb or Booking.com) directly. - -However, you can easily implement this functionality by combining `tsdav` with an iCal parser. - -## Recommended Approach - -1. **Fetch the iCal feed**: Use a standard HTTP client (like `fetch` or `axios`) to download the `.ics` file from the source URL. -2. **Parse the feed**: Use a library like [ical.js](https://github.com/mozilla-comm/ical.js) or [node-ical](https://github.com/peterbraden/node-ical) to parse the iCal data. -3. **Push to CalDAV**: Iterate through the events in the parsed feed and use `createCalendarObject` to upload them to your target CalDAV calendar. - -## Example Integration - -Here is a conceptual example of how you might import events from an external iCal URL into a CalDAV calendar using `node-ical` and `tsdav`. +Fetch the subscription feed, parse it with [ical.js](https://github.com/kewisch/ical.js), and upload +one resource per UID. Install `ical.js` alongside `tsdav`. The parser and serializer preserve date +values, escaped text, recurrence rules, and recurrence exceptions. Include the feed's timezone +components in each resource. ```ts -import ical from 'node-ical'; +import ICAL from 'ical.js'; import { createCalendarObject, DAVCalendar } from 'tsdav'; -async function importExternalFeed( +export async function importExternalFeed( feedUrl: string, targetCalendar: DAVCalendar, authHeader: string, ) { - // 1. Fetch and parse the feed - const events = await ical.async.fromURL(feedUrl); - - for (const event of Object.values(events)) { - if (event.type !== 'VEVENT') continue; - - // 2. Convert the event back to an iCal string (if needed) or use the raw source - // Note: You should wrap each event in its own VCALENDAR/VEVENT structure for CalDAV - const iCalString = `BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//tsdav//Import Helper//EN -BEGIN:VEVENT -UID:${event.uid} -SUMMARY:${event.summary} -DTSTART:${event.start.toISOString().replace(/[-:]/g, '').split('.')[0]}Z -DTEND:${event.end.toISOString().replace(/[-:]/g, '').split('.')[0]}Z -DESCRIPTION:${event.description || ''} -END:VEVENT -END:VCALENDAR`; + const response = await fetch(feedUrl); + if (!response.ok) throw new Error(`Feed download failed: ${response.status}`); + const source = new ICAL.Component(ICAL.parse(await response.text())); + const byUid = new Map(); + for (const event of source.getAllSubcomponents('vevent')) { + const uid = event.getFirstPropertyValue('uid'); + if (typeof uid !== 'string' || !uid) throw new Error('Feed event has no UID'); + const group = byUid.get(uid) ?? []; + group.push(event); + byUid.set(uid, group); + } - // 3. Upload to CalDAV - try { - await createCalendarObject({ - calendar: targetCalendar, - filename: `${event.uid}.ics`, - iCalString, - headers: { - authorization: authHeader, - }, - }); - console.log(`Imported event: ${event.summary}`); - } catch (error) { - // In a real application, you'd handle 412 Precondition Failed (If-None-Match) - // if the event already exists, or use updateCalendarObject if you want to sync. - console.error(`Failed to import event ${event.uid}:`, error); + let imported = 0; + for (const [uid, events] of byUid) { + const resource = new ICAL.Component(['vcalendar', [], []]); + resource.addPropertyWithValue('version', '2.0'); + resource.addPropertyWithValue('prodid', '-//tsdav//Feed importer//EN'); + for (const timezone of source.getAllSubcomponents('vtimezone')) { + resource.addSubcomponent(new ICAL.Component(JSON.parse(JSON.stringify(timezone.toJSON())))); + } + for (const event of events) { + resource.addSubcomponent(new ICAL.Component(JSON.parse(JSON.stringify(event.toJSON())))); } + const result = await createCalendarObject({ + calendar: targetCalendar, + filename: `feed-${encodeURIComponent(uid)}.ics`, + iCalString: resource.toString(), + headers: { authorization: authHeader }, + }); + if (!result.ok) { + throw new Error(`Import failed for ${uid}: ${result.status}`); + } + imported += 1; } + return imported; } ``` -## Considerations - -- **Syncing**: To avoid duplicates, you should track which events have already been imported (e.g., by keeping a map of UIDs). -- **Updates**: If an event changes in the source feed, you should use `updateCalendarObject` instead of `createCalendarObject`. -- **Feed Polling**: You will need to set up your own periodic job (e.g., a cron job or a background worker) to poll the iCal feed and trigger the import process. +This is an import, not a feed synchronization policy. `createCalendarObject` uses `If-None-Match: *`; +a duplicate resource returns 412 and the example throws. For repeated polling, keep a UID-to-resource +mapping and fetched etags, use `updateCalendarObject` with the current etag for changes, and decide +explicitly whether a removed feed event should be deleted. A later failure can leave earlier uploads +completed, so persist successful imports individually in that workflow. diff --git a/docs/docs/caldav/syncCalendars.md b/docs/docs/caldav/syncCalendars.md index 48bc7b77..9480ec88 100644 --- a/docs/docs/caldav/syncCalendars.md +++ b/docs/docs/caldav/syncCalendars.md @@ -40,22 +40,25 @@ const { created, updated, deleted } = await syncCalendarsDetailed({ - `fetchOptions` options to pass to underlying fetch function :::info -`objects` inside `oldCalendars` are not needed when using `syncCalendarsDetailed`. +Both functions sync objects in updated calendars. Include previously stored objects to retain +unchanged objects during incremental WebDAV sync. Newly discovered calendars contain metadata only; +fetch their objects separately. ::: ### Return Value `syncCalendars` returns: -array of [DAVCalendar](../types/DAVCalendar.md) with calendar objects. +array of [DAVCalendar](../types/DAVCalendar.md). Updated calendars contain synced objects; unchanged +calendars retain the supplied objects, and newly discovered calendars contain metadata only. `syncCalendarsDetailed` returns: an object of - `created` array of [DAVCalendar](../types/DAVCalendar.md) without calendar objects. -- `updated` array of [DAVCalendar](../types/DAVCalendar.md) without calendar objects. -- `deleted` array of [DAVCalendar](../types/DAVCalendar.md) without calendar objects. +- `updated` array of [DAVCalendar](../types/DAVCalendar.md) with synced calendar objects. +- `deleted` array of the previously supplied [DAVCalendar](../types/DAVCalendar.md). ### Behavior @@ -71,4 +74,7 @@ return latest list of calendars with latest list of objects for `updated` calend When using `syncCalendarsDetailed`, -return three list of separate calendars without objects for `created`, `updated`, and `deleted`. +return separate lists for `created`, `updated`, and `deleted`, with the object behavior described above. + +Discovery and object-fetch failures reject before returning changes. Built-in sync queries all calendar +components, including tasks and journals, and preserves the token returned by the sync REPORT. diff --git a/docs/docs/helper.mdx b/docs/docs/helper.mdx index 671e1b7b..efbeadf6 100644 --- a/docs/docs/helper.mdx +++ b/docs/docs/helper.mdx @@ -6,30 +6,33 @@ import { Converter } from '../src/components/Converter.tsx'; # Helper -Helper to convert xml to expected js object to be consumed by tsdav +Inspect normalized DAV responses or serialize compact XML request objects. The normalized response +keys are for reading responses; request keys use the exact XML names and namespace prefixes. +All object inputs use JSON. ### xml -> js - - - - - - - - - - - - - -`} + + + /calendars/personal/event.ics + + + "123" + + + HTTP/1.1 200 OK + + +`} /> ### js -> xml @@ -37,14 +40,14 @@ Helper to convert xml to expected js object to be consumed by tsdav - import { createDAVClient } from 'https://unpkg.com/tsdav/dist/tsdav.mjs'; + import { createDAVClient } from 'https://unpkg.com/tsdav/dist/tsdav.js'; const client = await createDAVClient({ serverUrl: 'https://caldav.icloud.com', diff --git a/docs/docs/smart calendar sync.md b/docs/docs/smart calendar sync.md index 9630f2d7..856312c4 100644 --- a/docs/docs/smart calendar sync.md +++ b/docs/docs/smart calendar sync.md @@ -4,263 +4,95 @@ sidebar_position: 8 # Smart calendar sync -To actually achieve two way syncing of calendar events between cloud provider and your service. +Store each calendar's URL, ctag, sync token, supported reports, and object URLs, etags, and original +calendar data. This example uses Basic authentication with a username and application password. +Keep credentials in your application's protected credential storage. An incremental +sync needs the previous object list to distinguish created and updated objects and retain unchanged +objects. -## preparation - -You need to - -#### create database structures to store the calendar info - -for databases, you need following structures: - -##### App calendar - -your app's calendar object type like: +The following adapter keeps database operations explicit. Implement `CalendarStore` with your +existing database; its transaction must commit all object changes and their tokens together. ```ts -type AppCalendar = { - id: string; - userId: string; - timezone?: string; - name?: string; - description?: string; - email?: string; - createdAt: string; - updatedAt: string; -}; -``` - -this table is used for your app's display, daily use, etc, optional if you do not alredy have a table like this or you do not want one-to-many relations with your app calendar, you can skip creating this table. - -##### Caldav calendar +import { DAVClient, DAVCalendar, DAVObject, DAVCredentials } from 'tsdav'; -you need to store caldav calendar information obtained from `fetchCalendars` - -```typescript -type CaldavCalendar = { - id: string; - userId: string; - timezone: string; - name: string; - source: string; // your caldav provider name - ctag: string; // obtained from remote - syncToken: string; // obtained from remote - url: string; - credentialId: - createAt: string; -}; -``` - -##### credentials - -save caldav calendar credentials in another table, encryption is recommended: - -```ts -type CalendarCredential = { - account: string; - refreshToken?: string; - password?: string; - valid: boolean; - source: // your caldav provider name +interface CalendarTransaction { + putCalendar(calendar: DAVCalendar): Promise; + removeCalendar(url: string): Promise; + putObject(calendarUrl: string, object: DAVObject): Promise; + removeObject(calendarUrl: string, objectUrl: string): Promise; } -``` - -##### caldav calendar objects - -you also need to store caldav calendar objects obtained from `fetchCalendarObjects` - -```ts -export type CalendarObject = { - id: string; - calendarId: string; // foreign key reference the CaldavCalendar if needed - url: string; - etag: string; - start: string; // recommend to have this field for easy filtering/sorting - end: string; // recommend to have this field for easy filtering/sorting - data: string; // actual ics data -}; -``` - -to parse and obtain information from ics data, it's recommended to use a combination of - -https://github.com/natelindev/pretty-jcal and https://github.com/kewisch/ical.js - -for generating new ics data, it's recommended to use https://github.com/nwcell/ics.js/ - -## Actual syncing - -First you need to have user go through authorization process and obtain valid `CalendarCredential`, the method differs for each caldav provider. You need to find and setup it yourself. - -after having obtained the credentials, you can begin the actual sync - -First you need to get all stored calendars for the user - -```ts -const localCalendars = await this.db.getCalendarByUserIdAndSource(userId, source); -``` - -then you need to create caldav client using credentials - -```ts -const client = new DAVClient({ - serverUrl: 'https://caldav.icloud.com', - credentials: { - username: 'YOUR_APPLE_ID', - password: 'YOUR_APP_SPECIFIC_PASSWORD', - }, - authMethod: 'Basic', - defaultAccountType: 'caldav', -}); -``` - -##### Remote to local -you can use `syncCalendarsDetailed` function from the lib - -```ts -const { created, updated, deleted } = await client.syncCalendarsDetailed({ - oldCalendars: localCalendars.map((lc) => ({ - displayName: lc.name, - syncToken: lc.syncToken, - ctag: lc.ctag, - url: lc.url, - })), -}); -``` - -make sure you send the `syncToken` and `ctag`, this way the remote will know your last sync and identify the calendar changes. - -now you have all calendar changes on remote. make actual changes to your database like: - -```ts -await this.db.transaction(async (tx) => { - await Promise.all( - created.map(async (c) => { - // created - const calendarObjects = await client.fetchCalendarObjects({ calendar: c }); - - if (calendarObjects.length > 0) { - await Promise.all( - calendarObjects.map((co) => { - // parse start end time if needed - // const parsedObject = parse(co); - // const { start, end } = parsedObject; - return this.db.createCalendarObject(tx, { - ...co, - // start, - // end, - calendarId: c.id, - }); - }), - ); - } - }), - ); - - // deleted - if (deleted.length > 0) { - await this.db.deleteByUrls( - tx, - filteredDeleted.map((d) => d.url), - ); - } - - // updated - const localCalendarsToBeUpdated = await this.db.getByUrls( - tx, - updated.map((u) => u.url), - ); - - // find out and apply the change on calendar - await Promise.all( - localCalendarsToBeUpdated.map(async (lc) => { - const localObjects = await this.db.getCalendarById(tx, lc.id); - const { - created: createdObjects, - updated: updatedObjects, - deleted: deletedObjects, - } = ( - await client.smartCollectionSyncDetailed({ - collection: { - url: lc.url, - ctag: lc.ctag, - syncToken: lc.syncToken, - objects: localObjects, - objectMultiGet: client.calendarMultiGet, - }, - method: 'webdav', - }) - ).objects; - - // apply changes to local calendar objects - // created objects - if (createdObjects.length > 0) { - await Promise.all( - createdObjects - .filter((co) => co.url.includes('.ics')) - .map((co) => { - // parse start end time if needed - // const parsedObject = parse(co); - // const { start, end } = parsedObject; - - return this.db.createCalendarObject(tx, { - ...co, - // start, - // end, - url: URL.resolve(lc.url, co.url), - calendarId: lc.id, - }); - }), - ); - } - - // deleted objects - if (deletedObjects.length > 0) { - await this.db.deleteCalendarObjectByUrls( - tx, - deletedObjects.map((d) => URL.resolve(lc.url, d.url)), - ); - } - - // updated objects - if (updatedObjects.length > 0) { - await Promise.all( - updatedObjects.map((uo) => { - // parse start end time if needed - // const parsedObject = parse(co); - // const { start, end } = parsedObject; +interface CalendarStore { + readCalendars(): Promise; + transaction(work: (tx: CalendarTransaction) => Promise): Promise; +} - return this.db.updateCalendarObjectByUrl(tx, URL.resolve(lc.url, uo.url), { - etag: uo.etag, - data: uo.data, - start, - end, - }); - }), - ); +export async function synchronizeCalendars( + serverUrl: string, + credentials: DAVCredentials, + store: CalendarStore, +) { + const client = new DAVClient({ + serverUrl, + credentials, + defaultAccountType: 'caldav', + }); + await client.login(); + const local = await store.readCalendars(); + const remote = await client.fetchCalendars(); + const byUrl = new Map(local.map((calendar) => [calendar.url, calendar])); + const remoteUrls = new Set(remote.map((calendar) => calendar.url)); + + const results = await Promise.all(remote.map(async (calendar) => { + const previous = byUrl.get(calendar.url); + return client.smartCollectionSyncDetailed({ + collection: { + ...calendar, + ctag: previous?.ctag, + syncToken: previous?.syncToken, + objects: previous?.objects ?? [], + objectMultiGet: client.calendarMultiGet, + fetchObjects: async (params: Parameters>[0]) => { + if (!params) throw new Error('A collection is required'); + const { collection, ...options } = params; + return client.fetchCalendarObjects({ + ...options, + calendar: collection, + filters: { 'comp-filter': { _attributes: { name: 'VCALENDAR' } } }, + urlFilter: (url) => new URL(url, collection.url).href !== collection.url, + }); + }, + }, + }); + })); + + await store.transaction(async (tx) => { + for (const calendar of local) { + if (!remoteUrls.has(calendar.url)) await tx.removeCalendar(calendar.url); + } + for (const result of results) { + const { objects, objectMultiGet, fetchObjects, ...metadata } = result; + for (const object of [...objects.created, ...objects.updated]) { + await tx.putObject(result.url, { ...object, url: new URL(object.url, result.url).href }); } - }), - ); - - // update the syncToken & ctag for the calendars to be updated - await Promise.all( - filteredUpdated.map((u) => { - const lcu = localCalendarToBeUpdated.find((uo) => uo.url === u.url); - if (!lcu) { - throw new ValidationError(`local calendar with url ${u.url} not found `); + for (const object of objects.deleted) { + await tx.removeObject(result.url, new URL(object.url, result.url).href); } - return this.db.updateCalendarById(tx, lcu.id, { - syncToken: u.syncToken, - ctag: u.ctag, - }); - }), - ); -}); + await tx.putCalendar(metadata); + } + }); +} ``` -##### Local to remote - -when going local to remote, it's rather easy - -just generate the ics data and then use `createCalendarObject` , `updateCalendarObject` and `deleteCalendarObject` directly on remote caldav calendars. Update your locally stored calendar objects in your database after remote operation success. +Method selection uses the discovered reports: WebDAV sync retrieves changed objects through +`objectMultiGet`; basic sync uses ctag and a complete collection query through `fetchObjects`. +The client's multiget methods are bound and can be used as callbacks. Store the returned token, +which represents the successfully fetched changes. If discovery, sync, fetching, or the database +transaction fails, retain the previous tokens and retry from that state. + +For local changes, serialize a complete iCalendar resource and call `createCalendarObject`, +`updateCalendarObject`, or `deleteCalendarObject`. These helpers return a Fetch `Response`; check +`response.ok` before updating local state. A 412 response indicates a precondition conflict and +requires fetching the current remote object before deciding how to reconcile it. Preserve recurrence, +all-day dates, timezones, and exceptions when editing the data; see [feed import](./caldav/import-ical-feed.md). diff --git a/docs/docs/types/DAVResponse.md b/docs/docs/types/DAVResponse.md index fd450abf..b1dc30af 100644 --- a/docs/docs/types/DAVResponse.md +++ b/docs/docs/types/DAVResponse.md @@ -2,6 +2,8 @@ export type DAVResponse = { raw?: any; href?: string; + parseError?: string; + propStats?: DAVPropStat[]; status: number; statusText: string; ok: boolean; @@ -40,10 +42,20 @@ response type of [davRequest](../webdav/davRequest.md) - `raw` the entire [response](https://datatracker.ietf.org/doc/html/rfc4918#section-14.24) object, useful when need something that is not a prop or href - `href` [content element URI](https://datatracker.ietf.org/doc/html/rfc2518#section-12.3) -- `status` fetch response status +- `status` resource status, an all-failed property status, or the HTTP response status - `statusText` fetch response statusText -- `ok` fetch response ok +- `ok` whether the resource response has a successful status; false when all returned properties fail - `error` error object from error response - `responsedescription` [information about a status response within a Multi-Status](https://datatracker.ietf.org/doc/html/rfc4918#section-14.25) - `props` response [propstat](https://datatracker.ietf.org/doc/html/rfc4918#section-14.22) props with camel case names. + +`propStats` preserves each property's status, properties, namespace URI map (`namespaces`), and +optional error/description. `props` contains only successful properties. A response whose every +property failed has `ok: false`; mixed results require inspecting the statuses of the properties you +need. XML text stays a string, including numeric-looking display names and opaque tokens. Pure CDATA +uses `{ _cdata: string }`; mixed text and CDATA are joined in document order. When namespaces share a +local name, DAV names retain their usual key and colliding names use `{namespaceURI}localName`. + +`raw` preserves the complete body for unparsed responses. A malformed XML response has `ok: false` +and `parseError`, so callers can distinguish parsing failures from a successful empty result. diff --git a/docs/docs/webdav/account/serviceDiscovery.md b/docs/docs/webdav/account/serviceDiscovery.md index ffbdf894..a7fae16b 100644 --- a/docs/docs/webdav/account/serviceDiscovery.md +++ b/docs/docs/webdav/account/serviceDiscovery.md @@ -29,4 +29,16 @@ root url ### Behavior -use `/.well-known/` request to follow redirects to find redirected url +Requests `/.well-known/caldav` or `/.well-known/carddav` on the server's origin using +`PROPFIND`, then tries `GET` if necessary. Relative `Location` headers resolve against +that discovery request URL. For example, `Location: ../dav/` resolves to `/dav/`, even +when `serverUrl` contains a nested calendar path. + +Path-relative redirects retain the request's origin and port. Absolute and +protocol-relative redirects use their own host and port, including the protocol's +default port when none is specified. + +Discovery controls the method, body, and manual redirect handling. A `body` supplied +in `fetchOptions` is not sent with the `GET` fallback; other transport options and +headers still apply. If neither request redirects, the function returns `serverUrl` +as a normalized URL. diff --git a/docs/docs/webdav/collection/isCollectionDirty.md b/docs/docs/webdav/collection/isCollectionDirty.md index 9631ae58..50434819 100644 --- a/docs/docs/webdav/collection/isCollectionDirty.md +++ b/docs/docs/webdav/collection/isCollectionDirty.md @@ -25,7 +25,7 @@ const { isDirty, newCtag } = await isCollectionDirty({ ### Return Value - `isDirty` a boolean indicate if the collection is dirty -- `newCtag` if collection is dirty, new ctag of the collection +- `newCtag` the remote ctag, or `undefined` when the server does not provide one. An unavailable ctag means `isDirty: true` and basic sync fetches a complete snapshot. ### Behavior diff --git a/docs/docs/webdav/collection/smartCollectionSync.md b/docs/docs/webdav/collection/smartCollectionSync.md index a758c963..f97818c7 100644 --- a/docs/docs/webdav/collection/smartCollectionSync.md +++ b/docs/docs/webdav/collection/smartCollectionSync.md @@ -49,18 +49,21 @@ const { created, updated, deleted } = ( - `fetch` custom fetch implementation :::info -`objects` inside `collection` are not needed when using `smartCollectionSyncDetailed`. +Provide the previously stored `collection.objects` to distinguish created from updated objects. +Without that baseline, changed remote objects are classified as created. Basic sync also needs the +baseline to identify deletions. ::: ### Return Value `smartCollectionSync` returns: -array of latest [DAVObject](../../types/DAVObject.md) +the supplied collection with an `objects` array containing the latest [DAVObject](../../types/DAVObject.md) +and the resulting `syncToken` or `ctag`. `smartCollectionSyncDetailed` returns: -an object of +the supplied collection with the resulting `syncToken` or `ctag` and - `objects` - `created` array of [DAVObject](../../types/DAVObject.md) diff --git a/docs/docs/webdav/davRequest.md b/docs/docs/webdav/davRequest.md index a5401b4a..24d6c757 100644 --- a/docs/docs/webdav/davRequest.md +++ b/docs/docs/webdav/davRequest.md @@ -37,6 +37,7 @@ const [result] = await davRequest({ - `init` **required**, [DAVRequest](davRequest.md) Object - `convertIncoming` defaults to `true`, whether to convert the passed in init object request body, if `false`, davRequest would expect `init->body` is `xml` string, and would send it directly to target `url` without processing. - `parseOutgoing` defaults to `true`, whether to parse the return value in response body, if `false`, the response `raw` would be raw `xml` string returned from server. +- `headersToExclude` case-insensitive header names to remove after merging defaults, request headers, and `fetchOptions.headers` - `fetchOptions` options to pass to underlying fetch function - `fetch` custom fetch implementation to override the runtime's native `fetch` @@ -55,3 +56,7 @@ if request failed, response-> raw will be raw response text returned from server Multistatus responses without resource entries still return one result with the parsed `raw` payload, so callers can read collection metadata such as `raw.multistatus.syncToken`. Status codes are parsed even when the server omits the reason phrase. + +Successful unparsed response bodies are preserved in full. Parsed property failures remain available +in `propStats`; `props` includes only successful properties, and an all-failed propstat response has +`ok: false`. Malformed XML has `ok: false` and a `parseError`. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 80c493b5..dd16a379 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -1,4 +1,4 @@ -/** @type {import('@docusaurus/types').DocusaurusConfig} */ +/** @type {import('@docusaurus/types').Config} */ module.exports = { title: 'tsdav', tagline: 'webdav request made easy', @@ -54,7 +54,7 @@ module.exports = { }, presets: [ [ - '@docusaurus/preset-classic', + require.resolve('@docusaurus/preset-classic'), { docs: { path: 'docs', @@ -63,7 +63,7 @@ module.exports = { lastVersion: 'current', versions: { current: { - label: '2.3.2', + label: require('../package.json').version, }, '1.1.6': { label: '1.1.6', @@ -81,11 +81,11 @@ module.exports = { ], ], plugins: [ - '@cmfcmf/docusaurus-search-local', + require.resolve('@cmfcmf/docusaurus-search-local'), require.resolve('./docusuarusWebpack5Plugin'), - 'docusaurus-markdown-source-plugin', + require.resolve('docusaurus-markdown-source-plugin'), [ - 'docusaurus-plugin-llms', + require.resolve('docusaurus-plugin-llms'), { generateLLMsTxt: true, generateLLMsFullTxt: true, diff --git a/docs/docusuarusWebpack5Plugin.js b/docs/docusuarusWebpack5Plugin.js index 61d69b30..dbfbc74b 100644 --- a/docs/docusuarusWebpack5Plugin.js +++ b/docs/docusuarusWebpack5Plugin.js @@ -6,6 +6,7 @@ module.exports = function (context, options) { configureWebpack(config, isServer, utils) { return { resolve: { + alias: { 'xml-js': require.resolve('xml-js') }, // alias: { // path: require.resolve('path-browserify'), // }, diff --git a/docs/package.json b/docs/package.json index 3da75347..c595e580 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,8 @@ "swizzle": "docusaurus swizzle", "write-heading-ids": "docusaurus write-heading-ids", "write-translations": "docusaurus write-translations", - "test": "node --test tests/*.test.cjs" + "test": "node --test tests/*.test.cjs", + "typecheck": "tsc --noEmit" }, "browserslist": { "production": [ @@ -32,25 +33,21 @@ "@docusaurus/core": "3.10.2", "@docusaurus/preset-classic": "3.10.2", "@mdx-js/react": "3.1.1", - "@svgr/webpack": "8.1.0", "buffer": "6.0.3", - "clsx": "2.1.1", "docusaurus-markdown-source-plugin": "^2.2.5", - "file-loader": "6.2.0", "prism-react-renderer": "2.4.1", "react": "19.2.8", "react-dom": "19.2.8", "stream-browserify": "3.0.0", - "url-loader": "4.1.1", "xml-js": "1.6.11" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.10.2", "@docusaurus/theme-classic": "3.10.2", + "@docusaurus/types": "3.10.2", "@tsconfig/docusaurus": "2.0.9", "@types/node": "26.4.1", "@types/react": "19.2.18", - "@types/react-helmet": "6.1.11", "@types/react-router-dom": "5.3.3", "docusaurus-plugin-llms": "^0.6.0", "typescript": "7.0.2" diff --git a/docs/plans/review-fixes.md b/docs/plans/review-fixes.md new file mode 100644 index 00000000..5f4fe5e3 --- /dev/null +++ b/docs/plans/review-fixes.md @@ -0,0 +1,102 @@ +# Codebase review fixes + +Keep `xml-js`, the existing entry points, and runtime portability across Node.js, +browsers, Bun, Deno, and Workers. Do not run live provider tests. + +## Stage 1: Response and transport correctness + +- Normalize XML after parsing without coercing opaque strings or losing mixed text. +- Preserve namespaces and property statuses while retaining existing property access. +- Reject incomplete or failed collection discovery before calculating deletions. +- Preserve successful raw payloads and apply header exclusions after merging. + +## Stage 2: Synchronization and client behavior + +- Sync every supported calendar component and extensionless resources safely. +- Keep the REPORT sync token, respect query subsets, and validate expansion inputs. +- Replace quadratic URL comparisons with indexes. +- Share client authentication/request defaults; refresh expired OAuth tokens safely. +- Correct class return types, export `makeCollection`, and restrict build cleanup. + +## Stage 3: Documentation and dependencies + +- Repair browser, sync, feed-import, and free/busy examples and document actual contracts. +- Reuse the XML normalizer in the docs converter and replace `eval` with JSON parsing. +- Synchronize version labels and describe provider capabilities accurately. +- Remove unused direct dependencies, patch docs advisories, and keep `xml-js`. +- Add repeatable dependency and executable documentation/consumer checks. + +## Stage 4: Verification + +- Add regressions for the original failing scenarios, including both client APIs. +- Run unit tests, consumer type checks, lint, package build, and packaged entry checks. +- Run docs tests, type checking, production build, and both dependency audits. +- Review the final diff and report remaining verification limits. + +## Stage 5: PR #281 review and follow-up + +- Reproduce the relative discovery redirect bug and validate the PR and required CI. +- Merge the PR and preserve the existing local review fixes while updating `main`. +- Correct redirect authority handling and remove GET fallback bodies; add regressions. +- Verify the combined changes, including both client APIs, consumer examples, and docs. + +## Stage 6: Release readiness + +- Build before testing in the release workflow and include consumer/example checks. +- Inspect a freshly packed candidate from a clean snapshot of the local changes. +- Verify packaged imports and mocked requests across installed supported runtimes. +- Report the next version and outstanding Git/release steps without publishing. + +## Stage 7: Publish 2.3.5 + +- Credit `@bensynapse` and PR #281 in the changelog and GitHub release notes. +- Bump the version, regenerate package artifacts, and verify the final candidate. +- Commit the changes, pass the release PR checks, and merge without bypassing checks. +- Pass CI on the merged commit, publish the GitHub release, and verify npm publication. + +## Agent progress + +- Initial review completed; reproduced sync, parsing, authentication, typing, header, + build-cleanup, and documentation failures. +- Response normalization, final header exclusion, client authentication sharing, and sync safeguards implemented. +- Documentation examples and converter corrected; unused direct dependencies removed and docs advisories patched. +- Stages 1–4 completed; `xml-js` retained in the library and docs. +- Verification passed: 320 unit tests, 44 docs tests, 3 executable documentation tests, + public consumer type checks, lint, root/docs type checks, and package/docs builds. +- Frozen lockfile installs, a standalone docs install/build, packed CJS/ESM consumers, + and native Chrome import/parsing/request checks passed. Both dependency audits and + the dependency-usage checks are clean. +- Generated package output excluded from the patch. Live provider integration tests + were not run; local verification used Node.js 24 and Chrome. Other runtime smoke + checks remain covered by CI. +- PR #281 reviewed and merged as `c035f3e`; its eight regressions failed before the + change and its 306 unit tests plus required GitHub CI passed after the change. +- Stage 5 completed: corrected explicit redirect ports and GET fallback bodies, added + 19 regressions, and documented the discovery behavior. +- Combined verification passed: 347 unit tests, 3 executable documentation tests, + consumer type checks, lint, root type checking, and package/docs builds. Clearing + the stale Docusaurus cache resolved the first incremental docs-build failure. +- PR and post-merge GitHub CI passed all seven jobs. Follow-up fixes remain local + alongside the earlier review changes; generated package output is excluded. +- Stage 6 completed: release workflow now verifies fresh artifacts, lint, public + consumers/examples, and dependency usage before publishing. +- A clean root-only install exposed an example-test dependency on the docs toolchain; + Vitest now uses the root compiler settings. The original failure is resolved. +- Clean-snapshot verification passed: 347 unit tests, 3 executable documentation + tests, consumer type checks, type checking, lint, dependency usage, package build, + and packed-file inspection. The tarball contains 35 files and no test/source files. +- Packed CJS/ESM and mocked request checks passed on Node 18.20.8, 20.18.1, 22.21.1, + and 24.11.1, Bun 1.3.14, Deno 2.9.4, and native Chrome. Both browser bundles also + passed in a sandbox without Node globals. Both dependency audits remain clean. +- Recommend a 2.3.5 patch release. Version bump, committing/pushing the local changes, + and CI on the final commit remain before publishing. No release was published; + live provider verification was not run. +- Stage 7 authorized: preparing the 2.3.5 release with contributor credit, final + artifacts, and CI verification before publication. +- Stage 7 candidate prepared: version bumped to 2.3.5, artifacts regenerated, and + `@bensynapse` credited for PR #281 in the changelog and prepared release notes. +- Final candidate verification passed: 347 unit tests, 44 documentation tests, + 3 executable example tests, root/docs type checks and builds, lint, dependency + usage, both audits, packed-file inspection, and packed CJS/ESM request checks. +- Release PR checks, merge, final main CI, and publication will be recorded in the + GitHub release and its workflow runs. diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index d081a9a1..34818cdf 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -13,6 +13,9 @@ overrides: sockjs>uuid: 11.1.1 serialize-javascript@<7.0.5: 7.0.5 image-size: npm:image-size-next@2.1.1 + brace-expansion@1: 1.1.21 + brace-expansion@5: 5.0.12 + fast-uri@<3.1.8: 3.1.8 importers: @@ -30,21 +33,12 @@ importers: '@mdx-js/react': specifier: 3.1.1 version: 3.1.1(@types/react@19.2.18)(react@19.2.8) - '@svgr/webpack': - specifier: 8.1.0 - version: 8.1.0(typescript@7.0.2) buffer: specifier: 6.0.3 version: 6.0.3 - clsx: - specifier: 2.1.1 - version: 2.1.1 docusaurus-markdown-source-plugin: specifier: ^2.2.5 version: 2.2.5(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(postcss@8.5.28)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - file-loader: - specifier: 6.2.0 - version: 6.2.0(webpack@5.110.3(postcss@8.5.28)) prism-react-renderer: specifier: 2.4.1 version: 2.4.1(react@19.2.8) @@ -57,9 +51,6 @@ importers: stream-browserify: specifier: 3.0.0 version: 3.0.0 - url-loader: - specifier: 4.1.1 - version: 4.1.1(file-loader@6.2.0(webpack@5.110.3(postcss@8.5.28)))(webpack@5.110.3(postcss@8.5.28)) xml-js: specifier: 1.6.11 version: 1.6.11 @@ -70,6 +61,9 @@ importers: '@docusaurus/theme-classic': specifier: 3.10.2 version: 3.10.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) + '@docusaurus/types': + specifier: 3.10.2 + version: 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.28))(html-minifier-terser@7.2.0)(postcss@8.5.28)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tsconfig/docusaurus': specifier: 2.0.9 version: 2.0.9 @@ -79,9 +73,6 @@ importers: '@types/react': specifier: 19.2.18 version: 19.2.18 - '@types/react-helmet': - specifier: 6.1.11 - version: 6.1.11 '@types/react-router-dom': specifier: 5.3.3 version: 5.3.3 @@ -1782,9 +1773,6 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-helmet@6.1.11': - resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==} - '@types/react-router-config@5.0.11': resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} @@ -2208,11 +2196,11 @@ packages: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} - brace-expansion@1.1.18: - resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: @@ -2932,8 +2920,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-uri@3.1.7: - resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} fastq@1.20.3: resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} @@ -8376,10 +8364,6 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/react-helmet@6.1.11': - dependencies: - '@types/react': 19.2.18 - '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 @@ -8631,7 +8615,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.7 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -8832,12 +8816,12 @@ snapshots: widest-line: 4.0.1 wrap-ansi: 8.1.0 - brace-expansion@1.1.18: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -9599,7 +9583,7 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-uri@3.1.7: {} + fast-uri@3.1.8: {} fastq@1.20.3: dependencies: @@ -10828,11 +10812,11 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 1.1.18 + brace-expansion: 1.1.21 minimatch@9.0.7: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimist@1.2.8: {} diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index 1d58f34d..4d4f5b4c 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -10,3 +10,6 @@ overrides: sockjs>uuid: "11.1.1" serialize-javascript@<7.0.5: "7.0.5" image-size: "npm:image-size-next@2.1.1" + 'brace-expansion@1': '1.1.21' + 'brace-expansion@5': '5.0.12' + 'fast-uri@<3.1.8': '3.1.8' diff --git a/docs/src/components/Converter.module.css b/docs/src/components/Converter.module.css index 6ce03a84..1100b06b 100644 --- a/docs/src/components/Converter.module.css +++ b/docs/src/components/Converter.module.css @@ -1,25 +1,36 @@ .converter { display: flex; flex-direction: column; - justify-content: center; + gap: 0.65rem; width: 100%; - height: 100%; + margin-block: 1.5rem; } .textarea { width: 100%; - min-height: 20rem; - height: 100%; - border: none; - resize: none; - font-size: 1rem; - padding: 10px; - border-radius: 0.2rem; - margin-bottom: 1rem; + min-height: 16rem; + padding: 1rem; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 0.5rem; + background: var(--ifm-background-surface-color); + color: var(--ifm-font-color-base); + resize: vertical; + font-family: var(--ifm-font-family-monospace); + font-size: 0.9rem; + line-height: 1.5; +} + +.textarea:focus-visible { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 3px; } .title { - font-size: 1.5rem; - font-weight: bold; - margin-bottom: 1rem; -} \ No newline at end of file + font-size: 1rem; + font-weight: 600; +} + +.error { + color: var(--ifm-color-danger); + margin: 0; +} diff --git a/docs/src/components/Converter.tsx b/docs/src/components/Converter.tsx index d488987e..2cc0d004 100644 --- a/docs/src/components/Converter.tsx +++ b/docs/src/components/Converter.tsx @@ -1,69 +1,51 @@ -/* eslint-disable no-eval */ -/* eslint-disable no-nested-ternary */ -import React, { useState } from 'react'; - -import ErrorBoundary from '@docusaurus/ErrorBoundary'; - +import React, { useId, useState } from 'react'; import styles from './Converter.module.css'; -import { - DAVNamespaceString, - formatFilters, - formatProps, - safeGuard, - xml2js, - js2xml, -} from './ConverterBase'; +import { convertInput } from './ConverterBase'; type ConverterProps = { - content: any; + content: unknown; variant: 'prop' | 'filter' | 'xml' | 'xml-reverse'; }; -export const Converter = (props: ConverterProps) => { - const { variant, content } = props; - const [input, setInput] = useState( - typeof content === 'string' ? content : JSON.stringify(content, null, 2) +export const Converter = ({ variant, content }: ConverterProps) => { + const id = useId(); + const [input, setInput] = useState( + typeof content === 'string' ? content : JSON.stringify(content, null, 2), ); - + let output = ''; + let error = ''; + try { + output = convertInput(variant, input); + } catch (reason) { + error = reason instanceof Error ? reason.message : 'Conversion failed'; + } return ( - ( -
-

conversion failed, reason: {error.message}.

- -
- )} - > -
-

{variant}:

-