@vrtmrz/livesync-commonlib 0.2.3 → 0.3.1

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.
Files changed (91) hide show
  1. package/README.md +46 -8
  2. package/dist/API/DirectFileManipulatorV2.d.ts +8 -1
  3. package/dist/API/DirectFileManipulatorV2.js +52 -79
  4. package/dist/API/DirectFileWatchOwner.d.ts +24 -0
  5. package/dist/API/DirectFileWatchOwner.js +256 -0
  6. package/dist/API/processSetting.d.ts +1 -0
  7. package/dist/API/processSetting.js +3 -3
  8. package/dist/common/models/setting.const.defaults.js +1 -0
  9. package/dist/common/models/setting.const.qr.js +1 -0
  10. package/dist/common/models/setting.policy.d.ts +4 -0
  11. package/dist/common/models/setting.policy.js +1 -0
  12. package/dist/common/models/setting.type.d.ts +3 -1
  13. package/dist/common/models/shared.definition.configNames.js +1 -2
  14. package/dist/common/models/tweak.definition.d.ts +2 -1
  15. package/dist/common/pathNamespace.d.ts +4 -0
  16. package/dist/common/pathNamespace.js +10 -0
  17. package/dist/common/pathNormalization.d.ts +2 -0
  18. package/dist/common/pathNormalization.js +7 -2
  19. package/dist/common/settingConstants.d.ts +1 -0
  20. package/dist/common/settingConstants.js +4 -0
  21. package/dist/managers/ChunkFetcher.d.ts +3 -0
  22. package/dist/managers/ChunkFetcher.js +20 -13
  23. package/dist/managers/LiveSyncManagers.d.ts +2 -0
  24. package/dist/managers/LiveSyncManagers.js +1 -0
  25. package/dist/managers/chunkFetchSource.d.ts +14 -0
  26. package/dist/managers/chunkFetchSource.js +12 -0
  27. package/dist/platform/AmbiguousNormalisedPathError.d.ts +16 -0
  28. package/dist/platform/AmbiguousNormalisedPathError.js +21 -0
  29. package/dist/platform/StoragePathResolver.d.ts +15 -0
  30. package/dist/platform/StoragePathResolver.js +113 -0
  31. package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.d.ts +74 -0
  32. package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.js +169 -0
  33. package/dist/platform/browser/index.d.ts +1 -0
  34. package/dist/platform/browser/index.js +2 -0
  35. package/dist/platform/browser/storage.d.ts +23 -0
  36. package/dist/platform/browser/storage.js +138 -0
  37. package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.d.ts +79 -0
  38. package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.js +218 -0
  39. package/dist/platform/node/index.d.ts +1 -0
  40. package/dist/platform/node/index.js +2 -0
  41. package/dist/platform/node/storage.d.ts +30 -1
  42. package/dist/platform/node/storage.js +88 -0
  43. package/dist/platform/pathComparison.d.ts +53 -0
  44. package/dist/platform/pathComparison.js +39 -0
  45. package/dist/platform/pathTypes.d.ts +154 -0
  46. package/dist/platform/pathTypes.js +0 -0
  47. package/dist/platform/storagePathResolution.d.ts +13 -0
  48. package/dist/platform/storagePathResolution.js +9 -0
  49. package/dist/pouchdb/LiveSyncDBFunctions.js +5 -0
  50. package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
  51. package/dist/pouchdb/LiveSyncLocalDB.js +13 -10
  52. package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
  53. package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
  54. package/dist/replication/RemoteResource.d.ts +3 -0
  55. package/dist/replication/couchdb/LiveSyncReplicator.js +5 -3
  56. package/dist/replication/index.d.ts +2 -0
  57. package/dist/replication/index.js +2 -0
  58. package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
  59. package/dist/replication/journal/JournalSyncCore.js +35 -19
  60. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
  61. package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
  62. package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
  63. package/dist/replication/trystero/TrysteroReplicator.d.ts +9 -5
  64. package/dist/replication/trystero/TrysteroReplicator.js +98 -70
  65. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
  66. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
  67. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
  68. package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
  69. package/dist/serviceModules/Rebuilder.d.ts +1 -0
  70. package/dist/serviceModules/Rebuilder.js +35 -6
  71. package/dist/services/base/FileProcessingService.d.ts +2 -2
  72. package/dist/services/base/RemoteConnection.d.ts +2 -0
  73. package/dist/services/base/RemoteService.d.ts +1 -1
  74. package/dist/services/base/RemoteService.js +13 -5
  75. package/dist/services/implements/injectable/InjectableVaultService.d.ts +1 -1
  76. package/dist/services/lib/couchdbHttpCache.d.ts +5 -0
  77. package/dist/services/lib/couchdbHttpCache.js +36 -0
  78. package/dist/storage.d.ts +4 -0
  79. package/dist/storage.js +19 -0
  80. package/dist/string_and_binary/path.js +4 -7
  81. package/dist/worker/bgWorker.js +1 -1
  82. package/docs/adr/2026_11_shared_storage_path_resolution.md +73 -0
  83. package/docs/conflict-resolution.md +1 -1
  84. package/docs/design_docs/storage_path_resolution.md +305 -0
  85. package/docs/development.md +15 -1
  86. package/docs/platform-storage.md +61 -0
  87. package/docs/proven-in-use.md +30 -13
  88. package/docs/releasing.md +27 -0
  89. package/docs/service-feature-composition.md +7 -1
  90. package/docs/unicode-path-identity.md +104 -12
  91. package/package.json +10 -2
