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 +51 -2
- package/helpers/secp256k1-wasm/backend.js +2 -3
- package/package.json +3 -3
- 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/services/pending-decryption.js +20 -4
- 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/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
|
|
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 =
|
|
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.
|
|
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.
|
|
55
|
-
"build:wasm": "node wasm/build.
|
|
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
|
+
}
|
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
|
}
|