// @generated by scripts/build-runtime.mjs; do not edit. // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader. import { decodeSnapshot, encodeSnapshot } from "./chunk-HGV24P4T.ts"; import { SyncBackendConflictError, SyncBackendPublicationOutcomeUnknownError } from "./chunk-5MQ76BK5.ts"; import { portableSnapshotSelection } from "./chunk-YQ6UW7IF.ts"; // src/webdav-backend.ts import { createHash, randomUUID } from "node:crypto"; // src/webdav-client.ts import { XMLParser } from "fast-xml-parser"; var JSON_LIMIT = 1024 * 1024; var ERROR_LIMIT = 64 * 1024; var SNAPSHOT_LIMIT = 256 * 1024 * 1024; var XML_LIMIT = 1024 * 1024; var REQUEST_TIMEOUT_MS = 3e4; var MAX_REDIRECTS = 3; var WebDavHttpError = class extends Error { constructor(message, status, options) { super(message, options); this.status = status; this.name = "WebDavHttpError"; } status; }; var WebDavPreconditionError = class extends WebDavHttpError { constructor(message = "WebDAV precondition failed.") { super(message, 412); this.name = "WebDavPreconditionError"; } }; var WebDavClient = class { constructor(config, signal, timeoutMs = REQUEST_TIMEOUT_MS) { this.config = config; this.signal = signal; this.timeoutMs = timeoutMs; this.baseUrl = new URL(config.profile.url); assertSafeAuthenticatedUrl(this.baseUrl); if (!config.profile.username || !config.profile.password || config.profile.username.includes(":") || hasControlCharacter(config.profile.username) || hasControlCharacter(config.profile.password)) { throw new Error("Invalid WebDAV credentials."); } this.authorization = `Basic ${Buffer.from(`${config.profile.username}:${config.profile.password}`).toString("base64")}`; } config; signal; timeoutMs; baseUrl; authorization; async getBuffer(remotePath) { const response = await this.request(remotePath, { method: "GET" }); if (response.status === 404) { await response.body?.cancel(); return { missing: true }; } await this.requireOk(response, "read"); return { value: await readBounded(response, SNAPSHOT_LIMIT, "WebDAV response is too large"), etag: response.headers.get("etag") ?? void 0, missing: false }; } async getJson(remotePath) { const response = await this.request(remotePath, { method: "GET" }); if (response.status === 404) { await response.body?.cancel(); return { missing: true }; } await this.requireOk(response, "read"); const bytes = await readBounded(response, JSON_LIMIT, "WebDAV JSON response is too large"); try { return { value: JSON.parse(bytes.toString("utf8")), etag: response.headers.get("etag") ?? void 0, missing: false }; } catch (error) { throw new Error("WebDAV JSON response is malformed.", { cause: error }); } } async putBuffer(remotePath, body, contentType, options = {}) { const headers = { "content-type": contentType }; if (options.ifAbsent) headers["if-none-match"] = "*"; if (options.ifMatch) headers["if-match"] = options.ifMatch; const response = await this.request(remotePath, { method: "PUT", headers, body }); if (response.status === 412) { await response.body?.cancel(); throw new WebDavPreconditionError(); } await this.requireOk(response, "write"); await response.body?.cancel(); return response.headers.get("etag") ?? void 0; } async putJson(remotePath, value, options = {}) { return this.putBuffer( remotePath, Buffer.from(JSON.stringify(value)), "application/json", options ); } async makeCollection(remotePath) { const response = await this.request(remotePath, { method: "MKCOL" }); if (response.status === 405) { await response.body?.cancel(); return false; } await this.requireOk(response, "create collection"); await response.body?.cancel(); return true; } async ensureCollection(remotePath) { const segments = safeSegments(remotePath); for (let index = 1; index <= segments.length; index += 1) { await this.makeCollection(segments.slice(0, index).join("/")); } } async listCollection(remotePath) { const response = await this.request(remotePath, { method: "PROPFIND", headers: { depth: "1", "content-type": "application/xml; charset=utf-8" }, body: '' }); if (response.status === 404) { await response.body?.cancel(); throw new WebDavHttpError("WebDAV collection is missing.", 404); } if (response.status !== 207) await this.requireOk(response, "list collection"); const xml = (await readBounded(response, XML_LIMIT, "WebDAV directory response is too large")).toString("utf8"); try { const parsed = new XMLParser({ removeNSPrefix: true, ignoreAttributes: false, processEntities: false }).parse(xml); if (!parsed.multistatus || parsed.multistatus.response === void 0) { throw new Error("Missing DAV multistatus response."); } const responses = asArray(parsed.multistatus.response); return responses.map(parseEntry); } catch (error) { throw new Error("WebDAV directory response is malformed.", { cause: error }); } } async delete(remotePath) { const response = await this.request(remotePath, { method: "DELETE" }); if (response.status === 404) { await response.body?.cancel(); return; } if (response.status === 207) { await response.body?.cancel(); throw new WebDavHttpError("WebDAV probe cleanup returned a partial response.", 207); } await this.requireOk(response, "delete probe resource"); await response.body?.cancel(); } async request(remotePath, init) { throwIfAborted(this.signal); let url = appendRemotePath(this.baseUrl, remotePath); for (let redirects = 0; ; redirects += 1) { const timeout = AbortSignal.timeout(this.timeoutMs); const signal = this.signal ? AbortSignal.any([this.signal, timeout]) : timeout; let response; try { response = await fetch(url, { ...init, headers: { accept: "*/*", authorization: this.authorization, ...headersObject(init.headers) }, redirect: "manual", signal }); } catch (error) { throwIfAborted(this.signal); throw new Error( `WebDAV ${String(init.method ?? "GET")} request failed for ${redactText(safeUrl(url), this.config)}.`, { cause: redactError(error, this.config) } ); } if (![301, 302, 303, 307, 308].includes(response.status)) return response; await response.body?.cancel(); const method = String(init.method ?? "GET").toUpperCase(); if ([301, 302, 303].includes(response.status) && method !== "GET" && method !== "HEAD") { throw new Error( `WebDAV refused an ambiguous HTTP ${response.status} redirect for ${method}; configure the canonical collection URL or require HTTP 307/308.` ); } if (redirects >= MAX_REDIRECTS) throw new Error("WebDAV redirect limit exceeded."); const location = response.headers.get("location"); if (!location) throw new Error("WebDAV redirect is missing Location."); const next = new URL(location, url); if (next.origin !== this.baseUrl.origin) { throw new Error("WebDAV refused a cross-origin authenticated redirect."); } url = next; } } async requireOk(response, action) { if (response.ok) return; const body = (await readBounded(response, ERROR_LIMIT, "WebDAV error body is too large")).toString("utf8").replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ").trim(); const detail = body ? `: ${redactText(body, this.config)}` : ""; throw new WebDavHttpError( `WebDAV ${action} failed with HTTP ${response.status}${detail}`, response.status ); } }; function assertSafeAuthenticatedUrl(url) { const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "::1"; if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) { throw new Error( "Invalid WebDAV URL: HTTPS is required and embedded credentials, query, or fragment are not allowed." ); } } function hasControlCharacter(value) { return [...value].some((character) => { const code = character.codePointAt(0) ?? 0; return code < 32 || code >= 127 && code <= 159; }); } function appendRemotePath(base, remotePath) { const url = new URL(base); const suffix = safeSegments(remotePath).map(encodeURIComponent).join("/"); url.pathname = `${base.pathname.replace(/\/+$/u, "")}/${suffix}`; return url; } function safeSegments(value) { const segments = value.replace(/^\/+|\/+$/gu, "").split("/").filter(Boolean); if (segments.some( (segment) => segment === "." || segment === ".." || // biome-ignore lint/suspicious/noControlCharactersInRegex: Remote path segments cannot contain controls. /[\u0000-\u001f]/u.test(segment) )) { throw new Error("Invalid WebDAV remote path."); } return segments; } async function readBounded(response, limit, message) { const length = Number(response.headers.get("content-length")); if (Number.isFinite(length) && length > limit) { await response.body?.cancel(); throw new Error(message); } if (!response.body) return Buffer.alloc(0); const reader = response.body.getReader(); const chunks = []; let total = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; total += value.byteLength; if (total > limit) throw new Error(message); chunks.push(Buffer.from(value)); } } catch (error) { await reader.cancel().catch(() => void 0); throw error; } return Buffer.concat(chunks, total); } function parseEntry(value) { if (!value || typeof value !== "object") throw new Error("Invalid WebDAV response entry."); const record = value; const href = typeof record.href === "string" ? record.href : void 0; if (!href) throw new Error("Invalid WebDAV response href."); const propstat = asArray(record.propstat).find((item) => { if (!item || typeof item !== "object") return false; return String(item.status ?? "").includes(" 200 "); }); const prop = propstat?.prop; return { href, etag: typeof prop?.getetag === "string" ? prop.getetag : void 0, collection: !!prop?.resourcetype && typeof prop.resourcetype === "object" && Object.hasOwn(prop.resourcetype, "collection") }; } function asArray(value) { if (value === void 0) return []; return Array.isArray(value) ? value : [value]; } function headersObject(headers) { return Object.fromEntries(new Headers(headers).entries()); } function safeUrl(url) { return `${url.protocol}//${url.host}${url.pathname}`; } function redactError(error, config) { const message = error instanceof Error ? error.message : String(error); return new Error(redactText(message, config)); } function redactText(value, config) { let result = value; for (const secret of [config.profile.username, config.profile.password, config.profile.url]) { if (!secret) continue; for (const variant of /* @__PURE__ */ new Set([secret, encodeURIComponent(secret)])) { result = result.replace(new RegExp(escapeRegExp(variant), "giu"), "[redacted]"); } } return result.replace(/basic\s+[a-z0-9+/=]+/giu, "Basic [redacted]").replace(/\?[^\s]*/gu, "?[redacted]"); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } function throwIfAborted(signal) { if (!signal?.aborted) return; throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError"); } // src/webdav-backend.ts var VERSION = 1; var POST_COMMIT_TIMEOUT_MS = 3e4; var WebDavSyncBackend = class { constructor(config, postCommitTimeoutMs = POST_COMMIT_TIMEOUT_MS) { this.config = config; this.postCommitTimeoutMs = postCommitTimeoutMs; assertSafeDestination(config); this.identity = webDavBackendIdentity(config); this.destination = webDavStorageLocation(config); } config; postCommitTimeoutMs; identity; destination; conditionalVerified = false; checksums = /* @__PURE__ */ new Map(); capabilityCheck; get capability() { return this.conditionalVerified ? "atomic-conditional" : "conditional-required"; } sameRevision(left, right) { return left === right; } async readHead(signal) { const object = await new WebDavClient(this.config, signal).getJson( latestPath(this.config) ); throwIfAborted2(signal); if (object.missing) return void 0; const pointer = requirePointer(object.value, this.config.destination.namespace); this.registerChecksum(pointer.snapshot, pointer.sha256); return remoteHead(pointer, this.identity, object.etag); } async readSnapshot(reference, signal) { requireSnapshotReference(reference); const expectedChecksum = this.checksums.get(reference) ?? await this.resolveChecksum(reference, signal); const object = await new WebDavClient(this.config, signal).getBuffer( snapshotPath(this.config, reference) ); throwIfAborted2(signal); if (!object.value) throw new Error(`Snapshot not found: ${reference}`); if (!expectedChecksum || sha256(object.value) !== expectedChecksum) { throw new Error("Remote snapshot checksum mismatch."); } const snapshot = await decodeSnapshot(object.value, { signal }); if (snapshot.id !== reference || snapshot.profile !== this.config.destination.namespace) { throw new Error("Remote snapshot identity mismatch."); } validateSnapshotBundle(snapshot); return snapshot; } async publishSnapshot(snapshot, expected, options = {}) { throwIfAborted2(options.signal); assertSnapshotIdentity(snapshot, this.config.destination.namespace); await this.ensureAtomicConditions(options.signal); throwIfAborted2(options.signal); const client = new WebDavClient(this.config, options.signal); await client.ensureCollection(snapshotsPath(this.config)); const encoded = await encodeSnapshot(snapshot); throwIfAborted2(options.signal); const pointer = pointerFor(this.config, snapshot, sha256(encoded)); const stagedPath = snapshotPath(this.config, snapshot.id); try { await client.putBuffer(stagedPath, encoded, "application/gzip", { ifAbsent: true }); } catch (error) { if (!(error instanceof WebDavPreconditionError)) throw error; const existing = await client.getBuffer(stagedPath); if (!existing.value || sha256(existing.value) !== pointer.sha256) { throw new SyncBackendConflictError( `Immutable snapshot id already exists with different content: ${snapshot.id}` ); } } const currentObject = await client.getJson(latestPath(this.config)); throwIfAborted2(options.signal); const current = currentObject.missing ? void 0 : remoteHead( requirePointer(currentObject.value, this.config.destination.namespace), this.identity, currentObject.etag ); if (!matchesExpected(current, expected)) { throw new SyncBackendConflictError( "Remote changed while pushing. Run /sync pull first, then retry.", { currentHead: current } ); } const condition = publicationCondition(currentObject, expected, this.identity); throwIfAborted2(options.signal); options.onCommit?.(); const commitClient = new WebDavClient( this.config, AbortSignal.timeout(this.postCommitTimeoutMs), this.postCommitTimeoutMs ); try { await commitClient.putJson(latestPath(this.config), pointer, condition); } catch (error) { if (error instanceof WebDavPreconditionError) { const latest = await this.readHeadAfterCommit(commitClient).catch(() => void 0); throw new SyncBackendConflictError("Remote changed while publishing the WebDAV pointer.", { currentHead: latest }); } throw new SyncBackendPublicationOutcomeUnknownError( `Remote publication outcome is unknown: ${errorMessage(error)}`, { cause: error } ); } let verifiedObject; try { verifiedObject = await commitClient.getJson(latestPath(this.config)); } catch (error) { throw new SyncBackendPublicationOutcomeUnknownError( `Remote snapshot may be active, but publication could not be verified: ${errorMessage(error)}`, { cause: error } ); } let verified; try { verified = requirePointer(verifiedObject.value, this.config.destination.namespace); } catch (error) { throw new SyncBackendPublicationOutcomeUnknownError( `Remote snapshot may be active, but publication verification was malformed: ${errorMessage(error)}`, { cause: error } ); } if (!samePointer(pointer, verified)) { throw new SyncBackendConflictError( "Remote latest changed immediately after push. Run /sync status before continuing.", { phase: "after-commit", currentHead: remoteHead(verified, this.identity, verifiedObject.etag), candidateMayHaveBeenActive: true } ); } if (!strongEtag(verifiedObject.etag)) { throw new SyncBackendPublicationOutcomeUnknownError( "Remote snapshot is active, but the WebDAV server returned no strong ETag." ); } this.registerChecksum(verified.snapshot, verified.sha256); const head = remoteHead(verified, this.identity, verifiedObject.etag); const warning = await this.updateHistorySafely(commitClient, pointer); return { head, warnings: warning ? [warning] : [] }; } async listHistory(signal) { const object = await new WebDavClient(this.config, signal).getJson(historyPath(this.config)); throwIfAborted2(signal); if (object.missing) return []; const pointers = requireHistory(object.value, this.config.destination.namespace); for (const pointer of pointers) this.registerChecksum(pointer.snapshot, pointer.sha256); return pointers.map(remoteHistoryEntry); } async diagnose(signal) { const diagnostics = [ { key: "webdav-url", level: "info", message: `webdav URL/TLS/auth: configured (${webDavStorageLocation(this.config)})` } ]; try { await this.runCapabilityProbe(signal); diagnostics.push( { key: "webdav-collection", level: "info", message: "webdav collection read/write: ok" }, { key: "webdav-conditional", level: "info", message: "webdav conditional publication: atomic-conditional (verified)" }, { key: "webdav-cleanup", level: "info", message: "webdav probe cleanup: ok" } ); } catch (error) { this.capabilityCheck = void 0; this.conditionalVerified = false; if (error instanceof Error && error.name === "AbortError") throw error; diagnostics.push({ key: "webdav-probe", level: "error", message: `webdav publication is read-only until diagnostics pass: ${errorMessage(error)}` }); return diagnostics; } try { diagnostics.push({ key: "webdav-history", level: "info", message: await this.reconcileActiveHistory(signal) }); } catch (error) { if (error instanceof Error && error.name === "AbortError") throw error; diagnostics.push({ key: "webdav-history", level: "error", message: `webdav history repair failed: ${errorMessage(error)}` }); } return diagnostics; } ensureAtomicConditions(signal) { if (this.capabilityCheck) return this.capabilityCheck; this.conditionalVerified = false; const check = this.runCapabilityProbe(signal).finally(() => { if (this.capabilityCheck === check) this.capabilityCheck = void 0; }); this.capabilityCheck = check; return check; } async runCapabilityProbe(signal) { throwIfAborted2(signal); const probeCollection = `${rootPath(this.config)}/.pi-sync-probes/${randomUUID()}`; const probe = `${probeCollection}/conditional.txt`; const client = new WebDavClient(this.config, signal); let created = false; let operationError; try { created = true; await client.ensureCollection(probeCollection); await client.listCollection(probeCollection); await client.putBuffer(probe, Buffer.from("first"), "text/plain", { ifAbsent: true }); const read = await client.getBuffer(probe); const etag = strongEtag(read.etag); if (!read.value || !etag) { throw new Error("WebDAV server did not return a strong ETag for the capability probe."); } await expectPrecondition( client.putBuffer(probe, Buffer.from("replace"), "text/plain", { ifAbsent: true }), "If-None-Match" ); await expectPrecondition( client.putBuffer(probe, Buffer.from("replace"), "text/plain", { ifMatch: '"pi-sync-deliberately-stale"' }), "If-Match" ); const unchanged = await client.getBuffer(probe); if (!unchanged.value?.equals(Buffer.from("first"))) { throw new Error("WebDAV server changed a probe despite a failed precondition."); } await client.putBuffer(probe, Buffer.from("second"), "text/plain", { ifMatch: etag }); const changed = await client.getBuffer(probe); const changedEtag = strongEtag(changed.etag); if (!changed.value?.equals(Buffer.from("second")) || !changedEtag || changedEtag === etag) { throw new Error( "WebDAV server did not rotate its strong ETag after changing the capability probe." ); } } catch (error) { operationError = error; } if (created) { try { await new WebDavClient(this.config, void 0).delete(probeCollection); } catch (cleanupError) { const operationDetail = operationError ? `WebDAV probe failed: ${errorMessage(operationError)}; ` : ""; throw new Error( `${operationDetail}probe cleanup also failed; remove ${probeCollection}: ${errorMessage(cleanupError)}`, { cause: operationError ?? cleanupError } ); } } if (operationError) throw operationError; this.conditionalVerified = true; } async resolveChecksum(reference, signal) { await this.readHead(signal); if (this.checksums.has(reference)) return this.checksums.get(reference); await this.listHistory(signal); return this.checksums.get(reference); } registerChecksum(reference, checksum) { const known = this.checksums.get(reference); if (known && known !== checksum) { throw new Error("Remote snapshot reference was rebound to a different checksum."); } this.checksums.set(reference, checksum); } async readHeadAfterCommit(client) { const object = await client.getJson(latestPath(this.config)); if (object.missing) return void 0; return remoteHead( requirePointer(object.value, this.config.destination.namespace), this.identity, object.etag ); } async reconcileActiveHistory(signal) { const client = new WebDavClient(this.config, signal); const object = await client.getJson(latestPath(this.config)); throwIfAborted2(signal); if (object.missing) return "webdav history: no active snapshot"; const pointer = requirePointer(object.value, this.config.destination.namespace); this.registerChecksum(pointer.snapshot, pointer.sha256); const repaired = await this.ensureHistoryPointer(client, pointer); return repaired ? "webdav history: repaired active snapshot entry" : "webdav history: active snapshot entry present"; } async ensureHistoryPointer(client, pointer) { const object = await client.getJson( historyPath(this.config) ); const snapshots = object.missing ? [] : requireHistory(object.value, this.config.destination.namespace); const existing = snapshots.find((entry) => entry.snapshot === pointer.snapshot); if (existing) { if (!samePointer(existing, pointer)) { throw new Error("Remote history rebound an immutable snapshot reference."); } return false; } const next = [...snapshots, pointer].slice(-100); const condition = object.missing ? { ifAbsent: true } : { ifMatch: requireStrongEtag(object.etag, "history") }; await client.putJson( historyPath(this.config), { version: VERSION, snapshots: next }, condition ); return true; } async updateHistorySafely(client, pointer) { try { await this.ensureHistoryPointer(client, pointer); return void 0; } catch (error) { return `Remote snapshot is active, but history could not be updated: ${errorMessage(error)}. Run /sync doctor before relying on history.`; } } }; function rootPath(config) { return config.destination.path; } function latestPath(config) { return joinRemote(rootPath(config), "latest.json"); } function historyPath(config) { return joinRemote(rootPath(config), "history.json"); } function snapshotsPath(config) { return joinRemote(rootPath(config), "snapshots"); } function snapshotPath(config, reference) { requireSnapshotReference(reference); return joinRemote(snapshotsPath(config), `${reference}.json.gz`); } function webDavBackendIdentity(config) { return `webdav:${sha256( Buffer.from(JSON.stringify([secretFreeUrl(config.profile.url), config.destination.path])) )}`; } function webDavStorageLocation(config) { const url = new URL(secretFreeUrl(config.profile.url)); return `${url.host} \xB7 ${rootPath(config)}`; } function pointerFor(config, snapshot, checksum) { return { version: VERSION, profile: config.destination.namespace, snapshot: snapshot.id, sha256: checksum, createdAt: snapshot.createdAt, machine: snapshot.machine, syncSessions: snapshot.syncSessions === true || snapshot.files.some((file) => file.path.startsWith("sessions/")), ...snapshot.selection === void 0 ? {} : { selection: snapshot.selection } }; } function remoteHead(pointer, identity, etag) { return { snapshotRef: pointer.snapshot, snapshotId: pointer.snapshot, revision: encodeRevision(identity, etag, pointer), createdAt: pointer.createdAt, machine: pointer.machine, syncSessions: pointer.syncSessions === true, ...pointer.selection === void 0 ? {} : { selection: pointer.selection } }; } function encodeRevision(identity, etag, pointer) { return `webdav-v1:${Buffer.from(JSON.stringify({ identity, etag: etag ?? null, pointer: sha256(Buffer.from(canonicalJson(pointer))) })).toString("base64url")}`; } function decodeRevision(value, identity) { try { if (!value.startsWith("webdav-v1:")) return void 0; const parsed = JSON.parse(Buffer.from(value.slice(10), "base64url").toString("utf8")); if (parsed.identity !== identity || typeof parsed.etag !== "string") return void 0; return strongEtag(parsed.etag); } catch { return void 0; } } function publicationCondition(current, expected, identity) { if (expected.kind === "missing") return { ifAbsent: true }; const etag = decodeRevision(expected.revision, identity) ?? strongEtag(current.etag); if (!etag) throw new SyncBackendConflictError("WebDAV head has no usable strong ETag."); return { ifMatch: etag }; } async function expectPrecondition(operation, header) { try { await operation; } catch (error) { if (error instanceof WebDavPreconditionError) return; throw error; } throw new Error(`WebDAV server ignored ${header}; publication is read-only for safety.`); } function requireStrongEtag(value, resource) { const etag = strongEtag(value); if (!etag) throw new Error(`WebDAV ${resource} has no strong ETag.`); return etag; } function strongEtag(value) { return value && !value.startsWith("W/") && /^"[^"\r\n]+"$/u.test(value) ? value : void 0; } function requirePointer(value, expectedProfile) { if (!value || value.version !== VERSION || value.profile !== expectedProfile || !isSafeReference(value.snapshot) || !/^[0-9a-f]{64}$/u.test(value.sha256) || !isSafeMetadata(value.createdAt, 64) || !isSafeMetadata(value.machine, 256) || value.syncSessions !== void 0 && typeof value.syncSessions !== "boolean") { throw new Error("Remote latest pointer is malformed."); } if (value.selection !== void 0) portableSnapshotSelection(value.selection); return value; } function requireHistory(value, expectedProfile) { if (!value || value.version !== VERSION || !Array.isArray(value.snapshots)) { throw new Error("Remote history is malformed."); } const pointers = value.snapshots.map((item) => requirePointer(item, expectedProfile)); if (new Set(pointers.map((item) => item.snapshot)).size !== pointers.length) { throw new Error("Remote history contains duplicate snapshot references."); } return pointers; } function remoteHistoryEntry(pointer) { return { snapshotRef: pointer.snapshot, snapshotId: pointer.snapshot, createdAt: pointer.createdAt, machine: pointer.machine, syncSessions: pointer.syncSessions === true }; } function assertSnapshotIdentity(snapshot, namespace) { if (snapshot.version !== VERSION || snapshot.profile !== namespace || !isSafeReference(snapshot.id) || !isSafeMetadata(snapshot.createdAt, 64) || !isSafeMetadata(snapshot.machine, 256) || !Array.isArray(snapshot.files)) { throw new Error("Invalid snapshot identity for WebDAV publication."); } } function validateSnapshotBundle(snapshot) { const paths = /* @__PURE__ */ new Set(); for (const file of snapshot.files) { if (!file || !isSafeSnapshotPath(file.path) || paths.has(file.path) || typeof file.contentBase64 !== "string" || typeof file.sha256 !== "string") { throw new Error("Remote snapshot bundle is malformed."); } const content = Buffer.from(file.contentBase64, "base64"); if (content.toString("base64") !== file.contentBase64 || sha256(content) !== file.sha256) { throw new Error("Remote snapshot bundle is malformed."); } paths.add(file.path); } } function isSafeSnapshotPath(value) { if (typeof value !== "string" || !value || value.length > 4096 || value.includes("\\")) { return false; } if ([...value].some((character) => { const code = character.codePointAt(0) ?? 0; return code < 32 || code >= 127 && code <= 159; })) { return false; } return value.split("/").every((segment) => segment && segment !== "." && segment !== ".."); } function assertSafeDestination(config) { const url = new URL(config.profile.url); if (url.username || url.password || url.search || url.hash) throw new Error("Invalid WebDAV URL."); for (const value of [config.destination.path, config.destination.namespace]) { if (!value || value.includes("\\") || [...value].some((character) => { const code = character.codePointAt(0) ?? 0; return code < 32 || code >= 127 && code <= 159; }) || value.split("/").some((part) => part === "." || part === "..")) { throw new Error("Invalid WebDAV storage location."); } } } function isSafeMetadata(value, maxLength) { return typeof value === "string" && value.length > 0 && value.length <= maxLength && ![...value].some((character) => { const code = character.codePointAt(0) ?? 0; return code < 32 || code >= 127 && code <= 159; }); } function requireSnapshotReference(reference) { if (!isSafeReference(reference)) throw new Error("Invalid WebDAV snapshot reference."); } function isSafeReference(value) { return !!value && value.length <= 512 && // biome-ignore lint/suspicious/noControlCharactersInRegex: Remote references cannot contain terminal controls. !/[\\/\u0000-\u001f\u007f-\u009f]/u.test(value) && value !== "." && value !== ".."; } function matchesExpected(head, expected) { return expected.kind === "missing" ? head === void 0 : head?.revision === expected.revision; } function samePointer(left, right) { return canonicalJson(left) === canonicalJson(right); } function canonicalJson(value) { if (Array.isArray(value)) return JSON.stringify(value.map((item) => JSON.parse(canonicalJson(item)))); if (!value || typeof value !== "object") return JSON.stringify(value); return JSON.stringify( Object.fromEntries( Object.keys(value).sort().map((key) => [key, JSON.parse(canonicalJson(value[key]))]) ) ); } function joinRemote(...parts) { return parts.flatMap((part) => part.split("/")).filter(Boolean).join("/"); } function secretFreeUrl(value) { const url = new URL(value); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; return url.toString(); } function sha256(value) { return createHash("sha256").update(value).digest("hex"); } function throwIfAborted2(signal) { if (!signal?.aborted) return; throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError"); } function errorMessage(error) { if (error instanceof WebDavHttpError) return error.message; return error instanceof Error ? error.message : String(error); } export { WebDavSyncBackend, historyPath, latestPath, rootPath, snapshotPath, snapshotsPath, webDavBackendIdentity }; //# sourceMappingURL=webdav-backend-ZD5RHRIU.ts.map