libp2r2p 0.1.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 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++) out[i] = parseInt(base16.slice(i * 2, i * 2 + 2), 16)
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
 
@@ -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
+ }
@@ -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'
@@ -10,6 +12,7 @@ export * as idb from './idb/index.js'
10
12
  export * as idbQueue from './idb-queue/index.js'
11
13
  export * as key from './key/index.js'
12
14
  export * as network from './network/index.js'
15
+ export * as nip19 from './nip19/index.js'
13
16
  export * as nip44v3 from './nip44-v3/index.js'
14
17
  export * as nip46 from './nip46/index.js'
15
18
  export * as privateChannel from './private-channel/index.js'
package/nip19/index.js ADDED
@@ -0,0 +1,260 @@
1
+ import { bech32 } from '@scure/base'
2
+ import { BASE62_ALPHABET, base62ToBytes, bytesToBase62 } from '../base62/index.js'
3
+
4
+ const MAX_ENTITY_SIZE = 5000
5
+ const MAX_TLV_VALUE_SIZE = 255
6
+ const NOSTR_APP_D_TAG_MAX_LENGTH = 260
7
+ export const NAPP_ENTITY_REGEX = new RegExp(
8
+ `^\\+{1,3}[${BASE62_ALPHABET}]{48,${MAX_ENTITY_SIZE}}$`
9
+ )
10
+
11
+ const textEncoder = new TextEncoder()
12
+ const textDecoder = new TextDecoder()
13
+ const fatalTextDecoder = new TextDecoder('utf-8', { fatal: true })
14
+
15
+ const kindByChannel = {
16
+ main: 35128,
17
+ next: 35129,
18
+ draft: 35130
19
+ }
20
+ const channelByKind = Object.fromEntries(
21
+ Object.entries(kindByChannel).map(([channel, kind]) => [kind, channel])
22
+ )
23
+ const prefixByChannel = {
24
+ main: '+',
25
+ next: '++',
26
+ draft: '+++'
27
+ }
28
+ const channelByPrefix = Object.fromEntries(
29
+ Object.entries(prefixByChannel).map(([channel, prefix]) => [prefix, channel])
30
+ )
31
+
32
+ function bytesToHex (bytes) {
33
+ let result = ''
34
+ for (const byte of bytes) result += byte.toString(16).padStart(2, '0')
35
+ return result
36
+ }
37
+
38
+ function hexToBytes (hex, fieldName = 'hex value') {
39
+ if (typeof hex !== 'string' || !/^[0-9a-f]+$/i.test(hex) || hex.length % 2 !== 0) {
40
+ throw new Error(`Invalid ${fieldName}`)
41
+ }
42
+ const result = new Uint8Array(hex.length / 2)
43
+ for (let index = 0; index < result.length; index++) {
44
+ result[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16)
45
+ }
46
+ return result
47
+ }
48
+
49
+ function fixedHexToBytes (hex, byteLength, fieldName) {
50
+ const bytes = hexToBytes(hex, fieldName)
51
+ if (bytes.length !== byteLength) throw new Error(`${fieldName} should be ${byteLength} bytes`)
52
+ return bytes
53
+ }
54
+
55
+ function concatBytes (arrays) {
56
+ const size = arrays.reduce((total, array) => total + array.length, 0)
57
+ const result = new Uint8Array(size)
58
+ let offset = 0
59
+ for (const array of arrays) {
60
+ result.set(array, offset)
61
+ offset += array.length
62
+ }
63
+ return result
64
+ }
65
+
66
+ function encodeText (value, fieldName) {
67
+ if (typeof value !== 'string' || value.length === 0) {
68
+ throw new Error(`${fieldName} should be a non-empty string`)
69
+ }
70
+ const bytes = textEncoder.encode(value)
71
+ if (bytes.length > MAX_TLV_VALUE_SIZE) throw new Error(`${fieldName} is too big`)
72
+ // TextEncoder replaces lone surrogates. Reject them instead of silently changing
73
+ // the signed/canonical entity value.
74
+ if (fatalTextDecoder.decode(bytes) !== value) throw new Error(`Invalid ${fieldName} UTF-8`)
75
+ return bytes
76
+ }
77
+
78
+ function decodeText (bytes, fieldName) {
79
+ if (bytes.length === 0 || bytes.length > MAX_TLV_VALUE_SIZE) {
80
+ throw new Error(`Invalid ${fieldName} length`)
81
+ }
82
+ try {
83
+ return fatalTextDecoder.decode(bytes)
84
+ } catch {
85
+ throw new Error(`Invalid ${fieldName} UTF-8`)
86
+ }
87
+ }
88
+
89
+ function encodeTlvEntries (entries) {
90
+ return concatBytes(entries.map(([type, value]) => {
91
+ if (!Number.isInteger(type) || type < 0 || type > 255) throw new Error('Invalid TLV type')
92
+ if (!(value instanceof Uint8Array)) throw new TypeError('TLV value should be a Uint8Array')
93
+ if (value.length > MAX_TLV_VALUE_SIZE) throw new Error('TLV value is too big')
94
+ const entry = new Uint8Array(value.length + 2)
95
+ entry[0] = type
96
+ entry[1] = value.length
97
+ entry.set(value, 2)
98
+ return entry
99
+ }))
100
+ }
101
+
102
+ function decodeTlvEntries (bytes) {
103
+ const entries = []
104
+ let offset = 0
105
+ while (offset < bytes.length) {
106
+ if (bytes.length - offset < 2) throw new Error('Truncated TLV header')
107
+ const type = bytes[offset]
108
+ const length = bytes[offset + 1]
109
+ offset += 2
110
+ if (bytes.length - offset < length) throw new Error(`Truncated TLV value for type ${type}`)
111
+ entries.push([type, bytes.slice(offset, offset + length)])
112
+ offset += length
113
+ }
114
+ return entries
115
+ }
116
+
117
+ function onlyValue (values, fieldName) {
118
+ if (values.length > 1) throw new Error(`Duplicate ${fieldName}`)
119
+ return values[0]
120
+ }
121
+
122
+ export function nfileEncode ({ root, relays = [], author, mime, filename }) {
123
+ if (!Array.isArray(relays)) throw new TypeError('relays should be an array')
124
+ const entries = [[0, fixedHexToBytes(root, 32, 'MMR root')]]
125
+ for (const relay of relays) entries.push([1, encodeText(relay, 'relay hint')])
126
+ if (author !== undefined) entries.push([2, fixedHexToBytes(author, 32, 'author hint')])
127
+ if (mime !== undefined) entries.push([3, encodeText(mime, 'MIME')])
128
+ if (filename !== undefined) entries.push([4, encodeText(filename, 'filename')])
129
+
130
+ const words = bech32.toWords(encodeTlvEntries(entries))
131
+ return bech32.encode('nfile', words, MAX_ENTITY_SIZE)
132
+ }
133
+
134
+ export function nfileDecode (entity) {
135
+ if (typeof entity !== 'string' || entity !== entity.toLowerCase()) {
136
+ throw new Error('nfile should use canonical lowercase Bech32')
137
+ }
138
+ let decoded
139
+ try {
140
+ decoded = bech32.decode(entity, MAX_ENTITY_SIZE)
141
+ } catch (error) {
142
+ throw new Error(`Invalid nfile: ${error.message}`)
143
+ }
144
+ if (decoded.prefix !== 'nfile') throw new Error('Invalid nfile prefix')
145
+
146
+ let bytes
147
+ try {
148
+ bytes = new Uint8Array(bech32.fromWords(decoded.words))
149
+ } catch (error) {
150
+ throw new Error(`Invalid nfile data: ${error.message}`)
151
+ }
152
+
153
+ const known = new Map([[0, []], [1, []], [2, []], [3, []], [4, []]])
154
+ for (const [type, value] of decodeTlvEntries(bytes)) known.get(type)?.push(value)
155
+
156
+ const rootBytes = onlyValue(known.get(0), 'MMR root')
157
+ if (!rootBytes) throw new Error('Missing MMR root')
158
+ if (rootBytes.length !== 32) throw new Error('MMR root should be 32 bytes')
159
+
160
+ const authorBytes = onlyValue(known.get(2), 'author hint')
161
+ if (authorBytes && authorBytes.length !== 32) throw new Error('Author hint should be 32 bytes')
162
+ const mimeBytes = onlyValue(known.get(3), 'MIME')
163
+ const filenameBytes = onlyValue(known.get(4), 'filename')
164
+
165
+ const result = {
166
+ root: bytesToHex(rootBytes),
167
+ relays: known.get(1).map(value => decodeText(value, 'relay hint'))
168
+ }
169
+ if (authorBytes) result.author = bytesToHex(authorBytes)
170
+ if (mimeBytes) result.mime = decodeText(mimeBytes, 'MIME')
171
+ if (filenameBytes) result.filename = decodeText(filenameBytes, 'filename')
172
+ return result
173
+ }
174
+
175
+ function isNostrAppDTagSafe (value) {
176
+ return typeof value === 'string' && value.length <= NOSTR_APP_D_TAG_MAX_LENGTH
177
+ }
178
+
179
+ export function appEncode (ref) {
180
+ if (!isNostrAppDTagSafe(ref.dTag)) throw new Error('Invalid deduplication tag')
181
+ const channel = ref.channel ? (prefixByChannel[ref.channel] && ref.channel) : channelByKind[ref.kind]
182
+ if (!channel) throw new Error('Wrong channel')
183
+
184
+ // Keep the established app-entity byte ordering exactly for compatibility.
185
+ const groups = [
186
+ [textEncoder.encode(ref.dTag)],
187
+ (ref.relays || []).map(relay => textEncoder.encode(relay)),
188
+ [fixedHexToBytes(ref.pubkey, 32, 'author pubkey')]
189
+ ]
190
+ const entries = []
191
+ groups
192
+ .map((values, type) => [type, values])
193
+ .reverse()
194
+ .forEach(([type, values]) => {
195
+ for (const value of values) entries.push([type, value])
196
+ })
197
+ const entity = `${prefixByChannel[channel]}${bytesToBase62(encodeTlvEntries(entries))}`
198
+ if (entity.length > MAX_ENTITY_SIZE) throw new Error('App entity is too big')
199
+ return entity
200
+ }
201
+
202
+ export function appDecode (entity) {
203
+ if (typeof entity !== 'string' || entity.length > MAX_ENTITY_SIZE) throw new Error('Invalid app entity')
204
+ const prefix = entity.match(/^\+*/)?.[0]
205
+ const channel = channelByPrefix[prefix]
206
+ if (!channel) throw new Error('Invalid channel')
207
+
208
+ const values = new Map()
209
+ for (const [type, value] of decodeTlvEntries(base62ToBytes(entity.slice(prefix.length)))) {
210
+ const list = values.get(type) || []
211
+ list.push(value)
212
+ values.set(type, list)
213
+ }
214
+ const dTagBytes = values.get(0)?.[0]
215
+ const pubkeyBytes = values.get(2)?.[0]
216
+ if (!dTagBytes) throw new Error('Missing deduplication tag')
217
+ if (!pubkeyBytes) throw new Error('Missing author pubkey')
218
+ if (pubkeyBytes.length !== 32) throw new Error('Author pubkey should be 32 bytes')
219
+ const dTag = textDecoder.decode(dTagBytes)
220
+ if (!isNostrAppDTagSafe(dTag)) throw new Error('Invalid deduplication tag')
221
+
222
+ return {
223
+ dTag,
224
+ pubkey: bytesToHex(pubkeyBytes),
225
+ kind: kindByChannel[channel],
226
+ channel,
227
+ relays: (values.get(1) || []).map(value => textDecoder.decode(value))
228
+ }
229
+ }
230
+
231
+ export function npubEncode (hex) {
232
+ return bech32.encode('npub', bech32.toWords(fixedHexToBytes(hex, 32, 'pubkey')))
233
+ }
234
+
235
+ export function npubDecode (entity) {
236
+ return decodeSimpleEntity(entity, 'npub', 'pubkey')
237
+ }
238
+
239
+ export function nsecEncode (hex) {
240
+ return bech32.encode('nsec', bech32.toWords(fixedHexToBytes(hex, 32, 'secret key')))
241
+ }
242
+
243
+ export function nsecDecode (entity) {
244
+ return decodeSimpleEntity(entity, 'nsec', 'secret key')
245
+ }
246
+
247
+ function decodeSimpleEntity (entity, prefix, fieldName) {
248
+ if (typeof entity !== 'string' || !entity.startsWith(`${prefix}1`)) {
249
+ throw new Error(`Invalid ${prefix} format`)
250
+ }
251
+ try {
252
+ const decoded = bech32.decode(entity)
253
+ if (decoded.prefix !== prefix) throw new Error(`Invalid ${prefix} prefix`)
254
+ const bytes = new Uint8Array(bech32.fromWords(decoded.words))
255
+ if (bytes.length !== 32) throw new Error(`Invalid ${fieldName} length`)
256
+ return bytesToHex(bytes)
257
+ } catch (error) {
258
+ throw new Error(`Failed to decode ${prefix}: ${error.message}`)
259
+ }
260
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.1.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",
@@ -23,6 +25,7 @@
23
25
  "index.js",
24
26
  "key",
25
27
  "network",
28
+ "nip19",
26
29
  "nip44-v3",
27
30
  "nip46",
28
31
  "private-channel",
@@ -39,6 +42,8 @@
39
42
  "exports": {
40
43
  ".": "./index.js",
41
44
  "./base16": "./base16/index.js",
45
+ "./base36": "./base36/index.js",
46
+ "./base62": "./base62/index.js",
42
47
  "./base64": "./base64/index.js",
43
48
  "./base93": "./base93/index.js",
44
49
  "./content-key": "./content-key/index.js",
@@ -49,6 +54,7 @@
49
54
  "./idb-queue": "./idb-queue/index.js",
50
55
  "./key": "./key/index.js",
51
56
  "./network": "./network/index.js",
57
+ "./nip19": "./nip19/index.js",
52
58
  "./nip44-v3": "./nip44-v3/index.js",
53
59
  "./nip46": "./nip46/index.js",
54
60
  "./private-channel": "./private-channel/index.js",
@@ -63,6 +69,7 @@
63
69
  "@noble/ciphers": "2.2.0",
64
70
  "@noble/curves": "2.2.0",
65
71
  "@noble/hashes": "2.2.0",
72
+ "@scure/base": "2.0.0",
66
73
  "nostr-tools": "2.23.5"
67
74
  },
68
75
  "devDependencies": {