@vrtmrz/livesync-commonlib 0.1.24 → 0.1.25
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/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/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/services/base/SettingService.d.ts +1 -0
- package/dist/services/base/SettingService.js +37 -9
- package/docs/development.md +3 -3
- package/docs/p2p-transport-lifecycle.md +86 -4
- package/docs/proven-in-use.md +30 -0
- package/package.json +1 -1
|
@@ -2,8 +2,12 @@ import { Logger } from "../../common/logger.js";
|
|
|
2
2
|
import {
|
|
3
3
|
LOG_LEVEL_NOTICE,
|
|
4
4
|
LOG_LEVEL_VERBOSE,
|
|
5
|
-
SETTING_KEY_P2P_DEVICE_NAME
|
|
5
|
+
SETTING_KEY_P2P_DEVICE_NAME,
|
|
6
|
+
hasManagedP2PTurnConfiguration,
|
|
7
|
+
omitP2PRuntimeSettings
|
|
6
8
|
} from "../../common/types.js";
|
|
9
|
+
import { P2PConnectionPaths } from "../../common/models/setting.const.js";
|
|
10
|
+
import { normaliseP2PConnectionPath } from "../../common/models/setting.p2p.js";
|
|
7
11
|
import { P2PRoomSession } from "./P2PRoomSession.js";
|
|
8
12
|
import { P2PAutomationCoordinator } from "./P2PAutomationCoordinator.js";
|
|
9
13
|
import { PEER_REPLICATION_READINESS } from "../ReplicatorProvider.js";
|
|
@@ -12,19 +16,28 @@ import {
|
|
|
12
16
|
ACTIVE_P2P_RELAY_BINDING_CONFLICT,
|
|
13
17
|
activeP2PRelayBindingCovers
|
|
14
18
|
} from "./P2PConnectionProbeAdmission.js";
|
|
19
|
+
class StaleRoomReconciliationError extends Error {
|
|
20
|
+
}
|
|
21
|
+
const P2P_ROOM_OPEN_TIMEOUT_MS = 3e4;
|
|
22
|
+
const P2P_SETTINGS_PREPARATION_TIMEOUT_MS = 3e4;
|
|
23
|
+
const P2P_ICE_SERVER_EXPIRY_MARGIN_MS = 3e4;
|
|
15
24
|
class P2PRoomSessionOwner {
|
|
16
|
-
constructor(env, createSession = (sessionEnv) => new P2PRoomSession(sessionEnv)) {
|
|
25
|
+
constructor(env, createSession = (sessionEnv) => new P2PRoomSession(sessionEnv), options = {}) {
|
|
17
26
|
this.env = env;
|
|
18
27
|
this.createSession = createSession;
|
|
28
|
+
this.options = options;
|
|
19
29
|
}
|
|
20
30
|
env;
|
|
21
31
|
createSession;
|
|
32
|
+
options;
|
|
22
33
|
current;
|
|
23
34
|
lifecycleOperation = Promise.resolve();
|
|
24
35
|
persistentDemands = /* @__PURE__ */ new Set();
|
|
25
36
|
finiteDemands = /* @__PURE__ */ new Set();
|
|
26
37
|
automationCoordinator = new P2PAutomationCoordinator();
|
|
27
38
|
activeBinding;
|
|
39
|
+
pendingPreparation;
|
|
40
|
+
runtimeGeneration = 0;
|
|
28
41
|
get currentSession() {
|
|
29
42
|
return this.current;
|
|
30
43
|
}
|
|
@@ -52,6 +65,7 @@ class P2PRoomSessionOwner {
|
|
|
52
65
|
if (active && !enabled) {
|
|
53
66
|
Logger(this.env.services.context.translate("P2P.NotEnabled"), LOG_LEVEL_NOTICE);
|
|
54
67
|
}
|
|
68
|
+
this.fencePreparationForCurrentSettings(enabled);
|
|
55
69
|
await this.reconcileTransport();
|
|
56
70
|
}
|
|
57
71
|
/**
|
|
@@ -66,6 +80,7 @@ class P2PRoomSessionOwner {
|
|
|
66
80
|
}
|
|
67
81
|
const demand = /* @__PURE__ */ Symbol("p2p-finite-room-demand");
|
|
68
82
|
this.finiteDemands.add(demand);
|
|
83
|
+
this.fencePreparationForCurrentSettings(true);
|
|
69
84
|
try {
|
|
70
85
|
await this.reconcileTransport();
|
|
71
86
|
if (!this.finiteDemands.has(demand)) {
|
|
@@ -116,7 +131,7 @@ class P2PRoomSessionOwner {
|
|
|
116
131
|
return;
|
|
117
132
|
}
|
|
118
133
|
const binding = this.getEffectiveBinding();
|
|
119
|
-
if (this.current?.host.isServing && this.bindingsMatch(this.activeBinding, binding)) {
|
|
134
|
+
if (this.current?.host.isServing && this.bindingsMatch(this.activeBinding, binding) && this.connectionSettingsRemainUsable(this.activeBinding?.settings)) {
|
|
120
135
|
this.reconcileCurrentSessionPolicy(this.current);
|
|
121
136
|
Logger("P2P replicator is already open.");
|
|
122
137
|
return;
|
|
@@ -125,34 +140,55 @@ class P2PRoomSessionOwner {
|
|
|
125
140
|
await this.closeTransport();
|
|
126
141
|
}
|
|
127
142
|
let candidate;
|
|
143
|
+
const operationGeneration = this.runtimeGeneration;
|
|
128
144
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
const connectionSettings = await this.prepareConnectionSettings(binding, operationGeneration);
|
|
146
|
+
this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
147
|
+
const connectionBinding = { ...binding, settings: connectionSettings };
|
|
148
|
+
candidate = this.createSession(this.buildSessionEnv(connectionBinding));
|
|
149
|
+
if (hasManagedP2PTurnConfiguration(binding.settings)) {
|
|
150
|
+
await this.withRoomOpenDeadline(candidate.open());
|
|
151
|
+
} else {
|
|
152
|
+
await candidate.open();
|
|
153
|
+
}
|
|
131
154
|
if (!candidate.host.isServing) {
|
|
132
155
|
throw new Error("The P2P room did not start serving.");
|
|
133
156
|
}
|
|
134
157
|
const currentBinding = this.getEffectiveBinding();
|
|
135
|
-
if (!currentBinding.enabled || !this.hasRoomDemand() || !this.bindingsMatch(binding, currentBinding)) {
|
|
158
|
+
if (!currentBinding.enabled || !this.hasRoomDemand() || !this.bindingsMatch(binding, currentBinding) || operationGeneration !== this.runtimeGeneration || !this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
136
159
|
await candidate.retire();
|
|
137
160
|
return;
|
|
138
161
|
}
|
|
139
162
|
this.current = candidate;
|
|
140
|
-
this.activeBinding =
|
|
163
|
+
this.activeBinding = connectionBinding;
|
|
141
164
|
this.reconcileCurrentSessionPolicy(candidate);
|
|
142
165
|
} catch (error) {
|
|
143
166
|
await candidate?.retire(error).catch((retirementError) => {
|
|
144
|
-
|
|
167
|
+
if (hasManagedP2PTurnConfiguration(binding.settings)) {
|
|
168
|
+
Logger("The managed P2P room could not be retired cleanly.", LOG_LEVEL_VERBOSE);
|
|
169
|
+
} else {
|
|
170
|
+
Logger(retirementError, LOG_LEVEL_VERBOSE);
|
|
171
|
+
}
|
|
145
172
|
});
|
|
146
|
-
Logger(error instanceof Error ? error.message : "Error while opening P2P connection", LOG_LEVEL_NOTICE);
|
|
147
|
-
Logger(error, LOG_LEVEL_VERBOSE);
|
|
148
173
|
this.current = void 0;
|
|
149
174
|
this.activeBinding = void 0;
|
|
175
|
+
if (error instanceof StaleRoomReconciliationError || operationGeneration !== this.runtimeGeneration) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (hasManagedP2PTurnConfiguration(binding.settings)) {
|
|
179
|
+
Logger("The managed P2P room could not be prepared or opened.", LOG_LEVEL_NOTICE);
|
|
180
|
+
Logger("Managed P2P room failure details were omitted.", LOG_LEVEL_VERBOSE);
|
|
181
|
+
throw new Error("The managed P2P room could not be prepared or opened.");
|
|
182
|
+
}
|
|
183
|
+
Logger(error instanceof Error ? error.message : "Error while opening P2P connection", LOG_LEVEL_NOTICE);
|
|
184
|
+
Logger(error, LOG_LEVEL_VERBOSE);
|
|
150
185
|
}
|
|
151
186
|
});
|
|
152
187
|
}
|
|
153
188
|
async close() {
|
|
154
189
|
this.persistentDemands.clear();
|
|
155
190
|
this.finiteDemands.clear();
|
|
191
|
+
this.invalidatePreparation();
|
|
156
192
|
await this.enqueueLifecycleOperation(async () => {
|
|
157
193
|
await this.closeTransport();
|
|
158
194
|
});
|
|
@@ -179,11 +215,153 @@ class P2PRoomSessionOwner {
|
|
|
179
215
|
bindingsMatch(current, candidate) {
|
|
180
216
|
return current?.database === candidate.database && current.signature === candidate.signature;
|
|
181
217
|
}
|
|
218
|
+
connectionSettingsRemainUsable(settings) {
|
|
219
|
+
if (!settings) return false;
|
|
220
|
+
const managed = hasManagedP2PTurnConfiguration(settings);
|
|
221
|
+
const iceServers = settings.P2P_iceServers;
|
|
222
|
+
if (iceServers === void 0) return !managed;
|
|
223
|
+
if (!this.hasUsableIceServer(iceServers)) return false;
|
|
224
|
+
const hasTurnServer = this.hasUsableTurnServer(iceServers);
|
|
225
|
+
if (managed && !hasTurnServer) return false;
|
|
226
|
+
if (normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && !hasTurnServer) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const expiresAt = settings.P2P_iceServersExpiresAt;
|
|
230
|
+
if (expiresAt === void 0) return !managed;
|
|
231
|
+
return Number.isFinite(expiresAt) && expiresAt > Date.now() + P2P_ICE_SERVER_EXPIRY_MARGIN_MS;
|
|
232
|
+
}
|
|
233
|
+
fencePreparationForCurrentSettings(enabled) {
|
|
234
|
+
if (!enabled) {
|
|
235
|
+
this.invalidatePreparation();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const pendingChanged = this.pendingPreparation !== void 0 && this.pendingPreparation.signature !== this.getEffectiveBinding().signature;
|
|
239
|
+
if (pendingChanged) this.invalidatePreparation();
|
|
240
|
+
}
|
|
241
|
+
invalidatePreparation() {
|
|
242
|
+
this.runtimeGeneration += 1;
|
|
243
|
+
const pending = this.pendingPreparation;
|
|
244
|
+
this.pendingPreparation = void 0;
|
|
245
|
+
if (pending && !pending.controller.signal.aborted) {
|
|
246
|
+
pending.controller.abort(new Error("The P2P settings preparation was cancelled."));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async prepareConnectionSettings(binding, operationGeneration) {
|
|
250
|
+
const managed = hasManagedP2PTurnConfiguration(binding.settings);
|
|
251
|
+
const prepare = this.options.prepareP2PSettings;
|
|
252
|
+
if (!prepare) {
|
|
253
|
+
if (managed) throw new Error("The selected managed TURN configuration is not supported by this host.");
|
|
254
|
+
return binding.settings;
|
|
255
|
+
}
|
|
256
|
+
const controller = new AbortController();
|
|
257
|
+
const pending = {
|
|
258
|
+
signature: binding.signature,
|
|
259
|
+
controller
|
|
260
|
+
};
|
|
261
|
+
this.pendingPreparation = pending;
|
|
262
|
+
try {
|
|
263
|
+
const prepared = await this.prepareWithDeadline(prepare, binding.settings, controller);
|
|
264
|
+
if (prepared.P2P_iceServers === void 0 && prepared.P2P_iceServersExpiresAt === void 0) {
|
|
265
|
+
if (managed) throw new Error("The prepared managed TURN settings do not include ICE servers.");
|
|
266
|
+
return binding.settings;
|
|
267
|
+
}
|
|
268
|
+
const iceServers = this.copyPreparedIceServers(prepared.P2P_iceServers);
|
|
269
|
+
const expiresAt = prepared.P2P_iceServersExpiresAt;
|
|
270
|
+
const connectionSettings = {
|
|
271
|
+
...binding.settings,
|
|
272
|
+
P2P_iceServers: iceServers,
|
|
273
|
+
P2P_iceServersExpiresAt: expiresAt
|
|
274
|
+
};
|
|
275
|
+
if (!this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
276
|
+
throw new Error("The prepared managed TURN settings are missing usable ICE servers or expiry.");
|
|
277
|
+
}
|
|
278
|
+
this.assertReconciliationCurrent(binding, operationGeneration, connectionSettings);
|
|
279
|
+
return connectionSettings;
|
|
280
|
+
} finally {
|
|
281
|
+
if (this.pendingPreparation === pending) {
|
|
282
|
+
this.pendingPreparation = void 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
prepareWithDeadline(prepare, settings, controller) {
|
|
287
|
+
const preparation = Promise.resolve().then(() => prepare(Object.freeze({ ...settings }), controller.signal));
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
let settled = false;
|
|
290
|
+
const finish = (operation) => {
|
|
291
|
+
if (settled) return;
|
|
292
|
+
settled = true;
|
|
293
|
+
globalThis.clearTimeout(timeout);
|
|
294
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
295
|
+
operation();
|
|
296
|
+
};
|
|
297
|
+
const onAbort = () => finish(
|
|
298
|
+
() => reject(
|
|
299
|
+
controller.signal.reason instanceof Error ? controller.signal.reason : new Error("The P2P settings preparation was cancelled.")
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
const timeout = globalThis.setTimeout(() => {
|
|
303
|
+
controller.abort(new Error("The P2P settings preparation timed out."));
|
|
304
|
+
}, P2P_SETTINGS_PREPARATION_TIMEOUT_MS);
|
|
305
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
if (controller.signal.aborted) onAbort();
|
|
307
|
+
preparation.then(
|
|
308
|
+
(prepared) => finish(() => resolve(prepared)),
|
|
309
|
+
() => finish(() => reject(new Error("The P2P settings preparation failed.")))
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
copyPreparedIceServers(iceServers) {
|
|
314
|
+
if (!Array.isArray(iceServers) || iceServers.length === 0) return [];
|
|
315
|
+
return iceServers.map((server) => ({
|
|
316
|
+
urls: typeof server.urls === "string" ? server.urls : [...server.urls],
|
|
317
|
+
...server.username === void 0 ? {} : { username: server.username },
|
|
318
|
+
...server.credential === void 0 ? {} : { credential: server.credential }
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
hasUsableIceServer(iceServers) {
|
|
322
|
+
return Array.isArray(iceServers) && iceServers.some((server) => {
|
|
323
|
+
const urls = typeof server?.urls === "string" ? [server.urls] : server?.urls;
|
|
324
|
+
return Array.isArray(urls) && urls.some((url) => typeof url === "string" && url.trim().length > 0);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
hasUsableTurnServer(iceServers) {
|
|
328
|
+
return Array.isArray(iceServers) && iceServers.some((server) => {
|
|
329
|
+
const urls = typeof server?.urls === "string" ? [server.urls] : server?.urls;
|
|
330
|
+
return Array.isArray(urls) && urls.some((url) => typeof url === "string" && /^turns?:/iu.test(url));
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async withRoomOpenDeadline(opening) {
|
|
334
|
+
let timeout;
|
|
335
|
+
try {
|
|
336
|
+
await Promise.race([
|
|
337
|
+
opening,
|
|
338
|
+
new Promise((_resolve, reject) => {
|
|
339
|
+
timeout = globalThis.setTimeout(
|
|
340
|
+
() => reject(new Error("The P2P room opening timed out.")),
|
|
341
|
+
P2P_ROOM_OPEN_TIMEOUT_MS
|
|
342
|
+
);
|
|
343
|
+
})
|
|
344
|
+
]);
|
|
345
|
+
} finally {
|
|
346
|
+
if (timeout !== void 0) globalThis.clearTimeout(timeout);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
assertReconciliationCurrent(binding, operationGeneration, connectionSettings) {
|
|
350
|
+
if (operationGeneration !== this.runtimeGeneration || !this.hasRoomDemand()) {
|
|
351
|
+
throw new StaleRoomReconciliationError();
|
|
352
|
+
}
|
|
353
|
+
const currentBinding = this.getEffectiveBinding();
|
|
354
|
+
if (!currentBinding.enabled) throw new StaleRoomReconciliationError();
|
|
355
|
+
if (binding && !this.bindingsMatch(binding, currentBinding)) throw new StaleRoomReconciliationError();
|
|
356
|
+
if (connectionSettings && !this.connectionSettingsRemainUsable(connectionSettings)) {
|
|
357
|
+
throw new StaleRoomReconciliationError();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
182
360
|
/** Capture immutable session inputs while keeping policy live. */
|
|
183
361
|
getEffectiveBinding() {
|
|
184
|
-
const settings = {
|
|
362
|
+
const settings = omitP2PRuntimeSettings({
|
|
185
363
|
...this.env.services.setting.currentSettings()
|
|
186
|
-
};
|
|
364
|
+
});
|
|
187
365
|
const deviceName = this.env.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || this.env.services.vault.getVaultName();
|
|
188
366
|
const database = this.env.services.database.localDatabase.localDatabase;
|
|
189
367
|
this.automationCoordinator.reconcileIdentity(
|
|
@@ -245,5 +423,7 @@ class P2PRoomSessionOwner {
|
|
|
245
423
|
}
|
|
246
424
|
}
|
|
247
425
|
export {
|
|
248
|
-
P2PRoomSessionOwner
|
|
426
|
+
P2PRoomSessionOwner,
|
|
427
|
+
P2P_ICE_SERVER_EXPIRY_MARGIN_MS,
|
|
428
|
+
P2P_SETTINGS_PREPARATION_TIMEOUT_MS
|
|
249
429
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
TweakValuesShouldMatchedTemplate,
|
|
3
|
+
omitP2PRuntimeSettings
|
|
4
|
+
} from "../../common/types.js";
|
|
2
5
|
import { assessTweakCompatibility } from "../../common/models/tweak.compatibility.js";
|
|
3
6
|
import {
|
|
4
7
|
LOG_LEVEL_INFO,
|
|
@@ -253,12 +256,15 @@ class TrysteroReplicator {
|
|
|
253
256
|
);
|
|
254
257
|
});
|
|
255
258
|
const setting = {
|
|
256
|
-
...this.settings,
|
|
259
|
+
...omitP2PRuntimeSettings(this.settings),
|
|
257
260
|
configPassphraseStore: "",
|
|
258
261
|
encryptedCouchDBConnection: "",
|
|
259
262
|
encryptedPassphrase: "",
|
|
260
263
|
pluginSyncExtendedSetting: {}
|
|
261
264
|
};
|
|
265
|
+
delete setting.P2P_managedType;
|
|
266
|
+
delete setting.P2P_managedId;
|
|
267
|
+
delete setting.P2P_managedToken;
|
|
262
268
|
if (!passphrase || passphrase.trim() == "") {
|
|
263
269
|
Logger(
|
|
264
270
|
"Passphrase is required to transfer the configuration. The peer cannot be decrypt the config\nIf you repeatedly receive unintended configuration-sharing requests, change the RPC channel immediately. It allows you to leave the connection and disappear, while they are trying brute force attack for the decoy on their local.",
|
|
@@ -700,7 +706,7 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
700
706
|
const decryptedConfig = JSON.parse(
|
|
701
707
|
await decrypt(encryptedConfig, passphrase)
|
|
702
708
|
);
|
|
703
|
-
return decryptedConfig;
|
|
709
|
+
return omitP2PRuntimeSettings(decryptedConfig);
|
|
704
710
|
} catch (e) {
|
|
705
711
|
Logger("Error while decrypting the configuration", LOG_LEVEL_NOTICE);
|
|
706
712
|
Logger(e, LOG_LEVEL_VERBOSE);
|
|
@@ -121,6 +121,7 @@ export declare class TrysteroReplicatorP2PServer {
|
|
|
121
121
|
onAfterJoinRoom(): void;
|
|
122
122
|
startService(bindings?: BindableObject[], beforeAdvertisement?: () => void): Promise<void>;
|
|
123
123
|
start(bindings?: BindableObject[], beforeAdvertisement?: () => void): Promise<void>;
|
|
124
|
+
private logTransportError;
|
|
124
125
|
/**
|
|
125
126
|
* @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future.
|
|
126
127
|
* @param type
|
|
@@ -79,7 +79,7 @@ class TrysteroReplicatorP2PServer {
|
|
|
79
79
|
`Some error has been occurred while leaving the room, but possibly can be ignored`,
|
|
80
80
|
LOG_LEVEL_VERBOSE
|
|
81
81
|
);
|
|
82
|
-
|
|
82
|
+
this.logTransportError(ex);
|
|
83
83
|
}
|
|
84
84
|
this._room = void 0;
|
|
85
85
|
this._env.events.emitEvent(EVENT_P2P_DISCONNECTED);
|
|
@@ -97,7 +97,7 @@ class TrysteroReplicatorP2PServer {
|
|
|
97
97
|
this._rpcRoom = void 0;
|
|
98
98
|
} catch (ex) {
|
|
99
99
|
Logger(`Some error has been occurred while shutting down the server`, LOG_LEVEL_INFO);
|
|
100
|
-
|
|
100
|
+
this.logTransportError(ex);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
/** Release subscriptions whose lifetime is the host object rather than one transport join. */
|
|
@@ -459,7 +459,7 @@ You can chose as follows:
|
|
|
459
459
|
handshakeTimeoutMs: 3e4,
|
|
460
460
|
onJoinError: (error) => {
|
|
461
461
|
Logger("Some peer Failed to join Trystero room");
|
|
462
|
-
|
|
462
|
+
this.logTransportError(error);
|
|
463
463
|
}
|
|
464
464
|
});
|
|
465
465
|
await this.setRoom(room);
|
|
@@ -468,6 +468,13 @@ You can chose as follows:
|
|
|
468
468
|
void this.dispatchConnectionStatus();
|
|
469
469
|
await this.startService(bindings, beforeAdvertisement);
|
|
470
470
|
}
|
|
471
|
+
logTransportError(error) {
|
|
472
|
+
if (this.settings.P2P_iceServers !== void 0) {
|
|
473
|
+
Logger("Managed P2P transport error details were omitted.", LOG_LEVEL_VERBOSE);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
Logger(error, LOG_LEVEL_VERBOSE);
|
|
477
|
+
}
|
|
471
478
|
/**
|
|
472
479
|
* @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future.
|
|
473
480
|
* @param type
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import { P2PConnectionPaths } from "../../common/models/setting.const.js";
|
|
2
2
|
import {
|
|
3
3
|
hasValidP2PTurnServerUrl,
|
|
4
|
+
hasManagedP2PTurnConfiguration,
|
|
4
5
|
normaliseP2PConnectionPath,
|
|
5
6
|
normaliseP2PMaxWirePayloadBytes,
|
|
6
7
|
splitP2PRelayUrls,
|
|
7
8
|
splitP2PTurnServerUrls
|
|
8
9
|
} from "../../common/models/setting.p2p.js";
|
|
9
10
|
function getP2PReplicatorConfigurationIdentity(settings) {
|
|
11
|
+
const managedTurnSelected = hasManagedP2PTurnConfiguration(settings);
|
|
10
12
|
const turnServerValue = settings.P2P_turnServers ?? "";
|
|
11
13
|
const turnServers = splitP2PTurnServerUrls(turnServerValue);
|
|
12
14
|
const configuredPath = normaliseP2PConnectionPath(settings.P2P_connectionPath);
|
|
13
|
-
const effectivePath = configuredPath === P2PConnectionPaths.Relay && hasValidP2PTurnServerUrl(turnServerValue) ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
|
|
14
|
-
const turnAuthentication = turnServers.length > 0 ? [settings.P2P_turnUsername, settings.P2P_turnCredential] : void 0;
|
|
15
|
+
const effectivePath = configuredPath === P2PConnectionPaths.Relay && (managedTurnSelected || hasValidP2PTurnServerUrl(turnServerValue)) ? P2PConnectionPaths.Relay : P2PConnectionPaths.Automatic;
|
|
16
|
+
const turnAuthentication = !managedTurnSelected && turnServers.length > 0 ? [settings.P2P_turnUsername, settings.P2P_turnCredential] : void 0;
|
|
15
17
|
return JSON.stringify([
|
|
16
18
|
"p2p",
|
|
17
19
|
settings.P2P_AppID || "self-hosted-livesync",
|
|
18
20
|
settings.P2P_roomID,
|
|
19
21
|
settings.P2P_passphrase,
|
|
20
22
|
splitP2PRelayUrls(settings.P2P_relays),
|
|
21
|
-
turnServers,
|
|
23
|
+
managedTurnSelected ? void 0 : turnServers,
|
|
22
24
|
turnAuthentication,
|
|
25
|
+
managedTurnSelected ? [settings.P2P_managedType, settings.P2P_managedId, settings.P2P_managedToken] : void 0,
|
|
23
26
|
normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes),
|
|
24
27
|
effectivePath,
|
|
25
28
|
settings.P2P_useDiagRTC ?? false
|
|
@@ -2,6 +2,7 @@ import type { NecessaryServices } from "../../interfaces/ServiceModule.js";
|
|
|
2
2
|
import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator.js";
|
|
3
3
|
import { type UseP2PReplicatorResult } from "./UseP2PReplicatorResult.js";
|
|
4
4
|
import { type P2PServiceViews } from "../../p2p/P2PService.js";
|
|
5
|
+
import type { PrepareP2PSettings } from "../../p2p/P2PService.js";
|
|
5
6
|
/**
|
|
6
7
|
* Factory type: given the compatibility Replicator and the stable service
|
|
7
8
|
* views, returns the openReplicationUI callback for that instance.
|
|
@@ -14,6 +15,10 @@ import { type P2PServiceViews } from "../../p2p/P2PService.js";
|
|
|
14
15
|
export type OpenReplicationUIFactory = (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void>;
|
|
15
16
|
/** Same shape as OpenReplicationUIFactory, used for the rebuild/replicateAllFromServer flow. */
|
|
16
17
|
export type OpenRebuildUIFactory = OpenReplicationUIFactory;
|
|
18
|
+
/** Optional host integrations for the P2P room lifecycle. */
|
|
19
|
+
export interface P2PReplicatorFeatureOptions {
|
|
20
|
+
readonly prepareP2PSettings?: PrepareP2PSettings;
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
23
|
* Compose one private P2P service context and register non-owning active
|
|
19
24
|
* provider adapters over it.
|
|
@@ -23,4 +28,4 @@ export type OpenRebuildUIFactory = OpenReplicationUIFactory;
|
|
|
23
28
|
* migrated to those views.
|
|
24
29
|
* @param host
|
|
25
30
|
*/
|
|
26
|
-
export declare function useP2PReplicatorFeature(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, openReplicationUIFactory?: OpenReplicationUIFactory, openRebuildUIFactory?: OpenRebuildUIFactory): UseP2PReplicatorResult;
|
|
31
|
+
export declare function useP2PReplicatorFeature(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, openReplicationUIFactory?: OpenReplicationUIFactory, openRebuildUIFactory?: OpenRebuildUIFactory, options?: P2PReplicatorFeatureOptions): UseP2PReplicatorResult;
|
|
@@ -15,10 +15,13 @@ import {
|
|
|
15
15
|
supportedStopActiveTransfer
|
|
16
16
|
} from "../index.js";
|
|
17
17
|
import { getP2PReplicatorConfigurationIdentity } from "./p2pReplicatorConfigurationIdentity.js";
|
|
18
|
-
function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFactory) {
|
|
19
|
-
const service = createP2PService(
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFactory, options = {}) {
|
|
19
|
+
const service = createP2PService(
|
|
20
|
+
{
|
|
21
|
+
services: host.services
|
|
22
|
+
},
|
|
23
|
+
options
|
|
24
|
+
);
|
|
22
25
|
const replicator = service.compatibilityReplicator;
|
|
23
26
|
const { views, lifecycle } = service;
|
|
24
27
|
const configureReplicator = (instance) => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BaseRoomConfig } from "@trystero-p2p/nostr";
|
|
2
|
-
import type { P2PConnectionInfo } from "../../common/models/setting.type.js";
|
|
3
|
-
export declare function generateJoinRoomOptions(settings: P2PConnectionInfo): BaseRoomConfig;
|
|
2
|
+
import type { P2PConnectionInfo, P2PSyncSetting } from "../../common/models/setting.type.js";
|
|
3
|
+
export declare function generateJoinRoomOptions(settings: P2PConnectionInfo & Partial<Pick<P2PSyncSetting, "P2P_iceServers" | "P2P_iceServersExpiresAt">>): BaseRoomConfig;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { P2PConnectionPaths } from "../../common/models/setting.const.js";
|
|
2
2
|
import {
|
|
3
|
+
hasManagedP2PTurnConfiguration,
|
|
3
4
|
hasValidP2PTurnServerUrl,
|
|
4
5
|
normaliseP2PConnectionPath,
|
|
5
6
|
splitP2PRelayUrls,
|
|
@@ -8,11 +9,38 @@ import {
|
|
|
8
9
|
import { mixedHash } from "octagonal-wheels/hash/purejs";
|
|
9
10
|
import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
10
11
|
import { createDiagRTCPeerConnectionConstructor } from "./DiagRTCPeerConnections.js";
|
|
12
|
+
function containsTurnUrl(iceServers) {
|
|
13
|
+
return iceServers.some((server) => {
|
|
14
|
+
const urls = typeof server.urls === "string" ? [server.urls] : server.urls;
|
|
15
|
+
return urls.some((url) => /^turns?:/iu.test(url));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function copyIceServers(iceServers) {
|
|
19
|
+
return iceServers.map((server) => ({
|
|
20
|
+
urls: typeof server.urls === "string" ? server.urls : [...server.urls],
|
|
21
|
+
...server.username === void 0 ? {} : { username: server.username },
|
|
22
|
+
...server.credential === void 0 ? {} : { credential: server.credential }
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
11
25
|
function generateJoinRoomOptions(settings) {
|
|
12
26
|
const passphraseNumbers = mixedHash(settings.P2P_passphrase, 0);
|
|
13
27
|
const passphrase = passphraseNumbers[0].toString(36) + passphraseNumbers[1].toString(36);
|
|
14
28
|
const relays = splitP2PRelayUrls(settings.P2P_relays);
|
|
15
29
|
const turnServers = splitP2PTurnServerUrls(settings.P2P_turnServers);
|
|
30
|
+
const managedTurnSelected = hasManagedP2PTurnConfiguration(settings);
|
|
31
|
+
const preparedIceServers = settings.P2P_iceServers;
|
|
32
|
+
if (preparedIceServers !== void 0 && preparedIceServers.length === 0) {
|
|
33
|
+
throw new Error("Prepared ICE servers must not be empty.");
|
|
34
|
+
}
|
|
35
|
+
if (managedTurnSelected && preparedIceServers === void 0) {
|
|
36
|
+
throw new Error("The selected managed TURN configuration requires prepared ICE servers.");
|
|
37
|
+
}
|
|
38
|
+
if (managedTurnSelected && preparedIceServers && !containsTurnUrl(preparedIceServers)) {
|
|
39
|
+
throw new Error("The prepared ICE servers do not contain a TURN route.");
|
|
40
|
+
}
|
|
41
|
+
if (preparedIceServers !== void 0 && normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && !containsTurnUrl(preparedIceServers)) {
|
|
42
|
+
throw new Error("Relay-only P2P requires a prepared TURN route.");
|
|
43
|
+
}
|
|
16
44
|
const relayConfig = {
|
|
17
45
|
manualReconnection: true,
|
|
18
46
|
urls: relays
|
|
@@ -28,7 +56,9 @@ function generateJoinRoomOptions(settings) {
|
|
|
28
56
|
} else if (typeof compatGlobal.RTCPeerConnection !== "undefined") {
|
|
29
57
|
options.rtcPolyfill = compatGlobal.RTCPeerConnection;
|
|
30
58
|
}
|
|
31
|
-
if (
|
|
59
|
+
if (preparedIceServers !== void 0) {
|
|
60
|
+
options.turnConfig = copyIceServers(preparedIceServers);
|
|
61
|
+
} else if (!managedTurnSelected && turnServers.length > 0) {
|
|
32
62
|
options.turnConfig = [
|
|
33
63
|
{
|
|
34
64
|
urls: turnServers,
|
|
@@ -37,7 +67,7 @@ function generateJoinRoomOptions(settings) {
|
|
|
37
67
|
}
|
|
38
68
|
];
|
|
39
69
|
}
|
|
40
|
-
if (normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && hasValidP2PTurnServerUrl(settings.P2P_turnServers)) {
|
|
70
|
+
if (normaliseP2PConnectionPath(settings.P2P_connectionPath) === P2PConnectionPaths.Relay && (preparedIceServers !== void 0 ? containsTurnUrl(preparedIceServers) : !managedTurnSelected && hasValidP2PTurnServerUrl(settings.P2P_turnServers))) {
|
|
41
71
|
options.rtcConfig = {
|
|
42
72
|
iceTransportPolicy: "relay"
|
|
43
73
|
};
|
|
@@ -46,6 +46,9 @@ function migrateLegacyRemoteConfigurationsInPlace(settings, log) {
|
|
|
46
46
|
}
|
|
47
47
|
const preferredId = settings.remoteType === REMOTE_MINIO ? "legacy-s3" : settings.remoteType === REMOTE_P2P ? "legacy-p2p" : "legacy-couchdb";
|
|
48
48
|
settings.activeConfigurationId = settings.remoteConfigurations[preferredId] ? preferredId : createdIds[0];
|
|
49
|
+
if (settings.remoteConfigurations["legacy-p2p"]) {
|
|
50
|
+
settings.P2P_ActiveRemoteConfigurationId = "legacy-p2p";
|
|
51
|
+
}
|
|
49
52
|
return true;
|
|
50
53
|
}
|
|
51
54
|
function createRemoteConfigurationId() {
|
|
@@ -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
|
)
|
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
|
|