@vrtmrz/livesync-commonlib 0.2.2 → 0.3.0

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 (81) hide show
  1. package/README.md +86 -1
  2. package/dist/API/DirectFileManipulatorV2.d.ts +7 -1
  3. package/dist/API/DirectFileManipulatorV2.js +46 -67
  4. package/dist/API/DirectFileWatchOwner.d.ts +24 -0
  5. package/dist/API/DirectFileWatchOwner.js +256 -0
  6. package/dist/API/processSetting.js +3 -3
  7. package/dist/ContentSplitter/ContentSplitterRabinKarp.js +2 -17
  8. package/dist/ContentSplitter/ContentSplitterV1.js +9 -19
  9. package/dist/ContentSplitter/ContentSplitterV2.js +9 -19
  10. package/dist/common/models/setting.type.d.ts +1 -1
  11. package/dist/common/models/shared.definition.configNames.js +1 -2
  12. package/dist/common/models/tweak.definition.d.ts +1 -1
  13. package/dist/common/pathNormalization.d.ts +2 -0
  14. package/dist/common/pathNormalization.js +7 -2
  15. package/dist/common/utils.d.ts +33 -3
  16. package/dist/common/utils.js +12 -44
  17. package/dist/common/utils.object.js +2 -1
  18. package/dist/concurrency/keyedPendingWork.d.ts +13 -0
  19. package/dist/concurrency/keyedPendingWork.js +27 -0
  20. package/dist/conflicts/conflictCheckQueue.d.ts +45 -0
  21. package/dist/conflicts/conflictCheckQueue.js +108 -0
  22. package/dist/conflicts.d.ts +3 -0
  23. package/dist/conflicts.js +4 -0
  24. package/dist/diagnostics/integrity.js +11 -12
  25. package/dist/interfaces/ServiceModule.d.ts +42 -24
  26. package/dist/interfaces/ServiceModule.js +4 -3
  27. package/dist/managers/ChunkFetcher.d.ts +3 -0
  28. package/dist/managers/ChunkFetcher.js +20 -13
  29. package/dist/managers/LiveSyncManagers.d.ts +2 -0
  30. package/dist/managers/LiveSyncManagers.js +1 -0
  31. package/dist/managers/StorageEventManager.d.ts +3 -3
  32. package/dist/managers/StorageEventManager.js +15 -6
  33. package/dist/managers/chunkFetchSource.d.ts +14 -0
  34. package/dist/managers/chunkFetchSource.js +12 -0
  35. package/dist/pouchdb/LiveSyncDBFunctions.js +5 -0
  36. package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
  37. package/dist/pouchdb/LiveSyncLocalDB.js +14 -10
  38. package/dist/pouchdb/chunks.d.ts +16 -0
  39. package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
  40. package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
  41. package/dist/pouchdb/pouchdb-browser.js +2 -84
  42. package/dist/pouchdb/pouchdb-http.js +2 -84
  43. package/dist/pouchdb/purgeMulti.d.ts +4 -0
  44. package/dist/pouchdb/purgeMulti.js +89 -0
  45. package/dist/replication/RemoteResource.d.ts +3 -0
  46. package/dist/replication/index.d.ts +2 -0
  47. package/dist/replication/index.js +2 -0
  48. package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
  49. package/dist/replication/journal/JournalSyncCore.js +41 -20
  50. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
  51. package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
  52. package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
  53. package/dist/replication/trystero/TrysteroReplicator.d.ts +7 -5
  54. package/dist/replication/trystero/TrysteroReplicator.js +101 -74
  55. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
  56. package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
  57. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +3 -2
  58. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
  59. package/dist/replication/trystero/peerPatterns.d.ts +1 -0
  60. package/dist/replication/trystero/peerPatterns.js +6 -0
  61. package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
  62. package/dist/rpc/RpcRoom.d.ts +5 -1
  63. package/dist/rpc/RpcRoom.js +80 -105
  64. package/dist/serviceFeatures/offlineScanner.d.ts +4 -4
  65. package/dist/serviceFeatures/offlineScanner.js +14 -15
  66. package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +2 -2
  67. package/dist/serviceFeatures/remoteConfig.d.ts +10 -5
  68. package/dist/serviceModules/Rebuilder.d.ts +1 -0
  69. package/dist/serviceModules/Rebuilder.js +33 -5
  70. package/dist/serviceModules/ServiceFileHandlerBase.js +2 -7
  71. package/dist/services/implements/injectable/InjectableConflictService.d.ts +5 -5
  72. package/dist/services/lib/HandlerUtils.d.ts +8 -6
  73. package/dist/services/lib/HandlerUtils.js +14 -0
  74. package/dist/worker/bgWorker.js +1 -1
  75. package/docs/conflict-resolution.md +30 -0
  76. package/docs/development.md +50 -0
  77. package/docs/proven-in-use.md +80 -0
  78. package/docs/releasing.md +4 -0
  79. package/docs/service-feature-composition.md +63 -10
  80. package/docs/unicode-path-identity.md +64 -3
  81. package/package.json +11 -3
