Add NIP-17 gift-wrapped direct messages
Add outbound NIP-17 private messaging: build the kind-14 rumor -> kind-13 seal -> kind-1059 gift wrap chain on the existing NIP-44 primitives, wrapped in NostrDmActor (WrapDirectMessage/GiftWrapped). Publishing is left to NostrPublishActor. Extract nostrEventId() from signNostrEvent so the unsigned rumor can compute its id without a signature. Export the new actor and nip17 helpers. Add a round-trip test (wrap -> unwrap) and a negative decryption test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
582b0505cb
commit
f8ccd5d56b
5 changed files with 293 additions and 3 deletions
71
test/nostr_dm_actor_test.dart
Normal file
71
test/nostr_dm_actor_test.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:bip340/bip340.dart' as bip340;
|
||||
import 'package:swarm/swarm.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const _senderPriv =
|
||||
'0000000000000000000000000000000000000000000000000000000000000001';
|
||||
const _recipientPriv =
|
||||
'0000000000000000000000000000000000000000000000000000000000000002';
|
||||
|
||||
void main() {
|
||||
final senderPub = bip340.getPublicKey(_senderPriv);
|
||||
final recipientPub = bip340.getPublicKey(_recipientPriv);
|
||||
|
||||
test('WrapDirectMessage produces a kind-1059 gift wrap', () async {
|
||||
final actor = NostrDmActor(_senderPriv);
|
||||
final result = await actor.handle(
|
||||
WrapDirectMessage(recipientPubKey: recipientPub, content: 'hi'),
|
||||
);
|
||||
expect(result, isA<GiftWrapped>());
|
||||
final event =
|
||||
jsonDecode((result as GiftWrapped).event) as Map<String, dynamic>;
|
||||
expect(event['kind'], kGiftWrapKind);
|
||||
// The wrap is signed by an ephemeral key, never the sender.
|
||||
expect(event['pubkey'], isNot(equals(senderPub)));
|
||||
final pTags = (event['tags'] as List)
|
||||
.cast<List>()
|
||||
.where((t) => t.isNotEmpty && t[0] == 'p');
|
||||
expect(pTags.single[1], equals(recipientPub));
|
||||
});
|
||||
|
||||
test('recipient unwraps the gift back to the original rumor', () async {
|
||||
final actor = NostrDmActor(_senderPriv);
|
||||
final result = await actor.handle(
|
||||
WrapDirectMessage(
|
||||
recipientPubKey: recipientPub,
|
||||
content: 'check out this image',
|
||||
rumorTags: const [
|
||||
['imeta', 'url https://cdn.example/x.png', 'm image/png'],
|
||||
],
|
||||
),
|
||||
) as GiftWrapped;
|
||||
|
||||
final rumor = unwrapGift(result.event, _recipientPriv);
|
||||
expect(rumor['content'], equals('check out this image'));
|
||||
expect(rumor['kind'], equals(kDirectMessageKind));
|
||||
expect(rumor['pubkey'], equals(senderPub));
|
||||
|
||||
final tags = (rumor['tags'] as List).cast<List>();
|
||||
expect(
|
||||
tags.any((t) => t.isNotEmpty && t[0] == 'p' && t[1] == recipientPub),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
tags.any((t) => t.isNotEmpty && t[0] == 'imeta'),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('a different key cannot unwrap the gift', () async {
|
||||
final actor = NostrDmActor(_senderPriv);
|
||||
final result = await actor.handle(
|
||||
WrapDirectMessage(recipientPubKey: recipientPub, content: 'secret'),
|
||||
) as GiftWrapped;
|
||||
|
||||
const otherPriv =
|
||||
'0000000000000000000000000000000000000000000000000000000000000003';
|
||||
expect(() => unwrapGift(result.event, otherPriv), throwsA(anything));
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue