logInNostr: rewrite with new Bunker API

This commit is contained in:
Alex Gleason 2024-10-29 15:37:51 -05:00
parent 1b54bcf5f3
commit cfd4908e8d
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
6 changed files with 51 additions and 40 deletions

View File

@ -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));
};
}

View File

@ -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> | void;
onConnect?(request: NostrConnectRequest, event: NostrEvent): Promise<NostrConnectResponse> | 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();
}

View File

@ -14,7 +14,7 @@ const NostrExtensionIndicator: React.FC = () => {
const onClick = () => {
if (relay) {
dispatch(nostrExtensionLogIn(relay, AbortSignal.timeout(30_000)));
dispatch(nostrExtensionLogIn(relay));
dispatch(closeModal());
}
};

View File

@ -28,7 +28,7 @@ const ExtensionStep: React.FC<IExtensionStep> = ({ isLogin, onClickAlt, onClose
const onClick = () => {
if (relay) {
dispatch(nostrExtensionLogIn(relay, AbortSignal.timeout(30_000)));
dispatch(nostrExtensionLogIn(relay));
onClose();
}
};

View File

@ -34,7 +34,7 @@ const KeyAddStep: React.FC<IKeyAddStep> = ({ 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;
}

View File

@ -63,7 +63,7 @@ const KeygenStep: React.FC<IKeygenStep> = ({ onClose }) => {
onClose();
await dispatch(logInNostr(signer, relay, AbortSignal.timeout(30_000)));
await dispatch(logInNostr(signer, relay));
if (isMobile) {
dispatch(closeSidebar());