window.nostr.js 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "window.nostr.js",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "main": "dist/window.nostr.js",
6
6
  "devDependencies": {
@@ -24,6 +24,6 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "debounce": "2.0.0",
27
- "nostr-tools": "2.9.3"
27
+ "nostr-tools": "2.12.0"
28
28
  }
29
29
  }
package/src/App.svelte CHANGED
@@ -11,27 +11,26 @@
11
11
  import {
12
12
  BunkerSigner,
13
13
  type BunkerSignerParams,
14
- createAccount,
15
14
  parseBunkerInput,
16
15
  type BunkerPointer,
17
- type BunkerProfile,
18
- BUNKER_REGEX,
19
- queryBunkerProfile
16
+ BUNKER_REGEX
20
17
  } from 'nostr-tools/nip46'
21
- import {NIP05_REGEX, queryProfile} from 'nostr-tools/nip05'
18
+ import {NIP05_REGEX} from 'nostr-tools/nip05'
22
19
  import {npubEncode} from 'nostr-tools/nip19'
23
20
  import {onMount} from 'svelte'
24
21
  import mediaQueryStore from './mediaQueryStore.js'
25
22
  import Spinner from './Spinner.svelte'
26
23
 
24
+ const currentDomain = window.location.hostname
25
+ const currentProtocol = window.location.protocol
26
+
27
27
  const mobileMode = mediaQueryStore('only screen and (max-width: 640px)')
28
28
  const lskeys = {
29
29
  ORIGIN: 'wnj:origin',
30
30
  CLIENT_SECRET: 'wnj:clientSecret',
31
31
  Y_POS: 'wnj:ypos',
32
32
  CALLBACK_TOKEN: 'wnj:callbackToken',
33
- BUNKER_POINTER: 'wnj:bunkerPointer',
34
- CACHED_PUBKEY: 'wnj:cachedPubKey'
33
+ BUNKER_POINTER: 'wnj:bunkerPointer'
35
34
  }
36
35
 
37
36
  let myself: HTMLDivElement
@@ -48,9 +47,6 @@
48
47
  const pool = new SimplePool()
49
48
  let bunkerInput: HTMLInputElement
50
49
  let bunkerInputValue: string
51
- let nameInput: HTMLInputElement
52
- let nameInputValue: string
53
- let chosenProvider: BunkerProfile | undefined
54
50
  let clientSecret: Uint8Array
55
51
  const local = localStorage.getItem(lskeys.CLIENT_SECRET)
56
52
  if (local) {
@@ -61,6 +57,7 @@
61
57
  }
62
58
 
63
59
  let state: 'opened' | 'closed' | 'justopened' | 'justclosed' = 'closed'
60
+ let nostrLogin = false
64
61
  let bunkerPointer: BunkerPointer | null
65
62
  let resolveBunker: (_: BunkerSigner) => void
66
63
  let rejectBunker: (_: string) => void
@@ -71,6 +68,7 @@
71
68
  let showLogin: string | null = null
72
69
  let showConfirmAction: string | null = null
73
70
  let takingTooLong = false
71
+ let hasTriedToConnectButFailed = false
74
72
  let creating: boolean
75
73
  let awaitingCreation: boolean
76
74
  let errorMessage: string
@@ -83,17 +81,6 @@
83
81
  event: NostrEvent | null
84
82
  }
85
83
  let metadataSub: SubCloser | null
86
- let providers: BunkerProfile[] = [
87
- {
88
- picture: 'https://nsec.app/favicon.ico',
89
- name: 'Nsec.app',
90
- about:
91
- 'Store keys safely without a browser extension! Nsec.app is a non-custodial web app that can be connected to Nostr apps and provide restricted access to your keys.',
92
- nip05: '_@nsec.app',
93
- website: 'https://nsec.app',
94
- domain: 'nsec.app'
95
- } as BunkerProfile
96
- ]
97
84
 
98
85
  const connectBunkerError =
99
86
  'We could not connect to a NIP-46 bunker with that url, are you sure it is set up correctly?'
@@ -112,7 +99,6 @@
112
99
  let clickStart: number
113
100
 
114
101
  $: opened = state === 'justopened' || state === 'opened'
