libp2r2p 0.11.36 → 0.12.1

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,149 @@
1
+ import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'
2
+
3
+ import { bytesToHex, hexToBytes } from '../../base16/index.js'
4
+ import { ValidationError } from '../../error/index.js'
5
+ import { finalizeEvent } from '../../event/js/index.js'
6
+ import { nsecDecode, nsecEncode, npubDecode, npubEncode } from '../../nip19/index.js'
7
+
8
+ const HEX_SECKEY_REGEX = /^[0-9a-f]{64}$/i
9
+
10
+ export function validateSecretKey (secretKey) {
11
+ if (!(secretKey instanceof Uint8Array) || secretKey.length !== 32 || !secp256k1.utils.isValidSecretKey(secretKey)) {
12
+ throw new ValidationError('INVALID_SECRET_KEY')
13
+ }
14
+ }
15
+
16
+ export function generateSecretKey () {
17
+ return schnorr.utils.randomSecretKey()
18
+ }
19
+
20
+ export function getPublicKey (secretKey) {
21
+ validateSecretKey(secretKey)
22
+ return bytesToHex(schnorr.getPublicKey(secretKey))
23
+ }
24
+
25
+ export function keypairFromSecretKey (secretKey, pubkey) {
26
+ return {
27
+ secretKey,
28
+ seckey: bytesToHex(secretKey),
29
+ pubkey,
30
+ nsec: nsecEncode(bytesToHex(secretKey)),
31
+ npub: npubEncode(pubkey)
32
+ }
33
+ }
34
+
35
+ export function generateKeypair () {
36
+ const secretKey = generateSecretKey()
37
+ return keypairFromSecretKey(secretKey, getPublicKey(secretKey))
38
+ }
39
+
40
+ export function decodeSecretKey (raw) {
41
+ if (HEX_SECKEY_REGEX.test(raw)) {
42
+ return hexToBytes(raw.toLowerCase())
43
+ }
44
+ try {
45
+ return hexToBytes(nsecDecode(raw))
46
+ } catch (cause) {
47
+ throw new ValidationError('NOT_A_SECRET_KEY', { cause })
48
+ }
49
+ }
50
+
51
+ export function keypairFromSeckey (raw) {
52
+ const secretKey = decodeSecretKey(raw)
53
+ return keypairFromSecretKey(secretKey, getPublicKey(secretKey))
54
+ }
55
+
56
+ export function pubkeyFromNpub (npub) {
57
+ try {
58
+ return npubDecode(npub)
59
+ } catch (cause) {
60
+ throw new ValidationError('NOT_AN_NPUB', { cause })
61
+ }
62
+ }
63
+
64
+ export function nsecFromHex (hex) {
65
+ return nsecEncode(hex)
66
+ }
67
+
68
+ export function npubFromPubkey (pubkey) {
69
+ return npubEncode(pubkey)
70
+ }
71
+
72
+ function cleanProfileValue (value) {
73
+ return String(value ?? '').trim()
74
+ }
75
+
76
+ function profileContentFromEvent (event) {
77
+ if (!event?.content) return {}
78
+ try {
79
+ const parsed = JSON.parse(event.content)
80
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
81
+ } catch {}
82
+ return {}
83
+ }
84
+
85
+ export function profileEventTemplate ({ name = '', picture = '', profileEvent = null } = {}) {
86
+ const cleanName = cleanProfileValue(name)
87
+ const cleanPicture = cleanProfileValue(picture)
88
+ const content = profileContentFromEvent(profileEvent)
89
+
90
+ if (cleanName) content.name = cleanName
91
+ else delete content.name
92
+ if (cleanPicture) content.picture = cleanPicture
93
+ else delete content.picture
94
+
95
+ const tags = Array.isArray(profileEvent?.tags)
96
+ ? profileEvent.tags
97
+ .filter(t => Array.isArray(t) && t[0] !== 'name' && t[0] !== 'picture')
98
+ .map(t => t.slice())
99
+ : []
100
+ if (cleanName) tags.push(['name', cleanName])
101
+ if (cleanPicture) tags.push(['picture', cleanPicture])
102
+
103
+ return {
104
+ kind: 0,
105
+ created_at: Math.floor(Date.now() / 1000),
106
+ tags,
107
+ content: JSON.stringify(content)
108
+ }
109
+ }
110
+
111
+ export function signProfileEvent ({ secretKey, name = '', picture, profileEvent = null }) {
112
+ return finalizeEvent(profileEventTemplate({ name, picture, profileEvent }), secretKey)
113
+ }
114
+
115
+ export function relayListTags ({ writeRelays = [], readRelays = [] }) {
116
+ const write = new Set(writeRelays)
117
+ const read = new Set(readRelays)
118
+ const all = new Set([...write, ...read])
119
+ const tags = []
120
+ for (const url of all) {
121
+ const isWrite = write.has(url)
122
+ const isRead = read.has(url)
123
+ if (isWrite && isRead) tags.push(['r', url])
124
+ else if (isWrite) tags.push(['r', url, 'write'])
125
+ else tags.push(['r', url, 'read'])
126
+ }
127
+ return tags
128
+ }
129
+
130
+ export function signRelayListEvent ({ secretKey, writeRelays = [], readRelays = [] }) {
131
+ return finalizeEvent({
132
+ kind: 10002,
133
+ created_at: Math.floor(Date.now() / 1000),
134
+ tags: relayListTags({ writeRelays, readRelays }),
135
+ content: ''
136
+ }, secretKey)
137
+ }
138
+
139
+ export function parseProfileEvent (event) {
140
+ if (!event || event.kind !== 0) return { name: '', picture: '' }
141
+ let parsed = {}
142
+ try { parsed = JSON.parse(event.content) } catch { parsed = {} }
143
+ const fromTag = name => event.tags.find(t => t[0] === name)?.[1]
144
+ return {
145
+ name: (fromTag('name') || parsed.name || parsed.display_name || '').trim(),
146
+ about: (parsed.about || '').trim(),
147
+ picture: (fromTag('picture') || parsed.picture || '').trim()
148
+ }
149
+ }
package/key/index.js CHANGED
@@ -1,114 +1,74 @@
1
- import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'
2
-
3
- import { bytesToHex, hexToBytes } from '../base16/index.js'
4
- import { ValidationError } from '../error/index.js'
1
+ import { bytesToBase16 } from '../base16/index.js'
5
2
  import { finalizeEvent } from '../event/index.js'
