@vrtmrz/livesync-commonlib 0.1.31 → 0.1.33

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 (57) hide show
  1. package/README.md +12 -0
  2. package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
  3. package/dist/API/DirectFileManipulatorV2.js +17 -5
  4. package/dist/API/processSetting.d.ts +19 -0
  5. package/dist/API/processSetting.js +91 -6
  6. package/dist/common/idDerivation.d.ts +15 -0
  7. package/dist/common/idDerivation.js +102 -0
  8. package/dist/common/models/db.definition.d.ts +1 -0
  9. package/dist/common/models/setting.const.defaults.js +3 -0
  10. package/dist/common/models/setting.const.qr.js +4 -1
  11. package/dist/common/models/setting.type.d.ts +6 -0
  12. package/dist/common/models/tweak.compatibility.js +6 -2
  13. package/dist/common/models/tweak.definition.d.ts +8 -0
  14. package/dist/common/models/tweak.definition.js +4 -3
  15. package/dist/common/settingConstants.d.ts +3 -0
  16. package/dist/common/utils.js +3 -1
  17. package/dist/events/coreEvents.d.ts +3 -0
  18. package/dist/events/coreEvents.js +2 -0
  19. package/dist/hashing.d.ts +3 -0
  20. package/dist/hashing.js +4 -0
  21. package/dist/managers/EntryManager/EntryManagerImpls.js +10 -1
  22. package/dist/managers/HashManager/HashManager.d.ts +4 -0
  23. package/dist/managers/HashManager/HashManager.js +34 -2
  24. package/dist/managers/LiveSyncManagers.js +6 -0
  25. package/dist/pouchdb/LiveSyncDBFunctions.d.ts +1 -1
  26. package/dist/pouchdb/LiveSyncDBFunctions.js +33 -38
  27. package/dist/pouchdb/negotiation.js +7 -3
  28. package/dist/pouchdb/remoteFeatureCompatibility.d.ts +10 -0
  29. package/dist/pouchdb/remoteFeatureCompatibility.js +12 -1
  30. package/dist/pouchdb/remoteIdCompatibility.d.ts +5 -0
  31. package/dist/pouchdb/remoteIdCompatibility.js +28 -0
  32. package/dist/replication/CentralCompatibility.d.ts +1 -0
  33. package/dist/replication/CentralCompatibility.js +2 -1
  34. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +6 -2
  35. package/dist/replication/couchdb/LiveSyncReplicator.js +24 -5
  36. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +1 -0
  37. package/dist/replication/journal/LiveSyncJournalReplicator.js +50 -5
  38. package/dist/replication/trystero/ProxiedDB.d.ts +1 -1
  39. package/dist/replication/trystero/TrysteroReplicator.d.ts +369 -5
  40. package/dist/replication/trystero/TrysteroReplicator.js +31 -4
  41. package/dist/services/base/AppLifecycleService.d.ts +2 -0
  42. package/dist/services/base/AppLifecycleService.js +5 -0
  43. package/dist/services/base/IService.d.ts +1 -0
  44. package/dist/services/base/PathService.d.ts +3 -1
  45. package/dist/services/base/PathService.js +9 -4
  46. package/dist/services/base/SettingService.js +47 -0
  47. package/dist/settings.d.ts +2 -1
  48. package/dist/settings.js +14 -0
  49. package/dist/setupUri.d.ts +6 -0
  50. package/dist/setupUri.js +14 -0
  51. package/dist/string_and_binary/path.d.ts +1 -1
  52. package/dist/string_and_binary/path.js +3 -3
  53. package/dist/worker/bgWorker.js +1 -1
  54. package/docs/proven-in-use.md +12 -0
  55. package/docs/releasing.md +1 -1
  56. package/docs/setup-uri.md +58 -0
  57. package/package.json +17 -1
