Use profile storage for TURN source settings

This commit is contained in:
vorotamoroz
2026-09-15 18:28:20 +00:00
parent ab55eb5aff
commit a565070809
11 changed files with 42 additions and 57 deletions
@@ -5,23 +5,24 @@
import type { P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { P2PReplicatorPaneHost } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneHost";
import TurnConfiguration from "@/features/P2PSync/TurnConfiguration.svelte";
import { validateTurnSettings } from "@/integrations/iceServerSources";
import { validateIceServerSourceConfiguration } from "@/integrations/iceServerSources";
let { host }: { host: P2PReplicatorPaneHost } = $props();
const currentSettings = () => host.services.setting.currentSettings() as P2PSyncSetting;
function turnSettings(settings: P2PSyncSetting) {
return {
P2P_roomID: settings.P2P_roomID,
P2P_turnServers: settings.P2P_turnServers,
P2P_turnUsername: settings.P2P_turnUsername,
P2P_turnCredential: settings.P2P_turnCredential,
P2P_iceServerSource: structuredClone(settings.P2P_iceServerSource),
encryptedP2PIceServerSource: settings.encryptedP2PIceServerSource,
};
}
let draft = $state(turnSettings(currentSettings()));
let saved = $state(JSON.stringify(turnSettings(currentSettings())));
const isModified = $derived(JSON.stringify(draft) !== saved);
const sourceError = $derived(validateTurnSettings(draft));
const sourceError = $derived(validateIceServerSourceConfiguration(draft.P2P_iceServerSource));
const sourceNeedsRoom = $derived(!!draft.P2P_iceServerSource && (draft.P2P_roomID ?? "").trim() === "");
function loadSettings(settings: P2PSyncSetting): void {
const next = turnSettings(settings);
@@ -31,7 +32,7 @@
onMount(() => host.services.context.events.onEvent("setting-saved", (settings) => loadSettings(settings as P2PSyncSetting)));
async function save(): Promise<void> {
if (sourceError) return;
if (sourceError || sourceNeedsRoom) return;
const values = $state.snapshot(draft);
await host.services.setting.updateSettings((settings) => {
const next = { ...settings, ...values, remoteConfigurations: { ...settings.remoteConfigurations } };
@@ -52,7 +53,7 @@
<p>Configure TURN only when a direct peer-to-peer connection cannot be established.</p>
<TurnConfiguration bind:settings={draft} />
<div class="actions">
<button type="button" class="button mod-cta" disabled={!isModified || !!sourceError} onclick={save}>
<button type="button" class="button mod-cta" disabled={!isModified || !!sourceError || sourceNeedsRoom} onclick={save}>
Save TURN settings
</button>
<button type="button" class="button" disabled={!isModified} onclick={() => loadSettings(currentSettings())}>
@@ -9,7 +9,6 @@
export const liveSyncProvisionalEnglishMessages = {
"Configure TURN when a direct connection cannot be established or when you select TURN relay only.":
"Configure TURN when a direct connection cannot be established or when you select TURN relay only.",
"TURN configuration could not be decrypted.": "TURN configuration could not be decrypted.",
"TURN configuration": "TURN configuration",
Manual: "Manual",
Cloudflare: "Cloudflare",
+1 -4
View File
@@ -10,14 +10,13 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions", () => ({
}));
describe("TURN credentials in diagnostic reports", () => {
it("redacts top-level, encrypted, and inactive encoded source copies", async () => {
it("redacts top-level and inactive encoded source copies", async () => {
const token = "private+token/with=symbols";
const source = { version: 1, id: "cloudflare", configuration: { turnKeyId: "private-key", apiToken: token } };
const settings = {
...DEFAULT_SETTINGS,
remoteType: REMOTE_P2P,
P2P_iceServerSource: source,
encryptedP2PIceServerSource: "encrypted-private-copy",
remoteConfigurations: {
inactive: {
id: "inactive",
@@ -33,9 +32,7 @@ describe("TURN credentials in diagnostic reports", () => {
expect(text).not.toContain(token);
expect(text).not.toContain(encodeURIComponent(token));
expect(text).not.toContain("private-key");
expect(text).not.toContain("encrypted-private-copy");
expect(report.pluginConfig.remoteConfigurations.inactive.uri).toBe("sls+p2p://");
expect(settings.P2P_iceServerSource).toEqual(source);
expect(settings.encryptedP2PIceServerSource).toBe("encrypted-private-copy");
});
});
+12 -5
View File
@@ -1,15 +1,24 @@
import {
hasManagedP2PIceServerSource as hasManagedTurnSettings,
hasManagedP2PIceServerSource,
type ObsidianLiveSyncSettings,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { iceServerSourceDefinitions } from "@/integrations/iceServerSources";
export { hasManagedTurnSettings };
/** Include inactive profiles when deciding whether Markdown would disclose source settings. */
export function hasManagedTurnSettings(settings: Partial<ObsidianLiveSyncSettings>): boolean {
return (
hasManagedP2PIceServerSource(settings) ||
Object.values(settings.remoteConfigurations ?? {}).some(({ uri }) => {
if (!uri.startsWith("sls+p2p://")) return false;
const queryStart = uri.indexOf("?");
return queryStart >= 0 && new URLSearchParams(uri.slice(queryStart + 1).split("#", 1)[0]).has("source");
})
);
}
/** Reports retain the selected source label, but no opaque source configuration. */
export function redactTurnSourceForReport(settings: Partial<ObsidianLiveSyncSettings>): void {
if (settings.encryptedP2PIceServerSource) settings.encryptedP2PIceServerSource = "REDACTED";
if (settings.P2P_iceServerSource !== undefined) {
settings.P2P_iceServerSource = {
version: 1,
@@ -25,7 +34,6 @@ export function redactTurnSourceForReport(settings: Partial<ObsidianLiveSyncSett
export function omitManagedTurnProfilesFromMarkdown(settings: Partial<ObsidianLiveSyncSettings>): void {
if (!hasManagedTurnSettings(settings)) return;
delete settings.P2P_iceServerSource;
delete settings.encryptedP2PIceServerSource;
delete settings.remoteConfigurations;
delete settings.activeConfigurationId;
delete settings.P2P_ActiveRemoteConfigurationId;
@@ -48,5 +56,4 @@ export function preserveManagedTurnProfilesOnMarkdownImport(
merged.activeConfigurationId = current.activeConfigurationId;
merged.P2P_ActiveRemoteConfigurationId = current.P2P_ActiveRemoteConfigurationId;
merged.P2P_iceServerSource = structuredClone(current.P2P_iceServerSource);
merged.encryptedP2PIceServerSource = current.encryptedP2PIceServerSource;
}
@@ -1,21 +1,19 @@
<script lang="ts">
import type { P2PConnectionInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { iceServerSourceDefinitions, validateTurnSettings } from "@/integrations/iceServerSources";
import { iceServerSourceDefinitions, validateIceServerSourceConfiguration } from "@/integrations/iceServerSources";
import { translateLiveSyncMessage as translate, translateIfAvailable } from "@/common/translation";
type TurnSettings = Pick<P2PConnectionInfo,
"P2P_turnServers" | "P2P_turnUsername" | "P2P_turnCredential" | "P2P_iceServerSource" | "encryptedP2PIceServerSource">;
type TurnSettings = Pick<P2PConnectionInfo, "P2P_turnServers" | "P2P_turnUsername" | "P2P_turnCredential" | "P2P_iceServerSource">;
let { settings = $bindable() }: { settings: TurnSettings } = $props();
const sourceId = $derived(settings.P2P_iceServerSource?.id ?? (settings.encryptedP2PIceServerSource ? "unavailable" : "manual"));
const sourceId = $derived(settings.P2P_iceServerSource?.id ?? "manual");
const definition = $derived(iceServerSourceDefinitions.find((source) => source.id === sourceId));
const error = $derived(validateTurnSettings(settings));
const error = $derived(validateIceServerSourceConfiguration(settings.P2P_iceServerSource));
function selectSource(id: string) {
const selected = iceServerSourceDefinitions.find((source) => source.id === id);
settings.P2P_iceServerSource = selected
? { version: 1, id, configuration: Object.fromEntries(selected.fields.map((field) => [field.key, ""])) }
: undefined;
settings.encryptedP2PIceServerSource = "";
}
function fieldValue(key: string): string {
-11
View File
@@ -72,14 +72,3 @@ export function validateIceServerSourceConfiguration(
export function getIceServerSourceDefinition(id: string): IceServerSourceDefinition | undefined {
return iceServerSourceDefinitions.find((definition) => definition.id === id);
}
/** Validate the selected settings projection, including an unavailable encrypted source. */
export function validateTurnSettings(settings: {
readonly P2P_iceServerSource?: IceServerSourceDescriptorLike | null;
readonly encryptedP2PIceServerSource?: string;
}): string | undefined {
if (!settings.P2P_iceServerSource && settings.encryptedP2PIceServerSource) {
return "TURN configuration could not be decrypted.";
}
return validateIceServerSourceConfiguration(settings.P2P_iceServerSource);
}
@@ -2,17 +2,9 @@ import { describe, expect, it } from "vitest";
import {
iceServerSourceDefinitions,
validateIceServerSourceConfiguration,
validateTurnSettings,
} from "./iceServerSources";
describe("ICE server source catalogue", () => {
it("blocks an unavailable encrypted source instead of presenting manual settings as valid", () => {
expect(validateTurnSettings({ encryptedP2PIceServerSource: "private-ciphertext" })).toBe(
"TURN configuration could not be decrypted."
);
expect(validateTurnSettings({})).toBeUndefined();
});
it("describes the Cloudflare fields without owning manual TURN fields", () => {
expect(iceServerSourceDefinitions).toEqual([
{
@@ -1,6 +1,6 @@
<script lang="ts">
import TurnConfiguration from "@/features/P2PSync/TurnConfiguration.svelte";
import { validateTurnSettings } from "@/integrations/iceServerSources";
import { validateIceServerSourceConfiguration } from "@/integrations/iceServerSources";
// import { delay } from "octagonal-wheels/promises";
import DialogHeader from "@/modules/services/LiveSyncUI/components/DialogHeader.svelte";
import Guidance from "@/modules/services/LiveSyncUI/components/Guidance.svelte";
@@ -101,7 +101,7 @@
async function checkConnection() {
try {
processing = true;
const sourceError = validateTurnSettings(syncSetting);
const sourceError = validateIceServerSourceConfiguration(syncSetting.P2P_iceServerSource);
if (sourceError) return sourceError;
const trialRemoteSetting = generateSetting();
const admission = connectionProbe;
@@ -207,7 +207,7 @@
}
}
function commit() {
error = validateTurnSettings(syncSetting) ?? "";
error = validateIceServerSourceConfiguration(syncSetting.P2P_iceServerSource) ?? "";
if (error) return;
const setting = pickP2PSyncSettings(generateSetting());
setResult(setting);
@@ -221,7 +221,7 @@
syncSetting.P2P_roomID.trim() !== "" &&
syncSetting.P2P_passphrase.trim() !== "" &&
(syncSetting.P2P_DevicePeerName ?? "").trim() !== "" &&
validateTurnSettings(syncSetting) === undefined
validateIceServerSourceConfiguration(syncSetting.P2P_iceServerSource) === undefined
);
});
</script>