libp2r2p 0.0.2 → 0.2.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.
@@ -0,0 +1,193 @@
1
+ // Derived from https://github.com/ticlo/arrow-code/blob/master/src/base93.ts
2
+ // Apache-2.0. JSON-safe alphabet: space is included; double quote and
3
+ // backslash are intentionally excluded.
4
+
5
+ export const BASE93_ALPHABET =
6
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'()*+,-./:;<=>?@[]^_`{|}~ "
7
+
8
+ const ENCODING_TABLE = (() => {
9
+ const out = new Uint16Array(93)
10
+ for (let i = 0; i < out.length; i++) out[i] = BASE93_ALPHABET.charCodeAt(i)
11
+ return out
12
+ })()
13
+
14
+ const DECODING_TABLE = (() => {
15
+ const out = new Int16Array(128)
16
+ out.fill(-1)
17
+ for (let i = 0; i < BASE93_ALPHABET.length; i++) out[BASE93_ALPHABET.charCodeAt(i)] = i
18
+ return out
19
+ })()
20
+
21
+ function codesToString (codes, length) {
22
+ const chunkLength = 16384
23
+ let result = ''
24
+ for (let i = 0; i < length; i += chunkLength) {
25
+ result += String.fromCharCode.apply(
26
+ null,
27
+ Array.prototype.slice.call(codes, i, Math.min(i + chunkLength, length))
28
+ )
29
+ }
30
+ return result
31
+ }
32
+
33
+ function asBytes (bytes) {
34
+ if (bytes instanceof Uint8Array) return bytes
35
+ if (ArrayBuffer.isView(bytes)) return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
36
+ if (bytes instanceof ArrayBuffer) return new Uint8Array(bytes)
37
+ return Uint8Array.from(bytes)
38
+ }
39
+
40
+ export class Base93Encoder {
41
+ constructor (prefix = '') {
42
+ this.queuedBits = 0
43
+ this.bitCount = 0
44
+ this.parts = prefix ? [String(prefix)] : []
45
+ this.finished = false
46
+ }
47
+
48
+ update (bytes) {
49
+ if (this.finished) throw new Error('Base93 encoder is already finalized.')
50
+ const source = asBytes(bytes)
51
+ const output = new Uint16Array(Math.ceil(source.length * 8 / 6.5) + 4)
52
+ let position = 0
53
+ let queuedBits = this.queuedBits
54
+ let bitCount = this.bitCount
55
+
56
+ for (let i = 0; i < source.length; i++) {
57
+ queuedBits |= source[i] << bitCount
58
+ bitCount += 8
59
+ if (bitCount > 13) {
60
+ let value = queuedBits & 0x1fff
61
+ if (value > 456) {
62
+ queuedBits >>>= 13
63
+ bitCount -= 13
64
+ } else {
65
+ value = queuedBits & 0x3fff
66
+ queuedBits >>>= 14
67
+ bitCount -= 14
68
+ }
69
+ output[position++] = ENCODING_TABLE[value % 93]
70
+ output[position++] = ENCODING_TABLE[Math.floor(value / 93)]
71
+ }
72
+ }
73
+
74
+ this.queuedBits = queuedBits
75
+ this.bitCount = bitCount
76
+ if (position > 0) this.parts.push(codesToString(output, position))
77
+ return this
78
+ }
79
+
80
+ getEncoded () {
81
+ if (!this.finished) {
82
+ if (this.bitCount > 0) {
83
+ const output = new Uint16Array(2)
84
+ let position = 0
85
+ output[position++] = ENCODING_TABLE[this.queuedBits % 93]
86
+ if (this.bitCount > 7 || this.queuedBits > 92) {
87
+ output[position++] = ENCODING_TABLE[Math.floor(this.queuedBits / 93)]
88
+ }
89
+ this.parts.push(codesToString(output, position))
90
+ }
91
+ this.finished = true
92
+ this.queuedBits = 0
93
+ this.bitCount = 0
94
+ }
95
+ return this.parts.join('')
96
+ }
97
+ }
98
+
99
+ export function encode (bytes) {
100
+ return new Base93Encoder().update(bytes).getEncoded()
101
+ }
102
+
103
+ function decodeUnchecked (value) {
104
+ const output = new Uint8Array(Math.ceil(value.length * 7 / 8))
105
+ let queuedBits = 0
106
+ let bitCount = 0
107
+ let first = -1
108
+ let position = 0
109
+
110
+ for (let i = 0; i < value.length; i++) {
111
+ const code = value.charCodeAt(i)
112
+ if (code >= DECODING_TABLE.length || DECODING_TABLE[code] < 0) {
113
+ throw new Error(`Invalid Base93 character at offset ${i}.`)
114
+ }
115
+ const decoded = DECODING_TABLE[code]
116
+ if (first === -1) {
117
+ first = decoded
118
+ continue
119
+ }
120
+
121
+ const pair = first + decoded * 93
122
+ first = -1
123
+ queuedBits |= pair << bitCount
124
+ bitCount += (pair & 0x1fff) > 456 ? 13 : 14
125
+ while (bitCount > 7) {
126
+ output[position++] = queuedBits & 0xff
127
+ queuedBits >>>= 8
128
+ bitCount -= 8
129
+ }
130
+ }
131
+
132
+ if (first !== -1) output[position++] = (queuedBits | (first << bitCount)) & 0xff
133
+ return output.slice(0, position)
134
+ }
135
+
136
+ export function decode (text, offset = 0, length = -1) {
137
+ if (typeof text !== 'string') throw new TypeError('Base93 input must be a string.')
138
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > text.length) throw new RangeError('Invalid Base93 offset.')
139
+ if (!Number.isSafeInteger(length) || length < -1) throw new RangeError('Invalid Base93 length.')
140
+
141
+ const end = length < 0 ? text.length : Math.min(text.length, offset + length)
142
+ const encoded = text.slice(offset, end)
143
+ const bytes = decodeUnchecked(encoded)
144
+ if (encode(bytes) !== encoded) throw new Error('Non-canonical or truncated Base93 input.')
145
+ return bytes
146
+ }
147
+
148
+ function normalizeSource (source) {
149
+ if (typeof source === 'function') source = source()
150
+ if (typeof source === 'string') return [source]
151
+ if (source?.[Symbol.asyncIterator] || source?.[Symbol.iterator]) return source
152
+ throw new TypeError('Base93 stream source must be iterable.')
153
+ }
154
+
155
+ export class Base93Decoder {
156
+ constructor (source, { mimeType = '', preferTextStreamDecoding = false } = {}) {
157
+ this.source = normalizeSource(source)
158
+ this.asText = preferTextStreamDecoding && mimeType.startsWith('text/')
159
+ }
160
+
161
+ async * decodedValues () {
162
+ const textDecoder = this.asText ? new TextDecoder() : null
163
+ for await (const encoded of this.source) {
164
+ const bytes = decode(encoded)
165
+ const value = textDecoder ? textDecoder.decode(bytes, { stream: true }) : bytes
166
+ if (value.length > 0) yield value
167
+ }
168
+ if (textDecoder) {
169
+ const tail = textDecoder.decode()
170
+ if (tail.length > 0) yield tail
171
+ }
172
+ }
173
+
174
+ getDecoded () {
175
+ const iterator = this.decodedValues()[Symbol.asyncIterator]()
176
+ return new ReadableStream({
177
+ async pull (controller) {
178
+ try {
179
+ const { value, done } = await iterator.next()
180
+ if (done) controller.close()
181
+ else controller.enqueue(value)
182
+ } catch (error) {
183
+ controller.error(error)
184
+ }
185
+ },
186
+ async cancel () {
187
+ await iterator.return?.()
188
+ }
189
+ })
190
+ }
191
+ }
192
+
193
+ export default Base93Decoder
package/index.js CHANGED
@@ -10,6 +10,7 @@ export * as idb from './idb/index.js'
10
10
  export * as idbQueue from './idb-queue/index.js'
