Add initialize to nip46
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user