@@ -9,6 +9,9 @@ export declare class HashManager extends HashManagerCore {
9
9
  * Instance of the hash manager currently in use.
10
10
  */
11
11
  manager: HashManagerCore;
12
+ private chunkIdGenerator?;
13
+ clearCaches(): void;
14
+ private computeIndependentChunkId;
12
15
  /**
13
16
  * Checks whether the specified hash algorithm is available.
14
17
  *
@@ -43,6 +46,7 @@ export declare class HashManager extends HashManagerCore {
43
46
  * @returns The hash value (returned as a Promise)
44
47
  */
45
48
  computeHash(piece: string): Promise<string>;
49
+ usesIndependentIdKey(): boolean;
46
50
  /**
47
51
  * Computes the hash value without encryption.
48
52
  *
@@ -1,7 +1,8 @@
1
1
  import { FallbackWasmHashManager, XXHash32RawHashManager, XXHash64HashManager } from "./XXHashHashManager.js";
2
2
  import { FallbackPureJSHashManager, PureJSHashManager, SHA1HashManager } from "./PureJSHashManager.js";
3
- import { HashManagerCore } from "./HashManagerCore.js";
3
+ import { HashEncryptedPrefix, HashManagerCore } from "./HashManagerCore.js";
4
4
  import { LOG_LEVEL_VERBOSE, Logger } from "../../common/logger.js";
5
+ import { createChunkIdGenerator, configuredIdKey } from "../../common/idDerivation.js";
5
6
  const HashManagers = [
6
7
  XXHash64HashManager,
7
8
  XXHash32RawHashManager,
@@ -16,6 +17,22 @@ class HashManager extends HashManagerCore {
16
17
  * Instance of the hash manager currently in use.
17
18
  */
18
19
  manager = void 0;
20
+ chunkIdGenerator;
21
+ clearCaches() {
22
+ this.chunkIdGenerator = void 0;
23
+ }
24
+ async computeIndependentChunkId(key, piece) {
25
+ let cached = this.chunkIdGenerator;
26
+ if (cached?.key !== key) {
27
+ const entry = { key, task: createChunkIdGenerator(key) };
28
+ this.chunkIdGenerator = cached = entry;
29
+ void entry.task.catch(() => {
30
+ if (this.chunkIdGenerator === entry) this.clearCaches();
31
+ });
32
+ }
33
+ const generate = await cached.task;
34
+ return await generate(piece);
35
+ }
19
36
  /**
20
37
  * Checks whether the specified hash algorithm is available.
21
38
  *
@@ -71,7 +88,19 @@ class HashManager extends HashManagerCore {
71
88
  * @returns The hash value (returned as a Promise)
72
89
  */
73
90
  async computeHash(piece) {
74
- return await this.manager.computeHash(piece);
91
+ const settings = this.options.settingService.currentSettings();
92
+ const key = configuredIdKey(settings);
93
+ if (settings.encrypt && key) {
94
+ return HashEncryptedPrefix + await this.computeIndependentChunkId(key, piece);
95
+ }
96
+ this.clearCaches();
97
+ return settings.encrypt ? HashEncryptedPrefix + await this.manager.computeHashWithEncryption(piece) : await this.manager.computeHashWithoutEncryption(piece);
98
+ }
99
+ usesIndependentIdKey() {
100
+ const settings = this.options.settingService.currentSettings();
101
+ const active = settings.encrypt && configuredIdKey(settings) !== false;
102
+ if (!active) this.clearCaches();
103
+ return active;
75
104
  }
76
105
  /**
77
106
  * Computes the hash value without encryption.
@@ -89,6 +118,9 @@ class HashManager extends HashManagerCore {
89
118
  * @returns The hash value (returned as a Promise)
90
119
  */
91
120
  computeHashWithEncryption(piece) {
121
+ const key = configuredIdKey(this.options.settingService.currentSettings());
122
+ if (key) return this.computeIndependentChunkId(key, piece);
123
+ this.clearCaches();
92
124
  return this.manager.computeHashWithEncryption(piece);
93
125
  }
94
126
  }
@@ -39,6 +39,8 @@ class LiveSyncManagers {
39
39
  }