@@ -28,6 +28,9 @@ import { compatGlobal } from "../../common/coreEnvFunctions.js";
28
28
  import { computeKeyedId, configuredIdKey } from "../../common/idDerivation.js";
29
29
  import { getWebCrypto } from "../../mods.js";
30
30
  import { uint8ArrayToHexString } from "../../string_and_binary/convert.js";
31
+ import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
32
+ import { readLocalP2PPathIdentity, readRemoteP2PPathIdentity } from "./TrysteroReplicator.pathIdentity.js";
33
+ import { assertDatabasePathIdentity } from "../../pouchdb/pathIdentityCompatibility.js";
31
34
  import {
32
35
  fromP2PReplicationWireResult,
33
36
  toP2PReplicationWireResult,
@@ -205,6 +208,11 @@ class TrysteroReplicator {
205
208
  this._onSetup = false;
206
209
  }
207
210
  async getTweakSettings(fromPeerId) {
211
+ const currentSettings = this.currentSettings;
212
+ const pathNormalizationVersion = getPathNormalizationVersion(currentSettings);
213
+ if (pathNormalizationVersion === 1) {
214
+ await assertDatabasePathIdentity(this.db, currentSettings, true);
215
+ }
208
216
  const allSettings = JSON.parse(JSON.stringify(this.settings));
209
217
  for (const key in allSettings) {
210
218
  if (key == "encrypt") {
@@ -223,6 +231,7 @@ class TrysteroReplicator {
223
231
  const idKey = configuredIdKey({ idDerivationVersion, idDerivationKey: idSettings.idDerivationKey ?? "" });
224
232
  return {
225
233
  ...allSettings,
234
+ pathNormalizationVersion,
226
235
  idDerivationVersion,
227
236
  idDerivationProof: idKey && idSettings.encrypt && idSettings.usePathObfuscation ? await computeKeyedId(idKey, "peer-agreement", fromPeerId) : ""
228
237
  };
@@ -553,6 +562,28 @@ class TrysteroReplicator {
553
562
  }
554
563
  if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
555
564
  const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
565
+ const rejection = await this.checkPeerForReplication(remotePeer, logLevel, signal);
566
+ if (rejection) return rejection;
567
+ Logger(`P2P Replicating from ${remotePeer}`, logLevel, "p2p-replicator");
568
+ if (this._replicateFromPeers.has(remotePeer)) {
569
+ Logger(`Replication from ${remotePeer} is already in progress`, logLevel, "p2p-replicator");
570
+ return p2pReplicationFailed(new Error("Replication from this peer is already in progress"));
571
+ }
572
+ this._replicateFromPeers.add(remotePeer);
573
+ this.dispatchStatus();
574
+ try {
575
+ return await this.transferFromPeer(remotePeer, fromStart, logLevel, signal);
576
+ } catch (e) {
577
+ if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
578
+ Logger("Error while P2P replicating", logLevel, "p2p-replicator");
579
+ Logger(e, LOG_LEVEL_VERBOSE);
580
+ return p2pReplicationFailed(e);
581
+ } finally {
582
+ this._replicateFromPeers.delete(remotePeer);
583
+ this.dispatchStatus();
584
+ }
585
+ }
586
+ async checkPeerForReplication(remotePeer, logLevel, signal) {
556
587
  Logger(`P2P Requesting Authentication to ${remotePeer}`, logLevel, "p2p-replicator");
557
588
  const authenticated = signal ? await this.requestAuthenticate(remotePeer, signal) : await this.requestAuthenticate(remotePeer);
558
589
  if (authenticated !== true) {
@@ -572,71 +603,62 @@ class TrysteroReplicator {
572
603
  Logger("Tweak values are not matched", logLevel, "p2p-replicator");
573
604
  return p2pReplicationFailed(new Error("Tweak values are not matched"));
574
605
  }
575
- Logger(`P2P Replicating from ${remotePeer}`, logLevel, "p2p-replicator");
576
- if (this._replicateFromPeers.has(remotePeer)) {
577
- Logger(`Replication from ${remotePeer} is already in progress`, logLevel, "p2p-replicator");
578
- return p2pReplicationFailed(new Error("Replication from this peer is already in progress"));
579
- }
580
- this._replicateFromPeers.add(remotePeer);
581
- this.dispatchStatus();
582
- try {
583
- if (!this.server) {
584
- throw new Error("Server is not available");
585
- }
586
- const connection = this.server.getConnection(remotePeer);
587
- const remoteDB = connection.getRemoteDB(signal);
588
- Logger(`P2P replicateFrom preparing remote DB info for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
589
- const remoteDBInfo = await remoteDB.info();
590
- Logger(
591
- `P2P replicateFrom remote DB info for ${remotePeer}: ${remoteDBInfo.db_name} seq=${remoteDBInfo.update_seq}`,
592
- LOG_LEVEL_VERBOSE,
593
- "p2p-replicator"
594
- );
595
- const localDBInfo = await this.db.info();
596
- Logger(
597
- `P2P replicateFrom local DB info for ${remotePeer}: ${localDBInfo.db_name} seq=${localDBInfo.update_seq}`,
598
- LOG_LEVEL_VERBOSE,
599
- "p2p-replicator"
600
- );
601
- Logger(`P2P replicateFrom entering replicateShim for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
602
- const outcome = await replicateShim(
603
- this.db,
604
- remoteDB,
605
- async (docs, info) => {
606
- await this._env.processReplicatedDocs(docs);
607
- void this.dispatchReplicationProgress(remotePeer, info);
608
- void this.acknowledgeProgress(remotePeer, info);
609
- void this.notifyProgress(remotePeer);
610
- Logger(
611
- `P2P Replication from ${remotePeer}
606
+ return void 0;
607
+ }
608
+ async transferFromPeer(remotePeer, fromStart, logLevel, signal) {
609
+ if (!this.server) {
610
+ throw new Error("Server is not available");
611
+ }
612
+ const connection = this.server.getConnection(remotePeer);
613
+ const remoteDB = connection.getRemoteDB(signal);
614
+ const pathNormalizationVersion = getPathNormalizationVersion(this.currentSettings);
615
+ const [localPathIdentity, remotePathIdentity] = await Promise.all([
616
+ readLocalP2PPathIdentity(this.db, pathNormalizationVersion),
617
+ readRemoteP2PPathIdentity(remoteDB, pathNormalizationVersion)
618
+ ]);
619
+ if (localPathIdentity !== pathNormalizationVersion || remotePathIdentity !== pathNormalizationVersion) {
620
+ const error = new Error("P2P database path identity does not match the configured path identity.");
621
+ Logger(error.message, logLevel, "p2p-replicator");
622
+ return p2pReplicationFailed(error);
623
+ }
624
+ Logger(`P2P replicateFrom preparing remote DB info for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
625
+ const remoteDBInfo = await remoteDB.info();
626
+ Logger(
627
+ `P2P replicateFrom remote DB info for ${remotePeer}: ${remoteDBInfo.db_name} seq=${remoteDBInfo.update_seq}`,
628
+ LOG_LEVEL_VERBOSE,
629
+ "p2p-replicator"
630
+ );
631
+ const localDBInfo = await this.db.info();
632
+ Logger(
633
+ `P2P replicateFrom local DB info for ${remotePeer}: ${localDBInfo.db_name} seq=${localDBInfo.update_seq}`,
634
+ LOG_LEVEL_VERBOSE,
635
+ "p2p-replicator"
636
+ );
637
+ Logger(`P2P replicateFrom entering replicateShim for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
638
+ const outcome = await replicateShim(
639
+ this.db,
640
+ remoteDB,
641
+ async (docs, info) => {
642
+ await this._env.processReplicatedDocs(docs);
643
+ void this.dispatchReplicationProgress(remotePeer, info);
644
+ void this.acknowledgeProgress(remotePeer, info);
645
+ void this.notifyProgress(remotePeer);
646
+ Logger(
647
+ `P2P Replication from ${remotePeer}
612
648
  ${info.lastSeq} / ${info.maxSeqInBatch})`,
613
- logLevel,
614
- "p2p-replicator"
615
- );
616
- },
617
- {
618
- live: false,
619
- rewind: fromStart,
620
- signal
621
- /*, batch_size: batchSize */
622
- }
623
- );
624
- Logger(`P2P replicateFrom replicateShim returned for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
625
- void this.acknowledgeProgress(remotePeer, void 0);
626
- if (outcome.status === "cancelled") {
627
- Logger(`P2P Replication from ${remotePeer} has been cancelled`, logLevel, "p2p-replicator");
628
- return P2P_REPLICATION_CANCELLED;
629
- }
630
- Logger(`P2P Replication from ${remotePeer} has been completed`, logLevel, "p2p-replicator");
631
- } catch (e) {
632
- if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
633
- Logger("Error while P2P replicating", logLevel, "p2p-replicator");
634
- Logger(e, LOG_LEVEL_VERBOSE);
635
- return p2pReplicationFailed(e);
636
- } finally {
637
- this._replicateFromPeers.delete(remotePeer);
638
- this.dispatchStatus();
649
+ logLevel,
650
+ "p2p-replicator"
651
+ );
652
+ },
653
+ { live: false, rewind: fromStart, signal }
654
+ );
655
+ Logger(`P2P replicateFrom replicateShim returned for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
656
+ void this.acknowledgeProgress(remotePeer, void 0);
657
+ if (outcome.status === "cancelled") {
658
+ Logger(`P2P Replication from ${remotePeer} has been cancelled`, logLevel, "p2p-replicator");
659
+ return P2P_REPLICATION_CANCELLED;
639
660
  }
661
+ Logger(`P2P Replication from ${remotePeer} has been completed`, logLevel, "p2p-replicator");
640
662
  return P2P_REPLICATION_COMPLETED;
641
663
  }
642
664
  notifyProgress(excludePeerId) {
@@ -739,12 +761,6 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
739
761
  Logger("Peer is not found", logLevel);
740
762
  return false;
741
763
  }
742
- if (this.platform === "pseudo-replicator") {
743
- return true;
744
- }
745
- if (peerPlatform === "pseudo-replicator") {
746
- return true;
747
- }
748
764
  const connection = this.server.getConnection(peerId);
749
765
  const challengeBytes = new Uint8Array(16);
750
766
  (await getWebCrypto()).getRandomValues(challengeBytes);
@@ -752,6 +768,15 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
752
768
  const tweakValues = await connection.invokeRemoteObjectFunction("getTweakSettings", [challenge], 5e3, signal);
753
769
  if (signal?.aborted) return false;
754
770
  const thisTweakValues = await this.getTweakSettings(challenge);
771
+ const localPathNormalizationVersion = getPathNormalizationVersion(thisTweakValues);
772
+ const peerPathNormalizationVersion = getPathNormalizationVersion(tweakValues);
773
+ if (localPathNormalizationVersion !== peerPathNormalizationVersion) {
774
+ Logger("Replication cancelled: The path identity mode does not match this peer.", logLevel);
775
+ return false;
776
+ }
777
+ if (this.platform === "pseudo-replicator" || peerPlatform === "pseudo-replicator") {
778
+ return true;
779
+ }
755
780
  if (thisTweakValues.passphrase !== tweakValues.passphrase) {
756
781
  Logger(
757
782
  "Replication cancelled: Passphrase is not matched\nCannot replicate to a remote database until the problem is resolved.",
@@ -760,7 +785,10 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
760
785
  return false;
761
786
  }
762
787
  if (thisTweakValues.encrypt !== tweakValues.encrypt || thisTweakValues.usePathObfuscation !== tweakValues.usePathObfuscation) {
763
- Logger("Replication cancelled: The encryption or path obfuscation settings do not match this peer.", logLevel);
788
+ Logger(
789
+ "Replication cancelled: The encryption or path obfuscation settings do not match this peer.",
790
+ logLevel
791
+ );
764
792
  return false;
765
793
  }
766
794
  if (thisTweakValues.encrypt && thisTweakValues.usePathObfuscation && ((thisTweakValues.idDerivationVersion ?? 0) !== (tweakValues.idDerivationVersion ?? 0) || (thisTweakValues.idDerivationProof ?? "") !== (tweakValues.idDerivationProof ?? ""))) {
@@ -0,0 +1,11 @@
1
+ import type { PathNormalizationVersion } from "../../common/models/setting.type.js";
2
+ export declare function readLocalP2PPathIdentity(database: {
3
+ get(id: string): Promise<unknown>;
4
+ }, configuredVersion: PathNormalizationVersion): Promise<PathNormalizationVersion>;
5
+ export declare function readRemoteP2PPathIdentity(database: {
6
+ bulkGet(options: {
7
+ docs: {
8
+ id: string;
9
+ }[];
10
+ }): Promise<unknown>;
11
+ }, configuredVersion: PathNormalizationVersion): Promise<PathNormalizationVersion>;
@@ -0,0 +1,72 @@
1
+ import { VERSIONING_DOCID } from "../../common/models/db.const.js";
2
+ import {
3
+ assessRemoteFeatureDocument,
4
+ describeRemoteFeatureRejection,
5
+ NFC_PATH_IDENTITY_FEATURE
6
+ } from "../../pouchdb/remoteFeatureCompatibility.js";
7
+ import { isErrorOfMissingDoc } from "../../pouchdb/utils_couchdb.js";
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ function hasOwnField(value, key) {
12
+ return Object.hasOwn(value, key);
13
+ }
14
+ function isVersionInfoResponse(value) {
15
+ if (!isRecord(value) || !Array.isArray(value.results) || value.results.length !== 1) return false;
16
+ const [result] = value.results;
17
+ return isRecord(result) && result.id === VERSIONING_DOCID && Array.isArray(result.docs) && result.docs.length === 1 && isRecord(result.docs[0]);
18
+ }
19
+ function isMissingDocumentResult(result) {
20
+ if (!hasOwnField(result, "error")) return false;
21
+ const error = result.error;
22
+ const errorDetails = isRecord(error) ? error : void 0;
23
+ const status = errorDetails?.status ?? result.status;
24
+ const errorName = errorDetails?.name ?? result.name;
25
+ const errorCode = typeof error === "string" ? error : errorDetails?.error;
26
+ return status === 404 || status === void 0 && (errorName === "not_found" || errorCode === "not_found");
27
+ }
28
+ function parseRemotePathIdentityDocument(response) {
29
+ if (!isVersionInfoResponse(response)) {
30
+ throw new Error("P2P remote versioninfo response is malformed.");
31
+ }
32
+ const [documentResult] = response.results[0].docs;
33
+ if (hasOwnField(documentResult, "ok")) {
34
+ const document = documentResult.ok;
35
+ if (!isRecord(document) || document._id !== VERSIONING_DOCID) {
36
+ throw new Error("P2P remote versioninfo response is malformed.");
37
+ }
38
+ return { kind: "found", document };
39
+ }
40
+ if (isMissingDocumentResult(documentResult)) return { kind: "missing" };
41
+ throw new Error("P2P remote versioninfo could not be read.");
42
+ }
43
+ function resolvePathIdentity(result, configuredVersion) {
44
+ if (result.kind === "missing") {
45
+ if (configuredVersion === 0) return 0;
46
+ throw new Error("P2P NFC database is missing path identity version information.");
47
+ }
48
+ const assessment = assessRemoteFeatureDocument(result.document);
49
+ if (assessment.status === "older-generation" && configuredVersion === 0) return 0;
50
+ if (assessment.status !== "supported") {
51
+ throw new Error(describeRemoteFeatureRejection(assessment));
52
+ }
53
+ return assessment.usedFeatures.includes(NFC_PATH_IDENTITY_FEATURE) ? 1 : 0;
54
+ }
55
+ async function readLocalP2PPathIdentity(database, configuredVersion) {
56
+ let result;
57
+ try {
58
+ result = { kind: "found", document: await database.get(VERSIONING_DOCID) };
59
+ } catch (error) {
60
+ if (!isErrorOfMissingDoc(error)) throw error;
61
+ result = { kind: "missing" };
62
+ }
63
+ return resolvePathIdentity(result, configuredVersion);
64
+ }
65
+ async function readRemoteP2PPathIdentity(database, configuredVersion) {
66
+ const response = await database.bulkGet({ docs: [{ id: VERSIONING_DOCID }] });
67
+ return resolvePathIdentity(parseRemotePathIdentityDocument(response), configuredVersion);
68
+ }
69
+ export {
70
+ readLocalP2PPathIdentity,
71
+ readRemoteP2PPathIdentity
72
+ };
@@ -7,6 +7,7 @@ import {
7
7
  splitP2PRelayUrls,
8
8
  splitP2PTurnServerUrls
9
9
  } from "../../common/models/setting.p2p.js";
10
+ import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
10
11
  function getP2PReplicatorConfigurationIdentity(settings) {
11
12
  const managedTurnSelected = hasManagedP2PTurnConfiguration(settings);
12
13
  const turnServerValue = settings.P2P_turnServers ?? "";
@@ -25,6 +26,7 @@ function getP2PReplicatorConfigurationIdentity(settings) {
25
26
  managedTurnSelected ? [settings.P2P_managedType, settings.P2P_managedId, settings.P2P_managedToken] : void 0,
26
27
  normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes),
27
28
  effectivePath,
29
+ getPathNormalizationVersion(settings),
28
30
  settings.P2P_useDiagRTC ?? false
29
31
  ]);
30
32
  }
@@ -15,7 +15,7 @@ import {
15
15
  supportedStopActiveTransfer
16
16
  } from "../index.js";
17
17
  import { getP2PReplicatorConfigurationIdentity } from "./p2pReplicatorConfigurationIdentity.js";
18
- import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
18
+ import { assertSupportedPathNormalizationVersion } from "../../common/pathNormalization.js";
19
19
  function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFactory, options = {}) {
20
20
  const service = createP2PService(
21
21
  {
@@ -63,7 +63,11 @@ function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFa
63
63
  kind: REMOTE_P2P,
64
64
  diagnosticName: "P2P",
65
65
  readiness: PEER_REPLICATION_READINESS,
66
- isConfigured: (settings) => settings.remoteType === REMOTE_P2P && settings.P2P_Enabled && getPathNormalizationVersion(settings) === 0,
66
+ isConfigured: (settings) => {
67
+ if (settings.remoteType !== REMOTE_P2P || !settings.P2P_Enabled) return false;
68
+ assertSupportedPathNormalizationVersion(settings.pathNormalizationVersion);
69
+ return true;
70
+ },
67
71
  configurationIdentity: getP2PReplicatorConfigurationIdentity,
68
72
  create: createP2PReplicator,
69
73
  remoteResources: NO_REMOTE_RESOURCE_CAPABILITIES,
@@ -66,6 +66,7 @@ export declare class ServiceRebuilder extends ServiceModuleBase<ServiceRebuilder
66
66
  private scheduleInitialisation;
67
67
  scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
68
68
  scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
69
+ private markRemoteLockedForRebuild;
69
70
  private _tryResetRemoteDatabase;
70
71
  private _onResetLocalDatabase;
71
72
  suspendAllSync(): Promise<void>;
@@ -26,6 +26,12 @@ const FAST_FETCH_RETRY_DELAYS = [2e3, 5e3, 1e4, 2e4];
26
26
  function canResetCentralRemote(replicator) {
27
27
  return "tryResetRemoteDatabase" in replicator && typeof replicator.tryResetRemoteDatabase === "function";
28
28
  }
29
+ function canCheckJournalPathIdentity(replicator) {
30
+ return "assertRemotePathIdentity" in replicator && typeof replicator.assertRemotePathIdentity === "function";
31
+ }
32
+ function canLockJournalForRebuild(replicator) {
33
+ return canResetCentralRemote(replicator) && "markRemoteLockedForRebuild" in replicator && typeof replicator.markRemoteLockedForRebuild === "function";
34
+ }
29
35
  class ServiceRebuilder extends ServiceModuleBase {
30
36
  events;
31
37
  appLifecycle;
@@ -100,7 +106,7 @@ Please enable them from the settings screen after setup is complete.`,
100
106
  notifyThresholdOfRemoteStorageSize: DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize
101
107
  });
102
108
  await this.control.applySettings();
103
- await this.replication.markLocked();
109
+ await this.markRemoteLockedForRebuild();
104
110
  await this._tryResetRemoteDatabase();
105
111
  await this.replication.markLocked();
106
112
  await delay(500);
@@ -145,7 +151,7 @@ Please enable them from the settings screen after setup is complete.`,
145
151
  }
146
152
  return;
147
153
  }
148
- await this.replication.markLocked();
154
+ await this.markRemoteLockedForRebuild();
149
155
  await this._tryResetRemoteDatabase();
150
156
  await this.replication.markLocked();
151
157
  await delay(500);
@@ -170,9 +176,6 @@ Please enable them from the settings screen after setup is complete.`,
170
176
  await assertDatabasePathIdentity(this.database.localDatabase.localDatabase, settings);
171
177
  }
172
178
  if (getPathNormalizationVersion(settings) === 0) return;
173
- if (settings.remoteType !== REMOTE_COUCHDB) {
174
- throw new Error("NFC path identity is supported only with CouchDB. No database has been reset.");
175
- }
176
179
  const paths = await this.storageAccess.getFileNames();
177
180
  const selected = [];
178
181
  const databaseReady = this.database.isDatabaseReady();
@@ -191,6 +194,19 @@ Please enable them from the settings screen after setup is complete.`,
191
194
  }
192
195
  async checkRemotePathIdentityBeforeFetch() {
193
196
  const settings = this.setting.currentSettings();
197
+ if (settings.remoteType === REMOTE_MINIO) {
198
+ const trial = await this.replicator.getNewReplicator(settings);
199
+ if (!trial) throw new Error("Could not create a Journal replicator for the Fetch identity check.");
200
+ try {
201
+ if (!canCheckJournalPathIdentity(trial)) {
202
+ throw new Error("The Journal replicator does not support the Fetch identity check.");
203
+ }
204
+ await trial.assertRemotePathIdentity(settings);
205
+ } finally {
206
+ await trial.closeReplication();
207
+ }
208
+ return;
209
+ }
194
210
  if (settings.remoteType !== REMOTE_COUCHDB) return;
195
211
  const remote = settings.couchDB_URI.replace(/\/+$/, "") + (settings.couchDB_DBNAME === "" ? "" : "/" + settings.couchDB_DBNAME);
196
212
  const connection = await this.remote.connect(
@@ -206,7 +222,8 @@ Please enable them from the settings screen after setup is complete.`,
206
222
  settings.useRequestAPI,
207
223
  async () => {
208
224
  throw new Error("A version document does not require decryption.");
209
- }
225
+ },
226
+ { useCouchDBCacheControlHeadersOnly: settings.useCouchDBCacheControlHeadersOnly === true }
210
227
  );
211
228
  if (typeof connection === "string") throw new Error(connection);
212
229
  try {
@@ -262,6 +279,18 @@ Please enable them from the settings screen after setup is complete.`,
262
279
  scheduleFetch(prepareBeforeRestart) {
263
280
  return this.scheduleInitialisation(FlagFilesHumanReadable.FETCH_ALL, prepareBeforeRestart);
264
281
  }
282
+ async markRemoteLockedForRebuild() {
283
+ const settings = this.setting.currentSettings();
284
+ if (settings.remoteType !== REMOTE_MINIO) {
285
+ await this.replication.markLocked();
286
+ return;
287
+ }
288
+ const replicator = this.replicator.getActiveReplicator();
289
+ if (!replicator || !canLockJournalForRebuild(replicator)) {
290
+ throw new Error("The active Journal replicator does not support safe reconstruction.");
291
+ }
292
+ await replicator.markRemoteLockedForRebuild(settings);
293
+ }
265
294
  async _tryResetRemoteDatabase() {
266
295
  const currentReplicator = this.replicator.getActiveReplicator();
267
296
  const settings = this.setting.currentSettings();
@@ -12,13 +12,13 @@ export declare class FileProcessingService<T extends ServiceContext = ServiceCon
12
12
  * Dispatch a legacy optional notification, without confirming persistence.
13
13
  * i.e., hidden files synchronisation or customisation sync.
14
14
  */
15
- readonly processOptionalFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(path: import("../../common/types.js").FilePath) => Promise<boolean>>;
15
+ readonly processOptionalFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(path: import("../../storage.js").FilePath) => Promise<boolean>>;
16
16
  /**
17
17
  * Complete optional file operations through the first applicable handler.
18
18
  * Hosts migrate all optional providers together. Once any typed provider is
19
19
  * registered, dispatch uses this contract exclusively.
20
20
  */
21
- readonly processOptionalFileEventWithResult?: import("../lib/HandlerUtils.js").FirstHandledFunction<(path: import("../../common/types.js").FilePath, options?: import("./IService.js").OptionalFileEventOptions) => Promise<import("../lib/HandlerUtils.js").HandlerOutcome<void, string>>>;
21
+ readonly processOptionalFileEventWithResult?: import("../lib/HandlerUtils.js").FirstHandledFunction<(path: import("../../storage.js").FilePath, options?: import("./IService.js").OptionalFileEventOptions) => Promise<import("../lib/HandlerUtils.js").HandlerOutcome<void, string>>>;
22
22
  /**
23
23
  * Commit any pending file events that have been queued for processing.
24
24
  */
@@ -8,6 +8,8 @@ export interface RemoteConnectionOpenOptions {
8
8
  readonly encryptionAlgorithm?: E2EEAlgorithm;
9
9
  /** Apply HKDF Metadata encryption to obfuscated internal entries on writes. */
10
10
  readonly encryptInternalMetadata?: boolean;
11
+ /** Use HTTP headers alone for cache control; an absent value is false. */
12
+ readonly useCouchDBCacheControlHeadersOnly?: boolean;
11
13
  /**
12
14
  * Cancels abort-capable requests made through the connection when the
13
15
  * owning operation is cancelled.
@@ -1,4 +1,4 @@
1
- import type { CouchDBCredentials, EntryDoc } from "../../common/types.js";
1
+ import { type CouchDBCredentials, type EntryDoc } from "../../common/types.js";
2
2
  import type { IRemoteService } from "./IService.js";
3
3
  import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
4
4
  import { type LOG_LEVEL } from "../../common/types.js";
@@ -1,3 +1,4 @@
1
+ import { E2EEAlgorithms } from "../../common/types.js";
1
2
  import { ServiceBase } from "./ServiceBase.js";
2
3
  import {
3
4
  LOG_LEVEL_DEBUG,
@@ -12,6 +13,7 @@ import { AuthorizationHeaderGenerator } from "../../replication/httplib.js";
12
13
  import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
13
14
  import { createInstanceLogFunction, MARK_LOG_NETWORK_ERROR } from "../lib/logUtils.js";
14
15
  import { runWithTrackedPhysicalRequest } from "../lib/remoteActivity.js";
16
+ import { createCouchDBHttpCache } from "../lib/couchdbHttpCache.js";
15
17
  import { LiveSyncError } from "../../common/LSError.js";
16
18
  function createRemoteConnectionScope(ownerSignal) {
17
19
  const controller = new AbortController();
@@ -188,6 +190,12 @@ class RemoteService extends ServiceBase {
188
190
  return "Network is offline";
189
191
  }
190
192
  const connectionScope = createRemoteConnectionScope(connectionOptions.signal);
193
+ const encryptionAlgorithm = connectionOptions.encryptionAlgorithm ?? this._settingService.currentSettings().E2EEAlgorithm;
194
+ const prepareCachedRequest = createCouchDBHttpCache(
195
+ uri,
196
+ typeof passphrase === "string" && passphrase !== "false" && encryptionAlgorithm !== E2EEAlgorithms.ForceV1 ? getPBKDF2Salt : void 0,
197
+ connectionOptions.useCouchDBCacheControlHeadersOnly === true
198
+ );
191
199
  const conf = {
192
200
  adapter: "http",
193
201
  auth: "username" in auth ? auth : void 0,
@@ -222,11 +230,12 @@ class RemoteService extends ServiceBase {
222
230
  if (!("username" in auth)) {
223
231
  headers.append("authorization", authHeader);
224
232
  }
233
+ const cachedRequest = await prepareCachedRequest(requestSrc, opts);
225
234
  try {
226
235
  const signal = connectionScope.combine(opts?.signal);
227
236
  const response = await this.performFetch(
228
- requestSrc,
229
- { ...opts, headers, signal, cache: "no-store" },
237
+ cachedRequest.request,
238
+ { ...opts, headers, signal, cache: cachedRequest.cache },
230
239
  useRequestAPI ? FetchMethod.native : FetchMethod.webCompat
231
240
  );
232
241
  return response;
@@ -243,8 +252,8 @@ class RemoteService extends ServiceBase {
243
252
  "Failed to fetch by native fetch API. Trying to fetch by API to get more information."
244
253
  );
245
254
  const resp2 = await this.performFetch(
246
- requestSrc,
247
- { ...opts, headers, cache: "no-store" },
255
+ cachedRequest.request,
256
+ { ...opts, headers, cache: cachedRequest.cache },
248
257
  FetchMethod.native
249
258
  );
250
259
  if (resp2.status / 100 == 2) {
@@ -279,7 +288,6 @@ class RemoteService extends ServiceBase {
279
288
  }
280
289
  }
281
290
  };
282
- const encryptionAlgorithm = connectionOptions.encryptionAlgorithm ?? this._settingService.currentSettings().E2EEAlgorithm;
283
291
  let db;
284
292
  try {
285
293
  db = new this._pouchDB(uri, conf);
@@ -4,6 +4,6 @@ export declare abstract class InjectableVaultService<T extends ServiceContext> e
4
4
  }
5
5
  export declare class InjectableVaultServiceCompat<T extends ServiceContext> extends InjectableVaultService<T> {
6
6
  isStorageInsensitive: import("../../lib/HandlerUtils.js").HandlerFunction<() => boolean, unknown>;
7
- getActiveFilePath: import("../../lib/HandlerUtils.js").HandlerFunction<() => import("../../../common/types.js").FilePath | undefined, unknown>;
7
+ getActiveFilePath: import("../../lib/HandlerUtils.js").HandlerFunction<() => import("../../../storage.js").FilePath | undefined, unknown>;
8
8
  isValidPath(path: string): boolean;
9
9
  }
@@ -0,0 +1,5 @@
1
+ /** Keep encrypted HTTP responses within the Security Seed which produced them. */
2
+ export declare function createCouchDBHttpCache(uri: string, getPBKDF2Salt?: () => Promise<Uint8Array<ArrayBuffer>>, headersOnly?: boolean): (request: string | Request, options?: RequestInit) => Promise<{
3
+ request: string | Request;
4
+ cache: RequestCache;
5
+ }>;
@@ -0,0 +1,36 @@
1
+ import { uint8ArrayToHexString } from "../../string_and_binary/convert.js";
2
+ const processEpoch = Math.floor(Date.now() / 1e3);
3
+ function createCouchDBHttpCache(uri, getPBKDF2Salt, headersOnly = false) {
4
+ const databasePath = new URL(uri).pathname.replace(/\/$/, "");
5
+ let previousSalt;
6
+ let namespace;
7
+ const getNamespace = async () => {
8
+ const salt = await getPBKDF2Salt();
9
+ if (!previousSalt || previousSalt.length !== salt.length || previousSalt.some((byte, i) => byte !== salt[i])) {
10
+ previousSalt = salt.slice();
11
+ const epoch = new TextEncoder().encode(`${processEpoch}:`);
12
+ const input = new Uint8Array(epoch.length + previousSalt.length);
13
+ input.set(epoch);
14
+ input.set(previousSalt, epoch.length);
15
+ namespace = crypto.subtle.digest("SHA-256", input).then((digest) => uint8ArrayToHexString(new Uint8Array(digest)));
16
+ }
17
+ return namespace;
18
+ };
19
+ return async (request, options) => {
20
+ const method = (options?.method ?? (typeof request === "string" ? "GET" : request.method)).toUpperCase();
21
+ if (method !== "GET" && method !== "HEAD") {
22
+ return { request, cache: options?.cache };
23
+ }
24
+ if (headersOnly) return { request, cache: "no-store" };
25
+ const url = new URL(typeof request === "string" ? request : request.url);
26
+ const relativePath = url.pathname.startsWith(`${databasePath}/`) ? decodeURIComponent(url.pathname.slice(databasePath.length + 1)) : "";
27
+ if (getPBKDF2Salt && relativePath && !relativePath.startsWith("_local/")) {
28
+ url.searchParams.set("_livesync_epoch", await getNamespace());
29
+ request = typeof request === "string" ? url.href : new Request(url.href, request);
30
+ }
31
+ return { request, cache: "no-cache" };
32
+ };
33
+ }
34
+ export {
35
+ createCouchDBHttpCache
36
+ };
@@ -0,0 +1,4 @@
1
+ /** Host-neutral path identity, namespace, and storage resolution contracts. */
2
+ export type { FilePath, DocumentPath, StoragePath, PathComparisonKey, PathComparisonSettings, DocumentPathParts, DirectoryEntry, ReadDirectory, ResolvedRename, StorageEntry, StoragePathResolver, StoragePathResolverOptions, } from "./platform/pathTypes.js";
3
+ export { parseFilePath, parseDocumentPath, splitDocumentPath, joinDocumentPath, getStoragePathComparisonKey, getDocumentPathComparisonKey, } from "./platform/pathComparison.js";
4
+ export { createStoragePathResolver, AmbiguousNormalisedPathError } from "./platform/storagePathResolution.js";
@@ -0,0 +1,19 @@
1
+ import {
2
+ parseFilePath,
3
+ parseDocumentPath,
4
+ splitDocumentPath,
5
+ joinDocumentPath,
6
+ getStoragePathComparisonKey,
7
+ getDocumentPathComparisonKey
8
+ } from "./platform/pathComparison.js";
9
+ import { createStoragePathResolver, AmbiguousNormalisedPathError } from "./platform/storagePathResolution.js";
10
+ export {
11
+ AmbiguousNormalisedPathError,
12
+ createStoragePathResolver,
13
+ getDocumentPathComparisonKey,
14
+ getStoragePathComparisonKey,
15
+ joinDocumentPath,
16
+ parseDocumentPath,
17
+ parseFilePath,
18
+ splitDocumentPath
19
+ };
@@ -1,3 +1,4 @@
1
+ import { splitPathNamespace } from "../common/pathNamespace.js";
1
2
  import { Minimatch } from "minimatch";
2
3
  import { getWebCrypto } from "../mods.js";
3
4
  import {
@@ -13,7 +14,7 @@ import {
13
14
  import { memorizeFuncWithLRUCache } from "../common/utils.js";
14
15
  import { uint8ArrayToHexString, writeString } from "./convert.js";
15
16
  import { unique } from "octagonal-wheels/collection.js";
16
- import { CHeader, ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
17
+ import { CHeader } from "../common/models/fileaccess.const.js";
17
18
  import { computeKeyedId } from "../common/idDerivation.js";
18
19
  function isValidFilenameInWidows(filename) {
19
20
  const regex = /[\u0000-\u001f]|[\\":?<>|*#]/g;
@@ -45,12 +46,8 @@ function addPrefix(path, prefix) {
45
46
  return `${prefix ?? ""}${path}`;
46
47
  }
47
48
  function expandFilePathPrefix(path) {
48
- for (const prefix of [ICHeader, ICXHeader, PSCHeader]) {
49
- if (path.startsWith(prefix)) {
50
- return [prefix, path.substring(prefix.length)];
51
- }
52
- }
53
- return ["", path];
49
+ const [prefix, body] = splitPathNamespace(path);
50
+ return [prefix, body];
54
51
  }
55
52
  function expandDocumentIDPrefix(id) {
56
53
  for (const prefix of [CHeader, PREFIX_OBFUSCATED]) {