Add signer
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import {nip04, finalizeEvent, getPublicKey} from "nostr-tools"
|
||||
import {hexToBytes} from '@noble/hashes/utils'
|
||||
import {Emitter, tryCatch, randomId, sleep, equals, now} from "@welshman/lib"
|
||||
import {createEvent, TrustedEvent, EventTemplate, NOSTR_CONNECT} from "@welshman/util"
|
||||
import {subscribe, publish, Subscription} from "@welshman/net"
|
||||
import {ISigner, decrypt} from '../util'
|
||||
import {SecretSigner} from './secret'
|
||||
|
||||
export type NostrConnectHandler = {
|
||||
pubkey: string
|
||||
relays: string[]
|
||||
domain?: string
|
||||
}
|
||||
|
||||
export type ConnectResponse = {
|
||||
id: string
|
||||
error?: string
|
||||
result?: string
|
||||
}
|
||||
|
||||
let singleton: NostrConnectBroker
|
||||
|
||||
// FIXME set the full list of requested perms
|
||||
const Perms =
|
||||
"nip04_encrypt,nip04_decrypt,sign_event:0,sign_event:1,sign_event:4,sign_event:6,sign_event:7"
|
||||
|
||||
export class NostrConnectBroker extends Emitter {
|
||||
#sub: Subscription
|
||||
#signer: ISigner
|
||||
#ready = sleep(500)
|
||||
#closed = false
|
||||
#connectResult: any
|
||||
|
||||
static get(pubkey: string, secret: string, handler: NostrConnectHandler) {
|
||||
if (
|
||||
singleton?.pubkey !== pubkey ||
|
||||
singleton?.secret !== secret ||
|
||||
!equals(singleton?.handler, handler)
|
||||
) {
|
||||
singleton?.teardown()
|
||||
singleton = new NostrConnectBroker(pubkey, secret, handler)
|
||||
}
|
||||
|
||||
return singleton
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly pubkey: string,
|
||||
readonly secret: string,
|
||||
readonly handler: NostrConnectHandler,
|
||||
) {
|
||||
super()
|
||||
|
||||
this.#signer = new SecretSigner(secret)
|
||||
this.#sub = this.subscribe()
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
const sub = subscribe({
|
||||
relays: this.handler.relays,
|
||||
filters: [
|
||||
{
|
||||
since: now() - 30,
|
||||
kinds: [NOSTR_CONNECT],
|
||||
"#p": [getPublicKey(hexToBytes(this.secret))],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
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))
|
||||
|
||||
if (!res.id) {
|
||||
console.error(`Invalid nostr-connect response: ${json}`)
|
||||
}
|
||||
|
||||
if (res.result === "auth_url") {
|
||||
this.emit(`auth-${res.id}`, res)
|
||||
} else {
|
||||
this.emit(`res-${res.id}`, res)
|
||||
}
|
||||
})
|
||||
|
||||
sub.emitter.on("complete", () => {
|
||||
if (!this.#closed) {
|
||||
this.#sub = this.subscribe()
|
||||
}
|
||||
})
|
||||
|
||||
return sub
|
||||
}
|
||||
|
||||
async request(method: string, params: string[], admin = false) {
|
||||
// nsecbunker has a race condition
|
||||
await this.#ready
|
||||
|
||||
const id = randomId()
|
||||
const pubkey = admin ? this.handler.pubkey : this.pubkey
|
||||
const payload = JSON.stringify({id, method, params})
|
||||
const content = await nip04.encrypt(this.secret, pubkey, payload)
|
||||
const template = createEvent(NOSTR_CONNECT, {content, tags: [["p", pubkey]]})
|
||||
const event = finalizeEvent(template, this.secret as any)
|
||||
|
||||
publish({event, relays: this.handler.relays})
|
||||
|
||||
this.once(`auth-${id}`, res => {
|
||||
window.open(res.result, "Coracle", "width=600,height=800,popup=yes")
|
||||
})
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.once(`res-${id}`, ({result, error}: ConnectResponse) => {
|
||||
if (error) {
|
||||
reject(error as string)
|
||||
} else {
|
||||
resolve(result as string)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createAccount(username: string) {
|
||||
if (!this.handler.domain) {
|
||||
throw new Error("Unable to create an account without a handler domain")
|
||||
}
|
||||
|
||||
return this.request("create_account", [username, this.handler.domain, "", Perms], true)
|
||||
}
|
||||
|
||||
async connect(token = "") {
|
||||
if (!this.#connectResult) {
|
||||
const params = [this.pubkey, token, Perms]
|
||||
|
||||
this.#connectResult = await this.request("connect", params)
|
||||
}
|
||||
|
||||
return this.#connectResult === "ack"
|
||||
}
|
||||
|
||||
async signEvent(event: EventTemplate) {
|
||||
return JSON.parse(await this.request("sign_event", [JSON.stringify(event)]) as string)
|
||||
}
|
||||
|
||||
nip04Encrypt(pk: string, message: string) {
|
||||
return this.request("nip04_encrypt", [pk, message])
|
||||
}
|
||||
|
||||
nip04Decrypt(pk: string, message: string) {
|
||||
return this.request("nip04_decrypt", [pk, message])
|
||||
}
|
||||
|
||||
nip44Encrypt(pk: string, message: string) {
|
||||
return this.request("nip44_encrypt", [pk, message])
|
||||
}
|
||||
|
||||
nip44Decrypt(pk: string, message: string) {
|
||||
return this.request("nip44_decrypt", [pk, message])
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.#closed = true
|
||||
this.#sub?.close()
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectSigner implements ISigner {
|
||||
constructor(private broker: NostrConnectBroker) {}
|
||||
|
||||
isEnabled = () => true
|
||||
|
||||
getPubkey = async () => this.broker.pubkey
|
||||
|
||||
sign = (event: EventTemplate) => this.broker.signEvent(event)
|
||||
|
||||
nip04 = {
|
||||
encrypt: this.broker.nip04Encrypt,
|
||||
decrypt: this.broker.nip04Decrypt,
|
||||
}
|
||||
|
||||
nip44 = {
|
||||
encrypt: this.broker.nip44Encrypt,
|
||||
decrypt: this.broker.nip44Decrypt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {EventTemplate} from '@welshman/util'
|
||||
import {hash, own, Sign, ISigner, EncryptionImplementation} from '../util'
|
||||
|
||||
export type Extension = {
|
||||
sign: Sign
|
||||
nip04: EncryptionImplementation
|
||||
nip44: EncryptionImplementation
|
||||
getPublicKey: () => string | undefined
|
||||
}
|
||||
|
||||
export class ExtensionSigner implements ISigner {
|
||||
#lock = Promise.resolve()
|
||||
|
||||
#ext = () => (window as {nostr?: Extension}).nostr
|
||||
|
||||
#then = async <T>(f: (ext: Extension) => T | Promise<T>) => {
|
||||
const promise = this.#lock.then(() => {
|
||||
const ext = this.#ext()
|
||||
|
||||
if (!ext) throw new Error("Extension is not enabled")
|
||||
|
||||
return f(ext)
|
||||
})
|
||||
|
||||
// Recover from errors
|
||||
this.#lock = promise.then(() => undefined, () => undefined)
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
isEnabled = () => Boolean(this.#ext())
|
||||
|
||||
getPubkey = () =>
|
||||
this.#then(ext => {
|
||||
const pubkey = ext.getPublicKey()
|
||||
|
||||
if (!pubkey) throw new Error("Failed to retrieve pubkey")
|
||||
|
||||
return pubkey as string
|
||||
})
|
||||
|
||||
sign = (event: EventTemplate) =>
|
||||
this.#then(ext => ext.sign(hash(own(ext.getPublicKey() as string, event))))
|
||||
|
||||
nip04 = {
|
||||
encrypt: (pubkey: string, message: string) =>
|
||||
this.#then(ext => ext.nip04.encrypt(pubkey, message)),
|
||||
decrypt: (pubkey: string, message: string) =>
|
||||
this.#then(ext => ext.nip04.decrypt(pubkey, message)),
|
||||
}
|
||||
|
||||
nip44 = {
|
||||
encrypt: (pubkey: string, message: string) =>
|
||||
this.#then(ext => ext.nip44.encrypt(pubkey, message)),
|
||||
decrypt: (pubkey: string, message: string) =>
|
||||
this.#then(ext => ext.nip44.decrypt(pubkey, message)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {EventTemplate} from '@welshman/util'
|
||||
import {nip04, nip44, own, hash, sign, getPubkey, ISigner} from "../util"
|
||||
|
||||
export class SecretSigner implements ISigner {
|
||||
private pubkey: string
|
||||
|
||||
constructor(private secret: string) {
|
||||
this.pubkey = getPubkey(this.secret)
|
||||
}
|
||||
|
||||
isEnabled = () => true
|
||||
|
||||
getPubkey = async () => this.pubkey
|
||||
|
||||
sign = async (event: EventTemplate) => sign(hash(own(this.pubkey, event)), this.secret)
|
||||
|
||||
nip04 = {
|
||||
encrypt: async (pubkey: string, message: string) =>
|
||||
nip04.encrypt(pubkey, this.secret, message),
|
||||
decrypt: async (pubkey: string, message: string) =>
|
||||
nip04.decrypt(pubkey, this.secret, message),
|
||||
}
|
||||
|
||||
nip44 = {
|
||||
encrypt: async (pubkey: string, message: string) =>
|
||||
nip44.encrypt(pubkey, this.secret, message),
|
||||
decrypt: async (pubkey: string, message: string) =>
|
||||
nip44.decrypt(pubkey, this.secret, message),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user