libp2r2p 0.8.0 → 0.9.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.
Files changed (49) hide show
  1. package/README.md +83 -2
  2. package/base16/index.js +6 -3
  3. package/base36/index.js +12 -8
  4. package/base62/index.js +15 -11
  5. package/base64/index.js +18 -2
  6. package/base93/index.js +19 -7
  7. package/content-key/event/index.js +40 -12
  8. package/double-dh/index.js +21 -6
  9. package/ecdh/index.js +12 -1
  10. package/error/index.js +32 -0
  11. package/event/helpers/serialize.js +31 -0
  12. package/event/index.js +116 -0
  13. package/i18n/index.js +15 -13
  14. package/idb/index.js +5 -3
  15. package/idb-queue/index.js +21 -20
  16. package/index.js +10 -0
  17. package/key/index.js +31 -18
  18. package/kind/index.js +244 -0
  19. package/nip04/index.js +47 -0
  20. package/nip05/index.js +61 -0
  21. package/nip19/index.js +176 -43
  22. package/nip44/helpers.js +95 -0
  23. package/nip44/index.js +14 -0
  24. package/nip44-v3/index.js +35 -16
  25. package/nip46/helpers/frame.js +6 -6
  26. package/nip46/helpers/url.js +8 -6
  27. package/nip46/services/bunker-signer.js +7 -6
  28. package/nip46/services/client.js +8 -7
  29. package/nip46/services/server-session.js +8 -7
  30. package/nip46/services/transport.js +9 -8
  31. package/nip96/index.js +285 -0
  32. package/nip98/index.js +56 -0
  33. package/nwt/index.js +241 -0
  34. package/package.json +22 -3
  35. package/private-channel/helpers/chunks.js +7 -6
  36. package/private-channel/helpers/event.js +5 -4
  37. package/private-channel/index.js +63 -61
  38. package/private-channel/services/received-chunks.js +4 -3
  39. package/private-message/index.js +32 -31
  40. package/private-messenger/index.js +23 -22
  41. package/private-messenger/recovery/index.js +15 -14
  42. package/private-messenger/services/channel-state.js +2 -1
  43. package/relay/helpers/hll.js +3 -1
  44. package/relay/services/query.js +3 -3
  45. package/relay/services/relay-connection.js +222 -121
  46. package/relay/services/relay-pool.js +13 -10
  47. package/temporary-storage/index.js +3 -1
  48. package/url/index.js +131 -0
  49. package/web-storage-queue/index.js +8 -6
