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.
package/README.md CHANGED
@@ -358,6 +358,13 @@ By default, each high-level private-message send creates one fresh deletion
358
358
  keypair for its logical message. Every outer kind `3560` event produced for that
359
359
  send, including router chunks, recipient subsets, and nym carriers, carries the
360
360
  same public key in its `s` tag. The result always contains `delivery.reports`.
361
+ Each routed report also identifies its exact outer event in `outerEventId` and
362
+ the encrypted recipient subset in `receiverPubkeys`. The same recipient can
363
+ appear in multiple reports when a message spans chunks or recipient groups; it
364
+ is fully covered only when every report containing it succeeds. Fixed relay
365
+ lists publish one shared recipient set, while explicit `relayToReceivers` maps
366
+ and automatic NIP-65 routing can produce partial recipient coverage. Nym uses
367
+ one carrier union and does not expose per-recipient partial delivery.
361
368
  When libp2r2p generated the keypair, it also contains
362
369
  `delivery.deletionSeckey`; the public key can be derived from that secret.
363
370
 
@@ -765,6 +772,54 @@ import {
765
772
  } from 'libp2r2p/url'
766
773
  ```
767
774
 
775
+ Event hashing, signing, verification and public-key derivation use a bundled
776
+ libsecp256k1 WebAssembly backend after it becomes ready. Initialization is lazy:
777
+ the first operation uses JavaScript and starts the WASM compiler in the
778
+ background, so existing synchronous calls do not change. To use WASM for the
779
+ first operation, await the optional initializer:
780
+
781
+ ```js
782
+ import {
783
+ finalizeEvent,
784
+ initializeEventCrypto
785
+ } from 'libp2r2p/event'
786
+
787
+ const backend = await initializeEventCrypto() // 'wasm' or 'javascript'
788
+ const event = finalizeEvent(template, secretKey)
789
+ ```
790
+
791
+ Inaccessible WebAssembly, CSP restrictions and backend failures fall back to
792
+ JavaScript. Disable the backend for the current JavaScript realm with
793
+ `configureEventCrypto({ wasm: false })`. This runtime option does not reduce the
794
+ bundle. Applications that want to exclude the WASM artifact entirely should use
795
+ the JS-only event and key entrypoints:
796
+
797
+ ```js
798
+ import { finalizeEvent, isValidEvent } from 'libp2r2p/event/js'
799
+ import { getPublicKey } from 'libp2r2p/key/js'
800
+ ```
801
+
802
+ `generateSecretKey()` always uses JavaScript and does not initialize WASM.
803
+ Maintainers can rebuild the checked-in WASM module with `npm run build:wasm`;
804
+ see the
805
+ [WASM build guide](https://github.com/arthurfranca/libp2r2p/blob/main/wasm/README.md).
806
+ Run the non-gating comparison against the JavaScript backend with
807
+ `npm run benchmark:event` (implemented by `benchmark/event-crypto.js`).
808
+
809
+ Median speedup after explicit WASM initialization, measured with Node.js 24 on
810
+ an AMD Ryzen 5 5600G:
811
+
812
+ | Operation | 8 KiB | 256 KiB | 1 MiB |
813
+ | --- | ---: | ---: | ---: |
814
+ | `isValidEvent` | 5.83× | 2.58× | 1.94× |
815
+ | `finalizeEvent` | 13.64× | 3.11× | 2.06× |
816
+ | `getEventHash` | 1.27× | 1.60× | 1.59× |
817
+ | `getPublicKey` | 3.61× | — | — |
818
+
819
+ These numbers compare complete public operations against the same methods from
820
+ `event/js` and `key/js`. They are an indicative local measurement, not a CI
821
+ performance gate.
822
+
768
823
  Blossom server normalization accepts root `http:` origins for local
769
824
  development. Public Blossom validation additionally requires `https:` and a
770
825
  public host, and rejects credentials, paths, query strings and fragments.
@@ -1133,6 +1188,10 @@ complete per-relay report without delaying the first-success return. Connection
1133
1188
  establishment retains its separate 3-second deadline. PrivateMessenger inherits
1134
1189
  this publication policy, including seeder/recovery messages. A timeout indicates
1135
1190
  missing acknowledgement and cannot prove that the recipient did not receive it.
1191
+ Its optional `getRetryEvent({ event, report })` accepts one replacement signed
1192
+ event after every relay terminally rejected the original with
1193
+ `auth-required:`/`restricted:`/`invalid:`. The replacement and its retry share the original
1194
+ deadline; mixed failures and ambiguous timeouts never invoke it.
1136
1195
 
1137
1196
  ## Private file channels (0.11)
1138
1197
 
@@ -1161,10 +1220,32 @@ including an optional `onSendError` callback identifying the failed outbox messa
1161
1220
  Private messages, including multi-recipient channels, retry NIP-65 read relays
1162
1221
  in pairs per recipient when
1163
1222
  selected relays reject publication or become unreachable while connectivity is
1164
- confirmed. The exact encrypted outer events are reused. Optional `fallbackRelays` on
1223
+ confirmed. The exact encrypted outer events are reused, except that a 3560
1224
+ publication rejected in full by age-related AUTH policy may be re-enveloped once:
1225
+ ciphertext, router, chunks, seeds, recipients and deletion capability remain
1226
+ unchanged while every outer is re-signed with one fresh `created_at` and a
1227
+ refreshed `expiration`. The durable replacement is written before retry and is
1228
+ never refreshed a second time. A relay may use `invalid:` rather than
1229
+ `auth-required:` for a stale 3560 wrapper; `invalid:` triggers this refresh but
1230
+ never attempts NIP-42. Optional `fallbackRelays` on
1165
1231
  `createPrivateMessenger` and `createPrivateMessageSession` are tried after the
1166
1232
  primary destinations by default, including explicit per-call `relays`, channel `sendRelays`
1167
1233
  and channel/global `relays`, as well as explicit `relayToReceivers` maps.
1234
+
1235
+ This bounded re-envelope is preferred over NIP-42 authentication with the
1236
+ channel key because NIP-42 state belongs to the connection, not to one outer
1237
+ event. The `44b-relay` currently permits only one authenticated identity per
1238
+ connection and resets subscriptions when that identity changes. Even relays
1239
+ that support multiple identities per connection must bound them. A user with
1240
+ many contacts has one derived channel key per conversation, so authenticating
1241
+ each channel would either require one authenticated connection per channel or
1242
+ reconnect/resubscribe while switching identities, consuming connection,
1243
+ subscription and recovery budgets. It would also create a connection-scoped linkage
1244
+ between the connection and a channel key. The 3560 wrapper is disposable and
1245
+ privacy-oriented; relays are expected to impose freshness limits instead of
1246
+ correlating it with an authenticated session. A modern 3560 is normally
1247
+ accepted without AUTH, so refreshing an age-rejected outer should remain rare.
1248
+
1168
1249
  Retries preserve each encrypted recipient subset; success requires accepted
1169
1250
  publication covering every recipient in that subset. Receive routing adds them from the start for live
1170
1251
  messages, history and files while preserving automatic NIP-65 updates. Successful sends keep first-ACK
@@ -1176,6 +1257,20 @@ paused or otherwise unavailable now stay pending in the outbox and retry on a
1176
1257
  bounded session backoff (1–30 seconds, 20% jitter) instead of surfacing a
1177
1258
  user-facing send error.
1178
1259
 
1260
+ Multi-recipient callers that persist exact envelopes use the canonical
1261
+ `preparedPublications` array supplied through `onPreparedSeed`. Each element is
1262
+ one recipient group with its own encrypted `events`, recovery `seeds`, and
1263
+ `receiverPubkeys`. A later send can pass the array plus only the pending
1264
+ recipients; matching groups are replayed with their original IDs, bytes and
1265
+ signatures, and routed only for the pending peers. The legacy
1266
+ `preparedPublication` public argument is rejected with
1267
+ `LEGACY_PREPARED_PUBLICATION`; existing encrypted outbox snapshots containing
1268
+ `prepared.publication` are migrated internally to
1269
+ `prepared.publications` before processing. `yell` callers must reuse the
1270
+ original `createdAt` when replaying so the inner rumor identity matches;
1271
+ `broadcastRumor` and `broadcastEvent` already carry their original inner event.
1272
+ Nym broadcasts intentionally remain a single carrier union.
1273
+
1179
1274
 
1180
1275
  ### Relay backpressure and bounded synchronization (0.11.7)
1181
1276
 
@@ -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,98 @@
1
+ import { bytesToBase16 } from '../../base16/index.js'
2
+ import { ValidationError } from '../../error/index.js'
3
+ import { createSecp256k1WasmBackend } from './binding.js'
4
+
5
+ const SELF_TEST_SECRET = new Uint8Array(32).fill(7)
6
+ const SELF_TEST_PUBLIC_KEY =
7
+ '989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f'
8
+ const SELF_TEST_HASH = new Uint8Array(32).fill(9)
9
+ const SELF_TEST_ENTROPY = new Uint8Array(32).fill(11)
10
+
11
+ let enabled = true
12
+ let state = 'idle'
13
+ let backend = null
14
+ let pending = null
15
+ let generation = 0
16
+
17
+ async function initialize (forceRetry) {
18
+ if (!enabled) return 'javascript'
19
+ if (backend) return 'wasm'
20
+ if (pending) return pending
21
+ if (state === 'unavailable' && !forceRetry) return 'javascript'
22
+
23
+ const currentGeneration = generation
24
+ state = 'initializing'
25
+ pending = (async () => {
26
+ let candidate
27
+ try {
28
+ candidate = await createSecp256k1WasmBackend()
29
+ const publicKey = bytesToBase16(candidate.getPublicKey(SELF_TEST_SECRET))
30
+ const signature = candidate.signHash(
31
+ SELF_TEST_HASH,
32
+ SELF_TEST_SECRET,
33
+ SELF_TEST_ENTROPY
34
+ )
35
+ if (
36
+ publicKey !== SELF_TEST_PUBLIC_KEY ||
37
+ !candidate.verifyHash(signature, SELF_TEST_HASH, candidate.getPublicKey(SELF_TEST_SECRET))
38
+ ) {
39
+ throw Error('secp256k1 WASM self-test failed')
40
+ }
41
+ if (!enabled || currentGeneration !== generation) {
42
+ candidate.dispose()
43
+ return 'javascript'
44
+ }
45
+ backend = candidate
46
+ state = 'ready'
47
+ return 'wasm'
48
+ } catch {
49
+ candidate?.dispose()
50
+ if (enabled && currentGeneration === generation) state = 'unavailable'
51
+ return 'javascript'
52
+ } finally {
53
+ pending = null
54
+ }
55
+ })()
56
+ return pending
57
+ }
58
+
59
+ export function configureEventCrypto (options = {}) {
60
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
61
+ throw new ValidationError('INVALID_EVENT_CRYPTO_OPTIONS')
62
+ }
63
+ if (options.wasm !== undefined && typeof options.wasm !== 'boolean') {
64
+ throw new ValidationError('INVALID_EVENT_CRYPTO_OPTIONS')
65
+ }
66
+ const nextEnabled = options.wasm !== false
67
+ if (nextEnabled === enabled) return
68
+
69
+ enabled = nextEnabled
70
+ generation++
71
+ if (!enabled) {
72
+ backend?.dispose()
73
+ backend = null
74
+ state = 'disabled'
75
+ } else {
76
+ state = 'idle'
77
+ }
78
+ }
79
+
80
+ export function initializeEventCrypto () {
81
+ return initialize(true)
82
+ }
83
+
84
+ export function scheduleEventCryptoInitialization () {
85
+ if (!enabled || backend || pending || state === 'unavailable') return
86
+ initialize(false).catch(() => {})
87
+ }
88
+
89
+ export function getReadyEventCryptoBackend () {
90
+ return backend
91
+ }
92
+
93
+ export function markEventCryptoUnavailable () {
94
+ if (!backend) return
95
+ backend.dispose()
96
+ backend = null
97
+ state = 'unavailable'
98
+ }