Add initialize to nip46

This commit is contained in:
Jon Staab
2024-11-08 12:54:46 -08:00
parent f9cc3e034b
commit 8b320e7c9f
2 changed files with 128 additions and 55 deletions
+126 -55
View File
@@ -1,7 +1,7 @@
import {Emitter, sleep, tryCatch, randomId, equals, now} from "@welshman/lib"
import {Emitter, normalizeUrl, tryCatch, randomId, equals} from "@welshman/lib"
import {createEvent, TrustedEvent, StampedEvent, NOSTR_CONNECT} from "@welshman/util"
import {subscribe, publish, Subscription} from "@welshman/net"
import {ISigner, decrypt, hash, own} from '../util'
import {subscribe, publish, Subscription, SubscriptionEvent} from "@welshman/net"
import {ISigner, decrypt, hash, own, makeSecret, getPubkey} from '../util'
import {Nip01Signer} from './nip01'
export type Nip46Algorithm = "nip04" | "nip44"
@@ -12,22 +12,47 @@ export type Nip46Handler = {
domain?: string
}
export type Nip46InitiateParams = {
url: string
name: string
image: string
perms: string
relays: string[]
abortController?: AbortController
}
export type Nip46BrokerParams = {
secret: string
handler: Nip46Handler
algorithm?: Nip46Algorithm
}
export type Nip46Request = {
method: string
params: string[]
resolve: (result: Nip46ResponseWithResult) => void
}
export type Nip46Response = {
id: string
url: string
event: TrustedEvent
error?: string
result?: string
}
type Request = {
method: string
params: string[]
resolve: (result: string) => void
export type Nip46ResponseWithResult = {
id: string
url: string
event: TrustedEvent
result: string
}
export type Nip46ResponseWithError = {
id: string
url: string
event: TrustedEvent
error: string
}
let singleton: Nip46Broker
@@ -38,10 +63,62 @@ export class Nip46Broker extends Emitter {
#algorithm: Nip46Algorithm
#closed = false
#processing = false
#connectResult?: string
#queue: Request[] = []
#connectResponse?: Nip46Response
#queue: Nip46Request[] = []
#sub?: Subscription
static initiate({url, name, image, perms, relays, abortController}: Nip46InitiateParams) {
const secret = Math.random().toString(36).substring(7)
const clientSecret = makeSecret()
const clientPubkey = getPubkey(clientSecret)
const clientSigner = new Nip01Signer(clientSecret)
const params = new URLSearchParams({secret, url, name, image, perms})
for (const relay of relays) {
params.append('relay', relay)
}
const result = new Promise<string | undefined>(resolve => {
const complete = (pubkey?: string) => {
sub.close()
resolve(pubkey)
}
const sub = subscribe({
relays,
filters: [{kinds: [NOSTR_CONNECT], "#p": [clientPubkey]}],
onEvent: async ({pubkey, content}: TrustedEvent) => {
const response = await tryCatch(
async () => JSON.parse(
await decrypt(clientSigner, pubkey, content)
)
)
if (response?.result === secret) {
complete(pubkey)
}
},
})
abortController?.signal.addEventListener('abort', () => complete())
})
return {
result,
params,
clientSecret,
clientPubkey,
getLink: (template: string) => {
const temp = normalizeUrl(template)
const uri = `nostrconnect://${clientPubkey}?${params.toString()}`
return temp.includes('<nostrconnect>')
? temp.replace('<nostrconnect>', uri)
: new URL(temp).origin + '/' + uri
}
}
}
static parseBunkerLink(link: string) {
let token = ""
let pubkey = ""
@@ -91,25 +168,25 @@ export class Nip46Broker extends Emitter {
this.#sub = subscribe({
relays: this.#handler.relays,
filters: [{since: now() - 30, kinds: [NOSTR_CONNECT], "#p": [pubkey]}],
filters: [{kinds: [NOSTR_CONNECT], "#p": [pubkey]}],
})
this.#sub.emitter.on('send', resolve)
this.#sub.emitter.on(SubscriptionEvent.Send, resolve)
this.#sub.emitter.on("event", async (url: string, e: TrustedEvent) => {
const json = await decrypt(this.#signer, e.pubkey, e.content)
const res = await tryCatch(() => JSON.parse(json))
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 (!res.id) {
if (!response.id) {
console.error(`Invalid nostr-connect response: ${json}`)
}
console.log('nip46 response:', res)
console.log('nip46 response:', response)
if (res.result === "auth_url") {
this.emit(`auth-${res.id}`, res)
if (response.result === "auth_url") {
this.emit(`auth-${response.id}`, {...response, url, event})
} else {
this.emit(`res-${res.id}`, res)
this.emit(`res-${response.id}`, {...response, url, event})
}
})
@@ -130,20 +207,19 @@ export class Nip46Broker extends Emitter {
while (this.#queue.length > 0) {
const [{method, params, resolve}] = this.#queue.splice(0, 1)
// Throttle requests to the signer so the user isn't overwhelmed by dialogs, but time
// out and move on to other requests if they're ignored
// Note: currenlty throttle is too low to help with dialogs, but blocking prevents
// important user actions
await Promise.race([
this.request(method, params).then(resolve),
sleep(15),
])
this.request(method, params).then(resolve)
}
} 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")
@@ -166,23 +242,23 @@ export class Nip46Broker extends Emitter {
event: await this.#signer.sign(template),
})
this.once(`auth-${id}`, res => {
window.open(res.error, "Coracle", "width=600,height=800,popup=yes")
this.once(`auth-${id}`, response => {
window.open(response.error, "", "width=600,height=800,popup=yes")
})
return new Promise<string>((resolve, reject) => {
this.once(`res-${id}`, ({result, error}: Nip46Response) => {
if (error) {
reject(error as string)
return new Promise<Nip46ResponseWithResult>((resolve, reject) => {
this.once(`res-${id}`, (response: Nip46Response) => {
if (response.error) {
reject(response as Nip46ResponseWithError)
} else {
resolve(result as string)
resolve(response as Nip46ResponseWithResult)
}
})
})
}
enqueue = (method: string, params: string[]) =>
new Promise<string>(resolve => {
new Promise<Nip46ResponseWithResult>(resolve => {
this.#queue.push({method, params, resolve})
this.#processQueue()
})
@@ -195,37 +271,32 @@ export class Nip46Broker extends Emitter {
return this.enqueue("create_account", [username, this.#handler.domain, "", perms])
}
connect = async (token = "", perms = "") => {
if (!this.#connectResult) {
connect = async (token = "", perms = "", secret = "") => {
if (!this.#connectResponse) {
const params = ["", token, perms]
this.#connectResult = await this.enqueue("connect", params)
this.#connectResponse = await this.enqueue("connect", params)
}
return this.#connectResult === "ack"
return this.#connectResponse.result === 'ack'
}
getPublicKey = () => this.enqueue("get_public_key", [])
getPublicKey = () => this.#getResult(this.enqueue("get_public_key", []))
signEvent = async (event: StampedEvent) => {
return JSON.parse(await this.enqueue("sign_event", [JSON.stringify(event)]) as string)
}
signEvent = async (event: StampedEvent) =>
JSON.parse(await this.#getResult(this.enqueue("sign_event", [JSON.stringify(event)])))
nip04Encrypt = (pk: string, message: string) => {
return this.enqueue("nip04_encrypt", [pk, message])
}
nip04Encrypt = (pk: string, message: string) =>
this.#getResult(this.enqueue("nip04_encrypt", [pk, message]))
nip04Decrypt = (pk: string, message: string) => {
return this.enqueue("nip04_decrypt", [pk, message])
}
nip04Decrypt = (pk: string, message: string) =>
this.#getResult(this.enqueue("nip04_decrypt", [pk, message]))
nip44Encrypt = (pk: string, message: string) => {
return this.enqueue("nip44_encrypt", [pk, message])
}
nip44Encrypt = (pk: string, message: string) =>
this.#getResult(this.enqueue("nip44_encrypt", [pk, message]))
nip44Decrypt = (pk: string, message: string) => {
return this.enqueue("nip44_decrypt", [pk, message])
}
nip44Decrypt = (pk: string, message: string) =>
this.#getResult(this.enqueue("nip44_decrypt", [pk, message]))
teardown = () => {
this.#closed = true