@vrtmrz/livesync-commonlib 0.1.29 → 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.
Files changed (40) hide show
  1. package/README.md +5 -1
  2. package/dist/API/DirectFileManipulatorV2.d.ts +1 -0
  3. package/dist/API/DirectFileManipulatorV2.js +19 -2
  4. package/dist/API/processSetting.d.ts +179 -1
  5. package/dist/API/processSetting.js +5 -1
  6. package/dist/common/configForDoc.d.ts +1 -0
  7. package/dist/common/configForDoc.js +24 -7
  8. package/dist/common/models/db.type.d.ts +1 -0
  9. package/dist/common/models/setting.const.defaults.js +2 -0
  10. package/dist/common/models/setting.const.qr.js +2 -1
  11. package/dist/common/models/setting.lifecycle.js +3 -1
  12. package/dist/common/models/setting.type.d.ts +2 -0
  13. package/dist/common/models/shared.definition.configNames.js +4 -0
  14. package/dist/common/models/tweak.definition.d.ts +6 -0
  15. package/dist/common/models/tweak.definition.js +1 -0
  16. package/dist/common/settingConstants.d.ts +1 -0
  17. package/dist/common/utils.js +2 -1
  18. package/dist/pouchdb/LiveSyncDBFunctions.d.ts +1 -0
  19. package/dist/pouchdb/LiveSyncDBFunctions.js +12 -3
  20. package/dist/pouchdb/StreamingFetch.d.ts +2 -0
  21. package/dist/pouchdb/StreamingFetch.js +28 -0
  22. package/dist/pouchdb/encryption.d.ts +2 -2
  23. package/dist/pouchdb/encryption.js +15 -7
  24. package/dist/pouchdb/negotiation.d.ts +3 -2
  25. package/dist/pouchdb/negotiation.js +84 -32
  26. package/dist/pouchdb/remoteFeatureCompatibility.d.ts +25 -0
  27. package/dist/pouchdb/remoteFeatureCompatibility.js +52 -0
  28. package/dist/replication/couchdb/LiveSyncReplicator.js +21 -9
  29. package/dist/replication/index.d.ts +2 -0
  30. package/dist/replication/index.js +13 -1
  31. package/dist/replication/journal/LiveSyncJournalReplicator.js +2 -5
  32. package/dist/serviceModules/Rebuilder.js +11 -6
  33. package/dist/services/base/CommonlibMessages.d.ts +2 -0
  34. package/dist/services/base/CommonlibMessages.js +2 -0
  35. package/dist/services/base/RemoteConnection.d.ts +2 -0
  36. package/dist/services/base/RemoteService.js +9 -1
  37. package/dist/worker/bgWorker.js +1 -1
  38. package/docs/proven-in-use.md +4 -0
  39. package/docs/remote-feature-compatibility.md +177 -0
  40. 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<ObsidianLiveSyncSettings, "P2P_iceServers" | "P2P_iceServersExpiresAt">>;
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
- return omitP2PRuntimeSettings(JSON.parse(decrypted));
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 DoctorRegulation = DoctorRegulationV1_0_0;
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 options = [OPT_FIX];
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: OPT_FIX
296
+ defaultAction: fix
284
297
  }
285
298
  );
286
- if (ret == OPT_FIX || ret == OPTION_FIX_WITHOUT_REBUILD) {
299
+ if (ret == fix || ret == OPTION_FIX_WITHOUT_REBUILD) {
287
300
  applySettings[key] = value.value;
288
- if (ret == OPT_FIX) {
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
- settings.doctorProcessedVersion = r.version;
305
- isModified = true;
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;
@@ -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 = extractObject(TweakValuesTemplate, setting);
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;