zen-fs-config 0.5.39 → 0.5.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ import {
9
9
  registerBackend,
10
10
  unregisterBackend,
11
11
  wrapZenFSFileSystem
12
- } from "./chunk-6OS2F54Z.mjs";
12
+ } from "./chunk-S2S3XBK2.mjs";
13
13
  export {
14
14
  createBackend,
15
15
  getAccountFields,
@@ -122,6 +122,9 @@ async function wrapZenFSFileSystem(config) {
122
122
  if (typeof isolatedFS.writeFileWithMtime === "function") {
123
123
  backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
124
124
  }
125
+ if (typeof isolatedFS.shouldSync === "function") {
126
+ backend.shouldSync = (...args) => isolatedFS.shouldSync(...args);
127
+ }
125
128
  return backend;
126
129
  }
127
130
  var inMemoryCounter = 0;
@@ -185,7 +188,8 @@ var OPTIONAL_BACKEND_GLOBALS = {
185
188
  // third-party Gitee widget) would shadow the real backend and break mounting
186
189
  // with "Invalid mount configuration".
187
190
  "zen-fs-gitee": ["ZenFSGitee"],
188
- "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
191
+ "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"],
192
+ "zen-fs-webdav": ["ZenFSWebDAV"]
189
193
  };
190
194
  async function importOptionalBackend(pkg) {
191
195
  const candidates = OPTIONAL_BACKEND_GLOBALS[pkg];
@@ -273,9 +277,9 @@ registerBackend("GitHub", async (options) => {
273
277
  registerBackend("Gitee", async (options) => {
274
278
  const _gt = await importOptionalBackend("zen-fs-gitee");
275
279
  const Gitee = _gt?.Gitee ?? _gt?.default;
276
- if (typeof Gitee !== "function") {
280
+ if (!Gitee || typeof Gitee !== "function" && typeof Gitee.create !== "function") {
277
281
  throw new Error(
278
- `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
282
+ `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a usable backend (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
279
283
  );
280
284
  }
281
285
  return wrapZenFSFileSystem({
@@ -302,6 +306,39 @@ registerBackend("Gitee", async (options) => {
302
306
  // identity = the Git repo location
303
307
  identityFields: ["owner", "repo", "branch", "baseUrl"]
304
308
  });
309
+ async function resolveRemoteStorageHref(href) {
310
+ const raw = String(href ?? "").trim();
311
+ if (!raw) throw new Error("RemoteStorage href is required.");
312
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw.replace(/\/+$/, "");
313
+ const match = raw.match(/^(?:acct:)?([^@/]+)@([^@/]+)$/);
314
+ if (!match) {
315
+ throw new Error(
316
+ `RemoteStorage href "${raw}" is neither an absolute URL nor a user address (user@domain).`
317
+ );
318
+ }
319
+ const user = match[1];
320
+ const domain = match[2];
321
+ const webfingerUrl = `https://${domain}/.well-known/webfinger?resource=acct:${encodeURIComponent(user)}@${encodeURIComponent(domain)}`;
322
+ let response;
323
+ try {
324
+ response = await globalThis.fetch(webfingerUrl, {
325
+ headers: { Accept: "application/jrd+json, application/json" }
326
+ });
327
+ } catch (err) {
328
+ throw new Error(
329
+ `RemoteStorage WebFinger request failed (${webfingerUrl}): ${err instanceof Error ? err.message : String(err)}`
330
+ );
331
+ }
332
+ if (!response.ok) {
333
+ throw new Error(`RemoteStorage WebFinger lookup failed (HTTP ${response.status}): ${webfingerUrl}`);
334
+ }
335
+ const jrd = await response.json();
336
+ const link = (jrd?.links ?? []).find(
337
+ (item) => item && item.rel === "http://tools.ietf.org/id/draft-dejong-remotestorage" && item.href
338
+ );
339
+ if (!link) throw new Error(`WebFinger response has no RemoteStorage link: ${webfingerUrl}`);
340
+ return String(link.href).replace(/\/+$/, "");
341
+ }
305
342
  registerBackend("RemoteStorage", async (options) => {
306
343
  const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
307
344
  const RemoteStorageFileSystem = _rs?.RemoteStorageFileSystem ?? _rs?.default ?? (typeof _rs === "function" ? _rs : void 0);
@@ -311,7 +348,7 @@ registerBackend("RemoteStorage", async (options) => {
311
348
  );
312
349
  }
313
350
  return new RemoteStorageFileSystem({
314
- href: options.href,
351
+ href: await resolveRemoteStorageHref(options.href),
315
352
  token: options.token,
316
353
  basePath: options.basePath || void 0,
317
354
  preciseMtime: true,
@@ -322,7 +359,7 @@ registerBackend("RemoteStorage", async (options) => {
322
359
  label: "RemoteStorage",
323
360
  icon: "\u{1F4E1}",
324
361
  fields: [
325
- { key: "href", label: "User Address (href)", type: "text", placeholder: "user@5apps.com", required: true },
362
+ { key: "href", label: "User Address / Storage URL (href)", type: "text", placeholder: "user@5apps.com \u6216 https://storage.5apps.com/user", required: true },
326
363
  { key: "token", label: "Bearer Token", type: "password", placeholder: "rs-xxxxxxxx", required: true },
327
364
  { key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
328
365
  ],
@@ -333,6 +370,53 @@ registerBackend("RemoteStorage", async (options) => {
333
370
  // connections to the same endpoint with different tuning are deduplicated.
334
371
  identityFields: ["href", "token", "basePath"]
335
372
  });
373
+ registerBackend("WebDAV", async (options) => {
374
+ const _dav = await importOptionalBackend("zen-fs-webdav");
375
+ const SyncableWebDAVFS = _dav?.SyncableWebDAVFS ?? _dav?.default?.SyncableWebDAVFS ?? globalThis.ZenFSWebDAV?.SyncableWebDAVFS;
376
+ if (typeof SyncableWebDAVFS !== "function") {
377
+ throw new Error(
378
+ `Backend package "zen-fs-webdav" loaded, but its SyncableWebDAVFS export is not a constructor (typeof=${typeof SyncableWebDAVFS}). Run: npm install zen-fs-webdav, or load its UMD bundle via <script src=".../zen-fs-webdav.global.js"> so it exposes the global "ZenFSWebDAV".`
379
+ );
380
+ }
381
+ const instance = new SyncableWebDAVFS({
382
+ baseUrl: options.baseUrl,
383
+ username: options.username,
384
+ password: options.password,
385
+ token: options.token,
386
+ headers: options.headers,
387
+ timeout: options.timeout,
388
+ preciseMtime: true
389
+ });
390
+ if (options.purgeLegacyMtime === true) {
391
+ try {
392
+ await instance.purgeLegacyMtimeSidecars({ root: options.basePath ?? "/" });
393
+ } catch {
394
+ }
395
+ }
396
+ return instance;
397
+ }, {
398
+ type: "WebDAV",
399
+ label: "WebDAV",
400
+ icon: "\u2601\uFE0F",
401
+ fields: [
402
+ { key: "baseUrl", label: "Base URL", type: "text", placeholder: "https://dav.example.com/remote.php/dav/files/user/", required: true },
403
+ { key: "username", label: "Username", type: "text", placeholder: "admin" },
404
+ { key: "password", label: "Password", type: "password" },
405
+ { key: "token", label: "Token", type: "password", placeholder: "Bearer token (optional)" },
406
+ { key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" },
407
+ { key: "purgeLegacyMtime", label: "Purge legacy .mtime sidecars", type: "select", options: [
408
+ { value: "false", label: "no (default)" },
409
+ { value: "true", label: "yes (one-time migration)" }
410
+ ] }
411
+ ],
412
+ defaultOptions: { baseUrl: "", username: "", password: "", token: "", basePath: "/", purgeLegacyMtime: "false" },
413
+ // accountFields: credentials live on the endpoint, so a data-sync group can
414
+ // reuse them from an already-connected config-sync WebDAV backend.
415
+ accountFields: ["username", "password", "token"],
416
+ // identity = the WebDAV endpoint. Credentials are auth, not location, so two
417
+ // connections to the same URL are deduplicated as one replica.
418
+ identityFields: ["baseUrl"]
419
+ });
336
420
 
337
421
  export {
338
422
  registerBackend,
package/dist/index.js CHANGED
@@ -165,6 +165,9 @@ async function wrapZenFSFileSystem(config) {
165
165
  if (typeof isolatedFS.writeFileWithMtime === "function") {
166
166
  backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
167
167
  }
168
+ if (typeof isolatedFS.shouldSync === "function") {
169
+ backend.shouldSync = (...args) => isolatedFS.shouldSync(...args);
170
+ }
168
171
  return backend;
169
172
  }
170
173
  function _cacheGet(key) {
@@ -212,6 +215,39 @@ async function importOptionalBackend(pkg) {
212
215
  );
213
216
  }
214
217
  }
218
+ async function resolveRemoteStorageHref(href) {
219
+ const raw = String(href ?? "").trim();
220
+ if (!raw) throw new Error("RemoteStorage href is required.");
221
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw.replace(/\/+$/, "");
222
+ const match = raw.match(/^(?:acct:)?([^@/]+)@([^@/]+)$/);
223
+ if (!match) {
224
+ throw new Error(
225
+ `RemoteStorage href "${raw}" is neither an absolute URL nor a user address (user@domain).`
226
+ );
227
+ }
228
+ const user = match[1];
229
+ const domain = match[2];
230
+ const webfingerUrl = `https://${domain}/.well-known/webfinger?resource=acct:${encodeURIComponent(user)}@${encodeURIComponent(domain)}`;
231
+ let response;
232
+ try {
233
+ response = await globalThis.fetch(webfingerUrl, {
234
+ headers: { Accept: "application/jrd+json, application/json" }
235
+ });
236
+ } catch (err) {
237
+ throw new Error(
238
+ `RemoteStorage WebFinger request failed (${webfingerUrl}): ${err instanceof Error ? err.message : String(err)}`
239
+ );
240
+ }
241
+ if (!response.ok) {
242
+ throw new Error(`RemoteStorage WebFinger lookup failed (HTTP ${response.status}): ${webfingerUrl}`);
243
+ }
244
+ const jrd = await response.json();
245
+ const link = (jrd?.links ?? []).find(
246
+ (item) => item && item.rel === "http://tools.ietf.org/id/draft-dejong-remotestorage" && item.href
247
+ );
248
+ if (!link) throw new Error(`WebFinger response has no RemoteStorage link: ${webfingerUrl}`);
249
+ return String(link.href).replace(/\/+$/, "");
250
+ }
215
251
  var registry, metadataRegistry, inMemoryCounter, _memCache, OPTIONAL_BACKEND_GLOBALS;
216
252
  var init_backend_registry = __esm({
217
253
  "src/backend-registry.ts"() {
@@ -260,7 +296,8 @@ var init_backend_registry = __esm({
260
296
  // third-party Gitee widget) would shadow the real backend and break mounting
261
297
  // with "Invalid mount configuration".
262
298
  "zen-fs-gitee": ["ZenFSGitee"],
263
- "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
299
+ "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"],
300
+ "zen-fs-webdav": ["ZenFSWebDAV"]
264
301
  };
265
302
  registerBackend("GitHub", async (options) => {
266
303
  const _gh = await importOptionalBackend("zen-fs-github");
@@ -322,9 +359,9 @@ var init_backend_registry = __esm({
322
359
  registerBackend("Gitee", async (options) => {
323
360
  const _gt = await importOptionalBackend("zen-fs-gitee");
324
361
  const Gitee = _gt?.Gitee ?? _gt?.default;
325
- if (typeof Gitee !== "function") {
362
+ if (!Gitee || typeof Gitee !== "function" && typeof Gitee.create !== "function") {
326
363
  throw new Error(
327
- `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
364
+ `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a usable backend (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
328
365
  );
329
366
  }
330
367
  return wrapZenFSFileSystem({
@@ -360,7 +397,7 @@ var init_backend_registry = __esm({
360
397
  );
361
398
  }
362
399
  return new RemoteStorageFileSystem({
363
- href: options.href,
400
+ href: await resolveRemoteStorageHref(options.href),
364
401
  token: options.token,
365
402
  basePath: options.basePath || void 0,
366
403
  preciseMtime: true,
@@ -371,7 +408,7 @@ var init_backend_registry = __esm({
371
408
  label: "RemoteStorage",
372
409
  icon: "\u{1F4E1}",
373
410
  fields: [
374
- { key: "href", label: "User Address (href)", type: "text", placeholder: "user@5apps.com", required: true },
411
+ { key: "href", label: "User Address / Storage URL (href)", type: "text", placeholder: "user@5apps.com \u6216 https://storage.5apps.com/user", required: true },
375
412
  { key: "token", label: "Bearer Token", type: "password", placeholder: "rs-xxxxxxxx", required: true },
376
413
  { key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
377
414
  ],
@@ -382,6 +419,53 @@ var init_backend_registry = __esm({
382
419
  // connections to the same endpoint with different tuning are deduplicated.
383
420
  identityFields: ["href", "token", "basePath"]
384
421
  });
422
+ registerBackend("WebDAV", async (options) => {
423
+ const _dav = await importOptionalBackend("zen-fs-webdav");
424
+ const SyncableWebDAVFS = _dav?.SyncableWebDAVFS ?? _dav?.default?.SyncableWebDAVFS ?? globalThis.ZenFSWebDAV?.SyncableWebDAVFS;
425
+ if (typeof SyncableWebDAVFS !== "function") {
426
+ throw new Error(
427
+ `Backend package "zen-fs-webdav" loaded, but its SyncableWebDAVFS export is not a constructor (typeof=${typeof SyncableWebDAVFS}). Run: npm install zen-fs-webdav, or load its UMD bundle via <script src=".../zen-fs-webdav.global.js"> so it exposes the global "ZenFSWebDAV".`
428
+ );
429
+ }
430
+ const instance = new SyncableWebDAVFS({
431
+ baseUrl: options.baseUrl,
432
+ username: options.username,
433
+ password: options.password,
434
+ token: options.token,
435
+ headers: options.headers,
436
+ timeout: options.timeout,
437
+ preciseMtime: true
438
+ });
439
+ if (options.purgeLegacyMtime === true) {
440
+ try {
441
+ await instance.purgeLegacyMtimeSidecars({ root: options.basePath ?? "/" });
442
+ } catch {
443
+ }
444
+ }
445
+ return instance;
446
+ }, {
447
+ type: "WebDAV",
448
+ label: "WebDAV",
449
+ icon: "\u2601\uFE0F",
450
+ fields: [
451
+ { key: "baseUrl", label: "Base URL", type: "text", placeholder: "https://dav.example.com/remote.php/dav/files/user/", required: true },
452
+ { key: "username", label: "Username", type: "text", placeholder: "admin" },
453
+ { key: "password", label: "Password", type: "password" },
454
+ { key: "token", label: "Token", type: "password", placeholder: "Bearer token (optional)" },
455
+ { key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" },
456
+ { key: "purgeLegacyMtime", label: "Purge legacy .mtime sidecars", type: "select", options: [
457
+ { value: "false", label: "no (default)" },
458
+ { value: "true", label: "yes (one-time migration)" }
459
+ ] }
460
+ ],
461
+ defaultOptions: { baseUrl: "", username: "", password: "", token: "", basePath: "/", purgeLegacyMtime: "false" },
462
+ // accountFields: credentials live on the endpoint, so a data-sync group can
463
+ // reuse them from an already-connected config-sync WebDAV backend.
464
+ accountFields: ["username", "password", "token"],
465
+ // identity = the WebDAV endpoint. Credentials are auth, not location, so two
466
+ // connections to the same URL are deduplicated as one replica.
467
+ identityFields: ["baseUrl"]
468
+ });
385
469
  }
386
470
  });
387
471
 
@@ -1367,6 +1451,12 @@ function identityOptions(type, options) {
1367
1451
  for (const f of fields) {
1368
1452
  if (options && options[f] !== void 0) subset[f] = options[f];
1369
1453
  }
1454
+ if (type === "Gitee" || type === "GitHub") {
1455
+ const defaultEndpoint = type === "Gitee" ? "https://gitee.com/api/v5" : "https://api.github.com";
1456
+ const raw = options && options.baseUrl;
1457
+ const value = typeof raw === "string" && raw.trim() !== "" ? raw : defaultEndpoint;
1458
+ subset["baseUrl"] = value.replace(/\/+$/, "");
1459
+ }
1370
1460
  return subset;
1371
1461
  }
1372
1462
  function backendDedupKey(desc) {
@@ -2757,10 +2847,15 @@ var ConfigRepo = class {
2757
2847
  }
2758
2848
  const descPath = this.appDataGroupFilePath(id);
2759
2849
  try {
2760
- await this.cachedFS.unlink(descPath);
2761
- } catch {
2850
+ await this.deleteFile(descPath);
2851
+ } catch (err) {
2852
+ log2.log(`[ConfigRepo] removeAppDataGroup: deleteFile failed for "${id}":`, err);
2853
+ try {
2854
+ await this.cachedFS.unlink(descPath);
2855
+ } catch {
2856
+ }
2857
+ await this.unlinkVersionSidecar(this.cachedFS, descPath);
2762
2858
  }
2763
- await this.unlinkVersionSidecar(this.cachedFS, descPath);
2764
2859
  log2.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
2765
2860
  }
2766
2861
  async listAccountBackends() {
@@ -3143,12 +3238,25 @@ async function connect(appId, options = {}) {
3143
3238
  onConflict: options.onConflict,
3144
3239
  syncPollIntervalMs: options.syncPollIntervalMs
3145
3240
  });
3146
- const dataGroup2 = await repo2.createAppDataGroup("default", []);
3241
+ const appDataGroups2 = [];
3242
+ const descriptors2 = await repo2.listAppDataGroups();
3243
+ for (const d of descriptors2) {
3244
+ try {
3245
+ appDataGroups2.push(await repo2.getAppDataGroup(d.id));
3246
+ } catch (err) {
3247
+ log3(`connect: failed to load app data group "${d.id}":`, err);
3248
+ }
3249
+ }
3250
+ let dataGroup2 = appDataGroups2[0];
3251
+ if (!dataGroup2) {
3252
+ dataGroup2 = await repo2.createAppDataGroup("default", []);
3253
+ appDataGroups2.push(dataGroup2);
3254
+ }
3147
3255
  return {
3148
3256
  groupType: options.groupType ?? "config-sync",
3149
3257
  repo: repo2,
3150
3258
  dataGroup: dataGroup2,
3151
- appDataGroups: [dataGroup2]
3259
+ appDataGroups: appDataGroups2
3152
3260
  };
3153
3261
  }
3154
3262
  const { type, options: backendOptions } = options.backendInfo;
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  registerBackend,
10
10
  unregisterBackend,
11
11
  wrapZenFSFileSystem
12
- } from "./chunk-6OS2F54Z.mjs";
12
+ } from "./chunk-S2S3XBK2.mjs";
13
13
 
14
14
  // src/config-repo.ts
15
15
  import {
@@ -351,7 +351,7 @@ function registerFolderBackend() {
351
351
  const pathMod = await import("path");
352
352
  const dir = options.path;
353
353
  const store = new FolderStore({ path: dir, label: options.label, fs, pathMod });
354
- return (await import("./backend-registry-3FVPYPZL.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
354
+ return (await import("./backend-registry-RBKWNJZI.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
355
355
  },
356
356
  {
357
357
  type: "Folder",
@@ -911,6 +911,12 @@ function identityOptions(type, options) {
911
911
  for (const f of fields) {
912
912
  if (options && options[f] !== void 0) subset[f] = options[f];
913
913
  }
914
+ if (type === "Gitee" || type === "GitHub") {
915
+ const defaultEndpoint = type === "Gitee" ? "https://gitee.com/api/v5" : "https://api.github.com";
916
+ const raw = options && options.baseUrl;
917
+ const value = typeof raw === "string" && raw.trim() !== "" ? raw : defaultEndpoint;
918
+ subset["baseUrl"] = value.replace(/\/+$/, "");
919
+ }
914
920
  return subset;
915
921
  }
916
922
  function backendDedupKey(desc) {
@@ -2301,10 +2307,15 @@ var ConfigRepo = class {
2301
2307
  }
2302
2308
  const descPath = this.appDataGroupFilePath(id);
2303
2309
  try {
2304
- await this.cachedFS.unlink(descPath);
2305
- } catch {
2310
+ await this.deleteFile(descPath);
2311
+ } catch (err) {
2312
+ log2.log(`[ConfigRepo] removeAppDataGroup: deleteFile failed for "${id}":`, err);
2313
+ try {
2314
+ await this.cachedFS.unlink(descPath);
2315
+ } catch {
2316
+ }
2317
+ await this.unlinkVersionSidecar(this.cachedFS, descPath);
2306
2318
  }
2307
- await this.unlinkVersionSidecar(this.cachedFS, descPath);
2308
2319
  log2.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
2309
2320
  }
2310
2321
  async listAccountBackends() {
@@ -2686,12 +2697,25 @@ async function connect(appId, options = {}) {
2686
2697
  onConflict: options.onConflict,
2687
2698
  syncPollIntervalMs: options.syncPollIntervalMs
2688
2699
  });
2689
- const dataGroup2 = await repo2.createAppDataGroup("default", []);
2700
+ const appDataGroups2 = [];
2701
+ const descriptors2 = await repo2.listAppDataGroups();
2702
+ for (const d of descriptors2) {
2703
+ try {
2704
+ appDataGroups2.push(await repo2.getAppDataGroup(d.id));
2705
+ } catch (err) {
2706
+ log3(`connect: failed to load app data group "${d.id}":`, err);
2707
+ }
2708
+ }
2709
+ let dataGroup2 = appDataGroups2[0];
2710
+ if (!dataGroup2) {
2711
+ dataGroup2 = await repo2.createAppDataGroup("default", []);
2712
+ appDataGroups2.push(dataGroup2);
2713
+ }
2690
2714
  return {
2691
2715
  groupType: options.groupType ?? "config-sync",
2692
2716
  repo: repo2,
2693
2717
  dataGroup: dataGroup2,
2694
- appDataGroups: [dataGroup2]
2718
+ appDataGroups: appDataGroups2
2695
2719
  };
2696
2720
  }
2697
2721
  const { type, options: backendOptions } = options.backendInfo;
@@ -24478,6 +24478,9 @@ var ZenFSConfig = (() => {
24478
24478
  if (typeof isolatedFS.writeFileWithMtime === "function") {
24479
24479
  backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
24480
24480
  }
24481
+ if (typeof isolatedFS.shouldSync === "function") {
24482
+ backend.shouldSync = (...args) => isolatedFS.shouldSync(...args);
24483
+ }
24481
24484
  return backend;
24482
24485
  }
24483
24486
  function _cacheGet(key) {
@@ -24525,6 +24528,39 @@ var ZenFSConfig = (() => {
24525
24528
  );
24526
24529
  }
24527
24530
  }
24531
+ async function resolveRemoteStorageHref(href) {
24532
+ const raw = String(href ?? "").trim();
24533
+ if (!raw) throw new Error("RemoteStorage href is required.");
24534
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw.replace(/\/+$/, "");
24535
+ const match = raw.match(/^(?:acct:)?([^@/]+)@([^@/]+)$/);
24536
+ if (!match) {
24537
+ throw new Error(
24538
+ `RemoteStorage href "${raw}" is neither an absolute URL nor a user address (user@domain).`
24539
+ );
24540
+ }
24541
+ const user = match[1];
24542
+ const domain = match[2];
24543
+ const webfingerUrl = `https://${domain}/.well-known/webfinger?resource=acct:${encodeURIComponent(user)}@${encodeURIComponent(domain)}`;
24544
+ let response;
24545
+ try {
24546
+ response = await globalThis.fetch(webfingerUrl, {
24547
+ headers: { Accept: "application/jrd+json, application/json" }
24548
+ });
24549
+ } catch (err2) {
24550
+ throw new Error(
24551
+ `RemoteStorage WebFinger request failed (${webfingerUrl}): ${err2 instanceof Error ? err2.message : String(err2)}`
24552
+ );
24553
+ }
24554
+ if (!response.ok) {
24555
+ throw new Error(`RemoteStorage WebFinger lookup failed (HTTP ${response.status}): ${webfingerUrl}`);
24556
+ }
24557
+ const jrd = await response.json();
24558
+ const link5 = (jrd?.links ?? []).find(
24559
+ (item) => item && item.rel === "http://tools.ietf.org/id/draft-dejong-remotestorage" && item.href
24560
+ );
24561
+ if (!link5) throw new Error(`WebFinger response has no RemoteStorage link: ${webfingerUrl}`);
24562
+ return String(link5.href).replace(/\/+$/, "");
24563
+ }
24528
24564
  var registry, metadataRegistry, inMemoryCounter, _memCache, OPTIONAL_BACKEND_GLOBALS;
24529
24565
  var init_backend_registry = __esm({
24530
24566
  "src/backend-registry.ts"() {
@@ -24573,7 +24609,8 @@ var ZenFSConfig = (() => {
24573
24609
  // third-party Gitee widget) would shadow the real backend and break mounting
24574
24610
  // with "Invalid mount configuration".
24575
24611
  "zen-fs-gitee": ["ZenFSGitee"],
24576
- "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"]
24612
+ "zen-fs-remotestoragejs": ["ZenFSRemoteStorage", "RemoteStorageFileSystem"],
24613
+ "zen-fs-webdav": ["ZenFSWebDAV"]
24577
24614
  };
24578
24615
  registerBackend("GitHub", async (options) => {
24579
24616
  const _gh = await importOptionalBackend("zen-fs-github");
@@ -24635,9 +24672,9 @@ var ZenFSConfig = (() => {
24635
24672
  registerBackend("Gitee", async (options) => {
24636
24673
  const _gt = await importOptionalBackend("zen-fs-gitee");
24637
24674
  const Gitee = _gt?.Gitee ?? _gt?.default;
24638
- if (typeof Gitee !== "function") {
24675
+ if (!Gitee || typeof Gitee !== "function" && typeof Gitee.create !== "function") {
24639
24676
  throw new Error(
24640
- `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a constructor (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
24677
+ `Backend package "zen-fs-gitee" loaded, but its Gitee export is not a usable backend (typeof=${typeof Gitee}). Load its UMD bundle so it exposes the global "ZenFSGitee".`
24641
24678
  );
24642
24679
  }
24643
24680
  return wrapZenFSFileSystem({
@@ -24673,7 +24710,7 @@ var ZenFSConfig = (() => {
24673
24710
  );
24674
24711
  }
24675
24712
  return new RemoteStorageFileSystem({
24676
- href: options.href,
24713
+ href: await resolveRemoteStorageHref(options.href),
24677
24714
  token: options.token,
24678
24715
  basePath: options.basePath || void 0,
24679
24716
  preciseMtime: true,
@@ -24684,7 +24721,7 @@ var ZenFSConfig = (() => {
24684
24721
  label: "RemoteStorage",
24685
24722
  icon: "\u{1F4E1}",
24686
24723
  fields: [
24687
- { key: "href", label: "User Address (href)", type: "text", placeholder: "user@5apps.com", required: true },
24724
+ { key: "href", label: "User Address / Storage URL (href)", type: "text", placeholder: "user@5apps.com \u6216 https://storage.5apps.com/user", required: true },
24688
24725
  { key: "token", label: "Bearer Token", type: "password", placeholder: "rs-xxxxxxxx", required: true },
24689
24726
  { key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
24690
24727
  ],
@@ -24695,6 +24732,53 @@ var ZenFSConfig = (() => {
24695
24732
  // connections to the same endpoint with different tuning are deduplicated.
24696
24733
  identityFields: ["href", "token", "basePath"]
24697
24734
  });
24735
+ registerBackend("WebDAV", async (options) => {
24736
+ const _dav = await importOptionalBackend("zen-fs-webdav");
24737
+ const SyncableWebDAVFS = _dav?.SyncableWebDAVFS ?? _dav?.default?.SyncableWebDAVFS ?? globalThis.ZenFSWebDAV?.SyncableWebDAVFS;
24738
+ if (typeof SyncableWebDAVFS !== "function") {
24739
+ throw new Error(
24740
+ `Backend package "zen-fs-webdav" loaded, but its SyncableWebDAVFS export is not a constructor (typeof=${typeof SyncableWebDAVFS}). Run: npm install zen-fs-webdav, or load its UMD bundle via <script src=".../zen-fs-webdav.global.js"> so it exposes the global "ZenFSWebDAV".`
24741
+ );
24742
+ }
24743
+ const instance = new SyncableWebDAVFS({
24744
+ baseUrl: options.baseUrl,
24745
+ username: options.username,
24746
+ password: options.password,
24747
+ token: options.token,
24748
+ headers: options.headers,
24749
+ timeout: options.timeout,
24750
+ preciseMtime: true
24751
+ });
24752
+ if (options.purgeLegacyMtime === true) {
24753
+ try {
24754
+ await instance.purgeLegacyMtimeSidecars({ root: options.basePath ?? "/" });
24755
+ } catch {
24756
+ }
24757
+ }
24758
+ return instance;
24759
+ }, {
24760
+ type: "WebDAV",
24761
+ label: "WebDAV",
24762
+ icon: "\u2601\uFE0F",
24763
+ fields: [
24764
+ { key: "baseUrl", label: "Base URL", type: "text", placeholder: "https://dav.example.com/remote.php/dav/files/user/", required: true },
24765
+ { key: "username", label: "Username", type: "text", placeholder: "admin" },
24766
+ { key: "password", label: "Password", type: "password" },
24767
+ { key: "token", label: "Token", type: "password", placeholder: "Bearer token (optional)" },
24768
+ { key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" },
24769
+ { key: "purgeLegacyMtime", label: "Purge legacy .mtime sidecars", type: "select", options: [
24770
+ { value: "false", label: "no (default)" },
24771
+ { value: "true", label: "yes (one-time migration)" }
24772
+ ] }
24773
+ ],
24774
+ defaultOptions: { baseUrl: "", username: "", password: "", token: "", basePath: "/", purgeLegacyMtime: "false" },
24775
+ // accountFields: credentials live on the endpoint, so a data-sync group can
24776
+ // reuse them from an already-connected config-sync WebDAV backend.
24777
+ accountFields: ["username", "password", "token"],
24778
+ // identity = the WebDAV endpoint. Credentials are auth, not location, so two
24779
+ // connections to the same URL are deduplicated as one replica.
24780
+ identityFields: ["baseUrl"]
24781
+ });
24698
24782
  }
24699
24783
  });
24700
24784
 
@@ -27300,6 +27384,12 @@ var ZenFSConfig = (() => {
27300
27384
  for (const f of fields) {
27301
27385
  if (options && options[f] !== void 0) subset[f] = options[f];
27302
27386
  }
27387
+ if (type === "Gitee" || type === "GitHub") {
27388
+ const defaultEndpoint = type === "Gitee" ? "https://gitee.com/api/v5" : "https://api.github.com";
27389
+ const raw = options && options.baseUrl;
27390
+ const value = typeof raw === "string" && raw.trim() !== "" ? raw : defaultEndpoint;
27391
+ subset["baseUrl"] = value.replace(/\/+$/, "");
27392
+ }
27303
27393
  return subset;
27304
27394
  }
27305
27395
  function backendDedupKey(desc) {
@@ -28690,10 +28780,15 @@ var ZenFSConfig = (() => {
28690
28780
  }
28691
28781
  const descPath = this.appDataGroupFilePath(id);
28692
28782
  try {
28693
- await this.cachedFS.unlink(descPath);
28694
- } catch {
28783
+ await this.deleteFile(descPath);
28784
+ } catch (err2) {
28785
+ log6.log(`[ConfigRepo] removeAppDataGroup: deleteFile failed for "${id}":`, err2);
28786
+ try {
28787
+ await this.cachedFS.unlink(descPath);
28788
+ } catch {
28789
+ }
28790
+ await this.unlinkVersionSidecar(this.cachedFS, descPath);
28695
28791
  }
28696
- await this.unlinkVersionSidecar(this.cachedFS, descPath);
28697
28792
  log6.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
28698
28793
  }
28699
28794
  async listAccountBackends() {
@@ -29076,12 +29171,25 @@ var ZenFSConfig = (() => {
29076
29171
  onConflict: options.onConflict,
29077
29172
  syncPollIntervalMs: options.syncPollIntervalMs
29078
29173
  });
29079
- const dataGroup2 = await repo2.createAppDataGroup("default", []);
29174
+ const appDataGroups2 = [];
29175
+ const descriptors2 = await repo2.listAppDataGroups();
29176
+ for (const d of descriptors2) {
29177
+ try {
29178
+ appDataGroups2.push(await repo2.getAppDataGroup(d.id));
29179
+ } catch (err2) {
29180
+ log7(`connect: failed to load app data group "${d.id}":`, err2);
29181
+ }
29182
+ }
29183
+ let dataGroup2 = appDataGroups2[0];
29184
+ if (!dataGroup2) {
29185
+ dataGroup2 = await repo2.createAppDataGroup("default", []);
29186
+ appDataGroups2.push(dataGroup2);
29187
+ }
29080
29188
  return {
29081
29189
  groupType: options.groupType ?? "config-sync",
29082
29190
  repo: repo2,
29083
29191
  dataGroup: dataGroup2,
29084
- appDataGroups: [dataGroup2]
29192
+ appDataGroups: appDataGroups2
29085
29193
  };
29086
29194
  }
29087
29195
  const { type, options: backendOptions } = options.backendInfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.39",
3
+ "version": "0.5.41",
4
4
  "description": "Distributed config management library built on ZenFS, zen-fs-cache, and zen-fs-sync",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -52,7 +52,8 @@
52
52
  "zen-fs-sync": ">=0.4.7",
53
53
  "zen-fs-github": "*",
54
54
  "zen-fs-gitee": "*",
55
- "zen-fs-remotestoragejs": "*"
55
+ "zen-fs-remotestoragejs": "*",
56
+ "zen-fs-webdav": "*"
56
57
  },
57
58
  "dependencies": {
58
59
  "@richard432/localstorage-logger": "^0.1.0"
@@ -72,6 +73,9 @@
72
73
  },
73
74
  "zen-fs-remotestoragejs": {
74
75
  "optional": true
76
+ },
77
+ "zen-fs-webdav": {
78
+ "optional": true
75
79
  }
76
80
  },
77
81
  "devDependencies": {