libp2r2p 0.0.1 → 0.0.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
@@ -44,40 +44,87 @@ await messenger.tell({
44
44
 
45
45
  ### Deleting Private Broadcasts
46
46
 
47
- High-level private-message sends create one deletion capability for the whole
48
- logical message. The returned object keeps its normal result fields and adds
49
- `deletionPubkey` plus, when the library generated the key, `deletionSeckey`.
50
- All outer kind `3560` chunks for that send carry the public
51
- `['s', deletionPubkey]` tag.
47
+ By default, each high-level private-message send creates one fresh deletion
48
+ keypair for its logical message. Every outer kind `3560` event produced for that
49
+ send, including router chunks, recipient subsets, and nym carriers, carries the
50
+ same public key in its `s` tag. The result always contains `delivery.reports`.
51
+ When libp2r2p generated the keypair, it also contains
52
+ `delivery.deletionSeckey`; the public key can be derived from that secret.
53
+
54
+ This shared `s` value deliberately makes the outer events for one logical send
55
+ linkable to relay operators and other observers. Disable automatic capabilities
56
+ when that tradeoff is not acceptable. The messenger-wide setting defaults to
57
+ `true`, and a channel setting takes precedence:
52
58
 
53
- Pass a pre-generated `deletionPubkey` when the application needs to own and
54
- persist the secret before sending. In that case the result deliberately omits a
55
- secret it was not given:
59
+ ```js
60
+ const messenger = await createPrivateMessenger({
61
+ userSigner,
62
+ autoDeletionCapability: false,
63
+ channels: [{
64
+ signer: privateChannelSigner,
65
+ relays: ['wss://relay.example'],
66
+ autoDeletionCapability: true
67
+ }]
68
+ })
69
+ ```
70
+
71
+ With automatic capabilities disabled and no caller-supplied key, the outer
72
+ events have no `s` tag. They are not deliberately linkable through this
73
+ extension, but cannot later be deleted with it. A caller that already owns a
74
+ deletion key can supply its public key on an individual send; libp2r2p then
75
+ does not generate or return a key. Use a fresh caller-owned key for each
76
+ logical message unless cross-message linkability is intentional:
56
77
 
57
78
  ```js
58
79
  import { generateKeypair } from 'libp2r2p/key'
59
80
 
60
81
  const deletionKey = generateKeypair()
61
- const sent = await messenger.tell({
82
+ await messenger.tell({
62
83
  receiverPubkey,
63
84
  payload: { text: 'remove this later' },
64
85
  deletionPubkey: deletionKey.pubkey
65
86
  })
87
+ ```
88
+
89
+ ```js
90
+ import { finalizeEvent } from 'nostr-tools'
91
+ import { keypairFromSeckey } from 'libp2r2p/key'
92
+ import { relayPool } from 'libp2r2p/relay'
93
+
94
+ const sent = await messenger.tell({
95
+ receiverPubkey,
96
+ payload: { text: 'remove this later' }
97
+ })
66
98
 
67
- // Persist deletionKey.seckey with the application's copy of the message.
68
- console.log(sent.deletionPubkey)
99
+ if (sent.delivery.deletionSeckey) {
100
+ const deletionKey = keypairFromSeckey(sent.delivery.deletionSeckey)
101
+ // Persist this secret with the application's copy of the logical message.
102
+
103
+ const { result: outerEvents } = await relayPool.getEvents({
104
+ kinds: [3560],
105
+ authors: [channelPubkey],
106
+ '#s': [deletionKey.pubkey]
107
+ }, relays)
108
+ for (let offset = 0; offset < outerEvents.length; offset += 100) {
109
+ const deletion = finalizeEvent({
110
+ kind: 5,
111
+ created_at: Math.floor(Date.now() / 1000),
112
+ tags: [['k', '3560'], ...outerEvents.slice(offset, offset + 100).map(event => ['e', event.id])],
113
+ content: ''
114
+ }, deletionKey.secretKey)
115
+ await relayPool.sendEvent(deletion, relays)
116
+ }
117
+ }
69
118
  ```
70
119
 
71
- The `s` tag is public metadata, so it makes the outer chunks for one message
72
- linkable. It is a deletion capability, not the channel key or the sender's
73
- identity. libp2r2p does not automatically delete anything: an application can
74
- later find candidate outer events with a filter such as
75
- `{ kinds: [3560], authors: [channelPubkey], '#s': [deletionPubkey] }`, then
76
- sign a kind `5` deletion event with the matching secret key. Relay support for
77
- this capability is not universal. A relay that supports it SHOULD recognize
78
- only the explicit `k=3560` deletion form; a regular NIP-09 kind `5` event
79
- signed by the outer event's private-channel key MUST NOT delete a kind `3560`
80
- event, whether or not that event carries an `s` tag.
120
+ The `s` tag is public metadata and a deletion capability, not the channel key
121
+ or sender identity. libp2r2p does not delete anything automatically.
122
+
123
+ Relay support for this capability is not universal. A relay that implements it
124
+ should accept only a kind `5` request signed by the matching `s` key with exactly
125
+ one `['k', '3560']` tag, explicit matching `e` targets, and no `a` tags. A
126
+ regular NIP-09 kind `5` request signed by the outer event's private-channel key
127
+ must not delete a kind `3560` event, whether or not that event has an `s` tag.
81
128
 
82
129
  ### Temporary Send Storage
83
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -527,13 +527,13 @@ export async function publish ({ senderSigner, imkcSigner, privateChannelSigner
527
527
  } finally {
528
528
  cleanupEnvelopeRows(context.preparedRows)
529
529
  }
530
- return { results }
530
+ return results
531
531
  }
532
532
 
533
533
  for await (const wrappedEvent of wrapEvents({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds, temporaryStorageArea, _getIykcProofs })) {
534
534
  results.push(await _publish(wrappedEvent, withRecoveryRelays(relays, recoveryRelays)))
535
535
  }
536
- return { results }
536
+ return results
537
537
  }
538
538
 
539
539
  export async function publishNymEvent ({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, _publish = sendToRelays }) {
@@ -543,7 +543,7 @@ export async function publishNymEvent ({ nymSigner, privateChannelSigner, privat
543
543
  for await (const wrappedEvent of wrapNymEvents({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: normalizedDeletionPubkey, event, expirationSeconds })) {
544
544
  results.push(await _publish(wrappedEvent, publishRelays))
545
545
  }
546
- return { results }
546
+ return results
547
547
  }
548
548
 
549
549
  function readSignerFromMap (signersByPubkey, pubkey) {
@@ -21,22 +21,32 @@ function uniq (values) {
21
21
  return [...new Set((values || []).filter(Boolean))]
22
22
  }
23
23
 
24
- function resolveDeletionCapability (deletionPubkey) {
25
- if (deletionPubkey !== undefined) {
26
- if (typeof deletionPubkey !== 'string' || !HEX_PUBKEY.test(deletionPubkey)) {
27
- throw new Error('INVALID_DELETION_PUBKEY')
28
- }
29
- return { deletionPubkey: deletionPubkey.toLowerCase() }
24
+ function normalizeDeletionPubkey (deletionPubkey) {
25
+ if (deletionPubkey === undefined) return undefined
26
+ if (typeof deletionPubkey !== 'string' || !HEX_PUBKEY.test(deletionPubkey)) {
27
+ throw new Error('INVALID_DELETION_PUBKEY')
30
28
  }
29
+ return deletionPubkey.toLowerCase()
30
+ }
31
+
32
+ function resolveDeletionCapability ({ deletionPubkey, deletionSeckey, autoDeletionCapability = true }) {
33
+ if (deletionSeckey !== undefined) throw new Error('DELETION_SECKEY_NOT_ACCEPTED')
34
+ if (typeof autoDeletionCapability !== 'boolean') throw new Error('AUTO_DELETION_CAPABILITY_BOOLEAN_REQUIRED')
35
+
36
+ const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
37
+ if (normalizedDeletionPubkey) return { deletionPubkey: normalizedDeletionPubkey }
38
+ if (!autoDeletionCapability) return {}
39
+
31
40
  const { pubkey, seckey } = generateKeypair()
32
41
  return { deletionPubkey: pubkey, deletionSeckey: seckey }
33
42
  }
34
43
 
35
- function withDeletionCapability (result, capability) {
44
+ function withDelivery (result, reports, deletionSeckey) {
45
+ const delivery = { reports }
46
+ if (deletionSeckey !== undefined) delivery.deletionSeckey = deletionSeckey
36
47
  return {
37
48
  ...result,
38
- deletionPubkey: capability.deletionPubkey,
39
- ...(capability.deletionSeckey && { deletionSeckey: capability.deletionSeckey })
49
+ delivery
40
50
  }
41
51
  }
42
52
 
@@ -457,8 +467,10 @@ export async function ask ({
457
467
  content,
458
468
  expirationSeconds,
459
469
  temporaryStorageArea,
460
- deletionPubkey,
461
470
  _getIykcProofs,
471
+ deletionPubkey,
472
+ deletionSeckey,
473
+ autoDeletionCapability = true,
462
474
  _publish = privateChannel.publish
463
475
  }) {
464
476
  if (!receiverPubkey) throw new Error('RECEIVER_PUBKEY_REQUIRED')
@@ -474,10 +486,10 @@ export async function ask ({
474
486
  message: message || { code, payload, error, content }
475
487
  })
476
488
  })
477
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
478
- const results = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
489
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
490
+ const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
479
491
 
480
- return withDeletionCapability({ question, results }, deletionCapability)
492
+ return withDelivery({ question }, reports, deletion.deletionSeckey)
481
493
  }
482
494
 
483
495
  export async function reply ({
@@ -497,8 +509,10 @@ export async function reply ({
497
509
  content,
498
510
  expirationSeconds,
499
511
  temporaryStorageArea,
500
- deletionPubkey,
501
512
  _getIykcProofs,
513
+ deletionPubkey,
514
+ deletionSeckey,
515
+ autoDeletionCapability = true,
502
516
  _publish = privateChannel.publish
503
517
  }) {
504
518
  if (!question?.id) throw new Error('QUESTION_REQUIRED')
@@ -511,9 +525,9 @@ export async function reply ({
511
525
  message: message || { code, payload, error, content }
512
526
  })
513
527
  })
514
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
515
- const results = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
516
- return withDeletionCapability({ reply: event, results }, deletionCapability)
528
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
529
+ const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
530
+ return withDelivery({ reply: event }, reports, deletion.deletionSeckey)
517
531
  }
518
532
 
519
533
  export async function tell ({
@@ -532,8 +546,10 @@ export async function tell ({
532
546
  content,
533
547
  expirationSeconds,
534
548
  temporaryStorageArea,
535
- deletionPubkey,
536
549
  _getIykcProofs,
550
+ deletionPubkey,
551
+ deletionSeckey,
552
+ autoDeletionCapability = true,
537
553
  _publish = privateChannel.publish
538
554
  }) {
539
555
  if (!receiverPubkey) throw new Error('RECEIVER_PUBKEY_REQUIRED')
@@ -545,9 +561,9 @@ export async function tell ({
545
561
  message: message || { code, payload, error, content }
546
562
  })
547
563
  })
548
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
549
- const results = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
550
- return withDeletionCapability({ tell: event, results }, deletionCapability)
564
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
565
+ const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers: [receiverPubkey], receiverTag: receiverPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
566
+ return withDelivery({ tell: event }, reports, deletion.deletionSeckey)
551
567
  }
552
568
 
553
569
  export async function yell ({
@@ -566,8 +582,10 @@ export async function yell ({
566
582
  content,
567
583
  expirationSeconds,
568
584
  temporaryStorageArea,
569
- deletionPubkey,
570
585
  _getIykcProofs,
586
+ deletionPubkey,
587
+ deletionSeckey,
588
+ autoDeletionCapability = true,
571
589
  _publish = privateChannel.publish
572
590
  }) {
573
591
  const receivers = uniq(receiverPubkeys)
@@ -580,9 +598,9 @@ export async function yell ({
580
598
  message: message || { code, payload, error, content }
581
599
  })
582
600
  })
583
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
584
- const results = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
585
- return withDeletionCapability({ yell: event, results }, deletionCapability)
601
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
602
+ const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
603
+ return withDelivery({ yell: event }, reports, deletion.deletionSeckey)
586
604
  }
587
605
 
588
606
  export async function broadcastRumor ({
@@ -597,16 +615,18 @@ export async function broadcastRumor ({
597
615
  rumor,
598
616
  expirationSeconds,
599
617
  temporaryStorageArea,
600
- deletionPubkey,
601
618
  _getIykcProofs,
619
+ deletionPubkey,
620
+ deletionSeckey,
621
+ autoDeletionCapability = true,
602
622
  _publish = privateChannel.publish
603
623
  }) {
604
624
  const receivers = uniq(receiverPubkeys)
605
625
  if (!receivers.length) throw new Error('NO_RECEIVERS')
606
626
  const { event, wireEvent } = await makeOutgoingRumor({ senderSigner, rumor })
607
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
608
- const results = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
609
- return withDeletionCapability({ rumor: event, results }, deletionCapability)
627
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
628
+ const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
629
+ return withDelivery({ rumor: event }, reports, deletion.deletionSeckey)
610
630
  }
611
631
 
612
632
  export async function broadcastEvent ({
@@ -621,16 +641,18 @@ export async function broadcastEvent ({
621
641
  event,
622
642
  expirationSeconds,
623
643
  temporaryStorageArea,
624
- deletionPubkey,
625
644
  _getIykcProofs,
645
+ deletionPubkey,
646
+ deletionSeckey,
647
+ autoDeletionCapability = true,
626
648
  _publish = privateChannel.publish
627
649
  }) {
628
650
  const receivers = uniq(receiverPubkeys)
629
651
  if (!receivers.length) throw new Error('NO_RECEIVERS')
630
652
  const wireEvent = assertValidSignedEvent({ ...event, tags: cloneTags(event?.tags) })
631
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
632
- const results = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
633
- return withDeletionCapability({ event: wireEvent, results }, deletionCapability)
653
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
654
+ const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, _getIykcProofs, _publish })
655
+ return withDelivery({ event: wireEvent }, reports, deletion.deletionSeckey)
634
656
  }
635
657
 
636
658
  export async function broadcastNymRumor ({
@@ -643,13 +665,15 @@ export async function broadcastNymRumor ({
643
665
  rumor,
644
666
  expirationSeconds,
645
667
  deletionPubkey,
668
+ deletionSeckey,
669
+ autoDeletionCapability = true,
646
670
  _publish = privateChannel.publishNymEvent
647
671
  }) {
648
672
  if (!nymSigner?.getPublicKey) throw new Error('NYM_SIGNER_REQUIRED')
649
673
  const { event, wireEvent } = await makeOutgoingRumor({ senderSigner: nymSigner, rumor })
650
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
651
- const results = await sendNymMessage({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, _publish })
652
- return withDeletionCapability({ rumor: event, results }, deletionCapability)
674
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
675
+ const reports = await sendNymMessage({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, _publish })
676
+ return withDelivery({ rumor: event }, reports, deletion.deletionSeckey)
653
677
  }
654
678
 
655
679
  export async function broadcastNymEvent ({
@@ -662,11 +686,13 @@ export async function broadcastNymEvent ({
662
686
  event,
663
687
  expirationSeconds,
664
688
  deletionPubkey,
689
+ deletionSeckey,
690
+ autoDeletionCapability = true,
665
691
  _publish = privateChannel.publishNymEvent
666
692
  }) {
667
693
  if (!nymSigner?.getPublicKey) throw new Error('NYM_SIGNER_REQUIRED')
668
694
  const wireEvent = assertValidSignedEvent({ ...event, tags: cloneTags(event?.tags) })
669
- const deletionCapability = resolveDeletionCapability(deletionPubkey)
670
- const results = await sendNymMessage({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: deletionCapability.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, _publish })
671
- return withDeletionCapability({ event: wireEvent, results }, deletionCapability)
695
+ const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
696
+ const reports = await sendNymMessage({ nymSigner, privateChannelSigner, privateChannelReaderPubkey, deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, _publish })
697
+ return withDelivery({ event: wireEvent }, reports, deletion.deletionSeckey)
672
698
  }
@@ -112,6 +112,11 @@ function uniq (values) {
112
112
  return [...new Set((values || []).filter(Boolean))]
113
113
  }
114
114
 
115
+ function normalizeAutoDeletionCapability (value) {
116
+ if (typeof value !== 'boolean') throw new Error('AUTO_DELETION_CAPABILITY_BOOLEAN_REQUIRED')
117
+ return value
118
+ }
119
+
115
120
  function isPlainObject (value) {
116
121
  return value && typeof value === 'object' && !Array.isArray(value)
117
122
  }
@@ -140,6 +145,7 @@ export class PrivateMessenger {
140
145
  messageQueueMaxBytes = DEFAULT_MESSAGE_QUEUE_MAX_BYTES,
141
146
  seedQueueMaxBytes = DEFAULT_SEED_QUEUE_MAX_BYTES,
142
147
  temporaryStorageArea = globalThis.sessionStorage,
148
+ autoDeletionCapability = true,
143
149
  _indexedDB = globalThis.indexedDB,
144
150
  useContentKeys = true,
145
151
  onContentKeyChange,
@@ -165,6 +171,7 @@ export class PrivateMessenger {
165
171
  this.messageQueueMaxBytes = messageQueueMaxBytes
166
172
  this.seedQueueMaxBytes = seedQueueMaxBytes
167
173
  this.temporaryStorageArea = temporaryStorageArea
174
+ this.autoDeletionCapability = normalizeAutoDeletionCapability(autoDeletionCapability)
168
175
  this._indexedDB = _indexedDB
169
176
  this.useContentKeys = useContentKeys
170
177
  this.onContentKeyChange = onContentKeyChange
@@ -304,6 +311,9 @@ export class PrivateMessenger {
304
311
  const mode = channel.mode || defaults.mode || 'leecher'
305
312
  if (!signer && storesRecoverySeeds(mode)) throw new Error('PRIVATE_CHANNEL_WRITER_REQUIRED')
306
313
  const readerPubkey = channel.readerPubkey || channel.privateChannelReaderPubkey || await readerSigner?.getPublicKey?.() || pubkey
314
+ const autoDeletionCapability = channel.autoDeletionCapability === undefined
315
+ ? undefined
316
+ : normalizeAutoDeletionCapability(channel.autoDeletionCapability)
307
317
  out.push({
308
318
  pubkey,
309
319
  signer,
@@ -314,7 +324,8 @@ export class PrivateMessenger {
314
324
  sendRelays: uniq(hasChannelSendRelays ? channel.sendRelays : []),
315
325
  usesNip65WatchRelays: !hasChannelRelays && !hasDefaultRelays,
316
326
  mode,
317
- seeders: uniq(channel.seeders)
327
+ seeders: uniq(channel.seeders),
328
+ autoDeletionCapability
318
329
  })
319
330
  }
320
331
  return out
@@ -807,12 +818,13 @@ export class PrivateMessenger {
807
818
  ...routing,
808
819
  expirationSeconds: this.offlineRecoverySeconds,
809
820
  temporaryStorageArea: this.temporaryStorageArea,
821
+ deletionPubkey,
822
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
810
823
  message,
811
824
  code,
812
825
  payload,
813
826
  error,
814
827
  content,
815
- deletionPubkey,
816
828
  _getIykcProofs: this.contentKeyLookup()
817
829
  })
818
830
  }
@@ -832,12 +844,13 @@ export class PrivateMessenger {
832
844
  ...routing,
833
845
  expirationSeconds: this.offlineRecoverySeconds,
834
846
  temporaryStorageArea: this.temporaryStorageArea,
847
+ deletionPubkey,
848
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
835
849
  message,
836
850
  code,
837
851
  payload,
838
852
  error,
839
853
  content,
840
- deletionPubkey,
841
854
  _getIykcProofs: this.contentKeyLookup()
842
855
  })
843
856
  }
@@ -855,12 +868,13 @@ export class PrivateMessenger {
855
868
  ...routing,
856
869
  expirationSeconds: this.offlineRecoverySeconds,
857
870
  temporaryStorageArea: this.temporaryStorageArea,
871
+ deletionPubkey,
872
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
858
873
  message,
859
874
  code,
860
875
  payload,
861
876
  error,
862
877
  content,
863
- deletionPubkey,
864
878
  _getIykcProofs: this.contentKeyLookup()
865
879
  })
866
880
  }
@@ -878,12 +892,13 @@ export class PrivateMessenger {
878
892
  ...routing,
879
893
  expirationSeconds: this.offlineRecoverySeconds,
880
894
  temporaryStorageArea: this.temporaryStorageArea,
895
+ deletionPubkey,
896
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
881
897
  message,
882
898
  code,
883
899
  payload,
884
900
  error,
885
901
  content,
886
- deletionPubkey,
887
902
  _getIykcProofs: this.contentKeyLookup()
888
903
  })
889
904
  }
@@ -901,8 +916,9 @@ export class PrivateMessenger {
901
916
  ...routing,
902
917
  expirationSeconds: this.offlineRecoverySeconds,
903
918
  temporaryStorageArea: this.temporaryStorageArea,
904
- rumor,
905
919
  deletionPubkey,
920
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
921
+ rumor,
906
922
  _getIykcProofs: this.contentKeyLookup()
907
923
  })
908
924
  }
@@ -920,8 +936,9 @@ export class PrivateMessenger {
920
936
  ...routing,
921
937
  expirationSeconds: this.offlineRecoverySeconds,
922
938
  temporaryStorageArea: this.temporaryStorageArea,
923
- event,
924
939
  deletionPubkey,
940
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
941
+ event,
925
942
  _getIykcProofs: this.contentKeyLookup()
926
943
  })
927
944
  }
@@ -937,8 +954,9 @@ export class PrivateMessenger {
937
954
  privateChannelReaderPubkey: channel.readerPubkey,
938
955
  ...routing,
939
956
  expirationSeconds: this.offlineRecoverySeconds,
940
- rumor,
941
- deletionPubkey
957
+ deletionPubkey,
958
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
959
+ rumor
942
960
  })
943
961
  }
