Refactor nip46
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {cached, hash, omit, equals, assoc} from "@welshman/lib"
|
import {cached, hash, omit, equals, assoc} from "@welshman/lib"
|
||||||
import {withGetter, synced} from "@welshman/store"
|
import {withGetter, synced} from "@welshman/store"
|
||||||
import {type Nip46Handler} from "@welshman/signer"
|
|
||||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer, Nip55Signer} from "@welshman/signer"
|
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer, Nip55Signer} from "@welshman/signer"
|
||||||
|
|
||||||
export type SessionNip01 = {
|
export type SessionNip01 = {
|
||||||
@@ -19,7 +18,10 @@ export type SessionNip46 = {
|
|||||||
method: 'nip46'
|
method: 'nip46'
|
||||||
pubkey: string
|
pubkey: string
|
||||||
secret: string
|
secret: string
|
||||||
handler: Nip46Handler
|
handler: {
|
||||||
|
pubkey: string
|
||||||
|
relays: string[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionNip55 = {
|
export type SessionNip55 = {
|
||||||
@@ -28,12 +30,17 @@ export type SessionNip55 = {
|
|||||||
signer: string
|
signer: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionPubkey = {
|
||||||
|
method: 'pubkey'
|
||||||
|
pubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
export type SessionAnyMethod =
|
export type SessionAnyMethod =
|
||||||
SessionNip01 |
|
SessionNip01 |
|
||||||
SessionNip07 |
|
SessionNip07 |
|
||||||
SessionNip46 |
|
SessionNip46 |
|
||||||
SessionNip55
|
SessionNip55 |
|
||||||
|
SessionPubkey
|
||||||
|
|
||||||
export type Session = SessionAnyMethod & Record<string, any>
|
export type Session = SessionAnyMethod & Record<string, any>
|
||||||
|
|
||||||
@@ -78,8 +85,9 @@ export const getSigner = cached({
|
|||||||
case "nip46":
|
case "nip46":
|
||||||
return new Nip46Signer(
|
return new Nip46Signer(
|
||||||
Nip46Broker.get({
|
Nip46Broker.get({
|
||||||
secret: session.secret!,
|
clientSecret: session.secret!,
|
||||||
handler: session.handler!,
|
relays: session.handler!.relays,
|
||||||
|
signerPubkey: session.handler!.pubkey,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
case "nip55":
|
case "nip55":
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
export type Deferred<T> = Promise<T> & {
|
export type CustomPromise<T, E> = Promise<T> & {
|
||||||
resolve: (arg: T) => void
|
__errorType: E
|
||||||
reject: (arg: T) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defer = <T>(): Deferred<T> => {
|
export function makePromise<T, E>(
|
||||||
|
executor: (
|
||||||
|
resolve: (value: T | PromiseLike<T>) => void,
|
||||||
|
reject: (reason: E) => void
|
||||||
|
) => void
|
||||||
|
): CustomPromise<T, E> {
|
||||||
|
return new Promise(executor) as CustomPromise<T, E>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Deferred<T, E = T> = CustomPromise<T, E> & {
|
||||||
|
resolve: (arg: T) => void
|
||||||
|
reject: (arg: E) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defer = <T, E = T>(): Deferred<T, E> => {
|
||||||
let resolve, reject
|
let resolve, reject
|
||||||
const p = new Promise((resolve_, reject_) => {
|
const p = makePromise((resolve_, reject_) => {
|
||||||
resolve = resolve_
|
resolve = resolve_
|
||||||
reject = reject_
|
reject = reject_
|
||||||
})
|
})
|
||||||
|
|
||||||
return (Object.assign(p, {resolve, reject}) as unknown) as Deferred<T>
|
return (Object.assign(p, {resolve, reject}) as unknown) as Deferred<T, E>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,24 @@
|
|||||||
import {Emitter, sleep, tryCatch, randomId, equals} from "@welshman/lib"
|
import {throttle} from 'throttle-debounce'
|
||||||
import {createEvent, TrustedEvent, StampedEvent, NOSTR_CONNECT} from "@welshman/util"
|
import {Emitter, makePromise, defer, sleep, tryCatch, randomId, equals} from "@welshman/lib"
|
||||||
|
import {createEvent, normalizeRelayUrl, TrustedEvent, StampedEvent, NOSTR_CONNECT} from "@welshman/util"
|
||||||
import {subscribe, publish, Subscription, SubscriptionEvent} from "@welshman/net"
|
import {subscribe, publish, Subscription, SubscriptionEvent} from "@welshman/net"
|
||||||
import {ISigner, decrypt, hash, own, makeSecret, getPubkey} from '../util'
|
import {ISigner, decrypt, hash, own} from '../util'
|
||||||
import {Nip01Signer} from './nip01'
|
import {Nip01Signer} from './nip01'
|
||||||
|
|
||||||
export type Nip46Algorithm = "nip04" | "nip44"
|
export type Nip46Algorithm = "nip04" | "nip44"
|
||||||
|
|
||||||
export type Nip46Handler = {
|
export enum Nip46Event {
|
||||||
relays: string[]
|
Send = 'send',
|
||||||
pubkey: string
|
Receive = 'receive',
|
||||||
domain?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Nip46InitiateParams = {
|
|
||||||
url: string
|
|
||||||
name: string
|
|
||||||
image: string
|
|
||||||
perms: string
|
|
||||||
relays: string[]
|
|
||||||
abortController?: AbortController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Nip46BrokerParams = {
|
export type Nip46BrokerParams = {
|
||||||
secret: string
|
relays: string[]
|
||||||
handler: Nip46Handler
|
clientSecret: string
|
||||||
|
signerPubkey?: string
|
||||||
algorithm?: Nip46Algorithm
|
algorithm?: Nip46Algorithm
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Nip46Request = {
|
|
||||||
method: string
|
|
||||||
params: string[]
|
|
||||||
resolve: (result: Nip46ResponseWithResult) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Nip46Response = {
|
export type Nip46Response = {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
url: string
|
||||||
@@ -57,81 +43,194 @@ export type Nip46ResponseWithError = {
|
|||||||
|
|
||||||
let singleton: Nip46Broker
|
let singleton: Nip46Broker
|
||||||
|
|
||||||
export class Nip46Broker extends Emitter {
|
const popupManager = (() => {
|
||||||
#signer: ISigner
|
let pendingUrl = ""
|
||||||
#handler: Nip46Handler
|
let pendingSince = 0
|
||||||
#algorithm: Nip46Algorithm
|
let currentWindow: Window
|
||||||
#closed = false
|
|
||||||
#processing = false
|
|
||||||
#connectResponse?: Nip46Response
|
|
||||||
#queue: Nip46Request[] = []
|
|
||||||
#window?: Window
|
|
||||||
#sub?: Subscription
|
|
||||||
|
|
||||||
static initiate({url, name, image, perms, relays, abortController}: Nip46InitiateParams) {
|
const openPending = throttle(1000, () => {
|
||||||
const secret = Math.random().toString(36).substring(7)
|
// If it's been a while since they asked for it, drop the request
|
||||||
const clientSecret = makeSecret()
|
if (Date.now() - pendingSince > 10_000) return
|
||||||
const clientPubkey = getPubkey(clientSecret)
|
|
||||||
const clientSigner = new Nip01Signer(clientSecret)
|
|
||||||
const params = new URLSearchParams({secret, url, name, image, perms})
|
|
||||||
|
|
||||||
for (const relay of relays) {
|
// If we have an active, open window, continue to wait
|
||||||
params.append('relay', relay)
|
if (currentWindow && !currentWindow.closed) {
|
||||||
}
|
setTimeout(() => openPending(), 100)
|
||||||
|
} else {
|
||||||
|
// Attempt to open the window
|
||||||
|
const w = window.open(pendingUrl, "", "width=600,height=800,popup=yes")
|
||||||
|
|
||||||
const nostrconnect = `nostrconnect://${clientPubkey}?${params.toString()}`
|
// If open was successful, keep track of our window
|
||||||
|
if (w) {
|
||||||
const result = new Promise<string | undefined>(resolve => {
|
currentWindow = w
|
||||||
const complete = (pubkey?: string) => {
|
|
||||||
sub.close()
|
|
||||||
resolve(pubkey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sub = subscribe({
|
// In any case, this url has been handled
|
||||||
relays,
|
pendingUrl = ""
|
||||||
filters: [{kinds: [NOSTR_CONNECT], "#p": [clientPubkey]}],
|
pendingSince = 0
|
||||||
onEvent: async ({pubkey, content}: TrustedEvent) => {
|
}
|
||||||
const response = await tryCatch(
|
})
|
||||||
async () => JSON.parse(
|
|
||||||
await decrypt(clientSigner, pubkey, content)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response?.result === secret) {
|
return {
|
||||||
complete(pubkey)
|
open: (url: string) => {
|
||||||
}
|
pendingUrl = url
|
||||||
|
pendingSince = Date.now()
|
||||||
|
|
||||||
if (response?.result === 'ack') {
|
openPending()
|
||||||
console.warn("Bunker responded to nostrconnect with 'ack', which can lead to session hijacking")
|
},
|
||||||
complete(pubkey)
|
}
|
||||||
}
|
})()
|
||||||
},
|
|
||||||
|
export class Nip46Receiver extends Emitter {
|
||||||
|
public sub?: Subscription
|
||||||
|
|
||||||
|
constructor(public signer: ISigner, public params: Nip46BrokerParams) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
start = async () => {
|
||||||
|
if (this.sub) return
|
||||||
|
|
||||||
|
const userPubkey = await this.signer.getPubkey()
|
||||||
|
const filters = [{kinds: [NOSTR_CONNECT], "#p": [userPubkey]}]
|
||||||
|
|
||||||
|
this.sub = subscribe({relays: this.params.relays, filters})
|
||||||
|
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
this.sub!.emitter.on(SubscriptionEvent.Send, resolve)
|
||||||
|
|
||||||
|
this.sub!.emitter.on(SubscriptionEvent.Event, async (url: string, event: TrustedEvent) => {
|
||||||
|
const json = await decrypt(this.signer, event.pubkey, event.content)
|
||||||
|
const response = tryCatch(() => JSON.parse(json)) || {}
|
||||||
|
|
||||||
|
// Delay errors in case there's a zombie signer out there clogging things up
|
||||||
|
if (response.error) {
|
||||||
|
await sleep(3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.id) {
|
||||||
|
this.emit(Nip46Event.Receive, {...response, url, event} as Nip46Response)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
abortController?.signal.addEventListener('abort', () => complete())
|
this.sub!.emitter.on(SubscriptionEvent.Complete, () => {
|
||||||
|
this.sub = undefined
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return {result, params, nostrconnect, clientSecret, clientPubkey}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseBunkerLink(link: string) {
|
stop = () => {
|
||||||
let token = ""
|
this.sub?.close()
|
||||||
let pubkey = ""
|
this.removeAllListeners()
|
||||||
let relays: string[] = []
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
export class Nip46Sender extends Emitter {
|
||||||
const url = new URL(link)
|
public processing = false
|
||||||
|
public queue: Nip46Request[] = []
|
||||||
|
|
||||||
pubkey = url.hostname || url.pathname.replace(/\//g, '')
|
constructor(public signer: ISigner, public params: Nip46BrokerParams) {
|
||||||
relays = url.searchParams.getAll("relay") || []
|
super()
|
||||||
token = url.searchParams.get("secret") || ""
|
}
|
||||||
} catch {
|
|
||||||
// pass
|
public send = async (request: Nip46Request) => {
|
||||||
|
const {id, method, params} = request
|
||||||
|
const {relays, signerPubkey, algorithm = "nip44"} = this.params
|
||||||
|
|
||||||
|
if (!signerPubkey) {
|
||||||
|
throw new Error("Unable to send nip46 request without a signer pubkey")
|
||||||
}
|
}
|
||||||
|
|
||||||
return {token, pubkey, relays}
|
const payload = JSON.stringify({id, method, params})
|
||||||
|
const content = await this.signer[algorithm].encrypt(signerPubkey, payload)
|
||||||
|
const template = createEvent(NOSTR_CONNECT, {content, tags: [["p", signerPubkey]]})
|
||||||
|
const event = await this.signer.sign(template)
|
||||||
|
const pub = publish({relays, event})
|
||||||
|
|
||||||
|
this.emit(Nip46Event.Send, {...request, pub})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public process = async () => {
|
||||||
|
if (this.processing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (this.queue.length > 0) {
|
||||||
|
const [request] = this.queue.splice(0, 1)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.send(request)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`nip46 error:`, error, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.processing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueue = (request: Nip46Request) => {
|
||||||
|
this.queue.push(request)
|
||||||
|
this.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
stop = () => {
|
||||||
|
this.removeAllListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Nip46Request {
|
||||||
|
id = randomId()
|
||||||
|
promise = defer<Nip46ResponseWithResult, Nip46ResponseWithError>()
|
||||||
|
|
||||||
|
constructor(readonly method: string, readonly params: string[]) {}
|
||||||
|
|
||||||
|
listen = async (receiver: Nip46Receiver) => {
|
||||||
|
await receiver.start()
|
||||||
|
|
||||||
|
const onReceive = (response: Nip46Response) => {
|
||||||
|
if (response.id !== this.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.result === "auth_url") {
|
||||||
|
return popupManager.open(response.error!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
this.promise.reject(response as Nip46ResponseWithError)
|
||||||
|
} else {
|
||||||
|
this.promise.resolve(response as Nip46ResponseWithResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
receiver.off(Nip46Event.Receive, onReceive)
|
||||||
|
}
|
||||||
|
|
||||||
|
receiver.on(Nip46Event.Receive, onReceive)
|
||||||
|
}
|
||||||
|
|
||||||
|
send = async (sender: Nip46Sender) => {
|
||||||
|
sender.enqueue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Nip46Broker extends Emitter {
|
||||||
|
public signer: ISigner
|
||||||
|
public sender: Nip46Sender
|
||||||
|
public receiver: Nip46Receiver
|
||||||
|
|
||||||
|
constructor(public params: Nip46BrokerParams) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.signer = this.makeSigner()
|
||||||
|
this.sender = this.makeSender()
|
||||||
|
this.receiver = this.makeReceiver()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a static getter to avoid duplicate connections
|
||||||
|
|
||||||
static get(params: Nip46BrokerParams) {
|
static get(params: Nip46BrokerParams) {
|
||||||
if (!singleton?.hasParams(params)) {
|
if (!singleton?.hasParams(params)) {
|
||||||
singleton?.teardown()
|
singleton?.teardown()
|
||||||
@@ -141,192 +240,173 @@ export class Nip46Broker extends Emitter {
|
|||||||
return singleton
|
return singleton
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private params: Nip46BrokerParams) {
|
// Expose params without exposing params
|
||||||
super()
|
|
||||||
|
|
||||||
this.#handler = params.handler
|
|
||||||
this.#algorithm = params.algorithm || 'nip44'
|
|
||||||
this.#signer = new Nip01Signer(params.secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasParams(params: Nip46BrokerParams) {
|
hasParams(params: Nip46BrokerParams) {
|
||||||
return equals(this.params, params)
|
return equals(this.params, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
#subscribe = async () => {
|
// Getters for helper objects
|
||||||
const pubkey = await this.#signer.getPubkey()
|
|
||||||
|
|
||||||
return new Promise<void>(resolve => {
|
makeSigner = () => new Nip01Signer(this.params.clientSecret)
|
||||||
if (this.#sub) {
|
|
||||||
this.#sub.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#sub = subscribe({
|
makeSender = () => {
|
||||||
relays: this.#handler.relays,
|
const sender = new Nip46Sender(this.signer, this.params)
|
||||||
filters: [{kinds: [NOSTR_CONNECT], "#p": [pubkey]}],
|
|
||||||
})
|
|
||||||
|
|
||||||
this.#sub.emitter.on(SubscriptionEvent.Send, resolve)
|
sender.on(Nip46Event.Send, (data: any) => {
|
||||||
|
console.log('nip46 send:', data)
|
||||||
this.#sub.emitter.on(SubscriptionEvent.Event, async (url: string, event: TrustedEvent) => {
|
|
||||||
const json = await decrypt(this.#signer, event.pubkey, event.content)
|
|
||||||
const response = tryCatch(() => JSON.parse(json)) || {}
|
|
||||||
|
|
||||||
if (!response.id) {
|
|
||||||
console.error(`Invalid nostr-connect response: ${json}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay errors in case there's a zombie signer out there clogging things up
|
|
||||||
if (response.error) {
|
|
||||||
await sleep(3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.result === "auth_url") {
|
|
||||||
this.emit(`auth-${response.id}`, response)
|
|
||||||
} else {
|
|
||||||
this.emit(`res-${response.id}`, response)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.#sub.emitter.on("complete", () => {
|
|
||||||
this.#sub = undefined
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#processQueue = async () => {
|
|
||||||
if (this.#processing) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#processing = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (this.#queue.length > 0) {
|
|
||||||
const [{method, params, resolve}] = this.#queue.splice(0, 1)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.request(method, params)
|
|
||||||
|
|
||||||
console.log('nip46 response:', {method, params, ...response})
|
|
||||||
|
|
||||||
resolve(response)
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`nip46 error:`, {method, params, ...error})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.#processing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#getResult = async (promise: Promise<Nip46ResponseWithResult>) => {
|
|
||||||
const {result} = await promise
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
request = async (method: string, params: string[]) => {
|
|
||||||
if (this.#closed) {
|
|
||||||
throw new Error("Attempted to make a nip46 request with a closed broker")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.#sub) {
|
|
||||||
await this.#subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = randomId()
|
|
||||||
const recipient = this.#handler.pubkey
|
|
||||||
const payload = JSON.stringify({id, method, params})
|
|
||||||
const content = await this.#signer[this.#algorithm].encrypt(recipient, payload)
|
|
||||||
const template = createEvent(NOSTR_CONNECT, {content, tags: [["p", recipient]]})
|
|
||||||
|
|
||||||
console.log('nip46 request:', {id, method, params})
|
|
||||||
|
|
||||||
publish({
|
|
||||||
relays: this.#handler.relays,
|
|
||||||
event: await this.#signer.sign(template),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.once(`auth-${id}`, response => {
|
return sender
|
||||||
if (!this.#window || this.#window.closed) {
|
|
||||||
const w = window.open(response.error, "", "width=600,height=800,popup=yes")
|
|
||||||
|
|
||||||
if (w) {
|
|
||||||
this.#window = w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Promise<Nip46ResponseWithResult>((resolve, reject) => {
|
|
||||||
this.once(`res-${id}`, (response: Nip46Response) => {
|
|
||||||
if (response.error) {
|
|
||||||
reject(response as Nip46ResponseWithError)
|
|
||||||
} else {
|
|
||||||
resolve(response as Nip46ResponseWithResult)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue = (method: string, params: string[]) =>
|
makeReceiver = () => {
|
||||||
new Promise<Nip46ResponseWithResult>(resolve => {
|
const receiver = new Nip46Receiver(this.signer, this.params)
|
||||||
this.#queue.push({method, params, resolve})
|
|
||||||
this.#processQueue()
|
receiver.on(Nip46Event.Receive, (data: any) => {
|
||||||
|
console.log('nip46 receive:', data)
|
||||||
})
|
})
|
||||||
|
|
||||||
createAccount = (username: string, perms = "") => {
|
return receiver
|
||||||
if (!this.#handler.domain) {
|
|
||||||
throw new Error("Unable to create an account without a handler domain")
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.#getResult(this.enqueue("create_account", [username, this.#handler.domain, "", perms]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connect = async (token = "", perms = "", secret = "") => {
|
// Lifecycle methods
|
||||||
if (!this.#connectResponse) {
|
|
||||||
const params = ["", token, perms]
|
|
||||||
|
|
||||||
this.#connectResponse = await this.enqueue("connect", params)
|
setParams = (params: Partial<Nip46BrokerParams>) => {
|
||||||
}
|
this.params = {...this.params, ...params}
|
||||||
|
|
||||||
return this.#connectResponse.result === 'ack'
|
// Stop everything that's stateful
|
||||||
|
this.teardown()
|
||||||
|
|
||||||
|
// Set it back up again
|
||||||
|
this.sender = this.makeSender()
|
||||||
|
this.receiver = this.makeReceiver()
|
||||||
}
|
}
|
||||||
|
|
||||||
getPublicKey = () => this.#getResult(this.enqueue("get_public_key", []))
|
|
||||||
|
|
||||||
signEvent = async (event: StampedEvent) =>
|
|
||||||
JSON.parse(await this.#getResult(this.enqueue("sign_event", [JSON.stringify(event)])))
|
|
||||||
|
|
||||||
nip04Encrypt = (pk: string, message: string) =>
|
|
||||||
this.#getResult(this.enqueue("nip04_encrypt", [pk, message]))
|
|
||||||
|
|
||||||
nip04Decrypt = (pk: string, message: string) =>
|
|
||||||
this.#getResult(this.enqueue("nip04_decrypt", [pk, message]))
|
|
||||||
|
|
||||||
nip44Encrypt = (pk: string, message: string) =>
|
|
||||||
this.#getResult(this.enqueue("nip44_encrypt", [pk, message]))
|
|
||||||
|
|
||||||
nip44Decrypt = (pk: string, message: string) =>
|
|
||||||
this.#getResult(this.enqueue("nip44_decrypt", [pk, message]))
|
|
||||||
|
|
||||||
teardown = () => {
|
teardown = () => {
|
||||||
this.#closed = true
|
this.sender.stop()
|
||||||
this.#sub?.close()
|
this.receiver.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// General purpose utility methods
|
||||||
|
|
||||||
|
enqueue = async (method: string, params: string[]) => {
|
||||||
|
const request = new Nip46Request(method, params)
|
||||||
|
|
||||||
|
await request.listen(this.receiver)
|
||||||
|
await request.send(this.sender)
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
send = async (method: string, params: string[]) => {
|
||||||
|
const request = await this.enqueue(method, params)
|
||||||
|
const response = await request.promise
|
||||||
|
|
||||||
|
return response.result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods for initiating a connection
|
||||||
|
|
||||||
|
parseBunkerUrl = (url: string) => {
|
||||||
|
let connectSecret = ""
|
||||||
|
let signerPubkey = ""
|
||||||
|
let relays: string[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const _url = new URL(url)
|
||||||
|
|
||||||
|
relays = _url.searchParams.getAll("relay") || []
|
||||||
|
signerPubkey = _url.hostname || _url.pathname.replace(/\//g, '')
|
||||||
|
connectSecret = _url.searchParams.get("secret") || ""
|
||||||
|
} catch {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
|
||||||
|
return {signerPubkey, connectSecret, relays: relays.map(normalizeRelayUrl)}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeNostrconnectUrl = async (meta: Record<string, string> = {}) => {
|
||||||
|
const clientPubkey = await this.signer.getPubkey()
|
||||||
|
const secret = Math.random().toString(36).substring(7)
|
||||||
|
const params = new URLSearchParams({...meta, secret})
|
||||||
|
|
||||||
|
for (const relay of this.params.relays) {
|
||||||
|
params.append('relay', relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `nostrconnect://${clientPubkey}?${params.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForNostrconnect = (url: string, abort?: AbortController) => {
|
||||||
|
const secret = new URL(url).searchParams.get('secret')
|
||||||
|
|
||||||
|
return makePromise<Nip46ResponseWithResult, Nip46Response | undefined>((resolve, reject) => {
|
||||||
|
const onReceive = (response: Nip46Response) => {
|
||||||
|
if (["ack", secret].includes(response.result!)) {
|
||||||
|
this.setParams({signerPubkey: response.event.pubkey})
|
||||||
|
|
||||||
|
if (response.result === 'ack') {
|
||||||
|
console.warn("Bunker responded to nostrconnect with 'ack', which can lead to session hijacking")
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(response as Nip46ResponseWithResult)
|
||||||
|
} else {
|
||||||
|
reject(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
this.receiver.off(Nip46Event.Receive, onReceive)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.receiver.on(Nip46Event.Receive, onReceive)
|
||||||
|
this.receiver.start()
|
||||||
|
|
||||||
|
abort?.signal.addEventListener('abort', () => {
|
||||||
|
reject(undefined)
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal NIP 46 methods
|
||||||
|
|
||||||
|
ping = () => this.send("ping", [])
|
||||||
|
|
||||||
|
getPublicKey = () => this.send("get_public_key", [])
|
||||||
|
|
||||||
|
createAccount = (username: string, domain: string, perms = "") =>
|
||||||
|
this.send("create_account", [username, domain, "", perms])
|
||||||
|
|
||||||
|
connect = async (signerPubkey: string, connectSecret = "", perms = "") =>
|
||||||
|
this.send("connect", [signerPubkey, connectSecret, perms])
|
||||||
|
|
||||||
|
signEvent = async (event: StampedEvent) =>
|
||||||
|
JSON.parse(await this.send("sign_event", [JSON.stringify(event)]))
|
||||||
|
|
||||||
|
nip04Encrypt = (pk: string, message: string) => this.send("nip04_encrypt", [pk, message])
|
||||||
|
|
||||||
|
nip04Decrypt = (pk: string, message: string) => this.send("nip04_decrypt", [pk, message])
|
||||||
|
|
||||||
|
nip44Encrypt = (pk: string, message: string) => this.send("nip44_encrypt", [pk, message])
|
||||||
|
|
||||||
|
nip44Decrypt = (pk: string, message: string) => this.send("nip44_decrypt", [pk, message])
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Nip46Signer implements ISigner {
|
export class Nip46Signer implements ISigner {
|
||||||
#pubkey?: string
|
public pubkey?: string
|
||||||
|
|
||||||
constructor(private broker: Nip46Broker) {}
|
constructor(public broker: Nip46Broker) {}
|
||||||
|
|
||||||
getPubkey = async () => {
|
getPubkey = async () => {
|
||||||
if (!this.#pubkey) {
|
if (!this.pubkey) {
|
||||||
this.#pubkey = await this.broker.getPublicKey()
|
this.pubkey = await this.broker.getPublicKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.#pubkey
|
return this.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
sign = async (template: StampedEvent) =>
|
sign = async (template: StampedEvent) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user