mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-15 12:40:12 +00:00
refactor: organise source files by ownership
Separate host-independent helpers under common/core and colocate feature support code with its owners. Update imports, tests, and source-layout guidance while preserving runtime behaviour.
This commit is contained in:
1 parent
9a0941647b
commit
73adfe3a74
115 files changed
+115
-103
No files matched your search
@@ -0,0 +1,32 @@
|
||||
type LegacyLocalDatabaseSelection = {
|
||||
useIndexedDBAdapter: boolean;
|
||||
};
|
||||
|
||||
type LegacyBulkChunkPreSendSettings = {
|
||||
sendChunksBulk: boolean;
|
||||
sendChunksBulkMaxSize: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether persisted settings select the legacy PouchDB IndexedDB adapter.
|
||||
*
|
||||
* New local databases use IDB. Existing devices must retain this operative value until their local database has
|
||||
* been explicitly migrated, so compatibility code must not treat the setting as inert.
|
||||
*/
|
||||
export function usesLegacyIndexedDBAdapter(settings: LegacyLocalDatabaseSelection): boolean {
|
||||
return settings.useIndexedDBAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the removed automatic bulk chunk pre-send option in persisted settings.
|
||||
*
|
||||
* The field remains readable only so older settings and Setup URIs can be migrated to the supported behaviour.
|
||||
*
|
||||
* @returns `true` when the legacy setting was enabled and has been changed.
|
||||
*/
|
||||
export function disableLegacyBulkChunkPreSend(settings: LegacyBulkChunkPreSendSettings): boolean {
|
||||
if (!settings.sendChunksBulk) return false;
|
||||
settings.sendChunksBulk = false;
|
||||
settings.sendChunksBulkMaxSize = 1;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { disableLegacyBulkChunkPreSend, usesLegacyIndexedDBAdapter } from "./compatibilitySettings.ts";
|
||||
|
||||
describe("compatibility settings", () => {
|
||||
it.each([true, false])("preserves the operative legacy adapter selection (%s)", (useIndexedDBAdapter) => {
|
||||
expect(usesLegacyIndexedDBAdapter({ useIndexedDBAdapter })).toBe(useIndexedDBAdapter);
|
||||
});
|
||||
|
||||
it("disables automatic bulk chunk pre-send and restores its inert size value", () => {
|
||||
const settings = { sendChunksBulk: true, sendChunksBulkMaxSize: 16 };
|
||||
|
||||
expect(disableLegacyBulkChunkPreSend(settings)).toBe(true);
|
||||
expect(settings).toEqual({ sendChunksBulk: false, sendChunksBulkMaxSize: 1 });
|
||||
});
|
||||
|
||||
it("leaves an already migrated bulk chunk setting unchanged", () => {
|
||||
const settings = { sendChunksBulk: false, sendChunksBulkMaxSize: 4 };
|
||||
|
||||
expect(disableLegacyBulkChunkPreSend(settings)).toBe(false);
|
||||
expect(settings).toEqual({ sendChunksBulk: false, sendChunksBulkMaxSize: 4 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createNewVaultSettings as createCommonlibNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
/** Create settings for a new LiveSync environment. */
|
||||
export function createNewVaultSettings(): ObsidianLiveSyncSettings {
|
||||
return {
|
||||
...createCommonlibNewVaultSettings(),
|
||||
pathNormalizationVersion: 1,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
NEW_VAULT_SETTINGS,
|
||||
SETTINGS_SCHEMA_DEFAULTS,
|
||||
createNewVaultSettings as createCommonlibNewVaultSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { createNewVaultSettings } from "./newVaultSettings.ts";
|
||||
|
||||
describe("createNewVaultSettings", () => {
|
||||
it("selects NFC path identity only for new host settings", () => {
|
||||
expect(SETTINGS_SCHEMA_DEFAULTS.pathNormalizationVersion).toBe(0);
|
||||
expect(NEW_VAULT_SETTINGS.pathNormalizationVersion).toBe(0);
|
||||
expect(createCommonlibNewVaultSettings().pathNormalizationVersion).toBe(0);
|
||||
expect(createNewVaultSettings().pathNormalizationVersion).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps remote configurations independent of shared and other new-Vault settings", () => {
|
||||
const first = createNewVaultSettings();
|
||||
const second = createNewVaultSettings();
|
||||
const shared = createCommonlibNewVaultSettings();
|
||||
|
||||
expect(first.remoteConfigurations).not.toBe(second.remoteConfigurations);
|
||||
expect(first.remoteConfigurations).not.toBe(shared.remoteConfigurations);
|
||||
expect(first.pluginSyncExtendedSetting).not.toBe(second.pluginSyncExtendedSetting);
|
||||
expect(first.pluginSyncExtendedSetting).not.toBe(shared.pluginSyncExtendedSetting);
|
||||
|
||||
first.remoteConfigurations.example = {
|
||||
id: "example",
|
||||
name: "Example",
|
||||
uri: "sls+http://localhost/",
|
||||
isEncrypted: false,
|
||||
};
|
||||
|
||||
expect(second.remoteConfigurations).toEqual({});
|
||||
expect(shared.remoteConfigurations).toEqual({});
|
||||
expect(NEW_VAULT_SETTINGS.remoteConfigurations).toEqual({});
|
||||
expect(NEW_VAULT_SETTINGS.pluginSyncExtendedSetting).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
import {
|
||||
REMOTE_P2P,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
/** Returns whether the selected main remote represents the P2P-only setup. */
|
||||
export function isP2PMainRemote(settings: ObsidianLiveSyncSettings): boolean {
|
||||
if (settings.remoteType === REMOTE_P2P) return true;
|
||||
|
||||
const activeId = settings.activeConfigurationId?.trim();
|
||||
const activeConfiguration = activeId ? settings.remoteConfigurations?.[activeId] : undefined;
|
||||
if (!activeConfiguration) return false;
|
||||
|
||||
try {
|
||||
return ConnectionStringParser.parse(activeConfiguration.uri).type === "p2p";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
hasManagedP2PTurnConfiguration,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { pickP2PSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { CLOUDFLARE_TURN_TYPE } from "@/integrations/cloudflare/settings";
|
||||
|
||||
/** Include inactive profiles when deciding whether Markdown would disclose provider settings. */
|
||||
export function hasManagedTurnSettings(settings: Partial<ObsidianLiveSyncSettings>): boolean {
|
||||
return (
|
||||
hasManagedP2PTurnConfiguration(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("managedType")
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** Reports retain a recognised provider label and omit issued credentials. */
|
||||
export function redactTurnSettingsForReport(settings: Partial<ObsidianLiveSyncSettings>): void {
|
||||
if (settings.P2P_managedType) {
|
||||
settings.P2P_managedType =
|
||||
settings.P2P_managedType === CLOUDFLARE_TURN_TYPE ? CLOUDFLARE_TURN_TYPE : "redacted";
|
||||
}
|
||||
if (settings.P2P_managedId !== undefined) settings.P2P_managedId = "redacted";
|
||||
if (settings.P2P_managedToken !== undefined) settings.P2P_managedToken = "redacted";
|
||||
delete settings.P2P_iceServers;
|
||||
delete settings.P2P_iceServersExpiresAt;
|
||||
}
|
||||
|
||||
/** Managed connection profiles are shared through Setup URIs and QR codes. */
|
||||
export function omitManagedTurnProfilesFromMarkdown(settings: Partial<ObsidianLiveSyncSettings>): void {
|
||||
delete settings.P2P_iceServers;
|
||||
delete settings.P2P_iceServersExpiresAt;
|
||||
if (!hasManagedTurnSettings(settings)) return;
|
||||
delete settings.P2P_managedType;
|
||||
delete settings.P2P_managedId;
|
||||
delete settings.P2P_managedToken;
|
||||
delete settings.remoteConfigurations;
|
||||
delete settings.activeConfigurationId;
|
||||
delete settings.P2P_ActiveRemoteConfigurationId;
|
||||
}
|
||||
|
||||
/** Preserve the complete connection when Markdown omits its profile group. */
|
||||
export function preserveManagedTurnProfilesOnMarkdownImport(
|
||||
incoming: Partial<ObsidianLiveSyncSettings>,
|
||||
current: ObsidianLiveSyncSettings,
|
||||
merged: ObsidianLiveSyncSettings
|
||||
): void {
|
||||
if (
|
||||
!hasManagedTurnSettings(current) ||
|
||||
incoming.remoteConfigurations !== undefined ||
|
||||
incoming.P2P_managedType !== undefined
|
||||
)
|
||||
return;
|
||||
merged.remoteConfigurations = structuredClone(current.remoteConfigurations);
|
||||
merged.activeConfigurationId = current.activeConfigurationId;
|
||||
merged.P2P_ActiveRemoteConfigurationId = current.P2P_ActiveRemoteConfigurationId;
|
||||
Object.assign(merged, pickP2PSyncSettings(current));
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { MemoryValueStore } from "@vrtmrz/value-store";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
REMOTE_P2P,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
SettingService,
|
||||
type SettingServiceDependencies,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
|
||||
import { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
import {
|
||||
hasManagedTurnSettings,
|
||||
omitManagedTurnProfilesFromMarkdown,
|
||||
preserveManagedTurnProfilesOnMarkdownImport,
|
||||
redactTurnSettingsForReport,
|
||||
} from "./turnPrivacy";
|
||||
|
||||
class MemorySettingService extends SettingService {
|
||||
saved?: ObsidianLiveSyncSettings;
|
||||
protected saveData(settings: ObsidianLiveSyncSettings) {
|
||||
this.saved = structuredClone(settings);
|
||||
return Promise.resolve();
|
||||
}
|
||||
protected loadData() {
|
||||
return Promise.resolve(this.saved);
|
||||
}
|
||||
}
|
||||
|
||||
function configuredSettings() {
|
||||
return {
|
||||
...DEFAULT_SETTINGS,
|
||||
P2P_managedType: "CF",
|
||||
P2P_managedId: "private-key-id",
|
||||
P2P_managedToken: "private-token",
|
||||
remoteConfigurations: {
|
||||
managed: {
|
||||
id: "managed",
|
||||
name: "Managed TURN",
|
||||
isEncrypted: false,
|
||||
uri: "sls+p2p://room?managedType=CF&managedId=private-key-id&token=private-token",
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "central",
|
||||
P2P_ActiveRemoteConfigurationId: "managed",
|
||||
};
|
||||
}
|
||||
|
||||
describe("managed TURN settings privacy", () => {
|
||||
it("preserves the active managed room through Markdown import, save, and reload", async () => {
|
||||
const current = {
|
||||
...configuredSettings(),
|
||||
remoteType: REMOTE_P2P,
|
||||
activeConfigurationId: "managed",
|
||||
P2P_roomID: "local-room",
|
||||
P2P_relays: "wss://local-relay.example.test",
|
||||
P2P_passphrase: "local-passphrase",
|
||||
};
|
||||
const originalURI = ConnectionStringParser.serialize({ type: "p2p", settings: current });
|
||||
current.remoteConfigurations.managed.uri = originalURI;
|
||||
const service = new MemorySettingService(new ServiceContext(), {
|
||||
deviceValues: new MemoryValueStore(),
|
||||
APIService: {
|
||||
getSystemVaultName: () => "test-vault",
|
||||
getAppID: () => "test-app",
|
||||
addLog: () => undefined,
|
||||
confirm: { askString: async () => "" },
|
||||
} as unknown as SettingServiceDependencies["APIService"],
|
||||
});
|
||||
service.settings = structuredClone(current);
|
||||
const incoming: Partial<ObsidianLiveSyncSettings> = {
|
||||
P2P_roomID: "imported-room",
|
||||
P2P_relays: "wss://imported-relay.example.test",
|
||||
P2P_passphrase: "imported-passphrase",
|
||||
};
|
||||
const merged = { ...structuredClone(DEFAULT_SETTINGS), ...incoming };
|
||||
preserveManagedTurnProfilesOnMarkdownImport(incoming, current, merged);
|
||||
await service.applyExternalSettings(merged, true);
|
||||
const saved = service.saved!.remoteConfigurations.managed;
|
||||
const uri = saved.isEncrypted ? await service.decryptConfigurationItem(saved.uri, "*") : saved.uri;
|
||||
expect(uri).toBe(originalURI);
|
||||
expect(service.settings.P2P_roomID).toBe("local-room");
|
||||
await service.loadSettings();
|
||||
expect(service.settings.P2P_roomID).toBe("local-room");
|
||||
});
|
||||
|
||||
it("redacts provider fields and issued credentials, including unknown integrations", () => {
|
||||
const settings = configuredSettings();
|
||||
settings.P2P_managedType = "private-token";
|
||||
redactTurnSettingsForReport(settings);
|
||||
expect([settings.P2P_managedType, settings.P2P_managedId, settings.P2P_managedToken]).toEqual([
|
||||
"redacted",
|
||||
"redacted",
|
||||
"redacted",
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits the whole managed profile group from Markdown, including inactive sources", () => {
|
||||
const settings = configuredSettings();
|
||||
settings.P2P_managedType = "";
|
||||
expect(hasManagedTurnSettings(settings)).toBe(true);
|
||||
omitManagedTurnProfilesFromMarkdown(settings);
|
||||
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");
|
||||
});
|
||||
|
||||
it("preserves existing profiles and both selections when Markdown omits the group", () => {
|
||||
const current = configuredSettings();
|
||||
const incoming = { ...DEFAULT_SETTINGS };
|
||||
delete (incoming as Partial<typeof incoming>).remoteConfigurations;
|
||||
delete (incoming as Partial<typeof incoming>).P2P_managedType;
|
||||
const merged = { ...DEFAULT_SETTINGS, ...incoming };
|
||||
preserveManagedTurnProfilesOnMarkdownImport(incoming, current, merged);
|
||||
expect(merged.remoteConfigurations).toEqual(current.remoteConfigurations);
|
||||
expect(merged.remoteConfigurations).not.toBe(current.remoteConfigurations);
|
||||
expect(merged.P2P_managedToken).toEqual(current.P2P_managedToken);
|
||||
expect(merged.activeConfigurationId).toBe("central");
|
||||
expect(merged.P2P_ActiveRemoteConfigurationId).toBe("managed");
|
||||
});
|
||||
|
||||
it("retains the manual-only Markdown contract", () => {
|
||||
const settings = { ...DEFAULT_SETTINGS };
|
||||
const before = structuredClone(settings);
|
||||
omitManagedTurnProfilesFromMarkdown(settings);
|
||||
expect(settings).toEqual(before);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user