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 +96 -1
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/event/helpers/core.js +120 -0
- package/event/helpers/serialize.js +56 -0
- package/event/index.js +81 -74
- package/event/js/index.js +13 -0
- package/helpers/secp256k1-wasm/backend.js +98 -0
- package/helpers/secp256k1-wasm/binding.js +229 -0
- package/helpers/secp256k1-wasm/generated.js +25 -0
- package/key/helpers/core.js +149 -0
- package/key/index.js +57 -123
- package/key/js/index.js +13 -0
- package/package.json +7 -1
- package/private-channel/helpers/prepared.js +12 -0
- package/private-channel/index.js +271 -32
- package/private-message/index.js +44 -23
- package/private-messenger/file/index.js +4 -2
- package/private-messenger/helpers/fallback-race.js +13 -2
- package/private-messenger/helpers/send-routing.js +2 -1
- package/private-messenger/index.js +30 -15
- package/private-messenger/session/README.md +17 -3
- package/private-messenger/session/helpers/prepared-state.js +17 -0
- package/private-messenger/session/helpers/publication-journal.js +38 -10
- package/private-messenger/session/index.js +21 -9
- package/relay/services/relay-pool.js +127 -58
package/private-channel/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
const
|
|
566
|
-
if (
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
574
|
-
receivers,
|
|
705
|
+
senderSigner,
|
|
575
706
|
event,
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
582
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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
|
}
|
package/private-message/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ValidationError } from '../error/index.js'
|
|
|
4
4
|
import { generateKeypair, keypairFromSeckey } from '../key/index.js'
|
|
5
5
|
import * as privateChannel from '../private-channel/index.js'
|
|
6
6
|
import { normalizeRumor, deliveryInfo } from '../private-channel/helpers/rumor.js'
|
|
7
|
+
import { normalizePreparedPublications } from '../private-channel/helpers/prepared.js'
|
|
7
8
|
|
|
8
9
|
export const ASK_KIND = 7329
|
|
9
10
|
export const REPLY_KIND = 7330
|
|
@@ -41,17 +42,25 @@ function normalizeDeletionPubkey (deletionPubkey) {
|
|
|
41
42
|
return deletionPubkey.toLowerCase()
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function resolveDeletionCapability ({ deletionPubkey, deletionSeckey, autoDeletionCapability = true,
|
|
45
|
+
function resolveDeletionCapability ({ deletionPubkey, deletionSeckey, autoDeletionCapability = true, preparedPublications = [] }) {
|
|
45
46
|
if (deletionSeckey !== undefined) throw new ValidationError('DELETION_SECKEY_NOT_ACCEPTED')
|
|
46
47
|
if (typeof autoDeletionCapability !== 'boolean') throw new ValidationError('AUTO_DELETION_CAPABILITY_BOOLEAN_REQUIRED')
|
|
47
48
|
|
|
48
49
|
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
49
|
-
if (
|
|
50
|
-
const capability =
|
|
51
|
-
if (!capability
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
if (preparedPublications.length) {
|
|
51
|
+
const capability = preparedPublications[0].deletionCapability
|
|
52
|
+
if (!capability) throw new ValidationError('INVALID_PREPARED_PUBLICATION')
|
|
53
|
+
for (const publication of preparedPublications) {
|
|
54
|
+
const current = publication.deletionCapability
|
|
55
|
+
if (!current || (current.deletionPubkey !== capability.deletionPubkey) ||
|
|
56
|
+
(current.deletionSeckey !== capability.deletionSeckey) ||
|
|
57
|
+
(current.deletionPubkey !== undefined && normalizeDeletionPubkey(current.deletionPubkey) !== current.deletionPubkey) ||
|
|
58
|
+
(normalizedDeletionPubkey && normalizedDeletionPubkey !== capability.deletionPubkey) ||
|
|
59
|
+
(capability.deletionSeckey !== undefined && keypairFromSeckey(capability.deletionSeckey).pubkey !== capability.deletionPubkey) ||
|
|
60
|
+
publication.events?.some(event => event.tags.find(tag => tag[0] === 's')?.[1] !== capability.deletionPubkey)) {
|
|
61
|
+
throw new ValidationError('INVALID_PREPARED_PUBLICATION')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
55
64
|
return { ...capability }
|
|
56
65
|
}
|
|
57
66
|
if (normalizedDeletionPubkey) return { deletionPubkey: normalizedDeletionPubkey }
|
|
@@ -63,9 +72,9 @@ function resolveDeletionCapability ({ deletionPubkey, deletionSeckey, autoDeleti
|
|
|
63
72
|
|
|
64
73
|
function preparedSeedHandler (onPreparedSeed, deletion) {
|
|
65
74
|
if (!onPreparedSeed) return undefined
|
|
66
|
-
return seed => {
|
|
75
|
+
return (seed, options) => {
|
|
67
76
|
seed.publication.deletionCapability = deletion
|
|
68
|
-
return onPreparedSeed(seed)
|
|
77
|
+
return onPreparedSeed(seed, options)
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
|
|
@@ -668,11 +677,11 @@ async function sendPrivateMessage ({
|
|
|
668
677
|
_getIykcProofs,
|
|
669
678
|
fileChunkIndex,
|
|
670
679
|
onPreparedSeed,
|
|
671
|
-
|
|
680
|
+
preparedPublications,
|
|
672
681
|
_publish = privateChannel.publish
|
|
673
682
|
}) {
|
|
674
683
|
if (!privateChannelSigner?.getPublicKey) throw new ValidationError('PRIVATE_CHANNEL_WRITER_REQUIRED')
|
|
675
|
-
return _publish({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed,
|
|
684
|
+
return _publish({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, preparedPublications: preparedPublications?.length ? preparedPublications : undefined, _getIykcProofs })
|
|
676
685
|
}
|
|
677
686
|
|
|
678
687
|
async function sendNymMessage ({
|
|
@@ -841,21 +850,29 @@ export async function yell ({
|
|
|
841
850
|
deletionSeckey,
|
|
842
851
|
autoDeletionCapability = true,
|
|
843
852
|
fileChunkIndex,
|
|
853
|
+
createdAt,
|
|
844
854
|
onPreparedSeed,
|
|
855
|
+
preparedPublication,
|
|
856
|
+
preparedPublications,
|
|
845
857
|
_publish = privateChannel.publish
|
|
846
858
|
}) {
|
|
847
|
-
const
|
|
859
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
860
|
+
const receivers = uniq(receiverPubkeys?.length
|
|
861
|
+
? receiverPubkeys
|
|
862
|
+
: prepared.flatMap(publication => publication.receiverPubkeys || []))
|
|
848
863
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
864
|
+
const rumor = makeMessageRumor({
|
|
865
|
+
kind: TELL_KIND,
|
|
866
|
+
tags: [],
|
|
867
|
+
message: message || { code, payload, error, content }
|
|
868
|
+
})
|
|
869
|
+
if (createdAt !== undefined) rumor.created_at = createdAt
|
|
849
870
|
const { event, wireEvent } = await makeOutgoingRumor({
|
|
850
871
|
senderSigner,
|
|
851
|
-
rumor
|
|
852
|
-
kind: TELL_KIND,
|
|
853
|
-
tags: [],
|
|
854
|
-
message: message || { code, payload, error, content }
|
|
855
|
-
})
|
|
872
|
+
rumor
|
|
856
873
|
})
|
|
857
|
-
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
858
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
874
|
+
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability, preparedPublications: prepared })
|
|
875
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion), preparedPublications: prepared, _getIykcProofs, _publish })
|
|
859
876
|
return withDelivery({ yell: event }, reports, deletion.deletionSeckey)
|
|
860
877
|
}
|
|
861
878
|
|
|
@@ -878,13 +895,15 @@ export async function broadcastRumor ({
|
|
|
878
895
|
fileChunkIndex,
|
|
879
896
|
onPreparedSeed,
|
|
880
897
|
preparedPublication,
|
|
898
|
+
preparedPublications,
|
|
881
899
|
_publish = privateChannel.publish
|
|
882
900
|
}) {
|
|
883
901
|
const receivers = uniq(receiverPubkeys)
|
|
884
902
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
903
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
885
904
|
const { event, wireEvent } = await makeOutgoingRumor({ senderSigner, rumor })
|
|
886
|
-
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability,
|
|
887
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion),
|
|
905
|
+
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability, preparedPublications: prepared })
|
|
906
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion), preparedPublications: prepared, _getIykcProofs, _publish })
|
|
888
907
|
return withDelivery({ rumor: event }, reports, deletion.deletionSeckey)
|
|
889
908
|
}
|
|
890
909
|
|
|
@@ -907,13 +926,15 @@ export async function broadcastEvent ({
|
|
|
907
926
|
fileChunkIndex,
|
|
908
927
|
onPreparedSeed,
|
|
909
928
|
preparedPublication,
|
|
929
|
+
preparedPublications,
|
|
910
930
|
_publish = privateChannel.publish
|
|
911
931
|
}) {
|
|
912
932
|
const receivers = uniq(receiverPubkeys)
|
|
913
933
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
934
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
914
935
|
const wireEvent = assertValidSignedEvent({ ...event, tags: cloneTags(event?.tags) })
|
|
915
|
-
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability,
|
|
916
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion),
|
|
936
|
+
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability, preparedPublications: prepared })
|
|
937
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion), preparedPublications: prepared, _getIykcProofs, _publish })
|
|
917
938
|
return withDelivery({ event: wireEvent }, reports, deletion.deletionSeckey)
|
|
918
939
|
}
|
|
919
940
|
|
|
@@ -4,6 +4,7 @@ import { createFileCache } from './helpers/cache.js'
|
|
|
4
4
|
export { createFileCache, DEFAULT_FILE_CACHE_BYTES } from './helpers/cache.js'
|
|
5
5
|
import { createFileCatalog } from './helpers/catalog.js'
|
|
6
6
|
import { assertMessagePublished } from '../helpers/publication.js'
|
|
7
|
+
import { normalizePreparedPublications } from '../../private-channel/helpers/prepared.js'
|
|
7
8
|
import { ValidationError } from '../../error/index.js'
|
|
8
9
|
import { IRFS_CHUNK_BYTES, decodeIrfsChunk } from '../../irfs/index.js'
|
|
9
10
|
import { createQueue } from '../../idb-queue/index.js'
|
|
@@ -151,9 +152,10 @@ export function createPrivateFileTransfer ({ messenger, resolveChannel, storage,
|
|
|
151
152
|
function sendOptions (file, routes) {
|
|
152
153
|
return { senderSigner: messenger.userSigner, imkcSigner: messenger.contentKeySigner, privateChannelSigner: file.signer, ...routes, temporaryStorageArea: messenger.temporaryStorageArea, expirationSeconds: messenger.eventExpirationSecondsFor(file.controlChannelPubkey), _getIykcProofs: messenger.contentKeyLookup() }
|
|
153
154
|
}
|
|
154
|
-
async function publishChunk (descriptor, event, { signal, attemptStartedAt, onPreparedSeed, preparedPublication, onDebug } = {}) {
|
|
155
|
+
async function publishChunk (descriptor, event, { signal, attemptStartedAt, onPreparedSeed, preparedPublication, preparedPublications, onDebug } = {}) {
|
|
155
156
|
const file = await register(descriptor)
|
|
156
157
|
usable(file)
|
|
158
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
157
159
|
const decoded = checked(file, event)
|
|
158
160
|
await catalog.ready
|
|
159
161
|
if (ownsStorage) {
|
|
@@ -162,7 +164,7 @@ export function createPrivateFileTransfer ({ messenger, resolveChannel, storage,
|
|
|
162
164
|
}
|
|
163
165
|
let result
|
|
164
166
|
try {
|
|
165
|
-
const options = { ...sendOptions(file, await routing(file, file.peerPubkey, signal, attemptStartedAt, onDebug)), receiverPubkeys: [file.peerPubkey], fileChunkIndex: decoded.index,
|
|
167
|
+
const options = { ...sendOptions(file, await routing(file, file.peerPubkey, signal, attemptStartedAt, onDebug)), receiverPubkeys: [file.peerPubkey], fileChunkIndex: decoded.index, preparedPublications: prepared.length ? prepared : undefined, ...(onPreparedSeed ? { onPreparedSeed } : messenger.channels.get(file.controlChannelPubkey)?.mode === 'watchtower' ? { onPreparedSeed: seed => saveSeed(file, seed) } : {}) }
|
|
166
168
|
result = event.sig ? await _messages.broadcastEvent({ ...options, event }) : await _messages.broadcastRumor({ ...options, rumor: event })
|
|
167
169
|
await assertMessagePublished(result, result?.rumor || result?.event || { ...event, pubkey: event.pubkey || messenger.userPubkey })
|
|
168
170
|
} catch (error) { await uploadLeases.get(file.root)?.(); uploadLeases.delete(file.root); throw error }
|
|
@@ -2,7 +2,7 @@ import { isReplaceableRelayFailure } from '../../relay/helpers/failure.js'
|
|
|
2
2
|
|
|
3
3
|
// Each invocation owns one already signed outer event and its encrypted subset.
|
|
4
4
|
// Lanes share coverage and the tried set; only started reports outlive the race.
|
|
5
|
-
export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tried, nextPrimary, nextFallback, sendEvent, isOnline, isCurrent, signal, pauseSignal, remember, rememberSuccess = () => {}, delay, attemptStartedAt, monotonicNow = () => performance.now(), now, setTimer, clearTimer }) {
|
|
5
|
+
export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tried, getRetryEvent, nextPrimary, nextFallback, sendEvent, isOnline, isCurrent, signal, pauseSignal, remember, rememberSuccess = () => {}, delay, attemptStartedAt, monotonicNow = () => performance.now(), now, setTimer, clearTimer }) {
|
|
6
6
|
const pending = new Set(receivers)
|
|
7
7
|
const summaries = []
|
|
8
8
|
const transportSignal = signal && pauseSignal ? AbortSignal.any([signal, pauseSignal]) : signal || pauseSignal
|
|
@@ -33,6 +33,13 @@ export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tr
|
|
|
33
33
|
...(interruptedWork && !signal?.aborted ? { retryWhenAvailable: true, ...(offline ? { retryWhenOnline: true } : {}) } : {})
|
|
34
34
|
})
|
|
35
35
|
}
|
|
36
|
+
const fail = error => {
|
|
37
|
+
if (done) return
|
|
38
|
+
done = true
|
|
39
|
+
clearTimer(timer); timer = null
|
|
40
|
+
transportSignal?.removeEventListener('abort', interrupted)
|
|
41
|
+
outcome.reject(error)
|
|
42
|
+
}
|
|
36
43
|
const check = () => {
|
|
37
44
|
if (done) return
|
|
38
45
|
if (!pending.size) finish(true)
|
|
@@ -59,7 +66,11 @@ export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tr
|
|
|
59
66
|
const slot = Promise.withResolvers()
|
|
60
67
|
summaries.push(slot.promise)
|
|
61
68
|
let result
|
|
62
|
-
try { result = await sendEvent(event, relays, { signal: transportSignal }) } catch (reason) {
|
|
69
|
+
try { result = await sendEvent(event, relays, { signal: transportSignal, getRetryEvent }) } catch (reason) {
|
|
70
|
+
if (reason?.code === 'RETRY_EVENT_PREPARATION_FAILED') {
|
|
71
|
+
fail(reason)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
63
74
|
result = { success: false, promise: Promise.resolve({ fulfilled: 0, errors: [{ reason }] }) }
|
|
64
75
|
}
|
|
65
76
|
const settled = Promise.resolve(result.promise).catch(reason => ({ fulfilled: 0, errors: [{ reason }] }))
|
|
@@ -123,6 +123,7 @@ export function createSendRelayRouting ({ peer, peers = [peer], relaysByPubkey,
|
|
|
123
123
|
}
|
|
124
124
|
const result = await publishWithEarlyFallback({
|
|
125
125
|
event, receivers, first: first.length ? { receivers, relays: first } : null, mirrors, tried,
|
|
126
|
+
getRetryEvent: context.getRetryEvent,
|
|
126
127
|
nextPrimary: pending => pick(pending, false), nextFallback: pending => pick(pending, true),
|
|
127
128
|
sendEvent, isOnline, isCurrent, signal, pauseSignal, remember, rememberSuccess,
|
|
128
129
|
delay: fallbackDelayMs, attemptStartedAt, monotonicNow, now, setTimer, clearTimer
|
|
@@ -158,7 +159,7 @@ export function createSendRelayRouting ({ peer, peers = [peer], relaysByPubkey,
|
|
|
158
159
|
for (const relay of relays) tried.add(normalized(relay))
|
|
159
160
|
// Every relay in this batch serves the same pending recipient subset.
|
|
160
161
|
// One ACK covers that subset, never unrelated members of the ciphertext.
|
|
161
|
-
result = await sendEvent(event, relays, { signal: transportSignal })
|
|
162
|
+
result = await sendEvent(event, relays, { signal: transportSignal, getRetryEvent: context.getRetryEvent })
|
|
162
163
|
const settled = Promise.resolve(result.promise)
|
|
163
164
|
summaries.push(settled)
|
|
164
165
|
settled.then(report => rememberSuccess(report?.succeededRelays || [])).catch(() => {})
|