@vrtmrz/livesync-commonlib 0.1.24 → 0.1.26
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/API/processSetting.d.ts +1 -1
- package/dist/API/processSetting.js +10 -6
- package/dist/common/ConnectionString.js +15 -8
- package/dist/common/models/setting.const.qr.js +5 -0
- package/dist/common/models/setting.lifecycle.js +5 -3
- package/dist/common/models/setting.p2p.d.ts +10 -0
- package/dist/common/models/setting.p2p.js +13 -0
- package/dist/common/models/setting.type.d.ts +10 -0
- package/dist/common/models/tweak.definition.d.ts +5 -0
- package/dist/common/settingConstants.d.ts +5 -0
- package/dist/common/types.d.ts +3 -3
- package/dist/common/types.js +6 -0
- package/dist/common/utils.js +4 -1
- package/dist/interfaces/DatabaseFileAccess.d.ts +31 -3
- package/dist/interfaces/FileReflectionProvenance.d.ts +2 -2
- package/dist/managers/EntryManager/EntryManager.d.ts +4 -2
- package/dist/managers/EntryManager/EntryManager.js +9 -4
- package/dist/managers/EntryManager/EntryManagerImpls.d.ts +12 -4
- package/dist/managers/EntryManager/EntryManagerImpls.js +27 -9
- package/dist/p2p/P2PService.d.ts +7 -1
- package/dist/p2p/P2PService.js +13 -7
- package/dist/p2p/index.d.ts +3 -1
- package/dist/p2p/index.js +6 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +4 -2
- package/dist/pouchdb/LiveSyncLocalDB.js +8 -4
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +22 -1
- package/dist/replication/trystero/P2PRoomSessionOwner.js +193 -13
- package/dist/replication/trystero/TrysteroReplicator.js +9 -3
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +1 -0
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +10 -3
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +6 -3
- package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +6 -1
- package/dist/replication/trystero/useP2PReplicatorFeature.js +7 -4
- package/dist/rpc/transports/trysteroUtils.d.ts +2 -2
- package/dist/rpc/transports/trysteroUtils.js +32 -2
- package/dist/serviceFeatures/remoteConfig.js +3 -0
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.d.ts +26 -2
- package/dist/serviceModules/ServiceDatabaseFileAccessBase.js +57 -5
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +119 -1
- package/dist/serviceModules/ServiceFileHandlerBase.js +450 -70
- package/dist/services/base/SettingService.d.ts +1 -0
- package/dist/services/base/SettingService.js +37 -9
- package/docs/conflict-resolution.md +28 -15
- package/docs/development.md +3 -3
- package/docs/p2p-transport-lifecycle.md +86 -4
- package/docs/proven-in-use.md +31 -1
- package/package.json +1 -1
|
@@ -63,6 +63,7 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
63
63
|
*/
|
|
64
64
|
saveSettingData(): Promise<void>;
|
|
65
65
|
private encryptRemoteConfigurationUris;
|
|
66
|
+
private hasManagedP2PProfileURI;
|
|
66
67
|
private decryptRemoteConfigurationUris;
|
|
67
68
|
/**
|
|
68
69
|
* Event triggered before realising the settings.
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
LOG_LEVEL_VERBOSE,
|
|
6
6
|
SALT_OF_PASSPHRASE,
|
|
7
7
|
SETTING_KEY_P2P_DEVICE_NAME,
|
|
8
|
+
omitP2PRuntimeSettings,
|
|
8
9
|
prepareSettingsForLoad
|
|
9
10
|
} from "../../common/types.js";
|
|
10
11
|
import { handlers } from "../lib/HandlerUtils.js";
|
|
@@ -143,12 +144,7 @@ class SettingService extends ServiceBase {
|
|
|
143
144
|
async saveSettingData() {
|
|
144
145
|
this.saveDeviceAndVaultName();
|
|
145
146
|
const previousSettings = this._lastPersistedSettings ?? this.cloneSettings(this.settings);
|
|
146
|
-
const settings =
|
|
147
|
-
...this.settings,
|
|
148
|
-
remoteConfigurations: Object.fromEntries(
|
|
149
|
-
Object.entries(this.settings.remoteConfigurations || {}).map(([id, config]) => [id, { ...config }])
|
|
150
|
-
)
|
|
151
|
-
};
|
|
147
|
+
const settings = this.cloneSettings(this.settings);
|
|
152
148
|
const hookResults = await this.onBeforeSaveSettingData(settings, previousSettings);
|
|
153
149
|
for (const patch of hookResults) {
|
|
154
150
|
if (patch instanceof Error || !patch) continue;
|
|
@@ -161,7 +157,15 @@ class SettingService extends ServiceBase {
|
|
|
161
157
|
this.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, settings.P2P_DevicePeerName.trim());
|
|
162
158
|
settings.P2P_DevicePeerName = "";
|
|
163
159
|
}
|
|
160
|
+
delete settings.P2P_managedType;
|
|
161
|
+
delete settings.P2P_managedId;
|
|
162
|
+
delete settings.P2P_managedToken;
|
|
164
163
|
if (this.usedPassphrase == "" && !await this.getPassphrase(settings)) {
|
|
164
|
+
if (Object.values(settings.remoteConfigurations).some((config) => this.hasManagedP2PProfileURI(config.uri))) {
|
|
165
|
+
const message = "Failed to retrieve a passphrase for managed P2P source data. Settings were not saved.";
|
|
166
|
+
this._log(message, LOG_LEVEL_URGENT);
|
|
167
|
+
throw new Error(message);
|
|
168
|
+
}
|
|
165
169
|
this._log("Failed to retrieve passphrase. data.json contains unencrypted items!", LOG_LEVEL_NOTICE);
|
|
166
170
|
} else {
|
|
167
171
|
if (settings.couchDB_PASSWORD != "" || settings.couchDB_URI != "" || settings.couchDB_USER != "" || settings.couchDB_DBNAME) {
|
|
@@ -218,8 +222,24 @@ class SettingService extends ServiceBase {
|
|
|
218
222
|
if (config.isEncrypted || config.uri.trim() === "") {
|
|
219
223
|
continue;
|
|
220
224
|
}
|
|
221
|
-
const
|
|
225
|
+
const managedP2PProfile = this.hasManagedP2PProfileURI(config.uri);
|
|
226
|
+
let encryptedURI;
|
|
227
|
+
try {
|
|
228
|
+
encryptedURI = await this.encryptConfigurationItem(config.uri, settings);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (managedP2PProfile) {
|
|
231
|
+
const message = `Failed to encrypt managed P2P remote configuration '${id}'. Settings were not saved.`;
|
|
232
|
+
this._log(message, LOG_LEVEL_URGENT);
|
|
233
|
+
throw new Error(message);
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
222
237
|
if (encryptedURI === "") {
|
|
238
|
+
if (managedP2PProfile) {
|
|
239
|
+
const message = `Failed to encrypt managed P2P remote configuration '${id}'. Settings were not saved.`;
|
|
240
|
+
this._log(message, LOG_LEVEL_URGENT);
|
|
241
|
+
throw new Error(message);
|
|
242
|
+
}
|
|
223
243
|
this._log(
|
|
224
244
|
`Failed to encrypt remote configuration '${id}'. This entry will be saved in plain text.`,
|
|
225
245
|
LOG_LEVEL_URGENT
|
|
@@ -233,6 +253,14 @@ class SettingService extends ServiceBase {
|
|
|
233
253
|
};
|
|
234
254
|
}
|
|
235
255
|
}
|
|
256
|
+
hasManagedP2PProfileURI(uri) {
|
|
257
|
+
const trimmed = uri.trim();
|
|
258
|
+
if (!trimmed.startsWith("sls+p2p://")) return false;
|
|
259
|
+
const queryStart = trimmed.indexOf("?");
|
|
260
|
+
if (queryStart < 0) return false;
|
|
261
|
+
const query = trimmed.slice(queryStart + 1).split("#", 1)[0];
|
|
262
|
+
return (new URLSearchParams(query).get("managedType") ?? "").trim().length > 0;
|
|
263
|
+
}
|
|
236
264
|
async decryptRemoteConfigurationUris(settings, passphrase) {
|
|
237
265
|
const configs = settings.remoteConfigurations || {};
|
|
238
266
|
for (const [id, config] of Object.entries(configs)) {
|
|
@@ -321,7 +349,7 @@ class SettingService extends ServiceBase {
|
|
|
321
349
|
try {
|
|
322
350
|
this.settings = await this.adjustSettings({
|
|
323
351
|
...this.settings,
|
|
324
|
-
...partial
|
|
352
|
+
...omitP2PRuntimeSettings(partial)
|
|
325
353
|
});
|
|
326
354
|
} catch (ex) {
|
|
327
355
|
this._log("Error in applying external settings: ", LOG_LEVEL_URGENT);
|
|
@@ -550,7 +578,7 @@ class SettingService extends ServiceBase {
|
|
|
550
578
|
}
|
|
551
579
|
cloneSettings(settings) {
|
|
552
580
|
return {
|
|
553
|
-
...settings,
|
|
581
|
+
...omitP2PRuntimeSettings(settings),
|
|
554
582
|
remoteConfigurations: Object.fromEntries(
|
|
555
583
|
Object.entries(settings.remoteConfigurations || {}).map(([id, config]) => [id, { ...config }])
|
|
556
584
|
)
|
|
@@ -31,13 +31,14 @@ Commonlib follows these rules:
|
|
|
31
31
|
- compare file content byte-for-byte; a path, size, modification time, or revision generation is not proof of identity;
|
|
32
32
|
- use the nearest `available` revision shared by both leaves as the base for three-way merge;
|
|
33
33
|
- retain a manual conflict when the common revision or a required body is missing or compacted;
|
|
34
|
-
-
|
|
35
|
-
-
|
|
34
|
+
- recognise an unchanged file by comparing its bytes with its exact readable file-reflection provenance;
|
|
35
|
+
- save a genuine edit as a child of that recorded revision, even when a newer database winner exists;
|
|
36
|
+
- deduplicate unknown-origin content against current non-deleted leaves, preserving differing content as a fresh independent root under the same document ID; and
|
|
36
37
|
- never select the newest modification time as a package-level default.
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Ordinary saving and incoming reflection share this classification. A historical byte match alone cannot distinguish an unchanged stale file from an intentional revert, and a prefix or suffix relationship does not prove which version the user edited. Unknown-origin content must not acquire an inferred parent from the current winner or its parent.
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
After a resolution has propagated, an exact recorded revision can still identify unchanged content below a deleted losing leaf. The resolved winner may replace those bytes under the existing reflection policy. If the recorded body is unavailable, the file follows the conservative unknown-origin path. General history-query helpers remain available for callers with separate contracts, including existing deletion and rename recovery; ordinary save and overwrite protection do not scan all historical bodies.
|
|
41
42
|
|
|
42
43
|
## Resolution classes
|
|
43
44
|
|
|
@@ -47,8 +48,10 @@ This check matters after a resolution has propagated. If a receiving device stil
|
|
|
47
48
|
| Text or structured data has a shared available base and non-overlapping changes | Perform a conservative three-way merge. |
|
|
48
49
|
| One side deletes content which the other leaves unchanged | Preserve the deletion. |
|
|
49
50
|
| One side deletes content which the other modifies | Retain a manual conflict. |
|
|
50
|
-
| A receiving file matches
|
|
51
|
-
| A receiving file
|
|
51
|
+
| A receiving file matches its exact readable recorded revision | Apply the propagated result under the existing conflict policy. |
|
|
52
|
+
| A receiving file differs from its readable recorded revision | Preserve the edit as a child of that revision. |
|
|
53
|
+
| Origin is unknown and no current non-deleted leaf matches | Preserve a fresh independent root for host conflict resolution. |
|
|
54
|
+
| Origin is unknown and current non-deleted leaves already contain the bytes | Avoid duplicate storage; infer provenance only for a unique match. |
|
|
52
55
|
| A body or common ancestor is missing or compacted | Retain a manual conflict. |
|
|
53
56
|
| Content is binary or otherwise not semantically mergeable | Leave the selection policy to the host; Commonlib cannot infer user intent. |
|
|
54
57
|
|
|
@@ -74,19 +77,29 @@ The compatibility implementation accepts an injected, device-local `FileReflecti
|
|
|
74
77
|
path -> { revision, observedStorageMtime? }
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
`revision` is the exact database revision
|
|
80
|
+
`revision` is the exact database revision most recently saved from or reflected in this device's storage. It identifies the base for subsequent local edits, rather than certifying that the current file still contains those bytes. `observedStorageMtime` is the raw modification time of the saved snapshot or the file observed after reflection. It is neither rounded nor compared across devices, and it is diagnostic only: revision identity never comes from a timestamp, path, size, or content hash.
|
|
78
81
|
|
|
79
82
|
The record is updated only after a successful database-to-storage reflection or storage-to-database write. A plain read does not change provenance. A record remains authoritative when a user edits the displayed file to bytes which happen to equal another branch; content equality must not silently change the branch being extended.
|
|
80
83
|
|
|
84
|
+
File-handler saves and reflections share a lock for the same Metadata document, held from snapshot acquisition through provenance recording. Different documents can proceed concurrently. An ordinary save keeps one captured body and its recorded base through the database write; a later local edit is processed as a subsequent change, without rereading the file to certify that it remained unchanged. The handler acquires the lock before loading a file body from storage. Rename operations acquire both document locks in a consistent order, and internal calls reuse the caller's lock rather than acquiring it again. Host conflict-check hooks run after the locks are released, allowing a hook to await another file-handler operation on the same document. Host queues may count document-lock waiters against their concurrency limits, so a burst for one document can temporarily delay unrelated files.
|
|
85
|
+
|
|
86
|
+
These locks order Commonlib file-handler operations; they do not stop user edits, external filesystem writes, or database replication. Incoming overwrite and deletion protection therefore still checks current storage. Restored storage events also retain bounded rechecks because they finish before the file watcher starts and cannot depend on a subsequent change notification.
|
|
87
|
+
|
|
81
88
|
User-directed reconciliation may select any exact current live revision, including a conflict leaf rather than the deterministic winner. `dbToStorageWithSpecificRev()` rechecks that the selected revision is still live before reflecting its content. `storeFileToDBWithBaseRevision()` performs the same live check before storing current storage content as a child of that revision. If the content already matches the selected non-deleted revision, it records that revision directly without creating a child. A host can set `createIfDifferent` to `false` when it intends only to record an exact content match; differing content then fails instead of changing the revision tree. A host may also create a logical deletion on an explicitly selected branch with `deleteRevisionFromDB()` after performing its own current-live-revision check. This leaves storage unchanged and clears device-local provenance only when that record names the deleted revision.
|
|
82
89
|
|
|
83
90
|
`storeWithBaseRevision()` is the force-write primitive for deliberately preserving a branch below an exact revision, including a revision which is no longer a leaf. It must not be used when an operation means 'advance this revision only if it is still current'. For that compare-and-store boundary, `storeWithLiveBaseRevision()` uses PouchDB's ordinary revision check and returns `false` if the supplied base has already been advanced. Any current revision-tree leaf is eligible, including a non-winning conflict leaf or a logical-deletion leaf. The caller must refresh its state after refusal rather than silently falling back to the deterministic winner.
|
|
84
91
|
|
|
92
|
+
`storeIndependentRevision()` explicitly preserves unknown-origin content through `putDBEntryAsIndependentRoot()`. Passing an undefined base to `storeWithBaseRevision()` retains its existing latest-revision behaviour and does not create an independent root. `findLiveContentRevisions()` compares only current non-deleted leaves using locally available chunks. Additional provenance comparisons also use local-only retrieval, so they do not introduce remote chunk requests or delivery waits; a missing body leads to preservation rather than an inferred base.
|
|
93
|
+
|
|
85
94
|
A successful non-deleted reconciliation records the exact selected or newly created revision as provenance. Applying a logical deletion removes the storage item and its provenance record. Deletion provenance is not retained indefinitely: an absent storage item and a current logical-deletion winner already agree and need no further reconciliation.
|
|
86
95
|
|
|
87
96
|
Hosts may construct the namespaced store handle during service composition, before its backing database is open. Store operations begin only after the host's storage lifecycle is ready. They fail rather than wait when that lifecycle contract is violated, because an implicit readiness wait could hang after failed initialisation or wait on its own initialisation handler. Database reset is a transient unavailable boundary; the host must avoid file processing during it and reconstruct derived provenance after reopening.
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
For ordinary saving and incoming reflection, an absent or unreadable recorded base permits reconstruction only from exactly one matching current non-deleted leaf. Multiple matches avoid duplicate storage without guessing branch identity. No current match creates a fresh independent root, even when an ancestor has the same bytes. Deletion and rename retain their existing provenance-recovery contracts.
|
|
99
|
+
|
|
100
|
+
An unchanged stale file is not saved below the winner. When the database has advanced without a conflict, the existing database-to-storage path reflects it; a conflict instead remains subject to the host's reflection and resolution policy. Independent roots retain the file's modification time and use the existing Metadata/Chunks format, with fresh revision identifiers and reusable chunks. They have no shared ancestor with unrelated existing branches, including after replication. This does not disable a host's explicit newer-file option.
|
|
101
|
+
|
|
102
|
+
Saving and reflection revalidate captured file content and provenance across asynchronous work. Restored saves reconcile changed storage directly, because host file watchers may not yet be registered. Retries are bounded; a file which keeps changing causes the operation to fail rather than report an uncompleted save as successful. These checks do not make external filesystem edits atomic with a host write: the storage contract has no conditional write operation covering that final interval.
|
|
90
103
|
|
|
91
104
|
## Operations while a conflict exists
|
|
92
105
|
|
|
@@ -99,7 +112,7 @@ With a proven displayed revision:
|
|
|
99
112
|
|
|
100
113
|
When edit provenance cannot be reconstructed, the new bytes are preserved as another manual-resolution branch instead of being attached silently to the deterministic winner. A deletion has no remaining file body from which to reconstruct provenance, so an unproven deletion preserves every live branch and requests conflict review. A cross-path rename with an unproven source keeps the newly stored target and preserves every source branch for review.
|
|
101
114
|
|
|
102
|
-
If
|
|
115
|
+
If the recorded base cannot be read, an ordinary save preserves differing storage bytes on an independent root without guessing the winner's parent. This also works when the existing winner is generation one or has unavailable chunks. Existing unreadable branches remain available for repair. A host may separately offer confirmed exact-revision discard; it must not infer permission for discard from a failed read.
|
|
103
116
|
|
|
104
117
|
These fallbacks favour recoverability. They may temporarily leave a duplicate target or a still-visible source conflict, but they do not discard a branch whose relationship to the user's operation cannot be proved.
|
|
105
118
|
|
|
@@ -145,19 +158,19 @@ For a cross-path rename, such as `draft.md` to `published.md`, Commonlib stores
|
|
|
145
158
|
|
|
146
159
|
### Reconstructing provenance after local state is unavailable
|
|
147
160
|
|
|
148
|
-
Suppose a local-database reset removed the device-local record, but the storage file still has exactly the bytes held by `C2`. If `C2` is the only
|
|
161
|
+
Suppose a local-database reset removed the device-local record, but the storage file still has exactly the bytes held by current non-deleted leaf `C2`. If `C2` is the only matching current leaf, ordinary saving or reflection can reconstruct `C2` as the displayed base and a later edit extends it normally. An unchanged-time scan alone does not guarantee reconstruction.
|
|
149
162
|
|
|
150
|
-
If both
|
|
163
|
+
If both current leaves contain the same bytes, ordinary saving avoids duplicate storage without selecting either as provenance. If neither contains the storage bytes, those bytes are preserved on an independent root. An unproven deletion preserves both existing branches, and a cross-path rename preserves unproven source branches after storing the target. These outcomes may require user review, but they do not guess that the deterministic winner was displayed.
|
|
151
164
|
|
|
152
165
|
### Receiving a resolution while showing the losing branch
|
|
153
166
|
|
|
154
|
-
Device A may resolve the conflict while Device B still displays the bytes from `C2`. When the resolved tree reaches Device B, the
|
|
167
|
+
Device A may resolve the conflict while Device B still displays the bytes from recorded revision `C2`. When the resolved tree reaches Device B, the exact recorded body identifies those bytes as unchanged, even below the deleted losing leaf. Device B can apply the resolution without recreating the conflict. If Device B edited the file after displaying `C2`, its bytes differ from that recorded body and the overwrite guard preserves the edit, including an intentional revert to some other historical content.
|
|
155
168
|
|
|
156
169
|
### Starting and resetting the provenance store
|
|
157
170
|
|
|
158
171
|
A host may create the namespaced provenance handle while composing services. It then opens the backing key-value database in its sequential settings lifecycle before enabling scans, storage watchers, or replication. If opening fails, start-up stops; a provenance operation is not held waiting for a readiness state which may never arrive.
|
|
159
172
|
|
|
160
|
-
During a local-database reset, the store is temporarily unavailable. A racing lookup fails promptly and is treated as unknown provenance, selecting the conservative behaviours above. Once the database has reopened,
|
|
173
|
+
During a local-database reset, the store is temporarily unavailable. A racing lookup fails promptly and is treated as unknown provenance, selecting the conservative behaviours above. Once the database has reopened, saving or reflection can reconstruct a record from a unique matching current non-deleted leaf.
|
|
161
174
|
|
|
162
175
|
## Unsafe shortcuts
|
|
163
176
|
|
|
@@ -167,11 +180,11 @@ Do not:
|
|
|
167
180
|
- select a winner from modification time unless the host exposes and the user selects that destructive policy;
|
|
168
181
|
- assume the PouchDB winner is the content currently displayed in storage;
|
|
169
182
|
- replace a recorded displayed revision merely because current bytes match another branch;
|
|
170
|
-
-
|
|
183
|
+
- infer that a file is unchanged from historical byte equality without its exact provenance;
|
|
171
184
|
- discard storage content because history lookup failed;
|
|
172
185
|
- infer revision identity from path, size, modification time, or hash without the revision identifier; or
|
|
173
186
|
- automatically merge overlapping text changes or unrelated binary contents.
|
|
174
187
|
|
|
175
188
|
## Verification ownership
|
|
176
189
|
|
|
177
|
-
Commonlib unit tests build real in-memory PouchDB trees and inject provenance fakes at the file-handler boundary. They cover
|
|
190
|
+
Commonlib unit tests build real in-memory PouchDB trees and inject provenance fakes at the file-handler boundary. They cover revision ancestry, conservative merge, content preservation, independent roots, repeated events, and the separate explicit reconciliation, deletion, and rename contracts. A maintained host should additionally verify persistent device-local provenance, real file events, restart processing, replication of the resulting revision trees, and dialogue policy. Package-level tests do not establish mobile lifecycle behaviour or the original reporter's environment.
|
package/docs/development.md
CHANGED
|
@@ -134,15 +134,15 @@ Keep [the maintained-host evidence](proven-in-use.md) aligned with the downstrea
|
|
|
134
134
|
|
|
135
135
|
## Integration tests
|
|
136
136
|
|
|
137
|
-
The integration suite owns Commonlib's direct CouchDB and S3-compatible storage checks. Run it with managed, disposable CouchDB and
|
|
137
|
+
The integration suite owns Commonlib's direct CouchDB and S3-compatible storage checks. Run it with managed, disposable CouchDB and RustFS containers:
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
140
|
npm run test:integration:managed
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
This command requires Docker Compose, creates only test credentials and data, and removes the containers and their volumes after the run. It is the same entry point used by package CI.
|
|
143
|
+
This command requires Docker Compose, creates only test credentials and data, and removes the containers and their volumes after the run. It is the same entry point used by package CI. The RustFS server and client images are pinned by digest; the Compose services retain the names `minio` and `minio-init`.
|
|
144
144
|
|
|
145
|
-
To test services that are already running, use `npm run test:integration`. The runner accepts `hostname`, `username`, and `password` for CouchDB, and `minioEndpoint`, `accessKey`, `secretKey`, and `bucketName` for S3-compatible storage. Without overrides, it uses the endpoints and test credentials from the managed Compose environment. The selected
|
|
145
|
+
To test services that are already running, use `npm run test:integration`. The runner accepts `hostname`, `username`, and `password` for CouchDB, and `minioEndpoint`, `accessKey`, `secretKey`, and `bucketName` for S3-compatible storage. Without overrides, it uses the endpoints and test credentials from the managed Compose environment. The selected S3-compatible bucket must already exist when services are not managed by the runner.
|
|
146
146
|
|
|
147
147
|
## Messages and translation
|
|
148
148
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
date: 2026-
|
|
3
|
-
commonlib-version: "0.1.
|
|
4
|
-
self-hosted-livesync-version: "1.0.
|
|
2
|
+
date: 2026-09-16
|
|
3
|
+
commonlib-version: "0.1.25-dev.turn-credentials.6"
|
|
4
|
+
self-hosted-livesync-version: "1.0.28"
|
|
5
5
|
status: unreleased
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -173,6 +173,87 @@ Do not close the raw values returned by `room.getPeers()`. Both closing them bef
|
|
|
173
173
|
|
|
174
174
|
Commonlib consequently does not expose a forced physical-disconnection command. Such an operation requires a future Trystero API which removes and destroys peers through its shared-peer manager, followed by proof that immediate reconnection remains possible.
|
|
175
175
|
|
|
176
|
+
## Optional host preparation
|
|
177
|
+
|
|
178
|
+
The `/p2p` entry exposes the optional `prepareP2PSettings(settings, signal)`
|
|
179
|
+
hook through `P2PReplicatorFeatureOptions` and `P2PServiceOptions`. The hook
|
|
180
|
+
receives the requested settings snapshot before a new room is created. It may
|
|
181
|
+
return `P2P_iceServers` and `P2P_iceServersExpiresAt`; the latter is an absolute
|
|
182
|
+
Unix time in milliseconds. Commonlib copies only those two runtime values onto
|
|
183
|
+
its own connection snapshot, so changes to room identity or other static
|
|
184
|
+
settings in the returned object cannot rewrite the requested binding.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
useP2PReplicatorFeature(core, openReplicationUiFactory, openRebuildUiFactory, {
|
|
188
|
+
prepareP2PSettings: async (settings, signal) => ({
|
|
189
|
+
...settings,
|
|
190
|
+
...(await prepareHostIceSettings(settings, signal)),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The hook is called for every new room when supplied, including manual profiles;
|
|
196
|
+
a manual host branch can return the settings unchanged. A consumer which omits
|
|
197
|
+
the hook retains existing manual TURN behaviour. A non-empty
|
|
198
|
+
`P2P_managedType` explicitly requests host preparation and fails safely when
|
|
199
|
+
the hook is absent or does not return usable TURN settings. Provider choice,
|
|
200
|
+
HTTP requests, response validation, timeouts within the provider call, and
|
|
201
|
+
credential-safe error detail belong to the host. Commonlib checks only that a
|
|
202
|
+
managed result has a TURN route and a finite expiry beyond the 30-second room
|
|
203
|
+
establishment margin.
|
|
204
|
+
|
|
205
|
+
A host can also return a non-empty ICE override without selecting a managed
|
|
206
|
+
provider. This supports ordinary STUN-only or TURN settings, and does not
|
|
207
|
+
require an expiry. When the host supplies an expiry, Commonlib applies the same
|
|
208
|
+
30-second establishment margin. A runtime override used with relay-only routing
|
|
209
|
+
must contain a TURN route; Commonlib rejects it rather than silently changing
|
|
210
|
+
the route policy. The host remains responsible for validating the individual
|
|
211
|
+
ICE server values.
|
|
212
|
+
|
|
213
|
+
`P2PRoomSessionOwner` retains prepared settings only in the active room's
|
|
214
|
+
immutable connection snapshot. Matching requested settings and usable runtime
|
|
215
|
+
credentials retain that room. An expiry inside the margin causes the next
|
|
216
|
+
reconciliation to retire the room, prepare another snapshot, and open its
|
|
217
|
+
replacement. Explicit reconnect always prepares again. Disconnect, settings
|
|
218
|
+
changes, and suspension abort or invalidate pending preparation; a late result
|
|
219
|
+
cannot publish an obsolete room. Preparation and room opening both have bounded
|
|
220
|
+
owner waits.
|
|
221
|
+
|
|
222
|
+
There is no periodic renewal, per-peer preparation, provider retry engine, or
|
|
223
|
+
update of a live `RTCPeerConnection`. Internal Trystero reconnection within an
|
|
224
|
+
existing room does not recheck expiry. A host can use the existing
|
|
225
|
+
disconnect/connect flow when a long-lived room needs fresh credentials.
|
|
226
|
+
Trystero retains ownership of physical peers and its offer pool.
|
|
227
|
+
|
|
228
|
+
Room retirement may cancel a transfer. The next replication attempt uses the
|
|
229
|
+
existing database checkpoint and revision comparison, retaining saved Metadata
|
|
230
|
+
and Chunks. This is document-level continuation; a partial network message may
|
|
231
|
+
be resent. An unfinished automatic baseline remains eligible under the existing
|
|
232
|
+
peer policy, while an interrupted manual operation requires another request.
|
|
233
|
+
|
|
234
|
+
Managed profiles use the ordinary `sls+p2p://` URI query fields
|
|
235
|
+
`managedType`, `managedId`, and `token`. Commonlib preserves the corresponding
|
|
236
|
+
`P2P_managedType`, `P2P_managedId`, and `P2P_managedToken` scalar projections
|
|
237
|
+
when it parses, serialises, or activates a profile. Provider names and the
|
|
238
|
+
meaning of identifiers and tokens remain host-defined.
|
|
239
|
+
|
|
240
|
+
The token is persisted only inside the selected P2P profile URI, using the
|
|
241
|
+
existing whole-profile encryption. General settings saves omit the three
|
|
242
|
+
duplicate top-level projections and never synthesise a profile from flat
|
|
243
|
+
fields. Explicit profile operations own URI updates. Flat-settings migration
|
|
244
|
+
creates and selects `legacy-p2p` once, including when CouchDB remains the main
|
|
245
|
+
remote. An encryption failure refuses to replace a managed profile with
|
|
246
|
+
plaintext data.
|
|
247
|
+
|
|
248
|
+
Encrypted Setup sharing and plain QR sharing already include
|
|
249
|
+
`remoteConfigurations`, so they carry managed settings through the profile
|
|
250
|
+
without new compact fields. Storage, Setup, QR, configuration exchange, and
|
|
251
|
+
external-import boundaries omit `P2P_iceServers` and
|
|
252
|
+
`P2P_iceServersExpiresAt`. Setup, QR, and configuration exchange also omit the
|
|
253
|
+
duplicate top-level provider projections. Runtime credentials therefore do not
|
|
254
|
+
become a later connection override. Hosts must redact tokens, issued
|
|
255
|
+
credentials, and provider responses from logs and reports.
|
|
256
|
+
|
|
176
257
|
## Verification
|
|
177
258
|
|
|
178
259
|
Maintain all five boundaries when this lifecycle changes:
|
|
@@ -182,7 +263,8 @@ Maintain all five boundaries when this lifecycle changes:
|
|
|
182
263
|
- target-aware automation tests must prove bounded delayed advertisement discovery, non-interactive acceptance outcomes, explicit partial and blocked results, baseline de-duplication across AutoSync and configured targets, cancellation of delayed AutoStart after suspension, and a finite demand beside an AutoStart-held room;
|
|
183
264
|
- Commonlib rebuild tests must prove that first-device P2P initialisation does not reset a remote database and that an additional-device P2P Fetch performs one explicit peer-selection pass before resuming reflection;
|
|
184
265
|
- the Self-hosted LiveSync Compose P2P lifecycle test must replace a current replicator, rediscover the same real peer, perform bidirectional RPC, and verify transferred content from a separate process;
|
|
185
|
-
- the Self-hosted LiveSync real-Obsidian P2P Setup URI workflow must generate the second-device URI on the first device, accept both peer directions visibly, and verify a two-way note round-trip;
|
|
266
|
+
- the Self-hosted LiveSync real-Obsidian P2P Setup URI workflow must generate the second-device URI on the first device, accept both peer directions visibly, and verify a two-way note round-trip;
|
|
267
|
+
- prepared-settings tests must prove managed TURN and expiry requirements, an unmanaged STUN-only override without expiry, relay-only rejection without TURN, immutable requested values, cancellation, expiry replacement, and late-result fencing; and
|
|
186
268
|
- the relay-disconnect test must observe the original WebSocket reach `CLOSED`, remain closed while reconnection is paused, and be replaced after reconnection resumes.
|
|
187
269
|
|
|
188
270
|
The corresponding product decision and rejected alternatives are recorded in Self-hosted LiveSync's P2P room and transport lifecycle ADR.
|
package/docs/proven-in-use.md
CHANGED
|
@@ -14,7 +14,7 @@ The consumer contract in `test/contracts/serviceContext.ts` checks shared result
|
|
|
14
14
|
|
|
15
15
|
Commonlib supplies the two sleep preferences and preserves them through Setup URI encoding. Self-hosted LiveSync applies those values once at its injected bounded-activity runner, so remote operations and downloaded-document application share the same host-owned Wake Lock policy. Focused downstream tests cover the platform preference table and both activity categories; operating-system suspend behaviour remains real-platform integration evidence rather than a Commonlib guarantee.
|
|
16
16
|
|
|
17
|
-
Commonlib's real-PouchDB and injected-boundary unit tests prove that conservative merge uses the nearest revision actually shared by both conflict leaves, multiple remaining leaves are selected in a deterministic order, each completed pair is reconstructed from the committed tree, storage content
|
|
17
|
+
Commonlib's real-PouchDB and injected-boundary unit tests prove that conservative merge uses the nearest revision actually shared by both conflict leaves, multiple remaining leaves are selected in a deterministic order, each completed pair is reconstructed from the committed tree, storage content with exact provenance remains recognisable below a deleted losing branch after resolution, and ordinary file saves preserve edits below their recorded base or on an independent root when the base cannot be read. Incoming reflection uses the same classification. Current non-deleted leaf matches prevent duplicate preservation without using historical byte equality as proof of an unchanged file. Conflict-time deletion and rename retain their separate displayed-branch contracts. They also verify that a boolean history check stops after its first exact content match without reading an older unavailable revision, while revision enumeration still returns every match. Unknown-origin local content remains readable on a fresh independent root when the winning body is unavailable, including when the existing winner is generation one. A separately confirmed exact deletion of an unreadable current leaf remains an explicit repair operation. Exact reconciliation tests cover reflecting a selected live leaf, storing Vault content as its child, recording a matching leaf without creating a revision, clearing matching provenance after exact branch deletion, and refusing to record a stale revision or a revision whose content differs. Separate live-base tests advance a selected conflict leaf or logical-deletion leaf through PouchDB's ordinary revision check, retain the other live leaf, and reject reuse of a stale base. Self-hosted LiveSync supplies the host composition, persistent device-local provenance, and the per-revision repair interface. Its optional two-Vault scopes cover post-resolution propagation and edit, logical deletion, case-only rename, and cross-path rename while conflicts remain active. The conflict-operation scope passes in real Obsidian against the exact locally packed Commonlib artefact, including replicated parent revisions and preservation of the other live branches. Its Document History scenario restores readable historical bytes as a new live successor and reflects that exact revision to the Vault.
|
|
18
18
|
|
|
19
19
|
Commonlib's chunk-reachability tests use real PouchDB revision trees. They verify that an ordinary superseded revision releases its unique chunks, a chunk shared by current files remains reachable, live conflict branches and their nearest available shared ancestor remain protected, and losing-branch ancestry becomes eligible after resolution. They also replicate a collected chunk deletion to another PouchDB database and verify that writing the same content can recreate and replicate the content-addressed chunk. Self-hosted LiveSync owns the CouchDB-specific Garbage Collection V3 workflow, including device checks, confirmation, deletion, push-only replication, and remote compaction.
|
|
20
20
|
|
|
@@ -70,6 +70,36 @@ The Obsidian plug-in, CLI, WebApp, and WebPeer compose P2P ownership through `us
|
|
|
70
70
|
|
|
71
71
|
Commonlib's focused P2P compatibility tests establish legacy defaulting, bounded outgoing message sizes, connection-string and QR-setting round trips, application of the configured bound across its Trystero server and client surfaces, relay-only ICE mapping when a valid TURN URL is present, and replacement of a serving transport when an effective compatibility value changes. They do not prove that a particular network path requires a smaller preset or that a configured TURN service can allocate a relay. Self-hosted LiveSync owns profile selection, user-facing controls, and real-Obsidian composition; device- and network-specific connectivity remains downstream or reporter-environment evidence.
|
|
72
72
|
|
|
73
|
+
The optional P2P settings-preparation hook lets a host supply connection-only
|
|
74
|
+
ICE servers without adding provider logic to Commonlib. Focused Commonlib tests
|
|
75
|
+
cover immutable requested settings, provider-scalar identity, room reuse before
|
|
76
|
+
expiry, replacement inside the 30-second expiry margin, explicit reconnect,
|
|
77
|
+
bounded preparation, cancellation, late-result fencing, manual compatibility,
|
|
78
|
+
an unmanaged STUN-only override without provider metadata or expiry,
|
|
79
|
+
relay-only rejection without TURN, relay-only mapping with TURN, and omission
|
|
80
|
+
of runtime credentials from persistence and sharing. Profile tests cover the
|
|
81
|
+
`managedType`, `managedId`, and `token` URI fields, one-profile flat migration,
|
|
82
|
+
activation, whole-profile encryption, and failure without plaintext
|
|
83
|
+
replacement.
|
|
84
|
+
|
|
85
|
+
Self-hosted LiveSync supplies the Cloudflare HTTP function, response validation,
|
|
86
|
+
provider selection, settings UI, and host hook. Mocked HTTP and injected-room
|
|
87
|
+
tests establish those boundaries without calling Cloudflare. On 16 September
|
|
88
|
+
2026, two isolated Obsidian 1.12.7 instances on one Linux host exercised the
|
|
89
|
+
exact `0.1.25-dev.turn-credentials.6` package. Both acquired real Cloudflare
|
|
90
|
+
credentials and selected relay candidates, and a note reached the receiving
|
|
91
|
+
Vault. Explicit disconnection and reconnection issued fresh credentials,
|
|
92
|
+
selected relay candidates at both endpoints again, and allowed a second note
|
|
93
|
+
to reach the other Vault. A separate real-Obsidian check with dummy credentials
|
|
94
|
+
verified encrypted profile persistence and restoration after restart, with
|
|
95
|
+
issued ICE credentials excluded from saved settings.
|
|
96
|
+
|
|
97
|
+
Natural TTL expiry, interruption during a replication batch, mobile lifecycle,
|
|
98
|
+
mixed manual and managed peers, and different-network connectivity retain
|
|
99
|
+
separate downstream runtime validation requirements. The live-provider check
|
|
100
|
+
establishes issuance and explicit reconnection for the tested package; it does
|
|
101
|
+
not establish those additional behaviours or the eventual published artefact.
|
|
102
|
+
|
|
73
103
|
Focused room-session and RPC tests additionally establish that a manual stop delivers cancellation to every concurrent finite replication while leaving the room reusable, that session retirement rejects new work and awaits admitted work before leaving, and that caller cancellation reaches a cancellation-aware incoming RPC handler. The replication-shim tests establish cancellation at safe batch boundaries and preserve the checkpoint for an atomic database write which has already settled. Self-hosted LiveSync composes the provider stop capability into its manual command; physical transport interruption and operating-system process termination remain host and runtime boundaries rather than rollback guarantees.
|
|
74
104
|
|
|
75
105
|
This is evidence that the current package boundary can be consumed without the Obsidian host, but it is not yet a recommended standalone Commonlib client factory. The high-level lifecycle, readiness, error, and disposal contract remains future package work after Self-hosted LiveSync 1.0 rather than a gate for that release.
|
package/package.json
CHANGED