@vrtmrz/livesync-commonlib 0.1.35 → 0.1.38
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/dist/common/settingConstants.js +1 -1
- package/dist/services/base/CommonlibMessages.d.ts +7 -0
- package/dist/services/base/CommonlibMessages.js +7 -0
- package/dist/services/base/IService.d.ts +3 -1
- package/dist/services/base/RemoteService.js +6 -2
- package/dist/services/base/SettingService.d.ts +19 -3
- package/dist/services/base/SettingService.js +263 -48
- package/docs/proven-in-use.md +4 -0
- package/docs/settings-lifecycle.md +6 -0
- package/package.json +2 -2
|
@@ -71,7 +71,7 @@ const SettingInformation = {
|
|
|
71
71
|
},
|
|
72
72
|
configPassphrase: {
|
|
73
73
|
name: "Passphrase of sensitive configuration items",
|
|
74
|
-
desc: "
|
|
74
|
+
desc: "SettingService.ConfigurationPassphraseDeviceLocalDescription"
|
|
75
75
|
},
|
|
76
76
|
configPassphraseStore: {
|
|
77
77
|
name: "Encrypting sensitive configuration items"
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* these messages so that symbolic keys are never exposed to users.
|
|
6
6
|
*/
|
|
7
7
|
export declare const commonlibEnglishMessages: {
|
|
8
|
+
readonly "SettingService.ConfigurationPassphraseDeviceLocalDescription": "This passphrase stays on this device. When another device needs to decrypt protected settings without a saved key, enter the same passphrase at start-up.";
|
|
9
|
+
readonly "SettingService.ConfigurationPassphraseTitle": "Configuration passphrase";
|
|
10
|
+
readonly "SettingService.ConfigurationPassphraseRetryTitle": "Configuration could not be decrypted. Enter the configuration passphrase again.";
|
|
11
|
+
readonly "SettingService.ConfigurationPassphraseCancelled": "Configuration passphrase entry was cancelled or unavailable. Start-up was stopped; saved settings were retained.";
|
|
12
|
+
readonly "SettingService.ConfigurationEncryptionSaveFailed": "Configuration encryption could not be saved. Verify the saved settings and device key before retrying.";
|
|
13
|
+
readonly "SettingService.ConfigurationPassphraseRollbackFailed": "The configuration passphrase could not be restored after a write failure. Verify the saved settings and device key before retrying.";
|
|
14
|
+
readonly "SettingService.ConfigurationCouldNotBeDecrypted": "Existing configuration could not be decrypted. The configuration key was not changed.";
|
|
8
15
|
readonly "(BETA) Always overwrite with a newer file": "(BETA) Always overwrite with a newer file";
|
|
9
16
|
readonly "(Beta) Use ignore files": "(Beta) Use ignore files";
|
|
10
17
|
readonly "(Days passed, 0 to disable automatic-deletion)": "(Days passed, 0 to disable automatic-deletion)";
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
const commonlibEnglishMessages = {
|
|
2
|
+
"SettingService.ConfigurationPassphraseDeviceLocalDescription": "This passphrase stays on this device. When another device needs to decrypt protected settings without a saved key, enter the same passphrase at start-up.",
|
|
3
|
+
"SettingService.ConfigurationPassphraseTitle": "Configuration passphrase",
|
|
4
|
+
"SettingService.ConfigurationPassphraseRetryTitle": "Configuration could not be decrypted. Enter the configuration passphrase again.",
|
|
5
|
+
"SettingService.ConfigurationPassphraseCancelled": "Configuration passphrase entry was cancelled or unavailable. Start-up was stopped; saved settings were retained.",
|
|
6
|
+
"SettingService.ConfigurationEncryptionSaveFailed": "Configuration encryption could not be saved. Verify the saved settings and device key before retrying.",
|
|
7
|
+
"SettingService.ConfigurationPassphraseRollbackFailed": "The configuration passphrase could not be restored after a write failure. Verify the saved settings and device key before retrying.",
|
|
8
|
+
"SettingService.ConfigurationCouldNotBeDecrypted": "Existing configuration could not be decrypted. The configuration key was not changed.",
|
|
2
9
|
"(BETA) Always overwrite with a newer file": "(BETA) Always overwrite with a newer file",
|
|
3
10
|
"(Beta) Use ignore files": "(Beta) Use ignore files",
|
|
4
11
|
"(Days passed, 0 to disable automatic-deletion)": "(Days passed, 0 to disable automatic-deletion)",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FetchHttpHandler } from "@smithy/fetch-http-handler";
|
|
2
2
|
import { type LOG_LEVEL } from "octagonal-wheels/common/logger";
|
|
3
|
-
import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakAssessment, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
|
|
3
|
+
import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, ConfigPassphraseStore, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakAssessment, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
|
|
4
4
|
import type { LiveSyncLocalDB } from "../../pouchdb/LiveSyncLocalDB.js";
|
|
5
5
|
import type { ReplicatorInstance } from "../../replication/ReplicatorInstance.js";
|
|
6
6
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
|
@@ -323,6 +323,8 @@ export interface ISettingService {
|
|
|
323
323
|
deleteDeviceLocalConfig(key: string): void;
|
|
324
324
|
onBeforeSaveSettingData(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings): Promise<(Partial<ObsidianLiveSyncSettings> | void)[]>;
|
|
325
325
|
saveSettingData(): Promise<void>;
|
|
326
|
+
/** Save a new configuration wrapper and its device-local key, retaining the protected values. */
|
|
327
|
+
changeConfigurationEncryption(store: ConfigPassphraseStore, passphrase?: string): Promise<void>;
|
|
326
328
|
currentSettings(): ObsidianLiveSyncSettings;
|
|
327
329
|
updateSettings(updateFn: (current: ObsidianLiveSyncSettings) => ObsidianLiveSyncSettings, saveImmediately?: boolean): Promise<void>;
|
|
328
330
|
applyExternalSettings(partial: Partial<ObsidianLiveSyncSettings>, saveImmediately?: boolean): Promise<void>;
|
|
@@ -226,7 +226,7 @@ class RemoteService extends ServiceBase {
|
|
|
226
226
|
const signal = connectionScope.combine(opts?.signal);
|
|
227
227
|
const response = await this.performFetch(
|
|
228
228
|
requestSrc,
|
|
229
|
-
{ ...opts, headers, signal },
|
|
229
|
+
{ ...opts, headers, signal, cache: "no-store" },
|
|
230
230
|
useRequestAPI ? FetchMethod.native : FetchMethod.webCompat
|
|
231
231
|
);
|
|
232
232
|
return response;
|
|
@@ -242,7 +242,11 @@ class RemoteService extends ServiceBase {
|
|
|
242
242
|
this._log(
|
|
243
243
|
"Failed to fetch by native fetch API. Trying to fetch by API to get more information."
|
|
244
244
|
);
|
|
245
|
-
const resp2 = await this.performFetch(
|
|
245
|
+
const resp2 = await this.performFetch(
|
|
246
|
+
requestSrc,
|
|
247
|
+
{ ...opts, headers, cache: "no-store" },
|
|
248
|
+
FetchMethod.native
|
|
249
|
+
);
|
|
246
250
|
if (resp2.status / 100 == 2) {
|
|
247
251
|
this.showError(
|
|
248
252
|
"The request was successful by API. But the native fetch API failed! Please check CORS settings on the remote database!. While this condition, you cannot enable LiveSync",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ObsidianLiveSyncSettings, type SettingsMigrationState } from "../../common/types.js";
|
|
1
|
+
import { type ConfigPassphraseStore, type ObsidianLiveSyncSettings, type SettingsMigrationState } from "../../common/types.js";
|
|
2
2
|
import type { IAPIService, ISettingService } from "./IService.js";
|
|
3
3
|
import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
|
|
4
4
|
import { createInstanceLogFunction } from "../lib/logUtils.js";
|
|
@@ -22,6 +22,7 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
22
22
|
protected abstract loadData(): Promise<ObsidianLiveSyncSettings | undefined>;
|
|
23
23
|
private _lastPersistedSettings?;
|
|
24
24
|
private _settingsMigrationState?;
|
|
25
|
+
private saveQueue;
|
|
25
26
|
getSettingsMigrationState(): SettingsMigrationState | undefined;
|
|
26
27
|
_log: ReturnType<typeof createInstanceLogFunction>;
|
|
27
28
|
constructor(context: T, dependencies: SettingServiceDependencies);
|
|
@@ -63,6 +64,14 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
63
64
|
* Save the current settings to storage.
|
|
64
65
|
*/
|
|
65
66
|
saveSettingData(): Promise<void>;
|
|
67
|
+
private serialiseSave;
|
|
68
|
+
/** Change the configuration wrapper without changing the protected plaintext values. */
|
|
69
|
+
changeConfigurationEncryption(store: ConfigPassphraseStore, passphrase?: string): Promise<void>;
|
|
70
|
+
private restoreConfigurationPassphrase;
|
|
71
|
+
private prepareConfigurationKeyChange;
|
|
72
|
+
private prepareSettingSave;
|
|
73
|
+
private writePreparedSettings;
|
|
74
|
+
private commitPreparedSettings;
|
|
66
75
|
private encryptRemoteConfigurationUris;
|
|
67
76
|
private hasManagedP2PProfileURI;
|
|
68
77
|
private decryptRemoteConfigurationUris;
|
|
@@ -104,13 +113,15 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
104
113
|
applyExternalSettings(partial: Partial<ObsidianLiveSyncSettings>, saveImmediately?: boolean): Promise<void>;
|
|
105
114
|
applyPartial(partial: Partial<ObsidianLiveSyncSettings>, saveImmediately?: boolean): Promise<void>;
|
|
106
115
|
getPassphrase(settings: ObsidianLiveSyncSettings): Promise<string | false>;
|
|
107
|
-
private usedPassphrase
|
|
116
|
+
private usedPassphrase?;
|
|
117
|
+
private getSavingPassphrase;
|
|
118
|
+
private rememberPassphrase;
|
|
108
119
|
/**
|
|
109
120
|
* Clear any used passphrase from memory.
|
|
110
121
|
*/
|
|
111
122
|
clearUsedPassphrase(): void;
|
|
112
123
|
decryptConfigurationItem(encrypted: string, passphrase: string): Promise<string | false>;
|
|
113
|
-
encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings): Promise<string>;
|
|
124
|
+
encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings, passphrase?: string): Promise<string>;
|
|
114
125
|
private encryptPlainConfigurationItem;
|
|
115
126
|
private encryptConfigurationItemWith;
|
|
116
127
|
/**
|
|
@@ -118,6 +129,11 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
118
129
|
* @param settings The settings to decrypt.
|
|
119
130
|
*/
|
|
120
131
|
decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings>;
|
|
132
|
+
private decryptSettingsWithPassphrase;
|
|
133
|
+
private hasProtectedConfiguration;
|
|
134
|
+
private validateStoredIdConfiguration;
|
|
135
|
+
private isPlainConfigurationUri;
|
|
136
|
+
private loadConfiguration;
|
|
121
137
|
loadSettings(): Promise<void>;
|
|
122
138
|
private tryDecodeJson;
|
|
123
139
|
private cloneSettings;
|
|
@@ -13,7 +13,12 @@ import { ServiceBase } from "./ServiceBase.js";
|
|
|
13
13
|
import { createInstanceLogFunction } from "../lib/logUtils.js";
|
|
14
14
|
import { isCloudantURI } from "../../pouchdb/utils_couchdb.js";
|
|
15
15
|
import { decryptString, encryptString } from "../../encryption/stringEncryption.js";
|
|
16
|
-
import { encryptWithEphemeralSalt } from "octagonal-wheels/encryption/hkdf";
|
|
16
|
+
import { encryptWithEphemeralSalt, HKDF_SALTED_ENCRYPTED_PREFIX } from "octagonal-wheels/encryption/hkdf";
|
|
17
|
+
import {
|
|
18
|
+
ENCRYPT_V1_PREFIX_PROBABLY,
|
|
19
|
+
ENCRYPT_V2_PREFIX,
|
|
20
|
+
ENCRYPT_V3_PREFIX
|
|
21
|
+
} from "octagonal-wheels/encryption/encryption";
|
|
17
22
|
import {
|
|
18
23
|
activateP2PRemoteConfiguration,
|
|
19
24
|
activateRemoteConfiguration,
|
|
@@ -31,6 +36,8 @@ import {
|
|
|
31
36
|
restoreConnectionSettings,
|
|
32
37
|
selectConnectionSettings
|
|
33
38
|
} from "../../common/models/setting.policy.js";
|
|
39
|
+
class ConfigurationPassphraseError extends Error {
|
|
40
|
+
}
|
|
34
41
|
class SettingService extends ServiceBase {
|
|
35
42
|
deviceAndVaultName = "";
|
|
36
43
|
APIService;
|
|
@@ -44,6 +51,7 @@ class SettingService extends ServiceBase {
|
|
|
44
51
|
}
|
|
45
52
|
_lastPersistedSettings;
|
|
46
53
|
_settingsMigrationState;
|
|
54
|
+
saveQueue = Promise.resolve();
|
|
47
55
|
getSettingsMigrationState() {
|
|
48
56
|
return this._settingsMigrationState;
|
|
49
57
|
}
|
|
@@ -152,31 +160,131 @@ class SettingService extends ServiceBase {
|
|
|
152
160
|
/**
|
|
153
161
|
* Save the current settings to storage.
|
|
154
162
|
*/
|
|
155
|
-
|
|
156
|
-
this.
|
|
163
|
+
saveSettingData() {
|
|
164
|
+
return this.serialiseSave(async () => {
|
|
165
|
+
const prepared = await this.prepareSettingSave(this.cloneSettings(this.settings));
|
|
166
|
+
await this.writePreparedSettings(prepared);
|
|
167
|
+
this.commitPreparedSettings(prepared);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
serialiseSave(operation) {
|
|
171
|
+
const result = this.saveQueue.then(operation);
|
|
172
|
+
this.saveQueue = result.catch(() => void 0);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/** Change the configuration wrapper without changing the protected plaintext values. */
|
|
176
|
+
changeConfigurationEncryption(store, passphrase) {
|
|
177
|
+
const result = this.serialiseSave(async () => {
|
|
178
|
+
if (store !== "" && store !== "LOCALSTORAGE" && store !== "ASK_AT_LAUNCH") {
|
|
179
|
+
throw new Error("The configuration passphrase storage mode is unsupported.");
|
|
180
|
+
}
|
|
181
|
+
const oldLocalKey = this.getDeviceLocalConfig("ls-setting-passphrase");
|
|
182
|
+
const settings = this.cloneSettings(this.settings);
|
|
183
|
+
let targetKey;
|
|
184
|
+
if (store === "LOCALSTORAGE") {
|
|
185
|
+
targetKey = passphrase ?? oldLocalKey ?? false;
|
|
186
|
+
} else if (store === "") {
|
|
187
|
+
targetKey = "*";
|
|
188
|
+
} else {
|
|
189
|
+
targetKey = await this.getSavingPassphrase({ ...settings, configPassphraseStore: store });
|
|
190
|
+
}
|
|
191
|
+
if (!targetKey) {
|
|
192
|
+
throw new Error("A configuration passphrase is required. Settings were not saved.");
|
|
193
|
+
}
|
|
194
|
+
await this.prepareConfigurationKeyChange(settings, targetKey);
|
|
195
|
+
settings.configPassphraseStore = store;
|
|
196
|
+
const prepared = await this.prepareSettingSave(settings, targetKey);
|
|
197
|
+
const changesLocalKey = store === "LOCALSTORAGE" && targetKey !== oldLocalKey;
|
|
198
|
+
try {
|
|
199
|
+
if (changesLocalKey) this.setDeviceLocalConfig("ls-setting-passphrase", targetKey);
|
|
200
|
+
await this.writePreparedSettings(prepared);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (changesLocalKey) this.restoreConfigurationPassphrase(oldLocalKey, error);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
this.settings.configPassphraseStore = store;
|
|
206
|
+
this.commitPreparedSettings(prepared);
|
|
207
|
+
});
|
|
208
|
+
return result.catch((error) => {
|
|
209
|
+
this._log(this.context.translate("SettingService.ConfigurationEncryptionSaveFailed"), LOG_LEVEL_URGENT);
|
|
210
|
+
throw error;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
restoreConfigurationPassphrase(previous, cause) {
|
|
214
|
+
try {
|
|
215
|
+
if (previous) this.setDeviceLocalConfig("ls-setting-passphrase", previous);
|
|
216
|
+
else this.deleteDeviceLocalConfig("ls-setting-passphrase");
|
|
217
|
+
} catch {
|
|
218
|
+
const message = this.context.translate("SettingService.ConfigurationPassphraseRollbackFailed");
|
|
219
|
+
this._log(message, LOG_LEVEL_URGENT);
|
|
220
|
+
throw new Error(message, { cause });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async prepareConfigurationKeyChange(settings, targetKey) {
|
|
224
|
+
for (const config of Object.values(settings.remoteConfigurations)) {
|
|
225
|
+
if (config.isEncrypted && this.isPlainConfigurationUri(config.uri)) config.isEncrypted = false;
|
|
226
|
+
}
|
|
227
|
+
const hasCiphertext = this.hasProtectedConfiguration(settings);
|
|
228
|
+
if (!hasCiphertext) return;
|
|
229
|
+
const oldKey = await this.getSavingPassphrase(settings);
|
|
230
|
+
const failure = () => new Error(this.context.translate("SettingService.ConfigurationCouldNotBeDecrypted"));
|
|
231
|
+
if (!oldKey) throw failure();
|
|
232
|
+
if (settings.encryptedCouchDBConnection) {
|
|
233
|
+
const connection = this.tryDecodeJson(
|
|
234
|
+
await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, oldKey)
|
|
235
|
+
);
|
|
236
|
+
if (!restoreConnectionSettings(this.cloneSettings(settings), connection)) throw failure();
|
|
237
|
+
settings.encryptedCouchDBConnection = "";
|
|
238
|
+
}
|
|
239
|
+
if (settings.encryptedPassphrase) {
|
|
240
|
+
const plaintext = await this.decryptConfigurationItem(settings.encryptedPassphrase, oldKey);
|
|
241
|
+
if (plaintext === false) throw failure();
|
|
242
|
+
settings.encryptedPassphrase = requireConfigurationCiphertext(
|
|
243
|
+
await this.encryptPlainConfigurationItem(plaintext, settings, targetKey)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (settings.idDerivationVersion === 1 && settings.encryptedIdDerivationKey) {
|
|
247
|
+
const idKey = await this.decryptConfigurationItem(settings.encryptedIdDerivationKey, oldKey);
|
|
248
|
+
if (idKey === false) throw failure();
|
|
249
|
+
configuredIdKey({ ...settings, idDerivationKey: idKey });
|
|
250
|
+
}
|
|
251
|
+
for (const config of Object.values(settings.remoteConfigurations)) {
|
|
252
|
+
if (!config.isEncrypted) continue;
|
|
253
|
+
const plaintext = await this.decryptConfigurationItem(config.uri, oldKey);
|
|
254
|
+
if (plaintext === false) {
|
|
255
|
+
try {
|
|
256
|
+
ConnectionStringParser.parse(config.uri);
|
|
257
|
+
} catch {
|
|
258
|
+
throw failure();
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
config.uri = plaintext;
|
|
262
|
+
}
|
|
263
|
+
config.isEncrypted = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async prepareSettingSave(settings, selectedPassphrase) {
|
|
157
267
|
const previousSettings = this._lastPersistedSettings ?? this.cloneSettings(this.settings);
|
|
158
|
-
const settings = this.cloneSettings(this.settings);
|
|
159
268
|
const hookResults = await this.onBeforeSaveSettingData(settings, previousSettings);
|
|
269
|
+
const savedPatch = {};
|
|
160
270
|
for (const patch of hookResults) {
|
|
161
271
|
if (patch instanceof Error || !patch) continue;
|
|
162
272
|
Object.assign(settings, patch);
|
|
163
|
-
Object.assign(
|
|
273
|
+
Object.assign(savedPatch, patch);
|
|
164
274
|
}
|
|
275
|
+
const runtime = this.cloneSettings(settings);
|
|
165
276
|
const idKey = configuredIdKey(settings);
|
|
277
|
+
const configurationPassphrase = selectedPassphrase ?? await this.getSavingPassphrase(settings);
|
|
166
278
|
if (settings.idDerivationVersion === 1) {
|
|
167
279
|
if (idKey === false) {
|
|
168
280
|
throw new Error("The configured ID derivation key is unavailable.");
|
|
169
281
|
}
|
|
170
|
-
|
|
171
|
-
if (passphrase === "") {
|
|
172
|
-
passphrase = await this.getPassphrase(settings);
|
|
173
|
-
}
|
|
174
|
-
if (passphrase === false || passphrase === "") {
|
|
282
|
+
if (!configurationPassphrase) {
|
|
175
283
|
const message = "Failed to retrieve a passphrase for the ID derivation key. Settings were not saved.";
|
|
176
284
|
this._log(message, LOG_LEVEL_URGENT);
|
|
177
285
|
throw new Error(message);
|
|
178
286
|
}
|
|
179
|
-
const encryptedIdKey = await encryptString(idKey,
|
|
287
|
+
const encryptedIdKey = await encryptString(idKey, configurationPassphrase + SALT_OF_PASSPHRASE);
|
|
180
288
|
if (encryptedIdKey === "") {
|
|
181
289
|
const message = "Failed to encrypt the ID derivation key. Settings were not saved.";
|
|
182
290
|
this._log(message, LOG_LEVEL_URGENT);
|
|
@@ -184,18 +292,14 @@ class SettingService extends ServiceBase {
|
|
|
184
292
|
}
|
|
185
293
|
settings.encryptedIdDerivationKey = encryptedIdKey;
|
|
186
294
|
settings.idDerivationKey = "";
|
|
187
|
-
this.usedPassphrase = passphrase;
|
|
188
295
|
} else {
|
|
189
296
|
settings.encryptedIdDerivationKey = "";
|
|
190
297
|
}
|
|
191
298
|
settings.deviceAndVaultName = "";
|
|
192
299
|
if (settings.P2P_DevicePeerName && settings.P2P_DevicePeerName.trim() !== "") {
|
|
193
|
-
this._log("Saving device peer name to small config");
|
|
194
|
-
this.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, settings.P2P_DevicePeerName.trim());
|
|
195
300
|
settings.P2P_DevicePeerName = "";
|
|
196
301
|
}
|
|
197
302
|
omitUnpersistedSettings(settings);
|
|
198
|
-
const configurationPassphrase = this.usedPassphrase || await this.getPassphrase(settings);
|
|
199
303
|
if (!configurationPassphrase) {
|
|
200
304
|
if (Object.values(settings.remoteConfigurations).some((config) => this.hasManagedP2PProfileURI(config.uri))) {
|
|
201
305
|
const message = "Failed to retrieve a passphrase for managed P2P source data. Settings were not saved.";
|
|
@@ -213,7 +317,11 @@ class SettingService extends ServiceBase {
|
|
|
213
317
|
} else {
|
|
214
318
|
if (hasConnectionSettings(settings)) {
|
|
215
319
|
settings.encryptedCouchDBConnection = requireConfigurationCiphertext(
|
|
216
|
-
await this.encryptConfigurationItem(
|
|
320
|
+
await this.encryptConfigurationItem(
|
|
321
|
+
JSON.stringify(selectConnectionSettings(settings)),
|
|
322
|
+
settings,
|
|
323
|
+
configurationPassphrase
|
|
324
|
+
)
|
|
217
325
|
);
|
|
218
326
|
clearConnectionSettings(settings);
|
|
219
327
|
} else if (settings.encryptedCouchDBConnection) {
|
|
@@ -226,18 +334,32 @@ class SettingService extends ServiceBase {
|
|
|
226
334
|
}
|
|
227
335
|
if (settings.encrypt && settings.passphrase != "") {
|
|
228
336
|
settings.encryptedPassphrase = requireConfigurationCiphertext(
|
|
229
|
-
await this.encryptPlainConfigurationItem(settings.passphrase, settings)
|
|
337
|
+
await this.encryptPlainConfigurationItem(settings.passphrase, settings, configurationPassphrase)
|
|
230
338
|
);
|
|
231
339
|
settings.passphrase = "";
|
|
232
340
|
}
|
|
233
|
-
await this.encryptRemoteConfigurationUris(settings);
|
|
341
|
+
await this.encryptRemoteConfigurationUris(settings, configurationPassphrase);
|
|
234
342
|
}
|
|
235
343
|
const persisted = prepareSettingsForPersistence(settings);
|
|
236
|
-
|
|
237
|
-
this._lastPersistedSettings = this.cloneSettings(this.settings);
|
|
238
|
-
void this.onSettingSaved(settings);
|
|
344
|
+
return { persisted, runtime, patch: savedPatch, passphrase: configurationPassphrase };
|
|
239
345
|
}
|
|
240
|
-
async
|
|
346
|
+
async writePreparedSettings(prepared) {
|
|
347
|
+
this.saveDeviceAndVaultName();
|
|
348
|
+
if (prepared.runtime.P2P_DevicePeerName?.trim()) {
|
|
349
|
+
this.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, prepared.runtime.P2P_DevicePeerName.trim());
|
|
350
|
+
}
|
|
351
|
+
await this.saveData(prepared.persisted);
|
|
352
|
+
}
|
|
353
|
+
commitPreparedSettings(prepared) {
|
|
354
|
+
Object.assign(this.settings, prepared.patch);
|
|
355
|
+
this.settings.encryptedCouchDBConnection = prepared.persisted.encryptedCouchDBConnection;
|
|
356
|
+
this.settings.encryptedPassphrase = prepared.persisted.encryptedPassphrase;
|
|
357
|
+
this.settings.encryptedIdDerivationKey = prepared.persisted.encryptedIdDerivationKey;
|
|
358
|
+
this._lastPersistedSettings = prepared.runtime;
|
|
359
|
+
if (prepared.passphrase) this.rememberPassphrase(prepared.runtime, prepared.passphrase);
|
|
360
|
+
void this.onSettingSaved(prepared.persisted);
|
|
361
|
+
}
|
|
362
|
+
async encryptRemoteConfigurationUris(settings, passphrase) {
|
|
241
363
|
const configs = settings.remoteConfigurations || {};
|
|
242
364
|
for (const [id, config] of Object.entries(configs)) {
|
|
243
365
|
if (config.isEncrypted || config.uri.trim() === "") {
|
|
@@ -246,7 +368,7 @@ class SettingService extends ServiceBase {
|
|
|
246
368
|
const managedP2PProfile = this.hasManagedP2PProfileURI(config.uri);
|
|
247
369
|
let encryptedURI;
|
|
248
370
|
try {
|
|
249
|
-
encryptedURI = await this.encryptConfigurationItem(config.uri, settings);
|
|
371
|
+
encryptedURI = await this.encryptConfigurationItem(config.uri, settings, passphrase);
|
|
250
372
|
} catch (error) {
|
|
251
373
|
if (managedP2PProfile) {
|
|
252
374
|
const message = `Failed to encrypt managed P2P remote configuration '${id}'. Settings were not saved.`;
|
|
@@ -288,6 +410,7 @@ class SettingService extends ServiceBase {
|
|
|
288
410
|
return (new URLSearchParams(query).get("managedType") ?? "").trim().length > 0;
|
|
289
411
|
}
|
|
290
412
|
async decryptRemoteConfigurationUris(settings, passphrase) {
|
|
413
|
+
let verified = false;
|
|
291
414
|
const configs = settings.remoteConfigurations || {};
|
|
292
415
|
for (const [id, config] of Object.entries(configs)) {
|
|
293
416
|
if (!config.isEncrypted) {
|
|
@@ -319,7 +442,9 @@ class SettingService extends ServiceBase {
|
|
|
319
442
|
uri: decryptedURI,
|
|
320
443
|
isEncrypted: false
|
|
321
444
|
};
|
|
445
|
+
verified = true;
|
|
322
446
|
}
|
|
447
|
+
return verified;
|
|
323
448
|
}
|
|
324
449
|
/**
|
|
325
450
|
* Event triggered before realising the settings.
|
|
@@ -404,24 +529,37 @@ class SettingService extends ServiceBase {
|
|
|
404
529
|
const methods = {
|
|
405
530
|
"": () => Promise.resolve("*"),
|
|
406
531
|
LOCALSTORAGE: () => Promise.resolve(this.getDeviceLocalConfig("ls-setting-passphrase") ?? false),
|
|
407
|
-
ASK_AT_LAUNCH: () => this.APIService.confirm.askString(
|
|
532
|
+
ASK_AT_LAUNCH: () => this.APIService.confirm.askString(
|
|
533
|
+
this.context.translate("SettingService.ConfigurationPassphraseTitle"),
|
|
534
|
+
"passphrase",
|
|
535
|
+
"",
|
|
536
|
+
true
|
|
537
|
+
)
|
|
408
538
|
};
|
|
409
539
|
const method = settings.configPassphraseStore;
|
|
410
540
|
const methodFunc = method in methods ? methods[method] : methods[""];
|
|
411
541
|
return methodFunc();
|
|
412
542
|
}
|
|
413
|
-
usedPassphrase
|
|
543
|
+
usedPassphrase;
|
|
544
|
+
getSavingPassphrase(settings) {
|
|
545
|
+
if (settings.configPassphraseStore === "ASK_AT_LAUNCH" && this.usedPassphrase?.store === "ASK_AT_LAUNCH") {
|
|
546
|
+
return Promise.resolve(this.usedPassphrase.value);
|
|
547
|
+
}
|
|
548
|
+
return this.getPassphrase(settings);
|
|
549
|
+
}
|
|
550
|
+
rememberPassphrase(settings, passphrase) {
|
|
551
|
+
this.usedPassphrase = { store: settings.configPassphraseStore, value: passphrase };
|
|
552
|
+
}
|
|
414
553
|
/**
|
|
415
554
|
* Clear any used passphrase from memory.
|
|
416
555
|
*/
|
|
417
556
|
clearUsedPassphrase() {
|
|
418
|
-
this.usedPassphrase =
|
|
557
|
+
this.usedPassphrase = void 0;
|
|
419
558
|
}
|
|
420
559
|
async decryptConfigurationItem(encrypted, passphrase) {
|
|
421
560
|
try {
|
|
422
561
|
const dec = await decryptString(encrypted, passphrase + SALT_OF_PASSPHRASE);
|
|
423
562
|
if (dec) {
|
|
424
|
-
this.usedPassphrase = passphrase;
|
|
425
563
|
return dec;
|
|
426
564
|
}
|
|
427
565
|
} catch (ex) {
|
|
@@ -430,18 +568,15 @@ class SettingService extends ServiceBase {
|
|
|
430
568
|
}
|
|
431
569
|
return false;
|
|
432
570
|
}
|
|
433
|
-
async encryptConfigurationItem(src, settings) {
|
|
434
|
-
return this.encryptConfigurationItemWith(src, settings, encryptString);
|
|
571
|
+
async encryptConfigurationItem(src, settings, passphrase) {
|
|
572
|
+
return this.encryptConfigurationItemWith(src, settings, encryptString, passphrase);
|
|
435
573
|
}
|
|
436
|
-
async encryptPlainConfigurationItem(src, settings) {
|
|
437
|
-
return this.encryptConfigurationItemWith(src, settings, encryptWithEphemeralSalt);
|
|
574
|
+
async encryptPlainConfigurationItem(src, settings, passphrase) {
|
|
575
|
+
return this.encryptConfigurationItemWith(src, settings, encryptWithEphemeralSalt, passphrase);
|
|
438
576
|
}
|
|
439
|
-
async encryptConfigurationItemWith(src, settings, encrypt) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
const passphrase = await this.getPassphrase(settings);
|
|
444
|
-
if (passphrase === false) {
|
|
577
|
+
async encryptConfigurationItemWith(src, settings, encrypt, selectedPassphrase) {
|
|
578
|
+
const passphrase = selectedPassphrase ?? await this.getSavingPassphrase(settings);
|
|
579
|
+
if (!passphrase) {
|
|
445
580
|
this._log(
|
|
446
581
|
"Failed to obtain passphrase when saving data.json! Please verify the configuration.",
|
|
447
582
|
LOG_LEVEL_URGENT
|
|
@@ -450,7 +585,7 @@ class SettingService extends ServiceBase {
|
|
|
450
585
|
}
|
|
451
586
|
const dec = await encrypt(src, passphrase + SALT_OF_PASSPHRASE);
|
|
452
587
|
if (dec) {
|
|
453
|
-
this.
|
|
588
|
+
if (selectedPassphrase === void 0) this.rememberPassphrase(settings, passphrase);
|
|
454
589
|
return dec;
|
|
455
590
|
}
|
|
456
591
|
return "";
|
|
@@ -460,19 +595,24 @@ class SettingService extends ServiceBase {
|
|
|
460
595
|
* @param settings The settings to decrypt.
|
|
461
596
|
*/
|
|
462
597
|
async decryptSettings(settings) {
|
|
463
|
-
|
|
464
|
-
configuredIdKey(settings);
|
|
465
|
-
} else if (!settings.encryptedIdDerivationKey) {
|
|
466
|
-
throw new Error("The configured ID derivation key is missing or unavailable.");
|
|
467
|
-
}
|
|
598
|
+
this.validateStoredIdConfiguration(settings);
|
|
468
599
|
const passphrase = await this.getPassphrase(settings);
|
|
600
|
+
const result = await this.decryptSettingsWithPassphrase(settings, passphrase);
|
|
601
|
+
if (result.verified && passphrase) this.rememberPassphrase(settings, passphrase);
|
|
602
|
+
return result.settings;
|
|
603
|
+
}
|
|
604
|
+
async decryptSettingsWithPassphrase(settings, passphrase) {
|
|
605
|
+
let verified = false;
|
|
606
|
+
this.validateStoredIdConfiguration(settings);
|
|
469
607
|
if (settings.idDerivationVersion === 1) {
|
|
470
608
|
if (passphrase === false || passphrase === "") {
|
|
471
|
-
throw new
|
|
609
|
+
throw new ConfigurationPassphraseError(
|
|
610
|
+
"The configured ID derivation key cannot be decrypted without a passphrase."
|
|
611
|
+
);
|
|
472
612
|
}
|
|
473
613
|
const decryptedIdKey = await this.decryptConfigurationItem(settings.encryptedIdDerivationKey, passphrase);
|
|
474
614
|
if (decryptedIdKey === false) {
|
|
475
|
-
throw new
|
|
615
|
+
throw new ConfigurationPassphraseError("The configured ID derivation key could not be decrypted.");
|
|
476
616
|
}
|
|
477
617
|
try {
|
|
478
618
|
configuredIdKey({ ...settings, idDerivationKey: decryptedIdKey });
|
|
@@ -480,6 +620,7 @@ class SettingService extends ServiceBase {
|
|
|
480
620
|
throw new Error("The configured ID derivation key is invalid.");
|
|
481
621
|
}
|
|
482
622
|
settings.idDerivationKey = decryptedIdKey;
|
|
623
|
+
verified = true;
|
|
483
624
|
}
|
|
484
625
|
if (passphrase === false) {
|
|
485
626
|
this._log("No passphrase found for data.json! Verify configuration before syncing.", LOG_LEVEL_URGENT);
|
|
@@ -503,6 +644,8 @@ class SettingService extends ServiceBase {
|
|
|
503
644
|
LOG_LEVEL_URGENT
|
|
504
645
|
);
|
|
505
646
|
clearConnectionSettings(settings);
|
|
647
|
+
} else {
|
|
648
|
+
verified = true;
|
|
506
649
|
}
|
|
507
650
|
}
|
|
508
651
|
if (settings.encrypt && settings.encryptedPassphrase) {
|
|
@@ -510,6 +653,7 @@ class SettingService extends ServiceBase {
|
|
|
510
653
|
const decrypted = await this.decryptConfigurationItem(encrypted, passphrase);
|
|
511
654
|
if (decrypted) {
|
|
512
655
|
settings.passphrase = decrypted;
|
|
656
|
+
verified = true;
|
|
513
657
|
} else {
|
|
514
658
|
this._log(
|
|
515
659
|
"Failed to decrypt passphrase from data.json! Ensure configuration is correct before syncing with remote.",
|
|
@@ -518,9 +662,80 @@ class SettingService extends ServiceBase {
|
|
|
518
662
|
settings.passphrase = "";
|
|
519
663
|
}
|
|
520
664
|
}
|
|
521
|
-
await this.decryptRemoteConfigurationUris(settings, passphrase);
|
|
665
|
+
verified = await this.decryptRemoteConfigurationUris(settings, passphrase) || verified;
|
|
666
|
+
}
|
|
667
|
+
return { settings, verified };
|
|
668
|
+
}
|
|
669
|
+
hasProtectedConfiguration(settings) {
|
|
670
|
+
return !!(settings.encryptedCouchDBConnection || settings.encrypt && settings.encryptedPassphrase || settings.encryptedIdDerivationKey || Object.values(settings.remoteConfigurations || {}).some(
|
|
671
|
+
(config) => config.isEncrypted && !this.isPlainConfigurationUri(config.uri)
|
|
672
|
+
));
|
|
673
|
+
}
|
|
674
|
+
validateStoredIdConfiguration(settings) {
|
|
675
|
+
if (settings.idDerivationVersion !== 1) {
|
|
676
|
+
configuredIdKey(settings);
|
|
677
|
+
} else if (!settings.encryptedIdDerivationKey) {
|
|
678
|
+
throw new Error("The configured ID derivation key is missing or unavailable.");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
isPlainConfigurationUri(uri) {
|
|
682
|
+
try {
|
|
683
|
+
ConnectionStringParser.parse(uri);
|
|
684
|
+
return true;
|
|
685
|
+
} catch {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async loadConfiguration(settings) {
|
|
690
|
+
if (settings.configPassphraseStore !== "LOCALSTORAGE" || !this.hasProtectedConfiguration(settings)) {
|
|
691
|
+
return this.decryptSettings(settings);
|
|
692
|
+
}
|
|
693
|
+
if (settings.idDerivationVersion === 1) {
|
|
694
|
+
const ciphertext = settings.encryptedIdDerivationKey;
|
|
695
|
+
if (![HKDF_SALTED_ENCRYPTED_PREFIX, ENCRYPT_V1_PREFIX_PROBABLY, ENCRYPT_V2_PREFIX, ENCRYPT_V3_PREFIX].some(
|
|
696
|
+
(prefix) => ciphertext?.startsWith(prefix)
|
|
697
|
+
)) {
|
|
698
|
+
throw new Error("The configured ID derivation key is missing or unavailable.");
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
configuredIdKey(settings);
|
|
702
|
+
}
|
|
703
|
+
const previous = this.getDeviceLocalConfig("ls-setting-passphrase");
|
|
704
|
+
let candidate = previous || false;
|
|
705
|
+
let retry = false;
|
|
706
|
+
while (true) {
|
|
707
|
+
if (candidate) {
|
|
708
|
+
try {
|
|
709
|
+
const result = await this.decryptSettingsWithPassphrase(this.cloneSettings(settings), candidate);
|
|
710
|
+
if (result.verified) {
|
|
711
|
+
if (candidate !== previous) {
|
|
712
|
+
try {
|
|
713
|
+
this.setDeviceLocalConfig("ls-setting-passphrase", candidate);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
this.restoreConfigurationPassphrase(previous, error);
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
this.rememberPassphrase(settings, candidate);
|
|
720
|
+
return result.settings;
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (!(error instanceof ConfigurationPassphraseError)) throw error;
|
|
724
|
+
}
|
|
725
|
+
retry = true;
|
|
726
|
+
}
|
|
727
|
+
candidate = await this.APIService.confirm.askString(
|
|
728
|
+
this.context.translate(
|
|
729
|
+
retry ? "SettingService.ConfigurationPassphraseRetryTitle" : "SettingService.ConfigurationPassphraseTitle"
|
|
730
|
+
),
|
|
731
|
+
"passphrase",
|
|
732
|
+
"",
|
|
733
|
+
true
|
|
734
|
+
);
|
|
735
|
+
if (!candidate) {
|
|
736
|
+
throw new Error(this.context.translate("SettingService.ConfigurationPassphraseCancelled"));
|
|
737
|
+
}
|
|
522
738
|
}
|
|
523
|
-
return settings;
|
|
524
739
|
}
|
|
525
740
|
async loadSettings() {
|
|
526
741
|
const prepared = prepareSettingsForLoad(await this.loadData());
|
|
@@ -536,7 +751,7 @@ class SettingService extends ServiceBase {
|
|
|
536
751
|
settings.isConfigured = false;
|
|
537
752
|
}
|
|
538
753
|
}
|
|
539
|
-
this.settings = await this.
|
|
754
|
+
this.settings = await this.loadConfiguration(settings);
|
|
540
755
|
this.onDisplayLanguageChanged?.(this.settings.displayLanguage);
|
|
541
756
|
await this.adjustSettings(this.settings);
|
|
542
757
|
const migratedLegacyRemoteConfigurations = !hadRemoteConfigurations && Object.keys(this.settings.remoteConfigurations ?? {}).length > 0;
|
package/docs/proven-in-use.md
CHANGED
|
@@ -8,6 +8,10 @@ The code paths below identify the maintained Self-hosted LiveSync composition po
|
|
|
8
8
|
|
|
9
9
|
## Obsidian plug-in
|
|
10
10
|
|
|
11
|
+
Unreleased configuration-encryption tests cover changing the wrapper key without changing connection, E2EE, profile, or independent ID values. They also cover custom-key input before loading, failed writes, concurrent saves, cancellation, and unreadable-payload retention. Commonlib owns these service results. LiveSync owns the Apply controls, setting-tab registration, and database startup order; actual Apply and restart behaviour requires its focused real-Obsidian scenario.
|
|
12
|
+
|
|
13
|
+
The CouchDB connection fix bypasses browser HTTP caching so encrypted replication can continue after a remote Rebuild preserves document IDs and revisions. Commonlib's regression tests cover Metadata and Chunks encrypted with a replacement Security Seed while an earlier response remains cached. Self-hosted LiveSync's `couchdb-http-cache` scenario additionally populates the real browser HTTP cache before rebuilding, confirms that the Seed and ciphertext change while the revision remains unchanged, and requires subsequent replication to succeed without clearing that cache. The released LiveSync 1.0.34 fails this scenario; a local checked Commonlib artefact passes in the maintained LiveSync 1.0.34 composition on Linux Obsidian 1.12.7. This establishes the tested build's behaviour, with the reporter's Windows and Obsidian 1.13.7 environment still unverified.
|
|
14
|
+
|
|
11
15
|
Unreleased settings tests verify that Object Storage-only credentials, JWT values, and authentication headers reach the host persistence handler only after connection encryption, and that unavailable or failed required encryption preserves the previous persisted settings. The focused `/settings` entry also supplies conditional whole-profile Markdown preparation and import preservation. A clean packed consumer checks the exhaustive property policy and prepared output types with an ES2018 compile fixture. Commonlib owns these result and type contracts; Self-hosted LiveSync owns Markdown file writes, its settings interface, and restoration through a real Obsidian restart.
|
|
12
16
|
|
|
13
17
|
Unreleased colon-path regression tests establish that Commonlib preserves ordinary file paths through Metadata storage, Chunk retrieval, and storage reflection, with path obfuscation enabled or disabled. They use real PouchDB with injected storage. This result does not establish which filenames a host's file-creation API accepts; that boundary requires host runtime verification.
|
|
@@ -74,6 +74,10 @@ The standard Commonlib `SettingService` performs this preparation automatically.
|
|
|
74
74
|
|
|
75
75
|
## Preparing settings for persistence and Markdown
|
|
76
76
|
|
|
77
|
+
The existing `SettingService` provides `changeConfigurationEncryption(store, passphrase?)` for hosts which edit the configuration encryption mode or device-local custom key. Await it before marking those edits as saved. It prepares one wrapper key for connection credentials, the E2EE passphrase, remote profile URIs, and the independent ID key, and serialises the change with ordinary settings saves. It rejects empty keys and unreadable existing protected items. A failed write attempts to restore the previous device key and does not send a success event. The device key and settings document use separate host writes, so this is not a guarantee of rollback after process termination or an uncertain persistence error.
|
|
78
|
+
|
|
79
|
+
When `loadSettings()` cannot validate a custom configuration key, it requests a masked key through the host's existing `confirm.askString` capability. It validates each candidate on a fresh copy and stores only a validated key before continuing initialisation. Cancellation or unavailable input stops that load without changing the saved settings. The ID-key rejection contract remains in `decryptSettings()`; loading never replaces a missing or invalid ID key with another key. ASK mode retains one successful in-memory key for ordinary saves in that launch. Hosts own the visible prompt, cancellation on disposal, and the policy which prevents database and replication work before loading completes.
|
|
80
|
+
|
|
77
81
|
`SettingService` prepares a separate settings object before calling the host's persistence handler. Object Storage credentials, CouchDB credentials, JWT values, and authentication headers share the existing `encryptedCouchDBConnection` payload. Encryption runs when any protected connection property is present, including when every CouchDB property is empty. The same policy supplies the properties encrypted, cleared before saving, and restored on loading. Older plaintext settings and encrypted payloads without newer properties remain readable.
|
|
78
82
|
|
|
79
83
|
An unavailable configuration passphrase or an empty connection-encryption result stops the save before the host's persistence handler receives the object. Remote profile URI encryption also stops on failure when a configuration passphrase is explicitly selected. The previous persisted settings remain intact. Removing all connection values clears an older encrypted payload only after it can be decrypted, preserving recovery after a failed decryption. Profile names, IDs, and selections remain ordinary settings; profile URIs are encrypted separately.
|
|
@@ -116,3 +120,5 @@ The settings schema version is independent of the package version and of any rem
|
|
|
116
120
|
The settings lifecycle unit tests cover blank stores, representative legacy choices, migration idempotence, legacy file-name case normalisation, legacy review state, future-schema downgrade protection, and the distinction between new-Vault recommendations and stored-setting fallbacks. Credential tests cover Object Storage-only persistence, JWT and header restoration, legacy settings, removed credentials, encryption failures, conditional whole-profile Markdown export, and preservation of local values on import. Tweak-assessment tests cover effective defaults, unadvertised values, directional change sets, reconstruction impact, raw representation differences, immutable snapshots, and document-ID case behaviour. The packed-package test imports the focused `/settings` entry from a clean consumer and checks its declarations and runtime exports. Its ES2018 compile fixture checks missing policies and attempts to bypass prepared output types.
|
|
117
121
|
|
|
118
122
|
Hosts remain responsible for testing when setup is classified as new, how local review acknowledgement is stored, how replication is gated, and how their settings interface presents the result.
|
|
123
|
+
|
|
124
|
+
`SettingService.configurationEncryption.unit.spec.ts` covers key changes with ID versions 0 and 1, stale-cache avoidance, per-launch ASK reuse, concurrent saves, preparation and persistence errors, rollback reporting, custom-key input and cancellation, and retention of unreadable items. These are Commonlib-owned service contracts; Apply controls and real application restarts require downstream verification.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrtmrz/livesync-commonlib",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -1010,7 +1010,7 @@
|
|
|
1010
1010
|
"idb": "^8.0.3",
|
|
1011
1011
|
"markdown-it": "^14.2.0",
|
|
1012
1012
|
"minimatch": "^10.2.5",
|
|
1013
|
-
"octagonal-wheels": "^0.1.
|
|
1013
|
+
"octagonal-wheels": "^0.1.55",
|
|
1014
1014
|
"pouchdb-adapter-http": "^9.0.0",
|
|
1015
1015
|
"pouchdb-adapter-idb": "^9.0.0",
|
|
1016
1016
|
"pouchdb-adapter-indexeddb": "^9.0.0",
|