package/nwt/index.js ADDED
@@ -0,0 +1,241 @@
1
+ import { base64UrlToBytes, bytesToBase64Url } from '../base64/index.js'
2
+ import { ValidationError } from '../error/index.js'
3
+ import { isValidEvent } from '../event/index.js'
4
+ import { NWT } from '../kind/index.js'
5
+
6
+ const textDecoder = new TextDecoder('utf-8', { fatal: true })
7
+ const textEncoder = new TextEncoder()
8
+ const BASE64URL = /^[A-Za-z0-9_-]+$/
9
+ const REGISTERED_CLAIMS = new Set(['iss', 'sub', 'aud', 'iat', 'exp', 'nbf'])
10
+ const SINGLE_CLAIMS = new Set(['iss', 'sub', 'iat', 'exp', 'nbf'])
11
+ const TIMESTAMP_CLAIMS = new Set(['iat', 'exp', 'nbf'])
12
+ const MAX_CLAIMS = 512
13
+
14
+ function fail (code, { message = code, cause } = {}) {
15
+ throw new ValidationError(code, { message, cause })
16
+ }
17
+
18
+ function cloneTags (tags) {
19
+ return tags.map(tag => tag.slice())
20
+ }
21
+
22
+ function areTagsEqual (left, right) {
23
+ return left.length === right.length && left.every((tag, index) => {
24
+ const other = right[index]
25
+ return tag.length === other.length && tag.every((value, valueIndex) => value === other[valueIndex])
26
+ })
27
+ }
28
+
29
+ function normalizeTimestamp (value, code) {
30
+ if (!Number.isSafeInteger(value) || value < 0) fail(code)
31
+ return value
32
+ }
33
+
34
+ function parseTimestamp (value, code) {
35
+ if (!/^(?:0|[1-9][0-9]*)$/.test(value)) fail(code)
36
+ return normalizeTimestamp(Number(value), code)
37
+ }
38
+
39
+ function normalizeStringClaim (value, code) {
40
+ if (typeof value !== 'string' || value.length === 0) fail(code)
41
+ return value
42
+ }
43
+
44
+ function normalizeAudience (audience, { required = false } = {}) {
45
+ if (audience === undefined) {
46
+ if (required) fail('INVALID_AUDIENCE')
47
+ return []
48
+ }
49
+ const values = typeof audience === 'string' ? [audience] : audience
50
+ if (!Array.isArray(values) || (required && values.length === 0)) fail('INVALID_AUDIENCE')
51
+ return values.map(value => normalizeStringClaim(value, 'INVALID_AUDIENCE'))
52
+ }
53
+
54
+ function normalizeExtraClaims (claims) {
55
+ if (claims === undefined) return []
56
+ if (!Array.isArray(claims)) fail('INVALID_CLAIMS')
57
+ return claims.map(tag => {
58
+ if (!Array.isArray(tag) || tag.length < 2 || tag.some(value => typeof value !== 'string')) fail('INVALID_CLAIM')
59
+ if (tag[0].length === 0 || REGISTERED_CLAIMS.has(tag[0])) fail('INVALID_CUSTOM_CLAIM')
60
+ return tag.slice()
61
+ })
62
+ }
63
+
64
+ function parseClaims (event) {
65
+ if (event.tags.length > MAX_CLAIMS) fail('TOO_MANY_CLAIMS')
66
+
67
+ const single = new Map()
68
+ const audience = []
69
+ const claims = []
70
+
71
+ for (const tag of event.tags) {
72
+ if (tag.length < 2 || tag[0].length === 0) fail('INVALID_CLAIM')
73
+ const name = tag[0]
74
+
75
+ if (!REGISTERED_CLAIMS.has(name)) {
76
+ claims.push(tag.slice())
77
+ continue
78
+ }
79
+ if (tag.length !== 2 || tag[1].length === 0) fail('INVALID_REGISTERED_CLAIM')
80
+ if (name === 'aud') {
81
+ audience.push(tag[1])
82
+ continue
83
+ }
84
+ if (SINGLE_CLAIMS.has(name) && single.has(name)) fail('DUPLICATE_SINGLE_CLAIM')
85
+ single.set(name, TIMESTAMP_CLAIMS.has(name)
86
+ ? parseTimestamp(tag[1], `INVALID_${name.toUpperCase()}_CLAIM`)
87
+ : tag[1])
88
+ }
89
+
90
+ const expiration = single.get('exp') ?? null
91
+ const notBefore = single.get('nbf') ?? null
92
+ if (expiration !== null && notBefore !== null && notBefore > expiration) fail('INVALID_TIME_WINDOW')
93
+
94
+ return {
95
+ event,
96
+ id: event.id,
97
+ signer: event.pubkey,
98
+ issuer: single.get('iss') ?? event.pubkey,
99
+ subject: single.get('sub') ?? event.pubkey,
100
+ audience,
101
+ issuedAt: single.get('iat') ?? event.created_at,
102
+ expiration,
103
+ notBefore,
104
+ claims,
105
+ content: event.content
106
+ }
107
+ }
108
+
109
+ function parseVerifiedEvent (event) {
110
+ if (!isValidEvent(event)) fail('INVALID_NWT_EVENT')
111
+ if (event.kind !== NWT) fail('INVALID_NWT_KIND')
112
+ return parseClaims(event)
113
+ }
114
+
115
+ function wireEvent (event) {
116
+ return {
117
+ id: event.id,
118
+ pubkey: event.pubkey,
119
+ created_at: event.created_at,
120
+ kind: event.kind,
121
+ tags: cloneTags(event.tags),
122
+ content: event.content,
123
+ sig: event.sig
124
+ }
125
+ }
126
+
127
+ export async function createToken ({
128
+ signEvent,
129
+ issuer,
130
+ subject,
131
+ audience,
132
+ issuedAt,
133
+ expiration,
134
+ notBefore,
135
+ claims,
136
+ content = '',
137
+ createdAt = Math.floor(Date.now() / 1000)
138
+ } = {}) {
139
+ if (typeof signEvent !== 'function') fail('SIGN_EVENT_SHOULD_BE_A_FUNCTION')
140
+ if (typeof content !== 'string') fail('INVALID_CONTENT')
141
+
142
+ const tags = []
143
+ if (issuer !== undefined) tags.push(['iss', normalizeStringClaim(issuer, 'INVALID_ISSUER')])
144
+ if (subject !== undefined) tags.push(['sub', normalizeStringClaim(subject, 'INVALID_SUBJECT')])
145
+ for (const value of normalizeAudience(audience)) tags.push(['aud', value])
146
+ if (issuedAt !== undefined) tags.push(['iat', String(normalizeTimestamp(issuedAt, 'INVALID_ISSUED_AT'))])
147
+ if (expiration !== undefined) tags.push(['exp', String(normalizeTimestamp(expiration, 'INVALID_EXPIRATION'))])
148
+ if (notBefore !== undefined) tags.push(['nbf', String(normalizeTimestamp(notBefore, 'INVALID_NOT_BEFORE'))])
149
+ tags.push(...normalizeExtraClaims(claims))
150
+ if (tags.length > MAX_CLAIMS) fail('TOO_MANY_CLAIMS')
151
+
152
+ const expected = {
153
+ kind: NWT,
154
+ created_at: normalizeTimestamp(createdAt, 'INVALID_CREATED_AT'),
155
+ tags,
156
+ content
157
+ }
158
+ if (expiration !== undefined && notBefore !== undefined && notBefore > expiration) fail('INVALID_TIME_WINDOW')
159
+
160
+ const event = await signEvent({ ...expected, tags: cloneTags(expected.tags) })
161
+ parseVerifiedEvent(event)
162
+ if (
163
+ event.kind !== expected.kind ||
164
+ event.created_at !== expected.created_at ||
165
+ event.content !== expected.content ||
166
+ !areTagsEqual(event.tags, expected.tags)
167
+ ) fail('SIGNED_NWT_EVENT_WAS_CHANGED')
168
+ return event
169
+ }
170
+
171
+ export function encodeToken (event, { includeAuthorizationScheme = false } = {}) {
172
+ parseVerifiedEvent(event)
173
+ const token = bytesToBase64Url(textEncoder.encode(JSON.stringify(wireEvent(event))))
174
+ return includeAuthorizationScheme ? `Nostr ${token}` : token
175
+ }
176
+
177
+ export function decodeToken (value) {
178
+ if (typeof value !== 'string' || value.length === 0) fail('INVALID_NWT_TOKEN')
179
+ let token = value
180
+ if (value.startsWith('Nostr ')) token = value.slice(6)
181
+ else if (/\s/.test(value)) fail('INVALID_AUTHORIZATION_HEADER')
182
+ if (!BASE64URL.test(token) || token.length % 4 === 1) fail('INVALID_NWT_ENCODING')
183
+
184
+ let bytes
185
+ try {
186
+ bytes = base64UrlToBytes(token)
187
+ if (bytesToBase64Url(bytes) !== token) fail('INVALID_NWT_ENCODING')
188
+ } catch (cause) {
189
+ fail('INVALID_NWT_ENCODING', { cause })
190
+ }
191
+
192
+ try {
193
+ const event = JSON.parse(textDecoder.decode(bytes))
194
+ if (!event || typeof event !== 'object' || Array.isArray(event)) fail('INVALID_NWT_EVENT_JSON')
195
+ return event
196
+ } catch (error) {
197
+ if (error instanceof ValidationError && error.code === 'INVALID_NWT_EVENT_JSON') throw error
198
+ fail('INVALID_NWT_EVENT_JSON', { cause: error })
199
+ }
200
+ }
201
+
202
+ export function validateToken (value, {
203
+ audience,
204
+ signer,
205
+ issuer,
206
+ subject,
207
+ now = Math.floor(Date.now() / 1000),
208
+ clockSkewSeconds = 60,
209
+ requireAudience = false,
210
+ requireExpiration = false
211
+ } = {}) {
212
+ const event = typeof value === 'string' ? decodeToken(value) : value
213
+ const token = parseVerifiedEvent(event)
214
+ normalizeTimestamp(now, 'INVALID_NOW')
215
+ normalizeTimestamp(clockSkewSeconds, 'INVALID_CLOCK_SKEW')
216
+ if (!Number.isSafeInteger(now + clockSkewSeconds) || !Number.isSafeInteger(now - clockSkewSeconds)) {
217
+ fail('INVALID_CLOCK_SKEW')
218
+ }
219
+
220
+ if (token.notBefore !== null && now + clockSkewSeconds < token.notBefore) fail('NWT_NOT_YET_VALID')
221
+ if (token.expiration !== null && now - clockSkewSeconds >= token.expiration) fail('NWT_EXPIRED')
222
+ if (requireExpiration && token.expiration === null) fail('NWT_EXPIRATION_REQUIRED')
223
+
224
+ if (token.audience.length === 0) {
225
+ if (requireAudience) fail('NWT_AUDIENCE_REQUIRED')
226
+ } else {
227
+ if (audience === undefined) fail('NWT_AUDIENCE_REQUIRED')
228
+ const expectedAudience = normalizeAudience(audience, { required: true })
229
+ if (!expectedAudience.some(value => token.audience.includes(value))) fail('NWT_AUDIENCE_MISMATCH')
230
+ }
231
+
232
+ for (const [expected, actual, code] of [
233
+ [signer, token.signer, 'NWT_SIGNER_MISMATCH'],
234
+ [issuer, token.issuer, 'NWT_ISSUER_MISMATCH'],
235
+ [subject, token.subject, 'NWT_SUBJECT_MISMATCH']
236
+ ]) {
237
+ if (expected !== undefined && normalizeStringClaim(expected, code) !== actual) fail(code)
238
+ }
239
+
240
+ return token
241
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -20,20 +20,30 @@
20
20
  "content-key",
21
21
  "double-dh",
22
22
  "ecdh",
23
+ "error",
24
+ "event",
23
25
  "idb",
24
26
  "idb-queue",
25
27
  "i18n",
26
28
  "index.js",
27
29
  "key",
30
+ "kind",
28
31
  "network",
32
+ "nip04",
33
+ "nip05",
29
34
  "nip19",
35
+ "nip44",
30
36
  "nip44-v3",
31
37
  "nip46",
38
+ "nip96",
39
+ "nip98",
40
+ "nwt",
32
41
  "private-channel",
33
42
  "private-message",
34
43
  "private-messenger",
35
44
  "relay",
36
45
  "temporary-storage",
46
+ "url",
37
47
  "web-storage-queue"
38
48
  ],
