libp2r2p 0.12.0 → 0.12.2

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
@@ -264,6 +264,8 @@ requests, uses an origin/identity Web Lock when available, and stops on pause or
264
264
  close. Missing-key retries have no polling timer. MAC failures permit three total
265
265
  attempts, persisted across reloads; exhausted packets remain until FIFO eviction.
266
266
  `retryPendingDecryptions({ force: true })` explicitly retries exhausted packets.
267
+ Callers should request it when a committed content-key revision provides new
268
+ evidence, while startup, resume and routine reconciliation retain normal retries.
267
269
  Success enters the normal acknowledged inbox before removing the pending item.
268
270
  The session coordinator forwards this API.
269
271
 
@@ -358,6 +360,13 @@ By default, each high-level private-message send creates one fresh deletion
358
360
  keypair for its logical message. Every outer kind `3560` event produced for that
359
361
  send, including router chunks, recipient subsets, and nym carriers, carries the
360
362
  same public key in its `s` tag. The result always contains `delivery.reports`.
363
+ Each routed report also identifies its exact outer event in `outerEventId` and
364
+ the encrypted recipient subset in `receiverPubkeys`. The same recipient can
365
+ appear in multiple reports when a message spans chunks or recipient groups; it
366
+ is fully covered only when every report containing it succeeds. Fixed relay
367
+ lists publish one shared recipient set, while explicit `relayToReceivers` maps
368
+ and automatic NIP-65 routing can produce partial recipient coverage. Nym uses
369
+ one carrier union and does not expose per-recipient partial delivery.
361
370
  When libp2r2p generated the keypair, it also contains
362
371
  `delivery.deletionSeckey`; the public key can be derived from that secret.
363
372
 
@@ -797,7 +806,7 @@ Maintainers can rebuild the checked-in WASM module with `npm run build:wasm`;
797
806
  see the
