nappup 1.0.5 → 1.0.7
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/bin/nappup/helpers.js +4 -4
- package/bin/nappup/index.js +2 -2
- package/package.json +3 -2
- package/src/helpers/app.js +36 -0
- package/src/helpers/base62.js +59 -0
- package/src/helpers/byte.js +15 -0
- package/src/helpers/nip19.js +90 -0
- package/src/index.js +17 -11
- package/src/services/nostr-signer.js +1 -16
- package/src/helpers/.gitkeep +0 -0
- package/src/services/.gitkeep +0 -0
package/bin/nappup/helpers.js
CHANGED
|
@@ -8,15 +8,15 @@ import { fileTypeFromFile } from 'file-type'
|
|
|
8
8
|
export function parseArgs (args) {
|
|
9
9
|
let dir = null
|
|
10
10
|
let sk = null
|
|
11
|
-
let
|
|
11
|
+
let dTag = null
|
|
12
12
|
let channel = null
|
|
13
13
|
|
|
14
14
|
for (let i = 0; i < args.length; i++) {
|
|
15
15
|
if (args[i] === '-s' && args[i + 1]) {
|
|
16
16
|
sk = args[i + 1]
|
|
17
17
|
i++ // Skip the next argument as it's part of -k
|
|
18
|
-
} else if (args[i] === '-
|
|
19
|
-
|
|
18
|
+
} else if (args[i] === '-d' && args[i + 1]) {
|
|
19
|
+
dTag = args[i + 1]
|
|
20
20
|
i++
|
|
21
21
|
} else if (args[i] === '--main' && channel === null) {
|
|
22
22
|
channel = 'main'
|
|
@@ -32,7 +32,7 @@ export function parseArgs (args) {
|
|
|
32
32
|
return {
|
|
33
33
|
dir: path.resolve(dir ?? '.'),
|
|
34
34
|
sk,
|
|
35
|
-
|
|
35
|
+
dTag,
|
|
36
36
|
channel: channel || 'main'
|
|
37
37
|
}
|
|
38
38
|
}
|
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,
|
|
14
|
+
const { dir, sk, dTag, channel } = 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),
|
|
17
|
+
await toApp(fileList, await NostrSigner.create(sk), { log: console.log.bind(console), dTag, channel })
|
package/package.json
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
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.7",
|
|
10
10
|
"description": "Nostr App Uploader",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node --test 'test/**/*.test.js'"
|
|
13
|
+
"test": "node --test 'test/**/*.test.js'",
|
|
14
|
+
"test:only": "node --test --test-only 'test/**/*.test.js'"
|
|
14
15
|
},
|
|
15
16
|
"bin": {
|
|
16
17
|
"nappup": "bin/nappup/index.js"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { bytesToBase62 } from '#helpers/base62.js'
|
|
2
|
+
|
|
3
|
+
export const NOSTR_APP_ID_MAX_LENGTH = 19
|
|
4
|
+
|
|
5
|
+
export function isNostrAppDTagSafe (string) {
|
|
6
|
+
return isSubdomainSafe(string) && string.length <= NOSTR_APP_ID_MAX_LENGTH
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isSubdomainSafe (string) {
|
|
10
|
+
return /(?:^[A-Za-z0-9]$)|(?:^(?!.*--)[A-Za-z0-9][A-Za-z0-9-]{0,63}[A-Za-z0-9]$)/.test(string)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function deriveNostrAppDTag (string) {
|
|
14
|
+
return toSubdomainSafe(string, NOSTR_APP_ID_MAX_LENGTH)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function toSubdomainSafe (string, maxStringLength) {
|
|
18
|
+
const byteLength = base62MaxLengthToMaxSourceByteLength(maxStringLength)
|
|
19
|
+
const bytes = (await toSha1(string)).slice(0, byteLength)
|
|
20
|
+
return bytesToBase62(bytes, maxStringLength)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function toSha1 (string) {
|
|
24
|
+
const bytes = new TextEncoder().encode(string)
|
|
25
|
+
return new Uint8Array(await crypto.subtle.digest('SHA-1', bytes))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// base62MaxLengthToMaxSourceByteLength(19) === 14 byte length
|
|
29
|
+
function base62MaxLengthToMaxSourceByteLength (maxStringLength) {
|
|
30
|
+
const log62 = Math.log(62)
|
|
31
|
+
const log256 = Math.log(256)
|
|
32
|
+
|
|
33
|
+
const maxByteLength = (maxStringLength * log62) / log256
|
|
34
|
+
|
|
35
|
+
return Math.floor(maxByteLength)
|
|
36
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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)]))
|
|
5
|
+
|
|
6
|
+
export function bytesToBase62 (bytes, padLength = 0) {
|
|
7
|
+
if (bytes.length === 0) return ''.padStart(padLength, LEADER)
|
|
8
|
+
|
|
9
|
+
let num = 0n
|
|
10
|
+
for (const byte of bytes) {
|
|
11
|
+
num = (num << 8n) + BigInt(byte)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let result = ''
|
|
15
|
+
if (num === 0n) return LEADER.padStart(padLength, LEADER)
|
|
16
|
+
|
|
17
|
+
while (num > 0n) {
|
|
18
|
+
const remainder = num % BASE
|
|
19
|
+
result = ALPHABET[Number(remainder)] + result
|
|
20
|
+
num = num / BASE
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const byte of bytes) {
|
|
24
|
+
if (byte !== 0) break
|
|
25
|
+
|
|
26
|
+
result = LEADER + result
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result.padStart(padLength, LEADER)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function base62ToBytes (base62Str) {
|
|
33
|
+
if (typeof base62Str !== 'string') { throw new Error('base62ToBytes requires a string argument') }
|
|
34
|
+
if (base62Str.length === 0) return new Uint8Array()
|
|
35
|
+
|
|
36
|
+
let leadingZeros = 0
|
|
37
|
+
for (let i = 0; i < base62Str.length; i++) {
|
|
38
|
+
if (base62Str[i] !== LEADER) break
|
|
39
|
+
|
|
40
|
+
leadingZeros++
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let num = 0n
|
|
44
|
+
for (const char of base62Str) {
|
|
45
|
+
const value = CHAR_MAP.get(char)
|
|
46
|
+
if (value === undefined) { throw new Error(`Invalid character in Base62 string: ${char}`) }
|
|
47
|
+
num = num * BASE + value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const bytes = []
|
|
51
|
+
while (num > 0n) {
|
|
52
|
+
bytes.unshift(Number(num & 0xffn))
|
|
53
|
+
num = num >> 8n
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = new Uint8Array(leadingZeros + bytes.length)
|
|
57
|
+
result.set(bytes, leadingZeros)
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function bytesToHex (uint8aBytes) {
|
|
2
|
+
return Array.from(uint8aBytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function hexToBytes (hexString) {
|
|
6
|
+
const arr = new Uint8Array(hexString.length / 2) // create result array
|
|
7
|
+
for (let i = 0; i < arr.length; i++) {
|
|
8
|
+
const j = i * 2
|
|
9
|
+
const h = hexString.slice(j, j + 2)
|
|
10
|
+
const b = Number.parseInt(h, 16) // byte, created from string part
|
|
11
|
+
if (Number.isNaN(b) || b < 0) throw new Error('invalid hex')
|
|
12
|
+
arr[i] = b
|
|
13
|
+
}
|
|
14
|
+
return arr
|
|
15
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { bytesToHex, hexToBytes } from '#helpers/byte.js'
|
|
2
|
+
import { bytesToBase62, base62ToBytes, ALPHABET as base62Alphabet } from '#helpers/base62.js'
|
|
3
|
+
import { isNostrAppDTagSafe } from '#helpers/app.js'
|
|
4
|
+
|
|
5
|
+
const MAX_SIZE = 5000
|
|
6
|
+
export const BASE62_ENTITY_REGEX = new RegExp(`^app-[${base62Alphabet}]{,${MAX_SIZE}}$`)
|
|
7
|
+
export const kindByChannel = {
|
|
8
|
+
main: 37448,
|
|
9
|
+
next: 37449,
|
|
10
|
+
draft: 37450
|
|
11
|
+
}
|
|
12
|
+
const channelEnum = Object.keys(kindByChannel)
|
|
13
|
+
const textEncoder = new TextEncoder()
|
|
14
|
+
const textDecoder = new TextDecoder()
|
|
15
|
+
|
|
16
|
+
export function appEncode (ref) {
|
|
17
|
+
if (!isNostrAppDTagSafe(ref['#d'])) { throw new Error('Invalid deduplication tag') }
|
|
18
|
+
const channelIndex = Object.entries(kindByChannel)
|
|
19
|
+
.findIndex(([k, v]) => ref.channel ? k === ref.channel : v === ref.kind)
|
|
20
|
+
if (channelIndex === -1) throw new Error('Wrong channel')
|
|
21
|
+
const tlv = toTlv([
|
|
22
|
+
[textEncoder.encode(ref['#d'])], // type 0 (the array index)
|
|
23
|
+
(ref.relays || []).map(url => textEncoder.encode(url)), // type 1
|
|
24
|
+
[hexToBytes(ref.pubkey)], // type 2
|
|
25
|
+
[uintToBytes(channelIndex)] // type 3
|
|
26
|
+
])
|
|
27
|
+
const base62 = bytesToBase62(tlv)
|
|
28
|
+
return `app-${base62}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function appDecode (entity) {
|
|
32
|
+
const [, base62] = entity.split('-')
|
|
33
|
+
const tlv = tlvToObj(base62ToBytes(base62))
|
|
34
|
+
if (!tlv[0]?.[0]) throw new Error('Missing deduplication tag')
|
|
35
|
+
if (!tlv[2]?.[0]) throw new Error('Missing author pubkey')
|
|
36
|
+
if (tlv[2][0].length !== 32) throw new Error('Author pubkey should be 32 bytes')
|
|
37
|
+
if (!tlv[3]?.[0]) throw new Error('Missing channel enum')
|
|
38
|
+
if (tlv[3][0].length !== 1) throw new Error('Channel enum should be 1 byte')
|
|
39
|
+
|
|
40
|
+
const channel = channelEnum[parseInt(tlv[3][0])]
|
|
41
|
+
return {
|
|
42
|
+
'#d': textDecoder.decode(tlv[0][0]),
|
|
43
|
+
pubkey: bytesToHex(tlv[2][0]),
|
|
44
|
+
kind: kindByChannel[channel],
|
|
45
|
+
channel,
|
|
46
|
+
relays: tlv[1] ? tlv[1].map(url => textDecoder.decode(url)) : []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Return shortest uint8Array size (not fixed size)
|
|
51
|
+
function uintToBytes (n, bytes = []) {
|
|
52
|
+
do { bytes.unshift(n & 255) } while ((n >>= 8) > 0)
|
|
53
|
+
return new Uint8Array(bytes)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toTlv (tlvConfig) {
|
|
57
|
+
const arrays = []
|
|
58
|
+
tlvConfig
|
|
59
|
+
.map((v, i) => [i, v])
|
|
60
|
+
.reverse() // if the first type is 0, entity always starts with the '0' char
|
|
61
|
+
.forEach(([type, values]) => {
|
|
62
|
+
// just non-empty values will be included
|
|
63
|
+
values.forEach(v => {
|
|
64
|
+
if (v.length > 255) throw new Error('Value is too big')
|
|
65
|
+
const array = new Uint8Array(v.length + 2)
|
|
66
|
+
array.set([type], 0) // t
|
|
67
|
+
array.set([v.length], 1) // l
|
|
68
|
+
array.set(v, 2) // v
|
|
69
|
+
arrays.push(array)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
return new Uint8Array(arrays.reduce((r, v) => [...r, ...v]))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// { 0: [t0v0], 1: [t1v0, t1v2], 3: [t3v0] }
|
|
76
|
+
function tlvToObj (tlv) {
|
|
77
|
+
const ret = {}
|
|
78
|
+
let rest = tlv
|
|
79
|
+
let t, l, v
|
|
80
|
+
while (rest.length > 0) {
|
|
81
|
+
t = rest[0]
|
|
82
|
+
l = rest[1]
|
|
83
|
+
v = rest.slice(2, 2 + l)
|
|
84
|
+
rest = rest.slice(2 + l)
|
|
85
|
+
if (v.length < l) throw new Error(`not enough data to read on TLV ${t}`)
|
|
86
|
+
ret[t] ??= []
|
|
87
|
+
ret[t].push(v)
|
|
88
|
+
}
|
|
89
|
+
return ret
|
|
90
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import NMMR from 'nmmr'
|
|
2
|
-
import {
|
|
2
|
+
import { appEncode } from '#helpers/nip19.js'
|
|
3
3
|
import Base122Encoder from '#services/base122-encoder.js'
|
|
4
4
|
import nostrRelays from '#services/nostr-relays.js'
|
|
5
5
|
import NostrSigner from '#services/nostr-signer.js'
|
|
6
6
|
import { streamToChunks } from '#helpers/stream.js'
|
|
7
|
+
import { isNostrAppDTagSafe, deriveNostrAppDTag } from '#helpers/app.js'
|
|
7
8
|
|
|
8
9
|
export default async function (...args) {
|
|
9
10
|
try {
|
|
@@ -12,15 +13,20 @@ export default async function (...args) {
|
|
|
12
13
|
await nostrRelays.disconnectAll()
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, channel = 'main' } = {}) {
|
|
16
18
|
if (!nostrSigner && typeof window !== 'undefined') nostrSigner = window.nostr
|
|
17
19
|
if (!nostrSigner) throw new Error('No Nostr signer found')
|
|
18
20
|
if (typeof window !== 'undefined' && nostrSigner === window.nostr) {
|
|
19
21
|
nostrSigner.getRelays = NostrSigner.prototype.getRelays
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
if (typeof dTag === 'string') {
|
|
25
|
+
if (!isNostrAppDTagSafe(dTag)) throw new Error('dTag should be [A-Za-z0-9] with length ranging from 1 to 19')
|
|
26
|
+
} else {
|
|
27
|
+
dTag = fileList[0].webkitRelativePath.split('/')[0].trim()
|
|
28
|
+
if (!isNostrAppDTagSafe(dTag)) dTag = deriveNostrAppDTag(dTag || Math.random().toString(36))
|
|
29
|
+
}
|
|
24
30
|
let nmmr
|
|
25
31
|
const fileMetadata = []
|
|
26
32
|
|
|
@@ -47,16 +53,16 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, appId, cha
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
log(`Uploading bundle #${
|
|
51
|
-
const bundle = await uploadBundle(
|
|
56
|
+
log(`Uploading bundle #${dTag}`)
|
|
57
|
+
const bundle = await uploadBundle(dTag, channel, fileMetadata, nostrSigner)
|
|
52
58
|
|
|
53
|
-
const
|
|
54
|
-
|
|
59
|
+
const appEntity = appEncode({
|
|
60
|
+
'#d': bundle.tags.find(v => v[0] === 'd')[1],
|
|
55
61
|
pubkey: bundle.pubkey,
|
|
56
62
|
relays: [],
|
|
57
63
|
kind: bundle.kind
|
|
58
64
|
})
|
|
59
|
-
log(`Visit at https://44billion.net/${
|
|
65
|
+
log(`Visit at https://44billion.net/${appEntity}`)
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
async function uploadBinaryDataChunks (nmmr, signer, { mimeType } = {}) {
|
|
@@ -110,7 +116,7 @@ async function getPreviousCtags (dTagValue, currentCtagValue, writeRelays, signe
|
|
|
110
116
|
})
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
async function uploadBundle (
|
|
119
|
+
async function uploadBundle (dTag, channel, fileMetadata, signer) {
|
|
114
120
|
const kind = {
|
|
115
121
|
main: 37448, // stable
|
|
116
122
|
next: 37449, // insider
|
|
@@ -119,7 +125,7 @@ async function uploadBundle (appId, channel, fileMetadata, signer) {
|
|
|
119
125
|
const appBundle = {
|
|
120
126
|
kind,
|
|
121
127
|
tags: [
|
|
122
|
-
['d',
|
|
128
|
+
['d', dTag],
|
|
123
129
|
...fileMetadata.map(v => ['file', v.rootHash, v.filename, v.mimeType])
|
|
124
130
|
],
|
|
125
131
|
content: '',
|
|
@@ -5,6 +5,7 @@ import * as dotenv from 'dotenv'
|
|
|
5
5
|
import { getPublicKey, finalizeEvent } from 'nostr-tools/pure'
|
|
6
6
|
import { getConversationKey, encrypt, decrypt } from 'nostr-tools/nip44'
|
|
7
7
|
import nostrRelays, { seedRelays, freeRelays } from '#services/nostr-relays.js'
|
|
8
|
+
import { bytesToHex, hexToBytes } from '#helpers/byte.js'
|
|
8
9
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
9
10
|
|
|
10
11
|
const dotenvPath = process.env.DOTENV_CONFIG_PATH ?? `${__dirname}/../../.env`
|
|
@@ -132,22 +133,6 @@ export default class NostrSigner {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
function bytesToHex (uint8aBytes) {
|
|
136
|
-
return Array.from(uint8aBytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function hexToBytes (hexString) {
|
|
140
|
-
const arr = new Uint8Array(hexString.length / 2) // create result array
|
|
141
|
-
for (let i = 0; i < arr.length; i++) {
|
|
142
|
-
const j = i * 2
|
|
143
|
-
const h = hexString.slice(j, j + 2)
|
|
144
|
-
const b = Number.parseInt(h, 16) // byte, created from string part
|
|
145
|
-
if (Number.isNaN(b) || b < 0) throw new Error('invalid hex')
|
|
146
|
-
arr[i] = b
|
|
147
|
-
}
|
|
148
|
-
return arr
|
|
149
|
-
}
|
|
150
|
-
|
|
151
136
|
function generateSecretKey () {
|
|
152
137
|
const randomBytes = crypto.getRandomValues(new Uint8Array(40))
|
|
153
138
|
const B256 = 2n ** 256n // secp256k1 is short weierstrass curve
|
package/src/helpers/.gitkeep
DELETED
|
File without changes
|
package/src/services/.gitkeep
DELETED
|
File without changes
|