From ab55eb5affda12607ff6efc7fc9dbe6ce1bfb4f8 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Tue, 15 Sep 2026 17:18:40 +0000 Subject: [PATCH] Use existing Setup URI and QR sharing for TURN settings --- .../design_docs/renewable_turn_credentials.md | 59 ++++++++----------- docs/p2p.md | 13 ++-- docs/settings.md | 2 +- package-lock.json | 8 +-- package.json | 2 +- src/apps/cli/commands/runCommand.ts | 4 +- src/apps/cli/commands/runCommand.unit.spec.ts | 6 +- .../messages/LiveSyncProvisionalMessages.ts | 4 +- src/common/reportTool.unit.spec.ts | 4 +- src/common/turnSettingsPrivacy.ts | 2 +- src/common/turnSettingsPrivacy.unit.spec.ts | 4 +- src/common/types.ts | 2 +- src/features/P2PSync/TurnConfiguration.svelte | 2 +- .../SettingDialogue/PaneRemoteConfig.ts | 11 ---- .../SetupWizard/dialogs/UseSetupURI.svelte | 4 +- src/serviceFeatures/setupObsidian/qrCode.ts | 7 --- .../setupObsidian/qrCode.unit.spec.ts | 37 +++++++++--- .../setupObsidian/setupProtocol.ts | 11 ++-- .../setupObsidian/setupProtocol.unit.spec.ts | 14 ----- src/serviceFeatures/setupObsidian/setupUri.ts | 9 +-- 20 files changed, 89 insertions(+), 116 deletions(-) diff --git a/docs/design_docs/renewable_turn_credentials.md b/docs/design_docs/renewable_turn_credentials.md index 474142a0..958438e9 100644 --- a/docs/design_docs/renewable_turn_credentials.md +++ b/docs/design_docs/renewable_turn_credentials.md @@ -1,6 +1,6 @@ --- date: 2026-09-15 -commonlib-version: "0.1.25-dev.turn-credentials.3" +commonlib-version: "0.1.25-dev.turn-credentials.4" self-hosted-livesync-version: "1.0.28" status: unreleased --- @@ -151,21 +151,21 @@ and report a safe error; it must not silently save a plaintext replacement. | Destination | Provider API token | Issued TURN username and credential | | --- | --- | --- | | Saved P2P profile | Included | Omitted | -| Encrypted Setup URI | Included with the source and Key ID | Omitted | +| Setup URI and QR code | Included with the source and Key ID | Omitted | | Runtime room configuration | Available only to the source | Cached in memory and passed to WebRTC | | General report or diagnostic log | Redacted | Redacted | -Encrypted Setup URI sharing is the complete sharing route for managed -profiles. Preserve the independent main-remote and P2P selections and the -receiving device's own peer name. Raw profile and unencrypted QR copy actions -should offer encrypted Setup URI sharing when their output includes a managed -source, including one in an inactive profile. Never substitute a temporary -TURN password or silently export a profile missing its API token. +Setup URI, QR code, and profile sharing include the source configuration as +ordinary connection settings. Preserve the independent main-remote and P2P +selections and the receiving device's own peer name. Setup URIs keep their +existing passphrase encryption; QR codes keep their existing unencrypted +format and 'FOR YOUR EYES ONLY' display. Include managed sources in inactive +profiles as well. Issued temporary credentials remain runtime state. Markdown settings export must not leak tokens through either the top-level source or a profile URI. For this first delivery, omit the profile collection, its selections, and the source projection together when managed profiles are -present, and explain that connection sharing uses the encrypted Setup URI. +present, and explain that connection sharing uses Setup URIs or QR codes. Importing Markdown without that group preserves the local profiles and selections rather than replacing them with a filtered collection. @@ -404,31 +404,24 @@ LiveSync Setup dialogue, import handler, profile export, Markdown settings, and report paths. Existing fixed-field serialisers would otherwise discard the source. Generated credentials never populate the manual fields. -Managed profile strings need a distinguishable format, -`sls+p2p-v2://`. Commonlib `0.1.24` rejects that scheme, whereas it silently -drops unknown fields in ordinary `sls+p2p://` strings. Manual profiles retain -their current format. Validate the source before activation; unknown sources -must not become manual connections. +P2P profiles keep `sls+p2p://` and carry the optional source descriptor in an +additional `source` query parameter. Full Setup URIs keep +`obsidian://setuplivesync?settings=` and encrypt the existing settings object +directly. QR codes carry the same source through an appended setting index. +Existing setting indices and the established formats remain unchanged. -Full encrypted Setup URIs also need a distinguishable outer format, -`obsidian://setuplivesync-v2?settings=`, and a versioned encrypted -envelope when managed profiles are included. The old import path decrypts and -merges arbitrary JSON, so a nested profile version alone is insufficient. -Validate the new envelope before applying settings in every maintained host. -Apply stored settings schema checks on load and import, including downgrades; -older clients must not activate a managed profile after dropping its source. -Document any minimum-client and downgrade requirements with the implementation. +Missing source settings use the ordinary manual defaults. Older clients follow +their existing handling of additional fields; this feature adds no URI-version +gate or stored-settings restriction to prevent them from loading settings. +Clients which support source descriptors preserve unknown identifiers and +versions and validate them before activation, rather than silently selecting +manual TURN when an explicitly configured source is unsupported. -For a selected managed source, save the complete P2P connection in its -versioned profile and disable the persisted legacy P2P projection: clear its -Group ID and passphrase, and save `P2P_Enabled` and `P2P_AutoStart` as false. -A compatible client restores those runtime values from the selected profile. -This prevents an older client which rejects the profile URI from connecting -through leftover manual fields. Source-only settings without a configured -room can remain disabled until setup is complete. The live settings and -setting-saved notifications retain their usable runtime values. A selected -manual profile retains its established persisted representation, even when -another saved profile has a managed source. +Persist the ordinary P2P settings projection alongside its profile. Keep the +Group ID, enabled state, and autostart preference consistent with the current +settings. Optional configuration encryption continues to protect both the +profile and its source configuration. Issued credentials never populate the +persisted manual TURN fields. The P2P data protocol and Group ID remain unchanged. A peer using manually configured TURN can communicate with one using issued credentials; validate @@ -465,7 +458,7 @@ results contain no provider token, TURN username, or TURN credential. - Manual configuration, default STUN, and existing Setup URIs retain their behaviour. Unsupported managed sources fail explicitly. - Provider tokens survive restart, profile selection, optional configuration - encryption, and encrypted Setup URI sharing. Reports and logs reveal no + encryption, and ordinary Setup URI and QR code sharing. Reports and logs reveal no tokens or issued credentials, including inactive and encoded copies. - Issued credentials never enter persisted settings, exports, or reports. - Equivalent settings and valid credentials reuse the room. Expired diff --git a/docs/p2p.md b/docs/p2p.md index 1184409c..c2af90f8 100644 --- a/docs/p2p.md +++ b/docs/p2p.md @@ -53,13 +53,12 @@ particular TURN provider or operate a credential broker. See Cloudflare's [credential instructions](https://developers.cloudflare.com/realtime/turn/generate-credentials/) for creating a TURN key and its API token. -The API token is saved with the P2P profile. Use an encrypted Setup URI to share -it with your other devices. Managed profiles use the versioned Setup URI format -and require a client which supports that format; update receiving devices -before importing it. Older clients leave the saved managed P2P connection -disabled. Select and save a manual TURN configuration in a compatible client -before downgrading if P2P must remain usable. Plain QR export redirects to -encrypted Setup URI sharing. +The API token is saved with the P2P profile and included when sharing settings +through an existing Setup URI or QR code. Setup URIs retain their existing +passphrase encryption. QR codes retain their existing unencrypted format and +'FOR YOUR EYES ONLY' display. Missing source settings use the ordinary manual +configuration defaults. Receiving clients need support for the selected source +to acquire its temporary TURN credentials. Markdown settings omit the connection profile group when it contains a managed TURN source, including inactive profiles, and importing those omitted settings preserves this device's existing profiles. Diagnostic reports redact the source diff --git a/docs/settings.md b/docs/settings.md index 96fab9f8..84408af9 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -491,7 +491,7 @@ Setting key: P2P_iceServerSource Select **Manual** for the existing TURN server fields, or **Cloudflare** for a TURN Key ID and TURN Key API Token. The API token is persisted with the profile -and included in encrypted Setup URI sharing. Issued temporary credentials are +and included in Setup URI and QR code sharing. Issued temporary credentials are kept in memory only. Reports redact the source configuration. See [TURN credentials](p2p.md#turn-credentials) for sharing, expiry, and reconnect behaviour. diff --git a/package-lock.json b/package-lock.json index 6261b61d..4c6e006d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "file:.devmemo.local/packages/vrtmrz-livesync-commonlib-0.1.25-dev.turn-credentials.3.tgz", + "@vrtmrz/livesync-commonlib": "file:.devmemo.local/packages/vrtmrz-livesync-commonlib-0.1.25-dev.turn-credentials.4.tgz", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", @@ -4567,9 +4567,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.1.25-dev.turn-credentials.3", - "resolved": "file:.devmemo.local/packages/vrtmrz-livesync-commonlib-0.1.25-dev.turn-credentials.3.tgz", - "integrity": "sha512-hngE1zlNocD8IeMgRvssX4WN7SpINOyWOp9Mzs0PGM10yzVvYtR5WcVdxWtEIbaA71DLk9SPmgmQkgu+o7Oy8g==", + "version": "0.1.25-dev.turn-credentials.4", + "resolved": "file:.devmemo.local/packages/vrtmrz-livesync-commonlib-0.1.25-dev.turn-credentials.4.tgz", + "integrity": "sha512-q8jFJYzBzp7fsE+fU48A8gTvQ5DnJexkhReV8hUu2vhZvlV0e2ywUpPxkzx5KNNwSVpLSX96JCHRBLUvDMXKUQ==", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index 706922eb..3506aa9d 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "file:.devmemo.local/packages/vrtmrz-livesync-commonlib-0.1.25-dev.turn-credentials.3.tgz", + "@vrtmrz/livesync-commonlib": "file:.devmemo.local/packages/vrtmrz-livesync-commonlib-0.1.25-dev.turn-credentials.4.tgz", "@vrtmrz/obsidian-plugin-kit": "0.1.4", "@vrtmrz/ui-interactions": "0.1.2", "diff-match-patch": "^1.0.5", diff --git a/src/apps/cli/commands/runCommand.ts b/src/apps/cli/commands/runCommand.ts index 8e3bf50c..8f27b870 100644 --- a/src/apps/cli/commands/runCommand.ts +++ b/src/apps/cli/commands/runCommand.ts @@ -1,5 +1,5 @@ import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; -import { configURIBase, configURIBaseV2 } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const"; +import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const"; import { DEFAULT_SETTINGS, type FilePathWithPrefix, @@ -298,7 +298,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext throw new Error("setup requires one argument: "); } const setupURI = options.commandArgs[0].trim(); - if (!setupURI.startsWith(configURIBase) && !setupURI.startsWith(configURIBaseV2)) { + if (!setupURI.startsWith(configURIBase)) { throw new Error(`setup URI must start with ${configURIBase}`); } const passphrase = await standardIo.prompt("Enter setup URI passphrase: "); diff --git a/src/apps/cli/commands/runCommand.unit.spec.ts b/src/apps/cli/commands/runCommand.unit.spec.ts index f34e7cfd..3844cdf3 100644 --- a/src/apps/cli/commands/runCommand.unit.spec.ts +++ b/src/apps/cli/commands/runCommand.unit.spec.ts @@ -1,7 +1,7 @@ import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node"; import * as processSetting from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString"; -import { configURIBase, configURIBaseV2 } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const"; +import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const"; import { DEFAULT_SETTINGS, REMOTE_COUCHDB, @@ -419,7 +419,7 @@ describe("runCommand abnormal cases", () => { expect(appliedSettings.useIndexedDBAdapter).toBe(false); }); - it("setup imports managed TURN through the versioned encrypted URI", async () => { + it("setup imports managed TURN through the existing encrypted URI", async () => { const core = createCoreMock(); const source = { version: 1, @@ -434,7 +434,7 @@ describe("runCommand abnormal cases", () => { }, passphrase ); - expect(setupURI.startsWith(configURIBaseV2)).toBe(true); + expect(setupURI.startsWith(configURIBase)).toBe(true); expect(setupURI).not.toContain("private-token"); core.services.context.standardIo.prompt.mockResolvedValue(passphrase); await runCommand(makeOptions("setup", [setupURI]), { ...context, core }); diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index 5acfcb64..30508aa1 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -16,8 +16,8 @@ export const liveSyncProvisionalEnglishMessages = { "TURN Key ID": "TURN Key ID", "TURN Key API Token": "TURN Key API Token", "Unsupported TURN configuration": "Unsupported TURN configuration", - "The API token is saved with this profile and included in encrypted Setup URI sharing. Temporary TURN credentials are kept in memory only.": - "The API token is saved with this profile and included in encrypted Setup URI sharing. Temporary TURN credentials are kept in memory only.", + "The API token is saved with this profile and included in Setup URI and QR code sharing. Temporary TURN credentials are kept in memory only.": + "The API token is saved with this profile and included in Setup URI and QR code sharing. Temporary TURN credentials are kept in memory only.", "TURN relay only requires a TURN server or a configured credential source under Advanced Settings.": "TURN relay only requires a TURN server or a configured credential source under Advanced Settings.", "TURN relay only requires TURN configuration. Connection path has been restored to Automatic.": diff --git a/src/common/reportTool.unit.spec.ts b/src/common/reportTool.unit.spec.ts index 18afd7fe..e7c4170b 100644 --- a/src/common/reportTool.unit.spec.ts +++ b/src/common/reportTool.unit.spec.ts @@ -23,7 +23,7 @@ describe("TURN credentials in diagnostic reports", () => { id: "inactive", name: "Inactive TURN", isEncrypted: false, - uri: `sls+p2p-v2://room?source=${encodeURIComponent(JSON.stringify(source))}`, + uri: `sls+p2p://room?source=${encodeURIComponent(JSON.stringify(source))}`, }, }, }; @@ -34,7 +34,7 @@ describe("TURN credentials in diagnostic reports", () => { 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-v2://"); + expect(report.pluginConfig.remoteConfigurations.inactive.uri).toBe("sls+p2p://"); expect(settings.P2P_iceServerSource).toEqual(source); expect(settings.encryptedP2PIceServerSource).toBe("encrypted-private-copy"); }); diff --git a/src/common/turnSettingsPrivacy.ts b/src/common/turnSettingsPrivacy.ts index adc3ae32..4a9a51f2 100644 --- a/src/common/turnSettingsPrivacy.ts +++ b/src/common/turnSettingsPrivacy.ts @@ -21,7 +21,7 @@ export function redactTurnSourceForReport(settings: Partial): void { if (!hasManagedTurnSettings(settings)) return; delete settings.P2P_iceServerSource; diff --git a/src/common/turnSettingsPrivacy.unit.spec.ts b/src/common/turnSettingsPrivacy.unit.spec.ts index 5a56b967..0124028b 100644 --- a/src/common/turnSettingsPrivacy.unit.spec.ts +++ b/src/common/turnSettingsPrivacy.unit.spec.ts @@ -20,7 +20,7 @@ function configuredSettings() { id: "managed", name: "Managed TURN", isEncrypted: false, - uri: "sls+p2p-v2://room?source=private-token", + uri: "sls+p2p://room?source=private-token", }, }, activeConfigurationId: "central", @@ -42,7 +42,7 @@ describe("managed TURN settings privacy", () => { settings.P2P_iceServerSource.id = "manual"; expect(hasManagedTurnSettings(settings)).toBe(true); omitManagedTurnProfilesFromMarkdown(settings); - expect(JSON.stringify(settings)).not.toMatch(/private-token|private-key-id|sls\+p2p-v2/); + expect(JSON.stringify(settings)).not.toMatch(/private-token|private-key-id|sls\+p2p/); expect(settings).not.toHaveProperty("remoteConfigurations"); expect(settings).not.toHaveProperty("activeConfigurationId"); expect(settings).not.toHaveProperty("P2P_ActiveRemoteConfigurationId"); diff --git a/src/common/types.ts b/src/common/types.ts index 374e73d6..2be50128 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -51,7 +51,7 @@ export type queueItem = { export const FileWatchEventQueueMax = 10; -export { configURIBase, configURIBaseV2, configURIBaseQR } from "@vrtmrz/livesync-commonlib/compat/common/types"; +export { configURIBase, configURIBaseQR } from "@vrtmrz/livesync-commonlib/compat/common/types"; export { CHeader, diff --git a/src/features/P2PSync/TurnConfiguration.svelte b/src/features/P2PSync/TurnConfiguration.svelte index 780b4d68..8983b034 100644 --- a/src/features/P2PSync/TurnConfiguration.svelte +++ b/src/features/P2PSync/TurnConfiguration.svelte @@ -68,7 +68,7 @@ autocomplete={field.secret ? "new-password" : "off"} autocapitalize="off" spellcheck="false" /> {/each} -

{translate("The API token is saved with this profile and included in encrypted Setup URI sharing. Temporary TURN credentials are kept in memory only.")}

+

{translate("The API token is saved with this profile and included in Setup URI and QR code sharing. Temporary TURN credentials are kept in memory only.")}

{/if} {#if error}

{translateIfAvailable(error)}

diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index 209f3eaa..260ec3c0 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -1,5 +1,3 @@ -import { copySetupURI } from "@/serviceFeatures/setupObsidian/setupUri"; -import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; import { REMOTE_COUCHDB, REMOTE_MINIO, @@ -418,15 +416,6 @@ export function paneRemoteConfig( }) .addItem((item) => { item.setTitle("📤 Export").onClick(async () => { - if (config.uri.startsWith("sls+p2p-v2://")) { - await copySetupURI( - this.core, - createInstanceLogFunction("TURN setup sharing", this.services.API), - true, - getSettingsFromEditingSettings(this.editingSettings) - ); - return; - } await this.services.UI.promptCopyToClipboard( `Remote configuration: ${config.name}`, config.uri diff --git a/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte b/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte index 08208e1d..dcd29126 100644 --- a/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte +++ b/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte @@ -1,5 +1,5 @@