nappup 1.8.4 → 1.9.0

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/README.md CHANGED
@@ -26,7 +26,7 @@ nappup [directory] [options]
26
26
 
27
27
  | Flag | Description |
28
28
  |------|-------------|
29
- | `-s <secret_key>` | Your Nostr secret key (hex or nsec format) used to sign the application event. See [Authentication](#authentication) for alternatives. |
29
+ | `-s <secret_key>` | Your Nostr secret key (hex, nsec, or `bunker://` URL) used to sign the application event. See [Authentication](#authentication) for alternatives. |
30
30
  | `-d <d_tag>` | The identifier (`d` tag) for your application. Any UTF-8 text up to 260 characters. If omitted, defaults to the directory name. Avoid generic names like `dist` or `build` - use something unique among your other apps like `mycoolapp`. |
31
31
  | `-y` | Skip confirmation prompt. Useful for CI/CD pipelines or automated scripts. |
32
32
  | `-r` | Force re-upload. By default, Napp Up! might skip files that haven't changed. Use this flag to ensure everything is pushed fresh. |
@@ -43,13 +43,18 @@ Napp Up! supports multiple ways to provide your Nostr secret key:
43
43
  nappup -s nsec1...
44
44
  ```
45
45
 
46
- 2. **Environment variable**: Set `NOSTR_SECRET_KEY` in your environment or a `.env` file:
46
+ 2. **Remote signer (NIP-46)**: Pass a `bunker://` URL to sign events via a remote signer like [nak](https://github.com/fiatjaf/nak?tab=readme-ov-file#start-a-bunker-that-persists-its-metadata-secret-key-relays-authorized-client-pubkeys-to-disc) or [Amber](https://github.com/greenart7c3/Amber):
47
+ ```bash
48
+ nappup -s 'bunker://<pubkey>?relay=wss://relay.example.com&secret=<token>'
49
+ ```
50
+
51
+ 3. **Environment variable**: Set `NOSTR_SECRET_KEY` in your environment or a `.env` file (also supports `bunker://` URLs):
47
52
  ```bash
48
53
  export NOSTR_SECRET_KEY=nsec1...
49
54
  nappup ./dist
50
55
  ```
51
56
 
52
- 3. **Auto-generated key**: If no key is provided, Napp Up! will generate a new keypair automatically and store it (as nsec) in your project's `.env` file for future use.
57
+ 4. **Auto-generated key**: If no key is provided, Napp Up! will generate a new keypair automatically and store it (as nsec) in your project's `.env` file for future use.
53
58
 
54
59
  ### Examples
55
60
 
@@ -34,4 +34,18 @@ await confirmArgs(args)
34
34
 
35
35
  const fileList = await toFileList(getFiles(dir), dir)
36
36
 
37
- await toApp(fileList, await NostrSigner.create(sk), { log: console.log.bind(console), dTag, channel, shouldReupload })
37
+ const bunkerUrl = sk?.startsWith('bunker://') ? sk : (!sk && process.env.NOSTR_SECRET_KEY?.startsWith('bunker://')) ? process.env.NOSTR_SECRET_KEY : null
38
+
39
+ let signer
40
+ if (bunkerUrl) {
41
+ const { default: NostrBunkerSigner } = await import('#services/bunker-signer.js')
42
+ signer = await NostrBunkerSigner.create(bunkerUrl)
43
+ } else {
44
+ signer = await NostrSigner.create(sk)
45
+ }
46
+
47
+ try {
48
+ await toApp(fileList, signer, { log: console.log.bind(console), dTag, channel, shouldReupload })
49
+ } finally {
50
+ await signer.close?.()
51
+ }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "MIT",
9
- "version": "1.8.4",
9
+ "version": "1.9.0",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
package/src/index.js CHANGED
@@ -75,7 +75,7 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, onEvent =
75
75
  nostrSigner.getRelays = getRelays
76
76
  }
77
77
  const writeRelays = [...new Set((await nostrSigner.getRelays()).write.map(r => r.trim().replace(/\/$/, '')))]
78
- log(`Found ${writeRelays.length} outbox relays for pubkey ${nostrSigner.getPublicKey()}:\n${writeRelays.join(', ')}`)
78
+ log(`Found ${writeRelays.length} outbox relays for pubkey ${await nostrSigner.getPublicKey()}:\n${writeRelays.join(', ')}`)
79
79
  if (writeRelays.length === 0) throw new Error('No outbox relays found')
80
80
 
81
81
  if (typeof dTag === 'string') {
@@ -0,0 +1,55 @@
1
+ import { generateSecretKey } from 'nostr-tools/pure'
2
+ import { BunkerSigner, parseBunkerInput } from 'nostr-tools/nip46'
3
+ import { getRelays } from '#helpers/signer.js'
4
+
5
+ const CONNECT_TIMEOUT = 30_000
6
+ const createToken = Symbol('createToken')
7
+
8
+ export default class NostrBunkerSigner {
9
+ #bunker
10
+ #publicKey // hex, cached
11
+
12
+ constructor (token, bunker, publicKey) {
13
+ if (token !== createToken) throw new Error('Use NostrBunkerSigner.create(bunkerUrl) to instantiate this class.')
14
+ this.#bunker = bunker
15
+ this.#publicKey = publicKey
16
+ }
17
+
18
+ static async create (bunkerUrl) {
19
+ const bp = await parseBunkerInput(bunkerUrl)
20
+ if (!bp) throw new Error('Invalid bunker URL')
21
+ if (bp.relays.length === 0) throw new Error('Bunker URL must include at least one relay (?relay=wss://...)')
22
+
23
+ const clientSk = generateSecretKey()
24
+ const bunker = new BunkerSigner(clientSk, bp)
25
+
26
+ await Promise.race([
27
+ bunker.connect(),
28
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Bunker connection timed out')), CONNECT_TIMEOUT))
29
+ ])
30
+
31
+ const publicKey = await bunker.getPublicKey()
32
+ return new this(createToken, bunker, publicKey)
33
+ }
34
+
35
+ getPublicKey () {
36
+ return this.#publicKey
37
+ }
38
+
39
+ signEvent (event) {
40
+ return this.#bunker.signEvent(event)
41
+ }
42
+
43
+ async getRelays () {
44
+ return getRelays.call(this)
45
+ }
46
+
47
+ nip44 = {
48
+ encrypt: (pubkey, plaintext) => this.#bunker.nip44Encrypt(pubkey, plaintext),
49
+ decrypt: (pubkey, ciphertext) => this.#bunker.nip44Decrypt(pubkey, ciphertext)
50
+ }
51
+
52
+ async close () {
53
+ await this.#bunker.close()
54
+ }
55
+ }
@@ -68,39 +68,34 @@ export class NostrRelays {
68
68
  const events = []
69
69
  const promises = relays.map(async (url) => {
70
70
  let sub
71
- let isClosed = false
72
71
  const p = Promise.withResolvers()
72
+ const t = Promise.withResolvers()
73
73
  const timer = maybeUnref(setTimeout(() => {
74
- isClosed = true
75
74
  sub?.close()
76
- p.reject(new Error(`timeout: ${url}`))
75
+ t.reject(new Error(`timeout: ${url}`))
77
76
  }, timeout))
77
+
78
+ ;(async () => {
79
+ try {
80
+ const relay = await this.#getRelay(url)
81
+ sub = relay.subscribe([filter], {
82
+ onevent: (event) => {
83
+ event.meta = { relay: url }
84
+ events.push(event)
85
+ },
86
+ onclose: err => err ? p.reject(err) : p.resolve(),
87
+ oneose: () => { sub.close(); p.resolve() }
88
+ })
89
+ } catch (err) {
90
+ p.reject(err)
91
+ }
92
+ })()
93
+
78
94
  try {
79
- const relay = await this.#getRelay(url)
80
- sub = relay.subscribe([filter], {
81
- onevent: (event) => {
82
- event.meta = { relay: url }
83
- events.push(event)
84
- },
85
- onclose: err => {
86
- clearTimeout(timer)
87
- if (isClosed) return
88
- // May have closed normally, without error
89
- err ? p.reject(err) : p.resolve()
90
- },
91
- oneose: () => {
92
- clearTimeout(timer)
93
- isClosed = true
94
- sub.close()
95
- p.resolve()
96
- }
97
- })
98
- } catch (err) {
95
+ await Promise.race([p.promise, t.promise])
96
+ } finally {
99
97
  clearTimeout(timer)
100
- p.reject(err)
101
98
  }
102
-
103
- return p.promise
104
99
  })
105
100
 
106
101
  const results = await Promise.allSettled(promises)
@@ -119,27 +114,32 @@ export class NostrRelays {
119
114
  if (eventToSend.meta) delete eventToSend.meta
120
115
 
121
116
  const promises = relays.map(async (url) => {
122
- let timer
123
117
  const p = Promise.withResolvers()
124
- try {
125
- timer = maybeUnref(setTimeout(() => {
126
- p.reject(new Error(`timeout: ${url}`))
127
- }, timeout))
128
-
129
- const relay = await this.#getRelay(url)
130
- await relay.publish(eventToSend)
131
- p.resolve()
132
- } catch (err) {
133
- if (err.message?.startsWith('duplicate:')) return p.resolve()
134
- if (err.message?.startsWith('mute:')) {
135
- console.info(`${url} - ${err.message}`)
136
- return p.resolve()
118
+ const t = Promise.withResolvers()
119
+ const timer = maybeUnref(setTimeout(() => {
120
+ t.reject(new Error(`timeout: ${url}`))
121
+ }, timeout))
122
+
123
+ ;(async () => {
124
+ try {
125
+ const relay = await this.#getRelay(url)
126
+ await relay.publish(eventToSend)
127
+ p.resolve()
128
+ } catch (err) {
129
+ if (err.message?.startsWith('duplicate:')) return p.resolve()
130
+ if (err.message?.startsWith('mute:')) {
131
+ console.info(`${url} - ${err.message}`)
132
+ return p.resolve()
133
+ }
134
+ p.reject(err)
137
135
  }
138
- p.reject(err)
136
+ })()
137
+
138
+ try {
139
+ await Promise.race([p.promise, t.promise])
139
140
  } finally {
140
141
  clearTimeout(timer)
141
142
  }
142
- return p.promise
143
143
  })
144
144
 
145
145
  const results = await Promise.allSettled(promises)
@@ -46,6 +46,7 @@ export default class NostrSigner {
46
46
  let isNewSk = false
47
47
  if (process.env.NOSTR_SECRET_KEY) {
48
48
  let envSk = process.env.NOSTR_SECRET_KEY
49
+ if (envSk.startsWith('bunker://')) throw new Error('bunker:// URLs are not supported by NostrSigner. Use NostrBunkerSigner.create() instead.')
49
50
  if (envSk.startsWith('nsec')) envSk = nsecDecode(envSk)
50
51
  skBytes = base16ToBytes(envSk)
51
52
  } else {