libp2r2p 0.2.0 → 0.3.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 +13 -0
- package/base16/index.js +6 -1
- package/base36/index.js +114 -0
- package/base62/index.js +127 -0
- package/base64/index.js +3 -0
- package/index.js +2 -0
- package/nip19/index.js +1 -46
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -174,3 +174,16 @@ clearing is asynchronous too: `await messenger.clearChannel(channelPubkey)`.
|
|
|
174
174
|
Use explicit subpath imports for bundle size. The package root re-exports the
|
|
175
175
|
main messenger API for convenience, but applications that only need one piece
|
|
176
176
|
should import that subpath directly.
|
|
177
|
+
|
|
178
|
+
## Binary encodings
|
|
179
|
+
|
|
180
|
+
Base16, Base36, Base62, Base64/Base64URL, and Base93 helpers are available
|
|
181
|
+
through their matching `libp2r2p/<encoding>` subpaths. Base36 exposes both a
|
|
182
|
+
binary-safe variable-width codec and the canonical 32-byte/50-character
|
|
183
|
+
NsiteBase36 representation from NIP-5A. Base62 uses the same case-sensitive
|
|
184
|
+
alphabet as app NIP-19 entities; its default byte mode preserves leading zero
|
|
185
|
+
bytes, while integer mode supports fixed-width identifiers.
|
|
186
|
+
|
|
187
|
+
In NIP-5A, "no padding" means that no separate padding character such as `=`
|
|
188
|
+
is used. Leading `0` digits are nevertheless required to make every Nsite
|
|
189
|
+
Base36 value exactly 50 characters long.
|
package/base16/index.js
CHANGED
|
@@ -5,8 +5,13 @@ export function bytesToBase16 (bytes) {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function base16ToBytes (base16) {
|
|
8
|
+
if (typeof base16 !== 'string') throw new TypeError('Base16 value should be a string')
|
|
9
|
+
if (base16.length % 2 !== 0) throw new Error('Invalid Base16 length')
|
|
10
|
+
if (!/^[0-9a-f]*$/i.test(base16)) throw new Error('Invalid Base16 character')
|
|
8
11
|
const out = new Uint8Array(base16.length / 2)
|
|
9
|
-
for (let i = 0; i < out.length; i++)
|
|
12
|
+
for (let i = 0; i < out.length; i++) {
|
|
13
|
+
out[i] = Number.parseInt(base16.slice(i * 2, i * 2 + 2), 16)
|
|
14
|
+
}
|
|
10
15
|
return out
|
|
11
16
|
}
|
|
12
17
|
|
package/base36/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { base16ToBytes, bytesToBase16 } from '../base16/index.js'
|
|
2
|
+
|
|
3
|
+
export const BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
|
|
4
|
+
|
|
5
|
+
const BASE = BigInt(BASE36_ALPHABET.length)
|
|
6
|
+
const LEADER = BASE36_ALPHABET[0]
|
|
7
|
+
const CHAR_MAP = new Map(
|
|
8
|
+
[...BASE36_ALPHABET].map((character, index) => [character, BigInt(index)])
|
|
9
|
+
)
|
|
10
|
+
const NSITE_BYTE_LENGTH = 32
|
|
11
|
+
const NSITE_TEXT_LENGTH = 50
|
|
12
|
+
const MAX_NSITE_VALUE = (1n << 256n) - 1n
|
|
13
|
+
|
|
14
|
+
function bytesToInteger (bytes) {
|
|
15
|
+
let number = 0n
|
|
16
|
+
for (const byte of bytes) number = (number << 8n) + BigInt(byte)
|
|
17
|
+
return number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function integerToBase36 (number) {
|
|
21
|
+
let result = ''
|
|
22
|
+
while (number > 0n) {
|
|
23
|
+
result = BASE36_ALPHABET[Number(number % BASE)] + result
|
|
24
|
+
number /= BASE
|
|
25
|
+
}
|
|
26
|
+
return result || LEADER
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseBase36Integer (value) {
|
|
30
|
+
let number = 0n
|
|
31
|
+
for (const character of value) {
|
|
32
|
+
const digit = CHAR_MAP.get(character)
|
|
33
|
+
if (digit === undefined) throw new Error('Invalid Base36 character: ' + character)
|
|
34
|
+
number = number * BASE + digit
|
|
35
|
+
}
|
|
36
|
+
return number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function integerToMinimalBytes (number) {
|
|
40
|
+
if (number === 0n) return new Uint8Array([0])
|
|
41
|
+
const bytes = []
|
|
42
|
+
while (number > 0n) {
|
|
43
|
+
bytes.unshift(Number(number & 0xffn))
|
|
44
|
+
number >>= 8n
|
|
45
|
+
}
|
|
46
|
+
return Uint8Array.from(bytes)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A binary-safe Base36 codec. Every leading zero byte is represented by a
|
|
50
|
+
// leading zero character, making arbitrary byte arrays round-trip exactly.
|
|
51
|
+
export function bytesToBase36 (bytes) {
|
|
52
|
+
if (bytes.length === 0) return ''
|
|
53
|
+
|
|
54
|
+
const number = bytesToInteger(bytes)
|
|
55
|
+
let result = number === 0n ? '' : integerToBase36(number)
|
|
56
|
+
let leadingZeros = 0
|
|
57
|
+
while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) leadingZeros++
|
|
58
|
+
result = LEADER.repeat(leadingZeros) + result
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function base36ToBytes (value) {
|
|
63
|
+
if (typeof value !== 'string') throw new TypeError('Base36 value should be a string')
|
|
64
|
+
if (value.length === 0) return new Uint8Array()
|
|
65
|
+
|
|
66
|
+
let leadingZeros = 0
|
|
67
|
+
while (value[leadingZeros] === LEADER) leadingZeros++
|
|
68
|
+
const number = parseBase36Integer(value)
|
|
69
|
+
const suffix = number === 0n ? new Uint8Array() : integerToMinimalBytes(number)
|
|
70
|
+
const result = new Uint8Array(leadingZeros + suffix.length)
|
|
71
|
+
result.set(suffix, leadingZeros)
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function base16ToBase36 (value) {
|
|
76
|
+
return bytesToBase36(base16ToBytes(value))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function base36ToBase16 (value) {
|
|
80
|
+
return bytesToBase16(base36ToBytes(value))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// NIP-5A treats a raw 32-byte value as one unsigned integer and represents it
|
|
84
|
+
// as exactly 50 lowercase Base36 digits. Its leading zeros are numeric width,
|
|
85
|
+
// not zero-byte markers as they are in the binary-safe codec above.
|
|
86
|
+
export function bytesToNsiteBase36 (bytes) {
|
|
87
|
+
if (bytes.length !== NSITE_BYTE_LENGTH) {
|
|
88
|
+
throw new Error('Nsite Base36 input should be ' + NSITE_BYTE_LENGTH + ' bytes')
|
|
89
|
+
}
|
|
90
|
+
return integerToBase36(bytesToInteger(bytes)).padStart(NSITE_TEXT_LENGTH, LEADER)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function nsiteBase36ToBytes (value) {
|
|
94
|
+
if (typeof value !== 'string') throw new TypeError('Nsite Base36 value should be a string')
|
|
95
|
+
if (value.length !== NSITE_TEXT_LENGTH) {
|
|
96
|
+
throw new Error('Nsite Base36 value should be ' + NSITE_TEXT_LENGTH + ' characters')
|
|
97
|
+
}
|
|
98
|
+
if (!/^[0-9a-z]+$/.test(value)) throw new Error('Invalid Nsite Base36 character')
|
|
99
|
+
|
|
100
|
+
const number = parseBase36Integer(value)
|
|
101
|
+
if (number > MAX_NSITE_VALUE) throw new Error('Nsite Base36 value exceeds 32 bytes')
|
|
102
|
+
const bytes = integerToMinimalBytes(number)
|
|
103
|
+
const result = new Uint8Array(NSITE_BYTE_LENGTH)
|
|
104
|
+
if (number !== 0n) result.set(bytes, NSITE_BYTE_LENGTH - bytes.length)
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function base16ToNsiteBase36 (value) {
|
|
109
|
+
return bytesToNsiteBase36(base16ToBytes(value))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function nsiteBase36ToBase16 (value) {
|
|
113
|
+
return bytesToBase16(nsiteBase36ToBytes(value))
|
|
114
|
+
}
|
package/base62/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { base16ToBytes, bytesToBase16 } from '../base16/index.js'
|
|
2
|
+
|
|
3
|
+
export const BASE62_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
4
|
+
|
|
5
|
+
const BASE = BigInt(BASE62_ALPHABET.length)
|
|
6
|
+
const LEADER = BASE62_ALPHABET[0]
|
|
7
|
+
const CHAR_MAP = new Map(
|
|
8
|
+
[...BASE62_ALPHABET].map((character, index) => [character, BigInt(index)])
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
function readOptions (options, allowedKeys) {
|
|
12
|
+
if (options === undefined) return {}
|
|
13
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
14
|
+
throw new TypeError('Base62 options should be an object')
|
|
15
|
+
}
|
|
16
|
+
for (const key of Object.keys(options)) {
|
|
17
|
+
if (!allowedKeys.includes(key)) throw new Error('Unknown Base62 option: ' + key)
|
|
18
|
+
}
|
|
19
|
+
return options
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readMode (mode = 'bytes') {
|
|
23
|
+
if (mode !== 'bytes' && mode !== 'integer') throw new Error('Invalid Base62 mode: ' + mode)
|
|
24
|
+
return mode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readLength (value, name, defaultValue, minimum = 0) {
|
|
28
|
+
if (value === undefined) return defaultValue
|
|
29
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
30
|
+
const qualifier = minimum === 0 ? 'non-negative' : 'positive'
|
|
31
|
+
throw new RangeError('Base62 ' + name + ' should be a ' + qualifier + ' safe integer')
|
|
32
|
+
}
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bytesToInteger (bytes) {
|
|
37
|
+
let number = 0n
|
|
38
|
+
for (const byte of bytes) number = (number << 8n) + BigInt(byte)
|
|
39
|
+
return number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function integerToBase62 (number) {
|
|
43
|
+
let result = ''
|
|
44
|
+
while (number > 0n) {
|
|
45
|
+
result = BASE62_ALPHABET[Number(number % BASE)] + result
|
|
46
|
+
number /= BASE
|
|
47
|
+
}
|
|
48
|
+
return result || LEADER
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseBase62Integer (value) {
|
|
52
|
+
let number = 0n
|
|
53
|
+
for (const character of value) {
|
|
54
|
+
const digit = CHAR_MAP.get(character)
|
|
55
|
+
if (digit === undefined) throw new Error('Invalid Base62 character: ' + character)
|
|
56
|
+
number = number * BASE + digit
|
|
57
|
+
}
|
|
58
|
+
return number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function integerToMinimalBytes (number) {
|
|
62
|
+
if (number === 0n) return new Uint8Array([0])
|
|
63
|
+
const bytes = []
|
|
64
|
+
while (number > 0n) {
|
|
65
|
+
bytes.unshift(Number(number & 0xffn))
|
|
66
|
+
number >>= 8n
|
|
67
|
+
}
|
|
68
|
+
return Uint8Array.from(bytes)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Byte mode preserves every leading zero byte. Integer mode treats the input
|
|
72
|
+
// as an unsigned big-endian value and supports fixed-width textual output.
|
|
73
|
+
export function bytesToBase62 (bytes, options) {
|
|
74
|
+
const { mode: rawMode, minLength: rawMinLength } = readOptions(options, ['mode', 'minLength'])
|
|
75
|
+
const mode = readMode(rawMode)
|
|
76
|
+
if (mode === 'bytes' && rawMinLength !== undefined) {
|
|
77
|
+
throw new Error('Base62 minLength requires integer mode')
|
|
78
|
+
}
|
|
79
|
+
if (mode === 'integer') {
|
|
80
|
+
if (bytes.length === 0) throw new Error('Base62 integer input should not be empty')
|
|
81
|
+
const minLength = readLength(rawMinLength, 'minLength', 0)
|
|
82
|
+
return integerToBase62(bytesToInteger(bytes)).padStart(minLength, LEADER)
|
|
83
|
+
}
|
|
84
|
+
if (bytes.length === 0) return ''
|
|
85
|
+
|
|
86
|
+
const number = bytesToInteger(bytes)
|
|
87
|
+
let result = number === 0n ? '' : integerToBase62(number)
|
|
88
|
+
let leadingZeros = 0
|
|
89
|
+
while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) leadingZeros++
|
|
90
|
+
result = LEADER.repeat(leadingZeros) + result
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function base62ToBytes (value, options) {
|
|
95
|
+
if (typeof value !== 'string') throw new TypeError('Base62 value should be a string')
|
|
96
|
+
const { mode: rawMode, byteLength: rawByteLength } = readOptions(options, ['mode', 'byteLength'])
|
|
97
|
+
const mode = readMode(rawMode)
|
|
98
|
+
if (mode === 'bytes' && rawByteLength !== undefined) {
|
|
99
|
+
throw new Error('Base62 byteLength requires integer mode')
|
|
100
|
+
}
|
|
101
|
+
if (mode === 'integer') {
|
|
102
|
+
if (value.length === 0) throw new Error('Base62 integer value should not be empty')
|
|
103
|
+
const byteLength = readLength(rawByteLength, 'byteLength', undefined, 1)
|
|
104
|
+
const bytes = integerToMinimalBytes(parseBase62Integer(value))
|
|
105
|
+
if (byteLength === undefined) return bytes
|
|
106
|
+
if (bytes.length > byteLength) throw new Error('Base62 integer exceeds ' + byteLength + ' bytes')
|
|
107
|
+
const result = new Uint8Array(byteLength)
|
|
108
|
+
result.set(bytes, byteLength - bytes.length)
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let leadingZeros = 0
|
|
113
|
+
while (value[leadingZeros] === LEADER) leadingZeros++
|
|
114
|
+
const number = parseBase62Integer(value)
|
|
115
|
+
const suffix = number === 0n ? new Uint8Array() : integerToMinimalBytes(number)
|
|
116
|
+
const result = new Uint8Array(leadingZeros + suffix.length)
|
|
117
|
+
result.set(suffix, leadingZeros)
|
|
118
|
+
return result
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function base16ToBase62 (value, options) {
|
|
122
|
+
return bytesToBase62(base16ToBytes(value), options)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function base62ToBase16 (value, options) {
|
|
126
|
+
return bytesToBase16(base62ToBytes(value, options))
|
|
127
|
+
}
|
package/base64/index.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// base64 is useful for WebAuthn credential IDs and URL/query-string material.
|
|
4
4
|
|
|
5
5
|
export function bytesToBase64 (bytes) {
|
|
6
|
+
if (typeof Buffer === 'function' && typeof Buffer.from === 'function') {
|
|
7
|
+
return Buffer.from(bytes).toString('base64')
|
|
8
|
+
}
|
|
6
9
|
let s = ''
|
|
7
10
|
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
|
|
8
11
|
return btoa(s)
|
package/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './private-messenger/index.js'
|
|
2
2
|
|
|
3
3
|
export * as base16 from './base16/index.js'
|
|
4
|
+
export * as base36 from './base36/index.js'
|
|
5
|
+
export * as base62 from './base62/index.js'
|
|
4
6
|
export * as base64 from './base64/index.js'
|
|
5
7
|
export * as contentKey from './content-key/index.js'
|
|
6
8
|
export * as contentKeyEvent from './content-key/event/index.js'
|
package/nip19/index.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { bech32 } from '@scure/base'
|
|
2
|
+
import { BASE62_ALPHABET, base62ToBytes, bytesToBase62 } from '../base62/index.js'
|
|
2
3
|
|
|
3
4
|
const MAX_ENTITY_SIZE = 5000
|
|
4
5
|
const MAX_TLV_VALUE_SIZE = 255
|
|
5
6
|
const NOSTR_APP_D_TAG_MAX_LENGTH = 260
|
|
6
|
-
const BASE62_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
7
|
-
const BASE62_BASE = BigInt(BASE62_ALPHABET.length)
|
|
8
|
-
const BASE62_LEADER = BASE62_ALPHABET[0]
|
|
9
|
-
const BASE62_CHAR_MAP = new Map(
|
|
10
|
-
[...BASE62_ALPHABET].map((character, index) => [character, BigInt(index)])
|
|
11
|
-
)
|
|
12
|
-
|
|
13
7
|
export const NAPP_ENTITY_REGEX = new RegExp(
|
|
14
8
|
`^\\+{1,3}[${BASE62_ALPHABET}]{48,${MAX_ENTITY_SIZE}}$`
|
|
15
9
|
)
|
|
@@ -178,45 +172,6 @@ export function nfileDecode (entity) {
|
|
|
178
172
|
return result
|
|
179
173
|
}
|
|
180
174
|
|
|
181
|
-
function bytesToBase62 (bytes) {
|
|
182
|
-
if (bytes.length === 0) return ''
|
|
183
|
-
let number = 0n
|
|
184
|
-
for (const byte of bytes) number = (number << 8n) + BigInt(byte)
|
|
185
|
-
|
|
186
|
-
let result = ''
|
|
187
|
-
if (number === 0n) return BASE62_LEADER
|
|
188
|
-
while (number > 0n) {
|
|
189
|
-
result = BASE62_ALPHABET[Number(number % BASE62_BASE)] + result
|
|
190
|
-
number /= BASE62_BASE
|
|
191
|
-
}
|
|
192
|
-
for (const byte of bytes) {
|
|
193
|
-
if (byte !== 0) break
|
|
194
|
-
result = BASE62_LEADER + result
|
|
195
|
-
}
|
|
196
|
-
return result
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function base62ToBytes (value) {
|
|
200
|
-
if (typeof value !== 'string') throw new TypeError('Base62 value should be a string')
|
|
201
|
-
let leadingZeros = 0
|
|
202
|
-
while (value[leadingZeros] === BASE62_LEADER) leadingZeros++
|
|
203
|
-
|
|
204
|
-
let number = 0n
|
|
205
|
-
for (const character of value) {
|
|
206
|
-
const digit = BASE62_CHAR_MAP.get(character)
|
|
207
|
-
if (digit === undefined) throw new Error(`Invalid Base62 character: ${character}`)
|
|
208
|
-
number = number * BASE62_BASE + digit
|
|
209
|
-
}
|
|
210
|
-
const suffix = []
|
|
211
|
-
while (number > 0n) {
|
|
212
|
-
suffix.unshift(Number(number & 0xffn))
|
|
213
|
-
number >>= 8n
|
|
214
|
-
}
|
|
215
|
-
const result = new Uint8Array(leadingZeros + suffix.length)
|
|
216
|
-
result.set(suffix, leadingZeros)
|
|
217
|
-
return result
|
|
218
|
-
}
|
|
219
|
-
|
|
220
175
|
function isNostrAppDTagSafe (value) {
|
|
221
176
|
return typeof value === 'string' && value.length <= NOSTR_APP_D_TAG_MAX_LENGTH
|
|
222
177
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libp2r2p",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Peer-to-relay-to-peer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"p2r2p",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"files": [
|
|
15
15
|
"base16",
|
|
16
|
+
"base36",
|
|
17
|
+
"base62",
|
|
16
18
|
"base64",
|
|
17
19
|
"base93",
|
|
18
20
|
"content-key",
|
|
@@ -40,6 +42,8 @@
|
|
|
40
42
|
"exports": {
|
|
41
43
|
".": "./index.js",
|
|
42
44
|
"./base16": "./base16/index.js",
|
|
45
|
+
"./base36": "./base36/index.js",
|
|
46
|
+
"./base62": "./base62/index.js",
|
|
43
47
|
"./base64": "./base64/index.js",
|
|
44
48
|
"./base93": "./base93/index.js",
|
|
45
49
|
"./content-key": "./content-key/index.js",
|