@vrtmrz/livesync-commonlib 0.1.30 → 0.1.32
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.
- package/README.md +5 -0
- package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
- package/dist/API/DirectFileManipulatorV2.js +17 -5
- package/dist/API/processSetting.d.ts +19 -0
- package/dist/API/processSetting.js +91 -6
- package/dist/common/idDerivation.d.ts +13 -0
- package/dist/common/idDerivation.js +75 -0
- package/dist/common/models/db.definition.d.ts +1 -0
- package/dist/common/models/setting.const.defaults.js +3 -0
- package/dist/common/models/setting.const.qr.js +4 -1
- package/dist/common/models/setting.type.d.ts +6 -0
- package/dist/common/models/tweak.compatibility.js +6 -2
- package/dist/common/models/tweak.definition.d.ts +8 -0
- package/dist/common/models/tweak.definition.js +4 -3
- package/dist/common/settingConstants.d.ts +3 -0
- package/dist/common/utils.js +3 -1
- package/dist/events/coreEvents.d.ts +3 -0
- package/dist/events/coreEvents.js +2 -0
- package/dist/managers/EntryManager/EntryManagerImpls.js +10 -1
- package/dist/managers/HashManager/HashManager.d.ts +1 -0
- package/dist/managers/HashManager/HashManager.js +13 -1
- package/dist/pouchdb/LiveSyncDBFunctions.d.ts +1 -1
- package/dist/pouchdb/LiveSyncDBFunctions.js +33 -38
- package/dist/pouchdb/negotiation.js +7 -3
- package/dist/pouchdb/remoteFeatureCompatibility.d.ts +10 -0
- package/dist/pouchdb/remoteFeatureCompatibility.js +12 -1
- package/dist/pouchdb/remoteIdCompatibility.d.ts +5 -0
- package/dist/pouchdb/remoteIdCompatibility.js +28 -0
- package/dist/replication/CentralCompatibility.d.ts +1 -0
- package/dist/replication/CentralCompatibility.js +2 -1
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +6 -2
- package/dist/replication/couchdb/LiveSyncReplicator.js +24 -5
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +1 -0
- package/dist/replication/journal/LiveSyncJournalReplicator.js +50 -5
- package/dist/replication/trystero/TrysteroReplicator.d.ts +369 -5
- package/dist/replication/trystero/TrysteroReplicator.js +31 -4
- package/dist/services/base/AppLifecycleService.d.ts +2 -0
- package/dist/services/base/AppLifecycleService.js +5 -0
- package/dist/services/base/IService.d.ts +1 -0
- package/dist/services/base/PathService.d.ts +3 -1
- package/dist/services/base/PathService.js +9 -4
- package/dist/services/base/SettingService.d.ts +2 -0
- package/dist/services/base/SettingService.js +57 -3
- package/dist/settings.d.ts +2 -1
- package/dist/settings.js +14 -0
- package/dist/setupUri.d.ts +6 -0
- package/dist/setupUri.js +14 -0
- package/dist/string_and_binary/path.d.ts +1 -1
- package/dist/string_and_binary/path.js +3 -3
- package/dist/worker/bgWorker.js +1 -1
- package/docs/proven-in-use.md +8 -0
- package/docs/releasing.md +1 -1
- package/docs/setup-uri.md +58 -0
- package/package.json +9 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]:
|
|
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
|
-
...
|
|
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]:
|
|
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
|
-
...
|
|
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]));
|