Compare commits

...

9 Commits

Author SHA1 Message Date
fiatjaf
aec8ff5946 fix for updated typescript. 2025-04-02 11:44:41 -03:00
fiatjaf
e498c9144d nip46: auto-reconnect. 2025-04-02 10:58:26 -03:00
fiatjaf
42d47abba1 update readme and add more examples. 2025-04-02 10:53:33 -03:00
fiatjaf
303c35120c pool: deprecate subscribeManyMap and introduce subscribe/subscribeEose methods that take a single filter. 2025-04-02 10:37:10 -03:00
fiatjaf
4a738c93d0 nip46: stop supporting nip04-encrypted messages. 2025-04-02 10:25:19 -03:00
fiatjaf
2a11c9ec91 nip04: functions shouldn't be async. 2025-04-02 10:19:27 -03:00
fiatjaf
cbe3a9d683 pool subscribe methods accept an onauth param. 2025-04-01 19:16:42 -03:00
fiatjaf
2944a932b8 nip46: mark connection as closed when relays disconnect. 2025-03-29 18:03:39 -03:00
codytseng
6b39de04d7 Fix auth() not returning on consecutive calls 2025-03-17 13:31:24 -03:00
8 changed files with 308 additions and 116 deletions

203
README.md
View File

@@ -57,43 +57,43 @@ let event = finalizeEvent({
let isGood = verifyEvent(event) let isGood = verifyEvent(event)
``` ```
### Interacting with a relay ### Interacting with one or multiple relays
Doesn't matter what you do, you always should be using a `SimplePool`:
```js ```js
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure' import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'
import { Relay } from 'nostr-tools/relay' import { SimplePool } from 'nostr-tools/pool'
const relay = await Relay.connect('wss://relay.example.com') const pool = new SimplePool()
console.log(`connected to ${relay.url}`)
// let's query for an event that exists // let's query for an event that exists
const sub = relay.subscribe([ const event = relay.get(
['wss://relay.example.com'],
{ {
ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'], ids: ['d7dd5eb3ab747e16f8d0212d53032ea2a7cadef53837e5a6c66d42849fcb9027'],
}, },
], { )
onevent(event) { if (event) {
console.log('we got the event we wanted:', event) console.log('it exists indeed on this relay:', event)
}, }
oneose() {
sub.close()
}
})
// let's publish a new event while simultaneously monitoring the relay for it // let's publish a new event while simultaneously monitoring the relay for it
let sk = generateSecretKey() let sk = generateSecretKey()
let pk = getPublicKey(sk) let pk = getPublicKey(sk)
relay.subscribe([ pool.subscribe(
['wss://a.com', 'wss://b.com', 'wss://c.com'],
{ {
kinds: [1], kinds: [1],
authors: [pk], authors: [pk],
}, },
], { {
onevent(event) { onevent(event) {
console.log('got event:', event) console.log('got event:', event)
} }
}) }
)
let eventTemplate = { let eventTemplate = {
kind: 1, kind: 1,
@@ -104,7 +104,7 @@ let eventTemplate = {
// this assigns the pubkey, calculates the event id and signs the event in a single step // this assigns the pubkey, calculates the event id and signs the event in a single step
const signedEvent = finalizeEvent(eventTemplate, sk) const signedEvent = finalizeEvent(eventTemplate, sk)
await relay.publish(signedEvent) await pool.publish(['wss://a.com', 'wss://b.com'], signedEvent)
relay.close() relay.close()
``` ```
@@ -119,59 +119,116 @@ import WebSocket from 'ws'
useWebSocketImplementation(WebSocket) useWebSocketImplementation(WebSocket)
``` ```
### Interacting with multiple relays ### Parsing references (mentions) from a content based on NIP-27
```js ```js
import { SimplePool } from 'nostr-tools/pool' import * as nip27 from '@nostr/tools/nip27'
const pool = new SimplePool() for (let block of nip27.parse(evt.content)) {
switch (block.type) {
let relays = ['wss://relay.example.com', 'wss://relay.example2.com'] case 'text':
console.log(block.text)
let h = pool.subscribeMany( break
[...relays, 'wss://relay.example3.com'], case 'reference': {
[ if ('id' in block.pointer) {
{ console.log("it's a nevent1 uri", block.pointer)
authors: ['32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'], } else if ('identifier' in block.pointer) {
}, console.log("it's a naddr1 uri", block.pointer)
], } else {
{ console.log("it's an npub1 or nprofile1 uri", block.pointer)
onevent(event) {
// this will only be called once the first time the event is received
// ...
},
oneose() {
h.close()
} }
break
} }
) case 'url': {
console.log("it's a normal url:", block.url)
await Promise.any(pool.publish(relays, newEvent)) break
console.log('published to at least one relay!') }
case 'image':
let events = await pool.querySync(relays, { kinds: [0, 1] }) case 'video':
let event = await pool.get(relays, { case 'audio':
ids: ['44e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'], console.log("it's a media url:", block.url)
}) case 'relay':
console.log("it's a websocket url, probably a relay address:", block.url)
default:
break
}
}
``` ```
### Parsing references (mentions) from a content using NIP-10 and NIP-27 ### Connecting to a bunker using NIP-46
```js ```js
import { parseReferences } from 'nostr-tools/references' import { generateSecretKey, getPublicKey } from '@nostr/tools/pure'
import { BunkerSigner, parseBunkerInput } from '@nostr/tools/nip46'
import { SimplePool } from '@nostr/tools/pool'
let references = parseReferences(event) // the client needs a local secret key (which is generally persisted) for communicating with the bunker
let simpleAugmentedContent = event.content const localSecretKey = generateSecretKey()
for (let i = 0; i < references.length; i++) {
let { text, profile, event, address } = references[i] // parse a bunker URI
let augmentedReference = profile const bunkerPointer = await parseBunkerInput('bunker://abcd...?relay=wss://relay.example.com')
? `<strong>@${profilesCache[profile.pubkey].name}</strong>` if (!bunkerPointer) {
: event throw new Error('Invalid bunker input')
? `<em>${eventsCache[event.id].content.slice(0, 5)}</em>` }
: address
? `<a href="${text}">[link]</a>` // create the bunker instance
: text const pool = new SimplePool()
simpleAugmentedContent.replaceAll(text, augmentedReference) const bunker = new BunkerSigner(localSecretKey, bunkerPointer, { pool })
await bunker.connect()
// and use it
const pubkey = await bunker.getPublicKey()
const event = await bunker.signEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'Hello from bunker!'
})
// cleanup
await signer.close()
pool.close([])
```
### Parsing thread from any note based on NIP-10
```js
import * as nip10 from '@nostr/tools/nip10'
// event is a nostr event with tags
const refs = nip10.parse(event)
// get the root event of the thread
if (refs.root) {
console.log('root event:', refs.root.id)
console.log('root event relay hints:', refs.root.relays)
console.log('root event author:', refs.root.author)
}
// get the immediate parent being replied to
if (refs.reply) {
console.log('reply to:', refs.reply.id)
console.log('reply relay hints:', refs.reply.relays)
console.log('reply author:', refs.reply.author)
}
// get any mentioned events
for (let mention of refs.mentions) {
console.log('mentioned event:', mention.id)
console.log('mention relay hints:', mention.relays)
console.log('mention author:', mention.author)
}
// get any quoted events
for (let quote of refs.quotes) {
console.log('quoted event:', quote.id)
console.log('quote relay hints:', quote.relays)
}
// get any referenced profiles
for (let profile of refs.profiles) {
console.log('referenced profile:', profile.pubkey)
console.log('profile relay hints:', profile.relays)
} }
``` ```
@@ -205,32 +262,6 @@ declare global {
} }
``` ```
### Generating NIP-06 keys
```js
import {
privateKeyFromSeedWords,
accountFromSeedWords,
extendedKeysFromSeedWords,
accountFromExtendedKey
} from 'nostr-tools/nip06'
const mnemonic = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong'
const passphrase = '123' // optional
const accountIndex = 0
const sk0 = privateKeyFromSeedWords(mnemonic, passphrase, accountIndex)
const { privateKey: sk1, publicKey: pk1 } = accountFromSeedWords(mnemonic, passphrase, accountIndex)
const extendedAccountIndex = 0
const { privateExtendedKey, publicExtendedKey } = extendedKeysFromSeedWords(mnemonic, passphrase, extendedAccountIndex)
const { privateKey: sk2, publicKey: pk2 } = accountFromExtendedKey(privateExtendedKey)
const { publicKey: pk3 } = accountFromExtendedKey(publicExtendedKey)
```
### Encoding and decoding NIP-19 codes ### Encoding and decoding NIP-19 codes
```js ```js

View File

@@ -8,7 +8,7 @@ import {
} from './abstract-relay.ts' } from './abstract-relay.ts'
import { normalizeURL } from './utils.ts' import { normalizeURL } from './utils.ts'
import type { Event, Nostr } from './core.ts' import type { Event, EventTemplate, Nostr, VerifiedEvent } from './core.ts'
import { type Filter } from './filter.ts' import { type Filter } from './filter.ts'
import { alwaysTrue } from './helpers.ts' import { alwaysTrue } from './helpers.ts'
@@ -19,6 +19,7 @@ export type AbstractPoolConstructorOptions = AbstractRelayConstructorOptions & {
export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose'> & { export type SubscribeManyParams = Omit<SubscriptionParams, 'onclose'> & {
maxWait?: number maxWait?: number
onclose?: (reasons: string[]) => void onclose?: (reasons: string[]) => void
doauth?: (event: EventTemplate) => Promise<VerifiedEvent>
id?: string id?: string
label?: string label?: string
} }
@@ -61,10 +62,127 @@ export class AbstractSimplePool {
}) })
} }
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser { subscribe(relays: string[], filter: Filter, params: SubscribeManyParams): SubCloser {
return this.subscribeManyMap(Object.fromEntries(relays.map(url => [url, filters])), params) return this.subscribeMap(
relays.map(url => ({ url, filter })),
params,
)
} }
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
return this.subscribeMap(
relays.flatMap(url => filters.map(filter => ({ url, filter }))),
params,
)
}
subscribeMap(requests: { url: string; filter: Filter }[], params: SubscribeManyParams): SubCloser {
if (this.trackRelays) {
params.receivedEvent = (relay: AbstractRelay, id: string) => {
let set = this.seenOn.get(id)
if (!set) {
set = new Set()
this.seenOn.set(id, set)
}
set.add(relay)
}
}
const _knownIds = new Set<string>()
const subs: Subscription[] = []
// batch all EOSEs into a single
const eosesReceived: boolean[] = []
let handleEose = (i: number) => {
if (eosesReceived[i]) return // do not act twice for the same relay
eosesReceived[i] = true
if (eosesReceived.filter(a => a).length === requests.length) {
params.oneose?.()
handleEose = () => {}
}
}
// batch all closes into a single
const closesReceived: string[] = []
let handleClose = (i: number, reason: string) => {
if (closesReceived[i]) return // do not act twice for the same relay
handleEose(i)
closesReceived[i] = reason
if (closesReceived.filter(a => a).length === requests.length) {
params.onclose?.(closesReceived)
handleClose = () => {}
}
}
const localAlreadyHaveEventHandler = (id: string) => {
if (params.alreadyHaveEvent?.(id)) {
return true
}
const have = _knownIds.has(id)
_knownIds.add(id)
return have
}
// open a subscription in all given relays
const allOpened = Promise.all(
requests.map(async ({ url, filter }, i) => {
url = normalizeURL(url)
let relay: AbstractRelay
try {
relay = await this.ensureRelay(url, {
connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined,
})
} catch (err) {
handleClose(i, (err as any)?.message || String(err))
return
}
let subscription = relay.subscribe([filter], {
...params,
oneose: () => handleEose(i),
onclose: reason => {
if (reason.startsWith('auth-required:') && params.doauth) {
relay
.auth(params.doauth)
.then(() => {
relay.subscribe([filter], {
...params,
oneose: () => handleEose(i),
onclose: reason => {
handleClose(i, reason) // the second time we won't try to auth anymore
},
alreadyHaveEvent: localAlreadyHaveEventHandler,
eoseTimeout: params.maxWait,
})
})
.catch(err => {
handleClose(i, `auth was required and attempted, but failed with: ${err}`)
})
} else {
handleClose(i, reason)
}
},
alreadyHaveEvent: localAlreadyHaveEventHandler,
eoseTimeout: params.maxWait,
})
subs.push(subscription)
}),
)
return {
async close() {
await allOpened
subs.forEach(sub => {
sub.close()
})
},
}
}
/**
* @deprecated Use subscribeMap instead.
*/
subscribeManyMap(requests: { [relay: string]: Filter[] }, params: SubscribeManyParams): SubCloser { subscribeManyMap(requests: { [relay: string]: Filter[] }, params: SubscribeManyParams): SubCloser {
if (this.trackRelays) { if (this.trackRelays) {
params.receivedEvent = (relay: AbstractRelay, id: string) => { params.receivedEvent = (relay: AbstractRelay, id: string) => {
@@ -137,7 +255,28 @@ export class AbstractSimplePool {
let subscription = relay.subscribe(filters, { let subscription = relay.subscribe(filters, {
...params, ...params,
oneose: () => handleEose(i), oneose: () => handleEose(i),
onclose: reason => handleClose(i, reason), onclose: reason => {
if (reason.startsWith('auth-required:') && params.doauth) {
relay
.auth(params.doauth)
.then(() => {
relay.subscribe(filters, {
...params,
oneose: () => handleEose(i),
onclose: reason => {
handleClose(i, reason) // the second time we won't try to auth anymore
},
alreadyHaveEvent: localAlreadyHaveEventHandler,
eoseTimeout: params.maxWait,
})
})
.catch(err => {
handleClose(i, `auth was required and attempted, but failed with: ${err}`)
})
} else {
handleClose(i, reason)
}
},
alreadyHaveEvent: localAlreadyHaveEventHandler, alreadyHaveEvent: localAlreadyHaveEventHandler,
eoseTimeout: params.maxWait, eoseTimeout: params.maxWait,
}) })
@@ -156,10 +295,24 @@ export class AbstractSimplePool {
} }
} }
subscribeEose(
relays: string[],
filter: Filter,
params: Pick<SubscribeManyParams, 'label' | 'id' | 'onevent' | 'onclose' | 'maxWait' | 'doauth'>,
): SubCloser {
const subcloser = this.subscribe(relays, filter, {
...params,
oneose() {
subcloser.close()
},
})
return subcloser
}
subscribeManyEose( subscribeManyEose(
relays: string[], relays: string[],
filters: Filter[], filters: Filter[],
params: Pick<SubscribeManyParams, 'label' | 'id' | 'onevent' | 'onclose' | 'maxWait'>, params: Pick<SubscribeManyParams, 'label' | 'id' | 'onevent' | 'onclose' | 'maxWait' | 'doauth'>,
): SubCloser { ): SubCloser {
const subcloser = this.subscribeMany(relays, filters, { const subcloser = this.subscribeMany(relays, filters, {
...params, ...params,
@@ -177,7 +330,7 @@ export class AbstractSimplePool {
): Promise<Event[]> { ): Promise<Event[]> {
return new Promise(async resolve => { return new Promise(async resolve => {
const events: Event[] = [] const events: Event[] = []
this.subscribeManyEose(relays, [filter], { this.subscribeEose(relays, filter, {
...params, ...params,
onevent(event: Event) { onevent(event: Event) {
events.push(event) events.push(event)

View File

@@ -35,6 +35,7 @@ export class AbstractRelay {
private incomingMessageQueue = new Queue<string>() private incomingMessageQueue = new Queue<string>()
private queueRunning = false private queueRunning = false
private challenge: string | undefined private challenge: string | undefined
private authPromise: Promise<string> | undefined
private serial: number = 0 private serial: number = 0
private verifyEvent: Nostr['verifyEvent'] private verifyEvent: Nostr['verifyEvent']
@@ -77,6 +78,7 @@ export class AbstractRelay {
if (this.connectionPromise) return this.connectionPromise if (this.connectionPromise) return this.connectionPromise
this.challenge = undefined this.challenge = undefined
this.authPromise = undefined
this.connectionPromise = new Promise((resolve, reject) => { this.connectionPromise = new Promise((resolve, reject) => {
this.connectionTimeoutHandle = setTimeout(() => { this.connectionTimeoutHandle = setTimeout(() => {
reject('connection timed out') reject('connection timed out')
@@ -220,6 +222,7 @@ export class AbstractRelay {
return return
case 'AUTH': { case 'AUTH': {
this.challenge = data[1] as string this.challenge = data[1] as string
this.authPromise = undefined
this._onauth?.(data[1] as string) this._onauth?.(data[1] as string)
return return
} }
@@ -239,8 +242,9 @@ export class AbstractRelay {
public async auth(signAuthEvent: (evt: EventTemplate) => Promise<VerifiedEvent>): Promise<string> { public async auth(signAuthEvent: (evt: EventTemplate) => Promise<VerifiedEvent>): Promise<string> {
if (!this.challenge) throw new Error("can't perform auth, no challenge was received") if (!this.challenge) throw new Error("can't perform auth, no challenge was received")
if (this.authPromise) return this.authPromise
const evt = await signAuthEvent(makeAuthEvent(this.url, this.challenge)) const evt = await signAuthEvent(makeAuthEvent(this.url, this.challenge))
const ret = new Promise<string>((resolve, reject) => { this.authPromise = new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
const ep = this.openEventPublishes.get(evt.id) as EventPublishResolver const ep = this.openEventPublishes.get(evt.id) as EventPublishResolver
if (ep) { if (ep) {
@@ -251,7 +255,7 @@ export class AbstractRelay {
this.openEventPublishes.set(evt.id, { resolve, reject, timeout }) this.openEventPublishes.set(evt.id, { resolve, reject, timeout })
}) })
this.send('["AUTH",' + JSON.stringify(evt) + ']') this.send('["AUTH",' + JSON.stringify(evt) + ']')
return ret return this.authPromise
} }
public async publish(event: Event): Promise<string> { public async publish(event: Event): Promise<string> {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nostr/tools", "name": "@nostr/tools",
"version": "2.11.0", "version": "2.12.0",
"exports": { "exports": {
".": "./index.ts", ".": "./index.ts",
"./core": "./core.ts", "./core": "./core.ts",

View File

@@ -5,7 +5,7 @@ import { base64 } from '@scure/base'
import { utf8Decoder, utf8Encoder } from './utils.ts' import { utf8Decoder, utf8Encoder } from './utils.ts'
export async function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): Promise<string> { export function encrypt(secretKey: string | Uint8Array, pubkey: string, text: string): string {
const privkey: string = secretKey instanceof Uint8Array ? bytesToHex(secretKey) : secretKey const privkey: string = secretKey instanceof Uint8Array ? bytesToHex(secretKey) : secretKey
const key = secp256k1.getSharedSecret(privkey, '02' + pubkey) const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
const normalizedKey = getNormalizedX(key) const normalizedKey = getNormalizedX(key)
@@ -21,7 +21,7 @@ export async function encrypt(secretKey: string | Uint8Array, pubkey: string, te
return `${ctb64}?iv=${ivb64}` return `${ctb64}?iv=${ivb64}`
} }
export async function decrypt(secretKey: string | Uint8Array, pubkey: string, data: string): Promise<string> { export function decrypt(secretKey: string | Uint8Array, pubkey: string, data: string): string {
const privkey: string = secretKey instanceof Uint8Array ? bytesToHex(secretKey) : secretKey const privkey: string = secretKey instanceof Uint8Array ? bytesToHex(secretKey) : secretKey
let [ctb64, ivb64] = data.split('?iv=') let [ctb64, ivb64] = data.split('?iv=')
let key = secp256k1.getSharedSecret(privkey, '02' + pubkey) let key = secp256k1.getSharedSecret(privkey, '02' + pubkey)

View File

@@ -1,7 +1,7 @@
import { test, expect } from 'bun:test' import { test, expect } from 'bun:test'
import { v2 } from './nip44.js' import { v2 } from './nip44.js'
import { bytesToHex, hexToBytes } from '@noble/hashes/utils' import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
import { default as vec } from './nip44.vectors.json' assert { type: 'json' } import { default as vec } from './nip44.vectors.json' with { type: 'json' }
import { schnorr } from '@noble/curves/secp256k1' import { schnorr } from '@noble/curves/secp256k1'
const v2vec = vec.v2 const v2vec = vec.v2

View File

@@ -1,7 +1,6 @@
import { EventTemplate, NostrEvent, VerifiedEvent } from './core.ts' import { EventTemplate, NostrEvent, VerifiedEvent } from './core.ts'
import { generateSecretKey, finalizeEvent, getPublicKey, verifyEvent } from './pure.ts' import { generateSecretKey, finalizeEvent, getPublicKey, verifyEvent } from './pure.ts'
import { AbstractSimplePool, SubCloser } from './abstract-pool.ts' import { AbstractSimplePool, SubCloser } from './abstract-pool.ts'
import { decrypt as legacyDecrypt } from './nip04.ts'
import { getConversationKey, decrypt, encrypt } from './nip44.ts' import { getConversationKey, decrypt, encrypt } from './nip44.ts'
import { NIP05_REGEX } from './nip05.ts' import { NIP05_REGEX } from './nip05.ts'
import { SimplePool } from './pool.ts' import { SimplePool } from './pool.ts'
@@ -74,7 +73,7 @@ export type BunkerSignerParams = {
export class BunkerSigner { export class BunkerSigner {
private pool: AbstractSimplePool private pool: AbstractSimplePool
private subCloser: SubCloser private subCloser: SubCloser | undefined
private isOpen: boolean private isOpen: boolean
private serial: number private serial: number
private idPrefix: string private idPrefix: string
@@ -112,22 +111,20 @@ export class BunkerSigner {
this.listeners = {} this.listeners = {}
this.waitingForAuth = {} this.waitingForAuth = {}
this.setupSubscription(params)
}
private setupSubscription(params: BunkerSignerParams) {
const listeners = this.listeners const listeners = this.listeners
const waitingForAuth = this.waitingForAuth const waitingForAuth = this.waitingForAuth
const convKey = this.conversationKey const convKey = this.conversationKey
this.subCloser = this.pool.subscribeMany( this.subCloser = this.pool.subscribe(
this.bp.relays, this.bp.relays,
[{ kinds: [NostrConnect], authors: [bp.pubkey], '#p': [getPublicKey(this.secretKey)] }], { kinds: [NostrConnect], authors: [this.bp.pubkey], '#p': [getPublicKey(this.secretKey)] },
{ {
async onevent(event: NostrEvent) { onevent: async (event: NostrEvent) => {
let o const o = JSON.parse(decrypt(event.content, convKey))
try {
o = JSON.parse(decrypt(event.content, convKey))
} catch (err) {
o = JSON.parse(await legacyDecrypt(clientSecretKey, event.pubkey, event.content))
}
const { id, result, error } = o const { id, result, error } = o
if (result === 'auth_url' && waitingForAuth[id]) { if (result === 'auth_url' && waitingForAuth[id]) {
@@ -137,7 +134,7 @@ export class BunkerSigner {
params.onauth(error) params.onauth(error)
} else { } else {
console.warn( console.warn(
`nostr-tools/nip46: remote signer ${bp.pubkey} tried to send an "auth_url"='${error}' but there was no onauth() callback configured.`, `nostr-tools/nip46: remote signer ${this.bp.pubkey} tried to send an "auth_url"='${error}' but there was no onauth() callback configured.`,
) )
} }
return return
@@ -150,6 +147,13 @@ export class BunkerSigner {
delete listeners[id] delete listeners[id]
} }
}, },
onclose: () => {
if (this.isOpen) {
// If we get onclose but isOpen is still true, that means the client still wants to stay connected
this.subCloser!.close()
this.setupSubscription(params)
}
},
}, },
) )
this.isOpen = true this.isOpen = true
@@ -158,7 +162,7 @@ export class BunkerSigner {
// closes the subscription -- this object can't be used anymore after this // closes the subscription -- this object can't be used anymore after this
async close() { async close() {
this.isOpen = false this.isOpen = false
this.subCloser.close() this.subCloser!.close()
} }
async sendRequest(method: string, params: string[]): Promise<string> { async sendRequest(method: string, params: string[]): Promise<string> {

View File

@@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "nostr-tools", "name": "nostr-tools",
"version": "2.11.0", "version": "2.12.0",
"description": "Tools for making a Nostr client.", "description": "Tools for making a Nostr client.",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -274,7 +274,7 @@
"msw": "^2.1.4", "msw": "^2.1.4",
"node-fetch": "^2.6.9", "node-fetch": "^2.6.9",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"typescript": "^5.0.4" "typescript": "^5.8.2"
}, },
"scripts": { "scripts": {
"prepublish": "just build" "prepublish": "just build"