refactor: replace temporary nip46 file with official nostr-tools (#583)
This commit is contained in:
parent
ec11d53fac
commit
c4643b2d8b
6 changed files with 11 additions and 496 deletions
9
package-lock.json
generated
9
package-lock.json
generated
|
|
@ -59,7 +59,7 @@
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nostr-tools": "^2.16.2",
|
"nostr-tools": "^2.17.0",
|
||||||
"nstart-modal": "^2.0.0",
|
"nstart-modal": "^2.0.0",
|
||||||
"path-to-regexp": "^8.2.0",
|
"path-to-regexp": "^8.2.0",
|
||||||
"qr-code-styling": "^1.9.2",
|
"qr-code-styling": "^1.9.2",
|
||||||
|
|
@ -9875,9 +9875,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nostr-tools": {
|
"node_modules/nostr-tools": {
|
||||||
"version": "2.16.2",
|
"version": "2.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.17.0.tgz",
|
||||||
"integrity": "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw==",
|
"integrity": "sha512-lrvHM7cSaGhz7F0YuBvgHMoU2s8/KuThihDoOYk8w5gpVHTy0DeUCAgCN8uLGeuSl5MAWekJr9Dkfo5HClqO9w==",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ciphers": "^0.5.1",
|
"@noble/ciphers": "^0.5.1",
|
||||||
|
|
@ -9901,6 +9901,7 @@
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||||
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nostr-tools": "^2.16.2",
|
"nostr-tools": "^2.17.0",
|
||||||
"nstart-modal": "^2.0.0",
|
"nstart-modal": "^2.0.0",
|
||||||
"path-to-regexp": "^8.2.0",
|
"path-to-regexp": "^8.2.0",
|
||||||
"qr-code-styling": "^1.9.2",
|
"qr-code-styling": "^1.9.2",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { DEFAULT_NOSTRCONNECT_RELAY } from '@/constants'
|
import { DEFAULT_NOSTRCONNECT_RELAY } from '@/constants'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { createNostrConnectURI, NostrConnectParams } from '@/providers/NostrProvider/nip46'
|
import { createNostrConnectURI, NostrConnectParams } from 'nostr-tools/nip46'
|
||||||
import { Check, Copy, Loader } from 'lucide-react'
|
import { Check, Copy, Loader } from 'lucide-react'
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools'
|
import { generateSecretKey, getPublicKey } from 'nostr-tools'
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export class BunkerSigner implements ISigner {
|
||||||
throw new Error('Invalid bunker')
|
throw new Error('Invalid bunker')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.signer = new NBunkerSigner(this.clientSecretKey, bunkerPointer, {
|
this.signer = NBunkerSigner.fromBunker(this.clientSecretKey, bunkerPointer, {
|
||||||
onauth: (url) => {
|
onauth: (url) => {
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,485 +0,0 @@
|
||||||
import { EventTemplate, finalizeEvent, getPublicKey as calculatePukbey, nip04, NostrEvent, SimplePool, VerifiedEvent, verifyEvent } from "nostr-tools"
|
|
||||||
import { AbstractSimplePool, SubCloser } from "nostr-tools/abstract-pool"
|
|
||||||
import { Handlerinformation, NostrConnect } from "nostr-tools/kinds"
|
|
||||||
import { NIP05_REGEX } from "nostr-tools/nip05"
|
|
||||||
import { decrypt, encrypt, getConversationKey } from "nostr-tools/nip44"
|
|
||||||
import { RelayRecord } from "nostr-tools/relay"
|
|
||||||
|
|
||||||
|
|
||||||
export const BUNKER_REGEX = /^bunker:\/\/([0-9a-f]{64})\??([?/\w:.=&%-]*)$/
|
|
||||||
|
|
||||||
// const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
||||||
|
|
||||||
export type BunkerPointer = {
|
|
||||||
relays: string[]
|
|
||||||
pubkey: string
|
|
||||||
secret: null | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toBunkerURL(bunkerPointer: BunkerPointer): string {
|
|
||||||
const bunkerURL = new URL(`bunker://${bunkerPointer.pubkey}`)
|
|
||||||
bunkerPointer.relays.forEach(relay => {
|
|
||||||
bunkerURL.searchParams.append('relay', relay)
|
|
||||||
})
|
|
||||||
if (bunkerPointer.secret) {
|
|
||||||
bunkerURL.searchParams.set('secret', bunkerPointer.secret)
|
|
||||||
}
|
|
||||||
return bunkerURL.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This takes either a bunker:// URL or a name@domain.com NIP-05 identifier
|
|
||||||
and returns a BunkerPointer -- or null in case of error */
|
|
||||||
export async function parseBunkerInput(input: string): Promise<BunkerPointer | null> {
|
|
||||||
const match = input.match(BUNKER_REGEX)
|
|
||||||
if (match) {
|
|
||||||
try {
|
|
||||||
const pubkey = match[1]
|
|
||||||
const qs = new URLSearchParams(match[2])
|
|
||||||
return {
|
|
||||||
pubkey,
|
|
||||||
relays: qs.getAll('relay'),
|
|
||||||
secret: qs.get('secret'),
|
|
||||||
}
|
|
||||||
} catch (_err) {
|
|
||||||
console.log(_err)
|
|
||||||
/* just move to the next case */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return queryBunkerProfile(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NostrConnectParams = {
|
|
||||||
clientPubkey: string;
|
|
||||||
relays: string[];
|
|
||||||
secret: string;
|
|
||||||
perms?: string[];
|
|
||||||
name?: string;
|
|
||||||
url?: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ParsedNostrConnectURI = {
|
|
||||||
protocol: 'nostrconnect';
|
|
||||||
clientPubkey: string;
|
|
||||||
params: {
|
|
||||||
relays: string[];
|
|
||||||
secret: string;
|
|
||||||
perms?: string[];
|
|
||||||
name?: string;
|
|
||||||
url?: string;
|
|
||||||
image?: string;
|
|
||||||
};
|
|
||||||
originalString: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createNostrConnectURI(params: NostrConnectParams): string {
|
|
||||||
if (!params.clientPubkey) {
|
|
||||||
throw new Error('clientPubkey is required.');
|
|
||||||
}
|
|
||||||
if (!params.relays || params.relays.length === 0) {
|
|
||||||
throw new Error('At least one relay is required.');
|
|
||||||
}
|
|
||||||
if (!params.secret) {
|
|
||||||
throw new Error('secret is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams();
|
|
||||||
|
|
||||||
params.relays.forEach(relay => {
|
|
||||||
queryParams.append('relay', relay);
|
|
||||||
});
|
|
||||||
|
|
||||||
queryParams.append('secret', params.secret);
|
|
||||||
|
|
||||||
if (params.perms && params.perms.length > 0) {
|
|
||||||
queryParams.append('perms', params.perms.join(','));
|
|
||||||
}
|
|
||||||
if (params.name) {
|
|
||||||
queryParams.append('name', params.name);
|
|
||||||
}
|
|
||||||
if (params.url) {
|
|
||||||
queryParams.append('url', params.url);
|
|
||||||
}
|
|
||||||
if (params.image) {
|
|
||||||
queryParams.append('image', params.image);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `nostrconnect://${params.clientPubkey}?${queryParams.toString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseNostrConnectURI(uri: string): ParsedNostrConnectURI {
|
|
||||||
if (!uri.startsWith('nostrconnect://')) {
|
|
||||||
throw new Error('Invalid nostrconnect URI: Must start with "nostrconnect://".');
|
|
||||||
}
|
|
||||||
|
|
||||||
const [protocolAndPubkey, queryString] = uri.split('?');
|
|
||||||
if (!protocolAndPubkey || !queryString) {
|
|
||||||
throw new Error('Invalid nostrconnect URI: Missing query string.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientPubkey = protocolAndPubkey.substring('nostrconnect://'.length);
|
|
||||||
if (!clientPubkey) {
|
|
||||||
throw new Error('Invalid nostrconnect URI: Missing client-pubkey.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(queryString);
|
|
||||||
|
|
||||||
const relays = queryParams.getAll('relay');
|
|
||||||
if (relays.length === 0) {
|
|
||||||
throw new Error('Invalid nostrconnect URI: Missing "relay" parameter.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const secret = queryParams.get('secret');
|
|
||||||
if (!secret) {
|
|
||||||
throw new Error('Invalid nostrconnect URI: Missing "secret" parameter.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const permsString = queryParams.get('perms');
|
|
||||||
const perms = permsString ? permsString.split(',') : undefined;
|
|
||||||
|
|
||||||
const name = queryParams.get('name') || undefined;
|
|
||||||
const url = queryParams.get('url') || undefined;
|
|
||||||
const image = queryParams.get('image') || undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
protocol: 'nostrconnect',
|
|
||||||
clientPubkey,
|
|
||||||
params: {
|
|
||||||
relays,
|
|
||||||
secret,
|
|
||||||
perms,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
image,
|
|
||||||
},
|
|
||||||
originalString: uri,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function queryBunkerProfile(nip05: string): Promise<BunkerPointer | null> {
|
|
||||||
const match = nip05.match(NIP05_REGEX)
|
|
||||||
if (!match) return null
|
|
||||||
|
|
||||||
const [, name = '_', domain] = match
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = `https://${domain}/.well-known/nostr.json?name=${name}`
|
|
||||||
const res = await (await fetch(url, { redirect: 'error' })).json()
|
|
||||||
|
|
||||||
const pubkey = res.names[name]
|
|
||||||
const relays = res.nip46[pubkey] || []
|
|
||||||
|
|
||||||
return { pubkey, relays, secret: null }
|
|
||||||
} catch (_err) {
|
|
||||||
console.log(_err)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BunkerSignerParams = {
|
|
||||||
pool?: AbstractSimplePool
|
|
||||||
onauth?: (url: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BunkerSigner {
|
|
||||||
private pool: AbstractSimplePool
|
|
||||||
private subCloser: SubCloser | undefined
|
|
||||||
private isOpen: boolean
|
|
||||||
private serial: number
|
|
||||||
private idPrefix: string
|
|
||||||
private listeners: {
|
|
||||||
[id: string]: {
|
|
||||||
resolve: (_: string) => void
|
|
||||||
reject: (_: string) => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private waitingForAuth: { [id: string]: boolean }
|
|
||||||
private secretKey: Uint8Array
|
|
||||||
private conversationKey: Uint8Array | undefined
|
|
||||||
public bp: BunkerPointer | undefined
|
|
||||||
private parsedConnectionString: ParsedNostrConnectURI
|
|
||||||
|
|
||||||
private cachedPubKey: string | undefined
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance of the Nip46 class.
|
|
||||||
* @param relays - An array of relay addresses.
|
|
||||||
* @param remotePubkey - An optional remote public key. This is the key you want to sign as.
|
|
||||||
* @param secretKey - An optional key pair.
|
|
||||||
*/
|
|
||||||
public constructor(clientSecretKey: Uint8Array, connectionString: string, params: BunkerSignerParams = {}) {
|
|
||||||
|
|
||||||
this.parsedConnectionString = parseNostrConnectURI(connectionString)
|
|
||||||
this.pool = params.pool || new SimplePool()
|
|
||||||
this.secretKey = clientSecretKey
|
|
||||||
this.isOpen = false
|
|
||||||
this.idPrefix = Math.random().toString(36).substring(7)
|
|
||||||
this.serial = 0
|
|
||||||
this.listeners = {}
|
|
||||||
this.waitingForAuth = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async connect(maxWait: number = 300_000): Promise<string> {
|
|
||||||
if (this.isOpen) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const connectionSubCloser = this.pool.subscribe(
|
|
||||||
this.parsedConnectionString.params.relays,
|
|
||||||
{ kinds: [NostrConnect], '#p': [calculatePukbey(this.secretKey)] },
|
|
||||||
{
|
|
||||||
onevent: async (event: NostrEvent) => {
|
|
||||||
const remoteSignerPubkey = event.pubkey
|
|
||||||
let decrypted: string
|
|
||||||
if (event.content.includes('?iv=')) {
|
|
||||||
decrypted = nip04.decrypt(this.secretKey, remoteSignerPubkey, event.content)
|
|
||||||
} else {
|
|
||||||
const tempConvKey = getConversationKey(this.secretKey, remoteSignerPubkey)
|
|
||||||
decrypted = decrypt(event.content, tempConvKey)
|
|
||||||
}
|
|
||||||
const o = JSON.parse(decrypted)
|
|
||||||
const { result } = o
|
|
||||||
if (result === this.parsedConnectionString.params.secret) {
|
|
||||||
this.bp = {
|
|
||||||
relays: this.parsedConnectionString.params.relays,
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
secret: this.parsedConnectionString.params.secret,
|
|
||||||
}
|
|
||||||
this.conversationKey = getConversationKey(this.secretKey, event.pubkey)
|
|
||||||
this.isOpen = true
|
|
||||||
this.setupSubscription()
|
|
||||||
connectionSubCloser.close()
|
|
||||||
const bunkerInput = toBunkerURL(this.bp)
|
|
||||||
resolve(bunkerInput)
|
|
||||||
} else {
|
|
||||||
console.warn('Attack from ', remoteSignerPubkey, 'with secret', decrypted, 'expected', this.parsedConnectionString.params.secret)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose: () => {
|
|
||||||
connectionSubCloser.close()
|
|
||||||
reject(new Error('Connection closed'))
|
|
||||||
},
|
|
||||||
maxWait,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private setupSubscription(params: BunkerSignerParams = {}) {
|
|
||||||
const listeners = this.listeners
|
|
||||||
const waitingForAuth = this.waitingForAuth
|
|
||||||
const convKey = this.conversationKey
|
|
||||||
|
|
||||||
this.subCloser = this.pool.subscribe(
|
|
||||||
this.bp!.relays,
|
|
||||||
{ kinds: [NostrConnect], authors: [this.bp!.pubkey], '#p': [calculatePukbey(this.secretKey)] },
|
|
||||||
{
|
|
||||||
onevent: async (event: NostrEvent) => {
|
|
||||||
const remoteSignerPubkey = event.pubkey
|
|
||||||
let decrypted: string
|
|
||||||
if (event.content.includes('?iv=')) {
|
|
||||||
decrypted = nip04.decrypt(this.secretKey, remoteSignerPubkey, event.content)
|
|
||||||
} else {
|
|
||||||
decrypted = decrypt(event.content, convKey!)
|
|
||||||
}
|
|
||||||
const o = JSON.parse(decrypted)
|
|
||||||
const { id, result, error } = o
|
|
||||||
|
|
||||||
if (result === 'auth_url' && waitingForAuth[id]) {
|
|
||||||
delete waitingForAuth[id]
|
|
||||||
|
|
||||||
if (params.onauth) {
|
|
||||||
params.onauth(error)
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`nostr-tools/nip46: remote signer ${this.bp!.pubkey} tried to send an "auth_url"='${error}' but there was no onauth() callback configured.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const handler = listeners[id]
|
|
||||||
if (handler) {
|
|
||||||
if (error) handler.reject(error)
|
|
||||||
else if (result) handler.resolve(result)
|
|
||||||
delete listeners[id]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose: () => {
|
|
||||||
this.subCloser!.close()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
this.isOpen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// closes the subscription -- this object can't be used anymore after this
|
|
||||||
async close() {
|
|
||||||
this.isOpen = false
|
|
||||||
this.subCloser!.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendRequest(method: string, params: string[]): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
if (!this.isOpen) throw new Error('this signer is not open anymore, create a new one')
|
|
||||||
if (!this.subCloser) this.setupSubscription()
|
|
||||||
this.serial++
|
|
||||||
const id = `${this.idPrefix}-${this.serial}`
|
|
||||||
|
|
||||||
const encryptedContent = encrypt(JSON.stringify({ id, method, params }), this.conversationKey!)
|
|
||||||
|
|
||||||
// the request event
|
|
||||||
const verifiedEvent: VerifiedEvent = finalizeEvent(
|
|
||||||
{
|
|
||||||
kind: NostrConnect,
|
|
||||||
tags: [['p', this.bp!.pubkey]],
|
|
||||||
content: encryptedContent,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
},
|
|
||||||
this.secretKey,
|
|
||||||
)
|
|
||||||
|
|
||||||
// setup callback listener
|
|
||||||
this.listeners[id] = { resolve, reject }
|
|
||||||
this.waitingForAuth[id] = true
|
|
||||||
|
|
||||||
// publish the event
|
|
||||||
Promise.any(this.pool.publish(this.bp!.relays, verifiedEvent))
|
|
||||||
} catch (err) {
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the "connect" method on the bunker.
|
|
||||||
* The promise will be rejected if the response is not "pong".
|
|
||||||
*/
|
|
||||||
async ping(): Promise<void> {
|
|
||||||
const resp = await this.sendRequest('ping', [])
|
|
||||||
if (resp !== 'pong') throw new Error(`result is not pong: ${resp}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the "get_public_key" method on the bunker.
|
|
||||||
* (before we would return the public key hardcoded in the bunker parameters, but
|
|
||||||
* that is not correct as that may be the bunker pubkey and the actual signer
|
|
||||||
* pubkey may be different.)
|
|
||||||
*/
|
|
||||||
async getPublicKey(): Promise<string> {
|
|
||||||
if (!this.cachedPubKey) {
|
|
||||||
this.cachedPubKey = await this.sendRequest('get_public_key', [])
|
|
||||||
}
|
|
||||||
return this.cachedPubKey
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated removed from NIP
|
|
||||||
*/
|
|
||||||
async getRelays(): Promise<RelayRecord> {
|
|
||||||
return JSON.parse(await this.sendRequest('get_relays', []))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs an event using the remote private key.
|
|
||||||
* @param event - The event to sign.
|
|
||||||
* @returns A Promise that resolves to the signed event.
|
|
||||||
*/
|
|
||||||
async signEvent(event: EventTemplate): Promise<VerifiedEvent> {
|
|
||||||
const resp = await this.sendRequest('sign_event', [JSON.stringify(event)])
|
|
||||||
const signed: NostrEvent = JSON.parse(resp)
|
|
||||||
if (verifyEvent(signed)) {
|
|
||||||
return signed
|
|
||||||
} else {
|
|
||||||
throw new Error(`event returned from bunker is improperly signed: ${JSON.stringify(signed)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip04Encrypt(thirdPartyPubkey: string, plaintext: string): Promise<string> {
|
|
||||||
return await this.sendRequest('nip04_encrypt', [thirdPartyPubkey, plaintext])
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip04Decrypt(thirdPartyPubkey: string, ciphertext: string): Promise<string> {
|
|
||||||
return await this.sendRequest('nip04_decrypt', [thirdPartyPubkey, ciphertext])
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip44Encrypt(thirdPartyPubkey: string, plaintext: string): Promise<string> {
|
|
||||||
return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, plaintext])
|
|
||||||
}
|
|
||||||
|
|
||||||
async nip44Decrypt(thirdPartyPubkey: string, ciphertext: string): Promise<string> {
|
|
||||||
return await this.sendRequest('nip44_decrypt', [thirdPartyPubkey, ciphertext])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches info on available providers that announce themselves using NIP-89 events.
|
|
||||||
* @returns A promise that resolves to an array of available bunker objects.
|
|
||||||
*/
|
|
||||||
export async function fetchBunkerProviders(pool: AbstractSimplePool, relays: string[]): Promise<BunkerProfile[]> {
|
|
||||||
const events = await pool.querySync(relays, {
|
|
||||||
kinds: [Handlerinformation],
|
|
||||||
'#k': [NostrConnect.toString()],
|
|
||||||
})
|
|
||||||
|
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
|
|
||||||
// validate bunkers by checking their NIP-05 and pubkey
|
|
||||||
// map to a more useful object
|
|
||||||
const validatedBunkers = await Promise.all(
|
|
||||||
events.map(async (event, i) => {
|
|
||||||
try {
|
|
||||||
const content = JSON.parse(event.content)
|
|
||||||
|
|
||||||
// skip duplicates
|
|
||||||
try {
|
|
||||||
if (events.findIndex(ev => JSON.parse(ev.content).nip05 === content.nip05) !== i) return undefined
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
/***/
|
|
||||||
}
|
|
||||||
|
|
||||||
const bp = await queryBunkerProfile(content.nip05)
|
|
||||||
if (bp && bp.pubkey === event.pubkey && bp.relays.length) {
|
|
||||||
return {
|
|
||||||
bunkerPointer: bp,
|
|
||||||
nip05: content.nip05,
|
|
||||||
domain: content.nip05.split('@')[1],
|
|
||||||
name: content.name || content.display_name,
|
|
||||||
picture: content.picture,
|
|
||||||
about: content.about,
|
|
||||||
website: content.website,
|
|
||||||
local: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return validatedBunkers.filter(b => b !== undefined) as BunkerProfile[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BunkerProfile = {
|
|
||||||
bunkerPointer: BunkerPointer
|
|
||||||
domain: string
|
|
||||||
nip05: string
|
|
||||||
name: string
|
|
||||||
picture: string
|
|
||||||
about: string
|
|
||||||
website: string
|
|
||||||
local: boolean
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ISigner, TDraftEvent } from '@/types'
|
import { ISigner, TDraftEvent } from '@/types'
|
||||||
import { bytesToHex } from '@noble/hashes/utils'
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
import { BunkerSigner as NBunkerSigner } from './nip46'
|
import { BunkerSigner as NBunkerSigner, toBunkerURL } from 'nostr-tools/nip46'
|
||||||
|
|
||||||
export class NostrConnectionSigner implements ISigner {
|
export class NostrConnectionSigner implements ISigner {
|
||||||
signer: NBunkerSigner | null = null
|
signer: NBunkerSigner | null = null
|
||||||
|
|
@ -8,7 +8,6 @@ export class NostrConnectionSigner implements ISigner {
|
||||||
private pubkey: string | null = null
|
private pubkey: string | null = null
|
||||||
private connectionString: string
|
private connectionString: string
|
||||||
private bunkerString: string | null = null
|
private bunkerString: string | null = null
|
||||||
private readonly CONNECTION_TIMEOUT = 300_000 // 300 seconds
|
|
||||||
|
|
||||||
constructor(clientSecretKey: Uint8Array, connectionString: string) {
|
constructor(clientSecretKey: Uint8Array, connectionString: string) {
|
||||||
this.clientSecretKey = clientSecretKey
|
this.clientSecretKey = clientSecretKey
|
||||||
|
|
@ -23,12 +22,12 @@ export class NostrConnectionSigner implements ISigner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.signer = new NBunkerSigner(this.clientSecretKey, this.connectionString, {
|
this.signer = await NBunkerSigner.fromURI(this.clientSecretKey, this.connectionString, {
|
||||||
onauth: (url) => {
|
onauth: (url) => {
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.bunkerString = await this.signer.connect(this.CONNECTION_TIMEOUT)
|
this.bunkerString = toBunkerURL(this.signer.bp)
|
||||||
this.pubkey = await this.signer.getPublicKey()
|
this.pubkey = await this.signer.getPublicKey()
|
||||||
return {
|
return {
|
||||||
bunkerString: this.bunkerString,
|
bunkerString: this.bunkerString,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue