@vrtmrz/livesync-commonlib 0.1.28 → 0.1.30
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 -1
- package/dist/API/DirectFileManipulatorV2.d.ts +1 -0
- package/dist/API/DirectFileManipulatorV2.js +19 -2
- package/dist/API/processSetting.d.ts +179 -1
- package/dist/API/processSetting.js +5 -1
- package/dist/common/configForDoc.d.ts +1 -0
- package/dist/common/configForDoc.js +24 -7
- package/dist/common/models/db.type.d.ts +1 -0
- package/dist/common/models/setting.const.defaults.js +2 -0
- package/dist/common/models/setting.const.qr.js +2 -1
- package/dist/common/models/setting.lifecycle.js +3 -1
- package/dist/common/models/setting.type.d.ts +2 -0
- package/dist/common/models/shared.definition.configNames.js +4 -0
- package/dist/common/models/tweak.definition.d.ts +6 -0
- package/dist/common/models/tweak.definition.js +1 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/utils.js +2 -1
- package/dist/pouchdb/LiveSyncDBFunctions.d.ts +1 -0
- package/dist/pouchdb/LiveSyncDBFunctions.js +12 -3
- package/dist/pouchdb/StreamingFetch.d.ts +2 -0
- package/dist/pouchdb/StreamingFetch.js +28 -0
- package/dist/pouchdb/encryption.d.ts +2 -2
- package/dist/pouchdb/encryption.js +25 -15
- package/dist/pouchdb/negotiation.d.ts +3 -2
- package/dist/pouchdb/negotiation.js +84 -32
- package/dist/pouchdb/remoteFeatureCompatibility.d.ts +25 -0
- package/dist/pouchdb/remoteFeatureCompatibility.js +52 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +21 -9
- package/dist/replication/index.d.ts +2 -0
- package/dist/replication/index.js +13 -1
- package/dist/replication/journal/LiveSyncJournalReplicator.js +2 -5
- package/dist/serviceModules/Rebuilder.js +11 -6
- package/dist/services/base/CommonlibMessages.d.ts +2 -0
- package/dist/services/base/CommonlibMessages.js +2 -0
- package/dist/services/base/RemoteConnection.d.ts +2 -0
- package/dist/services/base/RemoteService.js +9 -1
- package/dist/worker/bgWorker.js +1 -1
- package/docs/development.md +2 -0
- package/docs/proven-in-use.md +6 -0
- package/docs/remote-feature-compatibility.md +177 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,10 @@ New profile-management code can create or update a CouchDB, Object Storage, or P
|
|
|
119
119
|
|
|
120
120
|
See [the remote configuration profile guide](docs/remote-configurations.md) for creation, activation, P2P selection, legacy import, persistence, and verification responsibilities.
|
|
121
121
|
|
|
122
|
+
## Remote database feature compatibility
|
|
123
|
+
|
|
124
|
+
CouchDB stores the representations already in use as feature identifiers in its version document. Assess that document before synchronisation or direct file operations, and retain unknown identifiers for host diagnostics. See [the remote feature compatibility contract](docs/remote-feature-compatibility.md) for the wire format, update rules, and recovery boundaries.
|
|
125
|
+
|
|
122
126
|
## Contract scope
|
|
123
127
|
|
|
124
128
|
The package is currently an infrastructure and compatibility boundary. The context, rooted-storage, standard-I/O, and revision-tree safety contracts have focused cross-platform, instance-isolation, or real-PouchDB tests. Platform details which cannot be shared, such as file timestamp fidelity and browser permission handling, remain host concerns and are documented separately.
|
|
@@ -127,7 +131,7 @@ The package is currently an infrastructure and compatibility boundary. The conte
|
|
|
127
131
|
|
|
128
132
|
The accepted replacement direction is an asynchronously created file client with stable `list`, `get`, `put`, `delete`, `watch`, and `close` operations. The operation result, conflict, concurrency, watch checkpoint, retry, and error contracts still require focused decisions and tests before that client can be published.
|
|
129
133
|
|
|
130
|
-
Package developers should read [the developer guide](docs/development.md). The focused contracts are described in [the storage guide](docs/platform-storage.md), [the standard-I/O guide](docs/platform-standard-io.md), [the settings lifecycle guide](docs/settings-lifecycle.md), [the remote configuration profile guide](docs/remote-configurations.md), [the conflict-resolution guide](docs/conflict-resolution.md), and [the P2P transport lifecycle guide](docs/p2p-transport-lifecycle.md).
|
|
134
|
+
Package developers should read [the developer guide](docs/development.md). The focused contracts are described in [the storage guide](docs/platform-storage.md), [the standard-I/O guide](docs/platform-standard-io.md), [the settings lifecycle guide](docs/settings-lifecycle.md), [the remote configuration profile guide](docs/remote-configurations.md), [the remote feature compatibility contract](docs/remote-feature-compatibility.md), [the conflict-resolution guide](docs/conflict-resolution.md), and [the P2P transport lifecycle guide](docs/p2p-transport-lifecycle.md).
|
|
131
135
|
|
|
132
136
|
## Proven in maintained hosts
|
|
133
137
|
|
|
@@ -13,6 +13,7 @@ export type DirectFileManipulatorOptions = {
|
|
|
13
13
|
passphrase: string | undefined;
|
|
14
14
|
database: string;
|
|
15
15
|
obfuscatePassphrase: string | undefined;
|
|
16
|
+
encryptInternalMetadata?: boolean;
|
|
16
17
|
useDynamicIterationCount?: boolean;
|
|
17
18
|
customChunkSize?: number;
|
|
18
19
|
minimumChunkSize?: number;
|
|
@@ -3,8 +3,14 @@ import {
|
|
|
3
3
|
DEFAULT_SYNC_PARAMETERS,
|
|
4
4
|
ProtocolVersions,
|
|
5
5
|
DOCID_SYNC_PARAMETERS,
|
|
6
|
-
E2EEAlgorithms
|
|
6
|
+
E2EEAlgorithms,
|
|
7
|
+
VER
|
|
7
8
|
} from "../common/types.js";
|
|
9
|
+
import { checkRemoteVersion } from "../pouchdb/negotiation.js";
|
|
10
|
+
import {
|
|
11
|
+
ENCRYPTED_INTERNAL_METADATA_FEATURE,
|
|
12
|
+
usesEncryptedInternalMetadata
|
|
13
|
+
} from "../pouchdb/remoteFeatureCompatibility.js";
|
|
8
14
|
import { PouchDB } from "../pouchdb/pouchdb-http.js";
|
|
9
15
|
import { LiveSyncLocalDB } from "../pouchdb/LiveSyncLocalDB.js";
|
|
10
16
|
import { isErrorOfMissingDoc } from "../pouchdb/utils_couchdb.js";
|
|
@@ -51,6 +57,15 @@ class DirectFileManipulator {
|
|
|
51
57
|
if (!await this.liveSyncLocalDB.initializeDatabase()) {
|
|
52
58
|
throw new Error("Direct database initialisation was rejected.");
|
|
53
59
|
}
|
|
60
|
+
const requiredFeatures = usesEncryptedInternalMetadata(this.settings) ? [ENCRYPTED_INTERNAL_METADATA_FEATURE] : [];
|
|
61
|
+
if (!await checkRemoteVersion(
|
|
62
|
+
this.liveSyncLocalDB.localDatabase,
|
|
63
|
+
async () => false,
|
|
64
|
+
VER,
|
|
65
|
+
requiredFeatures
|
|
66
|
+
)) {
|
|
67
|
+
throw new Error("Direct database version or features are not compatible.");
|
|
68
|
+
}
|
|
54
69
|
this.liveSyncLocalDB.refreshSettings();
|
|
55
70
|
this.ready.resolve();
|
|
56
71
|
} catch (error) {
|
|
@@ -162,7 +177,8 @@ class DirectFileManipulator {
|
|
|
162
177
|
this.options.useDynamicIterationCount ?? false,
|
|
163
178
|
false,
|
|
164
179
|
async () => await this.getReplicationPBKDF2Salt(this.getSettings()),
|
|
165
|
-
this.options.E2EEAlgorithm ?? E2EEAlgorithms.V2
|
|
180
|
+
this.options.E2EEAlgorithm ?? E2EEAlgorithms.V2,
|
|
181
|
+
usesEncryptedInternalMetadata(this.settings)
|
|
166
182
|
);
|
|
167
183
|
}
|
|
168
184
|
return Promise.resolve(true);
|
|
@@ -209,6 +225,7 @@ class DirectFileManipulator {
|
|
|
209
225
|
enableCompression: this.options.enableCompression ?? DEFAULT_SETTINGS.enableCompression,
|
|
210
226
|
handleFilenameCaseSensitive: this.options.handleFilenameCaseSensitive ?? DEFAULT_SETTINGS.handleFilenameCaseSensitive,
|
|
211
227
|
usePathObfuscation: !!this.options.obfuscatePassphrase,
|
|
228
|
+
encryptInternalMetadata: this.options.encryptInternalMetadata ?? false,
|
|
212
229
|
E2EEAlgorithm: this.options.E2EEAlgorithm ?? E2EEAlgorithms.V2
|
|
213
230
|
}
|
|
214
231
|
};
|
|
@@ -37,5 +37,183 @@ type ErasureProperties = keyof ObsidianLiveSyncSettings;
|
|
|
37
37
|
* @returns Generated setup URI
|
|
38
38
|
*/
|
|
39
39
|
export declare function encodeSettingsToSetupURI(settingString: ObsidianLiveSyncSettings, passphrase: string, removeProperties?: ErasureProperties[], skipDefaultValue?: boolean): Promise<string>;
|
|
40
|
-
export declare function decodeSettingsFromSetupURI(uri: string, passphrase: string): Promise<false | Omit<
|
|
40
|
+
export declare function decodeSettingsFromSetupURI(uri: string, passphrase: string): Promise<false | Omit<{
|
|
41
|
+
encryptInternalMetadata: boolean;
|
|
42
|
+
liveSync: boolean;
|
|
43
|
+
syncOnSave: boolean;
|
|
44
|
+
syncOnStart: boolean;
|
|
45
|
+
syncOnFileOpen: boolean;
|
|
46
|
+
syncOnEditorSave: boolean;
|
|
47
|
+
keepReplicationActiveInBackground: boolean;
|
|
48
|
+
allowSleepDuringSynchronisation: boolean;
|
|
49
|
+
allowSleepDuringSynchronisationOnDesktop: boolean;
|
|
50
|
+
syncMinimumInterval: number;
|
|
51
|
+
showVerboseLog: boolean;
|
|
52
|
+
lessInformationInLog: boolean;
|
|
53
|
+
showLongerLogInsideEditor: boolean;
|
|
54
|
+
showStatusOnEditor: boolean;
|
|
55
|
+
showStatusOnStatusbar: boolean;
|
|
56
|
+
showOnlyIconsOnEditor: boolean;
|
|
57
|
+
hideFileWarningNotice: boolean;
|
|
58
|
+
networkWarningStyle: "" | "icon" | "hidden";
|
|
59
|
+
displayLanguage: import("../common/models/setting.type.js").I18N_LANGS;
|
|
60
|
+
trashInsteadDelete: boolean;
|
|
61
|
+
doNotDeleteFolder: boolean;
|
|
62
|
+
batchSave: boolean;
|
|
63
|
+
batchSaveMinimumDelay: number;
|
|
64
|
+
batchSaveMaximumDelay: number;
|
|
65
|
+
syncMaxSizeInMB: number;
|
|
66
|
+
useIgnoreFiles: boolean;
|
|
67
|
+
ignoreFiles: string;
|
|
68
|
+
processSizeMismatchedFiles: boolean;
|
|
69
|
+
syncOnlyRegEx: import("../common/types.js").CustomRegExpSourceList<"|[]|">;
|
|
70
|
+
syncIgnoreRegEx: import("../common/types.js").CustomRegExpSourceList<"|[]|">;
|
|
71
|
+
syncAfterMerge: boolean;
|
|
72
|
+
resolveConflictsByNewerFile: boolean;
|
|
73
|
+
writeDocumentsIfConflicted: boolean;
|
|
74
|
+
disableMarkdownAutoMerge: boolean;
|
|
75
|
+
configPassphraseStore: import("../common/types.js").ConfigPassphraseStore;
|
|
76
|
+
encryptedPassphrase: string;
|
|
77
|
+
encryptedCouchDBConnection: string;
|
|
78
|
+
periodicReplication: boolean;
|
|
79
|
+
periodicReplicationInterval: number;
|
|
80
|
+
syncInternalFiles: boolean;
|
|
81
|
+
syncInternalFilesBeforeReplication: boolean;
|
|
82
|
+
syncInternalFilesInterval: number;
|
|
83
|
+
syncInternalFilesIgnorePatterns: import("../common/types.js").CustomRegExpSourceList<",">;
|
|
84
|
+
syncInternalFilesTargetPatterns: import("../common/types.js").CustomRegExpSourceList<",">;
|
|
85
|
+
watchInternalFileChanges: boolean;
|
|
86
|
+
suppressNotifyHiddenFilesChange: boolean;
|
|
87
|
+
syncInternalFileOverwritePatterns: import("../common/types.js").CustomRegExpSourceList<",">;
|
|
88
|
+
usePluginSync: boolean;
|
|
89
|
+
usePluginSettings: boolean;
|
|
90
|
+
showOwnPlugins: boolean;
|
|
91
|
+
autoSweepPlugins: boolean;
|
|
92
|
+
autoSweepPluginsPeriodic: boolean;
|
|
93
|
+
notifyPluginOrSettingUpdated: boolean;
|
|
94
|
+
deviceAndVaultName: string;
|
|
95
|
+
usePluginSyncV2: boolean;
|
|
96
|
+
usePluginEtc: boolean;
|
|
97
|
+
pluginSyncExtendedSetting: Record<import("../common/types.js").PluginSyncSettingEntry["key"], import("../common/types.js").PluginSyncSettingEntry>;
|
|
98
|
+
useAdvancedMode: boolean;
|
|
99
|
+
usePowerUserMode: boolean;
|
|
100
|
+
useEdgeCaseMode: boolean;
|
|
101
|
+
notifyThresholdOfRemoteStorageSize: number;
|
|
102
|
+
disableWorkerForGeneratingChunks: boolean;
|
|
103
|
+
processSmallFilesInUIThread: boolean;
|
|
104
|
+
savingDelay: number;
|
|
105
|
+
gcDelay: number;
|
|
106
|
+
skipOlderFilesOnSync: boolean;
|
|
107
|
+
useIndexedDBAdapter: boolean;
|
|
108
|
+
enableDebugTools: boolean;
|
|
109
|
+
writeLogToTheFile: boolean;
|
|
110
|
+
settingSyncFile: string;
|
|
111
|
+
writeCredentialsForSettingSync: boolean;
|
|
112
|
+
notifyAllSettingSyncFile: boolean;
|
|
113
|
+
suspendFileWatching: boolean;
|
|
114
|
+
suspendParseReplicationResult: boolean;
|
|
115
|
+
doNotSuspendOnFetching: boolean;
|
|
116
|
+
maxMTimeForReflectEvents: number;
|
|
117
|
+
versionUpFlash: string;
|
|
118
|
+
settingVersion: number;
|
|
119
|
+
isConfigured?: boolean;
|
|
120
|
+
lastReadUpdates: number;
|
|
121
|
+
doctorProcessedVersion: string;
|
|
122
|
+
remoteConfigurations: Record<string, import("../remoteConfigurations.js").RemoteConfiguration>;
|
|
123
|
+
activeConfigurationId: string;
|
|
124
|
+
P2P_ActiveRemoteConfigurationId: string;
|
|
125
|
+
couchDB_URI: string;
|
|
126
|
+
couchDB_USER: string;
|
|
127
|
+
couchDB_PASSWORD: string;
|
|
128
|
+
couchDB_DBNAME: string;
|
|
129
|
+
couchDB_CustomHeaders: string;
|
|
130
|
+
useJWT: boolean;
|
|
131
|
+
jwtAlgorithm: import("../common/models/auth.type.js").JWTAlgorithm;
|
|
132
|
+
jwtKey: string;
|
|
133
|
+
jwtKid: string;
|
|
134
|
+
jwtSub: string;
|
|
135
|
+
jwtExpDuration: number;
|
|
136
|
+
useRequestAPI: boolean;
|
|
137
|
+
accessKey: string;
|
|
138
|
+
secretKey: string;
|
|
139
|
+
bucket: string;
|
|
140
|
+
region: string;
|
|
141
|
+
endpoint: string;
|
|
142
|
+
useCustomRequestHandler: boolean;
|
|
143
|
+
bucketCustomHeaders: string;
|
|
144
|
+
bucketPrefix: string;
|
|
145
|
+
forcePathStyle: boolean;
|
|
146
|
+
remoteType: import("../common/types.js").RemoteType;
|
|
147
|
+
encrypt: boolean;
|
|
148
|
+
passphrase: string;
|
|
149
|
+
usePathObfuscation: boolean;
|
|
150
|
+
E2EEAlgorithm: import("../common/types.js").E2EEAlgorithm;
|
|
151
|
+
hashAlg: import("../common/types.js").HashAlgorithm;
|
|
152
|
+
minimumChunkSize: number;
|
|
153
|
+
customChunkSize: number;
|
|
154
|
+
longLineThreshold: number;
|
|
155
|
+
useSegmenter: boolean;
|
|
156
|
+
enableChunkSplitterV2: boolean;
|
|
157
|
+
doNotUseFixedRevisionForChunks: boolean;
|
|
158
|
+
chunkSplitterVersion: import("../common/types.js").ChunkSplitterVersion;
|
|
159
|
+
useEden: boolean;
|
|
160
|
+
maxChunksInEden: number;
|
|
161
|
+
maxTotalLengthInEden: number;
|
|
162
|
+
maxAgeInEden: number;
|
|
163
|
+
tweakModified: number | undefined;
|
|
164
|
+
checkIntegrityOnSave: boolean;
|
|
165
|
+
useHistory: boolean;
|
|
166
|
+
disableRequestURI: boolean;
|
|
167
|
+
sendChunksBulk: boolean;
|
|
168
|
+
sendChunksBulkMaxSize: number;
|
|
169
|
+
useDynamicIterationCount: boolean;
|
|
170
|
+
doNotPaceReplication: boolean;
|
|
171
|
+
readChunksOnline: boolean;
|
|
172
|
+
useOnlyLocalChunk: boolean;
|
|
173
|
+
concurrencyOfReadChunksOnline: number;
|
|
174
|
+
minimumIntervalOfReadChunksOnline: number;
|
|
175
|
+
enableCompression: boolean;
|
|
176
|
+
batch_size: number;
|
|
177
|
+
batches_limit: number;
|
|
178
|
+
ignoreVersionCheck: boolean;
|
|
179
|
+
disableCheckingConfigMismatch: boolean;
|
|
180
|
+
autoAcceptCompatibleTweak: boolean | undefined;
|
|
181
|
+
hashCacheMaxCount: number;
|
|
182
|
+
hashCacheMaxAmount: number;
|
|
183
|
+
permitEmptyPassphrase: boolean;
|
|
184
|
+
handleFilenameCaseSensitive: boolean;
|
|
185
|
+
checkConflictOnlyOnOpen: boolean;
|
|
186
|
+
showMergeDialogOnlyOnActive: boolean;
|
|
187
|
+
additionalSuffixOfDatabaseName: string | undefined;
|
|
188
|
+
useTimeouts: boolean;
|
|
189
|
+
deleteMetadataOfDeletedFiles: boolean;
|
|
190
|
+
automaticallyDeleteMetadataOfDeletedFiles: number;
|
|
191
|
+
P2P_AutoAccepting: import("../common/types.js").AutoAccepting;
|
|
192
|
+
P2P_AutoSyncPeers: string;
|
|
193
|
+
P2P_AutoWatchPeers: string;
|
|
194
|
+
P2P_SyncOnReplication: string;
|
|
195
|
+
P2P_RebuildFrom: string;
|
|
196
|
+
P2P_AutoAcceptingPeers: string;
|
|
197
|
+
P2P_AutoDenyingPeers: string;
|
|
198
|
+
P2P_IsHeadless?: boolean;
|
|
199
|
+
P2P_iceServers?: readonly RTCIceServer[];
|
|
200
|
+
P2P_iceServersExpiresAt?: number;
|
|
201
|
+
P2P_Enabled: boolean;
|
|
202
|
+
P2P_relays: string;
|
|
203
|
+
P2P_roomID: string;
|
|
204
|
+
P2P_passphrase: string;
|
|
205
|
+
P2P_AppID: string;
|
|
206
|
+
P2P_AutoStart: boolean;
|
|
207
|
+
P2P_AutoBroadcast: boolean;
|
|
208
|
+
P2P_DevicePeerName?: string;
|
|
209
|
+
P2P_turnServers: string;
|
|
210
|
+
P2P_turnUsername: string;
|
|
211
|
+
P2P_turnCredential: string;
|
|
212
|
+
P2P_managedType?: string;
|
|
213
|
+
P2P_managedId?: string;
|
|
214
|
+
P2P_managedToken?: string;
|
|
215
|
+
P2P_maxWirePayloadBytes?: number;
|
|
216
|
+
P2P_connectionPath?: import("../common/types.js").P2PConnectionPath;
|
|
217
|
+
P2P_useDiagRTC?: boolean;
|
|
218
|
+
}, "P2P_iceServers" | "P2P_iceServersExpiresAt">>;
|
|
41
219
|
export {};
|
|
@@ -128,7 +128,11 @@ async function decodeSettingsFromSetupURI(uri, passphrase) {
|
|
|
128
128
|
const encryptedSetting = uri.substring(configURIBase.length);
|
|
129
129
|
const decrypted = await decryptString(decodeURIComponent(encryptedSetting), passphrase);
|
|
130
130
|
try {
|
|
131
|
-
|
|
131
|
+
const imported = JSON.parse(decrypted);
|
|
132
|
+
return omitP2PRuntimeSettings({
|
|
133
|
+
...imported,
|
|
134
|
+
encryptInternalMetadata: typeof imported.encryptInternalMetadata === "boolean" ? imported.encryptInternalMetadata : false
|
|
135
|
+
});
|
|
132
136
|
} catch {
|
|
133
137
|
Logger(`Failed to parse settings from decrypted data`, LOG_LEVEL_NOTICE);
|
|
134
138
|
return false;
|
|
@@ -52,6 +52,7 @@ export declare const DoctorRegulationV0_24_30: DoctorRegulation;
|
|
|
52
52
|
export declare const DoctorRegulationV0_25_0: DoctorRegulation;
|
|
53
53
|
export declare const DoctorRegulationV0_25_27: DoctorRegulation;
|
|
54
54
|
export declare const DoctorRegulationV1_0_0: DoctorRegulation;
|
|
55
|
+
export declare const DoctorRegulationV1_0_33: DoctorRegulation;
|
|
55
56
|
export declare const DoctorRegulation: DoctorRegulation;
|
|
56
57
|
export declare function checkUnsuitableValues(setting: Partial<ObsidianLiveSyncSettings>, regulation?: DoctorRegulation): DoctorRegulation;
|
|
57
58
|
export declare const RebuildOptions: {
|
|
@@ -127,7 +127,19 @@ const DoctorRegulationV1_0_0 = {
|
|
|
127
127
|
enableCompression: void 0
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
|
-
const
|
|
130
|
+
const DoctorRegulationV1_0_33 = {
|
|
131
|
+
version: "1.0.33",
|
|
132
|
+
rules: {
|
|
133
|
+
...DoctorRegulationV1_0_0.rules,
|
|
134
|
+
encryptInternalMetadata: {
|
|
135
|
+
value: true,
|
|
136
|
+
level: 2 /* Recommended */,
|
|
137
|
+
detectionFunc: (settings) => settings.remoteType === REMOTE_COUCHDB && settings.encrypt === true && settings.usePathObfuscation === true && settings.E2EEAlgorithm === E2EEAlgorithms.V2,
|
|
138
|
+
reason: "E2EE V2 and Property Encryption can also protect file properties used by Hidden File Sync and Customisation Sync. This affects future writes; manually rebuild the remote database to protect existing properties, and update every synchronising client before enabling it, including devices currently running LiveSync."
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const DoctorRegulation = DoctorRegulationV1_0_33;
|
|
131
143
|
function checkUnsuitableValues(setting, regulation = DoctorRegulation) {
|
|
132
144
|
const result = {
|
|
133
145
|
version: regulation.version,
|
|
@@ -234,7 +246,8 @@ async function performDoctorConsultation(env, settings, {
|
|
|
234
246
|
[0 /* Must */]: translate("Doctor.Level.Must")
|
|
235
247
|
};
|
|
236
248
|
const level = value.level ? levelMap[value.level] : "Unknown";
|
|
237
|
-
const
|
|
249
|
+
const fix = key === "encryptInternalMetadata" ? "Enable without rebuilding \u2014 update every other device first" : OPT_FIX;
|
|
250
|
+
const options = [fix];
|
|
238
251
|
let askRebuild = false;
|
|
239
252
|
let askRebuildLocal = false;
|
|
240
253
|
if (value.requireRebuild) {
|
|
@@ -280,12 +293,12 @@ async function performDoctorConsultation(env, settings, {
|
|
|
280
293
|
current: `${++idx}`,
|
|
281
294
|
total: `${issueItems.length}`
|
|
282
295
|
}),
|
|
283
|
-
defaultAction:
|
|
296
|
+
defaultAction: fix
|
|
284
297
|
}
|
|
285
298
|
);
|
|
286
|
-
if (ret ==
|
|
299
|
+
if (ret == fix || ret == OPTION_FIX_WITHOUT_REBUILD) {
|
|
287
300
|
applySettings[key] = value.value;
|
|
288
|
-
if (ret ==
|
|
301
|
+
if (ret == fix) {
|
|
289
302
|
shouldRebuild = shouldRebuild || askRebuild || false;
|
|
290
303
|
shouldRebuildLocal = shouldRebuildLocal || askRebuildLocal || false;
|
|
291
304
|
}
|
|
@@ -300,9 +313,12 @@ async function performDoctorConsultation(env, settings, {
|
|
|
300
313
|
...applySettings
|
|
301
314
|
};
|
|
302
315
|
}
|
|
316
|
+
const newlyApplicableMetadataRule = checkUnsuitableValues(settings).rules.encryptInternalMetadata !== void 0;
|
|
303
317
|
if (skipped == 0) {
|
|
304
|
-
|
|
305
|
-
|
|
318
|
+
if (!newlyApplicableMetadataRule) {
|
|
319
|
+
settings.doctorProcessedVersion = r.version;
|
|
320
|
+
isModified = true;
|
|
321
|
+
}
|
|
306
322
|
} else {
|
|
307
323
|
if (await env.confirm.askYesNoDialog(translate("Doctor.Message.SomeSkipped"), {
|
|
308
324
|
title: translate("Doctor.Dialogue.TitleAlmostDone"),
|
|
@@ -322,6 +338,7 @@ export {
|
|
|
322
338
|
DoctorRegulationV0_25_0,
|
|
323
339
|
DoctorRegulationV0_25_27,
|
|
324
340
|
DoctorRegulationV1_0_0,
|
|
341
|
+
DoctorRegulationV1_0_33,
|
|
325
342
|
RebuildOptions,
|
|
326
343
|
RuleLevel,
|
|
327
344
|
checkUnsuitableValues,
|
|
@@ -126,6 +126,7 @@ export type EntryChunkPack = DatabaseEntry & {
|
|
|
126
126
|
export interface EntryVersionInfo extends DatabaseEntry {
|
|
127
127
|
type: EntryTypes["VERSION_INFO"];
|
|
128
128
|
version: number;
|
|
129
|
+
used_features?: string[];
|
|
129
130
|
}
|
|
130
131
|
export interface EntryHasPath {
|
|
131
132
|
path: FilePathWithPrefix | FilePath;
|
|
@@ -57,6 +57,7 @@ const SETTINGS_SCHEMA_DEFAULTS = {
|
|
|
57
57
|
encrypt: false,
|
|
58
58
|
passphrase: "",
|
|
59
59
|
usePathObfuscation: false,
|
|
60
|
+
encryptInternalMetadata: false,
|
|
60
61
|
doNotDeleteFolder: false,
|
|
61
62
|
resolveConflictsByNewerFile: false,
|
|
62
63
|
batchSave: false,
|
|
@@ -195,6 +196,7 @@ const SETTINGS_SCHEMA_DEFAULTS = {
|
|
|
195
196
|
const NEW_VAULT_SETTINGS = {
|
|
196
197
|
...SETTINGS_SCHEMA_DEFAULTS,
|
|
197
198
|
...PREFERRED_BASE,
|
|
199
|
+
encryptInternalMetadata: true,
|
|
198
200
|
remoteConfigurations: {},
|
|
199
201
|
pluginSyncExtendedSetting: {}
|
|
200
202
|
};
|
|
@@ -176,7 +176,8 @@ const KeyIndexOfSettings = {
|
|
|
176
176
|
P2P_useDiagRTC: -1,
|
|
177
177
|
// Do not encode into the QR Code.
|
|
178
178
|
P2P_maxWirePayloadBytes: 163,
|
|
179
|
-
P2P_connectionPath: 164
|
|
179
|
+
P2P_connectionPath: 164,
|
|
180
|
+
encryptInternalMetadata: 165
|
|
180
181
|
};
|
|
181
182
|
export {
|
|
182
183
|
KeyIndexOfSettings
|
|
@@ -106,13 +106,15 @@ function prepareSettingsForLoad(storedSettings) {
|
|
|
106
106
|
}
|
|
107
107
|
const normalisedFilenameCase = typeof migrated.handleFilenameCaseSensitive !== "boolean";
|
|
108
108
|
if (normalisedFilenameCase) migrated = { ...migrated, handleFilenameCaseSensitive: false };
|
|
109
|
+
const normalisedInternalMetadata = typeof migrated.encryptInternalMetadata !== "boolean";
|
|
110
|
+
if (normalisedInternalMetadata) migrated = { ...migrated, encryptInternalMetadata: false };
|
|
109
111
|
return {
|
|
110
112
|
settings: cloneSettings(migrated, SETTINGS_SCHEMA_DEFAULTS),
|
|
111
113
|
sourceVersion,
|
|
112
114
|
targetVersion,
|
|
113
115
|
isNewVault: false,
|
|
114
116
|
isFromFutureSchema: false,
|
|
115
|
-
changed: sourceVersion !== targetVersion || normalisedConfiguredState || normalisedFilenameCase,
|
|
117
|
+
changed: sourceVersion !== targetVersion || normalisedConfiguredState || normalisedFilenameCase || normalisedInternalMetadata,
|
|
116
118
|
requiresSyncReview: reviewReasons.length > 0,
|
|
117
119
|
reviewReasons
|
|
118
120
|
};
|
|
@@ -590,6 +590,8 @@ export interface EncryptionSettings {
|
|
|
590
590
|
* If not, the path will be stored as it is, as the document ID.
|
|
591
591
|
*/
|
|
592
592
|
usePathObfuscation: boolean;
|
|
593
|
+
/** Encrypt the Metadata of obfuscated internal files and Customisation Sync entries. */
|
|
594
|
+
encryptInternalMetadata: boolean;
|
|
593
595
|
/**
|
|
594
596
|
* The algorithm used for hashing the passphrase.
|
|
595
597
|
* This is used for E2EE.
|
|
@@ -16,6 +16,10 @@ const configurationNames = {
|
|
|
16
16
|
name: "Property Encryption",
|
|
17
17
|
desc: "If enabled, the file properties will be encrypted in the remote database. This is useful for protecting sensitive information in file paths, sizes, and IDs of its chunks. If you are using V1 E2EE, this only obfuscates the file path."
|
|
18
18
|
},
|
|
19
|
+
encryptInternalMetadata: {
|
|
20
|
+
name: "Encrypt internal file Properties",
|
|
21
|
+
desc: "Encrypt file properties (paths, times, sizes, and Chunk references) used by Hidden File Sync and Customisation Sync. This affects future writes; manually rebuild the remote database to protect existing properties. Requires E2EE V2, Property Encryption, and compatible clients on every device."
|
|
22
|
+
},
|
|
19
23
|
enableCompression: {
|
|
20
24
|
name: "Data Compression",
|
|
21
25
|
level: LEVEL_ADVANCED
|
|
@@ -20,6 +20,11 @@ export declare const TweakCompatibilityRules: readonly [{
|
|
|
20
20
|
readonly key: "usePathObfuscation";
|
|
21
21
|
readonly templateValue: false;
|
|
22
22
|
readonly reconstruction: "required";
|
|
23
|
+
}, {
|
|
24
|
+
readonly key: "encryptInternalMetadata";
|
|
25
|
+
readonly templateValue: false;
|
|
26
|
+
readonly reconstruction: "none";
|
|
27
|
+
readonly knownDefault: false;
|
|
23
28
|
}, {
|
|
24
29
|
readonly key: "enableCompression";
|
|
25
30
|
readonly templateValue: false;
|
|
@@ -108,6 +113,7 @@ export declare const TweakValuesTemplate: {
|
|
|
108
113
|
usePluginSyncV2: boolean;
|
|
109
114
|
encrypt: boolean;
|
|
110
115
|
usePathObfuscation: boolean;
|
|
116
|
+
encryptInternalMetadata: boolean;
|
|
111
117
|
E2EEAlgorithm: import("./setting.type.js").E2EEAlgorithm;
|
|
112
118
|
hashAlg: import("./setting.type.js").HashAlgorithm;
|
|
113
119
|
minimumChunkSize: number;
|
|
@@ -5,6 +5,7 @@ const TweakCompatibilityRules = Object.freeze([
|
|
|
5
5
|
{ key: "longLineThreshold", templateValue: 250, reconstruction: "none" },
|
|
6
6
|
{ key: "encrypt", templateValue: false, reconstruction: "required" },
|
|
7
7
|
{ key: "usePathObfuscation", templateValue: false, reconstruction: "required" },
|
|
8
|
+
{ key: "encryptInternalMetadata", templateValue: false, reconstruction: "none", knownDefault: false },
|
|
8
9
|
{ key: "enableCompression", templateValue: false, reconstruction: "none" },
|
|
9
10
|
{ key: "useEden", templateValue: false, reconstruction: "none" },
|
|
10
11
|
{ key: "customChunkSize", templateValue: 0, reconstruction: "recommended", compatibleLossyOrder: 1 },
|
|
@@ -136,6 +136,7 @@ export declare const AllSettingDefault: {
|
|
|
136
136
|
encrypt: boolean;
|
|
137
137
|
passphrase: string;
|
|
138
138
|
usePathObfuscation: boolean;
|
|
139
|
+
encryptInternalMetadata: boolean;
|
|
139
140
|
E2EEAlgorithm: import("./types.js").E2EEAlgorithm;
|
|
140
141
|
hashAlg: import("./types.js").HashAlgorithm;
|
|
141
142
|
minimumChunkSize: number;
|
package/dist/common/utils.js
CHANGED
|
@@ -448,7 +448,8 @@ function pickEncryptionSettings(setting) {
|
|
|
448
448
|
E2EEAlgorithm: setting.E2EEAlgorithm,
|
|
449
449
|
encrypt: setting.encrypt,
|
|
450
450
|
passphrase: setting.passphrase,
|
|
451
|
-
usePathObfuscation: setting.usePathObfuscation
|
|
451
|
+
usePathObfuscation: setting.usePathObfuscation,
|
|
452
|
+
encryptInternalMetadata: setting.encryptInternalMetadata
|
|
452
453
|
};
|
|
453
454
|
}
|
|
454
455
|
function pickP2PSyncSettings(setting) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type EntryDoc, type EntryMilestoneInfo, type RemoteDBSettings, type ChunkVersionRange, type TweakValues, type DeviceInfo, type TweakAssessment } from "../common/types.js";
|
|
2
|
+
export declare function getEffectiveTweakValues(setting: RemoteDBSettings): TweakValues;
|
|
2
3
|
export type ENSURE_DB_RESULT = "OK" | "INCOMPATIBLE" | "LOCKED" | "NODE_LOCKED" | "NODE_CLEANED" | ["MISMATCHED", TweakValues];
|
|
3
4
|
/**
|
|
4
5
|
* Ensures that the remote database is compatible with the current device.
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MILESTONE_DOCID as MILESTONE_DOC_ID,
|
|
3
3
|
TweakValuesTemplate,
|
|
4
|
-
DEVICE_ID_PREFERRED
|
|
4
|
+
DEVICE_ID_PREFERRED,
|
|
5
|
+
REMOTE_COUCHDB
|
|
5
6
|
} from "../common/types.js";
|
|
6
7
|
import { extractObject, isObjectDifferent, resolveWithIgnoreKnownError } from "../common/utils.js";
|
|
7
8
|
import { assessTweakCompatibility } from "../common/models/tweak.compatibility.js";
|
|
9
|
+
import { usesEncryptedInternalMetadata } from "./remoteFeatureCompatibility.js";
|
|
10
|
+
function getEffectiveTweakValues(setting) {
|
|
11
|
+
return {
|
|
12
|
+
...extractObject(TweakValuesTemplate, setting),
|
|
13
|
+
encryptInternalMetadata: setting.remoteType === REMOTE_COUCHDB && usesEncryptedInternalMetadata(setting)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
8
16
|
async function ensureRemoteIsCompatible(infoSrc, setting, deviceNodeID, currentVersionRange, nodeDeviceInfo, updateCallback, recordTweakAssessment) {
|
|
9
17
|
const now = Date.now();
|
|
10
18
|
const baseMilestone = {
|
|
@@ -25,7 +33,7 @@ async function ensureRemoteIsCompatible(infoSrc, setting, deviceNodeID, currentV
|
|
|
25
33
|
};
|
|
26
34
|
let remoteMilestone = infoSrc;
|
|
27
35
|
if (!remoteMilestone) remoteMilestone = baseMilestone;
|
|
28
|
-
const currentTweakValues =
|
|
36
|
+
const currentTweakValues = getEffectiveTweakValues(setting);
|
|
29
37
|
remoteMilestone.node_chunk_info = { ...baseMilestone.node_chunk_info, ...remoteMilestone.node_chunk_info };
|
|
30
38
|
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);
|
|
31
39
|
if (!remoteMilestone.node_info) {
|
|
@@ -119,5 +127,6 @@ async function ensureDatabaseIsCompatible(db, setting, deviceNodeID, currentVers
|
|
|
119
127
|
}
|
|
120
128
|
export {
|
|
121
129
|
ensureDatabaseIsCompatible,
|
|
122
|
-
ensureRemoteIsCompatible
|
|
130
|
+
ensureRemoteIsCompatible,
|
|
131
|
+
getEffectiveTweakValues
|
|
123
132
|
};
|
|
@@ -35,6 +35,8 @@ export declare class StreamingFetchFailure extends Error {
|
|
|
35
35
|
readonly status?: number;
|
|
36
36
|
}
|
|
37
37
|
export declare function isRetryableStreamingFetchFailure(error: unknown): error is StreamingFetchFailure;
|
|
38
|
+
/** Check remote feature requirements before Fast Fetch opens or resets the local database. */
|
|
39
|
+
export declare function checkRemoteFeaturesForInitialSync(remoteDbUrl: string, authHeader: string, customHeaders?: Record<string, string>): Promise<void>;
|
|
38
40
|
export type FetchChangesForInitialSyncProgress = {
|
|
39
41
|
totalFetched: number;
|
|
40
42
|
totalValidFetched: number;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { _fetch } from "../common/coreEnvFunctions.js";
|
|
2
2
|
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
|
|
3
|
+
import { VERSIONING_DOCID } from "../common/types.js";
|
|
4
|
+
import { assessRemoteFeatureDocument, describeRemoteFeatureRejection } from "./remoteFeatureCompatibility.js";
|
|
3
5
|
const FAST_FETCH_CHANGES_PAGE_LIMIT = 1e4;
|
|
4
6
|
const FAST_FETCH_CHANGES_PAGE_TIMEOUT_MS = 1e3;
|
|
5
7
|
class StreamingFetchFailure extends Error {
|
|
@@ -73,6 +75,31 @@ async function readResponseText(response, operation) {
|
|
|
73
75
|
throw transportFailure(operation, error);
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
async function checkRemoteFeaturesForInitialSync(remoteDbUrl, authHeader, customHeaders) {
|
|
79
|
+
const headers = new Headers(customHeaders);
|
|
80
|
+
headers.set("Accept", "application/json");
|
|
81
|
+
headers.set("Authorization", authHeader);
|
|
82
|
+
const url = new URL(`${remoteDbUrl}/_all_docs`);
|
|
83
|
+
url.searchParams.set("keys", JSON.stringify([VERSIONING_DOCID]));
|
|
84
|
+
url.searchParams.set("include_docs", "true");
|
|
85
|
+
const operation = "check remote feature requirements";
|
|
86
|
+
const response = await fetchResponse(url.toString(), { headers }, operation);
|
|
87
|
+
const text = await readResponseText(response, operation);
|
|
88
|
+
let result;
|
|
89
|
+
try {
|
|
90
|
+
result = JSON.parse(text);
|
|
91
|
+
} catch (cause) {
|
|
92
|
+
throw new StreamingFetchFailure("protocol", "The remote feature response is invalid.", false, { cause });
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(result?.rows) || result.rows.length !== 1 || result.rows[0]?.key !== VERSIONING_DOCID) {
|
|
95
|
+
throw new StreamingFetchFailure("protocol", "The remote feature response is invalid.", false);
|
|
96
|
+
}
|
|
97
|
+
const row = result.rows[0];
|
|
98
|
+
if (result.total_rows === 0 && row.error === "not_found") return;
|
|
99
|
+
const assessment = assessRemoteFeatureDocument(row.doc);
|
|
100
|
+
if (assessment.status === "supported" || assessment.status === "older-generation") return;
|
|
101
|
+
throw new StreamingFetchFailure("protocol", describeRemoteFeatureRejection(assessment), false);
|
|
102
|
+
}
|
|
76
103
|
async function saveCheckpoint(onCheckpoint, sequence) {
|
|
77
104
|
try {
|
|
78
105
|
await onCheckpoint?.(sequence);
|
|
@@ -478,6 +505,7 @@ async function fetchChangesForInitialSync(downloadToDB, remoteDbUrl, authHeader,
|
|
|
478
505
|
}
|
|
479
506
|
export {
|
|
480
507
|
StreamingFetchFailure,
|
|
508
|
+
checkRemoteFeaturesForInitialSync,
|
|
481
509
|
fetchChangesForInitialSync,
|
|
482
510
|
isRetryableStreamingFetchFailure
|
|
483
511
|
};
|
|
@@ -6,11 +6,11 @@ export declare const encryptHKDF: typeof encryptHKDFWorker;
|
|
|
6
6
|
export declare const decryptHKDF: typeof decryptHKDFWorker;
|
|
7
7
|
export declare let preprocessOutgoing: (doc: AnyEntry | EntryLeaf) => Promise<AnyEntry | EntryLeaf>;
|
|
8
8
|
export declare let preprocessIncoming: (doc: EntryDoc) => Promise<EntryDoc>;
|
|
9
|
-
export declare function getConfiguredFunctionsForEncryption(passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, algorithm: E2EEAlgorithm): {
|
|
9
|
+
export declare function getConfiguredFunctionsForEncryption(passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, algorithm: E2EEAlgorithm, encryptInternalMetadata?: boolean): {
|
|
10
10
|
incoming: (doc: AnyEntry | EntryLeaf) => Promise<AnyEntry | EntryLeaf>;
|
|
11
11
|
outgoing: (doc: EntryDoc) => Promise<AnyEntry | EntryLeaf>;
|
|
12
12
|
};
|
|
13
|
-
export declare const enableEncryption: (db: PouchDB.Database<EntryDoc>, passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, algorithm: E2EEAlgorithm) => void;
|
|
13
|
+
export declare const enableEncryption: (db: PouchDB.Database<EntryDoc>, passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, algorithm: E2EEAlgorithm, encryptInternalMetadata?: boolean) => void;
|
|
14
14
|
export declare function disableEncryption(): void;
|
|
15
15
|
export declare const EDEN_ENCRYPTED_KEY: DocumentID;
|
|
16
16
|
export declare const EDEN_ENCRYPTED_KEY_HKDF: DocumentID;
|