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 +21 -0
- package/README.md +75 -0
- package/bin/nappup/helpers.js +5 -1
- package/bin/nappup/index.js +2 -2
- package/package.json +4 -2
- package/src/helpers/app-metadata.js +54 -0
- package/src/helpers/app.js +3 -3
- package/src/helpers/base36.js +6 -0
- package/src/helpers/base62.js +5 -5
- package/src/helpers/nip01.js +32 -0
- package/src/helpers/nip19.js +5 -3
- package/src/helpers/stream.js +13 -0
- package/src/index.js +278 -21
- package/src/services/base93-decoder.js +107 -0
- package/src/services/base93-encoder.js +96 -0
- package/src/services/nostr-relays.js +63 -31
- package/src/services/nostr-signer.js +3 -2
- package/lib/GEMINI.md +0 -4
- package/lib/base122.js +0 -171
- package/src/services/base122-decoder.js +0 -56
- package/src/services/base122-encoder.js +0 -19
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
|
+
```
|
package/bin/nappup/helpers.js
CHANGED
|
@@ -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
|
|
package/bin/nappup/index.js
CHANGED
|
@@ -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
|
+
"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.
|
|
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
|
+
}
|
package/src/helpers/app.js
CHANGED
|
@@ -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
|
|
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
|
|
package/src/helpers/base36.js
CHANGED
|
@@ -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)
|
package/src/helpers/base62.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export const
|
|
2
|
-
const BASE = BigInt(
|
|
3
|
-
const LEADER =
|
|
4
|
-
const CHAR_MAP = new Map([...
|
|
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 =
|
|
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
|
+
}
|
package/src/helpers/nip19.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
|
|
2
|
-
import { bytesToBase62, base62ToBytes,
|
|
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
|
|
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
|
|
53
|
+
dTag,
|
|
52
54
|
pubkey: bytesToBase16(tlv[2][0]),
|
|
53
55
|
kind: kindByChannel[channel],
|
|
54
56
|
channel,
|
package/src/helpers/stream.js
CHANGED
|
@@ -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
|
+
}
|