@massalabs/gossip-sdk 0.0.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.
Files changed (69) hide show
  1. package/README.md +484 -0
  2. package/package.json +41 -0
  3. package/src/api/messageProtocol/index.ts +53 -0
  4. package/src/api/messageProtocol/mock.ts +13 -0
  5. package/src/api/messageProtocol/rest.ts +209 -0
  6. package/src/api/messageProtocol/types.ts +70 -0
  7. package/src/config/protocol.ts +97 -0
  8. package/src/config/sdk.ts +131 -0
  9. package/src/contacts.ts +210 -0
  10. package/src/core/SdkEventEmitter.ts +91 -0
  11. package/src/core/SdkPolling.ts +134 -0
  12. package/src/core/index.ts +9 -0
  13. package/src/crypto/bip39.ts +84 -0
  14. package/src/crypto/encryption.ts +77 -0
  15. package/src/db.ts +465 -0
  16. package/src/gossipSdk.ts +994 -0
  17. package/src/index.ts +211 -0
  18. package/src/services/announcement.ts +653 -0
  19. package/src/services/auth.ts +95 -0
  20. package/src/services/discussion.ts +380 -0
  21. package/src/services/message.ts +1055 -0
  22. package/src/services/refresh.ts +234 -0
  23. package/src/sw.ts +17 -0
  24. package/src/types/events.ts +108 -0
  25. package/src/types.ts +70 -0
  26. package/src/utils/base64.ts +39 -0
  27. package/src/utils/contacts.ts +161 -0
  28. package/src/utils/discussions.ts +55 -0
  29. package/src/utils/logs.ts +86 -0
  30. package/src/utils/messageSerialization.ts +257 -0
  31. package/src/utils/queue.ts +106 -0
  32. package/src/utils/type.ts +7 -0
  33. package/src/utils/userId.ts +114 -0
  34. package/src/utils/validation.ts +144 -0
  35. package/src/utils.ts +47 -0
  36. package/src/wasm/encryption.ts +108 -0
  37. package/src/wasm/index.ts +20 -0
  38. package/src/wasm/loader.ts +123 -0
  39. package/src/wasm/session.ts +276 -0
  40. package/src/wasm/userKeys.ts +31 -0
  41. package/test/config/protocol.spec.ts +31 -0
  42. package/test/config/sdk.spec.ts +163 -0
  43. package/test/db/helpers.spec.ts +142 -0
  44. package/test/db/operations.spec.ts +128 -0
  45. package/test/db/states.spec.ts +535 -0
  46. package/test/integration/discussion-flow.spec.ts +422 -0
  47. package/test/integration/messaging-flow.spec.ts +708 -0
  48. package/test/integration/sdk-lifecycle.spec.ts +325 -0
  49. package/test/mocks/index.ts +9 -0
  50. package/test/mocks/mockMessageProtocol.ts +100 -0
  51. package/test/services/auth.spec.ts +311 -0
  52. package/test/services/discussion.spec.ts +279 -0
  53. package/test/services/message-deduplication.spec.ts +299 -0
  54. package/test/services/message-startup.spec.ts +331 -0
  55. package/test/services/message.spec.ts +817 -0
  56. package/test/services/refresh.spec.ts +199 -0
  57. package/test/services/session-status.spec.ts +349 -0
  58. package/test/session/wasm.spec.ts +227 -0
  59. package/test/setup.ts +52 -0
  60. package/test/utils/contacts.spec.ts +156 -0
  61. package/test/utils/discussions.spec.ts +66 -0
  62. package/test/utils/queue.spec.ts +52 -0
  63. package/test/utils/serialization.spec.ts +120 -0
  64. package/test/utils/userId.spec.ts +120 -0
  65. package/test/utils/validation.spec.ts +223 -0
  66. package/test/utils.ts +212 -0
  67. package/tsconfig.json +26 -0
  68. package/tsconfig.tsbuildinfo +1 -0
  69. package/vitest.config.ts +28 -0
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Discussion e2e-style tests
3
+ *
4
+ * Uses real WASM SessionModule with real crypto.
5
+ * MockMessageProtocol provides in-memory message storage (no network).
6
+ */
7
+
8
+ import {
9
+ describe,
10
+ it,
11
+ expect,
12
+ beforeEach,
13
+ afterEach,
14
+ vi,
15
+ beforeAll,
16
+ } from 'vitest';
17
+ import { AnnouncementService } from '../../src/services/announcement';
18
+ import { DiscussionService } from '../../src/services/discussion';
19
+ import { db, Contact, DiscussionStatus } from '../../src/db';
20
+ import { MockMessageProtocol } from '../mocks';
21
+ import {
22
+ createTestSession,
23
+ cleanupTestSession,
24
+ TestSessionData,
25
+ } from '../utils';
26
+
27
+ describe('Discussion Flow', () => {
28
+ let mockProtocol: MockMessageProtocol;
29
+
30
+ let alice: TestSessionData;
31
+ let aliceAnnouncementService: AnnouncementService;
32
+ let aliceDiscussionService: DiscussionService;
33
+
34
+ let bob: TestSessionData;
35
+ let bobAnnouncementService: AnnouncementService;
36
+ let bobDiscussionService: DiscussionService;
37
+
38
+ beforeAll(async () => {
39
+ mockProtocol = new MockMessageProtocol();
40
+ });
41
+
42
+ beforeEach(async () => {
43
+ if (!db.isOpen()) {
44
+ await db.open();
45
+ }
46
+ await Promise.all(db.tables.map(table => table.clear()));
47
+ mockProtocol.clearMockData();
48
+
49
+ vi.clearAllMocks();
50
+
51
+ // Create real WASM sessions for Alice and Bob
52
+ alice = await createTestSession(`alice-${Date.now()}-${Math.random()}`);
53
+ bob = await createTestSession(`bob-${Date.now()}-${Math.random()}`);
54
+
55
+ aliceAnnouncementService = new AnnouncementService(
56
+ db,
57
+ mockProtocol,
58
+ alice.session
59
+ );
60
+ aliceDiscussionService = new DiscussionService(
61
+ db,
62
+ aliceAnnouncementService,
63
+ alice.session
64
+ );
65
+
66
+ bobAnnouncementService = new AnnouncementService(
67
+ db,
68
+ mockProtocol,
69
+ bob.session
70
+ );
71
+ bobDiscussionService = new DiscussionService(
72
+ db,
73
+ bobAnnouncementService,
74
+ bob.session
75
+ );
76
+ });
77
+
78
+ afterEach(async () => {
79
+ cleanupTestSession(alice);
80
+ cleanupTestSession(bob);
81
+ });
82
+
83
+ describe('Announcement Username Parsing', () => {
84
+ it('Bob receives announcement with username and uses it as contact name', async () => {
85
+ // Alice creates announcement with username in user_data
86
+ const jsonPayload = JSON.stringify({
87
+ u: 'Alice',
88
+ m: 'Hi, I would like to connect!',
89
+ });
90
+ const userData = new TextEncoder().encode(jsonPayload);
91
+
92
+ // Alice establishes outgoing session to Bob with user data
93
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
94
+ bob.session.ourPk,
95
+ userData
96
+ );
97
+
98
+ // Store the announcement (simulates network)
99
+ await mockProtocol.sendAnnouncement(aliceAnnouncement);
100
+
101
+ // Bob fetches and processes announcements
102
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
103
+
104
+ // Bob should have Alice as a contact with the username from announcement
105
+ const bobContact = await db.getContactByOwnerAndUserId(
106
+ bob.session.userIdEncoded,
107
+ alice.session.userIdEncoded
108
+ );
109
+
110
+ expect(bobContact).toBeDefined();
111
+ expect(bobContact?.name).toBe('Alice');
112
+
113
+ const bobDiscussion = await db.getDiscussionByOwnerAndContact(
114
+ bob.session.userIdEncoded,
115
+ alice.session.userIdEncoded
116
+ );
117
+
118
+ expect(bobDiscussion).toBeDefined();
119
+ expect(bobDiscussion?.announcementMessage).toBe(
120
+ 'Hi, I would like to connect!'
121
+ );
122
+ });
123
+
124
+ it('Bob receives JSON announcement without username (message only)', async () => {
125
+ const jsonPayload = JSON.stringify({ m: 'Hello without username' });
126
+ const userData = new TextEncoder().encode(jsonPayload);
127
+
128
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
129
+ bob.session.ourPk,
130
+ userData
131
+ );
132
+
133
+ await mockProtocol.sendAnnouncement(aliceAnnouncement);
134
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
135
+
136
+ const bobContact = await db.getContactByOwnerAndUserId(
137
+ bob.session.userIdEncoded,
138
+ alice.session.userIdEncoded
139
+ );
140
+
141
+ expect(bobContact).toBeDefined();
142
+ expect(bobContact?.name).toMatch(/^New Request \d+$/);
143
+
144
+ const bobDiscussion = await db.getDiscussionByOwnerAndContact(
145
+ bob.session.userIdEncoded,
146
+ alice.session.userIdEncoded
147
+ );
148
+
149
+ expect(bobDiscussion?.announcementMessage).toBe('Hello without username');
150
+ });
151
+
152
+ it('Bob receives announcement with username only (no message)', async () => {
153
+ const jsonPayload = JSON.stringify({ u: 'AliceUser' });
154
+ const userData = new TextEncoder().encode(jsonPayload);
155
+
156
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
157
+ bob.session.ourPk,
158
+ userData
159
+ );
160
+
161
+ await mockProtocol.sendAnnouncement(aliceAnnouncement);
162
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
163
+
164
+ const bobContact = await db.getContactByOwnerAndUserId(
165
+ bob.session.userIdEncoded,
166
+ alice.session.userIdEncoded
167
+ );
168
+
169
+ expect(bobContact).toBeDefined();
170
+ expect(bobContact?.name).toBe('AliceUser');
171
+
172
+ const bobDiscussion = await db.getDiscussionByOwnerAndContact(
173
+ bob.session.userIdEncoded,
174
+ alice.session.userIdEncoded
175
+ );
176
+
177
+ expect(bobDiscussion?.announcementMessage).toBeUndefined();
178
+ });
179
+
180
+ it('Bob receives announcement without username (no colon in message)', async () => {
181
+ const oldFormatMessage = 'Hi, this is an old format message';
182
+ const userData = new TextEncoder().encode(oldFormatMessage);
183
+
184
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
185
+ bob.session.ourPk,
186
+ userData
187
+ );
188
+
189
+ await mockProtocol.sendAnnouncement(aliceAnnouncement);
190
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
191
+
192
+ const bobContact = await db.getContactByOwnerAndUserId(
193
+ bob.session.userIdEncoded,
194
+ alice.session.userIdEncoded
195
+ );
196
+
197
+ expect(bobContact).toBeDefined();
198
+ expect(bobContact?.name).toMatch(/^New Request \d+$/);
199
+
200
+ const bobDiscussion = await db.getDiscussionByOwnerAndContact(
201
+ bob.session.userIdEncoded,
202
+ alice.session.userIdEncoded
203
+ );
204
+
205
+ expect(bobDiscussion?.announcementMessage).toBe(oldFormatMessage);
206
+ });
207
+
208
+ it('Bob receives JSON announcement with special characters (colons in message)', async () => {
209
+ const jsonPayload = JSON.stringify({
210
+ u: 'Alice:Smith',
211
+ m: 'Hello: how are you?',
212
+ });
213
+ const userData = new TextEncoder().encode(jsonPayload);
214
+
215
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
216
+ bob.session.ourPk,
217
+ userData
218
+ );
219
+
220
+ await mockProtocol.sendAnnouncement(aliceAnnouncement);
221
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
222
+
223
+ const bobContact = await db.getContactByOwnerAndUserId(
224
+ bob.session.userIdEncoded,
225
+ alice.session.userIdEncoded
226
+ );
227
+
228
+ expect(bobContact).toBeDefined();
229
+ expect(bobContact?.name).toBe('Alice:Smith');
230
+
231
+ const bobDiscussion = await db.getDiscussionByOwnerAndContact(
232
+ bob.session.userIdEncoded,
233
+ alice.session.userIdEncoded
234
+ );
235
+
236
+ expect(bobDiscussion?.announcementMessage).toBe('Hello: how are you?');
237
+ });
238
+
239
+ it('Bob receives legacy colon format (backwards compatibility)', async () => {
240
+ const legacyMessage = 'OldAlice:Hello from old client';
241
+ const userData = new TextEncoder().encode(legacyMessage);
242
+
243
+ const aliceAnnouncement = await alice.session.establishOutgoingSession(
244
+ bob.session.ourPk,
245
+ userData
246
+ );
247
+
248
+ await mockProtocol.sendAnnouncement(aliceAnnouncement);
249
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
250
+
251
+ const bobContact = await db.getContactByOwnerAndUserId(
252
+ bob.session.userIdEncoded,
253
+ alice.session.userIdEncoded
254
+ );
255
+
256
+ expect(bobContact?.name).toBe('OldAlice');
257
+
258
+ const bobDiscussion = await db.getDiscussionByOwnerAndContact(
259
+ bob.session.userIdEncoded,
260
+ alice.session.userIdEncoded
261
+ );
262
+
263
+ expect(bobDiscussion?.announcementMessage).toBe('Hello from old client');
264
+ });
265
+ });
266
+
267
+ describe('Discussion Initiation Happy Path', () => {
268
+ it('Alice sends announcement and Bob accepts', async () => {
269
+ // Alice adds Bob as a contact first
270
+ const aliceBobContact: Omit<Contact, 'id'> = {
271
+ ownerUserId: alice.session.userIdEncoded,
272
+ userId: bob.session.userIdEncoded,
273
+ name: 'Bob',
274
+ publicKeys: bob.session.ourPk.to_bytes(),
275
+ avatar: undefined,
276
+ isOnline: false,
277
+ lastSeen: new Date(),
278
+ createdAt: new Date(),
279
+ };
280
+
281
+ await db.contacts.add(aliceBobContact);
282
+
283
+ // Alice initiates discussion with Bob
284
+ const { discussionId: aliceDiscussionId } =
285
+ await aliceDiscussionService.initialize(aliceBobContact);
286
+
287
+ const aliceDiscussion = await db.discussions.get(aliceDiscussionId);
288
+ expect(aliceDiscussion).toBeDefined();
289
+ expect(aliceDiscussion?.status).toBe(DiscussionStatus.PENDING);
290
+ expect(aliceDiscussion?.direction).toBe('initiated');
291
+ expect(aliceDiscussion?.initiationAnnouncement).toBeDefined();
292
+
293
+ // Bob fetches announcements and sees Alice's request
294
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
295
+
296
+ const bobDiscussion = await db.getDiscussionByOwnerAndContact(
297
+ bob.session.userIdEncoded,
298
+ alice.session.userIdEncoded
299
+ );
300
+
301
+ expect(bobDiscussion).toBeDefined();
302
+ expect(bobDiscussion?.status).toBe(DiscussionStatus.PENDING);
303
+ expect(bobDiscussion?.direction).toBe('received');
304
+
305
+ if (!bobDiscussion) throw new Error('Bob discussion not found');
306
+
307
+ // Bob accepts the discussion
308
+ await bobDiscussionService.accept(bobDiscussion);
309
+
310
+ const bobDiscussionAfterAccept = await db.discussions.get(
311
+ bobDiscussion.id!
312
+ );
313
+ expect(bobDiscussionAfterAccept?.status).toBe(DiscussionStatus.ACTIVE);
314
+ expect(bobDiscussionAfterAccept?.initiationAnnouncement).toBeDefined();
315
+
316
+ // Alice fetches announcements and sees Bob's acceptance
317
+ await aliceAnnouncementService.fetchAndProcessAnnouncements();
318
+
319
+ const aliceDiscussionAfterAcceptance =
320
+ await db.discussions.get(aliceDiscussionId);
321
+ expect(aliceDiscussionAfterAcceptance?.status).toBe(
322
+ DiscussionStatus.ACTIVE
323
+ );
324
+ });
325
+
326
+ it('Both Alice and Bob send announcement at the same time', async () => {
327
+ // Alice adds Bob as contact
328
+ const aliceBobContact: Omit<Contact, 'id'> = {
329
+ ownerUserId: alice.session.userIdEncoded,
330
+ userId: bob.session.userIdEncoded,
331
+ name: 'Bob',
332
+ publicKeys: bob.session.ourPk.to_bytes(),
333
+ avatar: undefined,
334
+ isOnline: false,
335
+ lastSeen: new Date(),
336
+ createdAt: new Date(),
337
+ };
338
+
339
+ await db.contacts.add(aliceBobContact);
340
+
341
+ // Bob adds Alice as contact
342
+ const bobAliceContact: Omit<Contact, 'id'> = {
343
+ ownerUserId: bob.session.userIdEncoded,
344
+ userId: alice.session.userIdEncoded,
345
+ name: 'Alice',
346
+ publicKeys: alice.session.ourPk.to_bytes(),
347
+ avatar: undefined,
348
+ isOnline: false,
349
+ lastSeen: new Date(),
350
+ createdAt: new Date(),
351
+ };
352
+
353
+ await db.contacts.add(bobAliceContact);
354
+
355
+ // Both initiate at the same time
356
+ const { discussionId: aliceDiscussionId } =
357
+ await aliceDiscussionService.initialize(aliceBobContact);
358
+
359
+ const { discussionId: bobDiscussionId } =
360
+ await bobDiscussionService.initialize(bobAliceContact);
361
+
362
+ const aliceDiscussion = await db.discussions.get(aliceDiscussionId);
363
+ expect(aliceDiscussion?.status).toBe(DiscussionStatus.PENDING);
364
+ expect(aliceDiscussion?.direction).toBe('initiated');
365
+
366
+ const bobDiscussion = await db.discussions.get(bobDiscussionId);
367
+ expect(bobDiscussion?.status).toBe(DiscussionStatus.PENDING);
368
+ expect(bobDiscussion?.direction).toBe('initiated');
369
+
370
+ // Alice fetches and sees Bob's announcement
371
+ await aliceAnnouncementService.fetchAndProcessAnnouncements();
372
+
373
+ const aliceDiscussionAfterFetch =
374
+ await db.discussions.get(aliceDiscussionId);
375
+ expect(aliceDiscussionAfterFetch?.status).toBe(DiscussionStatus.ACTIVE);
376
+
377
+ // Bob fetches and sees Alice's announcement
378
+ await bobAnnouncementService.fetchAndProcessAnnouncements();
379
+
380
+ const bobDiscussionAfterFetch = await db.discussions.get(bobDiscussionId);
381
+ expect(bobDiscussionAfterFetch?.status).toBe(DiscussionStatus.ACTIVE);
382
+ });
383
+ });
384
+
385
+ describe('Discussion Initiation Failures', () => {
386
+ it('Alice signs announcement but network fails, then resend succeeds', async () => {
387
+ const aliceBobContact: Omit<Contact, 'id'> = {
388
+ ownerUserId: alice.session.userIdEncoded,
389
+ userId: bob.session.userIdEncoded,
390
+ name: 'Bob',
391
+ publicKeys: bob.session.ourPk.to_bytes(),
392
+ avatar: undefined,
393
+ isOnline: false,
394
+ lastSeen: new Date(),
395
+ createdAt: new Date(),
396
+ };
397
+
398
+ await db.contacts.add(aliceBobContact);
399
+
400
+ // Make network fail on first attempt
401
+ vi.spyOn(mockProtocol, 'sendAnnouncement')
402
+ .mockRejectedValueOnce(new Error('Network error'))
403
+ .mockResolvedValue('counter-123');
404
+
405
+ const { discussionId: aliceDiscussionId } =
406
+ await aliceDiscussionService.initialize(aliceBobContact);
407
+
408
+ let aliceDiscussion = await db.discussions.get(aliceDiscussionId);
409
+ expect(aliceDiscussion?.status).toBe(DiscussionStatus.SEND_FAILED);
410
+
411
+ await new Promise(resolve => setTimeout(resolve, 100));
412
+
413
+ // Resend should succeed
414
+ await aliceAnnouncementService.resendAnnouncements([aliceDiscussion!]);
415
+
416
+ aliceDiscussion = await db.discussions.get(aliceDiscussionId);
417
+ // After resend, status depends on whether Bob has responded
418
+ // At minimum it should not be SEND_FAILED anymore
419
+ expect(aliceDiscussion?.status).not.toBe(DiscussionStatus.SEND_FAILED);
420
+ });
421
+ });
422
+ });