mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-22 02:27:07 +00:00
Add optional Cloudflare TURN credentials and secure profile sharing
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { hasManagedTurnSettings } from "@/common/turnSettingsPrivacy";
|
||||
import { copySetupURI } from "./setupUri";
|
||||
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
|
||||
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
|
||||
import {
|
||||
encodeQR,
|
||||
@@ -9,7 +12,12 @@ import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import type { SetupFeatureHost } from "./types";
|
||||
|
||||
export async function encodeSetupSettingsAsQR(host: SetupFeatureHost) {
|
||||
const settingString = encodeSettingsToQRCodeData(host.services.setting.currentSettings());
|
||||
const settings = host.services.setting.currentSettings();
|
||||
if (hasManagedTurnSettings(settings)) {
|
||||
await copySetupURI(host, createInstanceLogFunction("SF:SetupQRCode", host.services.API));
|
||||
return "";
|
||||
}
|
||||
const settingString = encodeSettingsToQRCodeData(settings);
|
||||
const result = encodeQR(settingString, OutputFormat.SVG);
|
||||
if (result === "") {
|
||||
return "";
|
||||
|
||||
@@ -3,6 +3,9 @@ import { EVENT_REQUEST_SHOW_SETUP_QR } from "@vrtmrz/livesync-commonlib/compat/e
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { encodeSetupSettingsAsQR, useSetupQRCodeFeature } from "./qrCode";
|
||||
import { encodeQR, encodeSettingsToQRCodeData } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
|
||||
import { copySetupURI } from "./setupUri";
|
||||
|
||||
vi.mock("./setupUri", () => ({ copySetupURI: vi.fn() }));
|
||||
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => {
|
||||
return {
|
||||
@@ -15,6 +18,17 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => {
|
||||
});
|
||||
|
||||
describe("setupObsidian/qrCode", () => {
|
||||
it("routes inactive managed profiles through encrypted Setup URI sharing", async () => {
|
||||
const settings = {
|
||||
remoteConfigurations: { managed: { uri: "sls+p2p-v2://room?source=private-token" } },
|
||||
};
|
||||
const host = { services: { API: { addLog: vi.fn() }, setting: { currentSettings: () => settings } } } as any;
|
||||
await encodeSetupSettingsAsQR(host);
|
||||
expect(copySetupURI).toHaveBeenCalledWith(host, expect.any(Function));
|
||||
expect(encodeSettingsToQRCodeData).not.toHaveBeenCalled();
|
||||
expect(encodeQR).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -2,20 +2,23 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/
|
||||
import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
|
||||
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
|
||||
import type { SetupFeatureHost } from "@/serviceFeatures/setupObsidian/types";
|
||||
import { configURIBase } from "@/common/types";
|
||||
import { configURIBase, configURIBaseV2 } from "@/common/types";
|
||||
import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
|
||||
import { type SetupManager, UserMode } from "@/modules/features/SetupManager";
|
||||
|
||||
async function handleSetupProtocol(setupManager: SetupManager, conf: Record<string, string>) {
|
||||
async function handleSetupProtocol(setupManager: SetupManager, conf: Record<string, string>, uriBase = configURIBase) {
|
||||
if (conf.settings) {
|
||||
await setupManager.onUseSetupURI(UserMode.Unknown, `${configURIBase}${encodeURIComponent(conf.settings)}`);
|
||||
} else if (conf.settingsQR) {
|
||||
await setupManager.onUseSetupURI(UserMode.Unknown, `${uriBase}${encodeURIComponent(conf.settings)}`);
|
||||
} else if (conf.settingsQR && uriBase === configURIBase) {
|
||||
await setupManager.decodeQR(conf.settingsQR);
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSetupProtocolHandler(host: SetupFeatureHost, log: LogFunction, setupManager: SetupManager) {
|
||||
try {
|
||||
host.services.API.registerProtocolHandler("setuplivesync-v2", async (conf) => {
|
||||
await handleSetupProtocol(setupManager, conf, configURIBaseV2);
|
||||
});
|
||||
host.services.API.registerProtocolHandler("setuplivesync", async (conf) => {
|
||||
await handleSetupProtocol(setupManager, conf);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { registerSetupProtocolHandler, useSetupProtocolFeature } from "./setupPr
|
||||
vi.mock("@/common/types", () => {
|
||||
return {
|
||||
configURIBase: "mock-config://",
|
||||
configURIBaseV2: "mock-config-v2://",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -17,6 +18,19 @@ vi.mock("@/modules/features/SetupManager", () => {
|
||||
});
|
||||
|
||||
describe("setupObsidian/setupProtocol", () => {
|
||||
it("routes the versioned encrypted payload through the matching URI format", async () => {
|
||||
const handlers = new Map<string, (params: Record<string, string>) => Promise<void>>();
|
||||
const host = {
|
||||
services: { API: { registerProtocolHandler: vi.fn((action, handler) => handlers.set(action, handler)) } },
|
||||
} as any;
|
||||
const setupManager = { onUseSetupURI: vi.fn(), decodeQR: vi.fn() } as any;
|
||||
registerSetupProtocolHandler(host, vi.fn(), setupManager);
|
||||
await handlers.get("setuplivesync-v2")!({ settings: "encrypted settings" });
|
||||
expect(setupManager.onUseSetupURI).toHaveBeenCalledWith("unknown", "mock-config-v2://encrypted%20settings");
|
||||
await handlers.get("setuplivesync-v2")!({ settingsQR: "plain settings" });
|
||||
expect(setupManager.decodeQR).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -16,11 +16,16 @@ export async function askEncryptingPassphrase(host: SetupFeatureHost): Promise<s
|
||||
);
|
||||
}
|
||||
|
||||
export async function copySetupURI(host: SetupFeatureHost, log: LogFunction, stripExtra = true) {
|
||||
export async function copySetupURI(
|
||||
host: SetupFeatureHost,
|
||||
log: LogFunction,
|
||||
stripExtra = true,
|
||||
settings = host.services.setting.currentSettings()
|
||||
) {
|
||||
const encryptingPassphrase = await askEncryptingPassphrase(host);
|
||||
if (encryptingPassphrase === false) return;
|
||||
const encryptedURI = await encodeSettingsToSetupURI(
|
||||
host.services.setting.currentSettings(),
|
||||
settings,
|
||||
encryptingPassphrase,
|
||||
[...((stripExtra ? ["pluginSyncExtendedSetting"] : []) as (keyof ObsidianLiveSyncSettings)[])],
|
||||
true
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { CLOUDFLARE_ICE_SERVER_SOURCE_ID } from "@/integrations/cloudflare/settings";
|
||||
import type { IceServerSourceFactoryCatalogue } from "@vrtmrz/livesync-commonlib/p2p";
|
||||
import {
|
||||
createCloudflareIceServerSource,
|
||||
type CloudflareIceServerSourceFetch,
|
||||
} from "@/integrations/cloudflare/iceServerSource";
|
||||
|
||||
/**
|
||||
* Compose the closed LiveSync-owned ICE source catalogue from a host HTTP
|
||||
* adapter. The adapter is intentionally narrow so this feature does not take
|
||||
* a dependency on LiveSync core or on native request APIs.
|
||||
*/
|
||||
export function useIceServerSources(fetch: CloudflareIceServerSourceFetch): IceServerSourceFactoryCatalogue {
|
||||
return {
|
||||
[CLOUDFLARE_ICE_SERVER_SOURCE_ID]: (configuration) => createCloudflareIceServerSource(configuration, { fetch }),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user