39
49
  "scripts": {
@@ -51,28 +61,37 @@
51
61
  "./content-key/event": "./content-key/event/index.js",
52
62
  "./double-dh": "./double-dh/index.js",
53
63
  "./ecdh": "./ecdh/index.js",
64
+ "./error": "./error/index.js",
65
+ "./event": "./event/index.js",
54
66
  "./idb": "./idb/index.js",
55
67
  "./idb-queue": "./idb-queue/index.js",
56
68
  "./i18n": "./i18n/index.js",
57
69
  "./key": "./key/index.js",
70
+ "./kind": "./kind/index.js",
58
71
  "./network": "./network/index.js",
72
+ "./nip04": "./nip04/index.js",
73
+ "./nip05": "./nip05/index.js",
59
74
  "./nip19": "./nip19/index.js",
75
+ "./nip44": "./nip44/index.js",
60
76
  "./nip44-v3": "./nip44-v3/index.js",
61
77
  "./nip46": "./nip46/index.js",
78
+ "./nip96": "./nip96/index.js",
79
+ "./nip98": "./nip98/index.js",
80
+ "./nwt": "./nwt/index.js",
62
81
  "./private-channel": "./private-channel/index.js",
63
82
  "./private-message": "./private-message/index.js",
64
83
  "./private-messenger": "./private-messenger/index.js",
65
84
  "./private-messenger/recovery": "./private-messenger/recovery/index.js",
66
85
  "./relay": "./relay/index.js",
67
86
  "./temporary-storage": "./temporary-storage/index.js",
87
+ "./url": "./url/index.js",
68
88
  "./web-storage-queue": "./web-storage-queue/index.js"
69
89
  },
