diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountInfo.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountInfo.kt new file mode 100644 index 000000000..25454c7d9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountInfo.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.account + +import androidx.compose.runtime.Immutable + +/** + * Lightweight account metadata for account listing and switching. + * Does NOT contain private keys — those are stored in SecureKeyStorage. + */ +@Immutable +data class AccountInfo( + val npub: String, + val signerType: SignerType, + val isTransient: Boolean = false, +) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountStorage.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountStorage.kt new file mode 100644 index 000000000..9db0ccee5 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/AccountStorage.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.account + +/** + * Platform-agnostic interface for persisting account metadata. + * Implementations: + * - Android: LocalPreferences (EncryptedSharedPreferences) + * - Desktop: DesktopAccountStorage (encrypted JSON file) + * + * Does NOT handle private key storage — that's SecureKeyStorage's job. + * Constructor-injected, not expect/actual. + */ +interface AccountStorage { + /** Load all saved account metadata */ + suspend fun loadAccounts(): List + + /** Save or update account metadata */ + suspend fun saveAccount(info: AccountInfo) + + /** Delete account metadata by npub */ + suspend fun deleteAccount(npub: String) + + /** Get the npub of the currently active account, or null if none */ + suspend fun currentAccount(): String? + + /** Set which account is currently active */ + suspend fun setCurrentAccount(npub: String) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/SignerType.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/SignerType.kt new file mode 100644 index 000000000..da6ecbc0c --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/account/SignerType.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.model.account + +import androidx.compose.runtime.Immutable + +/** + * Represents how an account's signing is handled. + * Sealed class (not enum) because Remote carries additional data. + */ +@Immutable +sealed class SignerType { + /** Account has a local private key (nsec) */ + data object Internal : SignerType() + + /** Account uses a NIP-46 remote signer (bunker) */ + @Immutable + data class Remote( + val bunkerUri: String, + ) : SignerType() + + /** Account is view-only (npub only, no signing capability) */ + data object ViewOnly : SignerType() +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index f8c28cd80..84089985c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -670,7 +670,7 @@ fun App( val result = accountManager.loadSavedAccount() if (result.isSuccess) { val current = accountManager.currentAccount() - if (current?.signerType is com.vitorpamplona.amethyst.desktop.account.SignerType.Remote) { + if (current?.signerType is com.vitorpamplona.amethyst.commons.model.account.SignerType.Remote) { accountManager.startHeartbeat(scope) } } else if (accountManager.hasBunkerAccount()) { @@ -702,7 +702,7 @@ fun App( onLoginSuccess = { // Start heartbeat if bunker account val current = accountManager.currentAccount() - if (current?.signerType is com.vitorpamplona.amethyst.desktop.account.SignerType.Remote) { + if (current?.signerType is com.vitorpamplona.amethyst.commons.model.account.SignerType.Remote) { accountManager.startHeartbeat(scope) } }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt index 58df5131f..535be3a80 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.domain.nip46.NostrConnectLoginUseCase import com.vitorpamplona.amethyst.commons.domain.nip46.SignerConnectionState import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage import com.vitorpamplona.amethyst.commons.keystorage.SecureStorageException +import com.vitorpamplona.amethyst.commons.model.account.SignerType import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -65,14 +66,6 @@ import java.io.File import java.nio.file.Files import java.nio.file.attribute.PosixFilePermission -sealed class SignerType { - data object Internal : SignerType() - - data class Remote( - val bunkerUri: String, - ) : SignerType() -} - sealed class AccountState { data object LoggedOut : AccountState() diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerHeartbeatTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerHeartbeatTest.kt index b0733dc81..925e0fe1c 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerHeartbeatTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerHeartbeatTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.desktop.account import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage +import com.vitorpamplona.amethyst.commons.model.account.SignerType import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerKeyLoginTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerKeyLoginTest.kt index 525a8b468..78e52ca7a 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerKeyLoginTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerKeyLoginTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.desktop.account import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage +import com.vitorpamplona.amethyst.commons.model.account.SignerType import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip19Bech32.toNpub diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerLoadAccountTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerLoadAccountTest.kt index 005029e24..bb06fc493 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerLoadAccountTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerLoadAccountTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.desktop.account import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage +import com.vitorpamplona.amethyst.commons.model.account.SignerType import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip19Bech32.toNpub diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerNip46IsolationTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerNip46IsolationTest.kt index 33fe20c01..e06d841b3 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerNip46IsolationTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManagerNip46IsolationTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop.account import com.vitorpamplona.amethyst.commons.domain.nip46.SignerConnectionState import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage +import com.vitorpamplona.amethyst.commons.model.account.SignerType import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip19Bech32.toNpub