11
11
  export * as key from './key/index.js'
12
12
  export * as network from './network/index.js'
13
+ export * as nip19 from './nip19/index.js'
13
14
  export * as nip44v3 from './nip44-v3/index.js'
14
15
  export * as nip46 from './nip46/index.js'
15
16
  export * as privateChannel from './private-channel/index.js'
package/nip19/index.js ADDED
@@ -0,0 +1,305 @@
1
+ import { bech32 } from '@scure/base'
2
+
3
+ const MAX_ENTITY_SIZE = 5000
4
+ const MAX_TLV_VALUE_SIZE = 255
5
+ 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
+ export const NAPP_ENTITY_REGEX = new RegExp(
14
+ `^\\+{1,3}[${BASE62_ALPHABET}]{48,${MAX_ENTITY_SIZE}}$`
15
+ )
16
+
17
+ const textEncoder = new TextEncoder()
18
+ const textDecoder = new TextDecoder()
19
+ const fatalTextDecoder = new TextDecoder('utf-8', { fatal: true })
20
+
21
+ const kindByChannel = {
22
+ main: 35128,
23
+ next: 35129,
24
+ draft: 35130
25
+ }
26
+ const channelByKind = Object.fromEntries(
27
+ Object.entries(kindByChannel).map(([channel, kind]) => [kind, channel])
28
+ )
29
+ const prefixByChannel = {
30
+ main: '+',
31
+ next: '++',
32
+ draft: '+++'
33
+ }
34
+ const channelByPrefix = Object.fromEntries(
35
+ Object.entries(prefixByChannel).map(([channel, prefix]) => [prefix, channel])
36
+ )
37
+
38
+ function bytesToHex (bytes) {
39
+ let result = ''
40
+ for (const byte of bytes) result += byte.toString(16).padStart(2, '0')
41
+ return result
42
+ }
43
+
44
+ function hexToBytes (hex, fieldName = 'hex value') {
45
+ if (typeof hex !== 'string' || !/^[0-9a-f]+$/i.test(hex) || hex.length % 2 !== 0) {
46
+ throw new Error(`Invalid ${fieldName}`)
47
+ }
48
+ const result = new Uint8Array(hex.length / 2)
49
+ for (let index = 0; index < result.length; index++) {
50
+ result[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16)
51
+ }
52
+ return result
53
+ }
54
+
55
+ function fixedHexToBytes (hex, byteLength, fieldName) {
56
+ const bytes = hexToBytes(hex, fieldName)
57
+ if (bytes.length !== byteLength) throw new Error(`${fieldName} should be ${byteLength} bytes`)
58
+ return bytes
59
+ }
60
+
61
+ function concatBytes (arrays) {
62
+ const size = arrays.reduce((total, array) => total + array.length, 0)
63
+ const result = new Uint8Array(size)
64
+ let offset = 0
65
+ for (const array of arrays) {
66
+ result.set(array, offset)
67
+ offset += array.length
68
+ }
69
+ return result
70
+ }
71
+
72
+ function encodeText (value, fieldName) {
73
+ if (typeof value !== 'string' || value.length === 0) {
74
+ throw new Error(`${fieldName} should be a non-empty string`)
75
+ }
76
+ const bytes = textEncoder.encode(value)
77
+ if (bytes.length > MAX_TLV_VALUE_SIZE) throw new Error(`${fieldName} is too big`)
78
+ // TextEncoder replaces lone surrogates. Reject them instead of silently changing
79
+ // the signed/canonical entity value.
80
+ if (fatalTextDecoder.decode(bytes) !== value) throw new Error(`Invalid ${fieldName} UTF-8`)
81
+ return bytes
82
+ }
83
+
84
+ function decodeText (bytes, fieldName) {
85
+ if (bytes.length === 0 || bytes.length > MAX_TLV_VALUE_SIZE) {
86
+ throw new Error(`Invalid ${fieldName} length`)
87
+ }
88
+ try {
89
+ return fatalTextDecoder.decode(bytes)
90
+ } catch {
91
+ throw new Error(`Invalid ${fieldName} UTF-8`)
92
+ }
93
+ }
94
+
95
+ function encodeTlvEntries (entries) {
96
+ return concatBytes(entries.map(([type, value]) => {
97
+ if (!Number.isInteger(type) || type < 0 || type > 255) throw new Error('Invalid TLV type')
98
+ if (!(value instanceof Uint8Array)) throw new TypeError('TLV value should be a Uint8Array')
99
+ if (value.length > MAX_TLV_VALUE_SIZE) throw new Error('TLV value is too big')
100
+ const entry = new Uint8Array(value.length + 2)
101
+ entry[0] = type
102
+ entry[1] = value.length
103
+ entry.set(value, 2)
104
+ return entry
105
+ }))
106
+ }
107
+
108
+ function decodeTlvEntries (bytes) {
109
+ const entries = []
110
+ let offset = 0
111
+ while (offset < bytes.length) {
112
+ if (bytes.length - offset < 2) throw new Error('Truncated TLV header')
113
+ const type = bytes[offset]
114
+ const length = bytes[offset + 1]
115
+ offset += 2
116
+ if (bytes.length - offset < length) throw new Error(`Truncated TLV value for type ${type}`)
117
+ entries.push([type, bytes.slice(offset, offset + length)])
118
+ offset += length
119
+ }
120
+ return entries
121
+ }
122
+
123
+ function onlyValue (values, fieldName) {
124
+ if (values.length > 1) throw new Error(`Duplicate ${fieldName}`)
125
+ return values[0]
126
+ }
127
+
128
+ export function nfileEncode ({ root, relays = [], author, mime, filename }) {
129
+ if (!Array.isArray(relays)) throw new TypeError('relays should be an array')
130
+ const entries = [[0, fixedHexToBytes(root, 32, 'MMR root')]]
131
+ for (const relay of relays) entries.push([1, encodeText(relay, 'relay hint')])
132
+ if (author !== undefined) entries.push([2, fixedHexToBytes(author, 32, 'author hint')])
133
+ if (mime !== undefined) entries.push([3, encodeText(mime, 'MIME')])
134
+ if (filename !== undefined) entries.push([4, encodeText(filename, 'filename')])
135
+
136
+ const words = bech32.toWords(encodeTlvEntries(entries))
137
+ return bech32.encode('nfile', words, MAX_ENTITY_SIZE)
138
+ }
139
+
140
+ export function nfileDecode (entity) {
141
+ if (typeof entity !== 'string' || entity !== entity.toLowerCase()) {
142
+ throw new Error('nfile should use canonical lowercase Bech32')
143
+ }
144
+ let decoded
145
+ try {
146
+ decoded = bech32.decode(entity, MAX_ENTITY_SIZE)
147
+ } catch (error) {
148
+ throw new Error(`Invalid nfile: ${error.message}`)
149
+ }
150
+ if (decoded.prefix !== 'nfile') throw new Error('Invalid nfile prefix')
151
+
152
+ let bytes
153
+ try {
154
+ bytes = new Uint8Array(bech32.fromWords(decoded.words))
155
+ } catch (error) {
156
+ throw new Error(`Invalid nfile data: ${error.message}`)
157
+ }
158
+
159
+ const known = new Map([[0, []], [1, []], [2, []], [3, []], [4, []]])
160
+ for (const [type, value] of decodeTlvEntries(bytes)) known.get(type)?.push(value)
161
+
162
+ const rootBytes = onlyValue(known.get(0), 'MMR root')
163
+ if (!rootBytes) throw new Error('Missing MMR root')
164
+ if (rootBytes.length !== 32) throw new Error('MMR root should be 32 bytes')
165
+
166
+ const authorBytes = onlyValue(known.get(2), 'author hint')
167
+ if (authorBytes && authorBytes.length !== 32) throw new Error('Author hint should be 32 bytes')
168
+ const mimeBytes = onlyValue(known.get(3), 'MIME')
169
+ const filenameBytes = onlyValue(known.get(4), 'filename')
170
+
171
+ const result = {
172
+ root: bytesToHex(rootBytes),
173
+ relays: known.get(1).map(value => decodeText(value, 'relay hint'))
174
+ }
175
+ if (authorBytes) result.author = bytesToHex(authorBytes)
176
+ if (mimeBytes) result.mime = decodeText(mimeBytes, 'MIME')
177
+ if (filenameBytes) result.filename = decodeText(filenameBytes, 'filename')
178
+ return result
179
+ }
180
+
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
+ function isNostrAppDTagSafe (value) {
221
+ return typeof value === 'string' && value.length <= NOSTR_APP_D_TAG_MAX_LENGTH
222
+ }
223
+
224
+ export function appEncode (ref) {
225
+ if (!isNostrAppDTagSafe(ref.dTag)) throw new Error('Invalid deduplication tag')
226
+ const channel = ref.channel ? (prefixByChannel[ref.channel] && ref.channel) : channelByKind[ref.kind]
227
+ if (!channel) throw new Error('Wrong channel')
228
+
229
+ // Keep the established app-entity byte ordering exactly for compatibility.
230
+ const groups = [
231
+ [textEncoder.encode(ref.dTag)],
232
+ (ref.relays || []).map(relay => textEncoder.encode(relay)),
233
+ [fixedHexToBytes(ref.pubkey, 32, 'author pubkey')]
234
+ ]
235
+ const entries = []
236
+ groups
237
+ .map((values, type) => [type, values])
238
+ .reverse()
239
+ .forEach(([type, values]) => {
240
+ for (const value of values) entries.push([type, value])
241
+ })
242
+ const entity = `${prefixByChannel[channel]}${bytesToBase62(encodeTlvEntries(entries))}`
243
+ if (entity.length > MAX_ENTITY_SIZE) throw new Error('App entity is too big')
244
+ return entity
245
+ }
246
+
247
+ export function appDecode (entity) {
248
+ if (typeof entity !== 'string' || entity.length > MAX_ENTITY_SIZE) throw new Error('Invalid app entity')
249
+ const prefix = entity.match(/^\+*/)?.[0]
250
+ const channel = channelByPrefix[prefix]
251
+ if (!channel) throw new Error('Invalid channel')
252
+
253
+ const values = new Map()
254
+ for (const [type, value] of decodeTlvEntries(base62ToBytes(entity.slice(prefix.length)))) {
255
+ const list = values.get(type) || []
256
+ list.push(value)
257
+ values.set(type, list)
258
+ }
259
+ const dTagBytes = values.get(0)?.[0]
260
+ const pubkeyBytes = values.get(2)?.[0]
261
+ if (!dTagBytes) throw new Error('Missing deduplication tag')
262
+ if (!pubkeyBytes) throw new Error('Missing author pubkey')
263
+ if (pubkeyBytes.length !== 32) throw new Error('Author pubkey should be 32 bytes')
264
+ const dTag = textDecoder.decode(dTagBytes)
265
+ if (!isNostrAppDTagSafe(dTag)) throw new Error('Invalid deduplication tag')
266
+
267
+ return {
268
+ dTag,
269
+ pubkey: bytesToHex(pubkeyBytes),
270
+ kind: kindByChannel[channel],
271
+ channel,
272
+ relays: (values.get(1) || []).map(value => textDecoder.decode(value))
273
+ }
274
+ }
275
+
276
+ export function npubEncode (hex) {
277
+ return bech32.encode('npub', bech32.toWords(fixedHexToBytes(hex, 32, 'pubkey')))
278
+ }
279
+
280
+ export function npubDecode (entity) {
281
+ return decodeSimpleEntity(entity, 'npub', 'pubkey')
282
+ }
283
+
284
+ export function nsecEncode (hex) {
285
+ return bech32.encode('nsec', bech32.toWords(fixedHexToBytes(hex, 32, 'secret key')))
286
+ }
287
+
288
+ export function nsecDecode (entity) {
289
+ return decodeSimpleEntity(entity, 'nsec', 'secret key')
290
+ }
291
+
292
+ function decodeSimpleEntity (entity, prefix, fieldName) {
293
+ if (typeof entity !== 'string' || !entity.startsWith(`${prefix}1`)) {
294
+ throw new Error(`Invalid ${prefix} format`)
295
+ }
296
+ try {
297
+ const decoded = bech32.decode(entity)
298
+ if (decoded.prefix !== prefix) throw new Error(`Invalid ${prefix} prefix`)
299
+ const bytes = new Uint8Array(bech32.fromWords(decoded.words))
300
+ if (bytes.length !== 32) throw new Error(`Invalid ${fieldName} length`)
301
+ return bytesToHex(bytes)
302
+ } catch (error) {
303
+ throw new Error(`Failed to decode ${prefix}: ${error.message}`)
304
+ }
305
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -11,6 +11,28 @@
11
11
  "author": "arthurfranca",
12
12
  "type": "module",
13
13
  "sideEffects": false,
14
+ "files": [
15
+ "base16",
16
+ "base64",
17
+ "base93",
18
+ "content-key",
19
+ "double-dh",
20
+ "ecdh",
21
+ "idb",
22
+ "idb-queue",
23
+ "index.js",
24
+ "key",
25
+ "network",
26
+ "nip19",
27
+ "nip44-v3",
28
+ "nip46",
29
+ "private-channel",
30
+ "private-message",
31
+ "private-messenger",
32
+ "relay",
33
+ "temporary-storage",
34
+ "web-storage-queue"
35
+ ],
14
36
  "scripts": {
15
37
  "test": "node --test --experimental-test-module-mocks 'tests/**/*.test.js'",
16
38
  "test:only": "node --test --test-only --experimental-test-module-mocks 'tests/**/*.test.js'"
@@ -19,6 +41,7 @@
19
41
  ".": "./index.js",
20
42
  "./base16": "./base16/index.js",
21
43
  "./base64": "./base64/index.js",
44
+ "./base93": "./base93/index.js",
22
45
  "./content-key": "./content-key/index.js",
23
46
  "./content-key/event": "./content-key/event/index.js",
24
47
  "./double-dh": "./double-dh/index.js",
@@ -27,6 +50,7 @@
27
50
  "./idb-queue": "./idb-queue/index.js",
28
51
  "./key": "./key/index.js",
29
52
  "./network": "./network/index.js",
53
+ "./nip19": "./nip19/index.js",
30
54
  "./nip44-v3": "./nip44-v3/index.js",
31
55
  "./nip46": "./nip46/index.js",
32
56
  "./private-channel": "./private-channel/index.js",
@@ -41,6 +65,7 @@
41
65
  "@noble/ciphers": "2.2.0",
42
66
  "@noble/curves": "2.2.0",
43
67
  "@noble/hashes": "2.2.0",
68
+ "@scure/base": "2.0.0",
44
69
  "nostr-tools": "2.23.5"
45
70
  },
46
71
  "devDependencies": {
package/AGENTS.md DELETED
@@ -1,20 +0,0 @@
1
- # AGENTS.md
2
-
3
- ## Project Shape
4
-
5
- libp2r2p is an ESM package with no build step. Public package exports should
6
- come from their own folders, each with an `index.js` entry point. Prefer
7
- singular public subpaths, such as `libp2r2p/key` and `libp2r2p/relay`.
8
-
9
- Public export folders may organize implementation details with `constants/`,
10
- `helpers/`, and `services/` subfolders. Additional custom public subfolders
11
- are fine when they are part of the API, such as `private-messenger/recovery`
12
- or `content-key/event`, and those should also expose an `index.js`.
13
-
14
- Root-level `constants/`, `helpers/`, and `services/` folders are reserved
15
- for shared internal code that is not itself a public export. Do not expose
16
- internal files directly through `package.json`; add or adjust an export-folder
17
- `index.js` instead.
18
-
19
- Keep the `exports` object in `package.json` sorted, with `.` first.
20
- Do not keep backwards-compatible alias exports unless explicitly requested.
@@ -1,19 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import {
5
- base16ToBytes,
6
- bytesToBase16,
7
- bytesToHex,
8
- hexToBytes
9
- } from '../base16/index.js'
10
-
11
- test('base16 helpers encode bytes and expose hex aliases', () => {
12
- const bytes = new Uint8Array([0, 1, 15, 16, 254, 255])
13
- const encoded = '00010f10feff'
14
-
15
- assert.equal(bytesToBase16(bytes), encoded)
16
- assert.deepEqual(base16ToBytes(encoded), bytes)
17
- assert.equal(bytesToHex, bytesToBase16)
18
- assert.equal(hexToBytes, base16ToBytes)
19
- })
@@ -1,18 +0,0 @@
1
- import { test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import {
5
- base64ToBytes,
6
- base64UrlToBytes,
7
- bytesToBase64,
8
- bytesToBase64Url
9
- } from '../base64/index.js'
10
-
11
- test('base64 helpers encode bytes with plain and URL-safe alphabets', () => {
12
- const bytes = new Uint8Array([0, 1, 2, 251, 252, 253, 254, 255])
13
-
14
- assert.equal(bytesToBase64(bytes), 'AAEC+/z9/v8=')
15
- assert.deepEqual(base64ToBytes('AAEC+/z9/v8='), bytes)
16
- assert.equal(bytesToBase64Url(bytes), 'AAEC-_z9_v8')
17
- assert.deepEqual(base64UrlToBytes('AAEC-_z9_v8'), bytes)
18
- })
@@ -1,48 +0,0 @@
1
- import { afterEach, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { generateSecretKey, verifyEvent } from 'nostr-tools'
4
-
5
- import TestSigner from './helpers/test-signer.js'
6
- import { bytesToHex } from '../base16/index.js'
7
- import {
8
- CONTENT_KEY_KIND,
9
- makeContentKeyEvent,
10
- parseContentKeyEvent,
11
- verifyContentKeyProof,
12
- verifyIykcProof
13
- } from '../content-key/event/index.js'
14
-
15
- afterEach(() => {
16
- TestSigner.releaseAll()
17
- })
18
-
19
- function signer () {
20
- return TestSigner.getOrCreate(bytesToHex(generateSecretKey()))
21
- }
22
-
23
- test('makeContentKeyEvent publishes a signed content pubkey proof', async () => {
24
- const user = signer()
25
- const contentKey = signer()
26
- const userPubkey = await user.getPublicKey()
27
- const contentPubkey = await contentKey.getPublicKey()
28
- const event = await makeContentKeyEvent({ userSigner: user, contentKeySigner: contentKey, createdAt: 7 })
29
- const parsed = parseContentKeyEvent(event)
30
-
31
- assert.equal(event.kind, CONTENT_KEY_KIND)
32
- assert.equal(event.pubkey, userPubkey)
33
- assert.deepEqual(event.tags, [['cp', contentPubkey]])
34
- assert.equal(parsed.iykcPubkey, contentPubkey)
35
- assert.equal(parsed.iykcProof, `${event.created_at}:${event.sig}`)
36
- assert.equal(verifyIykcProof({ receiverPubkey: userPubkey, iykcPubkey: contentPubkey, iykcProof: parsed.iykcProof }), true)
37
- assert.equal(verifyContentKeyProof({ ownerPubkey: userPubkey, contentPubkey, proof: parsed.iykcProof }), true)
38
- assert.equal(verifyEvent(event), true)
39
- })
40
-
41
- test('parseContentKeyEvent rejects malformed content-key events', async () => {
42
- const event = await makeContentKeyEvent({ userSigner: signer(), contentKeySigner: signer(), createdAt: 7 })
43
-
44
- assert.equal(parseContentKeyEvent({ ...event, tags: event.tags.concat([['x', 'nope']]) }), null)
45
- assert.equal(parseContentKeyEvent({ ...event, tags: [['cp', event.tags[0][1], 'extra']] }), null)
46
- assert.equal(parseContentKeyEvent({ ...event, content: 'nope' }), null)
47
- assert.equal(parseContentKeyEvent({ ...event, tags: [['cp', 'f'.repeat(64)]] }), null)
48
- })