@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.
Files changed (54) hide show
  1. package/README.md +5 -0
  2. package/dist/API/DirectFileManipulatorV2.d.ts +2 -0
  3. package/dist/API/DirectFileManipulatorV2.js +17 -5
  4. package/dist/API/processSetting.d.ts +19 -0
  5. package/dist/API/processSetting.js +91 -6
  6. package/dist/common/idDerivation.d.ts +13 -0
  7. package/dist/common/idDerivation.js +75 -0
  8. package/dist/common/models/db.definition.d.ts +1 -0
  9. package/dist/common/models/setting.const.defaults.js +3 -0
  10. package/dist/common/models/setting.const.qr.js +4 -1
  11. package/dist/common/models/setting.type.d.ts +6 -0
  12. package/dist/common/models/tweak.compatibility.js +6 -2
  13. package/dist/common/models/tweak.definition.d.ts +8 -0
  14. package/dist/common/models/tweak.definition.js +4 -3
  15. package/dist/common/settingConstants.d.ts +3 -0
  16. package/dist/common/utils.js +3 -1
  17. package/dist/events/coreEvents.d.ts +3 -0
  18. package/dist/events/coreEvents.js +2 -0
  19. package/dist/managers/EntryManager/EntryManagerImpls.js +10 -1
  20. package/dist/managers/HashManager/HashManager.d.ts +1 -0
  21. package/dist/managers/HashManager/HashManager.js +13 -1
  22. package/dist/pouchdb/LiveSyncDBFunctions.d.ts +1 -1
  23. package/dist/pouchdb/LiveSyncDBFunctions.js +33 -38
  24. package/dist/pouchdb/negotiation.js +7 -3
  25. package/dist/pouchdb/remoteFeatureCompatibility.d.ts +10 -0
  26. package/dist/pouchdb/remoteFeatureCompatibility.js +12 -1
  27. package/dist/pouchdb/remoteIdCompatibility.d.ts +5 -0
  28. package/dist/pouchdb/remoteIdCompatibility.js +28 -0
  29. package/dist/replication/CentralCompatibility.d.ts +1 -0
  30. package/dist/replication/CentralCompatibility.js +2 -1
  31. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +6 -2
  32. package/dist/replication/couchdb/LiveSyncReplicator.js +24 -5
  33. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +1 -0
  34. package/dist/replication/journal/LiveSyncJournalReplicator.js +50 -5
  35. package/dist/replication/trystero/TrysteroReplicator.d.ts +369 -5
  36. package/dist/replication/trystero/TrysteroReplicator.js +31 -4
  37. package/dist/services/base/AppLifecycleService.d.ts +2 -0
  38. package/dist/services/base/AppLifecycleService.js +5 -0
  39. package/dist/services/base/IService.d.ts +1 -0
  40. package/dist/services/base/PathService.d.ts +3 -1
  41. package/dist/services/base/PathService.js +9 -4
  42. package/dist/services/base/SettingService.d.ts +2 -0
  43. package/dist/services/base/SettingService.js +57 -3
  44. package/dist/settings.d.ts +2 -1
  45. package/dist/settings.js +14 -0
  46. package/dist/setupUri.d.ts +6 -0
  47. package/dist/setupUri.js +14 -0
  48. package/dist/string_and_binary/path.d.ts +1 -1
  49. package/dist/string_and_binary/path.js +3 -3
  50. package/dist/worker/bgWorker.js +1 -1
  51. package/docs/proven-in-use.md +8 -0
  52. package/docs/releasing.md +1 -1
  53. package/docs/setup-uri.md +58 -0
  54. package/package.json +9 -1
package/README.md CHANGED
@@ -33,6 +33,7 @@ The package is ESM-only and declares Node 20 or later. Browser entry points are
33
33
  | `@vrtmrz/livesync-commonlib/rpc` | Existing LiveSync RPC and PouchDB bridge migration | Transitional only; not part of the stable Commonlib 1.0 contract |
34
34
  | `@vrtmrz/livesync-commonlib/remote-configurations` | Multiple-remote profile creation, naming, and selection | Focused, package-tested pre-1.0 contract |
35
35
  | `@vrtmrz/livesync-commonlib/settings` | New-Vault defaults, stored-setting fallbacks, and settings migration results | Focused, package-tested pre-1.0 contract |
36
+ | `@vrtmrz/livesync-commonlib/setup-uri` | Encrypted Setup URI generation, availability, and import | Focused, package-tested pre-1.0 contract |
36
37
  | `@vrtmrz/livesync-commonlib/compat/*` | Exact legacy imports still required by existing clients | Migration-only; paths may be removed as consumers migrate |
37
38
  | `@vrtmrz/livesync-commonlib/package.json` | Package metadata for tooling | Metadata export, not a runtime API |
38
39
 
@@ -113,6 +114,10 @@ New-Vault recommendations are deliberately separate from the conservative values
113
114
 
114
115
  See [the settings lifecycle guide](docs/settings-lifecycle.md) before initialising, importing, resetting, or migrating settings.
115
116
 
