Merge branch 'nspec' into 'main'

Add NSpec library for Nostr integration

See merge request soapbox-pub/soapbox!2937
environments/review-main-yi2y9f/deployments/4408
Alex Gleason 2024-02-11 19:02:39 +00:00
commit dceefef404
9 zmienionych plików z 161 dodań i 91 usunięć

Wyświetl plik

@ -61,6 +61,7 @@
"@lexical/react": "^0.13.1",
"@lexical/selection": "^0.13.1",
"@lexical/utils": "^0.13.1",
"@noble/hashes": "^1.3.3",
"@popperjs/core": "^2.11.5",
"@reach/combobox": "^0.18.0",
"@reach/menu-button": "^0.18.0",
@ -133,6 +134,7 @@
"mini-css-extract-plugin": "^2.6.0",
"nostr-machina": "^0.1.0",
"nostr-tools": "^1.14.2",
"nspec": "^0.1.0",
"path-browserify": "^1.0.1",
"postcss": "^8.4.29",
"process": "^0.11.10",

Wyświetl plik

@ -2,7 +2,7 @@ import { nip19 } from 'nostr-tools';
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import { getPublicKey } from 'soapbox/features/nostr/sign';
import { signer } from 'soapbox/features/nostr/sign';
import { selectAccount } from 'soapbox/selectors';
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
@ -134,7 +134,7 @@ const createAccount = (params: Record<string, any>) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const { instance } = getState();
const { nostrSignup } = getFeatures(instance);
const pubkey = nostrSignup ? await getPublicKey() : undefined;
const pubkey = (signer && nostrSignup) ? await signer.getPublicKey() : undefined;
dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
return api(getState, 'app').post('/api/v1/accounts', params, {

Wyświetl plik

@ -1,6 +1,6 @@
import { nip19 } from 'nostr-tools';
import { getPublicKey } from 'soapbox/features/nostr/sign';
import { signer } from 'soapbox/features/nostr/sign';
import { type AppDispatch } from 'soapbox/store';
import { verifyCredentials } from './auth';
@ -8,7 +8,11 @@ import { verifyCredentials } from './auth';
/** Log in with a Nostr pubkey. */
function nostrLogIn() {
return async (dispatch: AppDispatch) => {
const pubkey = await getPublicKey();
if (!signer) {
throw new Error('No Nostr signer available');
}
const pubkey = await signer.getPublicKey();
const npub = nip19.npubEncode(pubkey);
return dispatch(verifyCredentials(npub));

Wyświetl plik

@ -1,8 +1,8 @@
import { NiceRelay } from 'nostr-machina';
import { type Event } from 'nostr-tools';
import { type NostrEvent } from 'nspec';
import { useEffect, useMemo } from 'react';
import { nip04, signEvent } from 'soapbox/features/nostr/sign';
import { signer } from 'soapbox/features/nostr/sign';
import { useInstance } from 'soapbox/hooks';
import { connectRequestSchema, nwcRequestSchema } from 'soapbox/schemas/nostr';
import { jsonSchema } from 'soapbox/schemas/utils';
@ -14,14 +14,14 @@ function useSignerStream() {
const pubkey = instance.nostr?.pubkey;
const relay = useMemo(() => {
if (relayUrl) {
if (relayUrl && signer) {
return new NiceRelay(relayUrl);
}
}, [relayUrl]);
}, [relayUrl, !!signer]);
async function handleConnectEvent(event: Event) {
if (!relay || !pubkey) return;
const decrypted = await nip04.decrypt(pubkey, event.content);
async function handleConnectEvent(event: NostrEvent) {
if (!relay || !pubkey || !signer) return;
const decrypted = await signer.nip04!.decrypt(pubkey, event.content);
const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted);
if (!reqMsg.success) {
@ -32,12 +32,12 @@ function useSignerStream() {
const respMsg = {
id: reqMsg.data.id,
result: await signEvent(reqMsg.data.params[0], reqMsg.data.params[1]),
result: await signer.signEvent(reqMsg.data.params[0]),
};
const respEvent = await signEvent({
const respEvent = await signer.signEvent({
kind: 24133,
content: await nip04.encrypt(pubkey, JSON.stringify(respMsg)),
content: await signer.nip04!.encrypt(pubkey, JSON.stringify(respMsg)),
tags: [['p', pubkey]],
created_at: Math.floor(Date.now() / 1000),
});
@ -45,10 +45,10 @@ function useSignerStream() {
relay.send(['EVENT', respEvent]);
}
async function handleWalletEvent(event: Event) {
if (!relay || !pubkey) return;
async function handleWalletEvent(event: NostrEvent) {
if (!relay || !pubkey || !signer) return;
const decrypted = await nip04.decrypt(pubkey, event.content);
const decrypted = await signer.nip04!.decrypt(pubkey, event.content);
const reqMsg = jsonSchema.pipe(nwcRequestSchema).safeParse(decrypted);
if (!reqMsg.success) {

Wyświetl plik

@ -0,0 +1,44 @@
import { hexToBytes } from '@noble/hashes/utils';
import { type NostrSigner, type NostrEvent, NSecSigner } from 'nspec';
/** Use key from `localStorage` if available, falling back to NIP-07. */
export class SoapboxSigner implements NostrSigner {
#signer: NostrSigner;
constructor() {
const privateKey = localStorage.getItem('soapbox:nostr:privateKey');
const signer = privateKey ? new NSecSigner(hexToBytes(privateKey)) : window.nostr;
if (!signer) {
throw new Error('No Nostr signer available');
}
this.#signer = signer;
}
async getPublicKey(): Promise<string> {
return this.#signer.getPublicKey();
}
async signEvent(event: Omit<NostrEvent, 'id' | 'pubkey' | 'sig'>): Promise<NostrEvent> {
return this.#signer.signEvent(event);
}
nip04 = {
encrypt: (pubkey: string, plaintext: string): Promise<string> => {
if (!this.#signer.nip04) {
throw new Error('NIP-04 not supported by signer');
}
return this.#signer.nip04.encrypt(pubkey, plaintext);
},
decrypt: (pubkey: string, ciphertext: string): Promise<string> => {
if (!this.#signer.nip04) {
throw new Error('NIP-04 not supported by signer');
}
return this.#signer.nip04.decrypt(pubkey, ciphertext);
},
};
}

Wyświetl plik

@ -1,63 +1,13 @@
import {
type Event,
type EventTemplate,
generatePrivateKey,
getPublicKey as _getPublicKey,
finishEvent,
nip04 as _nip04,
} from 'nostr-tools';
import { type NostrSigner } from 'nspec';
import { powWorker } from 'soapbox/workers';
import { SoapboxSigner } from './SoapboxSigner';
/** localStorage key for the Nostr private key (if not using NIP-07). */
const LOCAL_KEY = 'soapbox:nostr:privateKey';
let signer: NostrSigner | undefined;
/** Get the private key from the browser, or generate one. */
const getPrivateKey = (): string => {
const local = localStorage.getItem(LOCAL_KEY);
if (!local) {
const key = generatePrivateKey();
localStorage.setItem(LOCAL_KEY, key);
return key;
}
return local;
};
/** Get the user's public key from NIP-07, or generate one. */
async function getPublicKey(): Promise<string> {
return window.nostr ? window.nostr.getPublicKey() : _getPublicKey(getPrivateKey());
try {
signer = new SoapboxSigner();
} catch (_) {
// No signer available
}
interface SignEventOpts {
pow?: number;
}
/** Sign an event with NIP-07, or the locally generated key. */
async function signEvent<K extends number>(template: EventTemplate<K>, opts: SignEventOpts = {}): Promise<Event<K>> {
if (opts.pow) {
const event = await powWorker.mine({ ...template, pubkey: await getPublicKey() }, opts.pow) as Omit<Event<K>, 'sig'>;
return window.nostr ? window.nostr.signEvent(event) as Promise<Event<K>> : finishEvent(event, getPrivateKey()) ;
} else {
return window.nostr ? window.nostr.signEvent(template) as Promise<Event<K>> : finishEvent(template, getPrivateKey()) ;
}
}
/** Crypto function with NIP-07, or the local key. */
const nip04 = {
/** Encrypt with NIP-07, or the local key. */
encrypt: async (pubkey: string, content: string) => {
return window.nostr?.nip04
? window.nostr.nip04.encrypt(pubkey, content)
: _nip04.encrypt(getPrivateKey(), pubkey, content);
},
/** Decrypt with NIP-07, or the local key. */
decrypt: async (pubkey: string, content: string) => {
return window.nostr?.nip04
? window.nostr.nip04.decrypt(pubkey, content)
: _nip04.decrypt(getPrivateKey(), pubkey, content);
},
};
export { getPublicKey, signEvent, nip04 };
export { signer };

Wyświetl plik

@ -1,12 +0,0 @@
import type { Event, EventTemplate } from 'nostr-tools';
interface Nostr {
getPublicKey(): Promise<string>;
signEvent(event: EventTemplate): Promise<Event>;
nip04?: {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
};
}
export default Nostr;

Wyświetl plik

@ -1,7 +1,7 @@
import type Nostr from './nostr';
import type { NostrSigner } from 'nspec';
declare global {
interface Window {
nostr?: Nostr;
nostr?: NostrSigner;
}
}

Wyświetl plik

@ -1832,7 +1832,7 @@
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.3.16.tgz#c3b6585c256461fe5e2eac85182b11b36ea2678b"
integrity sha512-b0kKg2weqKDLI+Ai5+tocgUEIidccdSfzUndbS2YnwIp5aVvd3M0D+DCcbrsSOSgMyrV9QKMqogtqMIjKwvDxw==
"@noble/ciphers@^0.2.0":
"@noble/ciphers@0.2.0", "@noble/ciphers@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.2.0.tgz#a12cda60f3cf1ab5d7c77068c3711d2366649ed7"
integrity sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==
@ -1844,16 +1844,35 @@
dependencies:
"@noble/hashes" "1.3.1"
"@noble/curves@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
dependencies:
"@noble/hashes" "1.3.2"
"@noble/curves@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e"
integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==
dependencies:
"@noble/hashes" "1.3.3"
"@noble/hashes@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
"@noble/hashes@1.3.3", "@noble/hashes@^1.3.3", "@noble/hashes@~1.3.2":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -2108,6 +2127,11 @@
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
"@scure/base@^1.1.5", "@scure/base@~1.1.4":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157"
integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==
"@scure/bip32@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10"
@ -2117,6 +2141,15 @@
"@noble/hashes" "~1.3.1"
"@scure/base" "~1.1.0"
"@scure/bip32@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.3.tgz#a9624991dc8767087c57999a5d79488f48eae6c8"
integrity sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==
dependencies:
"@noble/curves" "~1.3.0"
"@noble/hashes" "~1.3.2"
"@scure/base" "~1.1.4"
"@scure/bip39@1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a"
@ -2125,6 +2158,14 @@
"@noble/hashes" "~1.3.0"
"@scure/base" "~1.1.0"
"@scure/bip39@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.2.tgz#f3426813f4ced11a47489cbcf7294aa963966527"
integrity sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==
dependencies:
"@noble/hashes" "~1.3.2"
"@scure/base" "~1.1.4"
"@sentry-internal/tracing@7.74.1":
version "7.74.1"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.74.1.tgz#55ff387e61d2c9533a9a0d099d376332426c8e08"
@ -6274,6 +6315,11 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
lru-cache@^10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
lru-cache@^4.1.2:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@ -6570,6 +6616,25 @@ nostr-tools@^1.14.0, nostr-tools@^1.14.2:
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
nostr-tools@^2.1.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.1.5.tgz#d38ac1139343cf13654841b8727bab8dd70563eb"
integrity sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA==
dependencies:
"@noble/ciphers" "0.2.0"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-wasm@v0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@ -6584,6 +6649,18 @@ npm-run-path@^5.1.0:
dependencies:
path-key "^4.0.0"
nspec@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nspec/-/nspec-0.1.0.tgz#abde817cf34cb042d7315a70cf515037e489401b"
integrity sha512-HPVyFFVR2x49K7HJzEjlvvBR7x5t79G6bh7/SQvfm25hXVFq9xvYBQ6i3nluwJkizcBxm+fvErM5yqJEnM/1tA==
dependencies:
"@scure/base" "^1.1.5"
"@scure/bip32" "^1.3.3"
"@scure/bip39" "^1.2.2"
lru-cache "^10.2.0"
nostr-tools "^2.1.4"
zod "^3.22.4"
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
@ -9625,3 +9702,8 @@ zod@^3.21.0, zod@^3.21.4:
version "3.22.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060"
integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==
zod@^3.22.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==