Add initialize to nip46
This commit is contained in:
@@ -9,6 +9,8 @@ export const isNil = (x: any) => [null, undefined].includes(x)
|
|||||||
|
|
||||||
export type Maybe<T> = T | undefined
|
export type Maybe<T> = T | undefined
|
||||||
|
|
||||||
|
export const ifLet = <T>(x: T | undefined, f: (x: T) => void) => x === undefined ? undefined : f(x)
|
||||||
|
|
||||||
// Regular old utils
|
// Regular old utils
|
||||||
|
|
||||||
export const noop = (...args: unknown[]) => undefined
|
export const noop = (...args: unknown[]) => undefined
|
||||||
|
|||||||
@@ -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 {createEvent, TrustedEvent, StampedEvent, NOSTR_CONNECT} from "@welshman/util"
|
||||||
import {subscribe, publish, Subscription} from "@welshman/net"
|
import {subscribe, publish, Subscription, SubscriptionEvent} from "@welshman/net"
|
||||||
import {ISigner, decrypt, hash, own} from '../util'
|
import {ISigner, decrypt, hash, own, makeSecret, getPubkey} from '../util'
|
||||||
import {Nip01Signer} from './nip01'
|
import {Nip01Signer} from './nip01'
|
||||||
|
|
||||||
export type Nip46Algorithm = "nip04" | "nip44"
|
export type Nip46Algorithm = "nip04" | "nip44"
|
||||||
@@ -12,22 +12,47 @@ export type Nip46Handler = {
|
|||||||
domain?: string
|
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
|
secret: string
|
||||||
handler: Nip46Handler
|
handler: Nip46Handler
|
||||||
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
|
||||||
|
event: TrustedEvent
|
||||||
error?: string
|
error?: string
|
||||||
result?: string
|
result?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Request = {
|
export type Nip46ResponseWithResult = {
|
||||||
method: string
|
id: string
|
||||||
params: string[]
|
url: string
|
||||||
resolve: (result: string) => void
|
event: TrustedEvent
|
||||||
|
result: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Nip46ResponseWithError = {
|
||||||
|
id: string
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let singleton: Nip46Broker
|
let singleton: Nip46Broker
|
||||||
@@ -38,10 +63,62 @@ export class Nip46Broker extends Emitter {
|
|||||||
#algorithm: Nip46Algorithm
|
#algorithm: Nip46Algorithm
|
||||||
#closed = false
|
#closed = false
|
||||||
#processing = false
|
#processing = false
|
||||||
#connectResult?: string
|
#connectResponse?: Nip46Response
|
||||||
#queue: Request[] = []
|
#queue: Nip46Request[] = []
|
||||||
#sub?: Subscription
|
#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) {
|
static parseBunkerLink(link: string) {
|
||||||
let token = ""
|
let token = ""
|
||||||
let pubkey = ""
|
let pubkey = ""
|
||||||
@@ -91,25 +168,25 @@ export class Nip46Broker extends Emitter {
|
|||||||
|
|
||||||
this.#sub = subscribe({
|
this.#sub = subscribe({
|
||||||
relays: this.#handler.relays,
|
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) => {
|
this.#sub.emitter.on(SubscriptionEvent.Event, async (url: string, event: TrustedEvent) => {
|
||||||
const json = await decrypt(this.#signer, e.pubkey, e.content)
|
const json = await decrypt(this.#signer, event.pubkey, event.content)
|
||||||
const res = await tryCatch(() => JSON.parse(json))
|
const response = tryCatch(() => JSON.parse(json))
|
||||||
|
|
||||||
if (!res.id) {
|
if (!response.id) {
|
||||||
console.error(`Invalid nostr-connect response: ${json}`)
|
console.error(`Invalid nostr-connect response: ${json}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('nip46 response:', res)
|
console.log('nip46 response:', response)
|
||||||
|
|
||||||
if (res.result === "auth_url") {
|
if (response.result === "auth_url") {
|
||||||
this.emit(`auth-${res.id}`, res)
|
this.emit(`auth-${response.id}`, {...response, url, event})
|
||||||
} else {
|
} 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) {
|
while (this.#queue.length > 0) {
|
||||||
const [{method, params, resolve}] = this.#queue.splice(0, 1)
|
const [{method, params, resolve}] = this.#queue.splice(0, 1)
|
||||||
|
|
||||||
// Throttle requests to the signer so the user isn't overwhelmed by dialogs, but time
|
this.request(method, params).then(resolve)
|
||||||
// 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),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.#processing = false
|
this.#processing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#getResult = async (promise: Promise<Nip46ResponseWithResult>) => {
|
||||||
|
const {result} = await promise
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
request = async (method: string, params: string[]) => {
|
request = async (method: string, params: string[]) => {
|
||||||
if (this.#closed) {
|
if (this.#closed) {
|
||||||
throw new Error("Attempted to make a nip46 request with a closed broker")
|
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),
|
event: await this.#signer.sign(template),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.once(`auth-${id}`, res => {
|
this.once(`auth-${id}`, response => {
|
||||||
window.open(res.error, "Coracle", "width=600,height=800,popup=yes")
|
window.open(response.error, "", "width=600,height=800,popup=yes")
|
||||||
})
|
})
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<Nip46ResponseWithResult>((resolve, reject) => {
|
||||||
this.once(`res-${id}`, ({result, error}: Nip46Response) => {
|
this.once(`res-${id}`, (response: Nip46Response) => {
|
||||||
if (error) {
|
if (response.error) {
|
||||||
reject(error as string)
|
reject(response as Nip46ResponseWithError)
|
||||||
} else {
|
} else {
|
||||||
resolve(result as string)
|
resolve(response as Nip46ResponseWithResult)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueue = (method: string, params: string[]) =>
|
enqueue = (method: string, params: string[]) =>
|
||||||
new Promise<string>(resolve => {
|
new Promise<Nip46ResponseWithResult>(resolve => {
|
||||||
this.#queue.push({method, params, resolve})
|
this.#queue.push({method, params, resolve})
|
||||||
this.#processQueue()
|
this.#processQueue()
|
||||||
})
|
})
|
||||||
@@ -195,37 +271,32 @@ export class Nip46Broker extends Emitter {
|
|||||||
return this.enqueue("create_account", [username, this.#handler.domain, "", perms])
|
return this.enqueue("create_account", [username, this.#handler.domain, "", perms])
|
||||||
}
|
}
|
||||||
|
|
||||||
connect = async (token = "", perms = "") => {
|
connect = async (token = "", perms = "", secret = "") => {
|
||||||
if (!this.#connectResult) {
|
if (!this.#connectResponse) {
|
||||||
const params = ["", token, perms]
|
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) => {
|
signEvent = async (event: StampedEvent) =>
|
||||||
return JSON.parse(await this.enqueue("sign_event", [JSON.stringify(event)]) as string)
|
JSON.parse(await this.#getResult(this.enqueue("sign_event", [JSON.stringify(event)])))
|
||||||
}
|
|
||||||
|
|
||||||
nip04Encrypt = (pk: string, message: string) => {
|
nip04Encrypt = (pk: string, message: string) =>
|
||||||
return this.enqueue("nip04_encrypt", [pk, message])
|
this.#getResult(this.enqueue("nip04_encrypt", [pk, message]))
|
||||||
}
|
|
||||||
|
|
||||||
nip04Decrypt = (pk: string, message: string) => {
|
nip04Decrypt = (pk: string, message: string) =>
|
||||||
return this.enqueue("nip04_decrypt", [pk, message])
|
this.#getResult(this.enqueue("nip04_decrypt", [pk, message]))
|
||||||
}
|
|
||||||
|
|
||||||
nip44Encrypt = (pk: string, message: string) => {
|
nip44Encrypt = (pk: string, message: string) =>
|
||||||
return this.enqueue("nip44_encrypt", [pk, message])
|
this.#getResult(this.enqueue("nip44_encrypt", [pk, message]))
|
||||||
}
|
|
||||||
|
|
||||||
nip44Decrypt = (pk: string, message: string) => {
|
nip44Decrypt = (pk: string, message: string) =>
|
||||||
return this.enqueue("nip44_decrypt", [pk, message])
|
this.#getResult(this.enqueue("nip44_decrypt", [pk, message]))
|
||||||
}
|
|
||||||
|
|
||||||
teardown = () => {
|
teardown = () => {
|
||||||
this.#closed = true
|
this.#closed = true
|
||||||
|
|||||||
Reference in New Issue
Block a user