nappup 1.0.7 → 1.0.8
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/package.json
CHANGED
|
@@ -6,12 +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.8",
|
|
10
10
|
"description": "Nostr App Uploader",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node --test '
|
|
14
|
-
"test:only": "node --test --test-only '
|
|
13
|
+
"test": "node --test 'tests/**/*.test.js'",
|
|
14
|
+
"test:only": "node --test --test-only 'tests/**/*.test.js'"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
17
|
"nappup": "bin/nappup/index.js"
|
package/src/helpers/app.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bytesToBase36 } from '#helpers/base36.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// 63 - (1<channel> + 5<b36loggeduserpkslug> 50<b36pk>)
|
|
4
|
+
// <b36loggeduserpkslug> pk chars at positions [7][17][27][37][47]
|
|
5
|
+
// to avoid vanity or pow colisions
|
|
6
|
+
export const NOSTR_APP_D_TAG_MAX_LENGTH = 7
|
|
4
7
|
|
|
5
8
|
export function isNostrAppDTagSafe (string) {
|
|
6
|
-
return isSubdomainSafe(string) && string.length <=
|
|
9
|
+
return isSubdomainSafe(string) && string.length <= NOSTR_APP_D_TAG_MAX_LENGTH
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
function isSubdomainSafe (string) {
|
|
10
|
-
return /(?:^[
|
|
13
|
+
return /(?:^[a-z0-9]$)|(?:^(?!.*--)[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$)/.test(string)
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export function deriveNostrAppDTag (string) {
|
|
14
|
-
return toSubdomainSafe(string,
|
|
17
|
+
return toSubdomainSafe(string, NOSTR_APP_D_TAG_MAX_LENGTH)
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
async function toSubdomainSafe (string, maxStringLength) {
|
|
18
|
-
const byteLength =
|
|
21
|
+
const byteLength = baseMaxLengthToMaxSourceByteLength(maxStringLength, 36)
|
|
19
22
|
const bytes = (await toSha1(string)).slice(0, byteLength)
|
|
20
|
-
return
|
|
23
|
+
return bytesToBase36(bytes, maxStringLength)
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
async function toSha1 (string) {
|
|
@@ -25,12 +28,14 @@ async function toSha1 (string) {
|
|
|
25
28
|
return new Uint8Array(await crypto.subtle.digest('SHA-1', bytes))
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
// baseMaxLengthToMaxSourceByteLength(19, 62) === 14 byte length
|
|
32
|
+
// baseMaxLengthToMaxSourceByteLength(7, 36) === 4 byte length
|
|
33
|
+
function baseMaxLengthToMaxSourceByteLength (maxStringLength, base) {
|
|
34
|
+
if (!base) throw new Error('Which base?')
|
|
35
|
+
const baseLog = Math.log(base)
|
|
31
36
|
const log256 = Math.log(256)
|
|
32
37
|
|
|
33
|
-
const maxByteLength = (maxStringLength *
|
|
38
|
+
const maxByteLength = (maxStringLength * baseLog) / log256
|
|
34
39
|
|
|
35
40
|
return Math.floor(maxByteLength)
|
|
36
41
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function bytesToBase16 (uint8aBytes) {
|
|
2
2
|
return Array.from(uint8aBytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
export function
|
|
6
|
-
|
|
5
|
+
export function base16ToBytes (base16String) {
|
|
6
|
+
if (base16String.length % 2 !== 0) throw new Error('invalid hex: odd length')
|
|
7
|
+
const arr = new Uint8Array(base16String.length / 2) // create result array
|
|
7
8
|
for (let i = 0; i < arr.length; i++) {
|
|
8
9
|
const j = i * 2
|
|
9
|
-
const h =
|
|
10
|
+
const h = base16String.slice(j, j + 2)
|
|
10
11
|
const b = Number.parseInt(h, 16) // byte, created from string part
|
|
11
12
|
if (Number.isNaN(b) || b < 0) throw new Error('invalid hex')
|
|
12
13
|
arr[i] = b
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { base16ToBytes, bytesToBase16 } from '#helpers/base16.js'
|
|
2
|
+
import { base62ToBytes, bytesToBase62 } from '#helpers/base62.js'
|
|
3
|
+
|
|
4
|
+
export const BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
|
|
5
|
+
const BASE = BigInt(BASE36_ALPHABET.length)
|
|
6
|
+
const LEADER = BASE36_ALPHABET[0]
|
|
7
|
+
const CHAR_MAP = new Map([...BASE36_ALPHABET].map((char, index) => [char, BigInt(index)]))
|
|
8
|
+
|
|
9
|
+
export function bytesToBase36 (bytes, padLength = 0) {
|
|
10
|
+
return base16ToBase36(bytesToBase16(bytes), padLength)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function base16ToBase36 (hex, padLength = 0) {
|
|
14
|
+
return bigIntToBase36(base16ToBigInt(hex), padLength)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function base62ToBase36 (base62str, padLength = 0) {
|
|
18
|
+
return bytesToBase36(base62ToBytes(base62str), padLength)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function base36ToBytes (base36str) {
|
|
22
|
+
return base16ToBytes(base36ToBase16(base36str))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function base36ToBase16 (base36str) {
|
|
26
|
+
return bigIntToBase16(base36ToBigInt(base36str))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function base36ToBase62 (base36str, padLength = 0) {
|
|
30
|
+
return bytesToBase62(base36ToBytes(base36str), padLength)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function base36ToBigInt (base36str) {
|
|
34
|
+
if (typeof base36str !== 'string') {
|
|
35
|
+
throw new Error('Input must be a string.')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let result = 0n
|
|
39
|
+
for (const char of base36str) {
|
|
40
|
+
const value = CHAR_MAP.get(char)
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
throw new Error(`Invalid character in Base36 string: ${char}`)
|
|
43
|
+
}
|
|
44
|
+
result = result * BASE + value
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bigIntToBase36 (num, padLength) {
|
|
50
|
+
if (typeof num !== 'bigint') throw new Error('Input must be a BigInt.')
|
|
51
|
+
if (num < 0n) throw new Error('Can\'t be signed BigInt')
|
|
52
|
+
|
|
53
|
+
return num.toString(36).padStart(padLength, LEADER)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bigIntToBase16 (num) {
|
|
57
|
+
if (typeof num !== 'bigint') throw new Error('Input must be a BigInt.')
|
|
58
|
+
if (num < 0n) throw new Error('Can\'t be signed BigInt')
|
|
59
|
+
|
|
60
|
+
let hexString = num.toString(16)
|
|
61
|
+
if (hexString.length % 2 !== 0) hexString = `0${hexString}`
|
|
62
|
+
return hexString
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function base16ToBigInt (hex) {
|
|
66
|
+
if (typeof hex !== 'string') throw new Error('Input must be a string.')
|
|
67
|
+
return BigInt(`0x${hex}`)
|
|
68
|
+
}
|
package/src/helpers/nip19.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
|
|
2
2
|
import { bytesToBase62, base62ToBytes, ALPHABET as base62Alphabet } from '#helpers/base62.js'
|
|
3
3
|
import { isNostrAppDTagSafe } from '#helpers/app.js'
|
|
4
4
|
|
|
@@ -14,14 +14,14 @@ const textEncoder = new TextEncoder()
|
|
|
14
14
|
const textDecoder = new TextDecoder()
|
|
15
15
|
|
|
16
16
|
export function appEncode (ref) {
|
|
17
|
-
if (!isNostrAppDTagSafe(ref
|
|
17
|
+
if (!isNostrAppDTagSafe(ref.dTag)) { throw new Error('Invalid deduplication tag') }
|
|
18
18
|
const channelIndex = Object.entries(kindByChannel)
|
|
19
19
|
.findIndex(([k, v]) => ref.channel ? k === ref.channel : v === ref.kind)
|
|
20
20
|
if (channelIndex === -1) throw new Error('Wrong channel')
|
|
21
21
|
const tlv = toTlv([
|
|
22
|
-
[textEncoder.encode(ref
|
|
22
|
+
[textEncoder.encode(ref.dTag)], // type 0 (the array index)
|
|
23
23
|
(ref.relays || []).map(url => textEncoder.encode(url)), // type 1
|
|
24
|
-
[
|
|
24
|
+
[base16ToBytes(ref.pubkey)], // type 2
|
|
25
25
|
[uintToBytes(channelIndex)] // type 3
|
|
26
26
|
])
|
|
27
27
|
const base62 = bytesToBase62(tlv)
|
|
@@ -39,8 +39,8 @@ export function appDecode (entity) {
|
|
|
39
39
|
|
|
40
40
|
const channel = channelEnum[parseInt(tlv[3][0])]
|
|
41
41
|
return {
|
|
42
|
-
|
|
43
|
-
pubkey:
|
|
42
|
+
dTag: textDecoder.decode(tlv[0][0]),
|
|
43
|
+
pubkey: bytesToBase16(tlv[2][0]),
|
|
44
44
|
kind: kindByChannel[channel],
|
|
45
45
|
channel,
|
|
46
46
|
relays: tlv[1] ? tlv[1].map(url => textDecoder.decode(url)) : []
|
package/src/index.js
CHANGED
|
@@ -57,7 +57,7 @@ export async function toApp (fileList, nostrSigner, { log = () => {}, dTag, chan
|
|
|
57
57
|
const bundle = await uploadBundle(dTag, channel, fileMetadata, nostrSigner)
|
|
58
58
|
|
|
59
59
|
const appEntity = appEncode({
|
|
60
|
-
|
|
60
|
+
dTag: bundle.tags.find(v => v[0] === 'd')[1],
|
|
61
61
|
pubkey: bundle.pubkey,
|
|
62
62
|
relays: [],
|
|
63
63
|
kind: bundle.kind
|
|
@@ -43,7 +43,8 @@ export class NostrRelays {
|
|
|
43
43
|
// Disconnect from a relay.
|
|
44
44
|
async disconnect (url) {
|
|
45
45
|
if (this.#relays.has(url)) {
|
|
46
|
-
|
|
46
|
+
const relay = this.#relays.get(url)
|
|
47
|
+
if (relay.ws.readyState < 2) await relay.close().catch(console.log)
|
|
47
48
|
this.#relays.delete(url)
|
|
48
49
|
clearTimeout(this.#relayTimeouts.get(url))
|
|
49
50
|
this.#relayTimeouts.delete(url)
|
|
@@ -5,7 +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 {
|
|
8
|
+
import { bytesToBase16, base16ToBytes } from '#helpers/base16.js'
|
|
9
9
|
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
10
10
|
|
|
11
11
|
const dotenvPath = process.env.DOTENV_CONFIG_PATH ?? `${__dirname}/../../.env`
|
|
@@ -34,17 +34,17 @@ export default class NostrSigner {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
static async create (skHex) {
|
|
37
|
-
if (skHex) return new this(createToken,
|
|
37
|
+
if (skHex) return new this(createToken, base16ToBytes(skHex))
|
|
38
38
|
|
|
39
39
|
let skBytes
|
|
40
40
|
let isNewSk = false
|
|
41
41
|
if (process.env.NOSTR_SECRET_KEY) {
|
|
42
|
-
skBytes =
|
|
42
|
+
skBytes = base16ToBytes(process.env.NOSTR_SECRET_KEY)
|
|
43
43
|
} else {
|
|
44
44
|
isNewSk = true
|
|
45
45
|
skHex = generateSecretKey()
|
|
46
46
|
fs.appendFileSync(path.resolve(dotenvPath), `NOSTR_SECRET_KEY=${skHex}\n`)
|
|
47
|
-
skBytes =
|
|
47
|
+
skBytes = base16ToBytes(skHex)
|
|
48
48
|
}
|
|
49
49
|
const ret = new this(createToken, skBytes)
|
|
50
50
|
if (isNewSk) await ret.#initSk(skHex)
|
|
@@ -137,7 +137,7 @@ function generateSecretKey () {
|
|
|
137
137
|
const randomBytes = crypto.getRandomValues(new Uint8Array(40))
|
|
138
138
|
const B256 = 2n ** 256n // secp256k1 is short weierstrass curve
|
|
139
139
|
const N = B256 - 0x14551231950b75fc4402da1732fc9bebfn // curve (group) order
|
|
140
|
-
const bytesToNumber = b => BigInt('0x' + (
|
|
140
|
+
const bytesToNumber = b => BigInt('0x' + (bytesToBase16(b) || '0'))
|
|
141
141
|
const mod = (a, b) => { const r = a % b; return r >= 0n ? r : b + r } // mod division
|
|
142
142
|
const num = mod(bytesToNumber(randomBytes), N - 1n) + 1n // takes at least n+8 bytes
|
|
143
143
|
return num.toString(16).padStart(64, '0')
|