944
962
 
@@ -953,8 +971,9 @@ export class PrivateMessenger {
953
971
  privateChannelReaderPubkey: channel.readerPubkey,
954
972
  ...routing,
955
973
  expirationSeconds: this.offlineRecoverySeconds,
956
- event,
957
- deletionPubkey
974
+ deletionPubkey,
975
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
976
+ event
958
977
  })
959
978
  }
960
979
 
@@ -972,6 +991,7 @@ export class PrivateMessenger {
972
991
  ...routing,
973
992
  expirationSeconds: this.offlineRecoverySeconds,
974
993
  temporaryStorageArea: this.temporaryStorageArea,
994
+ autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
975
995
  code: SEEDER_PRESENCE_CODE,
976
996
  payload: {},
977
997
  _getIykcProofs: this.contentKeyLookup()
@@ -1036,6 +1056,10 @@ export class PrivateMessenger {
1036
1056
  return channel
1037
1057
  }
1038
1058
 
1059
+ autoDeletionCapabilityFor (channel) {
1060
+ return channel.autoDeletionCapability ?? this.autoDeletionCapability
1061
+ }
1062
+
1039
1063
  requireNymSigner (channel, override) {
1040
1064
  const signer = override || channel?.nymSigner || this.nymSigner
1041
1065
  if (!signer?.getPublicKey) throw new Error('NYM_SIGNER_REQUIRED')
@@ -79,7 +79,7 @@ export function createPublishSettlements (promises, timeout, { onSettled } = {})
79
79
 
80
80
  // Turns ordered relay settlements into a stable, caller-facing report. Failed
81
81
  // relays retain their Error while optional successful URLs make acknowledgements visible.
82
- export function publishSummary (settlements, relays, { result, includeSucceededRelays = false } = {}) {
82
+ export function publishSummary (settlements, relays, { includeSucceededRelays = false } = {}) {
83
83
  const succeededRelays = []
84
84
  const errors = []
85
85
 
@@ -94,7 +94,6 @@ export function publishSummary (settlements, relays, { result, includeSucceededR
94
94
  fulfilled: succeededRelays.length,
95
95
  errors
96
96
  }
97
- if (result !== undefined) summary.result = result
98
97
  if (includeSucceededRelays) summary.succeededRelays = succeededRelays
99
98
  return summary
100
99
  }
@@ -842,10 +842,9 @@ export class RelayPool {
842
842
  const urls = normalizedRelayUrls(relays)
843
843
  if (!urls.length) {
844
844
  const promise = Promise.resolve(publishSummary([], urls, {
845
- result: null,
846
845
  includeSucceededRelays: true
847
846
  }))
848
- return { result: null, total: 0, success: false, promise }
847
+ return { total: 0, success: false, promise }
849
848
  }
850
849
 
851
850
  const eventToSend = event.meta ? { ...event } : event
@@ -862,11 +861,10 @@ export class RelayPool {
862
861
  })
863
862
 
864
863
  // Resolves after every relay settles (or reaches the operation timeout) as
865
- // { result: null, success, total, fulfilled, succeededRelays, errors },
864
+ // { success, total, fulfilled, succeededRelays, errors },
866
865
  // where errors contains { relay, reason } entries for failed relays.
867
866
  const promise = settlement.promise
868
867
  .then(settlements => publishSummary(settlements, urls, {
869
- result: null,
870
868
  includeSucceededRelays: true
871
869
  }))
872
870
 
@@ -895,7 +893,6 @@ export class RelayPool {
895
893
  if (!success) settlement.timeout()
896
894
 
897
895
  return {
898
- result: null,
899
896
  total: urls.length,
900
897
  success,
901
898
  promise
@@ -445,7 +445,7 @@ test('wrapEvent creates private broadcast events under relay event size limit',
445
445
  assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
446
446
  })
447
447
 
448
- test('private broadcast wrappers add a normalized deletion pubkey when supplied', async () => {
448
+ test('private broadcast wrappers share and normalize one deletion pubkey per logical message', async () => {
449
449
  const alice = signer()
450
450
  const bob = signer()
451
451
  const bobPubkey = await bob.getPublicKey()
@@ -477,6 +477,7 @@ test('private broadcast wrappers add a normalized deletion pubkey when supplied'
477
477
  /INVALID_DELETION_PUBKEY/
478
478
  )
479
479
  }
480
+
480
481
  })
481
482
 
482
483
  test('publish splits relay-targeted routers by receiver subset with separate router pubkeys', async () => {
@@ -500,7 +501,7 @@ test('publish splits relay-targeted routers by receiver subset with separate rou
500
501
  }
501
502
  }
502
503
 
503
- await publish({
504
+ const reports = await publish({
504
505
  senderSigner: alice,
505
506
  receivers: [bobPubkey, carolPubkey, davePubkey],
506
507
  relayToReceivers: new Map([
@@ -524,6 +525,7 @@ test('publish splits relay-targeted routers by receiver subset with separate rou
524
525
  ['s', 'd'.repeat(64)],
525
526
  ['s', 'd'.repeat(64)]
526
527
  ])
528
+ assert.deepEqual(reports, [{ success: true }, { success: true }])
527
529
  assert.deepEqual(published[0].relays, ['wss://one.example', 'wss://two.example', 'wss://seed.example'])
528
530
  assert.deepEqual(published[1].relays, ['wss://three.example', 'wss://seed.example', 'wss://one.example'])
529
531
 
@@ -626,7 +628,7 @@ test('publishNymEvent mirrors carrier chunks to recovery relays', async () => {
626
628
  const nym = signer()
627
629
  const published = []
628
630
 
629
- await publishNymEvent({
631
+ const reports = await publishNymEvent({
630
632
  nymSigner: nym,
631
633
  privateChannelSigner: channel,
632
634
  event: eventFixture('nym mirror'),
@@ -642,6 +644,7 @@ test('publishNymEvent mirrors carrier chunks to recovery relays', async () => {
642
644
  assert.equal(published.length, 1)
643
645
  assert.deepEqual(published[0].relays, ['wss://receiver.example', 'wss://seed.example'])
644
646
  assert.deepEqual(published[0].outer.tags.find(tag => tag[0] === 's'), ['s', 'e'.repeat(64)])
647
+ assert.deepEqual(reports, [{ success: true }])
645
648
  assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
646
649
  })
647
650
 
@@ -955,6 +958,7 @@ test('subscribe buffers out-of-order nym carrier chunks by nym pubkey and inner
955
958
  const wrapped = await wrapNymEvent({
956
959
  nymSigner: nym,
957
960
  privateChannelSigner: channel,
961
+ deletionPubkey: 'e'.repeat(64),
958
962
  event: original
959
963
  })
960
964
  const routedEvents = []
@@ -963,6 +967,7 @@ test('subscribe buffers out-of-order nym carrier chunks by nym pubkey and inner
963
967
 
964
968
  assert.ok(wrapped.length > 1)
965
969
  for (const event of wrapped) assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
970
+ assert.deepEqual(new Set(wrapped.map(event => event.tags.find(tag => tag[0] === 's')?.[1])), new Set(['e'.repeat(64)]))
966
971
 
967
972
  subscribe({
968
973
  privateChannelSigner: channel,
@@ -1563,14 +1568,22 @@ test('wrapEvent chunks large jsonl without oversize events and unwraps reassembl
1563
1568
  const imkcPubkey = await imkc.getPublicKey()
1564
1569
  const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
1565
1570
  NsecSigner.setContentSigners(alice, [imkc])
1566
- const deletionPubkey = 'd'.repeat(64)
1567
- const wrapped = await wrapEvent({ senderSigner: alice, imkcSigner: imkc, receivers: [bobPubkey], deletionPubkey, event: original, _getIykcProofs: noContentKeys })
1571
+ const wrapped = await wrapEvent({
1572
+ senderSigner: alice,
1573
+ imkcSigner: imkc,
1574
+ receivers: [bobPubkey],
1575
+ deletionPubkey: 'd'.repeat(64),
1576
+ event: original,
1577
+ _getIykcProofs: noContentKeys
1578
+ })
1568
1579
 
1569
1580
  assert.ok(wrapped.length > 1)
1581
+ const deletionPubkeys = new Set()
1570
1582
  for (const event of wrapped) {
1571
1583
  assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
1572
- assert.deepEqual(event.tags.find(tag => tag[0] === 's'), ['s', deletionPubkey])
1584
+ deletionPubkeys.add(event.tags.find(tag => tag[0] === 's')?.[1])
1573
1585
  }
1586
+ assert.deepEqual(deletionPubkeys, new Set(['d'.repeat(64)]))
1574
1587
 
1575
1588
  const routers = []
1576
1589
  for (const event of wrapped) {
@@ -146,7 +146,7 @@ test('ask requires watching the sender private channel first', async () => {
146
146
  receiverPubkey: 'receiver',
147
147
  relays: ['wss://relay.example'],
148
148
  message: { code: 'PING' },
149
- _publish: async () => ({ results: [] })
149
+ _publish: async () => []
150
150
  }),
151
151
  /PRIVATE_MESSAGE_NOT_WATCHING/
152
152
  )
@@ -170,7 +170,7 @@ test('ask from a reader-only channel requires a writer signer', async () => {
170
170
  receiverPubkey: 'receiver',
171
171
  relays: ['wss://relay.example'],
172
172
  message: { code: 'PING' },
173
- _publish: async () => ({ results: [] })
173
+ _publish: async () => []
174
174
  }),
175
175
  /PRIVATE_CHANNEL_WRITER_REQUIRED/
176
176
  )
@@ -220,7 +220,7 @@ test('ask publishes an ask rumor and watch dispatches the reply with its questio
220
220
  message: { code: 'PING', payload: { ok: true } },
221
221
  _publish: async options => {
222
222
  published = options
223
- return { results: [{ success: true }] }
223
+ return [{ success: true }]
224
224
  }
225
225
  })
226
226
 
@@ -236,6 +236,8 @@ test('ask publishes an ask rumor and watch dispatches the reply with its questio
236
236
  assert.deepEqual(published.event.tags, [['r', 'receiver'], ['h', 'PING']])
237
237
  assert.equal(result.question.pubkey, senderPubkey)
238
238
  assert.equal(result.question.id, getEventHash({ ...published.event, pubkey: senderPubkey }))
239
+ assert.deepEqual(result.delivery.reports, [{ success: true }])
240
+ assert.equal(keypairFromSeckey(result.delivery.deletionSeckey).pubkey, published.deletionPubkey)
239
241
  assert.throws(() => getEventHash(published.event), /wrong or missing properties/)
240
242
 
241
243
  calls[0].onEvent({
@@ -259,7 +261,7 @@ test('reply tell and yell publish recognizable private message rumors', async ()
259
261
  const bobPubkey = pubkeyFixture(3)
260
262
  const _publish = async options => {
261
263
  published.push(options)
262
- return { results: [] }
264
+ return []
263
265
  }
264
266
  const question = {
265
267
  id: 'question-id',
@@ -321,37 +323,49 @@ test('reply tell and yell publish recognizable private message rumors', async ()
321
323
  assert.deepEqual(signedResult.event, signedEvent)
322
324
  })
323
325
 
324
- test('private message sends return generated deletion keys and forward supplied pubkeys', async () => {
326
+ test('private message sends share one deletion capability and return delivery reports', async () => {
325
327
  const published = []
328
+ const reports = [{ success: true, total: 1 }, { success: false, total: 1 }]
326
329
  const _publish = async options => {
327
330
  published.push(options)
328
- return { results: [] }
331
+ return reports
329
332
  }
330
- const suppliedPubkey = pubkeyFixture(99).toUpperCase()
331
333
 
332
- const supplied = await tell({
334
+ const generated = await tell({
333
335
  senderSigner: signer(pubkeyFixture(3)),
334
336
  receiverPubkey: 'alice',
335
337
  relays: ['wss://relay.example'],
336
- payload: 'supplied',
337
- deletionPubkey: suppliedPubkey,
338
+ payload: 'managed',
338
339
  _publish
339
340
  })
340
- const generated = await broadcastNymRumor({
341
- nymSigner: signer(pubkeyFixture(4)),
342
- privateChannelSigner: signer(pubkeyFixture(5)),
341
+
342
+ assert.deepEqual(generated.delivery.reports, reports)
343
+ assert.equal(keypairFromSeckey(generated.delivery.deletionSeckey).pubkey, published[0].deletionPubkey)
344
+ assert.equal('results' in generated, false)
345
+ assert.equal('deletionPubkey' in generated, false)
346
+ assert.equal('deletionSeckey' in generated, false)
347
+
348
+ const supplied = await tell({
349
+ senderSigner: signer(pubkeyFixture(3)),
350
+ receiverPubkey: 'alice',
343
351
  relays: ['wss://relay.example'],
344
- rumor: { kind: 9001, created_at: 1, tags: [], content: 'generated' },
352
+ payload: 'caller-managed',
353
+ deletionPubkey: 'A'.repeat(64),
345
354
  _publish
346
355
  })
356
+ assert.equal(published[1].deletionPubkey, 'a'.repeat(64))
357
+ assert.deepEqual(supplied.delivery, { reports })
347
358
 
348
- assert.equal(supplied.deletionPubkey, suppliedPubkey.toLowerCase())
349
- assert.equal('deletionSeckey' in supplied, false)
350
- assert.equal(published[0].deletionPubkey, suppliedPubkey.toLowerCase())
351
- assert.match(generated.deletionPubkey, /^[0-9a-f]{64}$/)
352
- assert.match(generated.deletionSeckey, /^[0-9a-f]{64}$/)
353
- assert.equal(keypairFromSeckey(generated.deletionSeckey).pubkey, generated.deletionPubkey)
354
- assert.equal(published[1].deletionPubkey, generated.deletionPubkey)
359
+ const untagged = await tell({
360
+ senderSigner: signer(pubkeyFixture(3)),
361
+ receiverPubkey: 'alice',
362
+ relays: ['wss://relay.example'],
363
+ payload: 'unlinkable',
364
+ autoDeletionCapability: false,
365
+ _publish
366
+ })
367
+ assert.equal(published[2].deletionPubkey, undefined)
368
+ assert.deepEqual(untagged.delivery, { reports })
355
369
 
356
370
  await assert.rejects(
357
371
  () => tell({
@@ -364,6 +378,28 @@ test('private message sends return generated deletion keys and forward supplied
364
378
  }),
365
379
  /INVALID_DELETION_PUBKEY/
366
380
  )
381
+ await assert.rejects(
382
+ () => tell({
383
+ senderSigner: signer(pubkeyFixture(3)),
384
+ receiverPubkey: 'alice',
385
+ relays: ['wss://relay.example'],
386
+ payload: 'bad secret',
387
+ deletionSeckey: 'b'.repeat(64),
388
+ _publish
389
+ }),
390
+ /DELETION_SECKEY_NOT_ACCEPTED/
391
+ )
392
+ await assert.rejects(
393
+ () => tell({
394
+ senderSigner: signer(pubkeyFixture(3)),
395
+ receiverPubkey: 'alice',
396
+ relays: ['wss://relay.example'],
397
+ payload: 'bad policy',
398
+ autoDeletionCapability: 'yes',
399
+ _publish
400
+ }),
401
+ /AUTO_DELETION_CAPABILITY_BOOLEAN_REQUIRED/
402
+ )
367
403
  })
368
404
 
369
405
  test('broadcastRumor validates normalized unsigned rumors before publishing', async () => {
@@ -416,9 +452,10 @@ test('nym broadcasts publish through the nym channel path without receivers', as
416
452
  const nymPubkey = pubkeyFixture(6)
417
453
  const channelPubkey = pubkeyFixture(7)
418
454
  const authorEvent = finalizeEvent({ kind: 9002, created_at: 30, tags: [], content: 'signed by another key' }, generateSecretKey())
455
+ const reports = [{ success: true, total: 1 }]
419
456
  const _publish = async options => {
420
457
  published.push(options)
421
- return { results: [] }
458
+ return reports
422
459
  }
423
460
 
424
461
  const rumorResult = await broadcastNymRumor({
@@ -443,9 +480,13 @@ test('nym broadcasts publish through the nym channel path without receivers', as
443
480
  assert.equal(published[0].event.id, undefined)
444
481
  assert.equal(rumorResult.rumor.pubkey, nymPubkey)
445
482
  assert.equal(rumorResult.rumor.id, getEventHash({ ...published[0].event, pubkey: nymPubkey }))
483
+ assert.deepEqual(rumorResult.delivery.reports, reports)
484
+ assert.equal(keypairFromSeckey(rumorResult.delivery.deletionSeckey).pubkey, published[0].deletionPubkey)
446
485
  assert.deepEqual(published[1].event, authorEvent)
447
486
  assert.notEqual(published[1].event.pubkey, nymPubkey)
448
487
  assert.deepEqual(eventResult.event, authorEvent)
488
+ assert.deepEqual(eventResult.delivery.reports, reports)
489
+ assert.equal(keypairFromSeckey(eventResult.delivery.deletionSeckey).pubkey, published[1].deletionPubkey)
449
490
  })
450
491
 
451
492
  test('parseRumorContent only reads h tags for private message kinds', () => {
@@ -76,35 +76,35 @@ function fakePrivateMessage () {
76
76
  },
77
77
  ask: async options => {
78
78
  sent.push({ method: 'ask', options })
79
- return { question: { id: 'question-id', kind: ASK_KIND, pubkey: 'user' }, results: [] }
79
+ return { question: { id: 'question-id', kind: ASK_KIND, pubkey: 'user' }, delivery: { reports: [] } }
80
80
  },
81
81
  reply: async options => {
82
82
  sent.push({ method: 'reply', options })
83
- return { reply: { id: 'reply-id', kind: REPLY_KIND }, results: [] }
83
+ return { reply: { id: 'reply-id', kind: REPLY_KIND }, delivery: { reports: [] } }
84
84
  },
85
85
  tell: async options => {
86
86
  sent.push({ method: 'tell', options })
87
- return { tell: { id: 'tell-id', kind: TELL_KIND }, results: [] }
87
+ return { tell: { id: 'tell-id', kind: TELL_KIND }, delivery: { reports: [] } }
88
88
  },
89
89
  yell: async options => {
90
90
  sent.push({ method: 'yell', options })
91
- return { yell: { id: 'yell-id', kind: TELL_KIND }, results: [] }
91
+ return { yell: { id: 'yell-id', kind: TELL_KIND }, delivery: { reports: [] } }
92
92
  },
93
93
  broadcastRumor: async options => {
94
94
  sent.push({ method: 'broadcastRumor', options })
95
- return { rumor: { id: 'raw-id', kind: 9001 }, results: [] }
95
+ return { rumor: { id: 'raw-id', kind: 9001 }, delivery: { reports: [] } }
96
96
  },
97
97
  broadcastEvent: async options => {
98
98
  sent.push({ method: 'broadcastEvent', options })
99
- return { event: options.event, results: [] }
99
+ return { event: options.event, delivery: { reports: [] } }
100
100
  },
101
101
  broadcastNymRumor: async options => {
102
102
  sent.push({ method: 'broadcastNymRumor', options })
103
- return { rumor: { id: 'nym-raw-id', kind: 9003, pubkey: options.nymSigner.getPublicKey() }, results: [] }
103
+ return { rumor: { id: 'nym-raw-id', kind: 9003, pubkey: options.nymSigner.getPublicKey() }, delivery: { reports: [] } }
104
104
  },
105
105
  broadcastNymEvent: async options => {
106
106
  sent.push({ method: 'broadcastNymEvent', options })
107
- return { event: options.event, results: [] }
107
+ return { event: options.event, delivery: { reports: [] } }
108
108
  },
109
109
  unwatch: channels => stopped.push(channels),
110
110
  clearChannelState: channel => cleared.push(channel)
@@ -516,35 +516,55 @@ test('private messenger delegates send helpers with scoped signers and relays',
516
516
  assert.deepEqual(sent.options.relays, ['wss://relay.example'])
517
517
  assert.equal(sent.options.expirationSeconds, 7 * 24 * 60 * 60)
518
518
  assert.equal(sent.options.temporaryStorageArea, globalThis.localStorage)
519
+ assert.equal(sent.options.autoDeletionCapability, true)
519
520
  }
520
521
  assert.equal(pm.sent[5].options.event.id, 'signed-id')
521
522
  assert.equal(pm.sent[6].options.nymSigner.getPublicKey(), 'global-nym')
522
523
  assert.equal(pm.sent[6].options.privateChannelSigner.getPublicKey(), 'channel')
523
524
  assert.deepEqual(pm.sent[6].options.relays, ['wss://relay.example'])
524
525
  assert.equal(pm.sent[6].options.expirationSeconds, 7 * 24 * 60 * 60)
526
+ assert.equal(pm.sent[6].options.autoDeletionCapability, true)
525
527
  assert.equal(pm.sent[7].options.nymSigner.getPublicKey(), 'global-nym')
526
528
  assert.equal(pm.sent[7].options.event.pubkey, 'author')
529
+ assert.equal(pm.sent[7].options.autoDeletionCapability, true)
527
530
  })
528
531
 
529
- test('private messenger forwards supplied deletion pubkeys to regular and nym sends', async () => {
532
+ test('private messenger configures automatic deletion capabilities and forwards caller pubkeys', async () => {
530
533
  const pm = fakePrivateMessage()
531
- const messenger = await new PrivateMessenger({ _privateMessage: pm }).init({
534
+ const messenger = await new PrivateMessenger({ _privateMessage: pm, autoDeletionCapability: true }).init({
532
535
  userSigner: signer('user'),
533
- channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'] }]
536
+ channels: [
537
+ { pubkey: 'inherited', signer: signer('inherited'), relays: ['wss://relay.example'] },
538
+ { pubkey: 'disabled', signer: signer('disabled'), relays: ['wss://relay.example'], autoDeletionCapability: false }
539
+ ]
534
540
  })
535
- const regularDeletionPubkey = 'a'.repeat(64)
536
- const nymDeletionPubkey = 'b'.repeat(64)
537
541
 
538
- await messenger.tell({ receiverPubkey: 'alice', payload: 'note', deletionPubkey: regularDeletionPubkey })
539
- await messenger.broadcastNymRumor({
540
- receiverPubkeys: ['alice'],
541
- nymSigner: signer('nym'),
542
- rumor: { kind: 9001, created_at: 1, tags: [], content: 'nym' },
543
- deletionPubkey: nymDeletionPubkey
542
+ await messenger.tell({ channelPubkey: 'inherited', receiverPubkey: 'alice', payload: 'default' })
543
+ await messenger.tell({ channelPubkey: 'disabled', receiverPubkey: 'alice', payload: 'disabled' })
544
+ await messenger.tell({ channelPubkey: 'disabled', receiverPubkey: 'alice', payload: 'caller-managed', deletionPubkey: 'a'.repeat(64) })
545
+
546
+ assert.equal(pm.sent[0].options.autoDeletionCapability, true)
547
+ assert.equal(pm.sent[1].options.autoDeletionCapability, false)
548
+ assert.equal(pm.sent[2].options.autoDeletionCapability, false)
549
+ assert.equal(pm.sent[2].options.deletionPubkey, 'a'.repeat(64))
550
+
551
+ const enabledPm = fakePrivateMessage()
552
+ const enabledMessenger = await new PrivateMessenger({ _privateMessage: enabledPm, autoDeletionCapability: false }).init({
553
+ userSigner: signer('other-user'),
554
+ channels: [{ pubkey: 'enabled', signer: signer('enabled'), relays: ['wss://relay.example'], autoDeletionCapability: true }]
544
555
  })
556
+ await enabledMessenger.tell({ receiverPubkey: 'alice', payload: 'enabled' })
557
+ assert.equal(enabledPm.sent[0].options.autoDeletionCapability, true)
545
558
 
546
- assert.equal(pm.sent[0].options.deletionPubkey, regularDeletionPubkey)
547
- assert.equal(pm.sent[1].options.deletionPubkey, nymDeletionPubkey)
559
+ await messenger.tell({
560
+ channelPubkey: 'inherited',
561
+ receiverPubkey: 'alice',
562
+ payload: 'ignored fields',
563
+ deletionSeckey: 'b'.repeat(64),
564
+ autoDeletionCapability: false
565
+ })
566
+ assert.equal(pm.sent[3].options.deletionPubkey, undefined)
567
+ assert.equal(pm.sent[3].options.autoDeletionCapability, true)
548
568
  })
549
569
 
550
570
  test('private messenger update accepts only same-user replacement signers', async () => {
@@ -1105,18 +1125,20 @@ test('seeder channels publish presence immediately and on interval', async () =>
1105
1125
  _clearInterval: timer => cleared.push(timer)
1106
1126
  }).init({
1107
1127
  userSigner: signer('user'),
1108
- channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder', seeders: ['alice'] }]
1128
+ channels: [{ pubkey: 'channel', signer: signer('channel'), relays: ['wss://relay.example'], mode: 'seeder', seeders: ['alice'], autoDeletionCapability: false }]
1109
1129
  })
1110
1130
 
1111
1131
  assert.equal(pm.sent[0].method, 'yell')
1112
1132
  assert.equal(pm.sent[0].options.code, SEEDER_PRESENCE_CODE)
1113
1133
  assert.deepEqual(pm.sent[0].options.receiverPubkeys, ['alice', 'user'])
1134
+ assert.equal(pm.sent[0].options.autoDeletionCapability, false)
1114
1135
  assert.equal(intervals[0].ms, 10 * 60 * 1000)
1115
1136
 
1116
1137
  await intervals[0].fn()
1117
1138
 
1118
1139
  assert.equal(pm.sent[1].method, 'yell')
1119
1140
  assert.equal(pm.sent[1].options.code, SEEDER_PRESENCE_CODE)
1141
+ assert.equal(pm.sent[1].options.autoDeletionCapability, false)
1120
1142
 
1121
1143
  messenger.close()
1122
1144
  assert.deepEqual(cleared, intervals)
@@ -1661,7 +1661,6 @@ describe('RelayPool.sendEvent', () => {
1661
1661
  onRelayResult: result => relayResults.push(result)
1662
1662
  })
1663
1663
 
1664
- assert.equal(early.result, null)
1665
1664
  assert.equal(early.total, 2)
1666
1665
  assert.equal(early.success, true)
1667
1666
  assert.deepEqual(relayResults, [{
@@ -1672,7 +1671,6 @@ describe('RelayPool.sendEvent', () => {
1672
1671
 
1673
1672
  delayed.reject(new Error('relay failed'))
1674
1673
  const full = await early.promise
1675
- assert.equal(full.result, null)
1676
1674
  assert.equal(full.success, true)
1677
1675
  assert.equal(full.total, 2)
1678
1676
  assert.equal(full.fulfilled, 1)
@@ -2050,7 +2048,6 @@ describe('RelayPool.sendEvent', () => {
2050
2048
  const early = await nostr.sendEvent({ id: 'ev1' }, [])
2051
2049
  const full = await early.promise
2052
2050
  assert.deepEqual(early, {
2053
- result: null,
2054
2051
  total: 0,
2055
2052
  success: false,
2056
2053
  promise: early.promise
@@ -2060,7 +2057,6 @@ describe('RelayPool.sendEvent', () => {
2060
2057
  total: 0,
2061
2058
  fulfilled: 0,
2062
2059
  errors: [],
2063
- result: null,
2064
2060
  succeededRelays: []
2065
2061
  })
2066
2062
  })