nappup 1.0.14 → 1.2.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,8 +26,9 @@ nappup [directory] [options]
26
26
 
27
27
  | Flag | Description |
28
28
  |------|-------------|
29
- | `-s <secret_key>` | Your Nostr secret key (hex format) used to sign the application event. See [Authentication](#authentication) for alternatives. |
29
+ | `-s <secret_key>` | Your Nostr secret key (hex or nsec format) used to sign the application event. See [Authentication](#authentication) for alternatives. |
30
30
  | `-d <d_tag>` | The unique identifier (`d` tag) for your application. If omitted, defaults to the directory name. Avoid generic names like `dist` or `build` - use something unique among your other apps like `mycoolapp`. |
31
+ | `-y` | Skip confirmation prompt. Useful for CI/CD pipelines or automated scripts. |
31
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. |
32
33
  | `--main` | Publish to the **main** release channel. This is the default behavior. |
33
34
  | `--next` | Publish to the **next** release channel. Ideal for beta testing or staging builds. |
@@ -37,36 +38,34 @@ nappup [directory] [options]
37
38
 
38
39
  Napp Up! supports multiple ways to provide your Nostr secret key:
39
40
 
40
- 1. **CLI flag**: Pass your hex-encoded secret key directly via `-s`:
41
+ 1. **CLI flag**: Pass your secret key (hex or nsec) directly via `-s`:
41
42
  ```bash
42
- nappup -s 0123456789abcdef...
43
+ nappup -s nsec1...
43
44
  ```
44
45
 
45
46
  2. **Environment variable**: Set `NOSTR_SECRET_KEY` in your environment or a `.env` file:
46
47
  ```bash
47
- export NOSTR_SECRET_KEY=0123456789abcdef...
48
+ export NOSTR_SECRET_KEY=nsec1...
48
49
  nappup ./dist
49
50
  ```
50
51
 
51
- 3. **Auto-generated key**: If no key is provided, Napp Up! will generate a new keypair automatically and store it in your project's `.env` file for future use.
52
-
53
- > **Note**: The secret key must be in **hex format**. If you have an `nsec`, convert it to hex first.
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.
54
53
 
55
54
  ### Examples
56
55
 
57
56
  Upload the current directory to the main channel:
58
57
  ```bash
59
- nappup -s 0123456789abcdef...
58
+ nappup -s nsec1...
60
59
  ```
61
60
 
62
61
  Or using an environment variable:
63
62
  ```bash
64
- NOSTR_SECRET_KEY=0123456789abcdef... nappup
63
+ NOSTR_SECRET_KEY=nsec1... nappup
65
64
  ```
66
65
 
67
66
  Upload a specific `dist` folder with a custom identifier to the `next` channel:
68
67
  ```bash
69
- nappup ./dist -s 0123456789abcdef... -d myapp --next
68
+ nappup ./dist -s nsec1... -d myapp --next
70
69
  ```
71
70
 
72
71
  Force re-upload a draft:
@@ -11,6 +11,7 @@ export function parseArgs (args) {
11
11
  let dTag = null
12
12
  let channel = null
13
13
  let shouldReupload = false
14
+ let yes = false
14
15
 
15
16
  for (let i = 0; i < args.length; i++) {
16
17
  if (args[i] === '-s' && args[i + 1]) {
@@ -27,6 +28,8 @@ export function parseArgs (args) {
27
28
  channel = 'draft'
28
29
  } else if (args[i] === '-r') {
29
30
  shouldReupload = true
31
+ } else if (args[i] === '-y') {
32
+ yes = true
30
33
  } else if (!args[i].startsWith('-') && dir === null) {
31
34
  dir = args[i]
32
35
  }
@@ -37,11 +40,13 @@ export function parseArgs (args) {
37
40
  sk,
38
41
  dTag,
39
42
  channel: channel || 'main',
40
- shouldReupload
43
+ shouldReupload,
44
+ yes
41
45
  }
42
46
  }
43
47
 
44
48
  export async function confirmArgs (args) {
49
+ if (args.yes) return
45
50
  const rl = readline.createInterface({
46
51
  input: process.stdin,
47
52
  output: process.stdout
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/44billion/nappup.git"
7
7
  },
8
8
  "license": "GPL-3.0-or-later",
9
- "version": "1.0.14",
9
+ "version": "1.2.0",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
@@ -0,0 +1,130 @@
1
+ const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
2
+
3
+ const ALPHABET_MAP = {}
4
+ for (let z = 0; z < ALPHABET.length; z++) {
5
+ const x = ALPHABET.charAt(z)
6
+ ALPHABET_MAP[x] = z
7
+ }
8
+
9
+ function polymod (values) {
10
+ let chk = 1
11
+ for (let p = 0; p < values.length; ++p) {
12
+ const top = chk >> 25
13
+ chk = (chk & 0x1ffffff) << 5 ^ values[p]
14
+ for (let i = 0; i < 5; ++i) {
15
+ if ((top >> i) & 1) {
16
+ chk ^= 0x3b6a57b2 >> i
17
+ }
18
+ }
19
+ }
20
+ return chk
21
+ }
22
+
23
+ function hrpExpand (hrp) {
24
+ const ret = []
25
+ for (let p = 0; p < hrp.length; ++p) {
26
+ ret.push(hrp.charCodeAt(p) >> 5)
27
+ }
28
+ ret.push(0)
29
+ for (let p = 0; p < hrp.length; ++p) {
30
+ ret.push(hrp.charCodeAt(p) & 31)
31
+ }
32
+ return ret
33
+ }
34
+
35
+ function verifyChecksum (hrp, data) {
36
+ return polymod(hrpExpand(hrp).concat(data)) === 1
37
+ }
38
+
39
+ function createChecksum (hrp, data) {
40
+ const values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0])
41
+ const mod = polymod(values) ^ 1
42
+ const ret = []
43
+ for (let p = 0; p < 6; ++p) {
44
+ ret.push((mod >> 5 * (5 - p)) & 31)
45
+ }
46
+ return ret
47
+ }
48
+
49
+ export function encode (hrp, data) {
50
+ const combined = data.concat(createChecksum(hrp, data))
51
+ let ret = hrp + '1'
52
+ for (let p = 0; p < combined.length; ++p) {
53
+ ret += ALPHABET.charAt(combined[p])
54
+ }
55
+ return ret
56
+ }
57
+
58
+ export function decode (bechString, limit = 90) {
59
+ let p
60
+ let hasLower = false
61
+ let hasUpper = false
62
+ for (p = 0; p < bechString.length; ++p) {
63
+ if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) {
64
+ return null
65
+ }
66
+ if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) {
67
+ hasLower = true
68
+ }
69
+ if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) {
70
+ hasUpper = true
71
+ }
72
+ }
73
+ if (hasLower && hasUpper) {
74
+ return null
75
+ }
76
+ bechString = bechString.toLowerCase()
77
+ const pos = bechString.lastIndexOf('1')
78
+ if (pos < 1 || pos + 7 > bechString.length || bechString.length > limit) {
79
+ return null
80
+ }
81
+ const hrp = bechString.substring(0, pos)
82
+ const data = []
83
+ for (p = pos + 1; p < bechString.length; ++p) {
84
+ const d = ALPHABET_MAP[bechString.charAt(p)]
85
+ if (d === undefined) {
86
+ return null
87
+ }
88
+ data.push(d)
89
+ }
90
+ if (!verifyChecksum(hrp, data)) {
91
+ return null
92
+ }
93
+ return { hrp, data: data.slice(0, data.length - 6) }
94
+ }
95
+
96
+ export function toWords (data) {
97
+ let value = 0
98
+ let bits = 0
99
+ const maxV = 31
100
+ const result = []
101
+ for (let i = 0; i < data.length; ++i) {
102
+ value = (value << 8) | data[i]
103
+ bits += 8
104
+ while (bits >= 5) {
105
+ bits -= 5
106
+ result.push((value >> bits) & maxV)
107
+ }
108
+ }
109
+ if (bits > 0) {
110
+ result.push((value << (5 - bits)) & maxV)
111
+ }
112
+ return result
113
+ }
114
+
115
+ export function fromWords (data) {
116
+ let value = 0
117
+ let bits = 0
118
+ const maxV = 255
119
+ const result = []
120
+ for (let i = 0; i < data.length; ++i) {
121
+ const element = data[i]
122
+ value = (value << 5) | element
123
+ bits += 5
124
+ while (bits >= 8) {
125
+ bits -= 8
126
+ result.push((value >> bits) & maxV)
127
+ }
128
+ }
129
+ return result
130
+ }
@@ -1,5 +1,6 @@
1
1
  import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
2
2
  import { bytesToBase62, base62ToBytes, BASE62_ALPHABET } from '#helpers/base62.js'
3
+ import { encode as bech32Encode, decode as bech32Decode, toWords, fromWords } from '#helpers/bech32.js'
3
4
  import { isNostrAppDTagSafe } from '#helpers/app.js'
4
5
 
5
6
  const MAX_SIZE = 5000
@@ -58,6 +59,22 @@ export function appDecode (entity) {
58
59
  }
59
60
  }
