Move authorization out of bunkerStore, into loginNostr
This commit is contained in:
parent
1c1b56575c
commit
4d99a6d4f6
|
@ -1,4 +1,5 @@
|
|||
import { NostrSigner, NRelay1 } from '@nostrify/nostrify';
|
||||
import { NostrSigner, NRelay1, NSecSigner } from '@nostrify/nostrify';
|
||||
import { generateSecretKey } from 'nostr-tools';
|
||||
|
||||
import { NostrRPC } from 'soapbox/features/nostr/NostrRPC';
|
||||
import { useBunkerStore } from 'soapbox/hooks/useBunkerStore';
|
||||
|
@ -12,9 +13,9 @@ const NOSTR_PUBKEY_SET = 'NOSTR_PUBKEY_SET';
|
|||
/** Log in with a Nostr pubkey. */
|
||||
function logInNostr(signer: NostrSigner, relay: NRelay1, signal: AbortSignal) {
|
||||
return async (dispatch: AppDispatch) => {
|
||||
const authorization = generateBunkerAuth();
|
||||
|
||||
const pubkey = await signer.getPublicKey();
|
||||
const bunker = useBunkerStore.getState();
|
||||
const authorization = bunker.authorize(pubkey);
|
||||
const bunkerPubkey = await authorization.signer.getPublicKey();
|
||||
|
||||
const rpc = new NostrRPC(relay, authorization.signer);
|
||||
|
@ -51,16 +52,17 @@ function logInNostr(signer: NostrSigner, relay: NRelay1, signal: AbortSignal) {
|
|||
throw new Error('Authorization failed');
|
||||
}
|
||||
|
||||
const { access_token } = dispatch(authLoggedIn(await tokenPromise));
|
||||
const accessToken = dispatch(authLoggedIn(await tokenPromise)).access_token as string;
|
||||
const bunkerState = useBunkerStore.getState();
|
||||
|
||||
useBunkerStore.getState().connect({
|
||||
accessToken: access_token as string,
|
||||
bunkerState.connect({
|
||||
pubkey,
|
||||
accessToken,
|
||||
authorizedPubkey,
|
||||
bunkerPubkey,
|
||||
secret: authorization.secret,
|
||||
bunkerSeckey: authorization.seckey,
|
||||
});
|
||||
|
||||
await dispatch(verifyCredentials(access_token as string));
|
||||
await dispatch(verifyCredentials(accessToken));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -74,6 +76,18 @@ function nostrExtensionLogIn(relay: NRelay1, signal: AbortSignal) {
|
|||
};
|
||||
}
|
||||
|
||||
/** Generate a bunker authorization object. */
|
||||
function generateBunkerAuth() {
|
||||
const secret = crypto.randomUUID();
|
||||
const seckey = generateSecretKey();
|
||||
|
||||
return {
|
||||
secret,
|
||||
seckey,
|
||||
signer: new NSecSigner(seckey),
|
||||
};
|
||||
}
|
||||
|
||||
function setNostrPubkey(pubkey: string | undefined) {
|
||||
return {
|
||||
type: NOSTR_PUBKEY_SET,
|
||||
|
|
|
@ -2,14 +2,14 @@ import { NSecSigner } from '@nostrify/nostrify';
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useNostr } from 'soapbox/contexts/nostr-context';
|
||||
import { NBunker, NBunkerOpts } from 'soapbox/features/nostr/NBunker';
|
||||
import { NBunker } from 'soapbox/features/nostr/NBunker';
|
||||
import { NKeys } from 'soapbox/features/nostr/keys';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useBunkerStore } from 'soapbox/hooks/useBunkerStore';
|
||||
|
||||
function useBunker() {
|
||||
const { relay } = useNostr();
|
||||
const { authorizations, connections } = useBunkerStore();
|
||||
const { connections } = useBunkerStore();
|
||||
|
||||
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||
const [isSubscribing, setIsSubscribing] = useState(true);
|
||||
|
@ -22,7 +22,7 @@ function useBunker() {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!relay || (!connection && !authorizations.length)) return;
|
||||
if (!relay || !connection) return;
|
||||
|
||||
const bunker = new NBunker({
|
||||
relay,
|
||||
|
@ -41,22 +41,6 @@ function useBunker() {
|
|||
},
|
||||
};
|
||||
})(),
|
||||
authorizations: authorizations.reduce((result, auth) => {
|
||||
const { secret, pubkey, bunkerSeckey } = auth;
|
||||
|
||||
const user = NKeys.get(pubkey) ?? window.nostr;
|
||||
if (!user) return result;
|
||||
|
||||
result.push({
|
||||
secret,
|
||||
signers: {
|
||||
user,
|
||||
bunker: new NSecSigner(bunkerSeckey),
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [] as NBunkerOpts['authorizations']),
|
||||
onSubscribed() {
|
||||
setIsSubscribed(true);
|
||||
setIsSubscribing(false);
|
||||
|
@ -66,7 +50,7 @@ function useBunker() {
|
|||
return () => {
|
||||
bunker.close();
|
||||
};
|
||||
}, [relay, connection, authorizations]);
|
||||
}, [relay, connection]);
|
||||
|
||||
return {
|
||||
isSubscribed,
|
||||
|
|
|
@ -18,15 +18,9 @@ interface NBunkerConnection {
|
|||
signers: NBunkerSigners;
|
||||
}
|
||||
|
||||
interface NBunkerAuthorization {
|
||||
secret: string;
|
||||
signers: NBunkerSigners;
|
||||
}
|
||||
|
||||
export interface NBunkerOpts {
|
||||
relay: NRelay;
|
||||
connection?: NBunkerConnection;
|
||||
authorizations: NBunkerAuthorization[];
|
||||
onSubscribed(): void;
|
||||
}
|
||||
|
||||
|
@ -34,7 +28,6 @@ export class NBunker {
|
|||
|
||||
private relay: NRelay;
|
||||
private connection?: NBunkerConnection;
|
||||
private authorizations: NBunkerAuthorization[];
|
||||
private onSubscribed: () => void;
|
||||
|
||||
private controller = new AbortController();
|
||||
|
@ -42,7 +35,6 @@ export class NBunker {
|
|||
constructor(opts: NBunkerOpts) {
|
||||
this.relay = opts.relay;
|
||||
this.connection = opts.connection;
|
||||
this.authorizations = opts.authorizations;
|
||||
this.onSubscribed = opts.onSubscribed;
|
||||
|
||||
this.open();
|
||||
|
@ -52,27 +44,9 @@ export class NBunker {
|
|||
if (this.connection) {
|
||||
this.subscribeConnection(this.connection);
|
||||
}
|
||||
for (const authorization of this.authorizations) {
|
||||
this.subscribeAuthorization(authorization);
|
||||
}
|
||||
this.onSubscribed();
|
||||
}
|
||||
|
||||
private async subscribeAuthorization(authorization: NBunkerAuthorization): Promise<void> {
|
||||
const { signers } = authorization;
|
||||
const bunkerPubkey = await signers.bunker.getPublicKey();
|
||||
|
||||
const filters: NostrFilter[] = [
|
||||
{ kinds: [24133], '#p': [bunkerPubkey], limit: 0 },
|
||||
];
|
||||
|
||||
for await (const { event, request } of this.subscribe(filters, signers)) {
|
||||
if (request.method === 'connect') {
|
||||
this.handleConnect(event, request, authorization);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async subscribeConnection(connection: NBunkerConnection): Promise<void> {
|
||||
const { authorizedPubkey, signers } = connection;
|
||||
const bunkerPubkey = await signers.bunker.getPublicKey();
|
||||
|
@ -164,17 +138,6 @@ export class NBunker {
|
|||
}
|
||||
}
|
||||
|
||||
private async handleConnect(event: NostrEvent, request: NostrConnectRequest, authorization: NBunkerAuthorization): Promise<void> {
|
||||
const [, secret] = request.params;
|
||||
|
||||
if (secret === authorization.secret) {
|
||||
await this.sendResponse(event.pubkey, {
|
||||
id: request.id,
|
||||
result: 'ack',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async sendResponse(pubkey: string, response: NostrConnectResponse): Promise<void> {
|
||||
const { user } = this.connection?.signers ?? {};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NSchema as n, NostrSigner, NSecSigner } from '@nostrify/nostrify';
|
||||
import { NSchema as n } from '@nostrify/nostrify';
|
||||
import { produce } from 'immer';
|
||||
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { z } from 'zod';
|
||||
import { create } from 'zustand';
|
||||
// eslint-disable-next-line import/extensions
|
||||
|
@ -8,22 +8,6 @@ import { persist } from 'zustand/middleware';
|
|||
|
||||
import { filteredArray, jsonSchema } from 'soapbox/schemas/utils';
|
||||
|
||||
/**
|
||||
* Temporary authorization details to establish a bunker connection with an app.
|
||||
* Will be upgraded to a `BunkerConnection` once the connection is established.
|
||||
*/
|
||||
interface BunkerAuthorization {
|
||||
/**
|
||||
* Authorization secret generated by the bunker.
|
||||
* The app should return it to us in its `connect` call to establish a connection.
|
||||
*/
|
||||
secret: string;
|
||||
/** User pubkey. Events will be signed by this pubkey. */
|
||||
pubkey: string;
|
||||
/** Secret key for this connection. NIP-46 responses will be signed by this key. */
|
||||
bunkerSeckey: Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bunker connection maps an OAuth token from Mastodon API to a user pubkey and bunker keypair.
|
||||
* The user pubkey is used to determine whether to use keys from localStorage or a browser extension,
|
||||
|
@ -40,14 +24,6 @@ interface BunkerConnection {
|
|||
bunkerSeckey: Uint8Array;
|
||||
}
|
||||
|
||||
/** Options for connecting to the bunker. */
|
||||
interface BunkerConnectRequest {
|
||||
accessToken: string;
|
||||
authorizedPubkey: string;
|
||||
bunkerPubkey: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
const connectionSchema = z.object({
|
||||
pubkey: z.string(),
|
||||
accessToken: z.string(),
|
||||
|
@ -55,74 +31,21 @@ const connectionSchema = z.object({
|
|||
bunkerSeckey: n.bech32('nsec'),
|
||||
});
|
||||
|
||||
const authorizationSchema = z.object({
|
||||
secret: z.string(),
|
||||
pubkey: z.string(),
|
||||
bunkerSeckey: n.bech32('nsec'),
|
||||
});
|
||||
|
||||
const stateSchema = z.object({
|
||||
connections: filteredArray(connectionSchema),
|
||||
authorizations: filteredArray(authorizationSchema),
|
||||
});
|
||||
|
||||
interface BunkerState {
|
||||
connections: BunkerConnection[];
|
||||
authorizations: BunkerAuthorization[];
|
||||
authorize(pubkey: string): { signer: NostrSigner; relays: string[]; secret: string };
|
||||
connect(request: BunkerConnectRequest): void;
|
||||
connect(connection: BunkerConnection): void;
|
||||
}
|
||||
|
||||
export const useBunkerStore = create<BunkerState>()(
|
||||
persist(
|
||||
(setState, getState) => ({
|
||||
(setState) => ({
|
||||
connections: [],
|
||||
authorizations: [],
|
||||
|
||||
/** Generate a new authorization and persist it into the store. */
|
||||
authorize(pubkey: string): { signer: NostrSigner; relays: string[]; secret: string } {
|
||||
const authorization: BunkerAuthorization = {
|
||||
pubkey,
|
||||
secret: crypto.randomUUID(),
|
||||
bunkerSeckey: generateSecretKey(),
|
||||
};
|
||||
|
||||
setState((state) => {
|
||||
return produce(state, (draft) => {
|
||||
draft.authorizations.push(authorization);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
signer: new NSecSigner(authorization.bunkerSeckey),
|
||||
secret: authorization.secret,
|
||||
relays: [],
|
||||
};
|
||||
},
|
||||
|
||||
/** Connect to a bunker using the authorization secret. */
|
||||
connect(request: BunkerConnectRequest): void {
|
||||
const { authorizations } = getState();
|
||||
|
||||
const authorization = authorizations.find(
|
||||
(existing) => existing.secret === request.secret && getPublicKey(existing.bunkerSeckey) === request.bunkerPubkey,
|
||||
);
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('Authorization not found');
|
||||
}
|
||||
|
||||
const connection: BunkerConnection = {
|
||||
pubkey: authorization.pubkey,
|
||||
accessToken: request.accessToken,
|
||||
authorizedPubkey: request.authorizedPubkey,
|
||||
bunkerSeckey: authorization.bunkerSeckey,
|
||||
};
|
||||
|
||||
connect(connection: BunkerConnection): void {
|
||||
setState((state) => {
|
||||
return produce(state, (draft) => {
|
||||
draft.connections.push(connection);
|
||||
draft.authorizations = draft.authorizations.filter((existing) => existing !== authorization);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -140,23 +63,18 @@ export const useBunkerStore = create<BunkerState>()(
|
|||
name: 'soapbox:bunker',
|
||||
storage: {
|
||||
getItem(name) {
|
||||
const connections = localStorage.getItem(`${name}:connections`);
|
||||
const authorizations = sessionStorage.getItem(`${name}:authorizations`);
|
||||
const connections = jsonSchema(nsecReviver)
|
||||
.pipe(filteredArray(connectionSchema))
|
||||
.catch([])
|
||||
.parse(localStorage.getItem(name));
|
||||
|
||||
const state = stateSchema.parse({
|
||||
connections: jsonSchema(nsecReviver).catch([]).parse(connections),
|
||||
authorizations: jsonSchema(nsecReviver).catch([]).parse(authorizations),
|
||||
});
|
||||
|
||||
return { state };
|
||||
return { state: { connections } };
|
||||
},
|
||||
setItem(name, { state }) {
|
||||
localStorage.setItem(`${name}:connections`, JSON.stringify(state.connections, nsecReplacer));
|
||||
sessionStorage.setItem(`${name}:authorizations`, JSON.stringify(state.authorizations, nsecReplacer));
|
||||
localStorage.setItem(name, JSON.stringify(state.connections, nsecReplacer));
|
||||
},
|
||||
removeItem(name) {
|
||||
localStorage.removeItem(`${name}:connections`);
|
||||
sessionStorage.removeItem(`${name}:authorizations`);
|
||||
localStorage.removeItem(name);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue