nappup 1.0.9 → 1.0.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Arthur França
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Napp Up!
2
+
3
+ ```text
4
+ _ _ _ _ _
5
+ | \ | | __ _ _ __ _ __ | | | |_ __ | |
6
+ | \| |/ _` | '_ \| '_ \| | | | '_ \| |
7
+ | |\ | (_| | |_) | |_) | |_| | |_) |_|
8
+ |_| \_|\__,_| .__/| .__/ \___/| .__/(_)
9
+ |_| |_| |_|
10
+ ```
11
+
12
+ **Napp Up!** is a powerful CLI tool for developers to effortlessly upload and manage Nostr applications. Ship your decentralized apps to the Nostr network with a single command.
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ nappup [directory] [options]
18
+ ```
19
+
20
+ ### Arguments
21
+
22
+ - `[directory]`
23
+ The root directory of your application to upload. If omitted, defaults to the current working directory (`.`).
24
+
25
+ ### Options
26
+
27
+ | Flag | Description |
28
+ |------|-------------|
29
+ | `-s <secret_key>` | Your Nostr secret key (hex format) used to sign the application event. See [Authentication](#authentication) for alternatives. |
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
+ | `-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
+ | `--main` | Publish to the **main** release channel. This is the default behavior. |
33
+ | `--next` | Publish to the **next** release channel. Ideal for beta testing or staging builds. |
34
+ | `--draft` | Publish to the **draft** release channel. Use this for internal testing or work-in-progress builds. |
35
+
36
+ ## Authentication
37
+
38
+ Napp Up! supports multiple ways to provide your Nostr secret key:
39
+
40
+ 1. **CLI flag**: Pass your hex-encoded secret key directly via `-s`:
41
+ ```bash
42
+ nappup -s 0123456789abcdef...
43
+ ```
44
+
45
+ 2. **Environment variable**: Set `NOSTR_SECRET_KEY` in your environment or a `.env` file:
46
+ ```bash
47
+ export NOSTR_SECRET_KEY=0123456789abcdef...
48
+ nappup ./dist
49
+ ```
50
+
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.
54
+
55
+ ### Examples
56
+
57
+ Upload the current directory to the main channel:
58
+ ```bash
59
+ nappup -s 0123456789abcdef...
60
+ ```
61
+
62
+ Or using an environment variable:
63
+ ```bash
64
+ NOSTR_SECRET_KEY=0123456789abcdef... nappup
65
+ ```
66
+
67
+ Upload a specific `dist` folder with a custom identifier to the `next` channel:
68
+ ```bash
69
+ nappup ./dist -s 0123456789abcdef... -d myapp --next
70
+ ```
71
+
72
+ Force re-upload a draft:
73
+ ```bash
74
+ nappup ~/my-repos/projectx/build/projectx --draft -r
75
+ ```
@@ -10,6 +10,7 @@ export function parseArgs (args) {
10
10
  let sk = null
11
11
  let dTag = null
12
12
  let channel = null
13
+ let shouldReupload = false
13
14
 
14
15
  for (let i = 0; i < args.length; i++) {
15
16
  if (args[i] === '-s' && args[i + 1]) {
@@ -24,6 +25,8 @@ export function parseArgs (args) {
24
25
  channel = 'next'
25
26
  } else if (args[i] === '--draft' && channel === null) {
26
27
  channel = 'draft'
28
+ } else if (args[i] === '-r') {
29
+ shouldReupload = true
27
30
  } else if (!args[i].startsWith('-') && dir === null) {
28
31
  dir = args[i]
29
32
  }
@@ -33,7 +36,8 @@ export function parseArgs (args) {
33
36
  dir: path.resolve(dir ?? '.'),
34
37
  sk,
35
38
  dTag,
36
- channel: channel || 'main'
39
+ channel: channel || 'main',
40
+ shouldReupload
37
41
  }
38
42
  }
39
43
 
@@ -11,7 +11,7 @@ import toApp from '#index.js'
11
11
  const args = parseArgs(process.argv.slice(2))
12
12
  await confirmArgs(args)
13
13
 
14
- const { dir, sk, dTag, channel } = args
14
+ const { dir, sk, dTag, channel, shouldReupload } = args
15
15
  const fileList = await toFileList(getFiles(dir), dir)
16
16
 
17
- await toApp(fileList, await NostrSigner.create(sk), { log: console.log.bind(console), dTag, channel })
17
+ await toApp(fileList, await NostrSigner.create(sk), { log: console.log.bind(console), dTag, channel, shouldReupload })
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.9",
9
+ "version": "1.0.12",
10
10
  "description": "Nostr App Uploader",
11
11
  "type": "module",
12
12
  "scripts": {
@@ -17,10 +17,12 @@
17
17
  "nappup": "bin/nappup/index.js"
18
18
  },
19
19
  "dependencies": {
20
+ "@noble/curves": "^2.0.0",
21
+ "@noble/hashes": "^2.0.0",
20
22
  "dotenv": "^17.2.0",
21
23
  "file-type": "^21.0.0",
22
24
  "mime-types": "^3.0.1",
23
- "nmmr": "^1.0.4",
25
+ "nmmr": "^1.0.9",
24
26
  "nostr-tools": "^2.15.0"
25
27
  },
26
28
  "devDependencies": {
@@ -0,0 +1,54 @@
1
+ export function extractHtmlMetadata (htmlContent) {
2
+ let name
3
+ let description
4
+
5
+ try {
6
+ const titleRegex = /<title[^>]*>([\s\S]*?)<\/title>/i
7
+ const titleMatch = htmlContent.match(titleRegex)
8
+ if (titleMatch && titleMatch[1]) {
9
+ name = titleMatch[1].trim()
10
+ }
11
+
12
+ const metaDescRegex = /<meta\s+[^>]*name\s*=\s*["']description["'][^>]*content\s*=\s*["']([^"']+)["'][^>]*>/i
13
+ const metaDescMatch = htmlContent.match(metaDescRegex)
14
+ if (metaDescMatch && metaDescMatch[1]) {
15
+ description = metaDescMatch[1].trim()
16
+ }
17
+
18
+ if (!description) {
19
+ const altMetaDescRegex = /<meta\s+[^>]*content\s*=\s*["']([^"']+)["'][^>]*name\s*=\s*["']description["'][^>]*>/i
20
+ const altMetaDescMatch = htmlContent.match(altMetaDescRegex)
21
+ if (altMetaDescMatch && altMetaDescMatch[1]) {
22
+ description = altMetaDescMatch[1].trim()
23
+ }
24
+ }
25
+ } catch (_) {
26
+ // ignore
27
+ }
28
+
29
+ return { name, description }
30
+ }
31
+
32
+ export function findFavicon (fileList) {
33
+ const faviconExtensions = ['ico', 'svg', 'webp', 'png', 'jpg', 'jpeg', 'gif']
34
+ for (const file of fileList) {
35
+ const filename = (file.webkitRelativePath || file.name || '').split('/').pop().toLowerCase()
36
+ if (filename.startsWith('favicon.')) {
37
+ const ext = filename.split('.').pop()
38
+ if (faviconExtensions.includes(ext)) {
39
+ return file
40
+ }
41
+ }
42
+ }
43
+ return null
44
+ }
45
+
46
+ export function findIndexFile (fileList) {
47
+ for (const file of fileList) {
48
+ const filename = (file.webkitRelativePath || file.name || '').split('/').pop().toLowerCase()
49
+ if (filename === 'index.html' || filename === 'index.htm') {
50
+ return file
51
+ }
52
+ }
53
+ return null
54
+ }
@@ -1,4 +1,4 @@
1
- import { bytesToBase36 } from '#helpers/base36.js'
1
+ import { bytesToBase36, isBase36 } from '#helpers/base36.js'
2
2
 
3
3
  // 63 - (1<channel> + 5<b36loggeduserpkslug> 50<b36pk>)
4
4
  // <b36loggeduserpkslug> pk chars at positions [7][17][27][37][47]
@@ -6,10 +6,10 @@ import { bytesToBase36 } from '#helpers/base36.js'
6
6
  export const NOSTR_APP_D_TAG_MAX_LENGTH = 7
7
7
 
8
8
  export function isNostrAppDTagSafe (string) {
9
- return isSubdomainSafe(string) && string.length <= NOSTR_APP_D_TAG_MAX_LENGTH
9
+ return string.length > 0 && string.length <= NOSTR_APP_D_TAG_MAX_LENGTH && isBase36(string)
10
10
  }
11
11
 
12
- function isSubdomainSafe (string) {
12
+ export function isSubdomainSafe (string) {
13
13
  return /(?:^[a-z0-9]$)|(?:^(?!.*--)[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$)/.test(string)
14
14
  }
15
15
 
@@ -5,6 +5,12 @@ export const BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
5
5
  const BASE = BigInt(BASE36_ALPHABET.length)
6
6
  const LEADER = BASE36_ALPHABET[0]
7
7
  const CHAR_MAP = new Map([...BASE36_ALPHABET].map((char, index) => [char, BigInt(index)]))
8
+ const BASE36_REGEX = /^[0-9a-z]+$/
9
+
10
+ export function isBase36 (str) {
11
+ if (typeof str !== 'string') return false
12
+ return BASE36_REGEX.test(str)
13
+ }
8
14
 
9
15
  export function bytesToBase36 (bytes, padLength = 0) {
10
16
  return base16ToBase36(bytesToBase16(bytes), padLength)
@@ -1,7 +1,7 @@
1
- export const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
2
- const BASE = BigInt(ALPHABET.length)
3
- const LEADER = ALPHABET[0]
4
- const CHAR_MAP = new Map([...ALPHABET].map((char, index) => [char, BigInt(index)]))
1
+ export const BASE62_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
2
+ const BASE = BigInt(BASE62_ALPHABET.length)
3
+ const LEADER = BASE62_ALPHABET[0]
4
+ const CHAR_MAP = new Map([...BASE62_ALPHABET].map((char, index) => [char, BigInt(index)]))
5
5
 
6
6
  export function bytesToBase62 (bytes, padLength = 0) {
7
7
  if (bytes.length === 0) return ''.padStart(padLength, LEADER)
@@ -16,7 +16,7 @@ export function bytesToBase62 (bytes, padLength = 0) {
16
16
 
17
17
  while (num > 0n) {
18
18
  const remainder = num % BASE
19
- result = ALPHABET[Number(remainder)] + result
19
+ result = BASE62_ALPHABET[Number(remainder)] + result
20
20
  num = num / BASE
21
21
  }
22
22
 
@@ -0,0 +1,32 @@
1
+ import { schnorr } from '@noble/curves/secp256k1.js'
2
+ import { sha256 } from '@noble/hashes/sha2.js'
3
+ import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
4
+ import { getPublicKey } from 'nostr-tools/pure'
5
+
6
+ function serializeEvent (event) {
7
+ return JSON.stringify([
8
+ 0,
9
+ event.pubkey,
10
+ event.created_at,
11
+ event.kind,
12
+ event.tags,
13
+ event.content
14
+ ])
15
+ }
16
+
17
+ function getEventHash (event) {
18
+ return sha256(new TextEncoder().encode(serializeEvent(event)))
19
+ }
20
+
21
+ function getSignature (eventHash, privkey) {
22
+ return bytesToBase16(schnorr.sign(eventHash, privkey))
23
+ }
24
+
25
+ export function finalizeEvent (event, privkey, withSig = true) {
26
+ event.pubkey ??= getPublicKey(privkey)
27
+ const eventHash = event.id ? base16ToBytes(event.id) : getEventHash(event)
28
+ event.id ??= bytesToBase16(eventHash)
29
+ if (withSig) event.sig ??= getSignature(eventHash, privkey)
30
+ else delete event.sig
31
+ return event
32
+ }
@@ -1,9 +1,9 @@
1
1
  import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
2
- import { bytesToBase62, base62ToBytes, ALPHABET as base62Alphabet } from '#helpers/base62.js'
2
+ import { bytesToBase62, base62ToBytes, BASE62_ALPHABET } from '#helpers/base62.js'
3
3
  import { isNostrAppDTagSafe } from '#helpers/app.js'
4
4
 
5
5
  const MAX_SIZE = 5000
6
- export const BASE62_ENTITY_REGEX = new RegExp(`^\\+{1,3}[${base62Alphabet}]{,${MAX_SIZE}}$`)
6
+ export const NAPP_ENTITY_REGEX = new RegExp(`^\\+{1,3}[${BASE62_ALPHABET}]{48,${MAX_SIZE}}$`)
7
7
  const textEncoder = new TextEncoder()
8
8
  const textDecoder = new TextDecoder()
9
9
 
@@ -46,9 +46,11 @@ export function appDecode (entity) {
46
46
  if (!tlv[0]?.[0]) throw new Error('Missing deduplication tag')
47
47
  if (!tlv[2]?.[0]) throw new Error('Missing author pubkey')
48
48
  if (tlv[2][0].length !== 32) throw new Error('Author pubkey should be 32 bytes')
49
+ const dTag = textDecoder.decode(tlv[0][0])
50
+ if (!isNostrAppDTagSafe(dTag)) { throw new Error('Invalid deduplication tag') }
49
51
 
50
52
  return {
51
- dTag: textDecoder.decode(tlv[0][0]),
53
+ dTag,
52
54
  pubkey: bytesToBase16(tlv[2][0]),
53
55
  kind: kindByChannel[channel],
54
56
  channel,
@@ -18,3 +18,16 @@ export async function * streamToChunks (stream, chunkSize) {
18
18
 
19
19
  if (buffer.length > 0) yield buffer
20
20
  }
21
+
22
+ export async function streamToText (stream) {
23
+ const reader = stream.getReader()
24
+ let result = ''
25
+ const decoder = new TextDecoder()
26
+ while (true) {
27
+ const { done, value } = await reader.read()
28
+ if (done) break
29
+ result += decoder.decode(value, { stream: true })
30
+ }
31
+ result += decoder.decode()
32
+ return result
33
+ }