6
- import { nsecDecode, nsecEncode, npubDecode, npubEncode } from '../nip19/index.js'
7
-
8
- const HEX_SECKEY_REGEX = /^[0-9a-f]{64}$/i
9
-
10
- export function generateSecretKey () {
11
- return schnorr.utils.randomSecretKey()
3
+ import {
4
+ getReadyEventCryptoBackend,
5
+ markEventCryptoUnavailable,
6
+ scheduleEventCryptoInitialization
7
+ } from '../helpers/secp256k1-wasm/backend.js'
8
+ import {
9
+ decodeSecretKey,
10
+ generateKeypair as generateKeypairWithJavaScript,
11
+ generateSecretKey,
12
+ getPublicKey as getPublicKeyWithJavaScript,
13
+ keypairFromSecretKey,
14
+ keypairFromSeckey as keypairFromSeckeyWithJavaScript,
15
+ npubFromPubkey,
16
+ nsecFromHex,
17
+ parseProfileEvent,
18
+ profileEventTemplate,
19
+ pubkeyFromNpub,
20
+ relayListTags,
21
+ validateSecretKey
22
+ } from './helpers/core.js'
23
+
24
+ export {
25
+ generateSecretKey,
26
+ npubFromPubkey,
27
+ nsecFromHex,
28
+ parseProfileEvent,
29
+ profileEventTemplate,
30
+ pubkeyFromNpub
12
31
  }
13
32
 
14
33
  export function getPublicKey (secretKey) {
15
- if (!(secretKey instanceof Uint8Array) || secretKey.length !== 32 || !secp256k1.utils.isValidSecretKey(secretKey)) {
16
- throw new ValidationError('INVALID_SECRET_KEY')
34
+ validateSecretKey(secretKey)
35
+ scheduleEventCryptoInitialization()
36
+ const backend = getReadyEventCryptoBackend()
37
+ if (!backend) return getPublicKeyWithJavaScript(secretKey)
38
+ try {
39
+ return bytesToBase16(backend.getPublicKey(secretKey))
40
+ } catch {
41
+ markEventCryptoUnavailable()
42
+ return getPublicKeyWithJavaScript(secretKey)
17
43
  }
18
- return bytesToHex(schnorr.getPublicKey(secretKey))
19
44
  }
20
45
 
21
46
  export function generateKeypair () {
22
- const secretKey = generateSecretKey()
23
- const pubkey = getPublicKey(secretKey)
24
- return {
25
- secretKey,
26
- seckey: bytesToHex(secretKey),
27
- pubkey,
28
- nsec: nsecEncode(bytesToHex(secretKey)),
29
- npub: npubEncode(pubkey)
30
- }
31
- }
32
-
33
- // Accepts either an `nsec1...` bech32 string or a 64-char hex secret key.
34
- // Returns both encodings so callers can store the hex form and display the
35
- // nsec form without re-decoding.
36
- export function keypairFromSeckey (raw) {
37
- let secretKey
38
- if (HEX_SECKEY_REGEX.test(raw)) {
39
- secretKey = hexToBytes(raw.toLowerCase())
40
- } else {
41
- try {
42
- secretKey = hexToBytes(nsecDecode(raw))
43
- } catch (cause) {
44
- throw new ValidationError('NOT_A_SECRET_KEY', { cause })
45
- }
46
- }
47
- const pubkey = getPublicKey(secretKey)
48
- return {
49
- secretKey,
50
- seckey: bytesToHex(secretKey),
51
- pubkey,
52
- nsec: nsecEncode(bytesToHex(secretKey)),
53
- npub: npubEncode(pubkey)
54
- }
55
- }
47
+ scheduleEventCryptoInitialization()
48
+ const backend = getReadyEventCryptoBackend()
49
+ if (!backend) return generateKeypairWithJavaScript()
56
50
 
57
- // npub-only: hex pubkeys are not accepted for read-only imports because
58
- // bech32 includes a checksum that protects against user-typo imports of a
59
- // pubkey they cannot sign for.
60
- export function pubkeyFromNpub (npub) {
61
51
  try {
62
- return npubDecode(npub)
63
- } catch (cause) {
64
- throw new ValidationError('NOT_AN_NPUB', { cause })
52
+ const secretKey = generateSecretKey()
53
+ return keypairFromSecretKey(secretKey, bytesToBase16(backend.getPublicKey(secretKey)))
54
+ } catch {
55
+ markEventCryptoUnavailable()
56
+ return generateKeypairWithJavaScript()
65
57
  }
66
58
  }
67
59
 
68
- export function nsecFromHex (hex) {
69
- return nsecEncode(hex)
70
- }
71
-
72
- export function npubFromPubkey (pubkey) {
73
- return npubEncode(pubkey)
74
- }
75
-
76
- function cleanProfileValue (value) {
77
- return String(value ?? '').trim()
78
- }
60
+ export function keypairFromSeckey (raw) {
61
+ scheduleEventCryptoInitialization()
62
+ const backend = getReadyEventCryptoBackend()
63
+ if (!backend) return keypairFromSeckeyWithJavaScript(raw)
79
64
 
80
- function profileContentFromEvent (event) {
81
- if (!event?.content) return {}
82
65
  try {
83
- const parsed = JSON.parse(event.content)
84
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
85
- } catch {}
86
- return {}
87
- }
88
-
89
- export function profileEventTemplate ({ name = '', picture = '', profileEvent = null } = {}) {
90
- const cleanName = cleanProfileValue(name)
91
- const cleanPicture = cleanProfileValue(picture)
92
- const content = profileContentFromEvent(profileEvent)
93
-
94
- if (cleanName) content.name = cleanName
95
- else delete content.name
96
- if (cleanPicture) content.picture = cleanPicture
97
- else delete content.picture
98
-
99
- const tags = Array.isArray(profileEvent?.tags)
100
- ? profileEvent.tags
101
- .filter(t => Array.isArray(t) && t[0] !== 'name' && t[0] !== 'picture')
102
- .map(t => t.slice())
103
- : []
104
- if (cleanName) tags.push(['name', cleanName])
105
- if (cleanPicture) tags.push(['picture', cleanPicture])
106
-
107
- return {
108
- kind: 0,
109
- created_at: Math.floor(Date.now() / 1000),
110
- tags,
111
- content: JSON.stringify(content)
66
+ const secretKey = decodeSecretKey(raw)
67
+ return keypairFromSecretKey(secretKey, bytesToBase16(backend.getPublicKey(secretKey)))
68
+ } catch (error) {
69
+ if (error?.code) throw error
70
+ markEventCryptoUnavailable()
71
+ return keypairFromSeckeyWithJavaScript(raw)
112
72
  }
113
73
  }
114
74
 
@@ -116,37 +76,11 @@ export function signProfileEvent ({ secretKey, name = '', picture, profileEvent
116
76
  return finalizeEvent(profileEventTemplate({ name, picture, profileEvent }), secretKey)
117
77
  }
118
78
 
119
- // Signs a NIP-65 kind:10002 relay-list event. Each URL present in both
120
- // `writeRelays` and `readRelays` is emitted as a bare `['r', url]` tag;
121
- // otherwise it gets the explicit `'read'` or `'write'` marker.
122
79
  export function signRelayListEvent ({ secretKey, writeRelays = [], readRelays = [] }) {
123
- const write = new Set(writeRelays)
124
- const read = new Set(readRelays)
125
- const all = new Set([...write, ...read])
126
- const tags = []
127
- for (const url of all) {
128
- const isWrite = write.has(url)
129
- const isRead = read.has(url)
130
- if (isWrite && isRead) tags.push(['r', url])
131
- else if (isWrite) tags.push(['r', url, 'write'])
132
- else tags.push(['r', url, 'read'])
133
- }
134
80
  return finalizeEvent({
135
81
  kind: 10002,
136
82
  created_at: Math.floor(Date.now() / 1000),
137
- tags,
83
+ tags: relayListTags({ writeRelays, readRelays }),
138
84
  content: ''
139
85
  }, secretKey)
140
86
  }
141
-
142
- export function parseProfileEvent (event) {
143
- if (!event || event.kind !== 0) return { name: '', picture: '' }
144
- let parsed = {}
145
- try { parsed = JSON.parse(event.content) } catch { parsed = {} }
146
- const fromTag = name => event.tags.find(t => t[0] === name)?.[1]
147
- return {
148
- name: (fromTag('name') || parsed.name || parsed.display_name || '').trim(),
149
- about: (parsed.about || '').trim(),
150
- picture: (fromTag('picture') || parsed.picture || '').trim()
151
- }
152
- }
@@ -0,0 +1,13 @@
1
+ export {
2
+ generateKeypair,
3
+ generateSecretKey,
4
+ getPublicKey,
5
+ keypairFromSeckey,
6
+ npubFromPubkey,
7
+ nsecFromHex,
8
+ parseProfileEvent,
9
+ profileEventTemplate,
10
+ pubkeyFromNpub,
11
+ signProfileEvent,
12
+ signRelayListEvent
13
+ } from '../helpers/core.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.11.36",
3
+ "version": "0.12.1",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -46,10 +46,13 @@
46
46
  "private-messenger",
47
47
  "relay",
48
48
  "temporary-storage",
49
+ "THIRD_PARTY_NOTICES.md",
49
50
  "url",
50
51
  "web-storage-queue"
51
52
  ],
52
53
  "scripts": {
54
+ "benchmark:event": "node --expose-gc benchmark/event-crypto.js",
55
+ "build:wasm": "node wasm/build.js",
53
56
  "test": "node --test --experimental-test-module-mocks 'tests/**/*.test.js'",
54
57
  "test:files": "node --test --experimental-test-module-mocks",
55
58
  "test:only": "node --test --test-only --experimental-test-module-mocks 'tests/**/*.test.js'"
@@ -67,10 +70,12 @@
67
70
  "./ecdh": "./ecdh/index.js",
68
71
  "./error": "./error/index.js",
69
72
  "./event": "./event/index.js",
73
+ "./event/js": "./event/js/index.js",
70
74
  "./idb": "./idb/index.js",
71
75
  "./idb-queue": "./idb-queue/index.js",
72
76
  "./irfs": "./irfs/index.js",
73
77
  "./key": "./key/index.js",
78
+ "./key/js": "./key/js/index.js",
74
79
  "./kind": "./kind/index.js",
75
80
  "./network": "./network/index.js",
76
81
  "./nip04": "./nip04/index.js",
@@ -105,6 +110,7 @@
105
110
  },
106
111
  "devDependencies": {
107
112
  "eslint": "^9.39.5",
113
+ "esbuild": "^0.28.2",
108
114
  "fake-indexeddb": "6.2.5",
109
115
  "globals": "^17.8.0",
110
116
  "neostandard": "^0.13.0"
@@ -0,0 +1,12 @@
1
+ import { ValidationError } from '../../error/index.js'
2
+
3
+ export function normalizePreparedPublications ({ preparedPublication, preparedPublications } = {}) {
4
+ if (preparedPublication !== undefined) throw new ValidationError('LEGACY_PREPARED_PUBLICATION')
5
+ if (preparedPublications !== undefined) {
6
+ if (!Array.isArray(preparedPublications) || preparedPublications.length === 0) {
7
+ throw new ValidationError('INVALID_PREPARED_PUBLICATIONS')
8
+ }
9
+ return preparedPublications
10
+ }
11
+ return []
12
+ }