From 172cfba6ae8819bd2d8a571ce7a3fd5699fdd9b3 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 8 Nov 2024 12:54:56 -0800 Subject: [PATCH 1/8] Get nostrconnect basically working --- src/app/views/Login.svelte | 57 ++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/app/views/Login.svelte b/src/app/views/Login.svelte index e7d35d779..05154a667 100644 --- a/src/app/views/Login.svelte +++ b/src/app/views/Login.svelte @@ -11,7 +11,7 @@ Nip46Broker, makeSecret, } from "@welshman/signer" - import {loadHandle, nip46Perms} from "@welshman/app" + import {loadHandle, nip46Perms, addSession} from "@welshman/app" import {parseJson} from "src/util/misc" import {appName} from "src/partials/state" import {showWarning} from "src/partials/Toast.svelte" @@ -25,6 +25,14 @@ import {router} from "src/app/util/router" import {boot} from "src/app/state" + // Define the interface for AppInfo + interface AppInfo { + name: string + packageName: string + iconData: string + iconUrl: string // the url to the App's icon + } + const signUp = () => { router.at("signup").replaceModal() } @@ -39,15 +47,36 @@ boot() } - // Define the interface for AppInfo - interface AppInfo { - name: string - packageName: string - iconData: string - iconUrl: string // the url to the App's icon - } + const useNsecApp = async () => { + loading = true - let signerApps: AppInfo[] = [] + const handler = { + domain: "nsec.app", + relays: ["wss://relay.nsec.app"], + pubkey: "e24a86943d37a91ab485d6f9a7c66097c25ddd67e8bd1b75ed252a3c266cf9bb", + } + + const {params, result, clientPubkey, clientSecret, abortController} = Nip46Broker.initiate({ + name: appName, + perms: nip46Perms, + relays: handler.relays, + url: import.meta.env.VITE_APP_URL, + image: import.meta.env.VITE_APP_URL + import.meta.env.VITE_APP_LOGO, + }) + + window.open(getLink('use.nsec.app')) + + const pubkey = await result + + if (pubkey) { + addSession({method: "nip46", pubkey, secret: clientSecret, handler}) + boot() + } else { + showWarning("Sorry, we weren't able to connect you. Please try again.") + } + + loading = false + } const useSigner = async (app: AppInfo) => { const signer = new Nip55Signer(app.packageName) @@ -89,11 +118,11 @@ // Hopefully it will be replaced by specifying the user's pubkey somewhere in the payload. if (!handle?.pubkey) { const broker = Nip46Broker.get({secret: makeSecret(), handler}) - const pubkey = await broker.createAccount(username, nip46Perms) + const response = await broker.createAccount(username, nip46Perms) - if (!pubkey) return false + if (!response.result) return false - handler = {...handler, pubkey} + handler = {...handler, pubkey: response.result} } else { handler = {...handler, pubkey: handle.pubkey} } @@ -111,6 +140,7 @@ } } + let signerApps: AppInfo[] = [] let handlers = [ // { // domain: "coracle-bunker.ngrok.io", @@ -206,6 +236,9 @@
+ + Use nsec.app + {#if getNip07()} Use Browser Extension From 5d1403de88de63b7c5e09028a17b6c735b9bc269 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 8 Nov 2024 14:12:40 -0800 Subject: [PATCH 2/8] Re-work login to use nostrconnect flow --- src/app/App.svelte | 24 +++--- src/app/views/Help.svelte | 25 +++++- src/app/views/Login.svelte | 167 ++++++++++++++----------------------- 3 files changed, 97 insertions(+), 119 deletions(-) diff --git a/src/app/App.svelte b/src/app/App.svelte index 6232c2cbe..c7c984239 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -447,20 +447,16 @@ slowConnections.set(getPubkeyRelays($pubkey).filter(url => getRelayQuality(url) < 0.5)) // Prune connections we haven't used in a while - for (const url of ctx.net.pool.data.keys()) { - const relay = relaysByUrl.get().get(url) - - if (relay?.stats) { - const lastActivity = max([ - relay.stats.last_open, - relay.stats.last_publish, - relay.stats.last_request, - relay.stats.last_event, - ]) - - if (lastActivity < ago(30)) { - ctx.net.pool.remove(url) - } + for (const connection of ctx.net.pool.data.values()) { + const lastActivity = max([ + connection.stats.lastOpen, + connection.stats.lastPublish, + connection.stats.lastRequest, + connection.stats.lastEvent, + ]) + + if (lastActivity && lastActivity < ago(30)) { + ctx.net.pool.remove(connection.url) } } }, 5_000) diff --git a/src/app/views/Help.svelte b/src/app/views/Help.svelte index 4d7fab5fd..62d4a3b2a 100644 --- a/src/app/views/Help.svelte +++ b/src/app/views/Help.svelte @@ -6,7 +6,7 @@ export let topic - const topics = ["web-of-trust", "nip-17-dms"] + const topics = ["web-of-trust", "nip-17-dms", "remote-signers"] const nip17Url = "https://github.com/nostr-protocol/nips/blob/master/17.md" @@ -27,6 +27,29 @@ You can set a minimum web of trust score on your content settings page, which will automatically mute anyone with a lower score than your threshold.

+ {:else if topic === "remote-signers"} +

+ Nostr uses cryptographic key pairs instead of passwords to authenticate users. This means that + you and nobody else controls your social identity - however it also requires some care to + avoid losing your keys, or having them stolen. +

+

+ Instead of pasting your private key (also known as an "nsec") into every nostr app you use, + it's wise to choose a single signer application to keep them for you. +

+

+ These signer apps may live on the internet, in which case you can simply log in by specifying + the signer's url. You can also run your own signer on your phone or computer. This method is + more secure, but a little more complicated to manage. +

+

+ If you're just getting started, consider using nsec.app, which is easy to use, and stores your keys in your browser, rather than on their server. + Don't forget to download your keys and store them in your password manager as a backup! +

{:else if topic === "nip-17-dms"}

NIP 17 improves upon the old NIP 04 direct diff --git a/src/app/views/Login.svelte b/src/app/views/Login.svelte index 05154a667..eb52de6f3 100644 --- a/src/app/views/Login.svelte +++ b/src/app/views/Login.svelte @@ -2,26 +2,17 @@ import {onMount} from "svelte" import {last, prop, objOf} from "ramda" import {Capacitor} from "@capacitor/core" - import {HANDLER_INFORMATION, NOSTR_CONNECT} from "@welshman/util" - import { - getNip07, - Nip07Signer, - getNip55, - Nip55Signer, - Nip46Broker, - makeSecret, - } from "@welshman/signer" + import {HANDLER_INFORMATION, NOSTR_CONNECT, normalizeRelayUrl} from "@welshman/util" + import {getNip07, Nip07Signer, getNip55, Nip55Signer, Nip46Broker} from "@welshman/signer" import {loadHandle, nip46Perms, addSession} from "@welshman/app" import {parseJson} from "src/util/misc" import {appName} from "src/partials/state" import {showWarning} from "src/partials/Toast.svelte" import Anchor from "src/partials/Anchor.svelte" - import FieldInline from "src/partials/FieldInline.svelte" - import Input from "src/partials/Input.svelte" import SearchSelect from "src/partials/SearchSelect.svelte" import FlexColumn from "src/partials/FlexColumn.svelte" import Heading from "src/partials/Heading.svelte" - import {load, loginWithNip07, loginWithNip46, loginWithNip55} from "src/engine" + import {load, loginWithNip07, loginWithNip55} from "src/engine" import {router} from "src/app/util/router" import {boot} from "src/app/state" @@ -47,37 +38,6 @@ boot() } - const useNsecApp = async () => { - loading = true - - const handler = { - domain: "nsec.app", - relays: ["wss://relay.nsec.app"], - pubkey: "e24a86943d37a91ab485d6f9a7c66097c25ddd67e8bd1b75ed252a3c266cf9bb", - } - - const {params, result, clientPubkey, clientSecret, abortController} = Nip46Broker.initiate({ - name: appName, - perms: nip46Perms, - relays: handler.relays, - url: import.meta.env.VITE_APP_URL, - image: import.meta.env.VITE_APP_URL + import.meta.env.VITE_APP_LOGO, - }) - - window.open(getLink('use.nsec.app')) - - const pubkey = await result - - if (pubkey) { - addSession({method: "nip46", pubkey, secret: clientSecret, handler}) - boot() - } else { - showWarning("Sorry, we weren't able to connect you. Please try again.") - } - - loading = false - } - const useSigner = async (app: AppInfo) => { const signer = new Nip55Signer(app.packageName) const pubkey = await signer.getPubkey() @@ -85,58 +45,57 @@ boot() } - const onSubmit = async () => { - if (!username) { - return showWarning("Please enter a user name.") + const normalizeHandler = async () => { + if (!handler.pubkey || !handler.relays) { + const handle = await loadHandle(`_@${handler.domain}`) + + handler.pubkey = handler.pubkey || handle?.pubkey + handler.relays = handler.relays || handle?.nip46 || handle?.relays } - if (!handler) { - return showWarning("Please select a login provider.") + if (handler.relays) { + handler.relays = handler.relays.map(normalizeRelayUrl) } + } - loading = true + const onSubmit = async () => { + abortController = new AbortController() try { - // Fill in pubkey and relays if they entered a custom doain - if (!handler.pubkey) { - const handle = await loadHandle(`_@${handler.domain}`) + await normalizeHandler() - handler.pubkey = handle?.pubkey - handler.relays = handle?.nip46 || handle?.relays || [] - } - - if (!handler.relays) { + if (!handler.relays || !handler.pubkey) { return showWarning("Sorry, we weren't able to find that provider.") } - // Find out whether this user exists, and if so what their pubkey is - const handle = await loadHandle(`${username}@${handler.domain}`) + const init = Nip46Broker.initiate({ + name: appName, + perms: nip46Perms, + relays: handler.relays, + url: import.meta.env.VITE_APP_URL, + image: import.meta.env.VITE_APP_URL + import.meta.env.VITE_APP_LOGO, + abortController, + }) - // If the user doesn't exist, use the handler's pubkey to ask the broker to create one. - // This flow may be going away. It will usually open the signer in another window/app - // In either case, update the handler to use the user's pubkey. This wacky legacy stuff, - // Hopefully it will be replaced by specifying the user's pubkey somewhere in the payload. - if (!handle?.pubkey) { - const broker = Nip46Broker.get({secret: makeSecret(), handler}) - const response = await broker.createAccount(username, nip46Perms) + window.open(init.getLink("use.nsec.app")) - if (!response.result) return false + const pubkey = await init.result - handler = {...handler, pubkey: response.result} - } else { - handler = {...handler, pubkey: handle.pubkey} + if (!pubkey) { + return showWarning("Sorry, we weren't able to connect you. Please try again.") } - // Now we can log in - const success = await loginWithNip46("", handler) + addSession({ + pubkey, + method: "nip46", + secret: init.clientSecret, + // Goofy legacy stuff, someday this will be gone + handler: {...handler, pubkey}, + }) - if (success) { - boot() - } else { - showWarning("Sorry, we weren't able to log you in with that provider.") - } + boot() } finally { - loading = false + abortController = undefined } } @@ -159,9 +118,8 @@ }, ] - let loading + let abortController: AbortController let handler = handlers[0] - let username = "" onMount(async () => { load({ @@ -211,36 +169,37 @@

- - - - - - - handlers}> - - {item.domain} - - - Log In +
+
+ handlers}> + + {item.domain} + +
+ Log In +
+

+ Choose a signer who you trust to hold your keys. + What is a signer? +

Or
-
- - Use nsec.app - +
{#if getNip07()} - + Use Browser Extension {/if} @@ -251,7 +210,7 @@ {/each} - Use Remote Signer + Use Self-Hosted Signer
From edfa94c4dc98ba93e7bf005928ee283e05e028ff Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 8 Nov 2024 15:42:51 -0800 Subject: [PATCH 3/8] Load feeds more eagerly, not just on feed list page --- src/app/App.svelte | 8 +------- src/app/state.ts | 13 ++++++++++--- src/app/views/FeedList.svelte | 3 --- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/app/App.svelte b/src/app/App.svelte index c7c984239..9317f92b8 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -7,13 +7,7 @@ import {ctx, ago, max, sleep, memoize} from "@welshman/lib" import * as lib from "@welshman/lib" import * as util from "@welshman/util" - import { - relaysByUrl, - getRelayQuality, - getPubkeyRelays, - trackRelayStats, - loadRelay, - } from "@welshman/app" + import {getRelayQuality, getPubkeyRelays, trackRelayStats, loadRelay} from "@welshman/app" import * as app from "@welshman/app" import logger from "src/util/logger" import * as misc from "src/util/misc" diff --git a/src/app/state.ts b/src/app/state.ts index 49a7fa577..d99615a89 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,6 +1,6 @@ -import {writable} from "svelte/store" +import {writable, get} from "svelte/store" import {uniq} from "@welshman/lib" -import {COMMUNITIES, FEEDS, APP_DATA} from "@welshman/util" +import {FEEDS, APP_DATA, getAddressTagValues, getIdFilters, getListTags} from "@welshman/util" import { pubkey, loadZapper, @@ -26,6 +26,7 @@ import { loadNotifications, loadFeedsAndLists, listenForNotifications, + userFeedFavorites, getSetting, } from "src/engine" @@ -65,16 +66,22 @@ export const loadUserData = async (hints: string[] = []) => { // Load less important user data loadZapper($pubkey) loadHandle($pubkey) + + // Load user feed selections, app data, and feeds that were favorited by the user load({ relays, filters: [ - {authors: [$pubkey], kinds: [COMMUNITIES, FEEDS]}, + {authors: [$pubkey], kinds: [FEEDS]}, { authors: [$pubkey], kinds: [APP_DATA], "#d": Object.values(appDataKeys), }, ], + }).then(() => { + const addrs = getAddressTagValues(getListTags(get(userFeedFavorites))) + + load({filters: getIdFilters(addrs)}) }) // Load enough to figure out web of trust diff --git a/src/app/views/FeedList.svelte b/src/app/views/FeedList.svelte index b6bba23a3..9949a5889 100644 --- a/src/app/views/FeedList.svelte +++ b/src/app/views/FeedList.svelte @@ -24,7 +24,6 @@ userFeeds, feedSearch, userListFeeds, - feedFavorites, userFavoritedFeeds, userFollows, addSinceToFilter, @@ -59,8 +58,6 @@ sortBy(displayFeed, [...$userFeeds, ...$userListFeeds, ...$userFavoritedFeeds]), ) - loadFeeds($feedFavorites.flatMap(s => getAddressTagValues(s.event.tags))) - load({ skipCache: true, forcePlatform: false, From ea7b90e222a95606d56c23f925ab18fb8e972018 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 8 Nov 2024 15:43:52 -0800 Subject: [PATCH 4/8] remove some community utils --- src/app/shared/FeedFormSectionKinds.svelte | 4 --- src/engine/state.ts | 34 ---------------------- 2 files changed, 38 deletions(-) diff --git a/src/app/shared/FeedFormSectionKinds.svelte b/src/app/shared/FeedFormSectionKinds.svelte index b14a86229..81dbd7a74 100644 --- a/src/app/shared/FeedFormSectionKinds.svelte +++ b/src/app/shared/FeedFormSectionKinds.svelte @@ -19,8 +19,6 @@ EVENT_RSVP, HANDLER_RECOMMENDATION, HANDLER_INFORMATION, - COMMUNITY, - GROUP, FILE_METADATA, RELAYS, } from "@welshman/util" @@ -57,8 +55,6 @@ {label: "Calendar event RSVP", kind: EVENT_RSVP}, {label: "Handler recommendation", kind: HANDLER_RECOMMENDATION}, {label: "Handler information", kind: HANDLER_INFORMATION}, - {label: "Community definition", kind: COMMUNITY}, - {label: "Group definition", kind: GROUP}, {label: "Image", kind: FILE_METADATA}, {label: "Relay selections", kind: RELAYS}, ]) diff --git a/src/engine/state.ts b/src/engine/state.ts index 12a4a0922..29cf80aac 100644 --- a/src/engine/state.ts +++ b/src/engine/state.ts @@ -39,7 +39,6 @@ import { ctx, groupBy, identity, - indexBy, now, pushToMapKey, setContext, @@ -64,7 +63,6 @@ import type { import { APP_DATA, CLIENT_AUTH, - COMMUNITIES, DIRECT_MESSAGE, FEED, FEEDS, @@ -298,38 +296,6 @@ export const userMutes = derived( l => new Set(getTagValues(["p", "e"], getListTags(l))), ) -// Communities - -export const communityListEvents = deriveEvents(repository, {filters: [{kinds: [COMMUNITIES]}]}) - -export const communityLists = derived( - [plaintext, communityListEvents], - ([$plaintext, $communityListEvents]) => - $communityListEvents.map(event => - readList( - asDecryptedEvent(event, { - content: $plaintext[event.id], - }), - ), - ), -) - -export const communityListsByPubkey = withGetter( - derived(communityLists, $ls => indexBy($l => $l.event.pubkey, $ls)), -) - -export const getCommunityList = (pk: string) => - communityListsByPubkey.get().get(pk) as PublishedList | undefined - -export const deriveCommunityList = (pk: string) => - derived(communityListsByPubkey, m => m.get(pk) as PublishedList | undefined) - -export const getCommunities = (pk: string) => - new Set(getAddressTagValues(getListTags(getCommunityList(pk)))) - -export const deriveCommunities = (pk: string) => - derived(communityListsByPubkey, m => new Set(getAddressTagValues(getListTags(m.get(pk))))) - export const isEventMuted = withGetter( derived( [userMutes, userFollows, userSettings, pubkey], From f6c38fdcc227a64e69578de771ce25afb1b16488 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 11 Nov 2024 09:46:32 -0800 Subject: [PATCH 5/8] Move initNip46 to commands --- src/app/views/Login.svelte | 27 +++------------- src/app/views/OnboardingKeys.svelte | 48 ++++++++++++++--------------- src/engine/commands.ts | 41 ++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 50 deletions(-) diff --git a/src/app/views/Login.svelte b/src/app/views/Login.svelte index eb52de6f3..e3f199f53 100644 --- a/src/app/views/Login.svelte +++ b/src/app/views/Login.svelte @@ -3,8 +3,8 @@ import {last, prop, objOf} from "ramda" import {Capacitor} from "@capacitor/core" import {HANDLER_INFORMATION, NOSTR_CONNECT, normalizeRelayUrl} from "@welshman/util" - import {getNip07, Nip07Signer, getNip55, Nip55Signer, Nip46Broker} from "@welshman/signer" - import {loadHandle, nip46Perms, addSession} from "@welshman/app" + import {getNip07, Nip07Signer, getNip55, Nip55Signer} from "@welshman/signer" + import {loadHandle} from "@welshman/app" import {parseJson} from "src/util/misc" import {appName} from "src/partials/state" import {showWarning} from "src/partials/Toast.svelte" @@ -12,7 +12,7 @@ import SearchSelect from "src/partials/SearchSelect.svelte" import FlexColumn from "src/partials/FlexColumn.svelte" import Heading from "src/partials/Heading.svelte" - import {load, loginWithNip07, loginWithNip55} from "src/engine" + import {load, initNip46, loginWithNip07, loginWithNip55} from "src/engine" import {router} from "src/app/util/router" import {boot} from "src/app/state" @@ -68,31 +68,12 @@ return showWarning("Sorry, we weren't able to find that provider.") } - const init = Nip46Broker.initiate({ - name: appName, - perms: nip46Perms, - relays: handler.relays, - url: import.meta.env.VITE_APP_URL, - image: import.meta.env.VITE_APP_URL + import.meta.env.VITE_APP_LOGO, - abortController, - }) - - window.open(init.getLink("use.nsec.app")) - - const pubkey = await init.result + const pubkey = await initNip46(handler, {abortController}) if (!pubkey) { return showWarning("Sorry, we weren't able to connect you. Please try again.") } - addSession({ - pubkey, - method: "nip46", - secret: init.clientSecret, - // Goofy legacy stuff, someday this will be gone - handler: {...handler, pubkey}, - }) - boot() } finally { abortController = undefined diff --git a/src/app/views/OnboardingKeys.svelte b/src/app/views/OnboardingKeys.svelte index be5f1f723..81bf610f1 100644 --- a/src/app/views/OnboardingKeys.svelte +++ b/src/app/views/OnboardingKeys.svelte @@ -1,8 +1,8 @@ - + Login with Signer -

To log in using a signer app, enter your connection string.

+

To log in using a signer app, enter a connection link starting with "bunker://".

+ + +
-
- - - -
- - - + Back + Continue
-
+ diff --git a/src/engine/commands.ts b/src/engine/commands.ts index 5c6046709..4d3353402 100644 --- a/src/engine/commands.ts +++ b/src/engine/commands.ts @@ -511,7 +511,7 @@ export const loginWithNip46 = async ( } export const initNip46 = async ( - handler: Nip46Handler, + handler: Nip46Handler & {nostrconnectTemplate: string}, params: Partial = {}, ) => { const init = Nip46Broker.initiate({ @@ -523,7 +523,7 @@ export const initNip46 = async ( ...params, }) - window.open(init.getLink("use.nsec.app")) + window.open(init.getLink(handler.nostrconnectTemplate)) const pubkey = await init.result diff --git a/src/util/router.ts b/src/util/router.ts index 511c36adc..3c0e04b37 100644 --- a/src/util/router.ts +++ b/src/util/router.ts @@ -240,6 +240,8 @@ class RouterExtension { open = (config = {}) => this.go({...config, modal: true}) + pushModal = (config = {}) => this.go({...config, modal: true}) + replace = (config = {}) => this.go({...config, replace: true}) replaceModal = (config = {}) => this.go({...config, replace: true, modal: true})