70
90
  "dependencies": {
71
91
  "@noble/ciphers": "2.2.0",
72
92
  "@noble/curves": "2.2.0",
73
93
  "@noble/hashes": "2.2.0",
74
- "@scure/base": "2.0.0",
75
- "nostr-tools": "2.23.5"
94
+ "@scure/base": "2.0.0"
76
95
  },
77
96
  "devDependencies": {
78
97
  "fake-indexeddb": "6.2.5"
@@ -1,7 +1,8 @@
1
- import { generateSecretKey, getPublicKey } from 'nostr-tools'
1
+ import { generateSecretKey, getPublicKey } from '../../key/index.js'
2
2
  import { bytesToBase64, base64ToBytes } from '../../base64/index.js'
3
3
  import { bytesToHex } from '../../base16/index.js'
4
- import { verifyIykcProof } from '../../content-key/event/index.js'
4
+ import { isValidIykcProof } from '../../content-key/event/index.js'
5
+ import { ValidationError } from '../../error/index.js'
5
6
  import * as nip44v3 from '../../nip44-v3/index.js'
6
7
  import { JSONL_CHUNK_BYTES } from './chunk-size.js'
7
8
  import { ROUTER_KIND } from '../constants/index.js'
@@ -28,7 +29,7 @@ function rowTempKey (id, index) {
28
29
 
29
30
  function normalizeContentKey ({ receiverPubkey, iykcPubkey = '', iykcProof = '' } = {}) {
30
31
  if (!iykcPubkey) return { iykcPubkey: '', iykcProof: '' }
31
- if (!verifyIykcProof({ receiverPubkey, iykcPubkey, iykcProof })) throw new Error('INVALID_IYKC_PROOF')
32
+ if (!isValidIykcProof({ receiverPubkey, iykcPubkey, iykcProof })) throw new ValidationError('INVALID_IYKC_PROOF')
32
33
  return { iykcPubkey, iykcProof }
33
34
  }
34
35
 
@@ -157,7 +158,7 @@ function setPreparedRow (id, index, row, temporaryStorage) {
157
158
 
158
159
  function readPreparedRow (preparedRows, index) {
159
160
  const row = storageFor(preparedRows.temporaryStorage).getItem(rowTempKey(preparedRows.id, index))
160
- if (typeof row !== 'string') throw new Error('MISSING_PREPARED_ROW')
161
+ if (typeof row !== 'string') throw new ValidationError('MISSING_PREPARED_ROW')
161
162
  return row
162
163
  }
163
164
 
@@ -187,7 +188,7 @@ async function prepareEnvelopeRowsOnce ({ id, senderSigner, receivers, receiverC
187
188
  ciphertext = encrypted[0]
188
189
  const nextContentPubkey = encrypted[1] || ''
189
190
  if (foundOwnContentPubkey && nextContentPubkey !== usedOwnContentPubkey) {
190
- throw new Error('INCONSISTENT_IMKC_PUBKEY')
191
+ throw new ValidationError('INCONSISTENT_IMKC_PUBKEY')
191
192
  }
192
193
  foundOwnContentPubkey = true
193
194
  if (nextContentPubkey) usedOwnContentPubkey = nextContentPubkey
@@ -240,7 +241,7 @@ export function preparedRowIndexesForReceivers (preparedRows, receivers) {
240
241
  for (const receiver of receivers || []) {
241
242
  const pubkey = receiverRecord(receiver, {}).receiverPubkey
242
243
  const index = preparedRows?.receiverRowIndexesByPubkey?.[pubkey]
243
- if (!pubkey || index === undefined) throw new Error('MISSING_PREPARED_RECEIVER')
244
+ if (!pubkey || index === undefined) throw new ValidationError('MISSING_PREPARED_RECEIVER')
244
245
  if (seen.has(index)) continue
245
246
  seen.add(index)
246
247
  indexes.push(index)
@@ -1,4 +1,5 @@
1
1
  import { NYM_CARRIER_KIND, ROUTER_KIND } from '../constants/index.js'
2
+ import { ValidationError } from '../../error/index.js'
2
3
 
3
4
  const encoder = new TextEncoder()
4
5
 
@@ -16,7 +17,7 @@ export function readReceiverTag (event) {
16
17
 
17
18
  export function readSenderTag (event) {
18
19
  const senderPubkey = event.tags?.find(t => t[0] === 'f')?.[1]
19
- if (!senderPubkey) throw new Error('MISSING_SENDER_TAG')
20
+ if (!senderPubkey) throw new ValidationError('MISSING_SENDER_TAG')
20
21
  return senderPubkey
21
22
  }
22
23
 
@@ -41,7 +42,7 @@ export function readChunkTag (event) {
41
42
  const index = Number(tag?.[1])
42
43
  const total = Number(tag?.[2])
43
44
  if (!Number.isInteger(index) || !Number.isInteger(total) || index < 0 || total < 1) {
44
- throw new Error('INVALID_CHUNK_TAG')
45
+ throw new ValidationError('INVALID_CHUNK_TAG')
45
46
  }
46
47
  return { index, total }
47
48
  }
@@ -49,7 +50,7 @@ export function readChunkTag (event) {
49
50
  export function makeRouterEvent ({ pubkey, senderPubkey, imkcPubkey, imkcProof, receiverPubkey, chunkIndex, chunkTotal, content }) {
50
51
  const tags = [['f', senderPubkey]]
51
52
  if (imkcPubkey) {
52
- if (!imkcProof) throw new Error('INVALID_IMKC_PROOF')
53
+ if (!imkcProof) throw new ValidationError('INVALID_IMKC_PROOF')
53
54
  tags.push(['imkc', imkcPubkey, imkcProof])
54
55
  }
55
56
  tags.push(['c', String(chunkIndex), String(chunkTotal)])
@@ -58,7 +59,7 @@ export function makeRouterEvent ({ pubkey, senderPubkey, imkcPubkey, imkcProof,
58
59
  }
59
60
 
60
61
  export function makeNymCarrierEvent ({ innerId, chunkIndex, chunkTotal, content, createdAt = nowSeconds() }) {
61
- if (!innerId) throw new Error('INNER_EVENT_ID_REQUIRED')
62
+ if (!innerId) throw new ValidationError('INNER_EVENT_ID_REQUIRED')
62
63
  return {
63
64
  kind: NYM_CARRIER_KIND,
64
65
  created_at: createdAt,