117
+ ## Setup URI sharing
118
+
119
+ The focused Setup URI entry offers an Ephemeral mode bound to the current fixed seven-day window and a Persistent mode compatible with existing readers. Both use the same URI structure. See [the Setup URI sharing guide](docs/setup-uri.md) for the availability boundary, import behaviour, and compatibility limits.
120
+
116
121
  ## Remote connection profiles
117
122
 
118
123
  New profile-management code can create or update a CouchDB, Object Storage, or P2P connection through the focused remote-configurations entry. Profile IDs provide identity, display names remain editable presentation, and the active selection is explicit. Older flat connection fields remain a runtime projection and an import-compatibility boundary rather than the preferred persistence model.
@@ -13,6 +13,8 @@ export type DirectFileManipulatorOptions = {
13
13
  passphrase: string | undefined;
14
14
  database: string;
15
15
  obfuscatePassphrase: string | undefined;
16
+ idDerivationVersion?: 0 | 1;
17
+ idDerivationKey?: string;
16
18
  encryptInternalMetadata?: boolean;
17
19
  useDynamicIterationCount?: boolean;
18
20
  customChunkSize?: number;
@@ -7,10 +7,8 @@ import {
7
7
  VER
8
8
  } from "../common/types.js";
9
9
  import { checkRemoteVersion } from "../pouchdb/negotiation.js";
10
- import {
11
- ENCRYPTED_INTERNAL_METADATA_FEATURE,
12
- usesEncryptedInternalMetadata
13
- } from "../pouchdb/remoteFeatureCompatibility.js";
10
+ import { requiredRemoteFeatures, usesEncryptedInternalMetadata } from "../pouchdb/remoteFeatureCompatibility.js";
11
+ import { assessRemoteDocumentIds } from "../pouchdb/remoteIdCompatibility.js";
14
12
  import { PouchDB } from "../pouchdb/pouchdb-http.js";
15
13
  import { LiveSyncLocalDB } from "../pouchdb/LiveSyncLocalDB.js";
16
14
  import { isErrorOfMissingDoc } from "../pouchdb/utils_couchdb.js";
@@ -57,7 +55,19 @@ class DirectFileManipulator {
57
55
  if (!await this.liveSyncLocalDB.initializeDatabase()) {
58
56
  throw new Error("Direct database initialisation was rejected.");
59
57
  }
60
- const requiredFeatures = usesEncryptedInternalMetadata(this.settings) ? [ENCRYPTED_INTERNAL_METADATA_FEATURE] : [];
58
+ const idSettings = {
59
+ ...this.settings,
60
+ passphrase: this.options.obfuscatePassphrase || this.settings.passphrase
61
+ };
62
+ const idCompatibility = await assessRemoteDocumentIds(
63
+ this.liveSyncLocalDB.localDatabase,
64
+ idSettings,
65
+ (path) => this.services.path.path2idWithSettings(path, idSettings)
66
+ );
67
+ if (idCompatibility === "mismatched") {
68
+ throw new Error("Direct database document IDs do not match the configured ID key.");
69
+ }
70
+ const requiredFeatures = requiredRemoteFeatures(this.settings);
61
71
  if (!await checkRemoteVersion(
62
72
  this.liveSyncLocalDB.localDatabase,
63
73
  async () => false,
@@ -200,6 +210,8 @@ class DirectFileManipulator {
200
210
  minimumChunkSize: this.options.minimumChunkSize ?? DEFAULT_SETTINGS.minimumChunkSize,
201
211
  encrypt: this.options.passphrase ? true : false,
202
212
  passphrase: this.options.passphrase ?? "",
213
+ idDerivationVersion: this.options.idDerivationVersion ?? 0,
214
+ idDerivationKey: this.options.idDerivationKey ?? "",
203
215
  deleteMetadataOfDeletedFiles: DEFAULT_SETTINGS.deleteMetadataOfDeletedFiles,
204
216
  customChunkSize: this.options.customChunkSize ?? DEFAULT_SETTINGS.customChunkSize,
205
217
  doNotPaceReplication: DEFAULT_SETTINGS.doNotPaceReplication,
@@ -26,6 +26,20 @@ export interface SplitQRCodeData {
26
26
  */
27
27
  export declare function encodeQR(settingString: string, format: OutputFormat): string | SplitQRCodeData;
28
28
  type ErasureProperties = keyof ObsidianLiveSyncSettings;
29
+ export type TimeBoundSetupURIMode = "ephemeral" | "persistent";
30
+ export interface TimeBoundSetupURIOptions {
31
+ mode?: TimeBoundSetupURIMode;
32
+ removeProperties?: ErasureProperties[];
33
+ skipDefaultValue?: boolean;
34
+ }
35
+ export interface TimeBoundSetupURIResult {
36
+ uri: string;
37
+ usableUntil: number | null;
38
+ }
39
+ /** Return the exclusive end of the current fixed Setup URI window. */
40
+ export declare function getTimeBoundSetupURIUsableUntil(): number;
41
+ /** Check the same fixed window that the Setup URI reader uses. */
42
+ export declare function isTimeBoundSetupURIUsableNow(usableUntil: number | null): boolean;
29
43
  /**
30
44
  * Generate setup URI with encrypted settings
31
45
  * @param settingString Settings to encode
@@ -37,8 +51,12 @@ type ErasureProperties = keyof ObsidianLiveSyncSettings;
37
51
  * @returns Generated setup URI
38
52
  */
39
53
  export declare function encodeSettingsToSetupURI(settingString: ObsidianLiveSyncSettings, passphrase: string, removeProperties?: ErasureProperties[], skipDefaultValue?: boolean): Promise<string>;
54
+ /** Generate a Setup URI for the current UTC window or in the existing persistent format. */
55
+ export declare function encodeTimeBoundSetupURI(settingString: ObsidianLiveSyncSettings, passphrase: string, options?: TimeBoundSetupURIOptions): Promise<TimeBoundSetupURIResult>;
40
56
  export declare function decodeSettingsFromSetupURI(uri: string, passphrase: string): Promise<false | Omit<{
41
57
  encryptInternalMetadata: boolean;
58
+ idDerivationVersion: 0 | 1;
59
+ idDerivationKey: string;
42
60
  liveSync: boolean;
43
61
  syncOnSave: boolean;
44
62
  syncOnStart: boolean;
@@ -74,6 +92,7 @@ export declare function decodeSettingsFromSetupURI(uri: string, passphrase: stri
74
92
  disableMarkdownAutoMerge: boolean;
75
93
  configPassphraseStore: import("../common/types.js").ConfigPassphraseStore;
76
94
  encryptedPassphrase: string;
95
+ encryptedIdDerivationKey: string;
77
96
  encryptedCouchDBConnection: string;
78
97
  periodicReplication: boolean;
79
98
  periodicReplicationInterval: number;
@@ -9,8 +9,10 @@ import {
9
9
  } from "../common/types.js";
10
10
  import { decryptString, encryptString } from "../encryption/stringEncryption.js";
11
11
  import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
12
+ import { configuredIdKey } from "../common/idDerivation.js";
12
13
  function encodeSettingsToQRCodeData(settings) {
13
14
  settings = omitP2PRuntimeSettings(settings);
15
+ configuredIdKey(settings);
14
16
  const fullIndexes = Object.entries(KeyIndexOfSettings);
15
17
  let maxIndex = 0;
16
18
  for (const [, index] of fullIndexes) {
@@ -50,6 +52,7 @@ function decodeSettingsFromQRCodeData(qr) {
50
52
  LOG_LEVEL_VERBOSE
51
53
  );
52
54
  }
55
+ configuredIdKey(newSettings);
53
56
  return omitP2PRuntimeSettings(newSettings);
54
57
  }
55
58
  var OutputFormat = /* @__PURE__ */ ((OutputFormat2) => {
@@ -94,15 +97,47 @@ function encodeQR(settingString, format) {
94
97
  return { total, parts };
95
98
  }
96
99
  }
100
+ const SETUP_URI_WINDOW_MS = 6048e5;
101
+ function getTimeBoundSetupURIUsableUntil() {
102
+ const usableUntil = (setupURIWindow(Date.now()) + 1) * SETUP_URI_WINDOW_MS;
103
+ if (!Number.isSafeInteger(usableUntil)) throw new Error("Invalid Setup URI clock");
104
+ return usableUntil;
105
+ }
106
+ function isTimeBoundSetupURIUsableNow(usableUntil) {
107
+ if (usableUntil === null) return true;
108
+ if (!Number.isSafeInteger(usableUntil) || usableUntil < SETUP_URI_WINDOW_MS) return false;
109
+ try {
110
+ return setupURIWindow(Date.now()) === usableUntil / SETUP_URI_WINDOW_MS - 1;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+ function setupURIWindow(now) {
116
+ if (!Number.isSafeInteger(now) || now < 0) throw new Error("Invalid Setup URI clock");
117
+ return Math.floor(now / SETUP_URI_WINDOW_MS);
118
+ }
119
+ async function ephemeralSetupURIPassphrase(passphrase, window) {
120
+ const text = new TextEncoder();
121
+ const keyBytes = await crypto.subtle.digest("SHA-256", text.encode(passphrase));
122
+ const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
123
+ const context = JSON.stringify(["livesync/setup-uri", "tb1", "ephemeral", window]);
124
+ const signed = await crypto.subtle.sign("HMAC", key, text.encode(context));
125
+ return Array.from(new Uint8Array(signed), (byte) => byte.toString(16).padStart(2, "0")).join("");
126
+ }
97
127
  const necessaryErasureProperties = [
98
128
  "configPassphraseStore",
99
129
  "encryptedCouchDBConnection",
100
- "encryptedPassphrase"
130
+ "encryptedPassphrase",
131
+ "encryptedIdDerivationKey"
101
132
  ];
102
133
  async function encodeSettingsToSetupURI(settingString, passphrase, removeProperties = ["pluginSyncExtendedSetting"], skipDefaultValue = false) {
103
134
  const setting = {
104
135
  ...omitP2PRuntimeSettings(settingString)
105
136
  };
137
+ configuredIdKey({
138
+ idDerivationVersion: setting.idDerivationVersion ?? 0,
139
+ idDerivationKey: setting.idDerivationKey ?? ""
140
+ });
106
141
  delete setting.P2P_managedType;
107
142
  delete setting.P2P_managedId;
108
143
  delete setting.P2P_managedToken;
@@ -124,15 +159,62 @@ async function encodeSettingsToSetupURI(settingString, passphrase, removePropert
124
159
  const uri = `${configURIBase}${encryptedSetting} `;
125
160
  return uri;
126
161
  }
162
+ async function encodeTimeBoundSetupURI(settingString, passphrase, options = {}) {
163
+ const { mode = "ephemeral", removeProperties, skipDefaultValue } = options;
164
+ if (mode === "persistent") {
165
+ return {
166
+ uri: await encodeSettingsToSetupURI(settingString, passphrase, removeProperties, skipDefaultValue),
167
+ usableUntil: null
168
+ };
169
+ }
170
+ if (mode !== "ephemeral") throw new Error("Invalid Setup URI mode");
171
+ const window = setupURIWindow(Date.now());
172
+ const effectivePassphrase = await ephemeralSetupURIPassphrase(passphrase, window);
173
+ const uri = await encodeSettingsToSetupURI(settingString, effectivePassphrase, removeProperties, skipDefaultValue);
174
+ if (setupURIWindow(Date.now()) !== window) throw new Error("Setup URI window changed during generation");
175
+ return { uri, usableUntil: (window + 1) * SETUP_URI_WINDOW_MS };
176
+ }
127
177
  async function decodeSettingsFromSetupURI(uri, passphrase) {
128
178
  const encryptedSetting = uri.substring(configURIBase.length);
129
- const decrypted = await decryptString(decodeURIComponent(encryptedSetting), passphrase);
179
+ const encrypted = decodeURIComponent(encryptedSetting);
180
+ let decrypted;
181
+ if (encrypted.startsWith("%$")) {
182
+ let window;
183
+ try {
184
+ window = setupURIWindow(Date.now());
185
+ } catch {
186
+ }
187
+ const matches = [];
188
+ if (window !== void 0) {
189
+ const candidate = await ephemeralSetupURIPassphrase(passphrase, window);
190
+ try {
191
+ matches.push({ mode: "ephemeral", value: await decryptString(encrypted, candidate) });
192
+ } catch {
193
+ }
194
+ }
195
+ try {
196
+ matches.push({ mode: "persistent", value: await decryptString(encrypted, passphrase) });
197
+ } catch {
198
+ }
199
+ if (matches.length !== 1) throw new Error("Cannot open Setup URI");
200
+ const matched = matches[0];
201
+ if (matched.mode === "ephemeral" && setupURIWindow(Date.now()) !== window) {
202
+ throw new Error("Cannot open Setup URI");
203
+ }
204
+ decrypted = matched.value;
205
+ } else {
206
+ decrypted = await decryptString(encrypted, passphrase);
207
+ }
130
208
  try {
131
209
  const imported = JSON.parse(decrypted);
132
- return omitP2PRuntimeSettings({
210
+ const settings = {
133
211
  ...imported,
134
- encryptInternalMetadata: typeof imported.encryptInternalMetadata === "boolean" ? imported.encryptInternalMetadata : false
135
- });
212
+ encryptInternalMetadata: typeof imported.encryptInternalMetadata === "boolean" ? imported.encryptInternalMetadata : false,
213
+ idDerivationVersion: imported.idDerivationVersion ?? 0,
214
+ idDerivationKey: imported.idDerivationKey ?? ""
215
+ };
216
+ configuredIdKey(settings);
217
+ return omitP2PRuntimeSettings(settings);
136
218
  } catch {
137
219
  Logger(`Failed to parse settings from decrypted data`, LOG_LEVEL_NOTICE);
138
220
  return false;
@@ -144,5 +226,8 @@ export {
144
226
  decodeSettingsFromSetupURI,
145
227
  encodeQR,
146
228
  encodeSettingsToQRCodeData,
147
- encodeSettingsToSetupURI
229
+ encodeSettingsToSetupURI,
230
+ encodeTimeBoundSetupURI,
231
+ getTimeBoundSetupURIUsableUntil,
232
+ isTimeBoundSetupURIUsableNow
148
233
  };
@@ -0,0 +1,13 @@
1
+ import type { EncryptionSettings } from "./models/setting.type.js";
2
+ export declare const ID_DERIVATION_VERSION = 1;
3
+ export declare const ID_RECOVERY_CODE_PREFIX = "sls-id-v1:";
4
+ /** Derive a portable key once, when the source string is saved. */
5
+ export declare function deriveIdKey(source: string): Promise<string>;
6
+ /** Export a saved key in a format which can be imported without deriving it again. */
7
+ export declare function formatIdRecoveryCode(key: string): string;
8
+ /** Treat a tagged recovery code as a saved key; derive ordinary source strings. */
9
+ export declare function deriveOrImportIdKey(input: string): Promise<string>;
10
+ /** Return the independent ID key, rejecting an incomplete new-mode setting. */
11
+ export declare function configuredIdKey(settings: Pick<EncryptionSettings, "idDerivationVersion" | "idDerivationKey">): string | false;
12
+ /** Derive distinct identifiers for content and document paths from one saved key. */
13
+ export declare function computeKeyedId(key: string, purpose: "chunk" | "document" | "peer-agreement" | "remote-agreement", value: string): Promise<string>;
@@ -0,0 +1,75 @@
1
+ import { getWebCrypto } from "../mods.js";
2
+ import { hexStringToUint8Array, uint8ArrayToHexString } from "../string_and_binary/convert.js";
3
+ const ID_DERIVATION_VERSION = 1;
4
+ const ID_RECOVERY_CODE_PREFIX = "sls-id-v1:";
5
+ const SOURCE_SALT = new TextEncoder().encode("self-hosted-livesync:id-source:v1");
6
+ const SOURCE_ITERATIONS = 31e4;
7
+ const HEX_KEY = /^[0-9a-f]{64}$/u;
8
+ async function deriveIdKey(source) {
9
+ if (source.length === 0) {
10
+ throw new Error("An ID source is required.");
11
+ }
12
+ const crypto = await getWebCrypto();
13
+ const sourceKey = await crypto.subtle.importKey(
14
+ "raw",
15
+ new TextEncoder().encode(source.normalize("NFC")),
16
+ "PBKDF2",
17
+ false,
18
+ ["deriveBits"]
19
+ );
20
+ const bits = await crypto.subtle.deriveBits(
21
+ { name: "PBKDF2", hash: "SHA-256", salt: SOURCE_SALT, iterations: SOURCE_ITERATIONS },
22
+ sourceKey,
23
+ 256
24
+ );
25
+ return uint8ArrayToHexString(new Uint8Array(bits));
26
+ }
27
+ function formatIdRecoveryCode(key) {
28
+ if (!HEX_KEY.test(key)) throw new Error("The configured ID key is invalid.");
29
+ return `${ID_RECOVERY_CODE_PREFIX}${key}`;
30
+ }
31
+ async function deriveOrImportIdKey(input) {
32
+ const candidate = input.trim();
33
+ if (candidate.startsWith(ID_RECOVERY_CODE_PREFIX)) {
34
+ const key = candidate.slice(ID_RECOVERY_CODE_PREFIX.length);
35
+ if (!HEX_KEY.test(key)) throw new Error("The ID recovery code is invalid.");
36
+ return key;
37
+ }
38
+ if (candidate.startsWith("sls-id-v")) throw new Error("The ID recovery code version is unsupported.");
39
+ return await deriveIdKey(input);
40
+ }
41
+ function configuredIdKey(settings) {
42
+ const version = settings.idDerivationVersion;
43
+ const key = settings.idDerivationKey;
44
+ if (version === 0 || version === void 0) {
45
+ if (key) throw new Error("An ID key requires a supported derivation version.");
46
+ return false;
47
+ }
48
+ if (version !== ID_DERIVATION_VERSION || !HEX_KEY.test(key)) {
49
+ throw new Error("The configured ID derivation is unavailable or invalid.");
50
+ }
51
+ return key;
52
+ }
53
+ async function computeKeyedId(key, purpose, value) {
54
+ if (!HEX_KEY.test(key)) throw new Error("The configured ID key is invalid.");
55
+ const crypto = await getWebCrypto();
56
+ const hmacKey = await crypto.subtle.importKey(
57
+ "raw",
58
+ hexStringToUint8Array(key),
59
+ { name: "HMAC", hash: "SHA-256" },
60
+ false,
61
+ ["sign"]
62
+ );
63
+ const input = new TextEncoder().encode(`self-hosted-livesync:id-v1:${purpose}\0${value}`);
64
+ const digest = await crypto.subtle.sign("HMAC", hmacKey, input);
65
+ return uint8ArrayToHexString(new Uint8Array(digest));
66
+ }
67
+ export {
68
+ ID_DERIVATION_VERSION,
69
+ ID_RECOVERY_CODE_PREFIX,
70
+ computeKeyedId,
71
+ configuredIdKey,
72
+ deriveIdKey,
73
+ deriveOrImportIdKey,
74
+ formatIdRecoveryCode
75
+ };
@@ -43,6 +43,7 @@ export interface EntryMilestoneInfo extends DatabaseEntry {
43
43
  tweak_values: {
44
44
  [key: NodeKey]: TweakValues;
45
45
  };
46
+ encrypted_id_derivation_proof?: string;
46
47
  }
47
48
  export interface EntryNodeInfo extends DatabaseEntry {
48
49
  _id: typeof NODEINFO_DOCID;
@@ -56,6 +56,8 @@ const SETTINGS_SCHEMA_DEFAULTS = {
56
56
  syncOnFileOpen: false,
57
57
  encrypt: false,
58
58
  passphrase: "",
59
+ idDerivationVersion: 0,
60
+ idDerivationKey: "",
59
61
  usePathObfuscation: false,
60
62
  encryptInternalMetadata: false,
61
63
  doNotDeleteFolder: false,
@@ -104,6 +106,7 @@ const SETTINGS_SCHEMA_DEFAULTS = {
104
106
  syncAfterMerge: false,
105
107
  configPassphraseStore: "",
106
108
  encryptedPassphrase: "",
109
+ encryptedIdDerivationKey: "",
107
110
  encryptedCouchDBConnection: "",
108
111
  permitEmptyPassphrase: false,
109
112
  remoteConfigurations: {},
@@ -177,7 +177,10 @@ const KeyIndexOfSettings = {
177
177
  // Do not encode into the QR Code.
178
178
  P2P_maxWirePayloadBytes: 163,
179
179
  P2P_connectionPath: 164,
180
- encryptInternalMetadata: 165
180
+ encryptInternalMetadata: 165,
181
+ idDerivationVersion: 166,
182
+ idDerivationKey: 167,
183
+ encryptedIdDerivationKey: -1
181
184
  };
182
185
  export {
183
186
  KeyIndexOfSettings
@@ -65,6 +65,8 @@ interface EncryptedUserSettings {
65
65
  * The encrypted passphrase used for E2EE.
66
66
  */
67
67
  encryptedPassphrase: string;
68
+ /** Encrypted copy of the independent ID key in local persisted settings. */
69
+ encryptedIdDerivationKey: string;
68
70
  /**
69
71
  * The encrypted connection details for CouchDB.
70
72
  */
@@ -585,6 +587,10 @@ export interface EncryptionSettings {
585
587
  * The passphrase used for E2EE.
586
588
  */
587
589
  passphrase: string;
590
+ /** Zero preserves the historical passphrase-derived ID scheme. */
591
+ idDerivationVersion: 0 | 1;
592
+ /** A 256-bit key encoded as lower-case hex; never the source string. */
593
+ idDerivationKey: string;
588
594
  /**
589
595
  * Indicates whether path obfuscation is used.
590
596
  * If not, the path will be stored as it is, as the document ID.
@@ -46,7 +46,7 @@ function transition(entries, sourceSide, targetSide) {
46
46
  const effectiveValueDiffers = source.effectiveValue !== target.effectiveValue;
47
47
  const shouldFillMissingTarget = !targetAdvertised && sourceAdvertised;
48
48
  if (source.effectiveValue === void 0 || !effectiveValueDiffers && !shouldFillMissingTarget) continue;
49
- changes[entry.key] = source.effectiveValue;
49
+ if (entry.key !== "idDerivationVersion") changes[entry.key] = source.effectiveValue;
50
50
  if (entry.relation === "equal") continue;
51
51
  const effect = reconstructionForTransition(entry.key, target.effectiveValue, source.effectiveValue);
52
52
  if (effect === "none") continue;
@@ -67,6 +67,8 @@ function assessTweakCompatibility(current, preferred) {
67
67
  const currentValues = snapshotTweakValues(current);
68
68
  const preferredValues = snapshotTweakValues(preferred);
69
69
  let representationDiffers = false;
70
+ const currentUsesKeyedDocumentIds = currentValues.encrypt !== false && currentValues.usePathObfuscation !== false;
71
+ const preferredUsesKeyedDocumentIds = preferredValues.encrypt !== false && preferredValues.usePathObfuscation !== false;
70
72
  const entries = TweakCompatibilityRules.map((rule) => {
71
73
  const knownDefault = "knownDefault" in rule ? rule.knownDefault : void 0;
72
74
  const currentValue = assessedValue(currentValues, rule.key, knownDefault);
@@ -74,7 +76,9 @@ function assessTweakCompatibility(current, preferred) {
74
76
  const currentAdvertised = currentValue.present && currentValue.rawValue !== void 0;
75
77
  const preferredAdvertised = preferredValue.present && preferredValue.rawValue !== void 0;
76
78
  let relation;
77
- if ((!currentAdvertised || !preferredAdvertised) && knownDefault === void 0) {
79
+ if (rule.key === "idDerivationVersion" && !currentUsesKeyedDocumentIds && !preferredUsesKeyedDocumentIds) {
80
+ relation = "equal";
81
+ } else if ((!currentAdvertised || !preferredAdvertised) && knownDefault === void 0) {
78
82
  relation = "unadvertised";
79
83
  } else {
80
84
  relation = currentValue.effectiveValue === preferredValue.effectiveValue ? "equal" : "different";
@@ -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: "idDerivationVersion";
25
+ readonly templateValue: 0;
26
+ readonly reconstruction: "required";
27
+ readonly knownDefault: 0;
23
28
  }, {
24
29
  readonly key: "encryptInternalMetadata";
25
30
  readonly templateValue: false;
@@ -112,6 +117,7 @@ export declare const TweakValuesTemplate: {
112
117
  tweakModified: number;
113
118
  usePluginSyncV2: boolean;
114
119
  encrypt: boolean;
120
+ idDerivationVersion: 0 | 1;
115
121
  usePathObfuscation: boolean;
116
122
  encryptInternalMetadata: boolean;
117
123
  E2EEAlgorithm: import("./setting.type.js").E2EEAlgorithm;
@@ -164,6 +170,7 @@ export declare const TweakValuesTemplate: {
164
170
  disableMarkdownAutoMerge?: boolean;
165
171
  configPassphraseStore?: import("./setting.type.js").ConfigPassphraseStore;
166
172
  encryptedPassphrase?: string;
173
+ encryptedIdDerivationKey?: string;
167
174
  encryptedCouchDBConnection?: string;
168
175
  periodicReplication?: boolean;
169
176
  periodicReplicationInterval?: number;
@@ -234,6 +241,7 @@ export declare const TweakValuesTemplate: {
234
241
  forcePathStyle?: boolean;
235
242
  remoteType?: import("./setting.type.js").RemoteType;
236
243
  passphrase?: string;
244
+ idDerivationKey?: string;
237
245
  doNotUseFixedRevisionForChunks?: boolean;
238
246
  checkIntegrityOnSave?: boolean;
239
247
  useHistory?: boolean;
@@ -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: "idDerivationVersion", templateValue: 0, reconstruction: "required", knownDefault: 0 },
8
9
  { key: "encryptInternalMetadata", templateValue: false, reconstruction: "none", knownDefault: false },
9
10
  { key: "enableCompression", templateValue: false, reconstruction: "none" },
10
11
  { key: "useEden", templateValue: false, reconstruction: "none" },
@@ -81,9 +82,9 @@ const TweakValuesRecommendedTemplate = {
81
82
  };
82
83
  const TweakValuesDefault = {
83
84
  ...Object.fromEntries(
84
- TweakCompatibilityRules.filter((rule) => "knownDefault" in rule && rule.key !== "handleFilenameCaseSensitive").map(
85
- ({ key, knownDefault }) => [key, knownDefault]
86
- )
85
+ TweakCompatibilityRules.filter(
86
+ (rule) => "knownDefault" in rule && rule.key !== "handleFilenameCaseSensitive"
87
+ ).map(({ key, knownDefault }) => [key, knownDefault])
87
88
  ),
88
89
  tweakModified: DEFAULT_SETTINGS.tweakModified
89
90
  };
@@ -64,6 +64,7 @@ export declare const AllSettingDefault: {
64
64
  disableMarkdownAutoMerge: boolean;
65
65
  configPassphraseStore: import("./types.js").ConfigPassphraseStore;
66
66
  encryptedPassphrase: string;
67
+ encryptedIdDerivationKey: string;
67
68
  encryptedCouchDBConnection: string;
68
69
  periodicReplication: boolean;
69
70
  periodicReplicationInterval: number;
@@ -135,6 +136,8 @@ export declare const AllSettingDefault: {
135
136
  remoteType: import("./types.js").RemoteType;
136
137
  encrypt: boolean;
137
138
  passphrase: string;
139
+ idDerivationVersion: 0 | 1;
140
+ idDerivationKey: string;
138
141
  usePathObfuscation: boolean;
139
142
  encryptInternalMetadata: boolean;
140
143
  E2EEAlgorithm: import("./types.js").E2EEAlgorithm;
@@ -449,7 +449,9 @@ function pickEncryptionSettings(setting) {
449
449
  encrypt: setting.encrypt,
450
450
  passphrase: setting.passphrase,
451
451
  usePathObfuscation: setting.usePathObfuscation,
452
- encryptInternalMetadata: setting.encryptInternalMetadata
452
+ encryptInternalMetadata: setting.encryptInternalMetadata,
453
+ idDerivationVersion: setting.idDerivationVersion,
454
+ idDerivationKey: setting.idDerivationKey
453
455
  };
454
456
  }
455
457
  function pickP2PSyncSettings(setting) {
@@ -7,6 +7,8 @@ export declare const EVENT_FILE_RENAMED = "file-renamed";
7
7
  export declare const EVENT_FILE_SAVED = "file-saved";
8
8
  export declare const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
9
9
  export declare const EVENT_DATABASE_REBUILT = "database-rebuilt";
10
+ /** Emitted by `AppLifecycleService.markIsReady()` each time it establishes application readiness. */
11
+ export declare const EVENT_APPLICATION_READY = "application-ready";
10
12
  export declare const EVENT_LOG_ADDED = "log-added";
11
13
  export declare const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri";
12
14
  export declare const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri";
@@ -37,6 +39,7 @@ declare global {
37
39
  old: FilePathWithPrefix;
38
40
  };
39
41
  [EVENT_DATABASE_REBUILT]: undefined;
42
+ [EVENT_APPLICATION_READY]: undefined;
40
43
  [EVENT_REQUEST_OPEN_P2P_SETTINGS]: undefined;
41
44
  [EVENT_LEAF_ACTIVE_CHANGED]: undefined;
42
45
  [EVENT_REQUEST_OPEN_SETUP_URI]: undefined;
@@ -6,6 +6,7 @@ const EVENT_FILE_RENAMED = "file-renamed";
6
6
  const EVENT_FILE_SAVED = "file-saved";
7
7
  const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed";
8
8
  const EVENT_DATABASE_REBUILT = "database-rebuilt";
9
+ const EVENT_APPLICATION_READY = "application-ready";
9
10
  const EVENT_LOG_ADDED = "log-added";
10
11
  const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri";
11
12
  const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri";
@@ -21,6 +22,7 @@ const EVENT_ON_UNRESOLVED_ERROR = "on-unresolved-error";
21
22
  const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
22
23
  const EVENT_CONFLICT_CANCELLED = "conflict-cancelled";
23
24
  export {
25
+ EVENT_APPLICATION_READY,
24
26
  EVENT_CONFLICT_CANCELLED,
25
27
  EVENT_DATABASE_REBUILT,
26
28
  EVENT_FILE_CHANGED,
@@ -228,6 +228,11 @@ function isTargetFile(host, filenameSrc) {
228
228
  return true;
229
229
  }
230
230
  async function prepareChunk({ chunkManager, hashManager }, piece) {
231
+ if (hashManager.usesIndependentIdKey()) {
232
+ const id = `${IDPrefixes.Chunk}${await hashManager.computeHash(piece)}`;
233
+ const cached = chunkManager.getCachedChunk(id);
234
+ return { isNew: !cached || cached.data !== piece, id, piece };
235
+ }
231
236
  const cachedChunkId = chunkManager.getChunkIDFromCache(piece);
232
237
  if (cachedChunkId !== false) {
233
238
  return { isNew: false, id: cachedChunkId, piece };
@@ -387,7 +392,11 @@ async function respondEntryFromMeta({ localDatabase, chunkManager }, settings, f
387
392
  }));
388
393
  edenChunks = Object.fromEntries(chunks2.map((e) => [e._id, e]));
389
394
  }
390
- const { waitForDelivery, preventRemoteRequest } = computeChunkRetrievalMethod(waitForReady, settings, localOnly);
395
+ const { waitForDelivery, preventRemoteRequest } = computeChunkRetrievalMethod(
396
+ waitForReady,
397
+ settings,
398
+ localOnly
399
+ );
391
400
  const childrenKeys = [...meta.children];
392
401
  const chunks = await chunkManager.read(
393
402
  childrenKeys,
@@ -43,6 +43,7 @@ export declare class HashManager extends HashManagerCore {
43
43
  * @returns The hash value (returned as a Promise)
44
44
  */
45
45
  computeHash(piece: string): Promise<string>;
46
+ usesIndependentIdKey(): boolean;
46
47
  /**
47
48
  * Computes the hash value without encryption.
48
49
  *
@@ -1,7 +1,8 @@
1
1
  import { FallbackWasmHashManager, XXHash32RawHashManager, XXHash64HashManager } from "./XXHashHashManager.js";
2
2
  import { FallbackPureJSHashManager, PureJSHashManager, SHA1HashManager } from "./PureJSHashManager.js";
3
- import { HashManagerCore } from "./HashManagerCore.js";
3
+ import { HashEncryptedPrefix, HashManagerCore } from "./HashManagerCore.js";
4
4
  import { LOG_LEVEL_VERBOSE, Logger } from "../../common/logger.js";
5
+ import { computeKeyedId, configuredIdKey } from "../../common/idDerivation.js";
5
6
  const HashManagers = [
6
7
  XXHash64HashManager,
7
8
  XXHash32RawHashManager,
@@ -71,8 +72,17 @@ class HashManager extends HashManagerCore {
71
72
  * @returns The hash value (returned as a Promise)
72
73
  */
73
74
  async computeHash(piece) {
75
+ const settings = this.options.settingService.currentSettings();
76
+ const key = configuredIdKey(settings);
77
+ if (settings.encrypt && key) {
78
+ return HashEncryptedPrefix + await computeKeyedId(key, "chunk", piece);
79
+ }
74
80
  return await this.manager.computeHash(piece);
75
81
  }
82
+ usesIndependentIdKey() {
83
+ const settings = this.options.settingService.currentSettings();
84
+ return settings.encrypt && configuredIdKey(settings) !== false;
85
+ }
76
86
  /**
77
87
  * Computes the hash value without encryption.
78
88
  *
@@ -89,6 +99,8 @@ class HashManager extends HashManagerCore {
89
99
  * @returns The hash value (returned as a Promise)
90
100
  */
91
101
  computeHashWithEncryption(piece) {
102
+ const key = configuredIdKey(this.options.settingService.currentSettings());
103
+ if (key) return computeKeyedId(key, "chunk", piece);
92
104
  return this.manager.computeHashWithEncryption(piece);
93
105
  }
94
106
  }
@@ -1,6 +1,6 @@
1
1
  import { type EntryDoc, type EntryMilestoneInfo, type RemoteDBSettings, type ChunkVersionRange, type TweakValues, type DeviceInfo, type TweakAssessment } from "../common/types.js";
2
2
  export declare function getEffectiveTweakValues(setting: RemoteDBSettings): TweakValues;
3
- export type ENSURE_DB_RESULT = "OK" | "INCOMPATIBLE" | "LOCKED" | "NODE_LOCKED" | "NODE_CLEANED" | ["MISMATCHED", TweakValues];
3
+ export type ENSURE_DB_RESULT = "OK" | "INCOMPATIBLE" | "ID_KEY_MISMATCH" | "LOCKED" | "NODE_LOCKED" | "NODE_CLEANED" | ["MISMATCHED", TweakValues];
4
4
  /**
5
5
  * Ensures that the remote database is compatible with the current device.
6
6
  *