798
807
  [WASM build guide](https://github.com/arthurfranca/libp2r2p/blob/main/wasm/README.md).
799
808
  Run the non-gating comparison against the JavaScript backend with
800
- `npm run benchmark:event`.
809
+ `npm run benchmark:event` (implemented by `benchmark/event-crypto.js`).
801
810
 
802
811
  Median speedup after explicit WASM initialization, measured with Node.js 24 on
803
812
  an AMD Ryzen 5 5600G:
@@ -1181,6 +1190,10 @@ complete per-relay report without delaying the first-success return. Connection
1181
1190
  establishment retains its separate 3-second deadline. PrivateMessenger inherits
1182
1191
  this publication policy, including seeder/recovery messages. A timeout indicates
1183
1192
  missing acknowledgement and cannot prove that the recipient did not receive it.
1193
+ Its optional `getRetryEvent({ event, report })` accepts one replacement signed
1194
+ event after every relay terminally rejected the original with
1195
+ `auth-required:`/`restricted:`/`invalid:`. The replacement and its retry share the original
1196
+ deadline; mixed failures and ambiguous timeouts never invoke it.
1184
1197
 
1185
1198
  ## Private file channels (0.11)
1186
1199
 
@@ -1209,10 +1222,32 @@ including an optional `onSendError` callback identifying the failed outbox messa
1209
1222
  Private messages, including multi-recipient channels, retry NIP-65 read relays
1210
1223
  in pairs per recipient when
1211
1224
  selected relays reject publication or become unreachable while connectivity is
1212
- confirmed. The exact encrypted outer events are reused. Optional `fallbackRelays` on
1225
+ confirmed. The exact encrypted outer events are reused, except that a 3560
1226
+ publication rejected in full by age-related AUTH policy may be re-enveloped once:
1227
+ ciphertext, router, chunks, seeds, recipients and deletion capability remain
1228
+ unchanged while every outer is re-signed with one fresh `created_at` and a
1229
+ refreshed `expiration`. The durable replacement is written before retry and is
1230
+ never refreshed a second time. A relay may use `invalid:` rather than
1231
+ `auth-required:` for a stale 3560 wrapper; `invalid:` triggers this refresh but
1232
+ never attempts NIP-42. Optional `fallbackRelays` on
1213
1233
  `createPrivateMessenger` and `createPrivateMessageSession` are tried after the
1214
1234
  primary destinations by default, including explicit per-call `relays`, channel `sendRelays`
1215
1235
  and channel/global `relays`, as well as explicit `relayToReceivers` maps.
1236
+
1237
+ This bounded re-envelope is preferred over NIP-42 authentication with the
1238
+ channel key because NIP-42 state belongs to the connection, not to one outer
1239
+ event. The `44b-relay` currently permits only one authenticated identity per
1240
+ connection and resets subscriptions when that identity changes. Even relays
1241
+ that support multiple identities per connection must bound them. A user with
1242
+ many contacts has one derived channel key per conversation, so authenticating
1243
+ each channel would either require one authenticated connection per channel or
1244
+ reconnect/resubscribe while switching identities, consuming connection,
1245
+ subscription and recovery budgets. It would also create a connection-scoped linkage
1246
+ between the connection and a channel key. The 3560 wrapper is disposable and
1247
+ privacy-oriented; relays are expected to impose freshness limits instead of
1248
+ correlating it with an authenticated session. A modern 3560 is normally
1249
+ accepted without AUTH, so refreshing an age-rejected outer should remain rare.
1250
+
1216
1251
  Retries preserve each encrypted recipient subset; success requires accepted
1217
1252
  publication covering every recipient in that subset. Receive routing adds them from the start for live
1218
1253
  messages, history and files while preserving automatic NIP-65 updates. Successful sends keep first-ACK
@@ -1224,6 +1259,20 @@ paused or otherwise unavailable now stay pending in the outbox and retry on a
1224
1259
  bounded session backoff (1–30 seconds, 20% jitter) instead of surfacing a
1225
1260
  user-facing send error.
1226
1261
 
1262
+ Multi-recipient callers that persist exact envelopes use the canonical
1263
+ `preparedPublications` array supplied through `onPreparedSeed`. Each element is
1264
+ one recipient group with its own encrypted `events`, recovery `seeds`, and
1265
+ `receiverPubkeys`. A later send can pass the array plus only the pending
1266
+ recipients; matching groups are replayed with their original IDs, bytes and
1267
+ signatures, and routed only for the pending peers. The legacy
1268
+ `preparedPublication` public argument is rejected with
1269
+ `LEGACY_PREPARED_PUBLICATION`; existing encrypted outbox snapshots containing
1270
+ `prepared.publication` are migrated internally to
1271
+ `prepared.publications` before processing. `yell` callers must reuse the
1272
+ original `createdAt` when replaying so the inner rumor identity matches;
1273
+ `broadcastRumor` and `broadcastEvent` already carry their original inner event.
1274
+ Nym broadcasts intentionally remain a single carrier union.
1275
+
1227
1276
 
1228
1277
  ### Relay backpressure and bounded synchronization (0.11.7)
1229
1278
 
@@ -1,3 +1,4 @@
1
+ import { bytesToBase16 } from '../../base16/index.js'
1
2
  import { ValidationError } from '../../error/index.js'
2
3
  import { createSecp256k1WasmBackend } from './binding.js'
3
4
 
@@ -13,8 +14,6 @@ let backend = null
13
14
  let pending = null
14
15
  let generation = 0
15
16
 
16
- const toHex = bytes => Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')
17
-
18
17
  async function initialize (forceRetry) {
19
18
  if (!enabled) return 'javascript'
20
19
  if (backend) return 'wasm'
@@ -27,7 +26,7 @@ async function initialize (forceRetry) {
27
26
  let candidate
28
27
  try {
29
28
  candidate = await createSecp256k1WasmBackend()
30
- const publicKey = toHex(candidate.getPublicKey(SELF_TEST_SECRET))
29
+ const publicKey = bytesToBase16(candidate.getPublicKey(SELF_TEST_SECRET))
31
30
  const signature = candidate.signHash(
32
31
  SELF_TEST_HASH,
33
32
  SELF_TEST_SECRET,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -51,8 +51,8 @@
51
51
  "web-storage-queue"
52
52
  ],
53
53
  "scripts": {
54
- "benchmark:event": "node --expose-gc benchmark/event-crypto.mjs",
55
- "build:wasm": "node wasm/build.mjs",
54
+ "benchmark:event": "node --expose-gc benchmark/event-crypto.js",
55
+ "build:wasm": "node wasm/build.js",
56
56
  "test": "node --test --experimental-test-module-mocks 'tests/**/*.test.js'",
57
57
  "test:files": "node --test --experimental-test-module-mocks",
58
58
  "test:only": "node --test --test-only --experimental-test-module-mocks 'tests/**/*.test.js'"
@@ -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
+ }
@@ -11,6 +11,7 @@ import { incompleteFetchError } from './helpers/fetch-error.js'
11
11
  import { readHistory } from './helpers/history.js'
12
12
  import { subscriptionError } from './helpers/subscription-error.js'
13
13
  import { createLookupWarningLogger } from './helpers/lookup-warning.js'
14
+ import { normalizePreparedPublications } from './helpers/prepared.js'
14
15
  import * as nip44v3 from '../nip44-v3/index.js'
15
16
  import { relayPool } from '../relay/index.js'
16
17
  import { JSONL_CHUNK_BYTES, NYM_CARRIER_CHUNK_CHARS } from './helpers/chunk-size.js'
@@ -40,7 +41,7 @@ const decoder = new TextDecoder()
40
41
  const warnLookupFailure = createLookupWarningLogger()
41
42
  const NIP44_V3_SCOPE = ''
42
43
 
43
- const sendToRelays = (event, relays) => relayPool.sendEvent(event, relays)
44
+ const sendToRelays = (event, relays, options) => relayPool.sendEvent(event, relays, options)
44
45
  const getEvents = (...args) => relayPool.getEvents(...args)
45
46
  const getLiveEventsGenerator = (...args) => relayPool.getLiveEventsGenerator(...args)
46
47
  const getEventsFeedGenerator = (...args) => relayPool.getEventsFeedGenerator(...args)
@@ -153,18 +154,29 @@ async function prepareRoutedMessage ({ senderSigner, imkcSigner, privateChannelS
153
154
  }
154
155
  }
155
156
 
156
- async function * wrapPreparedEvents ({ privateChannelSigner, senderSigner, event, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey, expirationSeconds = EXPIRATION_SECONDS, context, preparedPublication }) {
157
+ async function validatePreparedPublication ({ privateChannelSigner, senderSigner, event, receivers, preparedPublication }) {
158
+ const channelPubkey = await privateChannelSigner.getPublicKey()
159
+ const innerEventId = getEventHash({ ...event, pubkey: event.pubkey || await senderSigner.getPublicKey() })
160
+ const publicationReceivers = Array.isArray(preparedPublication?.receiverPubkeys)
161
+ ? [...new Set(preparedPublication.receiverPubkeys)]
162
+ : []
163
+ const expected = receivers ? receiverPubkeys(receivers).sort().join(',') : ''
164
+ if (preparedPublication?.innerEventId !== innerEventId || preparedPublication?.channelPubkey !== channelPubkey ||
165
+ !publicationReceivers.length || (receivers && publicationReceivers.slice().sort().join(',') !== expected) ||
166
+ !Array.isArray(preparedPublication.events) || !preparedPublication.events.length || !Array.isArray(preparedPublication.seeds) ||
167
+ preparedPublication.events.some(value => !isValidEvent(value) || value.pubkey !== channelPubkey || value.kind !== PRIVATE_BROADCAST_KIND)) {
168
+ throw new ValidationError('INVALID_PREPARED_PUBLICATION')
169
+ }
170
+ if (preparedPublication.events.some(value => Number(value.tags.find(tag => tag[0] === 'expiration')?.[1]) <= nowSeconds())) {
171
+ throw new ValidationError('PREPARED_PUBLICATION_EXPIRED')
172
+ }
173
+ return publicationReceivers
174
+ }
175
+
176
+ async function * wrapPreparedEvents ({ privateChannelSigner, senderSigner, event, receivers, receiverTag, fileChunkIndex, onPreparedSeed, onPreparedPublication, deletionPubkey, expirationSeconds = EXPIRATION_SECONDS, context, preparedPublication }) {
157
177
  let publication = preparedPublication
158
178
  if (publication) {
159
- const channelPubkey = await privateChannelSigner.getPublicKey()
160
- const expected = receiverPubkeys(receivers).sort().join(',')
161
- const innerEventId = getEventHash({ ...event, pubkey: event.pubkey || await senderSigner.getPublicKey() })
162
- if (publication.innerEventId !== innerEventId || publication.channelPubkey !== channelPubkey || !Array.isArray(publication.receiverPubkeys) || publication.receiverPubkeys.slice().sort().join(',') !== expected ||
163
- !Array.isArray(publication.events) || !publication.events.length || !Array.isArray(publication.seeds) ||
164
- publication.events.some(event => !isValidEvent(event) || event.pubkey !== channelPubkey || event.kind !== PRIVATE_BROADCAST_KIND)) {
165
- throw new ValidationError('INVALID_PREPARED_PUBLICATION')
166
- }
167
- if (publication.events.some(event => Number(event.tags.find(tag => tag[0] === 'expiration')?.[1]) <= nowSeconds())) throw new ValidationError('PREPARED_PUBLICATION_EXPIRED')
179
+ await validatePreparedPublication({ privateChannelSigner, senderSigner, event, receivers, preparedPublication: publication })
168
180
  } else {
169
181
  const routerSeckey = generateSecretKey()
170
182
  const routerPubkey = getPublicKey(routerSeckey)
@@ -194,6 +206,7 @@ async function * wrapPreparedEvents ({ privateChannelSigner, senderSigner, event
194
206
  }
195
207
  publication = { innerEventId: context.innerEventId, channelPubkey: context.channelPubkey, receiverPubkeys: receiverPubkeyList, events, seeds }
196
208
  }
209
+ onPreparedPublication?.(publication)
197
210
  // The callback commits recovery data before any bytes leave this publisher.
198
211
  // A session commits its encrypted journal; a direct messenger commits seeds.
199
212
  if (onPreparedSeed) {
@@ -203,9 +216,12 @@ async function * wrapPreparedEvents ({ privateChannelSigner, senderSigner, event
203
216
  }
204
217
 
205
218
  // Streaming version of wrapEvent
206
- export async function * wrapEvents ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey, event, expirationSeconds = EXPIRATION_SECONDS, temporaryStorageArea, _getIykcProofs = getIykcProofs, preparedPublication }) {
219
+ export async function * wrapEvents ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, onPreparedPublication, deletionPubkey, event, expirationSeconds = EXPIRATION_SECONDS, temporaryStorageArea, _getIykcProofs = getIykcProofs, preparedPublication, preparedPublications }) {
207
220
  const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
208
- const context = preparedPublication
221
+ const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
222
+ if (prepared.length > 1) throw new ValidationError('INVALID_PREPARED_PUBLICATIONS')
223
+ const publication = prepared[0]
224
+ const context = publication
209
225
  ? null
210
226
  : await prepareRoutedMessage({
211
227
  senderSigner,
@@ -218,7 +234,7 @@ export async function * wrapEvents ({ senderSigner, imkcSigner, privateChannelSi
218
234
  _getIykcProofs
219
235
  })
220
236
  try {
221
- yield * wrapPreparedEvents({ privateChannelSigner, senderSigner, event, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context, preparedPublication })
237
+ yield * wrapPreparedEvents({ privateChannelSigner, senderSigner, event, receivers, receiverTag, fileChunkIndex, onPreparedSeed, onPreparedPublication, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context, preparedPublication: publication })
222
238
  } finally {
223
239
  if (context) cleanupEnvelopeRows(context.preparedRows)
224
240
  }
@@ -558,29 +574,208 @@ function withRecoveryRelays (relays, recoveryRelays) {
558
574
  return uniq([...(relays || []), ...(recoveryRelays || [])])
559
575
  }
560
576
 
561
- export async function publish ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs = getIykcProofs, _publish = sendToRelays, preparedPublication }) {
577
+ function withPublicationCoverage (report, event, receivers) {
578
+ if (!report || typeof report !== 'object' || Array.isArray(report)) return report
579
+ return {
580
+ ...report,
581
+ outerEventId: event.id,
582
+ receiverPubkeys: receiverPubkeys(receivers)
583
+ }
584
+ }
585
+
586
+ function expirationRetentionSeconds (publication, expirationSeconds) {
587
+ if (Number.isSafeInteger(expirationSeconds) && expirationSeconds >= 0) return expirationSeconds
588
+ for (const event of publication.events || []) {
589
+ const expiration = Number(event.tags?.find(tag => tag[0] === 'expiration')?.[1])
590
+ if (Number.isSafeInteger(expiration) && expiration >= event.created_at) return expiration - event.created_at
591
+ }
592
+ return null
593
+ }
594
+
595
+ async function refreshPublicationForRetry ({ privateChannelSigner, publication, expirationSeconds }) {
596
+ if (publication?.outerRefreshAttempted === true) throw new ValidationError('OUTER_REFRESH_ALREADY_ATTEMPTED')
597
+ if (!Array.isArray(publication?.events) || publication.events.length === 0) throw new ValidationError('INVALID_PREPARED_PUBLICATION')
598
+ const channelPubkey = await privateChannelSigner.getPublicKey()
599
+ if (publication.channelPubkey !== channelPubkey) throw new ValidationError('INVALID_PREPARED_PUBLICATION')
600
+ const currentTime = nowSeconds()
601
+ const createdAt = Math.max(currentTime, ...publication.events.map(event => event.created_at + 1))
602
+ const retention = expirationRetentionSeconds(publication, expirationSeconds)
603
+ const events = []
604
+ for (const previous of publication.events) {
605
+ if (!isValidEvent(previous) || previous.kind !== PRIVATE_BROADCAST_KIND || previous.pubkey !== channelPubkey) {
606
+ throw new ValidationError('INVALID_PREPARED_PUBLICATION')
607
+ }
608
+ const tags = previous.tags.map(tag => {
609
+ if (tag[0] !== 'expiration' || retention === null) return [...tag]
610
+ return ['expiration', String(createdAt + retention)]
611
+ })
612
+ const refreshed = await privateChannelSigner.signEvent({
613
+ kind: PRIVATE_BROADCAST_KIND,
614
+ created_at: createdAt,
615
+ tags,
616
+ content: previous.content
617
+ })
618
+ if (!isValidEvent(refreshed) || refreshed.id === previous.id || refreshed.kind !== PRIVATE_BROADCAST_KIND || refreshed.pubkey !== channelPubkey ||
619
+ refreshed.created_at !== createdAt || refreshed.content !== previous.content || JSON.stringify(refreshed.tags) !== JSON.stringify(tags)) {
620
+ throw new ValidationError('INVALID_PREPARED_PUBLICATION')
621
+ }
622
+ if (eventByteLength(refreshed) > MAX_EVENT_BYTES) throw new ValidationError('EVENT_TOO_LARGE')
623
+ events.push(refreshed)
624
+ }
625
+ return {
626
+ ...structuredClone(publication),
627
+ events,
628
+ outerRefreshAttempted: true
629
+ }
630
+ }
631
+
632
+ async function publishOuterEvents ({
633
+ publication,
634
+ publishRelays,
635
+ primaryRelays,
636
+ receivers,
637
+ privateChannelSigner,
638
+ expirationSeconds,
639
+ onPreparedSeed,
640
+ _publish,
641
+ includeCoverage = true,
642
+ includeRoutingContext = true
643
+ }) {
644
+ let current = publication
645
+ let refreshed = false
646
+ let refreshWork
647
+ const results = []
648
+ for (let index = 0; index < current.events.length; index++) {
649
+ const getRetryEvent = index === 0 && current.outerRefreshAttempted !== true
650
+ ? async ({ event }) => {
651
+ if (refreshed || refreshWork || event.id !== current.events[0].id) return null
652
+ refreshWork = (async () => {
653
+ const next = await refreshPublicationForRetry({ privateChannelSigner, publication: current, expirationSeconds })
654
+ const firstSeed = next.seeds?.[0]
655
+ if (firstSeed && onPreparedSeed) await onPreparedSeed({ ...firstSeed, publication: next }, { replace: true })
656
+ current = next
657
+ refreshed = true
658
+ return current.events[0]
659
+ })()
660
+ return await refreshWork
661
+ }
662
+ : undefined
663
+ const report = await _publish(current.events[index], publishRelays, includeRoutingContext
664
+ ? {
665
+ receiverPubkeys: receiverPubkeys(receivers),
666
+ primaryRelays,
667
+ getRetryEvent
668
+ }
669
+ : { getRetryEvent })
670
+ results.push(includeCoverage ? withPublicationCoverage(report, current.events[index], receivers) : report)
671
+ }
672
+ return { publication: current, results }
673
+ }
674
+
675
+ function relayMapForReceivers (relayToReceivers, receivers) {
676
+ if (!relayToReceivers) return null
677
+ const wanted = new Set(receivers)
678
+ const relays = []
679
+ for (const [relay, values] of relayReceiverEntries(relayToReceivers)) {
680
+ const offered = (Array.isArray(values) ? values : [values])
681
+ .map(value => typeof value === 'string' ? value : receiverPubkeys([value])[0])
682
+ if (!offered.some(pubkey => wanted.has(pubkey))) continue
683
+ relays.push(relay)
684
+ }
685
+ return relays
686
+ }
687
+
688
+ export async function publish ({ senderSigner, imkcSigner, privateChannelSigner = senderSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs = getIykcProofs, _publish = sendToRelays, preparedPublication, preparedPublications }) {
562
689
  const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
563
690
  const results = []
564
- // Relays are grouped only when they have the exact same recipient pubkey set
565
- const groups = groupedRelayReceivers({ relayToReceivers, receivers })
566
- if (groups) {
567
- const context = preparedPublication
568
- ? null
569
- : await prepareRoutedMessage({
570
- senderSigner,
571
- imkcSigner,
691
+ const requestedReceivers = receiverPubkeys(receivers)
692
+ const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
693
+ if (prepared.length) {
694
+ const requested = new Set(requestedReceivers)
695
+ const covered = new Set()
696
+ for (let publicationIndex = 0; publicationIndex < prepared.length; publicationIndex++) {
697
+ const publication = prepared[publicationIndex]
698
+ const candidateReceivers = Array.isArray(publication?.receiverPubkeys)
699
+ ? [...new Set(publication.receiverPubkeys)]
700
+ : []
701
+ const candidateTargets = candidateReceivers.filter(pubkey => requested.has(pubkey))
702
+ if (!candidateTargets.length) continue
703
+ const publicationReceivers = await validatePreparedPublication({
572
704
  privateChannelSigner,
573
- privateChannelReaderPubkey,
574
- receivers,
705
+ senderSigner,
575
706
  event,
576
- temporaryStorageArea,
577
- _getIykcProofs
707
+ preparedPublication: publication
708
+ })
709
+ const targeted = publicationReceivers.filter(pubkey => requested.has(pubkey))
710
+ for (const pubkey of targeted) covered.add(pubkey)
711
+ if (onPreparedSeed) {
712
+ for (const seed of publication.seeds) await onPreparedSeed({ ...seed, publication })
713
+ }
714
+ const targetRelays = relayMapForReceivers(relayToReceivers, targeted) || uniq(relays)
715
+ const publishRelays = withRecoveryRelays(targetRelays, recoveryRelays)
716
+ if (!publishRelays.length) throw new ValidationError('NO_RELAYS')
717
+ const outcome = await publishOuterEvents({
718
+ publication,
719
+ publishRelays,
720
+ primaryRelays: targetRelays,
721
+ receivers: targeted.map(pubkey => ({ receiverPubkey: pubkey })),
722
+ privateChannelSigner,
723
+ expirationSeconds,
724
+ onPreparedSeed,
725
+ _publish
578
726
  })
727
+ prepared[publicationIndex] = outcome.publication
728
+ results.push(...outcome.results)
729
+ }
730
+ if ([...requested].some(pubkey => !covered.has(pubkey))) {
731
+ throw new ValidationError('PREPARED_PUBLICATION_RECEIVER_MISSING')
732
+ }
733
+ return results
734
+ }
735
+ // Relays are grouped only when they have the exact same recipient pubkey set
736
+ const groups = groupedRelayReceivers({ relayToReceivers, receivers })
737
+ if (groups) {
738
+ const context = await prepareRoutedMessage({
739
+ senderSigner,
740
+ imkcSigner,
741
+ privateChannelSigner,
742
+ privateChannelReaderPubkey,
743
+ receivers,
744
+ event,
745
+ temporaryStorageArea,
746
+ _getIykcProofs
747
+ })
579
748
  try {
580
749
  for (const group of groups) {
581
- for await (const wrappedEvent of wrapPreparedEvents({ privateChannelSigner, senderSigner, event, receivers: group.receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey: normalizedDeletionPubkey, expirationSeconds, context, preparedPublication })) {
582
- results.push(await _publish(wrappedEvent, withRecoveryRelays(group.relays, recoveryRelays), { receiverPubkeys: receiverPubkeys(group.receivers), primaryRelays: group.relays }))
750
+ let publication
751
+ const events = []
752
+ for await (const wrappedEvent of wrapPreparedEvents({
753
+ privateChannelSigner,
754
+ senderSigner,
755
+ event,
756
+ receivers: group.receivers,
757
+ receiverTag,
758
+ fileChunkIndex,
759
+ onPreparedSeed,
760
+ onPreparedPublication: value => { publication = value },
761
+ deletionPubkey: normalizedDeletionPubkey,
762
+ expirationSeconds,
763
+ context
764
+ })) {
765
+ events.push(wrappedEvent)
583
766
  }
767
+ const publishRelays = withRecoveryRelays(group.relays, recoveryRelays)
768
+ const outcome = await publishOuterEvents({
769
+ publication: publication || { events },
770
+ publishRelays,
771
+ primaryRelays: group.relays,
772
+ receivers: group.receivers,
773
+ privateChannelSigner,
774
+ expirationSeconds,
775
+ onPreparedSeed,
776
+ _publish
777
+ })
778
+ results.push(...outcome.results)
584
779
  }
585
780
  } finally {
586
781
  if (context) cleanupEnvelopeRows(context.preparedRows)
@@ -588,9 +783,37 @@ export async function publish ({ senderSigner, imkcSigner, privateChannelSigner
588
783
  return results
589
784
  }
590
785
 
591
- for await (const wrappedEvent of wrapEvents({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, fileChunkIndex, onPreparedSeed, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds, temporaryStorageArea, _getIykcProofs, preparedPublication })) {
592
- results.push(await _publish(wrappedEvent, withRecoveryRelays(relays, recoveryRelays), { receiverPubkeys: receiverPubkeys(receivers), primaryRelays: relays }))
786
+ let publication
787
+ const events = []
788
+ for await (const wrappedEvent of wrapEvents({
789
+ senderSigner,
790
+ imkcSigner,
791
+ privateChannelSigner,
792
+ privateChannelReaderPubkey,
793
+ receivers,
794
+ receiverTag,
795
+ fileChunkIndex,
796
+ onPreparedSeed,
797
+ onPreparedPublication: value => { publication = value },
798
+ deletionPubkey: normalizedDeletionPubkey,
799
+ event,
800
+ expirationSeconds,
801
+ temporaryStorageArea,
802
+ _getIykcProofs
803
+ })) {
804
+ events.push(wrappedEvent)
593
805
  }
806
+ const outcome = await publishOuterEvents({
807
+ publication: publication || { events },
808
+ publishRelays: withRecoveryRelays(relays, recoveryRelays),
809
+ primaryRelays: relays,
810
+ receivers,
811
+ privateChannelSigner,
812
+ expirationSeconds,
813
+ onPreparedSeed,
814
+ _publish
815
+ })
816
+ results.push(...outcome.results)
594
817
  return results
595
818
  }
596
819
 
@@ -598,8 +821,24 @@ export async function publishNymEvent ({ nymSigner, privateChannelSigner, privat
598
821
  const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
599
822
  const results = []
600
823
  const publishRelays = withRecoveryRelays(relayToReceivers ? relaysFromRelayReceivers(relayToReceivers) : relays, recoveryRelays)
824
+ const events = []
601
825
  for await (const wrappedEvent of wrapNymEvents({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds })) {
602
- results.push(await _publish(wrappedEvent, publishRelays))
826
+ events.push(wrappedEvent)
827
+ }
828
+ if (events.length) {
829
+ const channelPubkey = await privateChannelSigner.getPublicKey()
830
+ const outcome = await publishOuterEvents({
831
+ publication: { channelPubkey, receiverPubkeys: [], events, seeds: [] },
832
+ publishRelays,
833
+ primaryRelays: publishRelays,
834
+ receivers: [],
835
+ privateChannelSigner,
836
+ expirationSeconds,
837
+ includeCoverage: false,
838
+ includeRoutingContext: false,
839
+ _publish
840
+ })
841
+ results.push(...outcome.results)
603
842
  }
604
843
  return results
605
844
  }