60
61
 
62
+ export function nsecEncode (hex) {
63
+ const bytes = base16ToBytes(hex)
64
+ const words = toWords(bytes)
65
+ return bech32Encode('nsec', words)
66
+ }
67
+
68
+ export function nsecDecode (nsec) {
69
+ const decoded = bech32Decode(nsec, MAX_SIZE)
70
+ if (!decoded) throw new Error('Invalid nsec')
71
+ const { hrp, data } = decoded
72
+ if (hrp !== 'nsec') throw new Error('Invalid nsec')
73
+ const bytes = fromWords(data)
74
+ if (bytes.length !== 32) throw new Error('Invalid nsec length')
75
+ return bytesToBase16(new Uint8Array(bytes))
76
+ }
77
+
61
78
  function toTlv (tlvConfig) {
62
79
  const arrays = []
63
80
  tlvConfig
@@ -16,19 +16,19 @@ export const freeRelays = [
16
16
  'wss://relay.nostr.band'
17
17
  ]
18
18
 
19
- // Interacts with Nostr relays.
19
+ // Interacts with Nostr relays
20
20
  export class NostrRelays {
21
21
  #relays = new Map()
22
22
  #relayTimeouts = new Map()
23
23
  #timeout = 30000 // 30 seconds
24
24
 
25
- // Get a relay connection, creating one if it doesn't exist.
25
+ // Get a relay connection, creating one if it doesn't exist
26
26
  async #getRelay (url) {
27
27
  if (this.#relays.has(url)) {
28
28
  clearTimeout(this.#relayTimeouts.get(url))
29
29
  this.#relayTimeouts.set(url, maybeUnref(setTimeout(() => this.disconnect(url), this.#timeout)))
30
30
  const relay = this.#relays.get(url)
31
- // reconnect if needed to avoid SendingOnClosedConnection errors
31
+ // Reconnect if needed to avoid SendingOnClosedConnection errors
32
32
  await relay.connect()
33
33
  return relay
34
34
  }
@@ -43,7 +43,7 @@ export class NostrRelays {
43
43
  return relay
44
44
  }
45
45
 
46
- // Disconnect from a relay.
46
+ // Disconnect from a relay
47
47
  async disconnect (url) {
48
48
  if (this.#relays.has(url)) {
49
49
  const relay = this.#relays.get(url)
@@ -54,7 +54,7 @@ export class NostrRelays {
54
54
  }
55
55
  }
56
56
 
57
- // Disconnect from all relays.
57
+ // Disconnect from all relays
58
58
  async disconnectAll () {
59
59
  for (const url of this.#relays.keys()) {
60
60
  await this.disconnect(url)
@@ -82,7 +82,7 @@ export class NostrRelays {
82
82
  onclose: err => {
83
83
  clearTimeout(timer)
84
84
  if (isClosed) return
85
- // may have closed normally, without error
85
+ // May have closed normally, without error
86
86
  err ? p.reject(err) : p.resolve()
87
87
  },
88
88
  oneose: () => {
@@ -110,7 +110,7 @@ export class NostrRelays {
110
110
  }
111
111
  }
112
112
 
113
- // Send an event to a list of relays.
113
+ // Send an event to a list of relays
114
114
  async sendEvent (event, relays, timeout = 3000) {
115
115
  const promises = relays.map(async (url) => {
116
116
  let timer
@@ -146,6 +146,7 @@ export class NostrRelays {
146
146
  }
147
147
  }
148
148
  }
149
- // Share same connection.
150
- // Connections aren't authenticated, thus no need to split by authed user.
149
+
150
+ // Share same connection
151
+ // Connections aren't authenticated, thus no need to split by authed user
151
152
  export default new NostrRelays()
@@ -7,6 +7,7 @@ import { getConversationKey, encrypt, decrypt } from 'nostr-tools/nip44'
7
7
  import nostrRelays, { seedRelays, freeRelays } from '#services/nostr-relays.js'
8
8
  import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
9
9
  import { finalizeEvent } from '#helpers/nip01.js'
10
+ import { nsecDecode, nsecEncode } from '#helpers/nip19.js'
10
11
  const __dirname = fileURLToPath(new URL('.', import.meta.url))
11
12
 
12
13
  const dotenvPath = process.env.DOTENV_CONFIG_PATH ?? `${__dirname}/../../.env`
@@ -34,21 +35,26 @@ export default class NostrSigner {
34
35
  this.#secretKey = skBytes
35
36
  }
36
37
 
37
- static async create (skHex) {
38
- if (skHex) return new this(createToken, base16ToBytes(skHex))
38
+ static async create (sk) {
39
+ if (sk) {
40
+ if (sk.startsWith('nsec')) sk = nsecDecode(sk)
41
+ return new this(createToken, base16ToBytes(sk))
42
+ }
39
43
 
40
44
  let skBytes
41
45
  let isNewSk = false
42
46
  if (process.env.NOSTR_SECRET_KEY) {
43
- skBytes = base16ToBytes(process.env.NOSTR_SECRET_KEY)
47
+ let envSk = process.env.NOSTR_SECRET_KEY
48
+ if (envSk.startsWith('nsec')) envSk = nsecDecode(envSk)
49
+ skBytes = base16ToBytes(envSk)
44
50
  } else {
45
51
  isNewSk = true
46
- skHex = generateSecretKey()
47
- fs.appendFileSync(path.resolve(dotenvPath), `NOSTR_SECRET_KEY=${skHex}\n`)
48
- skBytes = base16ToBytes(skHex)
52
+ sk = generateSecretKey()
53
+ fs.appendFileSync(path.resolve(dotenvPath), `NOSTR_SECRET_KEY=${nsecEncode(sk)}\n`)
54
+ skBytes = base16ToBytes(sk)
49
55
  }
50
56
  const ret = new this(createToken, skBytes)
51
- if (isNewSk) await ret.#initSk(skHex)
57
+ if (isNewSk) await ret.#initSk(sk)
52
58
  return ret
53
59
  }
54
60