@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,91 @@
1
+ /**
2
+ * SDK Event Emitter
3
+ *
4
+ * Type-safe event emitter for SDK events.
5
+ */
6
+
7
+ import type { Message, Discussion, Contact } from '../db';
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ // Event Types
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+
13
+ export type SdkEventType =
14
+ | 'message'
15
+ | 'messageSent'
16
+ | 'messageFailed'
17
+ | 'discussionRequest'
18
+ | 'discussionStatusChanged'
19
+ | 'sessionBroken'
20
+ | 'sessionRenewed'
21
+ | 'error';
22
+
23
+ export interface SdkEventHandlers {
24
+ message: (message: Message) => void;
25
+ messageSent: (message: Message) => void;
26
+ messageFailed: (message: Message, error: Error) => void;
27
+ discussionRequest: (discussion: Discussion, contact: Contact) => void;
28
+ discussionStatusChanged: (discussion: Discussion) => void;
29
+ sessionBroken: (discussion: Discussion) => void;
30
+ sessionRenewed: (discussion: Discussion) => void;
31
+ error: (error: Error, context: string) => void;
32
+ }
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Event Emitter Class
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ export class SdkEventEmitter {
39
+ private handlers = {
40
+ message: new Set<SdkEventHandlers['message']>(),
41
+ messageSent: new Set<SdkEventHandlers['messageSent']>(),
42
+ messageFailed: new Set<SdkEventHandlers['messageFailed']>(),
43
+ discussionRequest: new Set<SdkEventHandlers['discussionRequest']>(),
44
+ discussionStatusChanged: new Set<
45
+ SdkEventHandlers['discussionStatusChanged']
46
+ >(),
47
+ sessionBroken: new Set<SdkEventHandlers['sessionBroken']>(),
48
+ sessionRenewed: new Set<SdkEventHandlers['sessionRenewed']>(),
49
+ error: new Set<SdkEventHandlers['error']>(),
50
+ };
51
+
52
+ /**
53
+ * Register an event handler
54
+ */
55
+ on<K extends SdkEventType>(event: K, handler: SdkEventHandlers[K]): void {
56
+ (this.handlers[event] as Set<SdkEventHandlers[K]>).add(handler);
57
+ }
58
+
59
+ /**
60
+ * Remove an event handler
61
+ */
62
+ off<K extends SdkEventType>(event: K, handler: SdkEventHandlers[K]): void {
63
+ (this.handlers[event] as Set<SdkEventHandlers[K]>).delete(handler);
64
+ }
65
+
66
+ /**
67
+ * Emit an event to all registered handlers
68
+ */
69
+ emit<K extends SdkEventType>(
70
+ event: K,
71
+ ...args: Parameters<SdkEventHandlers[K]>
72
+ ): void {
73
+ const handlers = this.handlers[event] as Set<SdkEventHandlers[K]>;
74
+ handlers.forEach(handler => {
75
+ try {
76
+ (handler as (...args: Parameters<SdkEventHandlers[K]>) => void)(
77
+ ...args
78
+ );
79
+ } catch (error) {
80
+ console.error(`[SdkEventEmitter] Error in ${event} handler:`, error);
81
+ }
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Remove all handlers for all events
87
+ */
88
+ clear(): void {
89
+ Object.values(this.handlers).forEach(set => set.clear());
90
+ }
91
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * SDK Polling Manager
3
+ *
4
+ * Manages polling timers for messages, announcements, and session refresh.
5
+ */
6
+
7
+ import type { SdkConfig } from '../config/sdk';
8
+ import type { Discussion } from '../db';
9
+
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Types
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ export interface PollingCallbacks {
15
+ /** Fetch messages from protocol */
16
+ fetchMessages: () => Promise<void>;
17
+ /** Fetch and process announcements */
18
+ fetchAnnouncements: () => Promise<void>;
19
+ /** Handle session refresh for active discussions */
20
+ handleSessionRefresh: (discussions: Discussion[]) => Promise<void>;
21
+ /** Get active discussions for session refresh */
22
+ getActiveDiscussions: () => Promise<Discussion[]>;
23
+ /** Called when a polling error occurs */
24
+ onError: (error: Error, context: string) => void;
25
+ }
26
+
27
+ interface PollingTimers {
28
+ messages: ReturnType<typeof setInterval> | null;
29
+ announcements: ReturnType<typeof setInterval> | null;
30
+ sessionRefresh: ReturnType<typeof setInterval> | null;
31
+ }
32
+
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ // Polling Manager Class
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+
37
+ export class SdkPolling {
38
+ private timers: PollingTimers = {
39
+ messages: null,
40
+ announcements: null,
41
+ sessionRefresh: null,
42
+ };
43
+
44
+ private callbacks: PollingCallbacks | null = null;
45
+
46
+ /**
47
+ * Start polling with the given configuration and callbacks.
48
+ */
49
+ start(config: SdkConfig, callbacks: PollingCallbacks): void {
50
+ // Stop any existing timers first
51
+ this.stop();
52
+
53
+ this.callbacks = callbacks;
54
+
55
+ console.log('[SdkPolling] Starting polling', {
56
+ messagesIntervalMs: config.polling.messagesIntervalMs,
57
+ announcementsIntervalMs: config.polling.announcementsIntervalMs,
58
+ sessionRefreshIntervalMs: config.polling.sessionRefreshIntervalMs,
59
+ });
60
+
61
+ // Start message polling
62
+ this.timers.messages = setInterval(async () => {
63
+ try {
64
+ await this.callbacks?.fetchMessages();
65
+ } catch (error) {
66
+ console.error('[SdkPolling] Message polling error:', error);
67
+ this.callbacks?.onError(
68
+ error instanceof Error ? error : new Error(String(error)),
69
+ 'message_polling'
70
+ );
71
+ }
72
+ }, config.polling.messagesIntervalMs);
73
+
74
+ // Start announcement polling
75
+ this.timers.announcements = setInterval(async () => {
76
+ try {
77
+ await this.callbacks?.fetchAnnouncements();
78
+ } catch (error) {
79
+ console.error('[SdkPolling] Announcement polling error:', error);
80
+ this.callbacks?.onError(
81
+ error instanceof Error ? error : new Error(String(error)),
82
+ 'announcement_polling'
83
+ );
84
+ }
85
+ }, config.polling.announcementsIntervalMs);
86
+
87
+ // Start session refresh polling
88
+ this.timers.sessionRefresh = setInterval(async () => {
89
+ try {
90
+ const discussions = await this.callbacks?.getActiveDiscussions();
91
+ if (discussions && discussions.length > 0) {
92
+ await this.callbacks?.handleSessionRefresh(discussions);
93
+ }
94
+ } catch (error) {
95
+ console.error('[SdkPolling] Session refresh polling error:', error);
96
+ this.callbacks?.onError(
97
+ error instanceof Error ? error : new Error(String(error)),
98
+ 'session_refresh_polling'
99
+ );
100
+ }
101
+ }, config.polling.sessionRefreshIntervalMs);
102
+ }
103
+
104
+ /**
105
+ * Stop all polling timers.
106
+ */
107
+ stop(): void {
108
+ if (this.timers.messages) {
109
+ clearInterval(this.timers.messages);
110
+ this.timers.messages = null;
111
+ }
112
+ if (this.timers.announcements) {
113
+ clearInterval(this.timers.announcements);
114
+ this.timers.announcements = null;
115
+ }
116
+ if (this.timers.sessionRefresh) {
117
+ clearInterval(this.timers.sessionRefresh);
118
+ this.timers.sessionRefresh = null;
119
+ }
120
+
121
+ this.callbacks = null;
122
+ }
123
+
124
+ /**
125
+ * Check if polling is currently running.
126
+ */
127
+ isRunning(): boolean {
128
+ return (
129
+ this.timers.messages !== null ||
130
+ this.timers.announcements !== null ||
131
+ this.timers.sessionRefresh !== null
132
+ );
133
+ }
134
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Core SDK components
3
+ */
4
+
5
+ export { SdkEventEmitter } from './SdkEventEmitter';
6
+ export type { SdkEventType, SdkEventHandlers } from './SdkEventEmitter';
7
+
8
+ export { SdkPolling } from './SdkPolling';
9
+ export type { PollingCallbacks } from './SdkPolling';
@@ -0,0 +1,84 @@
1
+ /**
2
+ * BIP39 utilities for mnemonic generation, validation, and seed derivation
3
+ * Using @scure/bip39 for browser compatibility
4
+ */
5
+
6
+ import {
7
+ generateMnemonic as generateMnemonicScure,
8
+ mnemonicToSeedSync,
9
+ validateMnemonic as validateMnemonicScure,
10
+ } from '@scure/bip39';
11
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
12
+ import { Account, PrivateKey } from '@massalabs/massa-web3';
13
+ import varint from 'varint';
14
+
15
+ export const PRIVATE_KEY_VERSION = 0;
16
+
17
+ /**
18
+ * Generate a new BIP39 mnemonic phrase
19
+ * @param strength - Entropy strength in bits (128, 160, 192, 224, 256)
20
+ * @returns Generated mnemonic phrase
21
+ */
22
+ export function generateMnemonic(strength = 256): string {
23
+ // @scure/bip39 generateMnemonic expects strength in bits (128, 160, 192, 224, 256)
24
+ return generateMnemonicScure(wordlist, strength);
25
+ }
26
+
27
+ /**
28
+ * Validate a BIP39 mnemonic phrase
29
+ * @param mnemonic - The mnemonic phrase to validate
30
+ * @returns True if valid, false otherwise
31
+ */
32
+ export function validateMnemonic(mnemonic: string): boolean {
33
+ return validateMnemonicScure(mnemonic, wordlist);
34
+ }
35
+
36
+ /**
37
+ * Generate a seed from mnemonic and optional passphrase
38
+ * @param mnemonic - The BIP39 mnemonic phrase
39
+ * @param passphrase - Optional passphrase (empty string if not provided)
40
+ * @returns Uint8Array seed
41
+ */
42
+ export function mnemonicToSeed(
43
+ mnemonic: string,
44
+ passphrase: string = ''
45
+ ): Uint8Array {
46
+ return mnemonicToSeedSync(mnemonic, passphrase);
47
+ }
48
+
49
+ /**
50
+ * Create a Massa blockchain account from a mnemonic phrase
51
+ *
52
+ * @param mnemonic - The BIP39 mnemonic phrase
53
+ * @param passphrase - Optional passphrase for additional security
54
+ * @returns Massa Account object
55
+ * @throws Error if mnemonic is invalid
56
+ */
57
+ export async function accountFromMnemonic(
58
+ mnemonic: string,
59
+ passphrase?: string
60
+ ): Promise<Account> {
61
+ try {
62
+ if (!validateMnemonic(mnemonic)) {
63
+ throw new Error('Invalid mnemonic phrase');
64
+ }
65
+
66
+ const seed = mnemonicToSeed(mnemonic, passphrase);
67
+
68
+ const versionArray = varint.encode(PRIVATE_KEY_VERSION);
69
+
70
+ const privateKeyBytes = seed.slice(0, 32);
71
+ const privateKey = new Uint8Array([...versionArray, ...privateKeyBytes]);
72
+
73
+ const pkey = PrivateKey.fromBytes(privateKey);
74
+ const account = await Account.fromPrivateKey(pkey);
75
+ return account;
76
+ } catch (error) {
77
+ console.error('Error in accountFromMnemonic:', error);
78
+ console.error(
79
+ 'Error stack:',
80
+ error instanceof Error ? error.stack : 'No stack'
81
+ );
82
+ throw error;
83
+ }
84
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * High-level encryption utilities
3
+ *
4
+ * Provides convenient wrappers for encrypting and decrypting strings
5
+ * using the WASM encryption primitives.
6
+ */
7
+
8
+ import {
9
+ decryptAead,
10
+ encryptAead,
11
+ EncryptionKey,
12
+ generateNonce,
13
+ Nonce,
14
+ } from '../wasm/encryption';
15
+
16
+ /**
17
+ * Encrypt a plaintext string using AES-256-SIV
18
+ *
19
+ * @param plaintext - The string to encrypt
20
+ * @param key - The encryption key (64 bytes)
21
+ * @param salt - Optional salt/nonce bytes (will generate random if not provided)
22
+ * @returns Object containing encrypted data and nonce bytes
23
+ */
24
+ export async function encrypt(
25
+ plaintext: string,
26
+ key: EncryptionKey,
27
+ salt?: Uint8Array
28
+ ): Promise<{ encryptedData: Uint8Array; nonce: Uint8Array }> {
29
+ const nonce = salt ? Nonce.from_bytes(salt) : await generateNonce();
30
+ const encryptedData = await encryptAead(
31
+ key,
32
+ nonce,
33
+ new TextEncoder().encode(plaintext),
34
+ new Uint8Array()
35
+ );
36
+ return { encryptedData, nonce: nonce.to_bytes() };
37
+ }
38
+
39
+ /**
40
+ * Decrypt encrypted data back to a string
41
+ *
42
+ * @param encryptedData - The encrypted data bytes
43
+ * @param salt - The nonce/salt used during encryption
44
+ * @param key - The encryption key (must match encryption key)
45
+ * @returns The decrypted plaintext string
46
+ * @throws Error if decryption fails (wrong key, corrupted data, etc.)
47
+ */
48
+ export async function decrypt(
49
+ encryptedData: Uint8Array,
50
+ salt: Uint8Array,
51
+ key: EncryptionKey
52
+ ): Promise<string> {
53
+ const plain = await decryptAead(
54
+ key,
55
+ Nonce.from_bytes(salt),
56
+ encryptedData,
57
+ new Uint8Array()
58
+ );
59
+ if (!plain) {
60
+ throw new Error('Failed to decrypt data');
61
+ }
62
+ return new TextDecoder().decode(plain);
63
+ }
64
+
65
+ /**
66
+ * Derive an encryption key from a seed string and nonce
67
+ *
68
+ * @param seedString - The seed string (e.g., password)
69
+ * @param nonce - The nonce/salt for key derivation
70
+ * @returns The derived encryption key
71
+ */
72
+ export async function deriveKey(
73
+ seedString: string,
74
+ nonce: Uint8Array
75
+ ): Promise<EncryptionKey> {
76
+ return await EncryptionKey.from_seed(seedString, nonce);
77
+ }