diff --git a/src/actions/nostr.ts b/src/actions/nostr.ts index d58bc571a..e1b638c58 100644 --- a/src/actions/nostr.ts +++ b/src/actions/nostr.ts @@ -1,7 +1,7 @@ import { NostrSigner, NRelay1, NSecSigner } from '@nostrify/nostrify'; import { generateSecretKey } from 'nostr-tools'; -import { NostrRPC } from 'soapbox/features/nostr/NostrRPC'; +import { NBunker } from 'soapbox/features/nostr/NBunker'; import { useBunkerStore } from 'soapbox/hooks/nostr/useBunkerStore'; import { type AppDispatch } from 'soapbox/store'; @@ -11,52 +11,44 @@ import { obtainOAuthToken } from './oauth'; const NOSTR_PUBKEY_SET = 'NOSTR_PUBKEY_SET'; /** Log in with a Nostr pubkey. */ -function logInNostr(signer: NostrSigner, relay: NRelay1, signal: AbortSignal) { +function logInNostr(signer: NostrSigner, relay: NRelay1) { return async (dispatch: AppDispatch) => { const authorization = generateBunkerAuth(); const pubkey = await signer.getPublicKey(); const bunkerPubkey = await authorization.signer.getPublicKey(); - const rpc = new NostrRPC(relay, authorization.signer); - const sub = rpc.req([{ kinds: [24133], '#p': [bunkerPubkey], limit: 0 }], { signal: AbortSignal.timeout(1_000) }); + let authorizedPubkey: string | undefined; - const tokenPromise = dispatch(obtainOAuthToken({ + using bunker = new NBunker({ + relay, + userSigner: signer, + bunkerSigner: authorization.signer, + onConnect(request, event) { + const [, secret] = request.params; + + if (secret === authorization.secret) { + bunker.authorize(event.pubkey); + authorizedPubkey = event.pubkey; + return { id: request.id, result: 'ack' }; + } else { + return { id: request.id, result: '', error: 'Invalid secret' }; + } + }, + }); + + const token = await dispatch(obtainOAuthToken({ grant_type: 'nostr_bunker', pubkey: bunkerPubkey, relays: [relay.socket.url], secret: authorization.secret, })); - let authorizedPubkey: string | undefined; - - for await (const { request, respond, requestEvent } of sub) { - if (request.method === 'connect') { - const [, secret] = request.params; - - if (secret === authorization.secret) { - authorizedPubkey = requestEvent.pubkey; - await respond({ result: 'ack' }); - } else { - await respond({ result: '', error: 'Invalid secret' }); - throw new Error('Invalid secret'); - } - } - if (request.method === 'get_public_key') { - await respond({ result: pubkey }); - break; - } - - // FIXME: this needs to actually be a full bunker that handles all methods... - // maybe NBunker can be modular? It should be okay to make multiple instances of it at once. - // Then it could be used for knox too. - } - if (!authorizedPubkey) { throw new Error('Authorization failed'); } - const accessToken = dispatch(authLoggedIn(await tokenPromise)).access_token as string; + const accessToken = dispatch(authLoggedIn(token)).access_token as string; const bunkerState = useBunkerStore.getState(); bunkerState.connect({ @@ -71,12 +63,12 @@ function logInNostr(signer: NostrSigner, relay: NRelay1, signal: AbortSignal) { } /** Log in with a Nostr extension. */ -function nostrExtensionLogIn(relay: NRelay1, signal: AbortSignal) { +function nostrExtensionLogIn(relay: NRelay1) { return async (dispatch: AppDispatch) => { if (!window.nostr) { throw new Error('No Nostr signer available'); } - return dispatch(logInNostr(window.nostr, relay, signal)); + return dispatch(logInNostr(window.nostr, relay)); }; } diff --git a/src/features/nostr/NBunker.ts b/src/features/nostr/NBunker.ts index 86cc76b9f..b3d1a69a6 100644 --- a/src/features/nostr/NBunker.ts +++ b/src/features/nostr/NBunker.ts @@ -19,10 +19,26 @@ export interface NBunkerOpts { /** * Callback when a `connect` request has been received. * This is a good place to call `bunker.authorize()` with the remote client's pubkey. - * It's up to the caller to verify the request parameters and secret. + * It's up to the caller to verify the request parameters and secret, and then return a response object. * All other methods are handled by the bunker automatically. + * + * ```ts + * const bunker = new Bunker({ + * ...opts, + * onConnect(request, event) { + * const [, secret] = request.params; + * + * if (secret === authorization.secret) { + * bunker.authorize(event.pubkey); // Authorize the pubkey for signer actions. + * return { id: request.id, result: 'ack' }; // Return a success response. + * } else { + * return { id: request.id, result: '', error: 'Invalid secret' }; + * } + * }, + * }); + * ``` */ - onConnect?(request: NostrConnectRequest, event: NostrEvent): Promise | void; + onConnect?(request: NostrConnectRequest, event: NostrEvent): Promise | NostrConnectResponse; /** * Callback when an error occurs while parsing a request event. * Client errors are not captured here, only errors that occur before arequest's `id` can be known, @@ -86,7 +102,10 @@ export class NBunker { const { pubkey } = event; if (request.method === 'connect') { - onConnect?.(request, event); + if (onConnect) { + const response = await onConnect(request, event); + return this.sendResponse(pubkey, response); + } return; } @@ -191,7 +210,7 @@ export class NBunker { this.controller.abort(); } - [Symbol.asyncDispose](): void { + [Symbol.dispose](): void { this.close(); } diff --git a/src/features/ui/components/modals/nostr-login-modal/components/nostr-extension-indicator.tsx b/src/features/ui/components/modals/nostr-login-modal/components/nostr-extension-indicator.tsx index 932a3d9d0..adc56b707 100644 --- a/src/features/ui/components/modals/nostr-login-modal/components/nostr-extension-indicator.tsx +++ b/src/features/ui/components/modals/nostr-login-modal/components/nostr-extension-indicator.tsx @@ -14,7 +14,7 @@ const NostrExtensionIndicator: React.FC = () => { const onClick = () => { if (relay) { - dispatch(nostrExtensionLogIn(relay, AbortSignal.timeout(30_000))); + dispatch(nostrExtensionLogIn(relay)); dispatch(closeModal()); } }; diff --git a/src/features/ui/components/modals/nostr-login-modal/steps/extension-step.tsx b/src/features/ui/components/modals/nostr-login-modal/steps/extension-step.tsx index 22d8d0638..36688eb16 100644 --- a/src/features/ui/components/modals/nostr-login-modal/steps/extension-step.tsx +++ b/src/features/ui/components/modals/nostr-login-modal/steps/extension-step.tsx @@ -28,7 +28,7 @@ const ExtensionStep: React.FC = ({ isLogin, onClickAlt, onClose const onClick = () => { if (relay) { - dispatch(nostrExtensionLogIn(relay, AbortSignal.timeout(30_000))); + dispatch(nostrExtensionLogIn(relay)); onClose(); } }; diff --git a/src/features/ui/components/modals/nostr-login-modal/steps/key-add-step.tsx b/src/features/ui/components/modals/nostr-login-modal/steps/key-add-step.tsx index a1066a708..486afbe77 100644 --- a/src/features/ui/components/modals/nostr-login-modal/steps/key-add-step.tsx +++ b/src/features/ui/components/modals/nostr-login-modal/steps/key-add-step.tsx @@ -34,7 +34,7 @@ const KeyAddStep: React.FC = ({ onClose }) => { if (result.type === 'nsec') { const seckey = result.data; const signer = NKeys.add(seckey); - dispatch(logInNostr(signer, relay, AbortSignal.timeout(30_000))); + dispatch(logInNostr(signer, relay)); onClose(); return; } diff --git a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx index 696e43228..ae50f6a9f 100644 --- a/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx +++ b/src/features/ui/components/modals/nostr-signup-modal/steps/keygen-step.tsx @@ -63,7 +63,7 @@ const KeygenStep: React.FC = ({ onClose }) => { onClose(); - await dispatch(logInNostr(signer, relay, AbortSignal.timeout(30_000))); + await dispatch(logInNostr(signer, relay)); if (isMobile) { dispatch(closeSidebar());