Split key recovery components, bump deps

This commit is contained in:
Jon Staab
2026-01-13 13:16:57 -08:00
parent cdee6ca743
commit adb2ce4846
11 changed files with 213 additions and 166 deletions
View File
+12 -12
View File
@@ -61,16 +61,16 @@
"@types/throttle-debounce": "^5.0.2",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.8",
"@welshman/app": "^0.7.1",
"@welshman/content": "^0.7.1",
"@welshman/editor": "^0.7.1",
"@welshman/feeds": "^0.7.1",
"@welshman/lib": "^0.7.1",
"@welshman/net": "^0.7.1",
"@welshman/router": "^0.7.1",
"@welshman/signer": "^0.7.1",
"@welshman/store": "^0.7.1",
"@welshman/util": "^0.7.1",
"@welshman/app": "^0.8.0-pre.1",
"@welshman/content": "^0.8.0-pre.1",
"@welshman/editor": "^0.8.0-pre.1",
"@welshman/feeds": "^0.8.0-pre.1",
"@welshman/lib": "^0.8.0-pre.1",
"@welshman/net": "^0.8.0-pre.1",
"@welshman/router": "^0.8.0-pre.1",
"@welshman/signer": "^0.8.0-pre.1",
"@welshman/store": "^0.8.0-pre.1",
"@welshman/util": "^0.8.0-pre.1",
"compressorjs": "^1.2.1",
"daisyui": "^4.12.24",
"date-picker-svelte": "^2.16.0",
@@ -80,13 +80,13 @@
"husky": "^9.1.7",
"idb": "^8.0.3",
"nostr-signer-capacitor-plugin": "^0.0.4",
"nostr-tools": "^2.14.2",
"nostr-tools": "^2.19.4",
"prettier-plugin-tailwindcss": "^0.6.14",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2",
"tippy.js": "^6.3.7",
"@pomade/core": "^0.0.7"
"@pomade/core": "^0.0.9"
},
"pnpm": {
"ignoredBuiltDependencies": [
+4 -1
View File
@@ -25,7 +25,10 @@
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
<!-- Use a real link so people can copy the href -->
<a href={url} class="link-content whitespace-nowrap" onclick={stopPropagation(preventDefault(expand))}>
<a
href={url}
class="link-content whitespace-nowrap"
onclick={stopPropagation(preventDefault(expand))}>
<Icon icon={LinkRound} size={3} class="inline-block" />
{displayUrl(url)}
</a>
-3
View File
@@ -3,7 +3,6 @@
import {encrypt} from "nostr-tools/nip49"
import {hexToBytes} from "@welshman/lib"
import {makeSecret} from "@welshman/util"
import type {Profile} from "@welshman/util"
import {preventDefault, downloadText} from "@lib/html"
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import ArrowDown from "@assets/icons/arrow-down.svg?dataurl"
@@ -14,9 +13,7 @@
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import SignUpComplete from "@app/components/SignUpComplete.svelte"
import {pushToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal"
import {PLATFORM_NAME} from "@app/core/state"
type Props = {
-141
View File
@@ -1,141 +0,0 @@
<script lang="ts">
import {Client, decodeChallenge} from '@pomade/core'
import {chunk, sleep, uniq, identity, tryCatch} from "@welshman/lib"
import {
makeEvent,
createProfile,
PROFILE,
isReplaceable,
getAddress,
RelayMode,
getPubkey,
} from "@welshman/util"
import type {SessionPomade} from '@welshman/app'
import {pubkey, session, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import KeyDownload from "@app/components/KeyDownload.svelte"
import {pushToast} from "@app/util/toast"
import {pushModal, clearModals} from "@app/util/modal"
import {logout} from "@app/core/commands"
import {INDEXER_RELAYS, POMADE_SIGNERS, PLATFORM_NAME, userSpaceUrls} from "@app/core/state"
const {email, clientOptions: {secret, peers}} = $session as SessionPomade
const requestRecovery = async () => {
await Client.requestChallenge(email, peers)
sent = true
}
const confirmRecovery = async () => {
const challenges = input.split(/\n/).map(x => x.trim()).filter(x => tryCatch(() => decodeChallenge(x)))
if (challenges.length < 2) {
return pushToast({
theme: "error",
message: "Failed to recover, not enough valid challenges were provided."
})
}
const request = await Client.recoverWithChallenge(email, challenges)
if (!request.ok) {
console.log(request.messages)
return pushToast({
theme: "error",
message: `Failed to recover: ${request.messages[0]?.payload.message.toLowerCase()}`
})
}
const result = await Client.selectRecovery(request.clientSecret, getPubkey(secret), peers)
if (!result.ok) {
console.log(result.messages)
return pushToast({
theme: "error",
message: `Failed to recover: ${result.messages[0]?.payload.message.toLowerCase()}`
})
}
pushModal(KeyDownload, {secret: result.userSecret, next: clearModals, submitText: "Done"})
}
const submit = async () => {
loading = true
try {
if (sent) {
await confirmRecovery()
} else {
await requestRecovery()
}
} finally {
loading = false
}
}
const back = () => history.back()
let sent = $state(false)
let loading = $state(false)
let input = $state("")
let userSecret = $state("")
</script>
<form class="column gap-4" onsubmit={preventDefault(submit)}>
<ModalHeader>
{#snippet title()}
Recover your Key
{/snippet}
{#snippet info()}
Take control over your cryptographic identity
{/snippet}
</ModalHeader>
{#if sent}
<p>
Your recovery codes have been sent!
</p>
<p>
For security reasons, you may receive three or more emails with recovery codes in them. Please paste <i>all</i>
recovery codes into the text box below, on separate lines.
</p>
<textarea
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered leading-4 text-xs"
bind:value={input}></textarea>
{:else}
<p>
When you signed up, your Nostr secret key was split into multiple pieces and stored on separate
third-party servers to keep it safe.
</p>
<p>
If you're ready to take control of your cryptographic identity, click below. We'll confirm your
email by sending you some recovery codes.
</p>
{/if}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
{#if sent}
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Confirm recovery</Spinner>
<Icon icon={AltArrowRight} />
</Button>
{:else}
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Request recovery</Spinner>
<Icon icon={AltArrowRight} />
</Button>
{/if}
</ModalFooter>
</form>
@@ -0,0 +1,108 @@
<script lang="ts">
import {Client, decodeChallenge} from "@pomade/core"
import {tryCatch} from "@welshman/lib"
import {getPubkey} from "@welshman/util"
import type {SessionPomade} from "@welshman/app"
import {session} from "@welshman/app"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import KeyDownload from "@app/components/KeyDownload.svelte"
import {pushToast} from "@app/util/toast"
import {pushModal, clearModals} from "@app/util/modal"
import {POMADE_SIGNERS} from "@app/core/state"
const {
email,
clientOptions: {secret, peers},
} = $session as SessionPomade
const confirmRecovery = async () => {
const challenges = input
.split(/\n/)
.map(x => x.trim())
.filter(x => tryCatch(() => decodeChallenge(x)))
if (challenges.length < 2) {
return pushToast({
theme: "error",
message: "Failed to recover, not enough valid challenges were provided.",
})
}
const request = await Client.recoverWithChallenge(email, challenges)
if (!request.ok) {
console.log(request.messages)
return pushToast({
theme: "error",
message: `Failed to recover: ${request.messages[0]?.payload.message.toLowerCase()}`,
})
}
const result = await Client.selectRecovery(request.clientSecret, getPubkey(secret), peers)
if (!result.ok) {
console.log(result.messages)
return pushToast({
theme: "error",
message: `Failed to recover: ${result.messages[0]?.payload.message.toLowerCase()}`,
})
}
pushModal(KeyDownload, {secret: result.userSecret, next: clearModals, submitText: "Done"})
}
const submit = async () => {
loading = true
try {
await confirmRecovery()
} finally {
loading = false
}
}
const back = () => history.back()
let loading = $state(false)
let input = $state("")
</script>
<form class="column gap-4" onsubmit={preventDefault(submit)}>
<ModalHeader>
{#snippet title()}
Recover your Key
{/snippet}
{#snippet info()}
Take control over your cryptographic identity
{/snippet}
</ModalHeader>
<p>Your recovery codes have been sent!</p>
<p>
For security reasons, you may receive three or more emails with recovery codes in them. Please
paste <i>all</i>
recovery codes into the text box below, on separate lines.
</p>
<textarea
rows={POMADE_SIGNERS.length + 1}
class="textarea textarea-bordered text-xs leading-4"
bind:value={input}></textarea>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Confirm recovery</Spinner>
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
</form>
@@ -0,0 +1,68 @@
<script lang="ts">
import {Client} from "@pomade/core"
import type {SessionPomade} from "@welshman/app"
import {session} from "@welshman/app"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {pushModal} from "@app/util/modal"
import KeyRecoveryConfirm from "@app/components/KeyRecoveryConfirm.svelte"
const {
email,
clientOptions: {peers},
} = $session as SessionPomade
const requestRecovery = async () => {
await Client.requestChallenge(email, peers)
pushModal(KeyRecoveryConfirm)
}
const submit = async () => {
loading = true
try {
await requestRecovery()
} finally {
loading = false
}
}
const back = () => history.back()
let loading = $state(false)
</script>
<form class="column gap-4" onsubmit={preventDefault(submit)}>
<ModalHeader>
{#snippet title()}
Recover your Key
{/snippet}
{#snippet info()}
Take control over your cryptographic identity
{/snippet}
</ModalHeader>
<p>
When you signed up, your Nostr secret key was split into multiple pieces and stored on separate
third-party servers to keep it safe.
</p>
<p>
If you're ready to take control of your cryptographic identity, click below. We'll confirm your
email by sending you some recovery codes.
</p>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Request recovery</Spinner>
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
</form>
+3 -4
View File
@@ -19,6 +19,7 @@
try {
const client = new Client($session.clientOptions)
const result = await client.listSessions()
const pubkey = await client.getPubkey()
if (result.ok) {
// Group sessions by client pubkey and collect peers
@@ -34,15 +35,13 @@
if (existing) {
existing.peers.push(peer)
} else {
} else if (item.client !== pubkey) {
sessionMap.set(item.client, {...item, peers: [peer]})
}
}
}
sessions = Array.from(sessionMap.values()).filter(
s => s.client !== client.pubkey, // Filter out current session
)
sessions = Array.from(sessionMap.values())
} else {
pushToast({
theme: "error",
+3 -1
View File
@@ -39,7 +39,9 @@
let client: Client | undefined = undefined
try {
const {ok, clientOptions} = await Client.register(2, 3, makeSecret())
const userSecret = makeSecret()
console.log(userSecret)
const {ok, clientOptions} = await Client.register(2, 3, userSecret)
if (!ok) {
return pushToast({
+13 -2
View File
@@ -1,7 +1,18 @@
import {page} from "$app/stores"
import type {Unsubscriber} from "svelte/store"
import {derived, get} from "svelte/store"
import {partition, call, sortBy, assoc, dissoc, chunk, sleep, identity, WEEK, ago} from "@welshman/lib"
import {
partition,
call,
sortBy,
assoc,
dissoc,
chunk,
sleep,
identity,
WEEK,
ago,
} from "@welshman/lib"
import {
getListTags,
getRelayTagValues,
@@ -94,7 +105,7 @@ const pullAndListen = ({relays, filters, signal}: PullOpts) => {
request({
relays,
signal,
filters: unionFilters(filters.map(dissoc('limit'))).map(assoc("limit", 0)),
filters: unionFilters(filters.map(dissoc("limit"))).map(assoc("limit", 0)),
})
}
+2 -2
View File
@@ -20,7 +20,7 @@
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import ProfileEdit from "@app/components/ProfileEdit.svelte"
import ProfileDelete from "@app/components/ProfileDelete.svelte"
import KeyRecovery from "@app/components/KeyRecovery.svelte"
import KeyRecoveryRequest from "@app/components/KeyRecoveryRequest.svelte"
import SignerStatus from "@app/components/SignerStatus.svelte"
import InfoKeys from "@app/components/InfoKeys.svelte"
import {PLATFORM_NAME} from "@app/core/state"
@@ -41,7 +41,7 @@
const startDelete = () => pushModal(ProfileDelete)
const startRecovery = () => pushModal(KeyRecovery)
const startRecovery = () => pushModal(KeyRecoveryRequest)
let showAdvanced = false
</script>