diff --git a/src/modules/features/SetupManager.ts b/src/modules/features/SetupManager.ts index 51edf441..b5b88410 100644 --- a/src/modules/features/SetupManager.ts +++ b/src/modules/features/SetupManager.ts @@ -5,6 +5,7 @@ import { LOG_LEVEL_VERBOSE, } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings"; +import { generateAdaptiveJournalRepositoryIdV1 } from "@vrtmrz/livesync-commonlib/adaptive-journal"; import { defaultRemoteProviderRegistry, upsertRemoteConfigurationInPlace, @@ -71,6 +72,29 @@ export const enum UserMode { Update = "unknown", // Alias for Unknown for better readability } +type AdaptiveJournalIdentitySetting = { + expectedRepositoryId?: string; + journalFormat?: string; +}; + +async function prepareAdaptiveRepositoryIdentityForSetup( + settings: T, + userMode: UserMode +): Promise { + const adaptiveSettings = settings as T & AdaptiveJournalIdentitySetting; + if ( + userMode !== UserMode.NewUser || + adaptiveSettings.journalFormat !== "adaptive-v1" || + (adaptiveSettings.expectedRepositoryId ?? "").trim() !== "" + ) { + return settings; + } + return { + ...settings, + expectedRepositoryId: await generateAdaptiveJournalRepositoryIdV1(), + }; +} + /** * Setup Manager to handle onboarding and configuration setup */ @@ -210,10 +234,15 @@ export class SetupManager extends AbstractModule { } const newSetting = copySettingsForRemoteProfileUpdate(currentSetting); + const preparedSettings = await prepareAdaptiveRepositoryIdentityForSetup(configuration.settings, userMode); + const preparedConfiguration = { + ...configuration, + settings: preparedSettings, + } as BuiltInRemoteConfiguration; if (activate) { - defaultRemoteProviderRegistry.applyConfiguration(newSetting, configuration); + defaultRemoteProviderRegistry.applyConfiguration(newSetting, preparedConfiguration); } else { - Object.assign(newSetting, configuration.settings); + Object.assign(newSetting, preparedSettings); } const activateForP2P = defaultRemoteProviderRegistry.supportsActivationRole(type, "p2p"); diff --git a/src/modules/features/SetupManager.unit.spec.ts b/src/modules/features/SetupManager.unit.spec.ts index 74eb2f6d..3df59188 100644 --- a/src/modules/features/SetupManager.unit.spec.ts +++ b/src/modules/features/SetupManager.unit.spec.ts @@ -462,6 +462,60 @@ describe("SetupManager", () => { expect(nextSettings).toEqual(expect.objectContaining({ ...bucketSettings, remoteType: REMOTE_MINIO })); }); + it("preselects a repository identity during fresh Adaptive Object Storage onboarding", async () => { + const { manager, setting, dialogManager } = createSetupManager(); + dialogManager.openWithExplicitCancel + .mockResolvedValueOnce({ + endpoint: "https://storage.example", + accessKey: "key", + secretKey: "secret", + bucket: "notes", + region: "auto", + bucketPrefix: "", + useCustomRequestHandler: false, + bucketCustomHeaders: "", + forcePathStyle: true, + expectedRepositoryId: "", + journalFormat: "adaptive-v1", + packReadPolicy: "whole-pack", + }) + .mockResolvedValueOnce(true); + + await manager.onBucketManualSetup(UserMode.NewUser, setting.currentSettings()); + + const current = setting.currentSettings(); + expect(current.expectedRepositoryId).toMatch(/^[A-Za-z0-9_-]{43}$/u); + const activeProfile = current.remoteConfigurations[current.activeConfigurationId]; + expect(activeProfile?.uri).toContain(`expectedRepositoryId=${current.expectedRepositoryId}`); + }); + + it("leaves an existing-device Adaptive Object Storage attachment on trust on first use", async () => { + const { manager, setting, dialogManager } = createSetupManager(); + dialogManager.openWithExplicitCancel + .mockResolvedValueOnce({ + endpoint: "https://storage.example", + accessKey: "key", + secretKey: "secret", + bucket: "notes", + region: "auto", + bucketPrefix: "", + useCustomRequestHandler: false, + bucketCustomHeaders: "", + forcePathStyle: true, + expectedRepositoryId: "", + journalFormat: "adaptive-v1", + packReadPolicy: "whole-pack", + }) + .mockResolvedValueOnce(true); + + await manager.onBucketManualSetup(UserMode.ExistingUser, setting.currentSettings()); + + const current = setting.currentSettings(); + expect(current.expectedRepositoryId).toBe(""); + const activeProfile = current.remoteConfigurations[current.activeConfigurationId]; + expect(activeProfile?.uri).not.toContain("expectedRepositoryId="); + }); + it("creates and selects a P2P profile during fresh manual onboarding", async () => { const { manager, setting, dialogManager } = createSetupManager(); setting.settings = {