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 +8 -3
- package/bin/nappup/index.js +15 -1
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/services/bunker-signer.js +55 -0
- package/src/services/nostr-relays.js +42 -42
- package/src/services/nostr-signer.js +1 -0
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
|
|
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. **
|
|
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
|
-
|
|
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
|
|
package/bin/nappup/index.js
CHANGED
|
@@ -34,4 +34,18 @@ await confirmArgs(args)
|
|
|
34
34
|
|
|
35
35
|
const fileList = await toFileList(getFiles(dir), dir)
|
|
36
36
|
|
|
37
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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 {
|