Skip to content

Commit bfa40da

Browse files
committed
nip46: improve fromURI() and implement "switch_relays".
1 parent 9078f45 commit bfa40da

File tree

3 files changed

+58
-103
lines changed

3 files changed

+58
-103
lines changed

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nostr/tools",
3-
"version": "2.19.4",
3+
"version": "2.20.0",
44
"exports": {
55
".": "./index.ts",
66
"./core": "./core.ts",

nip46.ts

Lines changed: 56 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -87,31 +87,7 @@ export type NostrConnectParams = {
8787
image?: string
8888
}
8989

90-
export type ParsedNostrConnectURI = {
91-
protocol: 'nostrconnect'
92-
clientPubkey: string
93-
params: {
94-
relays: string[]
95-
secret: string
96-
perms?: string[]
97-
name?: string
98-
url?: string
99-
image?: string
100-
}
101-
originalString: string
102-
}
103-
10490
export function createNostrConnectURI(params: NostrConnectParams): string {
105-
if (!params.clientPubkey) {
106-
throw new Error('clientPubkey is required.')
107-
}
108-
if (!params.relays || params.relays.length === 0) {
109-
throw new Error('At least one relay is required.')
110-
}
111-
if (!params.secret) {
112-
throw new Error('secret is required.')
113-
}
114-
11591
const queryParams = new URLSearchParams()
11692

11793
params.relays.forEach(relay => {
@@ -136,55 +112,6 @@ export function createNostrConnectURI(params: NostrConnectParams): string {
136112
return `nostrconnect://${params.clientPubkey}?${queryParams.toString()}`
137113
}
138114

139-
export function parseNostrConnectURI(uri: string): ParsedNostrConnectURI {
140-
if (!uri.startsWith('nostrconnect://')) {
141-
throw new Error('Invalid nostrconnect URI: Must start with "nostrconnect://".')
142-
}
143-
144-
const [protocolAndPubkey, queryString] = uri.split('?')
145-
if (!protocolAndPubkey || !queryString) {
146-
throw new Error('Invalid nostrconnect URI: Missing query string.')
147-
}
148-
149-
const clientPubkey = protocolAndPubkey.substring('nostrconnect://'.length)
150-
if (!clientPubkey) {
151-
throw new Error('Invalid nostrconnect URI: Missing client-pubkey.')
152-
}
153-
154-
const queryParams = new URLSearchParams(queryString)
155-
156-
const relays = queryParams.getAll('relay')
157-
if (relays.length === 0) {
158-
throw new Error('Invalid nostrconnect URI: Missing "relay" parameter.')
159-
}
160-
161-
const secret = queryParams.get('secret')
162-
if (!secret) {
163-
throw new Error('Invalid nostrconnect URI: Missing "secret" parameter.')
164-
}
165-
166-
const permsString = queryParams.get('perms')
167-
const perms = permsString ? permsString.split(',') : undefined
168-
169-
const name = queryParams.get('name') || undefined
170-
const url = queryParams.get('url') || undefined
171-
const image = queryParams.get('image') || undefined
172-
173-
return {
174-
protocol: 'nostrconnect',
175-
clientPubkey,
176-
params: {
177-
relays,
178-
secret,
179-
perms,
180-
name,
181-
url,
182-
image,
183-
},
184-
originalString: uri,
185-
}
186-
}
187-
188115
export type BunkerSignerParams = {
189116
pool?: AbstractSimplePool
190117
onauth?: (url: string) => void
@@ -238,15 +165,15 @@ export class BunkerSigner implements Signer {
238165
params: BunkerSignerParams = {},
239166
): BunkerSigner {
240167
if (bp.relays.length === 0) {
241-
throw new Error('No relays specified for this bunker')
168+
throw new Error('no relays specified for this bunker')
242169
}
243170

244171
const signer = new BunkerSigner(clientSecretKey, params)
245172

246173
signer.conversationKey = getConversationKey(clientSecretKey, bp.pubkey)
247174
signer.bp = bp
248175

249-
signer.setupSubscription(params)
176+
signer.setupSubscription()
250177
return signer
251178
}
252179

@@ -257,22 +184,22 @@ export class BunkerSigner implements Signer {
257184
public static async fromURI(
258185
clientSecretKey: Uint8Array,
259186
connectionURI: string,
260-
params: BunkerSignerParams = {},
261-
maxWait: number = 300_000,
187+
bunkerParams: BunkerSignerParams = {},
188+
maxWaitOrAbort: number | AbortSignal = 300_000,
262189
): Promise<BunkerSigner> {
263-
const signer = new BunkerSigner(clientSecretKey, params)
264-
const parsedURI = parseNostrConnectURI(connectionURI)
190+
const signer = new BunkerSigner(clientSecretKey, bunkerParams)
191+
const uri = new URL(connectionURI)
265192
const clientPubkey = getPublicKey(clientSecretKey)
266193

267194
return new Promise((resolve, reject) => {
268-
const timer = setTimeout(() => {
269-
sub.close()
270-
reject(new Error(`Connection timed out after ${maxWait / 1000} seconds`))
271-
}, maxWait)
272-
195+
let success = false
273196
const sub = signer.pool.subscribe(
274-
parsedURI.params.relays,
275-
{ kinds: [NostrConnect], '#p': [clientPubkey] },
197+
uri.searchParams.getAll('relay'),
198+
{
199+
kinds: [NostrConnect],
200+
'#p': [clientPubkey],
201+
limit: 0,
202+
},
276203
{
277204
onevent: async (event: NostrEvent) => {
278205
try {
@@ -281,41 +208,48 @@ export class BunkerSigner implements Signer {
281208

282209
const response = JSON.parse(decryptedContent)
283210

284-
if (response.result === parsedURI.params.secret) {
285-
clearTimeout(timer)
211+
if (response.result === uri.searchParams.get('secret')) {
286212
sub.close()
287213

288214
signer.bp = {
289215
pubkey: event.pubkey,
290-
relays: parsedURI.params.relays,
291-
secret: parsedURI.params.secret,
216+
relays: uri.searchParams.getAll('relay'),
217+
secret: uri.searchParams.get('secret'),
292218
}
293219
signer.conversationKey = getConversationKey(clientSecretKey, event.pubkey)
294-
signer.setupSubscription(params)
220+
signer.setupSubscription()
221+
222+
success = true
223+
await Promise.race([new Promise(resolve => setTimeout(resolve, 1000)), signer.switchRelays()])
295224
resolve(signer)
296225
}
297226
} catch (e) {
298-
console.warn('Failed to process potential connection event', e)
227+
console.warn('failed to process potential connection event', e)
299228
}
300229
},
301230
onclose: () => {
302-
clearTimeout(timer)
303-
reject(new Error('Subscription closed before connection was established.'))
231+
if (!success) reject(new Error('subscription closed before connection was established.'))
304232
},
305-
maxWait,
233+
maxWait: typeof maxWaitOrAbort === 'number' ? maxWaitOrAbort : undefined,
234+
abort: typeof maxWaitOrAbort !== 'number' ? maxWaitOrAbort : undefined,
306235
},
307236
)
308237
})
309238
}
310239

311-
private setupSubscription(params: BunkerSignerParams) {
240+
private setupSubscription() {
312241
const listeners = this.listeners
313242
const waitingForAuth = this.waitingForAuth
314243
const convKey = this.conversationKey
315244

316245
this.subCloser = this.pool.subscribe(
317246
this.bp.relays,
318-
{ kinds: [NostrConnect], authors: [this.bp.pubkey], '#p': [getPublicKey(this.secretKey)] },
247+
{
248+
kinds: [NostrConnect],
249+
authors: [this.bp.pubkey],
250+
'#p': [getPublicKey(this.secretKey)],
251+
limit: 0,
252+
},
319253
{
320254
onevent: async (event: NostrEvent) => {
321255
const o = JSON.parse(decrypt(event.content, convKey))
@@ -324,8 +258,8 @@ export class BunkerSigner implements Signer {
324258
if (result === 'auth_url' && waitingForAuth[id]) {
325259
delete waitingForAuth[id]
326260

327-
if (params.onauth) {
328-
params.onauth(error)
261+
if (this.params.onauth) {
262+
this.params.onauth(error)
329263
} else {
330264
console.warn(
331265
`nostr-tools/nip46: remote signer ${this.bp.pubkey} tried to send an "auth_url"='${error}' but there was no onauth() callback configured.`,
@@ -349,6 +283,27 @@ export class BunkerSigner implements Signer {
349283
this.isOpen = true
350284
}
351285

286+
async switchRelays(): Promise<boolean> {
287+
try {
288+
const switchResp = await this.sendRequest('switch_relays', [])
289+
let relays = JSON.parse(switchResp) as string[] | null
290+
if (!relays) return false
291+
if (JSON.stringify(relays.sort()) === JSON.stringify(this.bp.relays)) return false
292+
293+
this.bp.relays = relays
294+
let previousCloser = this.subCloser!
295+
setTimeout(() => {
296+
previousCloser.close()
297+
}, 5000)
298+
299+
this.subCloser = undefined
300+
this.setupSubscription()
301+
return true
302+
} catch {
303+
return false
304+
}
305+
}
306+
352307
// closes the subscription -- this object can't be used anymore after this
353308
async close() {
354309
this.isOpen = false
@@ -359,7 +314,7 @@ export class BunkerSigner implements Signer {
359314
return new Promise(async (resolve, reject) => {
360315
try {
361316
if (!this.isOpen) throw new Error('this signer is not open anymore, create a new one')
362-
if (!this.subCloser) this.setupSubscription(this.params)
317+
if (!this.subCloser) this.setupSubscription()
363318

364319
this.serial++
365320
const id = `${this.idPrefix}-${this.serial}`
@@ -469,7 +424,7 @@ export async function createAccount(
469424
email?: string,
470425
localSecretKey: Uint8Array = generateSecretKey(),
471426
): Promise<BunkerSigner> {
472-
if (email && !EMAIL_REGEX.test(email)) throw new Error('Invalid email')
427+
if (email && !EMAIL_REGEX.test(email)) throw new Error('invalid email')
473428

474429
let rpc = BunkerSigner.fromBunker(localSecretKey, bunker.bunkerPointer, params)
475430

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"type": "module",
33
"name": "nostr-tools",
4-
"version": "2.19.4",
4+
"version": "2.20.0",
55
"description": "Tools for making a Nostr client.",
66
"repository": {
77
"type": "git",

0 commit comments

Comments
 (0)