libp2r2p 0.11.35 → 0.12.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
@@ -765,6 +765,54 @@ import {
765
765
  } from 'libp2r2p/url'
766
766
  ```
767
767
 
768
+ Event hashing, signing, verification and public-key derivation use a bundled
769
+ libsecp256k1 WebAssembly backend after it becomes ready. Initialization is lazy:
770
+ the first operation uses JavaScript and starts the WASM compiler in the
771
+ background, so existing synchronous calls do not change. To use WASM for the
772
+ first operation, await the optional initializer:
773
+
774
+ ```js
775
+ import {
776
+ finalizeEvent,
777
+ initializeEventCrypto
778
+ } from 'libp2r2p/event'
779
+
780
+ const backend = await initializeEventCrypto() // 'wasm' or 'javascript'
781
+ const event = finalizeEvent(template, secretKey)
782
+ ```
783
+
784
+ Inaccessible WebAssembly, CSP restrictions and backend failures fall back to
785
+ JavaScript. Disable the backend for the current JavaScript realm with
786
+ `configureEventCrypto({ wasm: false })`. This runtime option does not reduce the
787
+ bundle. Applications that want to exclude the WASM artifact entirely should use
788
+ the JS-only event and key entrypoints:
789
+
790
+ ```js
791
+ import { finalizeEvent, isValidEvent } from 'libp2r2p/event/js'
792
+ import { getPublicKey } from 'libp2r2p/key/js'
793
+ ```
794
+
795
+ `generateSecretKey()` always uses JavaScript and does not initialize WASM.
796
+ Maintainers can rebuild the checked-in WASM module with `npm run build:wasm`;
797
+ see the
798
+ [WASM build guide](https://github.com/arthurfranca/libp2r2p/blob/main/wasm/README.md).
799
+ Run the non-gating comparison against the JavaScript backend with
800
+ `npm run benchmark:event`.
801
+
802
+ Median speedup after explicit WASM initialization, measured with Node.js 24 on
803
+ an AMD Ryzen 5 5600G:
804
+
805
+ | Operation | 8 KiB | 256 KiB | 1 MiB |
806
+ | --- | ---: | ---: | ---: |
807
+ | `isValidEvent` | 5.83× | 2.58× | 1.94× |
808
+ | `finalizeEvent` | 13.64× | 3.11× | 2.06× |
809
+ | `getEventHash` | 1.27× | 1.60× | 1.59× |
810
+ | `getPublicKey` | 3.61× | — | — |
811
+
812
+ These numbers compare complete public operations against the same methods from
813
+ `event/js` and `key/js`. They are an indicative local measurement, not a CI
814
+ performance gate.
815
+
768
816
  Blossom server normalization accepts root `http:` origins for local
769
817
  development. Public Blossom validation additionally requires `https:` and a
770
818
  public host, and rejects credentials, paths, query strings and fragments.
@@ -1307,3 +1355,7 @@ outbox checkpoint run concurrently. The entry remains hidden and ineligible for
1307
1355
  saved/publication/cleanup until admission; remote entries keep checkpoint-first
1308
1356
  ordering. A failed local commit after admission remains retryable through the
1309
1357
  existing outbox path.
1358
+
1359
+ The 0.11.36 kind registry adds `CONTACT_LIST = 13300` as a replaceable,
1360
+ project-specific contact list used only inside private personal copies.
1361
+ `FOLLOW_SET = 30000` remains the addressable NIP-02-style custom follow set.
@@ -0,0 +1,46 @@
1
+ # Third-party notices
2
+
3
+ The generated WebAssembly module includes libsecp256k1:
4
+
5
+ Copyright (c) 2013 Pieter Wuille
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
24
+
25
+ The build and JavaScript wrapper are derived from nostr-wasm by fiatjaf:
26
+ https://github.com/fiatjaf/nostr-wasm
27
+
28
+ Copyright (c) 2023 fiatjaf
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ of this software and associated documentation files (the "Software"), to deal
32
+ in the Software without restriction, including without limitation the rights
33
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ copies of the Software, and to permit persons to whom the Software is
35
+ furnished to do so, subject to the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be included in
38
+ all copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46
+ THE SOFTWARE.
@@ -0,0 +1,120 @@
1
+ import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'
2
+ import { sha256 } from '@noble/hashes/sha2.js'
3
+
4
+ import { bytesToBase16, base16ToBytes } from '../../base16/index.js'
5
+ import { ValidationError } from '../../error/index.js'
6
+ import {
7
+ isAddressableKind,
8
+ isEphemeralKind,
9
+ isRegularKind,
10
+ isReplaceableKind
11
+ } from '../../kind/index.js'
12
+ import {
13
+ assertSerializableEvent,
14
+ isSerializableEvent,
15
+ serializeEvent,
16
+ serializableEventError
17
+ } from './serialize.js'
18
+
19
+ const HEX_32 = /^[0-9a-f]{64}$/
20
+ const HEX_64 = /^[0-9a-f]{128}$/
21
+ const textEncoder = new TextEncoder()
22
+
23
+ export function copyTags (tags) {
24
+ return Array.isArray(tags) ? tags.map(tag => Array.isArray(tag) ? tag.slice() : tag) : tags
25
+ }
26
+
27
+ export function validateSecretKey (secretKey) {
28
+ if (!(secretKey instanceof Uint8Array) || secretKey.length !== 32 || !secp256k1.utils.isValidSecretKey(secretKey)) {
29
+ throw new ValidationError('INVALID_SECRET_KEY')
30
+ }
31
+ }
32
+
33
+ export function getEventHash (event) {
34
+ return bytesToBase16(sha256(textEncoder.encode(serializeEvent(event))))
35
+ }
36
+
37
+ export function finalizeEvent (template, secretKey) {
38
+ validateSecretKey(secretKey)
39
+ const pubkey = bytesToBase16(schnorr.getPublicKey(secretKey))
40
+ const event = { ...template, tags: copyTags(template?.tags), pubkey }
41
+ const id = getEventHash(event)
42
+ return { ...event, id, sig: bytesToBase16(schnorr.sign(base16ToBytes(id), secretKey)) }
43
+ }
44
+
45
+ export function eventValidationError (event) {
46
+ const structureError = serializableEventError(event)
47
+ if (structureError) return structureError
48
+ if (typeof event.id !== 'string' || !HEX_32.test(event.id)) return 'INVALID_EVENT_ID'
49
+ if (typeof event.sig !== 'string' || !HEX_64.test(event.sig)) return 'INVALID_EVENT_SIGNATURE'
50
+ try {
51
+ const id = getEventHash(event)
52
+ if (id !== event.id) return 'EVENT_ID_MISMATCH'
53
+ if (!schnorr.verify(base16ToBytes(event.sig), base16ToBytes(id), base16ToBytes(event.pubkey))) {
54
+ return 'INVALID_EVENT_SIGNATURE'
55
+ }
56
+ } catch {
57
+ return 'INVALID_EVENT_SIGNATURE'
58
+ }
59
+ return null
60
+ }
61
+
62
+ export function isValidEvent (event) {
63
+ return eventValidationError(event) === null
64
+ }
65
+
66
+ export function assertValidEvent (event) {
67
+ const code = eventValidationError(event)
68
+ if (code) throw new ValidationError(code)
69
+ return event
70
+ }
71
+
72
+ function hasTagDefinedClassification (event, classification) {
73
+ if (!event || typeof event !== 'object' || !Array.isArray(event.tags)) return false
74
+
75
+ if (classification === 'ephemeral') {
76
+ if (!Number.isSafeInteger(event.created_at) || event.created_at < 0) return false
77
+ const timestamp = String(event.created_at)
78
+ return event.tags.some(tag => Array.isArray(tag) && tag[0] === 'expiration' && tag[1] === timestamp)
79
+ }
80
+
81
+ const dTag = event.tags.find(tag => Array.isArray(tag) && tag[0] === 'd')
82
+ if (typeof dTag?.[1] !== 'string') return false
83
+ if (classification === 'replaceable') return dTag[1] === ''
84
+ if (classification === 'addressable') return dTag[1] !== ''
85
+ return false
86
+ }
87
+
88
+ export function classifyEvent (event, { includeLegacyKindRanges = true } = {}) {
89
+ if (!event || typeof event !== 'object') return []
90
+ const options = { includeLegacyKindRanges }
91
+ return [
92
+ ['regular', isRegularEvent],
93
+ ['replaceable', isReplaceableEvent],
94
+ ['ephemeral', isEphemeralEvent],
95
+ ['addressable', isAddressableEvent]
96
+ ].filter(([, predicate]) => predicate(event, options)).map(([classification]) => classification)
97
+ }
98
+
99
+ export function isRegularEvent (event, options) {
100
+ if (!event || typeof event !== 'object') return false
101
+ return (options?.includeLegacyKindRanges !== false && isRegularKind(event.kind)) ||
102
+ (!isReplaceableEvent(event, options) && !isAddressableEvent(event, options))
103
+ }
104
+
105
+ export function isReplaceableEvent (event, options) {
106
+ return (options?.includeLegacyKindRanges !== false && isReplaceableKind(event?.kind)) ||
107
+ hasTagDefinedClassification(event, 'replaceable')
108
+ }
109
+
110
+ export function isEphemeralEvent (event, options) {
111
+ return (options?.includeLegacyKindRanges !== false && isEphemeralKind(event?.kind)) ||
112
+ hasTagDefinedClassification(event, 'ephemeral')
113
+ }
114
+
115
+ export function isAddressableEvent (event, options) {
116
+ return (options?.includeLegacyKindRanges !== false && isAddressableKind(event?.kind)) ||
117
+ hasTagDefinedClassification(event, 'addressable')
118
+ }
119
+
120
+ export { assertSerializableEvent, isSerializableEvent }
@@ -29,3 +29,59 @@ export function serializeEvent (event) {
29
29
  assertSerializableEvent(event)
30
30
  return JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content])
31
31
  }
32
+
33
+ function isHighSurrogate (value) {
34
+ return value >= 0xd800 && value <= 0xdbff
35
+ }
36
+
37
+ function isLowSurrogate (value) {
38
+ return value >= 0xdc00 && value <= 0xdfff
39
+ }
40
+
41
+ function chunkEnd (value, start, chunkSize) {
42
+ let end = Math.min(start + Math.max(2, chunkSize), value.length)
43
+ if (
44
+ end < value.length &&
45
+ end > start + 1 &&
46
+ isHighSurrogate(value.charCodeAt(end - 1)) &&
47
+ isLowSurrogate(value.charCodeAt(end))
48
+ ) {
49
+ end--
50
+ }
51
+ return end
52
+ }
53
+
54
+ function * serializeJsonString (value, chunkSize) {
55
+ yield '"'
56
+ for (let start = 0; start < value.length;) {
57
+ const end = chunkEnd(value, start, chunkSize)
58
+ const serialized = JSON.stringify(value.slice(start, end))
59
+ yield serialized.slice(1, -1)
60
+ start = end
61
+ }
62
+ yield '"'
63
+ }
64
+
65
+ /**
66
+ * Produces the exact same text as serializeEvent() without constructing the
67
+ * complete serialized event in one JavaScript string.
68
+ */
69
+ export function * serializeEventChunks (event, { chunkSize = 4 * 1024, validate = true } = {}) {
70
+ if (validate) assertSerializableEvent(event)
71
+ yield `[0,"${event.pubkey}",${event.created_at},${event.kind},[`
72
+
73
+ for (let tagIndex = 0; tagIndex < event.tags.length; tagIndex++) {
74
+ if (tagIndex) yield ','
75
+ yield '['
76
+ const tag = event.tags[tagIndex]
77
+ for (let valueIndex = 0; valueIndex < tag.length; valueIndex++) {
78
+ if (valueIndex) yield ','
79
+ yield * serializeJsonString(tag[valueIndex], chunkSize)
80
+ }
81
+ yield ']'
82
+ }
83
+
84
+ yield '],'
85
+ yield * serializeJsonString(event.content, chunkSize)
86
+ yield ']'
87
+ }
package/event/index.js CHANGED
@@ -1,43 +1,89 @@
1
- import { schnorr, secp256k1 } from '@noble/curves/secp256k1.js'
2
- import { sha256 } from '@noble/hashes/sha2.js'
3
-
4
- import { bytesToBase16, base16ToBytes } from '../base16/index.js'
1
+ import { base16ToBytes, bytesToBase16 } from '../base16/index.js'
5
2
  import { ValidationError } from '../error/index.js'
6
3
  import {
7
- isAddressableKind,
8
- isEphemeralKind,
9
- isRegularKind,
10
- isReplaceableKind
11
- } from '../kind/index.js'
4
+ configureEventCrypto,
5
+ getReadyEventCryptoBackend,
6
+ initializeEventCrypto,
7
+ markEventCryptoUnavailable,
8
+ scheduleEventCryptoInitialization
9
+ } from '../helpers/secp256k1-wasm/backend.js'
10
+ import {
11
+ copyTags,
12
+ eventValidationError as eventValidationErrorWithJavaScript,
13
+ finalizeEvent as finalizeEventWithJavaScript,
14
+ getEventHash as getEventHashWithJavaScript,
15
+ validateSecretKey
16
+ } from './helpers/core.js'
12
17
  import {
13
18
  assertSerializableEvent,
14
19
  isSerializableEvent,
15
- serializeEvent,
16
20
  serializableEventError
17
21
  } from './helpers/serialize.js'
18
22
 
23
+ export {
24
+ classifyEvent,
25
+ isAddressableEvent,
26
+ isEphemeralEvent,
27
+ isRegularEvent,
28
+ isReplaceableEvent
29
+ } from './helpers/core.js'
30
+ export { assertSerializableEvent, isSerializableEvent }
31
+ export { configureEventCrypto, initializeEventCrypto }
32
+
19
33
  const HEX_32 = /^[0-9a-f]{64}$/
20
34
  const HEX_64 = /^[0-9a-f]{128}$/
21
- const textEncoder = new TextEncoder()
22
35
 
23
- function copyTags (tags) {
24
- return Array.isArray(tags) ? tags.map(tag => Array.isArray(tag) ? tag.slice() : tag) : tags
36
+ function useWasmOrFallback (operation, fallback) {
37
+ scheduleEventCryptoInitialization()
38
+ const backend = getReadyEventCryptoBackend()
39
+ if (!backend) return fallback()
40
+ try {
41
+ return operation(backend)
42
+ } catch {
43
+ markEventCryptoUnavailable()
44
+ return fallback()
45
+ }
25
46
  }
26
47
 
27
- export { assertSerializableEvent, isSerializableEvent }
28
-
29
48
  export function getEventHash (event) {
30
- return bytesToBase16(sha256(textEncoder.encode(serializeEvent(event))))
49
+ assertSerializableEvent(event)
50
+ return useWasmOrFallback(
51
+ backend => bytesToBase16(backend.hashEvent(event)),
52
+ () => getEventHashWithJavaScript(event)
53
+ )
31
54
  }
32
55
 
33
56
  export function finalizeEvent (template, secretKey) {
34
- if (!(secretKey instanceof Uint8Array) || secretKey.length !== 32 || !secp256k1.utils.isValidSecretKey(secretKey)) {
35
- throw new ValidationError('INVALID_SECRET_KEY')
57
+ validateSecretKey(secretKey)
58
+ const event = {
59
+ ...template,
60
+ tags: copyTags(template?.tags),
61
+ pubkey: '0'.repeat(64)
36
62
  }
37
- const pubkey = bytesToBase16(schnorr.getPublicKey(secretKey))
38
- const event = { ...template, tags: copyTags(template?.tags), pubkey }
39
- const id = getEventHash(event)
40
- return { ...event, id, sig: bytesToBase16(schnorr.sign(base16ToBytes(id), secretKey)) }
63
+ assertSerializableEvent(event)
64
+ return useWasmOrFallback(
65
+ backend => {
66
+ const entropy = crypto.getRandomValues(new Uint8Array(32))
67
+ let id
68
+ const { publicKey, signature } = backend.signEventHash(
69
+ secretKey,
70
+ publicKeyBytes => {
71
+ event.pubkey = bytesToBase16(publicKeyBytes)
72
+ const hash = backend.hashEvent(event)
73
+ id = bytesToBase16(hash)
74
+ return hash
75
+ },
76
+ entropy
77
+ )
78
+ return {
79
+ ...event,
80
+ pubkey: bytesToBase16(publicKey),
81
+ id,
82
+ sig: bytesToBase16(signature)
83
+ }
84
+ },
85
+ () => finalizeEventWithJavaScript(template, secretKey)
86
+ )
41
87
  }
42
88
 
43
89
  function eventValidationError (event) {
@@ -45,16 +91,25 @@ function eventValidationError (event) {
45
91
  if (structureError) return structureError
46
92
  if (typeof event.id !== 'string' || !HEX_32.test(event.id)) return 'INVALID_EVENT_ID'
47
93
  if (typeof event.sig !== 'string' || !HEX_64.test(event.sig)) return 'INVALID_EVENT_SIGNATURE'
94
+
95
+ scheduleEventCryptoInitialization()
96
+ const backend = getReadyEventCryptoBackend()
97
+ if (!backend) return eventValidationErrorWithJavaScript(event)
48
98
  try {
49
- const id = getEventHash(event)
99
+ const id = bytesToBase16(backend.hashEvent(event))
50
100
  if (id !== event.id) return 'EVENT_ID_MISMATCH'
51
- if (!schnorr.verify(base16ToBytes(event.sig), base16ToBytes(id), base16ToBytes(event.pubkey))) {
101
+ if (!backend.verifyHash(
102
+ base16ToBytes(event.sig),
103
+ base16ToBytes(id),
104
+ base16ToBytes(event.pubkey)
105
+ )) {
52
106
  return 'INVALID_EVENT_SIGNATURE'
53
107
  }
108
+ return null
54
109
  } catch {
55
- return 'INVALID_EVENT_SIGNATURE'
110
+ markEventCryptoUnavailable()
111
+ return eventValidationErrorWithJavaScript(event)
56
112
  }
57
- return null
58
113
  }
59
114
 
60
115
  export function isValidEvent (event) {
@@ -66,51 +121,3 @@ export function assertValidEvent (event) {
66
121
  if (code) throw new ValidationError(code)
67
122
  return event
68
123
  }
69
-
70
- function hasTagDefinedClassification (event, classification) {
71
- if (!event || typeof event !== 'object' || !Array.isArray(event.tags)) return false
72
-
73
- if (classification === 'ephemeral') {
74
- if (!Number.isSafeInteger(event.created_at) || event.created_at < 0) return false
75
- const timestamp = String(event.created_at)
76
- return event.tags.some(tag => Array.isArray(tag) && tag[0] === 'expiration' && tag[1] === timestamp)
77
- }
78
-
79
- const dTag = event.tags.find(tag => Array.isArray(tag) && tag[0] === 'd')
80
- if (typeof dTag?.[1] !== 'string') return false
81
- if (classification === 'replaceable') return dTag[1] === ''
82
- if (classification === 'addressable') return dTag[1] !== ''
83
- return false
84
- }
85
-
86
- export function classifyEvent (event, { includeLegacyKindRanges = true } = {}) {
87
- if (!event || typeof event !== 'object') return []
88
- const options = { includeLegacyKindRanges }
89
- return [
90
- ['regular', isRegularEvent],
91
- ['replaceable', isReplaceableEvent],
92
- ['ephemeral', isEphemeralEvent],
93
- ['addressable', isAddressableEvent]
94
- ].filter(([, predicate]) => predicate(event, options)).map(([classification]) => classification)
95
- }
96
-
97
- export function isRegularEvent (event, options) {
98
- if (!event || typeof event !== 'object') return false
99
- return (options?.includeLegacyKindRanges !== false && isRegularKind(event.kind)) ||
100
- (!isReplaceableEvent(event, options) && !isAddressableEvent(event, options))
101
- }
102
-
103
- export function isReplaceableEvent (event, options) {
104
- return (options?.includeLegacyKindRanges !== false && isReplaceableKind(event?.kind)) ||
105
- hasTagDefinedClassification(event, 'replaceable')
106
- }
107
-
108
- export function isEphemeralEvent (event, options) {
109
- return (options?.includeLegacyKindRanges !== false && isEphemeralKind(event?.kind)) ||
110
- hasTagDefinedClassification(event, 'ephemeral')
111
- }
112
-
113
- export function isAddressableEvent (event, options) {
114
- return (options?.includeLegacyKindRanges !== false && isAddressableKind(event?.kind)) ||
115
- hasTagDefinedClassification(event, 'addressable')
116
- }
@@ -0,0 +1,13 @@
1
+ export {
2
+ assertSerializableEvent,
3
+ assertValidEvent,
4
+ classifyEvent,
5
+ finalizeEvent,
6
+ getEventHash,
7
+ isAddressableEvent,
8
+ isEphemeralEvent,
9
+ isRegularEvent,
10
+ isReplaceableEvent,
11
+ isSerializableEvent,
12
+ isValidEvent
13
+ } from '../helpers/core.js'
@@ -0,0 +1,99 @@
1
+ import { ValidationError } from '../../error/index.js'
2
+ import { createSecp256k1WasmBackend } from './binding.js'
3
+
4
+ const SELF_TEST_SECRET = new Uint8Array(32).fill(7)
5
+ const SELF_TEST_PUBLIC_KEY =
6
+ '989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f'
7
+ const SELF_TEST_HASH = new Uint8Array(32).fill(9)
8
+ const SELF_TEST_ENTROPY = new Uint8Array(32).fill(11)
9
+
10
+ let enabled = true
11
+ let state = 'idle'
12
+ let backend = null
13
+ let pending = null
14
+ let generation = 0
15
+
16
+ const toHex = bytes => Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')
17
+
18
+ async function initialize (forceRetry) {
19
+ if (!enabled) return 'javascript'
20
+ if (backend) return 'wasm'
21
+ if (pending) return pending
22
+ if (state === 'unavailable' && !forceRetry) return 'javascript'
23
+
24
+ const currentGeneration = generation
25
+ state = 'initializing'
26
+ pending = (async () => {
27
+ let candidate
28
+ try {
29
+ candidate = await createSecp256k1WasmBackend()
30
+ const publicKey = toHex(candidate.getPublicKey(SELF_TEST_SECRET))
31
+ const signature = candidate.signHash(
32
+ SELF_TEST_HASH,
33
+ SELF_TEST_SECRET,
34
+ SELF_TEST_ENTROPY
35
+ )
36
+ if (
37
+ publicKey !== SELF_TEST_PUBLIC_KEY ||
38
+ !candidate.verifyHash(signature, SELF_TEST_HASH, candidate.getPublicKey(SELF_TEST_SECRET))
39
+ ) {
40
+ throw Error('secp256k1 WASM self-test failed')
41
+ }
42
+ if (!enabled || currentGeneration !== generation) {
43
+ candidate.dispose()
44
+ return 'javascript'
45
+ }
46
+ backend = candidate
47
+ state = 'ready'
48
+ return 'wasm'
49
+ } catch {
50
+ candidate?.dispose()
51
+ if (enabled && currentGeneration === generation) state = 'unavailable'
52
+ return 'javascript'
53
+ } finally {
54
+ pending = null
55
+ }
56
+ })()
57
+ return pending
58
+ }
59
+
60
+ export function configureEventCrypto (options = {}) {
61
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
62
+ throw new ValidationError('INVALID_EVENT_CRYPTO_OPTIONS')
63
+ }
64
+ if (options.wasm !== undefined && typeof options.wasm !== 'boolean') {
65
+ throw new ValidationError('INVALID_EVENT_CRYPTO_OPTIONS')
66
+ }
67
+ const nextEnabled = options.wasm !== false
68
+ if (nextEnabled === enabled) return
69
+
70
+ enabled = nextEnabled
71
+ generation++
72
+ if (!enabled) {
73
+ backend?.dispose()
74
+ backend = null
75
+ state = 'disabled'
76
+ } else {
77
+ state = 'idle'
78
+ }
79
+ }
80
+
81
+ export function initializeEventCrypto () {
82
+ return initialize(true)
83
+ }
84
+
85
+ export function scheduleEventCryptoInitialization () {
86
+ if (!enabled || backend || pending || state === 'unavailable') return
87
+ initialize(false).catch(() => {})
88
+ }
89
+
90
+ export function getReadyEventCryptoBackend () {
91
+ return backend
92
+ }
93
+
94
+ export function markEventCryptoUnavailable () {
95
+ if (!backend) return
96
+ backend.dispose()
97
+ backend = null
98
+ state = 'unavailable'
99
+ }