@@ -1,3 +1,4 @@
1
+ import type { PathIdentityInspection } from "../pouchdb/pathIdentityCompatibility.js";
1
2
  import type { RemoteDBSettings, RemotePreferredTweakResult } from "../common/types.js";
2
3
  import type { JoinedDevicesSnapshot, ForgetJoinedDeviceResult } from "./joinedDevices.js";
3
4
  import type { RemoteDBStatus } from "./LiveSyncAbstractReplicator.js";
@@ -36,6 +37,8 @@ export type RemoteConnectionProbeResult = {
36
37
  export interface RemoteConnectionProbe {
37
38
  check(options?: RemoteConnectionProbeOptions): Promise<RemoteConnectionProbeResult>;
38
39
  getStatus(): Promise<false | RemoteDBStatus>;
40
+ /** Inspect a central remote before choosing initial host settings. */
41
+ inspectPathIdentity?(): Promise<PathIdentityInspection>;
39
42
  dispose(): Promise<void>;
40
43
  }
41
44
  /** A finite preferred-tweak reader bound to one trial configuration. */
@@ -13,3 +13,5 @@ export { assessRemoteFeatureDocument, describeRemoteFeatureRejection, ENCRYPTED_
13
13
  export type { RemoteFeatureAssessment } from "../pouchdb/remoteFeatureCompatibility.js";
14
14
  export type { JoinedDevice, JoinedDevicesSnapshot, ForgetJoinedDeviceResult } from "./joinedDevices.js";
15
15
  export type { ReplicationStatics, CouchDBReplicationPhase, CouchDBReplicationProgress, ReplicationCheckpointProgress, } from "../common/models/shared.definition.js";
16
+ export { inspectDatabasePathIdentity } from "../pouchdb/pathIdentityCompatibility.js";
17
+ export type { PathIdentityInspection } from "../pouchdb/pathIdentityCompatibility.js";
@@ -43,6 +43,7 @@ import {
43
43
  REMOTE_FEATURE_GENERATION,
44
44
  usesEncryptedInternalMetadata
45
45
  } from "../pouchdb/remoteFeatureCompatibility.js";
46
+ import { inspectDatabasePathIdentity } from "../pouchdb/pathIdentityCompatibility.js";
46
47
  export {
47
48
  CAPABILITY_NOT_APPLICABLE,
48
49
  CAPABILITY_NOT_IMPLEMENTED,
@@ -72,6 +73,7 @@ export {
72
73
  centralRemoteAdministrationVerified,
73
74
  defineReplicatorProviderDefinitions,
74
75
  describeRemoteFeatureRejection,
76
+ inspectDatabasePathIdentity,
75
77
  isCentralRemoteAdministrationVerified,
76
78
  isReplicationCompleted,
77
79
  milestoneSatisfiesCentralRemoteAdministration,
@@ -62,6 +62,7 @@ export declare class JournalSyncCore {
62
62
  private _createSendCompressTransformStream;
63
63
  private _createSendUploadWritableStream;
64
64
  sendLocalJournal(showMessage?: boolean): Promise<boolean>;
65
+ hasRemoteJournals(): Promise<boolean>;
65
66
  _getRemoteJournals(): Promise<string[]>;
66
67
  processDocuments(allDocs: ProcessingEntry[]): Promise<boolean>;
67
68
  private _createReceiveReadableStream;
@@ -33,6 +33,7 @@ import {
33
33
  encryptBinary as encryptBinaryHKDF,
34
34
  decryptBinary as decryptBinaryHKDF
35
35
  } from "octagonal-wheels/encryption/hkdf";
36
+ import { assertDatabasePathIdentity } from "../../pouchdb/pathIdentityCompatibility.js";
36
37
  const RECORD_SPLIT = `
37
38
  `;
38
39
  const UNIT_SPLIT = ``;
@@ -45,6 +46,9 @@ function serializeDoc(doc) {
45
46
  }
46
47
  return te.encode(JSON.stringify(doc) + RECORD_SPLIT);
47
48
  }
49
+ function isJournalKey(key) {
50
+ return !key.startsWith("_");
51
+ }
48
52
  class JournalSyncCore {
49
53
  _settings;
50
54
  storage;
@@ -217,7 +221,13 @@ class JournalSyncCore {
217
221
  this.syncParamsHandler = void 0;
218
222
  }
219
223
  async resetCheckpointInfo() {
220
- await this.updateCheckPointInfo((info) => ({ ...CheckPointInfoDefault }));
224
+ await this.updateCheckPointInfo(() => ({
225
+ ...CheckPointInfoDefault,
226
+ knownIDs: /* @__PURE__ */ new Set(),
227
+ sentIDs: /* @__PURE__ */ new Set(),
228
+ receivedFiles: /* @__PURE__ */ new Set(),
229
+ sentFiles: /* @__PURE__ */ new Set()
230
+ }));
221
231
  }
222
232
  getJournalEpochFromSyncParams(params) {
223
233
  return `${params.protocolVersion}:${params.pbkdf2salt}`;
@@ -262,32 +272,33 @@ class JournalSyncCore {
262
272
  }
263
273
  async resetBucket() {
264
274
  this.resetAllCaches();
265
- let files = [];
266
275
  try {
276
+ const skippedFiles = [];
277
+ let from = "";
278
+ let files;
267
279
  do {
268
- files = await this.storage.listFiles("", 100);
269
- if (files.length == 0) {
270
- break;
271
- }
272
- if (!await this.storage.deleteFiles(files)) {
273
- return false;
280
+ files = await this.storage.listFiles(from, 100);
281
+ if (files.length > 0) {
282
+ const nextFrom = files[files.length - 1];
283
+ if (nextFrom <= from) {
284
+ throw new Error("Journal reset listing did not advance. Control documents have been retained.");
285
+ }
286
+ skippedFiles.push(...files.filter((key) => !isJournalKey(key)));
287
+ const journals = files.filter(isJournalKey);
288
+ if (journals.length > 0 && !await this.storage.deleteFiles(journals)) return false;
289
+ from = nextFrom;
274
290
  }
275
- } while (files.length != 0);
291
+ } while (files.length > 0);
292
+ while (skippedFiles.length > 0) {
293
+ if (!await this.storage.deleteFiles(skippedFiles.splice(0, 100))) return false;
294
+ }
276
295
  } catch (ex) {
277
296
  Logger(`WARNING! Could not delete files.`, LOG_LEVEL_NOTICE, "reset-bucket");
278
297
  Logger(ex, LOG_LEVEL_VERBOSE);
279
298
  return false;
280
299
  }
281
- const journals = await this._getRemoteJournals();
282
- if (journals.length == 0) {
283
- Logger("Nothing to delete!", LOG_LEVEL_NOTICE);
284
- return true;
285
- }
286
- if (!await this.storage.deleteFiles(journals)) {
287
- return false;
288
- }
289
- Logger(`${journals.length} items has been deleted!`, LOG_LEVEL_NOTICE);
290
300
  await this.resetCheckpointInfo();
301
+ Logger("Remote bucket cleared", LOG_LEVEL_NOTICE);
291
302
  return true;
292
303
  }
293
304
  getRemoteKey() {
@@ -502,6 +513,7 @@ class JournalSyncCore {
502
513
  });
503
514
  }
504
515
  async sendLocalJournal(showMessage = false) {
516
+ await assertDatabasePathIdentity(this.db, this.currentSettings);
505
517
  this.updateInfo({ syncStatus: "JOURNAL_SEND" });
506
518
  return await shareRunningResult("send_journal_stream", async () => {
507
519
  this.requestedStop = false;
@@ -544,12 +556,15 @@ class JournalSyncCore {
544
556
  }
545
557
  });
546
558
  }
559
+ async hasRemoteJournals() {
560
+ return (await this.storage.listFiles("", 1)).some(isJournalKey);
561
+ }
547
562
  async _getRemoteJournals() {
548
563
  const checkPointInfo = await this.getCheckpointInfo();
549
564
  const StartAfter = [...checkPointInfo.receivedFiles.keys()].sort(
550
565
  (a, b) => b.localeCompare(a, void 0, { numeric: true })
551
566
  )[0];
552
- const files = (await this.storage.listFiles(StartAfter)).filter((e) => !e.startsWith("_"));
567
+ const files = (await this.storage.listFiles(StartAfter)).filter(isJournalKey);
553
568
  if (!files) return [];
554
569
  return files.sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
555
570
  }
@@ -677,7 +692,12 @@ class JournalSyncCore {
677
692
  const piece = decompressed.slice(idxFrom, idxTo);
678
693
  const strPiece = d.decode(piece);
679
694
  if (strPiece.startsWith("~")) {
680
- const [idPart, dataPart] = strPiece.substring(1).split(UNIT_SPLIT);
695
+ const separatorIndex = strPiece.indexOf(UNIT_SPLIT);
696
+ if (separatorIndex === -1) {
697
+ throw new Error("Invalid journal chunk record: missing separator");
698
+ }
699
+ const idPart = strPiece.substring(1, separatorIndex);
700
+ const dataPart = strPiece.substring(separatorIndex + 1);
681
701
  result.push({
682
702
  _id: idPart,
683
703
  data: unescapeNewLineFromString(dataPart),
@@ -718,6 +738,7 @@ class JournalSyncCore {
718
738
  });
719
739
  }
720
740
  async receiveRemoteJournal(showMessage = false) {
741
+ await assertDatabasePathIdentity(this.db, this.currentSettings);
721
742
  this.updateInfo({ syncStatus: "JOURNAL_RECEIVE" });
722
743
  return await shareRunningResult("receive_journal_stream", async () => {
723
744
  this.requestedStop = false;
@@ -1,3 +1,4 @@
1
+ import type { PathIdentityInspection } from "../../pouchdb/pathIdentityCompatibility.js";
1
2
  import { type RemoteDBSettings, type ChunkVersionRange, type NodeData, type RemotePreferredTweakResult, type TweakAssessment } from "../../common/types.js";
2
3
  import { JournalSyncCore } from "./JournalSyncCore.js";
3
4
  import { LiveSyncAbstractReplicator, type RemoteDBStatus } from "../LiveSyncAbstractReplicator.js";
@@ -23,7 +24,16 @@ export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicato
23
24
  private journalTransferStopGeneration;
24
25
  getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise<Uint8Array<ArrayBuffer>>;
25
26
  setupJournalSyncClient(settings?: RemoteDBSettings): JournalSyncCore;
26
- ensureBucketIsCompatible(deviceNodeID: string, currentVersionRange: ChunkVersionRange, setting?: RemoteDBSettings, client?: Pick<JournalSyncCore, "downloadJsonWithResult" | "getCheckpointInfo" | "uploadJson">, recordTweakAssessment?: (assessment: TweakAssessment) => void): Promise<ENSURE_DB_RESULT>;
27
+ private isBucketPathIdentityCompatible;
28
+ private isBucketIdDerivationCompatible;
29
+ private assertBucketIdentity;
30
+ /** Inspect identity without publishing this node, preparing a seed, or changing checkpoints. */
31
+ assertRemotePathIdentity(setting: RemoteDBSettings): Promise<void>;
32
+ /** Read path identity for initial setup without publishing a node or preparing a seed. */
33
+ inspectRemotePathIdentity(setting: RemoteDBSettings): Promise<PathIdentityInspection>;
34
+ /** Lock existing journals for replacement without declaring the replacement's identity. */
35
+ markRemoteLockedForRebuild(setting: RemoteDBSettings): Promise<void>;
36
+ ensureBucketIsCompatible(deviceNodeID: string, currentVersionRange: ChunkVersionRange, setting?: RemoteDBSettings, client?: Pick<JournalSyncCore, "downloadJsonWithResult" | "getCheckpointInfo" | "uploadJson" | "hasRemoteJournals">, recordTweakAssessment?: (assessment: TweakAssessment) => void): Promise<ENSURE_DB_RESULT>;
27
37
  constructor(env: LiveSyncJournalReplicatorEnv);
28
38
  migrate(from: number, to: number): Promise<boolean>;
29
39
  /**
@@ -62,8 +72,6 @@ export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicato
62
72
  setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise<void>;
63
73
  getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise<RemotePreferredTweakResult>;
64
74
  getRemoteStatus(setting: RemoteDBSettings): Promise<false | RemoteDBStatus>;
65
- private supportsLegacyPathIdentity;
66
- private assertLegacyPathIdentity;
67
75
  private getOwnJoinedDeviceId;
68
76
  getJoinedDevices(setting: RemoteDBSettings): Promise<JoinedDevicesSnapshot>;
69
77
  forgetJoinedDevice(setting: RemoteDBSettings, deviceId: string): Promise<ForgetJoinedDeviceResult>;
@@ -12,7 +12,7 @@ import { MinioStorageAdapter } from "./objectstore/MinioStorageAdapter.js";
12
12
  import { LiveSyncAbstractReplicator } from "../LiveSyncAbstractReplicator.js";
13
13
  import { ensureRemoteIsCompatible } from "../../pouchdb/LiveSyncDBFunctions.js";
14
14
  import { getEffectiveTweakValues } from "../../pouchdb/LiveSyncDBFunctions.js";
15
- import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
15
+ import { assertSupportedPathNormalizationVersion, getPathNormalizationVersion } from "../../common/pathNormalization.js";
16
16
  import { JournalStorageReadStatuses } from "./objectstore/JournalStorageAdapter.js";
17
17
  import { computeKeyedId, configuredIdKey } from "../../common/idDerivation.js";
18
18
  import { decryptString, encryptString } from "../../encryption/stringEncryption.js";
@@ -44,8 +44,17 @@ const currentVersionRange = {
44
44
  current: 2
45
45
  };
46
46
  function journalVersionRange(setting) {
47
+ if (getPathNormalizationVersion(setting) === 1) return { min: 4, max: 4, current: 4 };
47
48
  return configuredIdKey(setting) && setting.encrypt && setting.usePathObfuscation ? { min: 3, max: 3, current: 3 } : currentVersionRange;
48
49
  }
50
+ function pathIdentityFromJournalRange(range) {
51
+ if (range?.min === 4 && range?.max === 4) return 1;
52
+ if (!range) return 0;
53
+ if (Number.isSafeInteger(range.min) && Number.isSafeInteger(range.max) && range.min >= 0 && range.max <= 3 && range.min <= range.max) {
54
+ return 0;
55
+ }
56
+ throw new Error("The Journal declares an unsupported path identity range.");
57
+ }
49
58
  class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
50
59
  get client() {
51
60
  return this.setupJournalSyncClient();
@@ -66,7 +75,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
66
75
  return await this.setupJournalSyncClient(setting).getReplicationPBKDF2Salt(refresh);
67
76
  }
68
77
  setupJournalSyncClient(settings = this.currentSettings) {
69
- this.assertLegacyPathIdentity(settings);
78
+ assertSupportedPathNormalizationVersion(settings.pathNormalizationVersion);
70
79
  if (this._client) {
71
80
  this._client.applyNewConfig(settings, this.simpleStore, this.env);
72
81
  } else {
@@ -79,33 +88,105 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
79
88
  }
80
89
  return this._client;
81
90
  }
82
- async ensureBucketIsCompatible(deviceNodeID, currentVersionRange2, setting = this.currentSettings, client = this.setupJournalSyncClient(setting), recordTweakAssessment) {
83
- const milestoneResult = await client.downloadJsonWithResult(MILSTONE_DOCID);
84
- if (milestoneResult.status === JournalStorageReadStatuses.UNAVAILABLE) {
85
- throw milestoneResult.error;
91
+ async isBucketPathIdentityCompatible(setting, milestone, client) {
92
+ const pathVersion = getPathNormalizationVersion(setting);
93
+ const ranges = milestone?.node_chunk_info ?? {};
94
+ const nodes = /* @__PURE__ */ new Set([...milestone?.accepted_nodes ?? [], ...Object.keys(ranges)]);
95
+ for (const node of nodes) {
96
+ const range = ranges[node];
97
+ const usesNFC = range?.min === 4 && range?.max === 4;
98
+ if (usesNFC !== (pathVersion === 1)) return false;
99
+ }
100
+ const preferred = milestone?.tweak_values?.[DEVICE_ID_PREFERRED];
101
+ if (preferred && getPathNormalizationVersion(preferred) !== pathVersion) return false;
102
+ if (nodes.size === 0 && await client.hasRemoteJournals()) {
103
+ throw new Error(
104
+ "The Journal compatibility milestone is missing. Rebuild from the authoritative Vault before synchronising."
105
+ );
86
106
  }
87
- const downloadedMilestone = milestoneResult.status === JournalStorageReadStatuses.AVAILABLE ? milestoneResult.value : false;
107
+ return true;
108
+ }
109
+ async isBucketIdDerivationCompatible(setting, milestone) {
88
110
  const idKey = configuredIdKey(setting);
89
- let encryptedProof;
90
- const preferredTweak = downloadedMilestone && downloadedMilestone.tweak_values?.[DEVICE_ID_PREFERRED];
91
- if (preferredTweak?.idDerivationVersion === 1 && preferredTweak.usePathObfuscation === true && (!idKey || !setting.encrypt || !setting.usePathObfuscation)) {
92
- return "ID_KEY_MISMATCH";
111
+ const usesKeyedDocumentIds = !!idKey && setting.encrypt && setting.usePathObfuscation;
112
+ const preferred = milestone?.tweak_values?.[DEVICE_ID_PREFERRED];
113
+ const stored = milestone?.encrypted_id_derivation_proof;
114
+ if (!usesKeyedDocumentIds) {
115
+ return !stored && !(preferred?.idDerivationVersion === 1 && preferred.usePathObfuscation === true);
93
116
  }
94
- if (idKey && setting.encrypt && setting.usePathObfuscation) {
95
- const expected = await computeKeyedId(idKey, "remote-agreement", "journal-milestone-v1");
96
- const stored = downloadedMilestone && downloadedMilestone.encrypted_id_derivation_proof;
97
- if (stored) {
98
- try {
99
- if (await decryptString(stored, setting.passphrase) !== expected) return "ID_KEY_MISMATCH";
100
- } catch {
101
- return "ID_KEY_MISMATCH";
102
- }
103
- } else if (downloadedMilestone) {
104
- return "ID_KEY_MISMATCH";
105
- } else {
106
- encryptedProof = await encryptString(expected, setting.passphrase);
107
- }
117
+ if (!milestone) return true;
118
+ if (!stored) return false;
119
+ const expected = await computeKeyedId(idKey, "remote-agreement", "journal-milestone-v1");
120
+ try {
121
+ return await decryptString(stored, setting.passphrase) === expected;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+ async assertBucketIdentity(setting, milestone, client) {
127
+ if (!await this.isBucketPathIdentityCompatible(setting, milestone, client)) {
128
+ throw new Error(
129
+ "The Journal and settings use different path identity versions. Rebuild from the authoritative Vault or Fetch with matching settings before synchronising."
130
+ );
108
131
  }
132
+ if (!await this.isBucketIdDerivationCompatible(setting, milestone)) {
133
+ throw new Error(
134
+ "The Journal and settings use different document ID derivation modes or keys. Rebuild from the authoritative Vault or Fetch with matching settings before synchronising."
135
+ );
136
+ }
137
+ }
138
+ /** Inspect identity without publishing this node, preparing a seed, or changing checkpoints. */
139
+ async assertRemotePathIdentity(setting) {
140
+ const client = this.setupJournalSyncClient(setting);
141
+ await this.assertBucketIdentity(setting, await readRemoteMilestone(client), client);
142
+ }
143
+ /** Read path identity for initial setup without publishing a node or preparing a seed. */
144
+ async inspectRemotePathIdentity(setting) {
145
+ const client = this.setupJournalSyncClient(setting);
146
+ const milestone = await readRemoteMilestone(client);
147
+ const versions = /* @__PURE__ */ new Set();
148
+ const ranges = milestone?.node_chunk_info ?? {};
149
+ const nodes = /* @__PURE__ */ new Set([...milestone?.accepted_nodes ?? [], ...Object.keys(ranges)]);
150
+ for (const node of nodes) {
151
+ versions.add(pathIdentityFromJournalRange(ranges[node]));
152
+ }
153
+ const preferred = milestone?.tweak_values?.[DEVICE_ID_PREFERRED];
154
+ if (preferred) versions.add(getPathNormalizationVersion(preferred));
155
+ if (versions.size > 1) throw new Error("The Journal has conflicting path identity declarations.");
156
+ if (nodes.size === 0 && await client.hasRemoteJournals()) {
157
+ throw new Error(
158
+ "The Journal path identity declaration is missing. Use matching settings or Rebuild from the authoritative Vault."
159
+ );
160
+ }
161
+ const version = [...versions][0];
162
+ return version === void 0 ? { status: "empty" } : { status: "configured", pathNormalizationVersion: version };
163
+ }
164
+ /** Lock existing journals for replacement without declaring the replacement's identity. */
165
+ async markRemoteLockedForRebuild(setting) {
166
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
167
+ const client = this.setupJournalSyncClient(setting);
168
+ const milestone = await readRemoteMilestone(client);
169
+ if (!milestone) return;
170
+ const previousRange = Object.values(milestone.node_chunk_info ?? {}).sort((a, b) => b.min - a.min)[0];
171
+ const lockedMilestone = {
172
+ ...milestone,
173
+ locked: true,
174
+ accepted_nodes: previousRange ? [this.nodeid] : [],
175
+ node_chunk_info: previousRange ? { ...milestone.node_chunk_info, [this.nodeid]: { ...previousRange } } : milestone.node_chunk_info
176
+ };
177
+ if (!await client.uploadJson(MILSTONE_DOCID, lockedMilestone)) {
178
+ throw new Error("Could not upload remote milestone");
179
+ }
180
+ }
181
+ async ensureBucketIsCompatible(deviceNodeID, currentVersionRange2, setting = this.currentSettings, client = this.setupJournalSyncClient(setting), recordTweakAssessment) {
182
+ const downloadedMilestone = await readRemoteMilestone(client) ?? false;
183
+ if (!await this.isBucketPathIdentityCompatible(setting, downloadedMilestone || void 0, client)) {
184
+ return "INCOMPATIBLE";
185
+ }
186
+ if (!await this.isBucketIdDerivationCompatible(setting, downloadedMilestone || void 0)) {
187
+ return "ID_KEY_MISMATCH";
188
+ }
189
+ const encryptedProof = downloadedMilestone ? void 0 : await this.createInitialIdDerivationProof(setting);
109
190
  const cPointInfo = await client.getCheckpointInfo();
110
191
  const progress = [...cPointInfo?.receivedFiles || []].sort().pop() || "";
111
192
  return await ensureRemoteIsCompatible(
@@ -291,7 +372,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
291
372
  }
292
373
  async checkReplicationConnectivity(skipCheck, ignoreCleanLock = false, showMessage = false, setting = this.currentSettings, client, recordCompatibilityDecision) {
293
374
  recordCompatibilityDecision?.(CENTRAL_COMPATIBILITY_NOT_ASSESSED);
294
- if (!this.supportsLegacyPathIdentity(setting)) return false;
375
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
295
376
  const activeClient = client ?? this.setupJournalSyncClient(setting);
296
377
  if (!await activeClient.isAvailable()) {
297
378
  return false;
@@ -388,7 +469,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
388
469
  this.updateInfo();
389
470
  }
390
471
  async tryResetRemoteDatabase(setting) {
391
- this.assertLegacyPathIdentity(setting);
472
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
392
473
  this.closeReplication();
393
474
  try {
394
475
  if (!await this.client.resetBucket()) {
@@ -403,7 +484,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
403
484
  }
404
485
  }
405
486
  async tryCreateRemoteDatabase(setting) {
406
- this.assertLegacyPathIdentity(setting);
487
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
407
488
  this.closeReplication();
408
489
  Logger("Remote Database Created or Connected", LOG_LEVEL_NOTICE);
409
490
  if (!await this.ensurePBKDF2Salt(setting, true, false)) {
@@ -418,7 +499,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
418
499
  return await encryptString(proof, setting.passphrase);
419
500
  }
420
501
  async markRemoteLocked(setting, locked, lockByClean) {
421
- this.assertLegacyPathIdentity(setting);
502
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
422
503
  const defInitPoint = {
423
504
  _id: MILSTONE_DOCID,
424
505
  type: "milestoneinfo",
@@ -432,6 +513,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
432
513
  };
433
514
  const client = this.setupJournalSyncClient(setting);
434
515
  const existingMilestone = await readRemoteMilestone(client);
516
+ await this.assertBucketIdentity(setting, existingMilestone, client);
435
517
  const initialProof = existingMilestone ? void 0 : await this.createInitialIdDerivationProof(setting);
436
518
  const remoteMilestone = {
437
519
  ...defInitPoint,
@@ -452,7 +534,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
452
534
  }
453
535
  }
454
536
  async markRemoteResolved(setting) {
455
- this.assertLegacyPathIdentity(setting);
537
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
456
538
  const defInitPoint = {
457
539
  _id: MILSTONE_DOCID,
458
540
  type: "milestoneinfo",
@@ -465,6 +547,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
465
547
  };
466
548
  const client = this.setupJournalSyncClient(setting);
467
549
  const existingMilestone = await readRemoteMilestone(client);
550
+ await this.assertBucketIdentity(setting, existingMilestone, client);
468
551
  const initialProof = existingMilestone ? void 0 : await this.createInitialIdDerivationProof(setting);
469
552
  const remoteMilestone = {
470
553
  ...defInitPoint,
@@ -482,7 +565,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
482
565
  const endpoint = setting.endpoint;
483
566
  let testClient;
484
567
  try {
485
- this.assertLegacyPathIdentity(setting);
568
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
486
569
  testClient = new MinioStorageAdapter(setting, this.env);
487
570
  await testClient.listFiles("", 1);
488
571
  Logger(`Connected to ${endpoint} successfully!`, LOG_LEVEL_NOTICE);
@@ -497,12 +580,13 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
497
580
  }
498
581
  }
499
582
  async resetRemoteTweakSettings(setting) {
500
- this.assertLegacyPathIdentity(setting);
583
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
501
584
  try {
502
585
  const remoteMilestone = await this.client.downloadJson(MILSTONE_DOCID);
503
586
  if (!remoteMilestone) {
504
587
  throw new Error("Missing remote milestone");
505
588
  }
589
+ await this.assertBucketIdentity(setting, remoteMilestone, this.client);
506
590
  remoteMilestone.tweak_values = {};
507
591
  Logger(`tweak values on the remote database have been cleared`, LOG_LEVEL_VERBOSE);
508
592
  await this.client.uploadJson(MILSTONE_DOCID, remoteMilestone);
@@ -512,7 +596,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
512
596
  }
513
597
  }
514
598
  async setPreferredRemoteTweakSettings(setting) {
515
- this.assertLegacyPathIdentity(setting);
599
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
516
600
  const trialClient = new JournalSyncCore(
517
601
  setting,
518
602
  this.simpleStore,
@@ -524,6 +608,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
524
608
  if (!remoteMilestone) {
525
609
  throw new Error("Missing remote milestone");
526
610
  }
611
+ await this.assertBucketIdentity(setting, remoteMilestone, trialClient);
527
612
  remoteMilestone.tweak_values[DEVICE_ID_PREFERRED] = getEffectiveTweakValues(setting);
528
613
  Logger(`Preferred tweak values have been registered`, LOG_LEVEL_VERBOSE);
529
614
  if (!await trialClient.uploadJson(MILSTONE_DOCID, remoteMilestone)) {
@@ -537,7 +622,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
537
622
  }
538
623
  }
539
624
  async getRemotePreferredTweakValues(setting) {
540
- this.assertLegacyPathIdentity(setting);
625
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
541
626
  const trialClient = new JournalSyncCore(
542
627
  setting,
543
628
  this.simpleStore,
@@ -574,7 +659,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
574
659
  }
575
660
  }
576
661
  async getRemoteStatus(setting) {
577
- this.assertLegacyPathIdentity(setting);
662
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
578
663
  const testClient = new MinioStorageAdapter(setting, this.env);
579
664
  try {
580
665
  return await testClient.getUsage();
@@ -582,14 +667,6 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
582
667
  testClient.dispose();
583
668
  }
584
669
  }
585
- supportsLegacyPathIdentity(setting) {
586
- return getPathNormalizationVersion(setting) === 0;
587
- }
588
- assertLegacyPathIdentity(setting) {
589
- if (!this.supportsLegacyPathIdentity(setting)) {
590
- throw new Error("Object Storage does not support NFC path identity.");
591
- }
592
- }
593
670
  async getOwnJoinedDeviceId() {
594
671
  const nodeInfo = await this.rawDatabase.get(NODEINFO_DOCID);
595
672
  const nodeId = nodeInfo?.nodeid;
@@ -599,7 +676,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
599
676
  return nodeId;
600
677
  }
601
678
  async getJoinedDevices(setting) {
602
- this.assertLegacyPathIdentity(setting);
679
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
603
680
  const trialClient = new JournalSyncCore(
604
681
  setting,
605
682
  this.simpleStore,
@@ -616,7 +693,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
616
693
  }
617
694
  }
618
695
  async forgetJoinedDevice(setting, deviceId) {
619
- this.assertLegacyPathIdentity(setting);
696
+ assertSupportedPathNormalizationVersion(setting.pathNormalizationVersion);
620
697
  const ownDeviceId = await this.getOwnJoinedDeviceId();
621
698
  const trialClient = new JournalSyncCore(
622
699
  setting,
@@ -630,6 +707,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
630
707
  const result = await trialClient.downloadJsonWithResult(MILSTONE_DOCID);
631
708
  if (result.status === JournalStorageReadStatuses.NOT_FOUND) return void 0;
632
709
  if (result.status === JournalStorageReadStatuses.UNAVAILABLE) throw result.error;
710
+ await this.assertBucketIdentity(setting, result.value, trialClient);
633
711
  return { milestone: result.value, revision: result.revision };
634
712
  },
635
713
  write: async (milestone, revision) => await trialClient.uploadJsonIfUnchanged(MILSTONE_DOCID, milestone, revision)
@@ -12,7 +12,7 @@ import { P2PRoomSession } from "./P2PRoomSession.js";
12
12
  import { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
13
13
  import { PEER_REPLICATION_READINESS } from "../ReplicatorProvider.js";
14
14
  import { getP2PReplicatorConfigurationIdentity } from "./p2pReplicatorConfigurationIdentity.js";
15
- import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
15
+ import { assertSupportedPathNormalizationVersion, getPathNormalizationVersion } from "../../common/pathNormalization.js";
16
16
  import {
17
17
  ACTIVE_P2P_RELAY_BINDING_CONFLICT,
18
18
  activeP2PRelayBindingCovers
@@ -58,8 +58,8 @@ class P2PRoomSessionOwner {
58
58
  /** Reconcile one long-lived reason for keeping the room available. */
59
59
  async setPersistentDemand(demand, active) {
60
60
  const settings = this.env.services.setting.currentSettings();
61
- const supportsPathIdentity = getPathNormalizationVersion(settings) === 0;
62
- const enabled = settings.P2P_Enabled && supportsPathIdentity;
61
+ assertSupportedPathNormalizationVersion(settings.pathNormalizationVersion);
62
+ const enabled = settings.P2P_Enabled;
63
63
  if (active && enabled) {
64
64
  this.persistentDemands.add(demand);
65
65
  } else {
@@ -67,8 +67,6 @@ class P2PRoomSessionOwner {
67
67
  }
68
68
  if (active && !settings.P2P_Enabled) {
69
69
  Logger(this.env.services.context.translate("P2P.NotEnabled"), LOG_LEVEL_NOTICE);
70
- } else if (active && !supportsPathIdentity) {
71
- Logger("P2P does not support NFC path identity.", LOG_LEVEL_NOTICE);
72
70
  }
73
71
  await this.fencePreparationForCurrentSettings(enabled);
74
72
  await this.reconcileTransport();
@@ -81,12 +79,10 @@ class P2PRoomSessionOwner {
81
79
  */
82
80
  async runWithFiniteDemand(task) {
83
81
  const settings = this.env.services.setting.currentSettings();
82
+ assertSupportedPathNormalizationVersion(settings.pathNormalizationVersion);
84
83
  if (!settings.P2P_Enabled) {
85
84
  throw new Error("P2P is not enabled.");
86
85
  }
87
- if (getPathNormalizationVersion(settings) !== 0) {
88
- throw new Error("P2P does not support NFC path identity.");
89
- }
90
86
  const demand = /* @__PURE__ */ Symbol("p2p-finite-room-demand");
91
87
  this.finiteDemands.add(demand);
92
88
  try {
@@ -108,8 +104,10 @@ class P2PRoomSessionOwner {
108
104
  }
109
105
  /** Arbitrate a complete signalling probe on the room lifecycle queue. */
110
106
  runConnectionProbe(trialSettings, runOwnedTrial) {
111
- if (getPathNormalizationVersion(this.env.services.setting.currentSettings()) !== 0) {
112
- return Promise.reject(new Error("P2P does not support NFC path identity."));
107
+ try {
108
+ assertSupportedPathNormalizationVersion(this.env.services.setting.currentSettings().pathNormalizationVersion);
109
+ } catch (error) {
110
+ return Promise.reject(error);
113
111
  }
114
112
  return this.enqueueLifecycleOperation(async () => {
115
113
  const current = this.current;
@@ -135,7 +133,8 @@ class P2PRoomSessionOwner {
135
133
  async reconcileTransport() {
136
134
  await this.enqueueLifecycleOperation(async () => {
137
135
  const settings = this.env.services.setting.currentSettings();
138
- if (!settings.P2P_Enabled || getPathNormalizationVersion(settings) !== 0) {
136
+ assertSupportedPathNormalizationVersion(settings.pathNormalizationVersion);
137
+ if (!settings.P2P_Enabled) {
139
138
  this.persistentDemands.clear();
140
139
  this.finiteDemands.clear();
141
140
  }
@@ -390,17 +389,19 @@ class P2PRoomSessionOwner {
390
389
  ...this.env.services.setting.currentSettings()
391
390
  });
392
391
  const database = this.env.services.database.localDatabase.localDatabase;
392
+ const pathNormalizationVersion = getPathNormalizationVersion(settings);
393
393
  this.automationCoordinator.reconcileIdentity(
394
394
  JSON.stringify([
395
395
  settings.P2P_AppID || "self-hosted-livesync",
396
396
  settings.P2P_roomID,
397
- settings.P2P_passphrase
397
+ settings.P2P_passphrase,
398
+ pathNormalizationVersion
398
399
  ]),
399
400
  database
400
401
  );
401
402
  return {
402
403
  database,
403
- enabled: settings.P2P_Enabled && getPathNormalizationVersion(settings) === 0,
404
+ enabled: settings.P2P_Enabled,
404
405
  settings,
405
406
  deviceName,
406
407
  signature: JSON.stringify([getP2PReplicatorConfigurationIdentity(settings), deviceName])