40
40
  async teardownManagers() {
41
41
  this.log("Teardown LiveSync Managers...", LOG_LEVEL_VERBOSE);
42
+ this.hashManager?.clearCaches();
43
+ this.entryManager?.hashManager?.clearCaches();
42
44
  if (this.changeManager) {
43
45
  this.changeManager.teardown();
44
46
  this.changeManager = void 0;
@@ -123,8 +125,12 @@ class LiveSyncManagers {
123
125
  }
124
126
  clearCaches() {
125
127
  this.chunkManager?.clearCaches();
128
+ this.hashManager?.clearCaches();
129
+ this.entryManager?.hashManager?.clearCaches();
126
130
  }
127
131
  async prepareHashFunction() {
132
+ this.hashManager?.clearCaches();
133
+ this.entryManager?.hashManager?.clearCaches();
128
134
  this.hashManager = new HashManager({
129
135
  settingService: this.options.settingService
130
136
  });
@@ -1,6 +1,6 @@
1
1
  import { type EntryDoc, type EntryMilestoneInfo, type RemoteDBSettings, type ChunkVersionRange, type TweakValues, type DeviceInfo, type TweakAssessment } from "../common/types.js";
2
2
  export declare function getEffectiveTweakValues(setting: RemoteDBSettings): TweakValues;
3
- export type ENSURE_DB_RESULT = "OK" | "INCOMPATIBLE" | "LOCKED" | "NODE_LOCKED" | "NODE_CLEANED" | ["MISMATCHED", TweakValues];
3
+ export type ENSURE_DB_RESULT = "OK" | "INCOMPATIBLE" | "ID_KEY_MISMATCH" | "LOCKED" | "NODE_LOCKED" | "NODE_CLEANED" | ["MISMATCHED", TweakValues];
4
4
  /**
5
5
  * Ensures that the remote database is compatible with the current device.
6
6
  *
@@ -34,7 +34,38 @@ async function ensureRemoteIsCompatible(infoSrc, setting, deviceNodeID, currentV
34
34
  let remoteMilestone = infoSrc;
35
35
  if (!remoteMilestone) remoteMilestone = baseMilestone;
36
36
  const currentTweakValues = getEffectiveTweakValues(setting);
37
- remoteMilestone.node_chunk_info = { ...baseMilestone.node_chunk_info, ...remoteMilestone.node_chunk_info };
37
+ const nodeChunkInfo = { ...baseMilestone.node_chunk_info, ...remoteMilestone.node_chunk_info };
38
+ let globalMin = currentVersionRange.min;
39
+ let globalMax = currentVersionRange.max;
40
+ for (const nodeId of remoteMilestone.accepted_nodes) {
41
+ if (nodeId == deviceNodeID) continue;
42
+ if (nodeId in nodeChunkInfo) {
43
+ const nodeInfo = nodeChunkInfo[nodeId];
44
+ globalMin = Math.max(nodeInfo.min, globalMin);
45
+ globalMax = Math.min(nodeInfo.max, globalMax);
46
+ } else {
47
+ globalMin = 0;
48
+ globalMax = 0;
49
+ }
50
+ }
51
+ if (globalMax < globalMin && !setting.ignoreVersionCheck) {
52
+ return "INCOMPATIBLE";
53
+ }
54
+ const preferred_tweak = remoteMilestone.tweak_values?.[DEVICE_ID_PREFERRED] ?? currentTweakValues;
55
+ const tweakAssessment = assessTweakCompatibility(currentTweakValues, preferred_tweak);
56
+ if (tweakAssessment.entries.some(({ key, relation }) => key === "idDerivationVersion" && relation === "different")) {
57
+ return "ID_KEY_MISMATCH";
58
+ }
59
+ if (!setting.disableCheckingConfigMismatch) {
60
+ recordTweakAssessment?.(tweakAssessment);
61
+ if (tweakAssessment.alignment === "mismatched") {
62
+ return ["MISMATCHED", preferred_tweak];
63
+ }
64
+ }
65
+ if (remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(deviceNodeID) == -1) {
66
+ return remoteMilestone.cleaned ? "NODE_CLEANED" : "NODE_LOCKED";
67
+ }
68
+ remoteMilestone.node_chunk_info = nodeChunkInfo;
38
69
  let writeMilestone = remoteMilestone.node_chunk_info[deviceNodeID].min != currentVersionRange.min || remoteMilestone.node_chunk_info[deviceNodeID].max != currentVersionRange.max || isObjectDifferent(remoteMilestone.tweak_values?.[deviceNodeID], currentTweakValues) || typeof remoteMilestone._rev == "undefined" || !(DEVICE_ID_PREFERRED in remoteMilestone.tweak_values);
39
70
  if (!remoteMilestone.node_info) {
40
71
  remoteMilestone.node_info = {};
@@ -69,43 +100,7 @@ async function ensureRemoteIsCompatible(infoSrc, setting, deviceNodeID, currentV
69
100
  }
70
101
  await updateCallback(remoteMilestone);
71
102
  }
72
- let globalMin = currentVersionRange.min;
73
- let globalMax = currentVersionRange.max;
74
- for (const nodeId of remoteMilestone.accepted_nodes) {
75
- if (nodeId == deviceNodeID) continue;
76
- if (nodeId in remoteMilestone.node_chunk_info) {
77
- const nodeInfo = remoteMilestone.node_chunk_info[nodeId];
78
- globalMin = Math.max(nodeInfo.min, globalMin);
79
- globalMax = Math.min(nodeInfo.max, globalMax);
80
- } else {
81
- globalMin = 0;
82
- globalMax = 0;
83
- }
84
- }
85
- if (globalMax < globalMin) {
86
- if (!setting.ignoreVersionCheck) {
87
- return "INCOMPATIBLE";
88
- }
89
- }
90
- if (!setting.disableCheckingConfigMismatch) {
91
- const preferred_tweak = remoteMilestone.tweak_values?.[DEVICE_ID_PREFERRED] ?? currentTweakValues;
92
- const current_tweak = currentTweakValues;
93
- const tweakAssessment = assessTweakCompatibility(current_tweak, preferred_tweak);
94
- recordTweakAssessment?.(tweakAssessment);
95
- if (tweakAssessment.alignment === "mismatched") {
96
- return ["MISMATCHED", preferred_tweak];
97
- }
98
- }
99
- if (remoteMilestone.locked) {
100
- if (remoteMilestone.accepted_nodes.indexOf(deviceNodeID) == -1) {
101
- if (remoteMilestone.cleaned) {
102
- return "NODE_CLEANED";
103
- }
104
- return "NODE_LOCKED";
105
- }
106
- return "LOCKED";
107
- }
108
- return "OK";
103
+ return remoteMilestone.locked ? "LOCKED" : "OK";
109
104
  }
110
105
  async function ensureDatabaseIsCompatible(db, setting, deviceNodeID, currentVersionRange, nodeDeviceInfo, recordTweakAssessment) {
111
106
  const remoteMilestone = await resolveWithIgnoreKnownError(
@@ -9,7 +9,7 @@ import { isErrorOfMissingDoc } from "./utils_couchdb.js";
9
9
  import {
10
10
  assessRemoteFeatureDocument,
11
11
  describeRemoteFeatureRejection,
12
- ENCRYPTED_INTERNAL_METADATA_FEATURE,
12
+ supportedFeatures,
13
13
  REMOTE_FEATURE_GENERATION
14
14
  } from "./remoteFeatureCompatibility.js";
15
15
  const checkRemoteVersion = async (db, migrate, barrier = VER, requiredFeatures = []) => {
@@ -32,7 +32,11 @@ const checkRemoteVersion = async (db, migrate, barrier = VER, requiredFeatures =
32
32
  if (isErrorOfMissingDoc(ex)) {
33
33
  const info = await db.info();
34
34
  if (info.doc_count > 0) return false;
35
- return await bumpRemoteVersion(db, requiredFeatures.length ? REMOTE_FEATURE_GENERATION : VER, requiredFeatures);
35
+ return await bumpRemoteVersion(
36
+ db,
37
+ requiredFeatures.length ? REMOTE_FEATURE_GENERATION : VER,
38
+ requiredFeatures
39
+ );
36
40
  }
37
41
  throw ex;
38
42
  }
@@ -75,7 +79,7 @@ const bumpRemoteVersion = async (db, barrier = VER, usedFeatures = []) => {
75
79
  };
76
80
  async function declareRemoteFeatures(db, requiredFeatures) {
77
81
  const requested = [...new Set(requiredFeatures)];
78
- if (requested.some((name) => name !== ENCRYPTED_INTERNAL_METADATA_FEATURE)) {
82
+ if (requested.some((name) => !supportedFeatures.has(name))) {
79
83
  throw new Error("A writer requested an unsupported remote feature.");
80
84
  }
81
85
  if (requested.length === 0) return true;
@@ -1,5 +1,15 @@
1
1
  export declare const REMOTE_FEATURE_GENERATION = 13;
2
2
  export declare const ENCRYPTED_INTERNAL_METADATA_FEATURE = "encrypted-internal-metadata-v1";
3
+ export declare const INDEPENDENT_ID_DERIVATION_FEATURE = "independent-id-derivation-v1";
4
+ export declare const supportedFeatures: Set<string>;
5
+ export declare function requiredRemoteFeatures(setting: {
6
+ encrypt?: boolean;
7
+ usePathObfuscation?: boolean;
8
+ E2EEAlgorithm?: string;
9
+ encryptInternalMetadata?: boolean;
10
+ idDerivationVersion: 0 | 1;
11
+ idDerivationKey: string;
12
+ }): string[];
3
13
  export declare function usesEncryptedInternalMetadata(setting: {
4
14
  encrypt?: boolean;
5
15
  usePathObfuscation?: boolean;
@@ -1,7 +1,15 @@
1
1
  import { E2EEAlgorithms, VERSIONING_DOCID } from "../common/types.js";
2
+ import { configuredIdKey } from "../common/idDerivation.js";
2
3
  const REMOTE_FEATURE_GENERATION = 13;
3
4
  const ENCRYPTED_INTERNAL_METADATA_FEATURE = "encrypted-internal-metadata-v1";
4
- const supportedFeatures = /* @__PURE__ */ new Set([ENCRYPTED_INTERNAL_METADATA_FEATURE]);
5
+ const INDEPENDENT_ID_DERIVATION_FEATURE = "independent-id-derivation-v1";
6
+ const supportedFeatures = /* @__PURE__ */ new Set([ENCRYPTED_INTERNAL_METADATA_FEATURE, INDEPENDENT_ID_DERIVATION_FEATURE]);
7
+ function requiredRemoteFeatures(setting) {
8
+ const features = [];
9
+ if (usesEncryptedInternalMetadata(setting)) features.push(ENCRYPTED_INTERNAL_METADATA_FEATURE);
10
+ if (setting.encrypt && configuredIdKey(setting)) features.push(INDEPENDENT_ID_DERIVATION_FEATURE);
11
+ return features;
12
+ }
5
13
  function usesEncryptedInternalMetadata(setting) {
6
14
  return setting.encryptInternalMetadata === true && setting.encrypt === true && setting.usePathObfuscation === true && setting.E2EEAlgorithm === E2EEAlgorithms.V2;
7
15
  }
@@ -45,8 +53,11 @@ function describeRemoteFeatureRejection(assessment) {
45
53
  }
46
54
  export {
47
55
  ENCRYPTED_INTERNAL_METADATA_FEATURE,
56
+ INDEPENDENT_ID_DERIVATION_FEATURE,
48
57
  REMOTE_FEATURE_GENERATION,
49
58
  assessRemoteFeatureDocument,
50
59
  describeRemoteFeatureRejection,
60
+ requiredRemoteFeatures,
61
+ supportedFeatures,
51
62
  usesEncryptedInternalMetadata
52
63
  };
@@ -0,0 +1,5 @@
1
+ import type { DocumentID, EntryDoc, RemoteDBSettings } from "../common/types.js";
2
+ import { type FilePathWithPrefix } from "../common/types.js";
3
+ export type RemoteIdCompatibility = "matching" | "mismatched" | "unverified";
4
+ /** Check a bounded set of existing document IDs before admitting an ID configuration. */
5
+ export declare function assessRemoteDocumentIds(db: PouchDB.Database<EntryDoc>, setting: Pick<RemoteDBSettings, "encrypt" | "idDerivationVersion" | "idDerivationKey" | "usePathObfuscation" | "passphrase" | "handleFilenameCaseSensitive">, path2id: (path: FilePathWithPrefix) => Promise<DocumentID>): Promise<RemoteIdCompatibility>;
@@ -0,0 +1,28 @@
1
+ import { configuredIdKey } from "../common/idDerivation.js";
2
+ import { ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
3
+ import { PREFIX_OBFUSCATED } from "../common/types.js";
4
+ async function assessRemoteDocumentIds(db, setting, path2id) {
5
+ configuredIdKey(setting);
6
+ if (!setting.encrypt || !setting.usePathObfuscation) return "unverified";
7
+ let matched = false;
8
+ for (const prefix of [
9
+ PREFIX_OBFUSCATED,
10
+ `${ICHeader}${PREFIX_OBFUSCATED}`,
11
+ `${ICXHeader}${PREFIX_OBFUSCATED}`,
12
+ `${PSCHeader}${PREFIX_OBFUSCATED}`
13
+ ]) {
14
+ const rows = await db.allDocs({ startkey: prefix, endkey: `${prefix}\uFFF0`, limit: 2 });
15
+ for (const row of rows.rows) {
16
+ if ("error" in row || !row.id) continue;
17
+ const doc = await db.get(row.id);
18
+ if (!("path" in doc) || typeof doc.path !== "string") continue;
19
+ const expected = await path2id(doc.path);
20
+ if (expected !== doc._id) return "mismatched";
21
+ matched = true;
22
+ }
23
+ }
24
+ return matched ? "matching" : "unverified";
25
+ }
26
+ export {
27
+ assessRemoteDocumentIds
28
+ };
@@ -11,6 +11,7 @@ export declare const CENTRAL_COMPATIBILITY_REJECTION_REASONS: Readonly<{
11
11
  readonly NODE_LOCKED: "node-locked";
12
12
  readonly NODE_CLEANED: "node-cleaned";
13
13
  readonly TWEAK_MISMATCH: "tweak-mismatch";
14
+ readonly ID_DERIVATION_MISMATCH: "id-derivation-mismatch";
14
15
  }>;
15
16
  export type CentralCompatibilityRejectionReason = (typeof CENTRAL_COMPATIBILITY_REJECTION_REASONS)[keyof typeof CENTRAL_COMPATIBILITY_REJECTION_REASONS];
16
17
  export declare const CENTRAL_COMPATIBILITY_NOT_ASSESSED: Readonly<{
@@ -7,7 +7,8 @@ const CENTRAL_COMPATIBILITY_REJECTION_REASONS = Object.freeze({
7
7
  INCOMPATIBLE_VERSION: "incompatible-version",
8
8
  NODE_LOCKED: "node-locked",
9
9
  NODE_CLEANED: "node-cleaned",
10
- TWEAK_MISMATCH: "tweak-mismatch"
10
+ TWEAK_MISMATCH: "tweak-mismatch",
11
+ ID_DERIVATION_MISMATCH: "id-derivation-mismatch"
11
12
  });
12
13
  const CENTRAL_COMPATIBILITY_NOT_ASSESSED = Object.freeze({
13
14
  status: CENTRAL_COMPATIBILITY_STATUSES.NOT_ASSESSED
@@ -1,16 +1,20 @@
1
1
  import { type EntryDoc, type RemoteDBSettings, type EntryLeaf, type SyncParameters, type DatabaseEntry, type NodeData, type RemotePreferredTweakResult } from "../../common/types.js";
2
2
  import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "../LiveSyncAbstractReplicator.js";
3
3
  import type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "../../services/base/RemoteConnection.js";
4
+ import type { IPathService } from "../../services/base/IService.js";
4
5
  import { type CentralCompatibilityDecisionRecorder } from "../CentralCompatibility.js";
5
6
  import { type ReplicationOutcome } from "../ReplicatorProvider.js";
6
7
  export type { OwnedCouchDBConnection, RemoteConnectionOpenOptions } from "../../services/base/RemoteConnection.js";
7
8
  /**
8
9
  * Compatibility constructor environment for the CouchDB Replicator facade.
9
10
  *
10
- * CouchDB adds only the bounded one-shot preflight policy to the shared
11
- * Replicator environment. Active-provider capabilities remain separate.
11
+ * CouchDB needs the host path service for remote ID checks and adds the
12
+ * bounded one-shot preflight policy. Active-provider capabilities remain separate.
12
13
  */
13
14
  export interface LiveSyncCouchDBReplicatorEnv extends LiveSyncReplicatorEnv {
15
+ services: LiveSyncReplicatorEnv["services"] & {
16
+ path: IPathService;
17
+ };
14
18
  /** Internal injection point for the bounded one-shot connectivity preflight. */
15
19
  oneShotConnectivityTimeoutMs?: number;
16
20
  }
@@ -22,10 +22,8 @@ import {
22
22
  } from "../../common/utils.js";
23
23
  import { Logger } from "../../common/logger.js";
24
24
  import { checkRemoteVersion, countCompromisedChunks, declareRemoteFeatures } from "../../pouchdb/negotiation.js";
25
- import {
26
- ENCRYPTED_INTERNAL_METADATA_FEATURE,
27
- usesEncryptedInternalMetadata
28
- } from "../../pouchdb/remoteFeatureCompatibility.js";
25
+ import { requiredRemoteFeatures, usesEncryptedInternalMetadata } from "../../pouchdb/remoteFeatureCompatibility.js";
26
+ import { assessRemoteDocumentIds } from "../../pouchdb/remoteIdCompatibility.js";
29
27
  import { isErrorOfMissingDoc } from "../../pouchdb/utils_couchdb.js";
30
28
  import { preprocessOutgoing } from "../../pouchdb/encryption.js";
31
29
  import { ensureDatabaseIsCompatible, getEffectiveTweakValues } from "../../pouchdb/LiveSyncDBFunctions.js";
@@ -889,6 +887,21 @@ ${sendingDocs}/(${sentDocsCount} done)`,
889
887
  this.remoteLockedAndDeviceNotAccepted = false;
890
888
  this.tweakSettingsMismatched = false;
891
889
  this.preferredTweakValue = void 0;
890
+ const idCompatibility = await assessRemoteDocumentIds(
891
+ dbRet.db,
892
+ setting,
893
+ (path) => this.env.services.path.path2idWithSettings(path, setting)
894
+ );
895
+ if (idCompatibility === "mismatched") {
896
+ recordCompatibilityDecision?.(
897
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.ID_DERIVATION_MISMATCH)
898
+ );
899
+ Logger("The remote document IDs do not match the configured ID key.", LOG_LEVEL_NOTICE);
900
+ return false;
901
+ }
902
+ if (idCompatibility === "unverified" && setting.idDerivationVersion === 1 && setting.usePathObfuscation) {
903
+ Logger("No remote document was available to verify the configured ID key.", LOG_LEVEL_INFO);
904
+ }
892
905
  if (!await checkRemoteVersion(dbRet.db, this.migrate.bind(this), VER)) {
893
906
  recordCompatibilityDecision?.(
894
907
  centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.INCOMPATIBLE_VERSION)
@@ -953,6 +966,11 @@ ${sendingDocs}/(${sentDocsCount} done)`,
953
966
  this.remoteCleaned = true;
954
967
  return false;
955
968
  }
969
+ } else if (ensure == "ID_KEY_MISMATCH") {
970
+ recordCompatibilityDecision?.(
971
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.ID_DERIVATION_MISMATCH)
972
+ );
973
+ return false;
956
974
  } else if (ensure == "OK") {
957
975
  } else if (ensure[0] == "MISMATCHED") {
958
976
  recordCompatibilityDecision?.(
@@ -967,7 +985,8 @@ ${sendingDocs}/(${sentDocsCount} done)`,
967
985
  this.preferredTweakValue = ensure[1];
968
986
  return false;
969
987
  }
970
- if (usesEncryptedInternalMetadata(setting) && !await declareRemoteFeatures(dbRet.db, [ENCRYPTED_INTERNAL_METADATA_FEATURE])) {
988
+ const requiredFeatures = requiredRemoteFeatures(setting);
989
+ if (requiredFeatures.length > 0 && !await declareRemoteFeatures(dbRet.db, requiredFeatures)) {
971
990
  recordCompatibilityDecision?.(
972
991
  centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.INCOMPATIBLE_VERSION)
973
992
  );
@@ -53,6 +53,7 @@ export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicato
53
53
  closeReplication(): void;
54
54
  tryResetRemoteDatabase(setting: RemoteDBSettings): Promise<void>;
55
55
  tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise<void>;
56
+ private createInitialIdDerivationProof;
56
57
  markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise<void>;
57
58
  markRemoteResolved(setting: RemoteDBSettings): Promise<void>;
58
59
  tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise<boolean>;
@@ -12,6 +12,8 @@ import { LiveSyncAbstractReplicator } from "../LiveSyncAbstractReplicator.js";
12
12
  import { ensureRemoteIsCompatible } from "../../pouchdb/LiveSyncDBFunctions.js";
13
13
  import { getEffectiveTweakValues } from "../../pouchdb/LiveSyncDBFunctions.js";
14
14
  import { JournalStorageReadStatuses } from "./objectstore/JournalStorageAdapter.js";
15
+ import { computeKeyedId, configuredIdKey } from "../../common/idDerivation.js";
16
+ import { decryptString, encryptString } from "../../encryption/stringEncryption.js";
15
17
  import {
16
18
  CENTRAL_COMPATIBILITY_ACCEPTED,
17
19
  CENTRAL_COMPATIBILITY_NOT_ASSESSED,
@@ -35,6 +37,9 @@ const currentVersionRange = {
35
37
  max: 2,
36
38
  current: 2
37
39
  };
40
+ function journalVersionRange(setting) {
41
+ return configuredIdKey(setting) && setting.encrypt && setting.usePathObfuscation ? { min: 3, max: 3, current: 3 } : currentVersionRange;
42
+ }
38
43
  class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
39
44
  get client() {
40
45
  return this.setupJournalSyncClient();
@@ -73,6 +78,27 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
73
78
  throw milestoneResult.error;
74
79
  }
75
80
  const downloadedMilestone = milestoneResult.status === JournalStorageReadStatuses.AVAILABLE ? milestoneResult.value : false;
81
+ const idKey = configuredIdKey(setting);
82
+ let encryptedProof;
83
+ const preferredTweak = downloadedMilestone && downloadedMilestone.tweak_values?.[DEVICE_ID_PREFERRED];
84
+ if (preferredTweak?.idDerivationVersion === 1 && preferredTweak.usePathObfuscation === true && (!idKey || !setting.encrypt || !setting.usePathObfuscation)) {
85
+ return "ID_KEY_MISMATCH";
86
+ }
87
+ if (idKey && setting.encrypt && setting.usePathObfuscation) {
88
+ const expected = await computeKeyedId(idKey, "remote-agreement", "journal-milestone-v1");
89
+ const stored = downloadedMilestone && downloadedMilestone.encrypted_id_derivation_proof;
90
+ if (stored) {
91
+ try {
92
+ if (await decryptString(stored, setting.passphrase) !== expected) return "ID_KEY_MISMATCH";
93
+ } catch {
94
+ return "ID_KEY_MISMATCH";
95
+ }
96
+ } else if (downloadedMilestone) {
97
+ return "ID_KEY_MISMATCH";
98
+ } else {
99
+ encryptedProof = await encryptString(expected, setting.passphrase);
100
+ }
101
+ }
76
102
  const cPointInfo = await client.getCheckpointInfo();
77
103
  const progress = [...cPointInfo?.receivedFiles || []].sort().pop() || "";
78
104
  return await ensureRemoteIsCompatible(
@@ -88,6 +114,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
88
114
  progress
89
115
  },
90
116
  async (info) => {
117
+ if (encryptedProof) info.encrypted_id_derivation_proof = encryptedProof;
91
118
  if (!await client.uploadJson(MILSTONE_DOCID, info)) {
92
119
  throw new Error("Could not upload remote milestone");
93
120
  }
@@ -269,7 +296,7 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
269
296
  let tweakAssessment;
270
297
  const ensure = await this.ensureBucketIsCompatible(
271
298
  this.nodeid,
272
- currentVersionRange,
299
+ journalVersionRange(setting),
273
300
  setting,
274
301
  client,
275
302
  (assessment) => {
@@ -285,6 +312,12 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
285
312
  LOG_LEVEL_NOTICE
286
313
  );
287
314
  return false;
315
+ } else if (ensure == "ID_KEY_MISMATCH") {
316
+ recordCompatibilityDecision?.(
317
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.ID_DERIVATION_MISMATCH)
318
+ );
319
+ Logger("The remote ID derivation key does not match this device.", LOG_LEVEL_NOTICE);
320
+ return false;
288
321
  } else if (ensure == "NODE_LOCKED") {
289
322
  recordCompatibilityDecision?.(
290
323
  centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_LOCKED)
@@ -366,6 +399,12 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
366
399
  }
367
400
  return await Promise.resolve();
368
401
  }
402
+ async createInitialIdDerivationProof(setting) {
403
+ const idKey = configuredIdKey(setting);
404
+ if (!idKey || !setting.encrypt || !setting.usePathObfuscation) return void 0;
405
+ const proof = await computeKeyedId(idKey, "remote-agreement", "journal-milestone-v1");
406
+ return await encryptString(proof, setting.passphrase);
407
+ }
369
408
  async markRemoteLocked(setting, locked, lockByClean) {
370
409
  const defInitPoint = {
371
410
  _id: MILSTONE_DOCID,
@@ -374,14 +413,17 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
374
413
  locked,
375
414
  cleaned: lockByClean,
376
415
  accepted_nodes: [this.nodeid],
377
- node_chunk_info: { [this.nodeid]: currentVersionRange },
416
+ node_chunk_info: { [this.nodeid]: journalVersionRange(setting) },
378
417
  node_info: {},
379
418
  tweak_values: {}
380
419
  };
381
420
  const client = this.setupJournalSyncClient(setting);
421
+ const existingMilestone = await readRemoteMilestone(client);
422
+ const initialProof = existingMilestone ? void 0 : await this.createInitialIdDerivationProof(setting);
382
423
  const remoteMilestone = {
383
424
  ...defInitPoint,
384
- ...await readRemoteMilestone(client) ?? {}
425
+ ...existingMilestone ?? {},
426
+ ...initialProof ? { encrypted_id_derivation_proof: initialProof } : {}
385
427
  };
386
428
  remoteMilestone.node_chunk_info = { ...defInitPoint.node_chunk_info, ...remoteMilestone.node_chunk_info };
387
429
  remoteMilestone.accepted_nodes = [this.nodeid];
@@ -403,14 +445,17 @@ class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator {
403
445
  created: Date.now(),
404
446
  locked: false,
405
447
  accepted_nodes: [this.nodeid],
406
- node_chunk_info: { [this.nodeid]: currentVersionRange },
448
+ node_chunk_info: { [this.nodeid]: journalVersionRange(setting) },
407
449
  node_info: {},
408
450
  tweak_values: {}
409
451
  };
410
452
  const client = this.setupJournalSyncClient(setting);
453
+ const existingMilestone = await readRemoteMilestone(client);
454
+ const initialProof = existingMilestone ? void 0 : await this.createInitialIdDerivationProof(setting);
411
455
  const remoteMilestone = {
412
456
  ...defInitPoint,
413
- ...await readRemoteMilestone(client) ?? {}
457
+ ...existingMilestone ?? {},
458
+ ...initialProof ? { encrypted_id_derivation_proof: initialProof } : {}
414
459
  };
415
460
  remoteMilestone.node_chunk_info = { ...defInitPoint.node_chunk_info, ...remoteMilestone.node_chunk_info };
416
461
  remoteMilestone.accepted_nodes = Array.from(/* @__PURE__ */ new Set([...remoteMilestone.accepted_nodes, this.nodeid]));
@@ -4,7 +4,7 @@ export declare function createHostingDB(env: ReplicatorHostEnv): {
4
4
  info: () => Promise<PouchDB.Core.DatabaseInfo>;
5
5
  changes: (options: PouchDB.Core.ChangesOptions) => PouchDB.Core.Changes<EntryDoc>;
6
6
  revsDiff: (diff: PouchDB.Core.RevisionDiffOptions) => Promise<PouchDB.Core.RevisionDiffResponse>;
7
- bulkDocs: (docs: PouchDB.Core.PutDocument<EntryDoc & Record<string, unknown>>[], options?: PouchDB.Core.BulkDocsOptions) => Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]>;
7
+ bulkDocs: (docs: PouchDB.Core.PutDocument<EntryDoc & Record<string, unknown>>[], options?: PouchDB.Core.BulkDocsOptions) => Promise<(PouchDB.Core.Error | PouchDB.Core.Response)[]>;
8
8
  bulkGet: (options: PouchDB.Core.BulkGetOptions) => Promise<PouchDB.Core.BulkGetResponse<EntryDoc>>;
9
9
  put: (doc: PouchDB.Core.PutDocument<EntryDoc & Record<string, unknown>>, options?: PouchDB.Core.PutOptions) => Promise<PouchDB.Core.Response>;
10
10
  get: (id: string, options?: PouchDB.Core.GetOptions) => Promise<EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta>;