feat(sign-in): an npub, recognised, confirmed as read-only, then written
Phase 4 of docs/npub-sign-in.md. The field accepts a third thing. CredentialParser recognises npub1... and nostr:npub1... as SignInCredential.NostrPublicKey: thirty-two bytes under the npub prefix that name a point on the curve -- the counterpart of the isValid() a secret is checked with, since not every x coordinate has a point above it. PublicKeyOnly goes, and its string with it; an npub that does not decode, or names no point, is InvalidKey. Hex stays a secret: a private key and an x-only public key are the same size, the confirm step shows the derived npub so a public key pasted as hex is visible for what it is, and guessing would never fire when it mattered, since an x coordinate is almost always also a valid scalar. The test pins the vector's own public key, pasted as hex, landing on a different npub. The confirm step's third arm says what the user is about to get and not get, once, before the choice: this profile and the people it follows; messages closed and nothing sent; paste the nsec later to open it. The landing caption, the field's label and its placeholder name the third input, the last with "to look around" for a user who does not know the word read-only. The view model takes the third writer the way it takes the other two and commits through one shared write-and-map. signInToProfile is idempotent by pubkey. Until now nothing called it twice for one key -- the writers refused a second sign-in before it got there. Pasting the nsec of a key held read-only is the first path that signs in a pubkey whose account already exists, and a second placeholder would be two kind-0 rows for one pubkey, two accounts disagreeing about which is this one. A repository test signs in twice and counts one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Pulled-From: curated/curated@bfad1f39a2
This commit is contained in:
@@ -264,7 +264,6 @@
|
||||
<string name="this_will_be_shown_when_people_open_your">This will be shown when people open your profile.</string>
|
||||
<string name="this_will_be_the_display_name_for_this_chat">This will be the display name for this chat room.</string>
|
||||
<string name="this_will_be_the_display_name_for_your">This will be the display name for your profile and also important for search.</string>
|
||||
<string name="this_will_give_you_read_only_access_to_the">This will give you read only access to the profile.</string>
|
||||
<string name="read_only">Read only</string>
|
||||
<string name="this_will_give_you_write_access_to_the">This will give you write access to the profile.</string>
|
||||
<string name="torch_will_be_broadcast_what_you_publish_to">Mantra broadcasts what you publish to a distributed set of relays, so it stays decentralised.</string>
|
||||
@@ -649,13 +648,13 @@
|
||||
<string name="copied_the_raw_json">Copied the raw JSON</string>
|
||||
<!-- Sign in: docs/nsec-sign-in.md, phase 4. -->
|
||||
<string name="a_password_protected_key_ncryptsec_is_not">A password-protected key (ncryptsec) is not supported yet. Decrypt it in the app it came from and paste the nsec.</string>
|
||||
<string name="an_npub_is_a_public_key_mantra_needs_the">An npub is a public key. Mantra needs the secret key, the nsec, to sign as you.</string>
|
||||
<string name="enter_a_recovery_phrase_or_an_nsec_to_sign_in">Enter a recovery phrase or an nsec to sign in.</string>
|
||||
<string name="paste">Paste</string>
|
||||
<string name="recognised_as_a_nostr_secret_key_no_wallet">Recognised as a nostr secret key. No wallet comes with it.</string>
|
||||
<string name="recognised_as_a_recovery_phrase_it_also">Recognised as a recovery phrase. It also restores the wallet.</string>
|
||||
<string name="recovery_phrase_or_nsec">Recovery phrase or nsec</string>
|
||||
<string name="sign_in_with_a_recovery_phrase_or_an_nsec">Sign in with a recovery phrase or an nsec</string>
|
||||
<string name="recognised_as_a_public_key_you_will_see">Recognised as a public key. You will see this profile and the people it follows; messages stay closed and nothing can be sent. Paste the nsec later to open it.</string>
|
||||
<string name="recovery_phrase_nsec_or_npub">Recovery phrase, nsec or npub</string>
|
||||
<string name="sign_in_with_a_recovery_phrase_an_nsec_or">Sign in with a recovery phrase, an nsec, or an npub to look around</string>
|
||||
<string name="signed_in">Signed in</string>
|
||||
<string name="signing_you_in">Signing you in…</string>
|
||||
<string name="that_is_not_a_recovery_phrase_or_a_nostr_key">That is not a recovery phrase or a nostr key.</string>
|
||||
@@ -664,7 +663,7 @@
|
||||
<string name="the_key_could_not_be_saved_on_this_device">The key could not be saved on this device.</string>
|
||||
<string name="the_words_or_the_key_never_leave_this_device">Whatever you paste stays on this device. Mantra checks it, shows you the profile it opens, and writes nothing until you confirm.</string>
|
||||
<string name="this_key_is_already_on_this_device">This key is already on this device.</string>
|
||||
<string name="twelve_words_or_nsec1">Twelve words, or nsec1…</string>
|
||||
<string name="twelve_words_nsec1_or_npub1">Twelve words, nsec1… or npub1…</string>
|
||||
<string name="use_a_different_key">Use a different key</string>
|
||||
<string name="you_are_signing_in_as">You are signing in as</string>
|
||||
<!-- Sign in: the search that found nothing, docs/nsec-sign-in.md, phase 5. -->
|
||||
|
||||
@@ -258,6 +258,17 @@ class DatabaseNostrRepository(
|
||||
override suspend fun signInToProfile(
|
||||
publicKey: HexKey
|
||||
) {
|
||||
// Idempotent by pubkey. Until the read-only kind, nothing called this twice for
|
||||
// one key -- the writers refused a second sign-in before it got here. Pasting the
|
||||
// nsec of a key held read-only is the first path that signs in a pubkey whose
|
||||
// account already exists, and a second placeholder would be two kind-0 rows for
|
||||
// one pubkey: two accounts disagreeing about which is this one, the failure
|
||||
// `setUpProfileForExistingKey`'s own comment describes.
|
||||
if (database.unsignedNostrEventDao().getLocalAccounts().any { it.unsignedNostrEvent?.pubKey == publicKey }) {
|
||||
logger.d("signInToProfile: $publicKey already has an account here, nothing to plant")
|
||||
return
|
||||
}
|
||||
|
||||
val profileEventTemplate = MetadataEvent.createNew()
|
||||
|
||||
saveUnsignedNostrEvent(
|
||||
|
||||
@@ -2,16 +2,18 @@ package press.mantra.compose.identity
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.MnemonicCode
|
||||
import fr.acinq.bitcoin.PrivateKey
|
||||
import fr.acinq.bitcoin.XonlyPublicKey
|
||||
import fr.acinq.phoenix.managers.nostrPublicKeyHex
|
||||
import fr.acinq.phoenix.utils.MnemonicLanguage
|
||||
|
||||
/**
|
||||
* What a user pasted into the sign-in field, once it has been recognised.
|
||||
*
|
||||
* Both carry the nostr public key, because the confirm step shows it and the duplicate
|
||||
* check compares it. Neither prints its secret.
|
||||
* All three carry the nostr public key, because the confirm step shows it and the
|
||||
* duplicate check compares it. None prints a secret.
|
||||
*/
|
||||
sealed interface SignInCredential {
|
||||
val nostrPublicKey: HexKey
|
||||
@@ -31,6 +33,11 @@ sealed interface SignInCredential {
|
||||
) : SignInCredential {
|
||||
override fun toString(): String = "NostrSecret(npub=$nostrPublicKey, key=<redacted>)"
|
||||
}
|
||||
|
||||
/** A bare nostr public key. Nothing can be signed with it: a profile to look at. */
|
||||
data class NostrPublicKey(
|
||||
override val nostrPublicKey: HexKey,
|
||||
) : SignInCredential
|
||||
}
|
||||
|
||||
/** Why an input was not accepted. The screen turns each into a sentence. */
|
||||
@@ -44,12 +51,9 @@ enum class CredentialProblem {
|
||||
/** Words, but the checksum or the wordlist says no: a wrong word, or one out of order. */
|
||||
InvalidPhrase,
|
||||
|
||||
/** An nsec or hex that does not decode to a valid secp256k1 secret. */
|
||||
/** An nsec or hex that does not decode to a valid secp256k1 secret, or an npub that does not decode to a point on the curve. */
|
||||
InvalidKey,
|
||||
|
||||
/** An npub. A public key cannot sign, and every write path here assumes a key that can. */
|
||||
PublicKeyOnly,
|
||||
|
||||
/** An ncryptsec (NIP-49). Decrypting one is a follow-on; say so rather than fail opaquely. */
|
||||
EncryptedKey,
|
||||
|
||||
@@ -63,11 +67,18 @@ enum class CredentialProblem {
|
||||
/**
|
||||
* Recognises a pasted credential. Pure, so it can be pinned row by row.
|
||||
*
|
||||
* A user does not choose an input type; they paste what they have. The two accepted
|
||||
* shapes are unambiguous -- words have spaces, keys do not -- and the two refused
|
||||
* shapes are named for what they are rather than lumped in with garbage, because a
|
||||
* user who pasted an npub or an ncryptsec did something reasonable and should be told
|
||||
* what to do instead.
|
||||
* A user does not choose an input type; they paste what they have. The three accepted
|
||||
* shapes are unambiguous -- words have spaces, keys do not, and a bech32 prefix says
|
||||
* which key -- and the one refused shape is named for what it is rather than lumped in
|
||||
* with garbage, because a user who pasted an ncryptsec did something reasonable and
|
||||
* should be told what to do instead.
|
||||
*
|
||||
* **Hex stays a secret.** A private key and an x-only public key are both thirty-two
|
||||
* bytes, and sixty-four hex characters cannot say which. The confirm step shows the
|
||||
* derived npub, so a public key pasted as hex is visible for what it is; guessing --
|
||||
* "not a valid secret, so try it as a public key" -- would never fire when it mattered,
|
||||
* since an x coordinate is, with overwhelming probability, also a valid scalar. An npub
|
||||
* has to arrive as an npub.
|
||||
*/
|
||||
object CredentialParser {
|
||||
|
||||
@@ -91,7 +102,7 @@ object CredentialParser {
|
||||
// prefix is how a key arrives from a link.
|
||||
val token = text.removePrefix("nostr:").lowercase()
|
||||
return when {
|
||||
token.startsWith("npub1") -> Result.Refused(CredentialProblem.PublicKeyOnly)
|
||||
token.startsWith("npub1") -> npub(token)
|
||||
token.startsWith("ncryptsec1") -> Result.Refused(CredentialProblem.EncryptedKey)
|
||||
token.startsWith("nsec1") -> nsec(token)
|
||||
hex64.matches(token) -> secret(runCatching { PrivateKey.fromHex(token) }.getOrNull())
|
||||
@@ -119,4 +130,21 @@ object CredentialParser {
|
||||
if (privateKey == null || !privateKey.isValid()) return Result.Refused(CredentialProblem.InvalidKey)
|
||||
return Result.Recognised(SignInCredential.NostrSecret(privateKey, privateKey.nostrPublicKeyHex()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Thirty-two bytes under the `npub` prefix that name a point on the curve -- the
|
||||
* counterpart of the `isValid()` a secret is checked with, since not every x
|
||||
* coordinate has a point above it.
|
||||
*/
|
||||
private fun npub(token: String): Result {
|
||||
val bytes = runCatching {
|
||||
val (hrp, data) = Bech32.decodeBytes(token)
|
||||
require(hrp == "npub") { "not an npub" }
|
||||
require(data.size == 32) { "not an x-only key" }
|
||||
data
|
||||
}.getOrNull() ?: return Result.Refused(CredentialProblem.InvalidKey)
|
||||
val xOnly = XonlyPublicKey(ByteVector32(bytes))
|
||||
if (!xOnly.publicKey.isValid()) return Result.Refused(CredentialProblem.InvalidKey)
|
||||
return Result.Recognised(SignInCredential.NostrPublicKey(xOnly.value.toHex()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import mantra.composeapp.generated.resources.keep_the_feed_alive
|
||||
import mantra.composeapp.generated.resources.learn_more
|
||||
import mantra.composeapp.generated.resources.mantra
|
||||
import mantra.composeapp.generated.resources.sign_in
|
||||
import mantra.composeapp.generated.resources.sign_in_with_a_recovery_phrase_or_an_nsec
|
||||
import mantra.composeapp.generated.resources.sign_in_with_a_recovery_phrase_an_nsec_or
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import press.mantra.compose.ui.composable.widgets.LocalSnackbarHostState
|
||||
import press.mantra.compose.ui.theme.readableContent
|
||||
@@ -82,7 +82,7 @@ fun LandingScreen(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.sign_in_with_a_recovery_phrase_or_an_nsec),
|
||||
text = stringResource(Res.string.sign_in_with_a_recovery_phrase_an_nsec_or),
|
||||
modifier = Modifier.padding(MaterialTheme.spacing.space125),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.Spellcheck
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -38,13 +39,13 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import mantra.composeapp.generated.resources.Res
|
||||
import mantra.composeapp.generated.resources.a_password_protected_key_ncryptsec_is_not
|
||||
import mantra.composeapp.generated.resources.an_npub_is_a_public_key_mantra_needs_the
|
||||
import mantra.composeapp.generated.resources.enter_a_recovery_phrase_or_an_nsec_to_sign_in
|
||||
import mantra.composeapp.generated.resources.next
|
||||
import mantra.composeapp.generated.resources.paste
|
||||
import mantra.composeapp.generated.resources.recognised_as_a_nostr_secret_key_no_wallet
|
||||
import mantra.composeapp.generated.resources.recognised_as_a_public_key_you_will_see
|
||||
import mantra.composeapp.generated.resources.recognised_as_a_recovery_phrase_it_also
|
||||
import mantra.composeapp.generated.resources.recovery_phrase_or_nsec
|
||||
import mantra.composeapp.generated.resources.recovery_phrase_nsec_or_npub
|
||||
import mantra.composeapp.generated.resources.sign_in
|
||||
import mantra.composeapp.generated.resources.signed_in
|
||||
import mantra.composeapp.generated.resources.signing_you_in
|
||||
@@ -54,9 +55,10 @@ import mantra.composeapp.generated.resources.that_recovery_phrase_is_not_valid_c
|
||||
import mantra.composeapp.generated.resources.the_key_could_not_be_saved_on_this_device
|
||||
import mantra.composeapp.generated.resources.the_words_or_the_key_never_leave_this_device
|
||||
import mantra.composeapp.generated.resources.this_key_is_already_on_this_device
|
||||
import mantra.composeapp.generated.resources.twelve_words_or_nsec1
|
||||
import mantra.composeapp.generated.resources.twelve_words_nsec1_or_npub1
|
||||
import mantra.composeapp.generated.resources.use_a_different_key
|
||||
import mantra.composeapp.generated.resources.you_are_signing_in_as
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.bitcoin.PrivateKey
|
||||
import fr.acinq.phoenix.data.WalletId
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
@@ -93,6 +95,7 @@ import press.mantra.compose.ui.view.state.SignInToProfileUIState
|
||||
fun SignInToProfileScreen(
|
||||
nostrRepository: NostrRepository,
|
||||
writeNostrKey: suspend (PrivateKey) -> IdentityWriter.WriteNostrCredentialResult,
|
||||
writeNostrPublicKey: suspend (HexKey) -> IdentityWriter.WriteNostrCredentialResult,
|
||||
writeRecoveryPhrase: (
|
||||
words: List<String>,
|
||||
onWritten: (WalletId) -> Unit,
|
||||
@@ -105,6 +108,7 @@ fun SignInToProfileScreen(
|
||||
factory = SignInToProfileViewModel.factory(
|
||||
nostrRepository = nostrRepository,
|
||||
writeNostrKey = writeNostrKey,
|
||||
writeNostrPublicKey = writeNostrPublicKey,
|
||||
writeRecoveryPhrase = writeRecoveryPhrase,
|
||||
)
|
||||
)
|
||||
@@ -191,8 +195,8 @@ private fun InputPrompt(viewModel: SignInToProfileViewModel) {
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
),
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace),
|
||||
label = { Text(text = stringResource(Res.string.recovery_phrase_or_nsec), maxLines = 1) },
|
||||
placeholder = { Text(text = stringResource(Res.string.twelve_words_or_nsec1), maxLines = 1) },
|
||||
label = { Text(text = stringResource(Res.string.recovery_phrase_nsec_or_npub), maxLines = 1) },
|
||||
placeholder = { Text(text = stringResource(Res.string.twelve_words_nsec1_or_npub1), maxLines = 1) },
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
@@ -260,6 +264,15 @@ private fun Confirm(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
is SignInCredential.NostrPublicKey -> {
|
||||
// What the user is about to get and not get, said here and nowhere
|
||||
// else: this is the one step before the choice is made.
|
||||
Icon(Icons.Default.Visibility, contentDescription = Decorative)
|
||||
Text(
|
||||
text = stringResource(Res.string.recognised_as_a_public_key_you_will_see),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +299,6 @@ private fun problemMessage(problem: CredentialProblem): String = stringResource(
|
||||
CredentialProblem.NotRecognised -> Res.string.that_is_not_a_recovery_phrase_or_a_nostr_key
|
||||
CredentialProblem.InvalidPhrase -> Res.string.that_recovery_phrase_is_not_valid_check
|
||||
CredentialProblem.InvalidKey -> Res.string.that_nostr_secret_key_is_not_valid
|
||||
CredentialProblem.PublicKeyOnly -> Res.string.an_npub_is_a_public_key_mantra_needs_the
|
||||
CredentialProblem.EncryptedKey -> Res.string.a_password_protected_key_ncryptsec_is_not
|
||||
CredentialProblem.AlreadyOnThisDevice -> Res.string.this_key_is_already_on_this_device
|
||||
CredentialProblem.CouldNotWrite -> Res.string.the_key_could_not_be_saved_on_this_device
|
||||
@@ -303,6 +315,7 @@ private fun SignInToProfileScreenPreview() {
|
||||
SignInToProfileScreen(
|
||||
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
|
||||
writeNostrKey = { IdentityWriter.WriteNostrCredentialResult.CannotLoadKeys },
|
||||
writeNostrPublicKey = { IdentityWriter.WriteNostrCredentialResult.CannotLoadKeys },
|
||||
writeRecoveryPhrase = { _, _, onError -> onError(WritingSeedState.Error.CannotLoadSeedMap) },
|
||||
onNavigateBack = {},
|
||||
onSignedIn = {},
|
||||
|
||||
@@ -704,6 +704,7 @@ fun MantraNavHost(
|
||||
SignInToProfileScreen(
|
||||
nostrRepository = databaseNostrRepository,
|
||||
writeNostrKey = { privateKey -> sovereignWalletViewModel.writeNostrKey(privateKey) },
|
||||
writeNostrPublicKey = { publicKey -> sovereignWalletViewModel.writeNostrPublicKey(publicKey) },
|
||||
writeRecoveryPhrase = { words, onWritten, onError ->
|
||||
sovereignWalletViewModel.writeSeed(
|
||||
words,
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import fr.acinq.bitcoin.PrivateKey
|
||||
import fr.acinq.phoenix.data.WalletId
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -25,14 +26,15 @@ import press.mantra.compose.ui.view.state.SignInToProfileUIState
|
||||
import press.mantra.compose.ui.view.state.form.SignInToProfileFormState
|
||||
|
||||
/**
|
||||
* Signs in with a secret the user already has: a recovery phrase or an nsec.
|
||||
* Signs in with something the user already has: a recovery phrase, an nsec, or -- to
|
||||
* look and not sign -- an npub.
|
||||
*
|
||||
* The two writers are injected as functions rather than reached for, so the commit can
|
||||
* be driven in a test without a key store, and because the recovery-phrase writer is
|
||||
* `SovereignWalletViewModel.writeSeed`, which lives in another view model and drives a
|
||||
* state machine of its own.
|
||||
* The three writers are injected as functions rather than reached for, so the commit
|
||||
* can be driven in a test without a key store, and because the recovery-phrase writer
|
||||
* is `SovereignWalletViewModel.writeSeed`, which lives in another view model and drives
|
||||
* a state machine of its own.
|
||||
*
|
||||
* What this does not do is decide where the user goes next. It writes the secret, plants
|
||||
* What this does not do is decide where the user goes next. It writes the credential, plants
|
||||
* the placeholder account, and hands the new id to [onSignedIn]; the nav host re-lists
|
||||
* identities, selects that one and goes to startup, which is the same tail the create
|
||||
* flow uses. The **order** inside [commit] is load-bearing: the account has to be in the
|
||||
@@ -43,6 +45,7 @@ import press.mantra.compose.ui.view.state.form.SignInToProfileFormState
|
||||
class SignInToProfileViewModel(
|
||||
private val nostrRepository: NostrRepository,
|
||||
private val writeNostrKey: suspend (PrivateKey) -> IdentityWriter.WriteNostrCredentialResult,
|
||||
private val writeNostrPublicKey: suspend (HexKey) -> IdentityWriter.WriteNostrCredentialResult,
|
||||
private val writeRecoveryPhrase: (
|
||||
words: List<String>,
|
||||
onWritten: (WalletId) -> Unit,
|
||||
@@ -57,6 +60,7 @@ class SignInToProfileViewModel(
|
||||
fun factory(
|
||||
nostrRepository: NostrRepository,
|
||||
writeNostrKey: suspend (PrivateKey) -> IdentityWriter.WriteNostrCredentialResult,
|
||||
writeNostrPublicKey: suspend (HexKey) -> IdentityWriter.WriteNostrCredentialResult,
|
||||
writeRecoveryPhrase: (
|
||||
words: List<String>,
|
||||
onWritten: (WalletId) -> Unit,
|
||||
@@ -67,6 +71,7 @@ class SignInToProfileViewModel(
|
||||
SignInToProfileViewModel(
|
||||
nostrRepository = nostrRepository,
|
||||
writeNostrKey = writeNostrKey,
|
||||
writeNostrPublicKey = writeNostrPublicKey,
|
||||
writeRecoveryPhrase = writeRecoveryPhrase,
|
||||
)
|
||||
}
|
||||
@@ -102,25 +107,30 @@ class SignInToProfileViewModel(
|
||||
data class Failed(val problem: CredentialProblem) : Outcome
|
||||
}
|
||||
|
||||
private suspend fun write(what: String, writer: suspend () -> IdentityWriter.WriteNostrCredentialResult): Outcome = try {
|
||||
when (val result = writer()) {
|
||||
is IdentityWriter.WriteNostrCredentialResult.Written -> Outcome.SignedIn(result.id)
|
||||
is IdentityWriter.WriteNostrCredentialResult.AlreadyExists -> Outcome.Failed(CredentialProblem.AlreadyOnThisDevice)
|
||||
is IdentityWriter.WriteNostrCredentialResult.CannotLoadKeys -> Outcome.Failed(CredentialProblem.CouldNotWrite)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
logger.e("could not write the $what", e)
|
||||
Outcome.Failed(CredentialProblem.CouldNotWrite)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the credential's secret to its store, then plants the placeholder account
|
||||
* that tells the navigation machine this pubkey has a history to fetch rather than a
|
||||
* profile to create. Suspends until both are done or one has failed.
|
||||
* Writes the credential to its store, then plants the placeholder account that tells
|
||||
* the navigation machine this pubkey has a history to fetch rather than a profile to
|
||||
* create. Suspends until both are done or one has failed. The plant is idempotent by
|
||||
* pubkey, which is what lets the nsec of a key held read-only sign in over it.
|
||||
*/
|
||||
suspend fun commit(credential: SignInCredential): Outcome {
|
||||
val written: Outcome = when (credential) {
|
||||
is SignInCredential.NostrSecret -> try {
|
||||
when (val result = writeNostrKey(credential.privateKey)) {
|
||||
is IdentityWriter.WriteNostrCredentialResult.Written -> Outcome.SignedIn(result.id)
|
||||
is IdentityWriter.WriteNostrCredentialResult.AlreadyExists -> Outcome.Failed(CredentialProblem.AlreadyOnThisDevice)
|
||||
is IdentityWriter.WriteNostrCredentialResult.CannotLoadKeys -> Outcome.Failed(CredentialProblem.CouldNotWrite)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
logger.e("could not write the nostr key", e)
|
||||
Outcome.Failed(CredentialProblem.CouldNotWrite)
|
||||
}
|
||||
is SignInCredential.NostrSecret -> write("nostr key") { writeNostrKey(credential.privateKey) }
|
||||
|
||||
is SignInCredential.NostrPublicKey -> write("nostr public key") { writeNostrPublicKey(credential.nostrPublicKey) }
|
||||
|
||||
is SignInCredential.RecoveryPhrase -> {
|
||||
// Callback-shaped writer, awaited: the seed writer drives WritingSeedState and
|
||||
|
||||
Reference in New Issue
Block a user