@massalabs/gossip-sdk 0.0.2-dev.20260128094509

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,325 @@
1
+ /**
2
+ * GossipSdk lifecycle and event wiring tests
3
+ */
4
+
5
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { GossipDatabase } from '../../src/db';
7
+ import type { EncryptionKey } from '../../src/wasm/encryption';
8
+
9
+ const protocolMock = vi.hoisted(() => ({
10
+ createMessageProtocolMock: vi.fn(),
11
+ }));
12
+
13
+ const eventState = vi.hoisted(() => ({
14
+ lastEvents: null as {
15
+ onMessageReceived?: (message: unknown) => void;
16
+ onDiscussionRequest?: (...args: unknown[]) => void;
17
+ onError?: (...args: unknown[]) => void;
18
+ } | null,
19
+ }));
20
+
21
+ const sessionMock = vi.hoisted(() => {
22
+ const userIdBytes = new Uint8Array(32).fill(9);
23
+ const userIdEncoded = 'gossip1testsessionuserid';
24
+ const state = { lastSessionInstance: null as MockSession | null };
25
+
26
+ class MockSession {
27
+ userId = userIdBytes;
28
+ userIdEncoded = userIdEncoded;
29
+ ourPk = { key: 'pk' };
30
+ load = vi.fn();
31
+ cleanup = vi.fn();
32
+ toEncryptedBlob = vi.fn().mockReturnValue(new Uint8Array([9]));
33
+ private onPersist?: () => void;
34
+
35
+ constructor(_keys: unknown, onPersist?: () => void) {
36
+ this.onPersist = onPersist;
37
+ state.lastSessionInstance = this;
38
+ }
39
+
40
+ emitPersist() {
41
+ this.onPersist?.();
42
+ }
43
+ }
44
+
45
+ return { MockSession, state, userIdBytes, userIdEncoded };
46
+ });
47
+
48
+ vi.mock('../../src/api/messageProtocol', () => ({
49
+ createMessageProtocol: () => protocolMock.createMessageProtocolMock(),
50
+ }));
51
+
52
+ vi.mock('../../src/wasm/loader', () => ({
53
+ startWasmInitialization: vi.fn(),
54
+ ensureWasmInitialized: vi.fn().mockResolvedValue(undefined),
55
+ }));
56
+
57
+ vi.mock('../../src/wasm/userKeys', () => ({
58
+ generateUserKeys: vi.fn().mockResolvedValue({}),
59
+ }));
60
+
61
+ vi.mock('../../src/wasm/session', async () => {
62
+ return {
63
+ SessionModule: sessionMock.MockSession,
64
+ };
65
+ });
66
+
67
+ vi.mock('../../src/services/auth', () => ({
68
+ AuthService: class {
69
+ constructor() {}
70
+ },
71
+ }));
72
+
73
+ vi.mock('../../src/services/announcement', () => ({
74
+ AnnouncementService: class {
75
+ constructor(
76
+ _db: unknown,
77
+ _protocol: unknown,
78
+ _session: unknown,
79
+ events: typeof eventState.lastEvents
80
+ ) {
81
+ eventState.lastEvents = events ?? null;
82
+ }
83
+ },
84
+ }));
85
+
86
+ vi.mock('../../src/services/message', () => ({
87
+ MessageService: class {
88
+ sendMessage = vi.fn();
89
+ fetchMessages = vi.fn();
90
+ resendMessages = vi.fn();
91
+ findMessageBySeeker = vi.fn();
92
+
93
+ constructor(
94
+ _db: unknown,
95
+ _protocol: unknown,
96
+ _session: unknown,
97
+ events: typeof eventState.lastEvents
98
+ ) {
99
+ eventState.lastEvents = events ?? null;
100
+ }
101
+ },
102
+ }));
103
+
104
+ vi.mock('../../src/services/discussion', () => ({
105
+ DiscussionService: class {
106
+ initialize = vi.fn();
107
+ accept = vi.fn();
108
+ renew = vi.fn();
109
+ isStableState = vi.fn();
110
+ constructor(
111
+ _db: unknown,
112
+ _announcement: unknown,
113
+ _session: unknown,
114
+ events: typeof eventState.lastEvents
115
+ ) {
116
+ eventState.lastEvents = events ?? null;
117
+ }
118
+ },
119
+ }));
120
+
121
+ vi.mock('../../src/services/refresh', () => ({
122
+ RefreshService: class {
123
+ handleSessionRefresh = vi.fn();
124
+ constructor(
125
+ _db: unknown,
126
+ _message: unknown,
127
+ _session: unknown,
128
+ events: typeof eventState.lastEvents
129
+ ) {
130
+ eventState.lastEvents = events ?? null;
131
+ }
132
+ },
133
+ }));
134
+
135
+ describe('GossipSdkImpl lifecycle', () => {
136
+ beforeEach(() => {
137
+ vi.clearAllMocks();
138
+ protocolMock.createMessageProtocolMock.mockReturnValue({
139
+ fetchMessages: vi.fn(),
140
+ sendMessage: vi.fn(),
141
+ sendAnnouncement: vi.fn(),
142
+ fetchAnnouncements: vi.fn(),
143
+ fetchPublicKeyByUserId: vi.fn(),
144
+ postPublicKey: vi.fn(),
145
+ changeNode: vi.fn(),
146
+ });
147
+ sessionMock.state.lastSessionInstance = null;
148
+ eventState.lastEvents = null;
149
+ });
150
+
151
+ it('initializes once and exposes auth service', async () => {
152
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
153
+ const sdk = new GossipSdkImpl();
154
+
155
+ await sdk.init({ db: new GossipDatabase() });
156
+ expect(sdk.isInitialized).toBe(true);
157
+ expect(() => sdk.auth).not.toThrow();
158
+
159
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
160
+ await sdk.init({ db: new GossipDatabase() });
161
+ expect(warnSpy).toHaveBeenCalled();
162
+ });
163
+
164
+ it('throws on openSession before init', async () => {
165
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
166
+ const sdk = new GossipSdkImpl();
167
+
168
+ await expect(sdk.openSession({ mnemonic: 'test words' })).rejects.toThrow(
169
+ 'SDK not initialized'
170
+ );
171
+ });
172
+
173
+ it('opens and closes session with getters wired', async () => {
174
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
175
+ const sdk = new GossipSdkImpl();
176
+
177
+ await sdk.init({ db: new GossipDatabase() });
178
+ await sdk.openSession({ mnemonic: 'test words' });
179
+
180
+ expect(sdk.isSessionOpen).toBe(true);
181
+ expect(sdk.userIdBytes).toBeInstanceOf(Uint8Array);
182
+ expect(sdk.userIdBytes.length).toBe(32);
183
+ expect(sdk.publicKeys).toBeDefined();
184
+
185
+ await sdk.closeSession();
186
+ expect(sdk.isSessionOpen).toBe(false);
187
+ expect(sessionMock.state.lastSessionInstance?.cleanup).toHaveBeenCalled();
188
+ expect(() => sdk.messages).toThrow('No session open');
189
+ });
190
+
191
+ it('restores encrypted session when provided', async () => {
192
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
193
+ const sdk = new GossipSdkImpl();
194
+ const encryptedSession = new Uint8Array([1, 2, 3]);
195
+ const encryptionKey = {} as EncryptionKey;
196
+
197
+ await sdk.init({ db: new GossipDatabase() });
198
+ await sdk.openSession({
199
+ mnemonic: 'test words',
200
+ encryptedSession,
201
+ encryptionKey,
202
+ });
203
+
204
+ expect(sessionMock.state.lastSessionInstance?.load).toHaveBeenCalled();
205
+ });
206
+
207
+ it('persists session via onPersist callback', async () => {
208
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
209
+ const sdk = new GossipSdkImpl();
210
+ const onPersist = vi.fn().mockResolvedValue(undefined);
211
+ const persistEncryptionKey = {} as EncryptionKey;
212
+
213
+ await sdk.init({ db: new GossipDatabase() });
214
+ await sdk.openSession({
215
+ mnemonic: 'test words',
216
+ onPersist,
217
+ persistEncryptionKey,
218
+ });
219
+
220
+ sessionMock.state.lastSessionInstance?.emitPersist();
221
+ expect(onPersist).toHaveBeenCalledWith(
222
+ new Uint8Array([9]),
223
+ persistEncryptionKey
224
+ );
225
+ });
226
+
227
+ it('bridges message events to sdk.on handlers', async () => {
228
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
229
+ const sdk = new GossipSdkImpl();
230
+ const handler = vi.fn();
231
+
232
+ await sdk.init({ db: new GossipDatabase() });
233
+ sdk.on('message', handler);
234
+ await sdk.openSession({ mnemonic: 'test words' });
235
+
236
+ eventState.lastEvents?.onMessageReceived?.({ id: 1 });
237
+ expect(handler).toHaveBeenCalledWith({ id: 1 });
238
+ });
239
+ });
240
+
241
+ describe('GossipSdkImpl.configurePersistence', () => {
242
+ beforeEach(() => {
243
+ vi.clearAllMocks();
244
+ protocolMock.createMessageProtocolMock.mockReturnValue({
245
+ fetchMessages: vi.fn(),
246
+ sendMessage: vi.fn(),
247
+ sendAnnouncement: vi.fn(),
248
+ fetchAnnouncements: vi.fn(),
249
+ fetchPublicKeyByUserId: vi.fn(),
250
+ postPublicKey: vi.fn(),
251
+ changeNode: vi.fn(),
252
+ });
253
+ sessionMock.state.lastSessionInstance = null;
254
+ eventState.lastEvents = null;
255
+ });
256
+
257
+ it('throws if called before session is opened', async () => {
258
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
259
+ const sdk = new GossipSdkImpl();
260
+ const onPersist = vi.fn();
261
+ const encryptionKey = {} as EncryptionKey;
262
+
263
+ await sdk.init({ db: new GossipDatabase() });
264
+
265
+ expect(() => sdk.configurePersistence(encryptionKey, onPersist)).toThrow(
266
+ 'No session open'
267
+ );
268
+ });
269
+
270
+ it('configures persistence after session is opened without initial onPersist', async () => {
271
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
272
+ const sdk = new GossipSdkImpl();
273
+ const onPersist = vi.fn().mockResolvedValue(undefined);
274
+ const encryptionKey = {} as EncryptionKey;
275
+
276
+ await sdk.init({ db: new GossipDatabase() });
277
+
278
+ await sdk.openSession({ mnemonic: 'test words' });
279
+
280
+ sdk.configurePersistence(encryptionKey, onPersist);
281
+
282
+ sessionMock.state.lastSessionInstance?.emitPersist();
283
+
284
+ expect(onPersist).toHaveBeenCalledWith(new Uint8Array([9]), encryptionKey);
285
+ });
286
+
287
+ it('replaces existing onPersist callback when reconfigured', async () => {
288
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
289
+ const sdk = new GossipSdkImpl();
290
+ const originalOnPersist = vi.fn().mockResolvedValue(undefined);
291
+ const newOnPersist = vi.fn().mockResolvedValue(undefined);
292
+ const originalKey = { original: true } as unknown as EncryptionKey;
293
+ const newKey = { new: true } as unknown as EncryptionKey;
294
+
295
+ await sdk.init({ db: new GossipDatabase() });
296
+
297
+ await sdk.openSession({
298
+ mnemonic: 'test words',
299
+ onPersist: originalOnPersist,
300
+ persistEncryptionKey: originalKey,
301
+ });
302
+
303
+ sdk.configurePersistence(newKey, newOnPersist);
304
+
305
+ sessionMock.state.lastSessionInstance?.emitPersist();
306
+
307
+ expect(originalOnPersist).not.toHaveBeenCalled();
308
+ expect(newOnPersist).toHaveBeenCalledWith(new Uint8Array([9]), newKey);
309
+ });
310
+
311
+ it('ensures persistence is called with correct encryption key', async () => {
312
+ const { GossipSdkImpl } = await import('../../src/gossipSdk');
313
+ const sdk = new GossipSdkImpl();
314
+ const onPersist = vi.fn().mockResolvedValue(undefined);
315
+ const specificKey = { keyId: 'test-key-123' } as unknown as EncryptionKey;
316
+
317
+ await sdk.init({ db: new GossipDatabase() });
318
+ await sdk.openSession({ mnemonic: 'test words' });
319
+
320
+ sdk.configurePersistence(specificKey, onPersist);
321
+ sessionMock.state.lastSessionInstance?.emitPersist();
322
+
323
+ expect(onPersist).toHaveBeenCalledWith(expect.any(Uint8Array), specificKey);
324
+ });
325
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Test Mocks
3
+ *
4
+ * Only MockMessageProtocol is needed - it provides an in-memory
5
+ * implementation to avoid network calls during tests.
6
+ *
7
+ * SessionModule uses real WASM - no mock needed since WASM works in Node.
8
+ */
9
+ export { MockMessageProtocol } from './mockMessageProtocol';
@@ -0,0 +1,100 @@
1
+ /**
2
+ * In-Memory Mock Message Protocol for Testing
3
+ *
4
+ * This mock stores messages and announcements in memory,
5
+ * allowing tests to verify messaging flows without network calls.
6
+ */
7
+
8
+ import type { IMessageProtocol } from '../../src/api/messageProtocol/types';
9
+ import type {
10
+ EncryptedMessage,
11
+ BulletinItem,
12
+ } from '../../src/api/messageProtocol/types';
13
+
14
+ export class MockMessageProtocol implements IMessageProtocol {
15
+ private messages: Map<string, Uint8Array> = new Map();
16
+ private announcements: BulletinItem[] = [];
17
+ private announcementCounter = 0;
18
+
19
+ constructor(
20
+ public baseUrl: string = 'mock://test',
21
+ public timeout: number = 10000,
22
+ public retryAttempts: number = 3
23
+ ) {}
24
+
25
+ async fetchMessages(seekers: Uint8Array[]): Promise<EncryptedMessage[]> {
26
+ const results: EncryptedMessage[] = [];
27
+ for (const seeker of seekers) {
28
+ const key = this.uint8ArrayToKey(seeker);
29
+ const ciphertext = this.messages.get(key);
30
+ if (ciphertext) {
31
+ results.push({ seeker, ciphertext });
32
+ }
33
+ }
34
+ return results;
35
+ }
36
+
37
+ async sendMessage(message: EncryptedMessage): Promise<void> {
38
+ const key = this.uint8ArrayToKey(message.seeker);
39
+ this.messages.set(key, message.ciphertext);
40
+ }
41
+
42
+ async sendAnnouncement(announcement: Uint8Array): Promise<string> {
43
+ const counter = String(++this.announcementCounter);
44
+ this.announcements.push({ counter, data: announcement });
45
+ return counter;
46
+ }
47
+
48
+ async fetchAnnouncements(
49
+ limit?: number,
50
+ cursor?: string
51
+ ): Promise<BulletinItem[]> {
52
+ let results = [...this.announcements];
53
+
54
+ // Filter by cursor (return only announcements after this counter)
55
+ if (cursor) {
56
+ const cursorNum = parseInt(cursor, 10);
57
+ results = results.filter(a => parseInt(a.counter, 10) > cursorNum);
58
+ }
59
+
60
+ // Apply limit
61
+ if (limit && limit > 0) {
62
+ results = results.slice(0, limit);
63
+ }
64
+
65
+ return results;
66
+ }
67
+
68
+ async fetchPublicKeyByUserId(_userId: Uint8Array): Promise<string> {
69
+ // Return empty string - tests should handle this
70
+ return '';
71
+ }
72
+
73
+ async postPublicKey(_publicKey: string): Promise<string> {
74
+ return 'mock-counter';
75
+ }
76
+
77
+ async changeNode(newBaseUrl: string): Promise<{ success: boolean }> {
78
+ this.baseUrl = newBaseUrl;
79
+ return { success: true };
80
+ }
81
+
82
+ // Test helper methods
83
+ clearMockData(): void {
84
+ this.messages.clear();
85
+ this.announcements = [];
86
+ this.announcementCounter = 0;
87
+ }
88
+
89
+ getStoredMessages(): Map<string, Uint8Array> {
90
+ return new Map(this.messages);
91
+ }
92
+
93
+ getStoredAnnouncements(): BulletinItem[] {
94
+ return [...this.announcements];
95
+ }
96
+
97
+ private uint8ArrayToKey(arr: Uint8Array): string {
98
+ return Array.from(arr).join(',');
99
+ }
100
+ }