115
-
116
102
  $: movingStyle = hasMoved
117
103
  ? 'cursor-grabbing outline-dashed outline-' +
118
104
  accent +
@@ -131,7 +117,7 @@
131
117
  showAuth = url
132
118
  } else if (identity) {
133
119
  showConfirmAction = url
134
- opened = true
120
+ state = 'opened'
135
121
  } else {
136
122
  showLogin = url
137
123
  }
@@ -176,7 +162,7 @@
176
162
  let windowNostr = {
177
163
  isWnj: true,
178
164
  async getPublicKey(): Promise<string> {
179
- if (!connecting && !connected) open()
165
+ if (!connecting && !connected) connectOrOpen()
180
166
  return (await bunker).getPublicKey()
181
167
  },
182
168
  async signEvent(event: NostrEvent): Promise<VerifiedEvent> {
@@ -191,8 +177,7 @@
191
177
  async getRelays(): Promise<{
192
178
  [url: string]: {read: boolean; write: boolean}
193
179
  }> {
194
- if (!connecting && !connected) connectOrOpen()
195
- return (await bunker).getRelays()
180
+ return {}
196
181
  },
197
182
  nip04: {
198
183
  async encrypt(pubkey: string, plaintext: string): Promise<string> {
@@ -223,18 +208,48 @@
223
208
  }
224
209
 
225
210
  onMount(() => {
211
+ // Verify Nstart callback if the hash contains "nostr-login"
212
+ const hash = window.location.hash
213
+ if (hash.startsWith('#nostr-login=')) {
214
+ // Extract the value after "nostr-login="
215
+ const value = hash.substring(hash.indexOf('=') + 1)
216
+
217
+ // Reset nostr-login data
218
+ const urlWithoutHash = window.location.href.split('#')[0]
219
+ history.replaceState(null, '', urlWithoutHash)
220
+
221
+ if (value.startsWith('bunker://')) {
222
+ bunkerInputValue = value
223
+ const event = new SubmitEvent('submit', {
224
+ bubbles: true,
225
+ cancelable: true
226
+ })
227
+ nostrLogin = true
228
+ open()
229
+ handleConnect(event)
230
+ }
231
+ }
232
+
226
233
  if (!bunkerPointer) {
227
234
  let data = localStorage.getItem(lskeys.BUNKER_POINTER)
228
235
  if (data) {
229
- bunkerPointer = JSON.parse(data)
236
+ bunkerPointer = JSON.parse(data) as BunkerPointer
237
+
238
+ // rebuild a bunker url from the pointer data so we can fill in the input
239
+ let bunkerURL = new URL(`bunker://${bunkerPointer.pubkey}`)
240
+ bunkerPointer.relays.forEach(relay => {
241
+ bunkerURL.searchParams.append('relay', relay)
242
+ })
243
+ if (bunkerPointer.secret) {
244
+ bunkerURL.searchParams.set('secret', bunkerPointer.secret)
245
+ }
246
+ bunkerInputValue = bunkerURL.toString()
247
+ // ~
248
+
230
249
  identify()
231
250
 
232
251
  // we must connect here so identify() works because we can't rely on the bunker params to read our pubkey
233
- // however we may first check if we have it cached locally before doing the expensive connection
234
- let cachedPubkey = localStorage.getItem(lskeys.CACHED_PUBKEY)
235
- if (!cachedPubkey) {
236
- connect()
237
- }
252
+ connect()
238
253
  }
239
254
  }
240
255
 
@@ -300,9 +315,9 @@
300
315
  async function handleConnect(ev: SubmitEvent) {
301
316
  ev.preventDefault()
302
317
  try {
303
- bunkerPointer = await parseBunkerInput(bunkerInput.value)
318
+ bunkerPointer = await parseBunkerInput(bunkerInputValue)
304
319
  if (!bunkerPointer) {
305
- if (bunkerInput.value.match(BUNKER_REGEX)) {
320
+ if (bunkerInputValue.match(BUNKER_REGEX)) {
306
321
  errorMessage = connectBunkerError
307
322
  } else {
308
323
  errorMessage = connectNip05Error
@@ -318,7 +333,7 @@
318
333
  // wait until the connection has succeeded before loading user data
319
334
  identify()
320
335
  } catch (error) {
321
- if (bunkerInput.value.match(BUNKER_REGEX)) {
336
+ if (bunkerInputValue.match(BUNKER_REGEX)) {
322
337
  errorMessage = connectBunkerError
323
338
  } else {
324
339
  errorMessage = connectNip05Error
@@ -330,83 +345,26 @@
330
345
  async function handleDisconnect(ev: MouseEvent) {
331
346
  ev.preventDefault()
332
347
  localStorage.removeItem(lskeys.BUNKER_POINTER)
333
- localStorage.removeItem(lskeys.CACHED_PUBKEY)
334
348
  reset()
335
349
  }
336
350
 
337
- async function handleOpenCreate(ev: MouseEvent) {
351
+ async function handleErasePointer(ev: MouseEvent) {
338
352
  ev.preventDefault()
339
- creating = true
340
-
341
- if (providers.length > 0 && providers[0].bunkerPointer == null) {
342
- let toRemove: BunkerProfile[] = []
343
- let promises: Promise<void>[] = []
344
-
345
- for (let i = 0; i < providers.length; i++) {
346
- let promise = queryBunkerProfile(providers[i].nip05).then(
347
- (bp: BunkerPointer | null) => {
348
- if (!bp) {
349
- toRemove.push(providers[i])
350
- } else {
351
- providers[i].bunkerPointer = bp
352
- }
353
- }
354
- )
355
- promises.push(promise)
356
- }
357
-
358
- await Promise.all(promises)
359
- for (let r = 0; r < toRemove.length; r++) {
360
- let idx = providers.indexOf(toRemove[r])
361
- providers.splice(idx, 1)
362
- }
363
-
364
- providers = providers
365
- }
353
+ bunkerInputValue = ''
354
+ localStorage.removeItem(lskeys.BUNKER_POINTER)
355
+ hasTriedToConnectButFailed = false
366
356
  }
367
357
 
368
358
  function handleOpenLogin(_: MouseEvent) {
369
359
  creating = false
370
360
  }
371
361
 
372
- async function handleCreate(ev: SubmitEvent) {
373
- ev.preventDefault()
374
- if (!chosenProvider) return
375
-
376
- awaitingCreation = true
377
- let bunker = await createAccount(
378
- chosenProvider,
379
- bunkerSignerParams,
380
- nameInput.value,
381
- chosenProvider.domain,
382
- undefined,
383
- clientSecret
384
- )
385
- awaitingCreation = false
386
-
387
- open()
388
- creating = false
389
-
390
- bunkerPointer = bunker.bp
391
- identify()
392
- connect(bunker)
393
- }
394
-
395
- const checkNameInput = debounce(async () => {
396
- if (chosenProvider && nameInput.value.length > 0) {
397
- if (await queryProfile(nameInput.value + '@' + chosenProvider.domain)) {
398
- nameInput.setCustomValidity(`'${nameInput.value}' is already taken.`)
399
- } else {
400
- nameInput.setCustomValidity('')
401
- }
402
- }
403
- }, 500)
404
-
405
362
  function handleAbortConnection() {
406
363
  takingTooLong = false
407
364
  connecting = false
408
365
  rejectBunker('connection aborted')
409
366
  reset()
367
+ open()
410
368
  }
411
369
 
412
370
  async function connect(b: BunkerSigner | undefined = undefined) {
@@ -415,7 +373,7 @@
415
373
 
416
374
  let connectionTimeout = setTimeout(() => {
417
375
  takingTooLong = true
418
- opened = true
376
+ state = 'opened'
419
377
  }, 5000)
420
378
 
421
379
  try {
@@ -434,18 +392,20 @@
434
392
  showAuth = null
435
393
  showLogin = null
436
394
  showConfirmAction = null
395
+ if (nostrLogin) {
396
+ open()
397
+ }
437
398
  }
438
399
  }
439
400
 
440
401
  // identify() is what gives a name and picture to our floating widget
441
402
  async function identify() {
442
- let pubkey = localStorage.getItem(lskeys.CACHED_PUBKEY)
443
- if (!pubkey) {
403
+ let pubkey: string
404
+ try {
444
405
  pubkey = await (await bunker).getPublicKey()
445
-
446
- // store this pubkey here so we don't have to connect and get our pubkey immediately
447
- // the next time we open this page
448
- localStorage.setItem(lskeys.CACHED_PUBKEY, pubkey)
406
+ } catch (err) {
407
+ hasTriedToConnectButFailed = true
408
+ return
449
409
  }
450
410
 
451
411
  identity = {
@@ -534,6 +494,10 @@
534
494
  localStorage.setItem(lskeys.Y_POS, ypos.toString())
535
495
  }
536
496
  }
497
+
498
+ function handleCreateAccount() {
499
+ window.location.href = `https://nstart.me?an=${currentDomain}&at=web&ac=${currentProtocol}//${currentDomain}&sfb=yes`
500
+ }
537
501
  </script>
538
502
 
539
503
  <svelte:window
@@ -708,39 +672,17 @@
708
672
  <!-- Create account view ################### -->
709
673
  {:else if creating}
710
674
  <div class="text-center text-lg">Create a Nostr account</div>
711
- <form class="mb-1 mt-4" on:submit={handleCreate}>
712
- <div class="flex flex-row">
713
- <!-- svelte-ignore a11y-autofocus -->
714
- <input
715
- class="box-border w-40 rounded px-2 py-1 text-lg text-neutral-800 outline-none"
716
- placeholder="bob"
717
- bind:this={nameInput}
718
- bind:value={nameInputValue}
719
- on:input={checkNameInput}
720
- autofocus
721
- autocapitalize="none"
722
- />
723
- <div class="mx-2 text-2xl">@</div>
724
- <select
725
- class="box-border w-full rounded px-2 py-1 text-lg text-neutral-800 outline-none"
726
- bind:value={chosenProvider}
727
- >
728
- {#each providers as prov}
729
- <option
730
- label={prov.domain}
731
- value={prov}
732
- class="px-2 py-1 text-lg"
733
- />
734
- {/each}
735
- </select>
736
- </div>
737
- <button
738
- class="mt-4 block w-full cursor-pointer rounded border-0 px-2 py-1 text-lg text-white disabled:cursor-default disabled:bg-neutral-400 disabled:text-neutral-200 bg-{accent}-900 hover:bg-{accent}-950"
739
- disabled={!chosenProvider || !nameInputValue || awaitingCreation}
740
- >
741
- Continue »
742
- </button>
743
- </form>
675
+ <div class="mt-4 text-base leading-5">
676
+ To use this Nostr app you need a profile. The following button opens a
677
+ wizard that help you to create your keypair and safely manage it in a
678
+ few steps. Are you ready?
679
+ </div>
680
+ <button
681
+ class="mt-4 block w-full cursor-pointer rounded border-0 px-2 py-1 text-lg text-white disabled:cursor-default disabled:bg-neutral-400 disabled:text-neutral-200 bg-{accent}-900 hover:bg-{accent}-950"
682
+ on:click={handleCreateAccount}
683
+ disabled={awaitingCreation}
684
+ >Create an account »
685
+ </button>
744
686
  <div class="mt-6 text-center text-sm leading-3">
745
687
  Do you already have a Nostr address?<br />
746
688
  <button
@@ -795,11 +737,19 @@
795
737
  </form>
796
738
  {#if !connecting}
797
739
  <div class="mt-6 text-center text-sm leading-3">
798
- Do you need a Nostr account?<br />
799
- <button
800
- class="cursor-pointer border-0 bg-transparent text-sm text-white underline"
801
- on:click={handleOpenCreate}>Sign up now</button
802
- >
740
+ {#if hasTriedToConnectButFailed}
741
+ Is this bunker provider broken?<br />
742
+ <button
743
+ class="cursor-pointer border-0 bg-transparent text-sm text-white underline"
744
+ on:click={handleErasePointer}>Clear it</button
745
+ >
746
+ {:else}
747
+ Do you need a Nostr account?<br />
748
+ <button
749
+ class="cursor-pointer border-0 bg-transparent text-sm text-white underline"
750
+ on:click={handleCreateAccount}>Sign up now</button
751
+ >
752
+ {/if}
803
753
  </div>
804
754
  {/if}
805
755