@massalabs/gossip-sdk 0.0.2-dev.20260128094509 → 0.0.2-dev.20260128160824

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 (148) hide show
  1. package/dist/api/messageProtocol/index.d.ts +19 -0
  2. package/dist/api/messageProtocol/index.js +26 -0
  3. package/dist/api/messageProtocol/mock.d.ts +12 -0
  4. package/{src/api/messageProtocol/mock.ts → dist/api/messageProtocol/mock.js} +2 -3
  5. package/dist/api/messageProtocol/rest.d.ts +22 -0
  6. package/dist/api/messageProtocol/rest.js +161 -0
  7. package/dist/api/messageProtocol/types.d.ts +61 -0
  8. package/dist/api/messageProtocol/types.js +6 -0
  9. package/dist/assets/generated/wasm/README.md +281 -0
  10. package/dist/assets/generated/wasm/gossip_wasm.d.ts +638 -0
  11. package/dist/assets/generated/wasm/gossip_wasm.js +1557 -0
  12. package/dist/assets/generated/wasm/gossip_wasm_bg.wasm +0 -0
  13. package/dist/assets/generated/wasm/gossip_wasm_bg.wasm.d.ts +164 -0
  14. package/dist/assets/generated/wasm/package.json +15 -0
  15. package/dist/assets/generated/wasm-node/README.md +281 -0
  16. package/dist/assets/generated/wasm-node/gossip_wasm.d.ts +443 -0
  17. package/dist/assets/generated/wasm-node/gossip_wasm.js +1488 -0
  18. package/dist/assets/generated/wasm-node/gossip_wasm_bg.wasm +0 -0
  19. package/dist/assets/generated/wasm-node/gossip_wasm_bg.wasm.d.ts +164 -0
  20. package/dist/assets/generated/wasm-node/package.json +11 -0
  21. package/dist/config/protocol.d.ts +36 -0
  22. package/dist/config/protocol.js +77 -0
  23. package/dist/config/sdk.d.ts +82 -0
  24. package/dist/config/sdk.js +55 -0
  25. package/{src/contacts.ts → dist/contacts.d.ts} +11 -95
  26. package/dist/contacts.js +166 -0
  27. package/dist/core/SdkEventEmitter.d.ts +36 -0
  28. package/dist/core/SdkEventEmitter.js +59 -0
  29. package/dist/core/SdkPolling.d.ts +35 -0
  30. package/dist/core/SdkPolling.js +100 -0
  31. package/{src/core/index.ts → dist/core/index.d.ts} +0 -2
  32. package/dist/core/index.js +5 -0
  33. package/dist/crypto/bip39.d.ts +34 -0
  34. package/dist/crypto/bip39.js +62 -0
  35. package/dist/crypto/encryption.d.ts +37 -0
  36. package/dist/crypto/encryption.js +46 -0
  37. package/dist/db.d.ts +190 -0
  38. package/dist/db.js +311 -0
  39. package/dist/gossipSdk.d.ts +274 -0
  40. package/dist/gossipSdk.js +690 -0
  41. package/dist/index.d.ts +59 -0
  42. package/dist/index.js +61 -0
  43. package/dist/services/announcement.d.ts +43 -0
  44. package/dist/services/announcement.js +491 -0
  45. package/dist/services/auth.d.ts +37 -0
  46. package/dist/services/auth.js +76 -0
  47. package/dist/services/discussion.d.ts +63 -0
  48. package/dist/services/discussion.js +297 -0
  49. package/dist/services/message.d.ts +74 -0
  50. package/dist/services/message.js +826 -0
  51. package/dist/services/refresh.d.ts +41 -0
  52. package/dist/services/refresh.js +205 -0
  53. package/{src/sw.ts → dist/sw.d.ts} +1 -8
  54. package/dist/sw.js +10 -0
  55. package/dist/types/events.d.ts +80 -0
  56. package/dist/types/events.js +7 -0
  57. package/dist/types.d.ts +32 -0
  58. package/dist/types.js +7 -0
  59. package/dist/utils/base64.d.ts +10 -0
  60. package/dist/utils/base64.js +30 -0
  61. package/dist/utils/contacts.d.ts +42 -0
  62. package/dist/utils/contacts.js +113 -0
  63. package/dist/utils/discussions.d.ts +24 -0
  64. package/dist/utils/discussions.js +38 -0
  65. package/dist/utils/logs.d.ts +19 -0
  66. package/dist/utils/logs.js +89 -0
  67. package/dist/utils/messageSerialization.d.ts +64 -0
  68. package/dist/utils/messageSerialization.js +184 -0
  69. package/dist/utils/queue.d.ts +50 -0
  70. package/dist/utils/queue.js +110 -0
  71. package/dist/utils/type.d.ts +10 -0
  72. package/dist/utils/type.js +4 -0
  73. package/dist/utils/userId.d.ts +40 -0
  74. package/dist/utils/userId.js +90 -0
  75. package/dist/utils/validation.d.ts +50 -0
  76. package/dist/utils/validation.js +112 -0
  77. package/dist/utils.d.ts +30 -0
  78. package/{src/utils.ts → dist/utils.js} +9 -19
  79. package/dist/wasm/encryption.d.ts +56 -0
  80. package/{src/wasm/encryption.ts → dist/wasm/encryption.js} +22 -51
  81. package/dist/wasm/index.d.ts +10 -0
  82. package/{src/wasm/index.ts → dist/wasm/index.js} +1 -8
  83. package/dist/wasm/loader.d.ts +22 -0
  84. package/dist/wasm/loader.js +78 -0
  85. package/dist/wasm/session.d.ts +85 -0
  86. package/dist/wasm/session.js +226 -0
  87. package/dist/wasm/userKeys.d.ts +17 -0
  88. package/{src/wasm/userKeys.ts → dist/wasm/userKeys.js} +6 -13
  89. package/package.json +15 -2
  90. package/src/api/messageProtocol/index.ts +0 -53
  91. package/src/api/messageProtocol/rest.ts +0 -209
  92. package/src/api/messageProtocol/types.ts +0 -70
  93. package/src/config/protocol.ts +0 -97
  94. package/src/config/sdk.ts +0 -131
  95. package/src/core/SdkEventEmitter.ts +0 -91
  96. package/src/core/SdkPolling.ts +0 -134
  97. package/src/crypto/bip39.ts +0 -84
  98. package/src/crypto/encryption.ts +0 -77
  99. package/src/db.ts +0 -465
  100. package/src/gossipSdk.ts +0 -994
  101. package/src/index.ts +0 -211
  102. package/src/services/announcement.ts +0 -653
  103. package/src/services/auth.ts +0 -95
  104. package/src/services/discussion.ts +0 -380
  105. package/src/services/message.ts +0 -1055
  106. package/src/services/refresh.ts +0 -234
  107. package/src/types/events.ts +0 -108
  108. package/src/types.ts +0 -70
  109. package/src/utils/base64.ts +0 -39
  110. package/src/utils/contacts.ts +0 -161
  111. package/src/utils/discussions.ts +0 -55
  112. package/src/utils/logs.ts +0 -86
  113. package/src/utils/messageSerialization.ts +0 -257
  114. package/src/utils/queue.ts +0 -106
  115. package/src/utils/type.ts +0 -7
  116. package/src/utils/userId.ts +0 -114
  117. package/src/utils/validation.ts +0 -144
  118. package/src/wasm/loader.ts +0 -123
  119. package/src/wasm/session.ts +0 -276
  120. package/test/config/protocol.spec.ts +0 -31
  121. package/test/config/sdk.spec.ts +0 -163
  122. package/test/db/helpers.spec.ts +0 -142
  123. package/test/db/operations.spec.ts +0 -128
  124. package/test/db/states.spec.ts +0 -535
  125. package/test/integration/discussion-flow.spec.ts +0 -422
  126. package/test/integration/messaging-flow.spec.ts +0 -708
  127. package/test/integration/sdk-lifecycle.spec.ts +0 -325
  128. package/test/mocks/index.ts +0 -9
  129. package/test/mocks/mockMessageProtocol.ts +0 -100
  130. package/test/services/auth.spec.ts +0 -311
  131. package/test/services/discussion.spec.ts +0 -279
  132. package/test/services/message-deduplication.spec.ts +0 -299
  133. package/test/services/message-startup.spec.ts +0 -331
  134. package/test/services/message.spec.ts +0 -817
  135. package/test/services/refresh.spec.ts +0 -199
  136. package/test/services/session-status.spec.ts +0 -349
  137. package/test/session/wasm.spec.ts +0 -227
  138. package/test/setup.ts +0 -52
  139. package/test/utils/contacts.spec.ts +0 -156
  140. package/test/utils/discussions.spec.ts +0 -66
  141. package/test/utils/queue.spec.ts +0 -52
  142. package/test/utils/serialization.spec.ts +0 -120
  143. package/test/utils/userId.spec.ts +0 -120
  144. package/test/utils/validation.spec.ts +0 -223
  145. package/test/utils.ts +0 -212
  146. package/tsconfig.json +0 -26
  147. package/tsconfig.tsbuildinfo +0 -1
  148. package/vitest.config.ts +0 -28
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Auth Service
3
+ *
4
+ * Handles storing and retrieving public keys by userId hash via the auth API.
5
+ */
6
+ import { UserPublicKeys } from '#wasm';
7
+ import { IMessageProtocol } from '../api/messageProtocol/types';
8
+ import { type GossipDatabase } from '../db';
9
+ export type PublicKeyResult = {
10
+ publicKey: UserPublicKeys;
11
+ error?: never;
12
+ } | {
13
+ publicKey?: never;
14
+ error: string;
15
+ };
16
+ export declare class AuthService {
17
+ private db;
18
+ messageProtocol: IMessageProtocol;
19
+ constructor(db: GossipDatabase, messageProtocol: IMessageProtocol);
20
+ /**
21
+ * Fetch public key by userId
22
+ * @param userId - Bech32-encoded userId (e.g., "gossip1...")
23
+ */
24
+ fetchPublicKeyByUserId(userId: string): Promise<PublicKeyResult>;
25
+ /**
26
+ * Ensure public key is published (check first, then publish if needed)
27
+ * @param publicKeys - UserPublicKeys instance
28
+ * @param userId - Bech32-encoded userId (e.g., "gossip1...")
29
+ */
30
+ ensurePublicKeyPublished(publicKeys: UserPublicKeys, userId: string): Promise<void>;
31
+ }
32
+ export declare const PUBLIC_KEY_NOT_FOUND_ERROR = "Public key not found";
33
+ export declare const PUBLIC_KEY_NOT_FOUND_MESSAGE = "Contact public key not found. It may not be published yet.";
34
+ export declare const FAILED_TO_FETCH_ERROR = "Failed to fetch";
35
+ export declare const FAILED_TO_FETCH_MESSAGE = "Failed to retrieve contact public key. Check your internet connection or try again later.";
36
+ export declare const FAILED_TO_RETRIEVE_CONTACT_PUBLIC_KEY_ERROR = "Failed to retrieve contact public key";
37
+ export declare function getPublicKeyErrorMessage(error: unknown): string;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Auth Service
3
+ *
4
+ * Handles storing and retrieving public keys by userId hash via the auth API.
5
+ */
6
+ import { UserPublicKeys } from '#wasm';
7
+ import { decodeUserId } from '../utils/userId';
8
+ import { encodeToBase64, decodeFromBase64 } from '../utils/base64';
9
+ export class AuthService {
10
+ constructor(db, messageProtocol) {
11
+ Object.defineProperty(this, "db", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: db
16
+ });
17
+ Object.defineProperty(this, "messageProtocol", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: messageProtocol
22
+ });
23
+ }
24
+ /**
25
+ * Fetch public key by userId
26
+ * @param userId - Bech32-encoded userId (e.g., "gossip1...")
27
+ */
28
+ async fetchPublicKeyByUserId(userId) {
29
+ try {
30
+ const base64PublicKey = await this.messageProtocol.fetchPublicKeyByUserId(decodeUserId(userId));
31
+ return {
32
+ publicKey: UserPublicKeys.from_bytes(decodeFromBase64(base64PublicKey)),
33
+ };
34
+ }
35
+ catch (err) {
36
+ return {
37
+ error: getPublicKeyErrorMessage(err),
38
+ };
39
+ }
40
+ }
41
+ /**
42
+ * Ensure public key is published (check first, then publish if needed)
43
+ * @param publicKeys - UserPublicKeys instance
44
+ * @param userId - Bech32-encoded userId (e.g., "gossip1...")
45
+ */
46
+ async ensurePublicKeyPublished(publicKeys, userId) {
47
+ const profile = await this.db.userProfile.get(userId);
48
+ if (!profile)
49
+ throw new Error('User profile not found');
50
+ const lastPush = profile.lastPublicKeyPush;
51
+ if (lastPush && !moreThanOneWeekAgo(lastPush)) {
52
+ return;
53
+ }
54
+ await this.messageProtocol.postPublicKey(encodeToBase64(publicKeys.to_bytes()));
55
+ await this.db.userProfile.update(userId, { lastPublicKeyPush: new Date() });
56
+ }
57
+ }
58
+ const ONE_WEEK_IN_MILLIS = 7 * 24 * 60 * 60 * 1000;
59
+ function moreThanOneWeekAgo(date) {
60
+ return Date.now() - date.getTime() >= ONE_WEEK_IN_MILLIS;
61
+ }
62
+ export const PUBLIC_KEY_NOT_FOUND_ERROR = 'Public key not found';
63
+ export const PUBLIC_KEY_NOT_FOUND_MESSAGE = 'Contact public key not found. It may not be published yet.';
64
+ export const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
65
+ export const FAILED_TO_FETCH_MESSAGE = 'Failed to retrieve contact public key. Check your internet connection or try again later.';
66
+ export const FAILED_TO_RETRIEVE_CONTACT_PUBLIC_KEY_ERROR = 'Failed to retrieve contact public key';
67
+ export function getPublicKeyErrorMessage(error) {
68
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
+ if (errorMessage.includes(PUBLIC_KEY_NOT_FOUND_ERROR)) {
70
+ return PUBLIC_KEY_NOT_FOUND_MESSAGE;
71
+ }
72
+ if (errorMessage.includes(FAILED_TO_FETCH_ERROR)) {
73
+ return FAILED_TO_FETCH_MESSAGE;
74
+ }
75
+ return `${FAILED_TO_RETRIEVE_CONTACT_PUBLIC_KEY_ERROR}. ${errorMessage}`;
76
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Discussion Service
3
+ *
4
+ * Class-based service for initializing, accepting, and managing discussions.
5
+ */
6
+ import { type Discussion, type Contact, type GossipDatabase } from '../db';
7
+ import { AnnouncementService } from './announcement';
8
+ import { SessionModule } from '../wasm/session';
9
+ import { GossipSdkEvents } from '../types/events';
10
+ /**
11
+ * Service for managing discussions between users.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const discussionService = new DiscussionService(db, announcementService, session);
16
+ *
17
+ * // Initialize a new discussion
18
+ * const result = await discussionService.initialize(contact, 'Hello!');
19
+ *
20
+ * // Accept a discussion request
21
+ * await discussionService.accept(discussion);
22
+ *
23
+ * // Renew a broken discussion
24
+ * await discussionService.renew(contactUserId);
25
+ * ```
26
+ */
27
+ export declare class DiscussionService {
28
+ private db;
29
+ private announcementService;
30
+ private session;
31
+ private events;
32
+ constructor(db: GossipDatabase, announcementService: AnnouncementService, session: SessionModule, events?: GossipSdkEvents);
33
+ /**
34
+ * Initialize a discussion with a contact using SessionManager
35
+ * @param contact - The contact to start a discussion with
36
+ * @param message - Optional message to include in the announcement
37
+ * @returns The discussion ID and the created announcement
38
+ */
39
+ initialize(contact: Contact, message?: string): Promise<{
40
+ discussionId: number;
41
+ announcement: Uint8Array;
42
+ }>;
43
+ /**
44
+ * Accept a discussion request from a contact using SessionManager
45
+ * @param discussion - The discussion to accept
46
+ */
47
+ accept(discussion: Discussion): Promise<void>;
48
+ /**
49
+ * Renew a discussion by resetting sent outgoing messages and sending a new announcement.
50
+ * @param contactUserId - The user ID of the contact whose discussion should be renewed.
51
+ */
52
+ renew(contactUserId: string): Promise<void>;
53
+ /**
54
+ * Check if new messages can be sent to session manager for encryption.
55
+ * Returns false if the discussion is broken or if there are failed messages
56
+ * that have not been encrypted.
57
+ *
58
+ * @param ownerUserId - The owner user ID
59
+ * @param contactUserId - The contact user ID
60
+ * @returns true if discussion is in stable state for sending messages
61
+ */
62
+ isStableState(ownerUserId: string, contactUserId: string): Promise<boolean>;
63
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Discussion Service
3
+ *
4
+ * Class-based service for initializing, accepting, and managing discussions.
5
+ */
6
+ import { DiscussionStatus, MessageDirection, MessageStatus, DiscussionDirection, } from '../db';
7
+ import { UserPublicKeys, SessionStatus } from '#wasm';
8
+ import { EstablishSessionError } from './announcement';
9
+ import { sessionStatusToString } from '../wasm/session';
10
+ import { decodeUserId } from '../utils/userId';
11
+ import { Logger } from '../utils/logs';
12
+ const logger = new Logger('DiscussionService');
13
+ /**
14
+ * Service for managing discussions between users.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const discussionService = new DiscussionService(db, announcementService, session);
19
+ *
20
+ * // Initialize a new discussion
21
+ * const result = await discussionService.initialize(contact, 'Hello!');
22
+ *
23
+ * // Accept a discussion request
24
+ * await discussionService.accept(discussion);
25
+ *
26
+ * // Renew a broken discussion
27
+ * await discussionService.renew(contactUserId);
28
+ * ```
29
+ */
30
+ export class DiscussionService {
31
+ constructor(db, announcementService, session, events = {}) {
32
+ Object.defineProperty(this, "db", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: void 0
37
+ });
38
+ Object.defineProperty(this, "announcementService", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: void 0
43
+ });
44
+ Object.defineProperty(this, "session", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: void 0
49
+ });
50
+ Object.defineProperty(this, "events", {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: void 0
55
+ });
56
+ this.db = db;
57
+ this.announcementService = announcementService;
58
+ this.session = session;
59
+ this.events = events;
60
+ }
61
+ /**
62
+ * Initialize a discussion with a contact using SessionManager
63
+ * @param contact - The contact to start a discussion with
64
+ * @param message - Optional message to include in the announcement
65
+ * @returns The discussion ID and the created announcement
66
+ */
67
+ async initialize(contact, message) {
68
+ const log = logger.forMethod('initialize');
69
+ try {
70
+ const userId = this.session.userIdEncoded;
71
+ // Encode message as UTF-8 if provided
72
+ const userData = message
73
+ ? new TextEncoder().encode(message)
74
+ : new Uint8Array(0);
75
+ log.info(`${userId} is establishing session with contact ${contact.name}`);
76
+ const result = await this.announcementService.establishSession(UserPublicKeys.from_bytes(contact.publicKeys), userData);
77
+ let status = DiscussionStatus.PENDING;
78
+ if (!result.success) {
79
+ log.error(`Failed to establish session with contact ${contact.name}, got error: ${result.error}`);
80
+ // if the error is due to the session manager failed to establish outgoing session, throw the error
81
+ if (result.error && result.error.includes(EstablishSessionError))
82
+ throw new Error(EstablishSessionError);
83
+ status = DiscussionStatus.SEND_FAILED;
84
+ }
85
+ else {
86
+ log.info(`session established with contact and announcement sent: ${result.announcement.length}... bytes`);
87
+ }
88
+ // Parse announcement message to extract only the actual message content.
89
+ // The message parameter may be JSON format: {"u":"username","m":"message"}
90
+ // We only want to store the "m" (message) field, not the full JSON.
91
+ let parsedAnnouncementMessage;
92
+ if (message) {
93
+ if (message.startsWith('{')) {
94
+ try {
95
+ const parsed = JSON.parse(message);
96
+ parsedAnnouncementMessage = parsed.m?.trim() || undefined;
97
+ }
98
+ catch {
99
+ // Invalid JSON, treat as plain text
100
+ parsedAnnouncementMessage = message;
101
+ }
102
+ }
103
+ else {
104
+ parsedAnnouncementMessage = message;
105
+ }
106
+ }
107
+ // Persist discussion immediately with the announcement for reliable retry
108
+ const discussionId = await this.db.discussions.add({
109
+ ownerUserId: userId,
110
+ contactUserId: contact.userId,
111
+ direction: DiscussionDirection.INITIATED,
112
+ status: status,
113
+ nextSeeker: undefined,
114
+ initiationAnnouncement: result.announcement,
115
+ announcementMessage: parsedAnnouncementMessage,
116
+ unreadCount: 0,
117
+ createdAt: new Date(),
118
+ updatedAt: new Date(),
119
+ });
120
+ log.info(`discussion created with id: ${discussionId}`);
121
+ // Emit status change event
122
+ const discussion = await this.db.discussions.get(discussionId);
123
+ if (discussion) {
124
+ this.events.onDiscussionStatusChanged?.(discussion);
125
+ }
126
+ return { discussionId, announcement: result.announcement };
127
+ }
128
+ catch (error) {
129
+ log.error(`Failed to initialize discussion, error: ${error}`);
130
+ throw new Error('Discussion initialization failed, error: ' + error);
131
+ }
132
+ }
133
+ /**
134
+ * Accept a discussion request from a contact using SessionManager
135
+ * @param discussion - The discussion to accept
136
+ */
137
+ async accept(discussion) {
138
+ const log = logger.forMethod('accept');
139
+ try {
140
+ const contact = await this.db.getContactByOwnerAndUserId(discussion.ownerUserId, discussion.contactUserId);
141
+ if (!contact)
142
+ throw new Error(`Contact ${discussion.contactUserId} not found for ownerUserId ${discussion.ownerUserId}`);
143
+ const result = await this.announcementService.establishSession(UserPublicKeys.from_bytes(contact.publicKeys));
144
+ let status = DiscussionStatus.ACTIVE;
145
+ if (!result.success) {
146
+ log.error(`Failed to establish session with contact ${contact.name}, got error: ${result.error}`);
147
+ // if the error is due to the session manager failed to establish outgoing session, throw the error
148
+ if (result.error && result.error.includes(EstablishSessionError))
149
+ throw new Error(EstablishSessionError);
150
+ status = DiscussionStatus.SEND_FAILED;
151
+ }
152
+ else {
153
+ log.info(`session established with contact and announcement sent: ${result.announcement.length}... bytes`);
154
+ }
155
+ // update discussion status
156
+ await this.db.discussions.update(discussion.id, {
157
+ status: status,
158
+ initiationAnnouncement: result.announcement,
159
+ updatedAt: new Date(),
160
+ });
161
+ log.info(`discussion updated in db with status: ${status}`);
162
+ // Emit status change event
163
+ const updatedDiscussion = await this.db.discussions.get(discussion.id);
164
+ if (updatedDiscussion) {
165
+ this.events.onDiscussionStatusChanged?.(updatedDiscussion);
166
+ }
167
+ return;
168
+ }
169
+ catch (error) {
170
+ log.error(`Failed to accept pending discussion, error: ${error}`);
171
+ throw new Error('Failed to accept pending discussion, error: ' + error);
172
+ }
173
+ }
174
+ /**
175
+ * Renew a discussion by resetting sent outgoing messages and sending a new announcement.
176
+ * @param contactUserId - The user ID of the contact whose discussion should be renewed.
177
+ */
178
+ async renew(contactUserId) {
179
+ const log = logger.forMethod('renew');
180
+ const ownerUserId = this.session.userIdEncoded;
181
+ const contact = await this.db.getContactByOwnerAndUserId(ownerUserId, contactUserId);
182
+ if (!contact)
183
+ throw new Error('Contact not found');
184
+ const existingDiscussion = await this.db.getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
185
+ if (!existingDiscussion)
186
+ throw new Error('Discussion with contact ' + contact.name + ' not found');
187
+ log.info(`renewing discussion between ${ownerUserId} and ${contactUserId}`);
188
+ // reset session by creating and sending a new announcement
189
+ const result = await this.announcementService.establishSession(UserPublicKeys.from_bytes(contact.publicKeys));
190
+ // if the error is due to the session manager failed to establish outgoing session, throw the error
191
+ if (result.error && result.error.includes(EstablishSessionError))
192
+ throw new Error(EstablishSessionError);
193
+ // get the new session status
194
+ const sessionStatus = this.session.peerSessionStatus(decodeUserId(contactUserId));
195
+ log.info(`session status for discussion between ${ownerUserId} and ${contactUserId} after reinitiation is ${sessionStatusToString(sessionStatus)}`);
196
+ // Determine discussion status based on send result and session state:
197
+ // - SEND_FAILED: announcement couldn't be sent
198
+ // - ACTIVE: session fully established (peer responded)
199
+ // - RECONNECTING: true renewal, waiting for peer's response
200
+ // - PENDING: first contact retry, waiting for peer's response
201
+ let status;
202
+ if (!result.success) {
203
+ status = DiscussionStatus.SEND_FAILED;
204
+ }
205
+ else if (sessionStatus === SessionStatus.Active) {
206
+ // Session fully established (peer already responded)
207
+ status = DiscussionStatus.ACTIVE;
208
+ }
209
+ else if (existingDiscussion.status === DiscussionStatus.ACTIVE) {
210
+ // True renewal: had working session before, now recovering
211
+ status = DiscussionStatus.RECONNECTING;
212
+ }
213
+ else {
214
+ // First contact retry: never had working session
215
+ status = DiscussionStatus.PENDING;
216
+ }
217
+ await this.db.transaction('rw', [this.db.discussions, this.db.messages], async () => {
218
+ await this.db.discussions.update(existingDiscussion.id, {
219
+ status: status,
220
+ direction: DiscussionDirection.INITIATED,
221
+ initiationAnnouncement: result.announcement,
222
+ updatedAt: new Date(),
223
+ });
224
+ log.info(`discussion updated with status: ${status}`);
225
+ /* Reset outgoing messages that haven't been acknowledged by the peer.
226
+ * When session is renewed, messages encrypted with the old session
227
+ * may not be decryptable by the peer with the new session.
228
+ *
229
+ * Messages to reset (not acknowledged):
230
+ * - SENDING: Was in progress, needs re-encryption with new session
231
+ * - FAILED: Previous send failed, needs re-encryption
232
+ * - SENT: On network but not acknowledged - peer may not have received
233
+ *
234
+ * Messages to keep (acknowledged by peer):
235
+ * - DELIVERED: Peer confirmed receipt
236
+ * - READ: Peer read it
237
+ */
238
+ const messagesToReset = await this.db.messages
239
+ .where('[ownerUserId+contactUserId]')
240
+ .equals([ownerUserId, contactUserId])
241
+ .and(message => message.direction === MessageDirection.OUTGOING &&
242
+ (message.status === MessageStatus.SENDING ||
243
+ message.status === MessageStatus.FAILED ||
244
+ message.status === MessageStatus.SENT))
245
+ .modify({
246
+ status: MessageStatus.WAITING_SESSION,
247
+ encryptedMessage: undefined,
248
+ seeker: undefined,
249
+ });
250
+ log.info(`reset ${messagesToReset} messages to WAITING_SESSION`);
251
+ });
252
+ // Emit events after transaction completes
253
+ const updatedDiscussion = await this.db.discussions.get(existingDiscussion.id);
254
+ if (updatedDiscussion) {
255
+ this.events.onDiscussionStatusChanged?.(updatedDiscussion);
256
+ this.events.onSessionRenewed?.(updatedDiscussion);
257
+ }
258
+ }
259
+ /**
260
+ * Check if new messages can be sent to session manager for encryption.
261
+ * Returns false if the discussion is broken or if there are failed messages
262
+ * that have not been encrypted.
263
+ *
264
+ * @param ownerUserId - The owner user ID
265
+ * @param contactUserId - The contact user ID
266
+ * @returns true if discussion is in stable state for sending messages
267
+ */
268
+ async isStableState(ownerUserId, contactUserId) {
269
+ const log = logger.forMethod('isStableState');
270
+ const discussion = await this.db.getDiscussionByOwnerAndContact(ownerUserId, contactUserId);
271
+ if (!discussion)
272
+ throw new Error('Discussion not found');
273
+ if (discussion.status === DiscussionStatus.BROKEN) {
274
+ log.info(`Discussion with ownerUserId ${ownerUserId} and contactUserId ${contactUserId} is broken`);
275
+ return false;
276
+ }
277
+ const messages = await this.db.messages
278
+ .where('[ownerUserId+contactUserId+direction]')
279
+ .equals([
280
+ discussion.ownerUserId,
281
+ discussion.contactUserId,
282
+ MessageDirection.OUTGOING,
283
+ ])
284
+ .sortBy('id');
285
+ /* If the discussion has been broken, all non delivered messages have been marked as failed and
286
+ their encryptedMessage field has been deleted.
287
+ If there are some unencrypted unsent messages in the conversation, the discussion is not stable
288
+ i.e. we should not encrypt any new message via session manager before these messages are not resent */
289
+ if (messages.length > 0 &&
290
+ !messages[messages.length - 1].encryptedMessage &&
291
+ messages[messages.length - 1].status === MessageStatus.FAILED) {
292
+ log.info(`Discussion with ownerUserId ${ownerUserId} and contactUserId ${contactUserId} has no encryptedMessage failed messages`);
293
+ return false;
294
+ }
295
+ return true;
296
+ }
297
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Message Reception Service
3
+ *
4
+ * Handles fetching encrypted messages from the protocol and decrypting them.
5
+ * This service works both in host app contexts and SDK/automation context.
6
+ */
7
+ import { type Message, type GossipDatabase } from '../db';
8
+ import { IMessageProtocol } from '../api/messageProtocol';
9
+ import { SessionModule } from '../wasm';
10
+ import { DiscussionService } from './discussion';
11
+ import { GossipSdkEvents } from '../types/events';
12
+ import { SdkConfig } from '../config/sdk';
13
+ export interface MessageResult {
14
+ success: boolean;
15
+ newMessagesCount: number;
16
+ error?: string;
17
+ }
18
+ export interface SendMessageResult {
19
+ success: boolean;
20
+ message?: Message;
21
+ error?: string;
22
+ }
23
+ export declare class MessageService {
24
+ private db;
25
+ private messageProtocol;
26
+ private session;
27
+ private discussionService;
28
+ private events;
29
+ private config;
30
+ constructor(db: GossipDatabase, messageProtocol: IMessageProtocol, session: SessionModule, discussionService: DiscussionService, events?: GossipSdkEvents, config?: SdkConfig);
31
+ fetchMessages(): Promise<MessageResult>;
32
+ private decryptMessages;
33
+ private storeDecryptedMessages;
34
+ findMessageBySeeker(seeker: Uint8Array, ownerUserId: string): Promise<Message | undefined>;
35
+ /**
36
+ * Check if a message is a duplicate based on content and timestamp.
37
+ *
38
+ * A message is considered duplicate if:
39
+ * - Same sender (contactUserId)
40
+ * - Same content
41
+ * - Incoming direction
42
+ * - Timestamp within deduplication window (default 30 seconds)
43
+ *
44
+ * This handles the edge case where:
45
+ * 1. Sender sends message successfully to network
46
+ * 2. Sender app crashes before updating DB status to SENT
47
+ * 3. On restart, message is reset to WAITING_SESSION and re-sent
48
+ * 4. Receiver gets the same message twice with different seekers
49
+ *
50
+ * @param ownerUserId - The owner's user ID
51
+ * @param contactUserId - The sender's user ID
52
+ * @param content - The message content
53
+ * @param timestamp - The message timestamp
54
+ * @returns true if a duplicate exists
55
+ */
56
+ private isDuplicateMessage;
57
+ private acknowledgeMessages;
58
+ sendMessage(message: Message): Promise<SendMessageResult>;
59
+ private serializeMessage;
60
+ resendMessages(messages: Map<string, Message[]>): Promise<void>;
61
+ /**
62
+ * Process messages that are waiting for an active session.
63
+ * Called when a session becomes Active to send queued messages.
64
+ * Per spec: when session becomes Active, encrypt and send WAITING_SESSION messages.
65
+ *
66
+ * @param contactUserId - The contact whose session became active
67
+ * @returns Number of messages successfully sent
68
+ */
69
+ processWaitingMessages(contactUserId: string): Promise<number>;
70
+ /**
71
+ * Get count of messages waiting for session with a specific contact.
72
+ */
73
+ getWaitingMessageCount(contactUserId: string): Promise<number>;
74
+ }