@massalabs/gossip-sdk 0.0.2-dev.20260319090011 → 0.0.2-dev.20260323091132

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.
@@ -16,6 +16,7 @@ export declare class MessageQueries {
16
16
  batchInsert(values: MessageInsert[]): Promise<void>;
17
17
  updateById(id: number, data: Partial<MessageInsert>): Promise<void>;
18
18
  deleteByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<void>;
19
+ deleteById(id: number): Promise<void>;
19
20
  deleteDeliveredKeepAlive(ownerUserId: string): Promise<void>;
20
21
  getOutgoingSentByOwner(ownerUserId: string): Promise<MessageRow[]>;
21
22
  getWaitingCount(ownerUserId: string, contactUserId: string): Promise<number>;
@@ -83,6 +83,11 @@ export class MessageQueries {
83
83
  .delete(schema.messages)
84
84
  .where(and(eq(schema.messages.ownerUserId, ownerUserId), eq(schema.messages.contactUserId, contactUserId)));
85
85
  }
86
+ async deleteById(id) {
87
+ await this.conn.db
88
+ .delete(schema.messages)
89
+ .where(eq(schema.messages.id, id));
90
+ }
86
91
  async deleteDeliveredKeepAlive(ownerUserId) {
87
92
  await this.conn.db
88
93
  .delete(schema.messages)
package/dist/gossip.d.ts CHANGED
@@ -6,6 +6,7 @@ import { MessageService } from './services/message.js';
6
6
  import { AuthService } from './services/auth.js';
7
7
  import { ProfileService } from './services/profile.js';
8
8
  import { ContactService } from './services/contact.js';
9
+ import { SelfMessageService } from './services/selfMessage.js';
9
10
  import { type ValidationResult } from './utils/validation.js';
10
11
  import { type StorageConfig } from './db/index.js';
11
12
  import { Queries } from './db/queries/index.js';
@@ -52,6 +53,7 @@ declare class GossipSdk {
52
53
  private _message;
53
54
  private _refresh;
54
55
  private _contact;
56
+ private _selfMessage;
55
57
  /**
56
58
  * Initialize the SDK. Call once at app startup.
57
59
  */
@@ -104,6 +106,8 @@ declare class GossipSdk {
104
106
  get announcements(): AnnouncementService;
105
107
  /** Contact management */
106
108
  get contacts(): ContactService;
109
+ /** Self-message service */
110
+ get selfMessages(): SelfMessageService;
107
111
  /**
108
112
  * Update state for all discussions:
109
113
  * - Cleanup orphaned peers
package/dist/gossip.js CHANGED
@@ -47,6 +47,7 @@ import { RefreshService } from './services/refresh.js';
47
47
  import { AuthService } from './services/auth.js';
48
48
  import { ProfileService } from './services/profile.js';
49
49
  import { ContactService } from './services/contact.js';
50
+ import { SelfMessageService } from './services/selfMessage.js';
50
51
  import { validateUserIdFormat, validateUsernameFormat, } from './utils/validation.js';
51
52
  import { QueueManager } from './utils/queue.js';
52
53
  import { encodeUserId, decodeUserId } from './utils/userId.js';
@@ -149,6 +150,12 @@ class GossipSdk {
149
150
  writable: true,
150
151
  value: null
151
152
  });
153
+ Object.defineProperty(this, "_selfMessage", {
154
+ enumerable: true,
155
+ configurable: true,
156
+ writable: true,
157
+ value: null
158
+ });
152
159
  }
153
160
  // ─────────────────────────────────────────────────────────────────
154
161
  // Lifecycle
@@ -234,6 +241,8 @@ class GossipSdk {
234
241
  this._discussion = new DiscussionService(this._announcement, session, this.eventEmitter, queries);
235
242
  this._message = new MessageService(messageProtocol, session, this.eventEmitter, config, queries);
236
243
  this._refresh = new RefreshService(this._message, this._discussion, this._announcement, session, this.eventEmitter, queries, this.config);
244
+ this._selfMessage = new SelfMessageService(queries, session.userIdEncoded, encryptionKey);
245
+ await this._selfMessage.ensureDiscussionExists();
237
246
  // Publish gossip ID (public key) on messageProtocol so the user is discoverable.
238
247
  // Non-blocking: login must succeed even when the API is unreachable.
239
248
  this._auth.publishPublicKey(session.ourPk, session.userIdEncoded, queries).catch(err => {
@@ -284,6 +293,7 @@ class GossipSdk {
284
293
  this._message = null;
285
294
  this._refresh = null;
286
295
  this._contact = null;
296
+ this._selfMessage = null;
287
297
  // Clear message queues
288
298
  this.messageQueues.clear();
289
299
  // Reset to initialized state
@@ -412,6 +422,14 @@ class GossipSdk {
412
422
  }
413
423
  return this._contact;
414
424
  }
425
+ /** Self-message service */
426
+ get selfMessages() {
427
+ this.requireSession();
428
+ if (!this._selfMessage) {
429
+ throw new Error('Self-message service not initialized');
430
+ }
431
+ return this._selfMessage;
432
+ }
415
433
  /**
416
434
  * Update state for all discussions:
417
435
  * - Cleanup orphaned peers
package/dist/index.d.ts CHANGED
@@ -27,3 +27,4 @@ export * from './wasm/index.js';
27
27
  export * from './db/db.js';
28
28
  export * from './db/queries/index.js';
29
29
  export * from './db/sqlite.js';
30
+ export { SELF_CONTACT_ID } from './services/selfMessage.js';
package/dist/index.js CHANGED
@@ -27,3 +27,4 @@ export * from './wasm/index.js';
27
27
  export * from './db/db.js';
28
28
  export * from './db/queries/index.js';
29
29
  export * from './db/sqlite.js';
30
+ export { SELF_CONTACT_ID } from './services/selfMessage.js';
@@ -10,6 +10,7 @@ import { SessionStatus } from '../wasm/bindings.js';
10
10
  import { decodeUserId, encodeUserId } from '../utils/userId.js';
11
11
  import { Logger } from '../utils/logs.js';
12
12
  import { SdkEventType } from '../core/SdkEventEmitter.js';
13
+ import { SELF_CONTACT_ID } from './selfMessage.js';
13
14
  const logger = new Logger('RefreshService');
14
15
  /**
15
16
  * Service for refreshing sessions and handling keep-alive messages.
@@ -104,6 +105,9 @@ export class RefreshService {
104
105
  const allRows = await this.queries.discussions.getByOwner(ownerUserId);
105
106
  const discussions = toSortedDiscussions(allRows);
106
107
  for (const discussion of discussions) {
108
+ if (discussion.contactUserId === SELF_CONTACT_ID) {
109
+ continue;
110
+ }
107
111
  const peerId = decodeUserId(discussion.contactUserId);
108
112
  const status = this.session.peerSessionStatus(peerId);
109
113
  const previous = this.sessionStatusMap.get(discussion.contactUserId);
@@ -145,7 +149,9 @@ export class RefreshService {
145
149
  discussions: discussions.map(discussion => discussion.contactUserId),
146
150
  });
147
151
  // Step 0: cleanup orphaned sessions
148
- const discussionPeerIds = new Set(discussions.map(discussion => discussion.contactUserId));
152
+ const discussionPeerIds = new Set(discussions
153
+ .filter(discussion => discussion.contactUserId !== SELF_CONTACT_ID)
154
+ .map(discussion => discussion.contactUserId));
149
155
  const sessionPeers = this.session.peerList();
150
156
  for (const peerId of sessionPeers) {
151
157
  const encoded = encodeUserId(peerId);
@@ -160,6 +166,9 @@ export class RefreshService {
160
166
  const refreshResult = await this.session.refresh();
161
167
  const keepAlivePeerIds = refreshResult.map(peer => encodeUserId(peer));
162
168
  for (const discussion of discussions) {
169
+ if (discussion.contactUserId === SELF_CONTACT_ID) {
170
+ continue;
171
+ }
163
172
  const peerId = decodeUserId(discussion.contactUserId);
164
173
  const status = this.session.peerSessionStatus(peerId);
165
174
  await this.handleSessionStatus(discussion, status);
@@ -168,6 +177,9 @@ export class RefreshService {
168
177
  const refreshRows = await this.queries.discussions.getByOwner(ownerUserId);
169
178
  const discussionsAfterRefresh = toSortedDiscussions(refreshRows);
170
179
  const activePendingDiscussions = discussionsAfterRefresh.filter(discussion => {
180
+ if (discussion.contactUserId === SELF_CONTACT_ID) {
181
+ return false;
182
+ }
171
183
  const status = this.session.peerSessionStatus(decodeUserId(discussion.contactUserId));
172
184
  return [
173
185
  SessionStatus.Active,
@@ -185,6 +197,9 @@ export class RefreshService {
185
197
  [SessionStatus.Active, SessionStatus.Saturated].includes(this.session.peerSessionStatus(decodeUserId(discussion.contactUserId))));
186
198
  const keepAliveSet = new Set(keepAlivePeerIds);
187
199
  for (const discussion of activeEstablishedDiscussions) {
200
+ if (discussion.contactUserId === SELF_CONTACT_ID) {
201
+ continue;
202
+ }
188
203
  if (!discussion.weAccepted)
189
204
  continue;
190
205
  // Send keep alive message if needed
@@ -0,0 +1,28 @@
1
+ import { Queries } from '../db/queries/index.js';
2
+ import type { EncryptionKey } from '../wasm/encryption.js';
3
+ import { type Message } from '../db/db.js';
4
+ export declare const SELF_CONTACT_ID = "__self__";
5
+ export declare class SelfMessageService {
6
+ private readonly queries;
7
+ private readonly ownerUserId;
8
+ private readonly encryptionKey;
9
+ constructor(queries: Queries, ownerUserId: string, encryptionKey: EncryptionKey);
10
+ ensureDiscussionExists(): Promise<void>;
11
+ private encryptContent;
12
+ private decryptContent;
13
+ send(content: string): Promise<Message>;
14
+ getMessages(): Promise<Message[]>;
15
+ editMessage(id: number, newContent: string): Promise<void>;
16
+ deleteMessage(id: number): Promise<void>;
17
+ sendReaction(emoji: string, originalMessageDbId: number): Promise<{
18
+ id: number;
19
+ emoji: string;
20
+ originalMessageId: number;
21
+ }>;
22
+ removeReaction(reactionId: number): Promise<void>;
23
+ getReactions(): Promise<{
24
+ id: number;
25
+ emoji: string;
26
+ originalMessageId: number;
27
+ }[]>;
28
+ }
@@ -0,0 +1,194 @@
1
+ import { decryptAead, encryptAead, nonceFromBytes, } from '../wasm/encryption.js';
2
+ import { MessageDirection, MessageStatus, MessageType, } from '../db/db.js';
3
+ import { encodeToBase64, decodeFromBase64 } from '../utils/base64.js';
4
+ export const SELF_CONTACT_ID = '__self__';
5
+ const AAD_EMPTY = new Uint8Array(0);
6
+ const ZERO_NONCE_BYTES = new Uint8Array(16);
7
+ export class SelfMessageService {
8
+ constructor(queries, ownerUserId, encryptionKey) {
9
+ Object.defineProperty(this, "queries", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: queries
14
+ });
15
+ Object.defineProperty(this, "ownerUserId", {
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true,
19
+ value: ownerUserId
20
+ });
21
+ Object.defineProperty(this, "encryptionKey", {
22
+ enumerable: true,
23
+ configurable: true,
24
+ writable: true,
25
+ value: encryptionKey
26
+ });
27
+ }
28
+ async ensureDiscussionExists() {
29
+ const existing = await this.queries.discussions.getByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID);
30
+ if (existing)
31
+ return;
32
+ const now = new Date();
33
+ await this.queries.discussions.insert({
34
+ ownerUserId: this.ownerUserId,
35
+ contactUserId: SELF_CONTACT_ID,
36
+ weAccepted: true,
37
+ sendAnnouncement: null,
38
+ direction: 'initiated',
39
+ nextSeeker: null,
40
+ initiationAnnouncement: null,
41
+ announcementMessage: null,
42
+ lastSyncTimestamp: null,
43
+ customName: null,
44
+ lastMessageId: null,
45
+ lastMessageContent: null,
46
+ lastMessageTimestamp: null,
47
+ unreadCount: 0,
48
+ pinned: false,
49
+ killedNextRetryAt: null,
50
+ saturatedRetryAt: null,
51
+ saturatedRetryDone: false,
52
+ createdAt: now,
53
+ updatedAt: now,
54
+ });
55
+ }
56
+ async encryptContent(plaintext) {
57
+ const nonce = await nonceFromBytes(ZERO_NONCE_BYTES);
58
+ const ciphertext = await encryptAead(this.encryptionKey, nonce, new TextEncoder().encode(plaintext), AAD_EMPTY);
59
+ // Store only ciphertext; nonce is a fixed zero value for all messages.
60
+ return encodeToBase64(ciphertext);
61
+ }
62
+ async decryptContent(content) {
63
+ const cipherBytes = decodeFromBase64(content);
64
+ const nonce = await nonceFromBytes(ZERO_NONCE_BYTES);
65
+ const plaintextBytes = await decryptAead(this.encryptionKey, nonce, cipherBytes, AAD_EMPTY);
66
+ if (!plaintextBytes) {
67
+ throw new Error('Failed to decrypt self message');
68
+ }
69
+ return new TextDecoder().decode(plaintextBytes);
70
+ }
71
+ async send(content) {
72
+ const encryptedContent = await this.encryptContent(content);
73
+ const now = new Date();
74
+ const id = await this.queries.messages.insert({
75
+ ownerUserId: this.ownerUserId,
76
+ contactUserId: SELF_CONTACT_ID,
77
+ content: encryptedContent,
78
+ type: MessageType.TEXT,
79
+ direction: MessageDirection.OUTGOING,
80
+ status: MessageStatus.SENT,
81
+ timestamp: now,
82
+ });
83
+ const discussion = await this.queries.discussions.getByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID);
84
+ if (discussion?.id != null) {
85
+ await this.queries.discussions.updateById(discussion.id, {
86
+ lastMessageId: id,
87
+ lastMessageContent: null,
88
+ lastMessageTimestamp: now,
89
+ updatedAt: now,
90
+ });
91
+ }
92
+ return {
93
+ id,
94
+ ownerUserId: this.ownerUserId,
95
+ contactUserId: SELF_CONTACT_ID,
96
+ content,
97
+ type: MessageType.TEXT,
98
+ direction: MessageDirection.OUTGOING,
99
+ status: MessageStatus.SENT,
100
+ timestamp: now,
101
+ };
102
+ }
103
+ async getMessages() {
104
+ const rows = await this.queries.messages.getVisibleByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID);
105
+ const result = [];
106
+ for (const row of rows) {
107
+ try {
108
+ const plaintext = await this.decryptContent(row.content);
109
+ result.push({
110
+ id: row.id,
111
+ ownerUserId: row.ownerUserId,
112
+ contactUserId: row.contactUserId,
113
+ content: plaintext,
114
+ type: row.type,
115
+ direction: MessageDirection.OUTGOING,
116
+ status: row.status,
117
+ timestamp: row.timestamp,
118
+ });
119
+ }
120
+ catch {
121
+ // Skip messages that cannot be decrypted
122
+ }
123
+ }
124
+ return result;
125
+ }
126
+ async editMessage(id, newContent) {
127
+ const row = await this.queries.messages.getById(id);
128
+ if (!row)
129
+ return;
130
+ const encryptedContent = await this.encryptContent(newContent);
131
+ const existingMetadata = row.metadata
132
+ ? JSON.parse(row.metadata)
133
+ : {};
134
+ await this.queries.messages.updateById(id, {
135
+ content: encryptedContent,
136
+ metadata: JSON.stringify({ ...existingMetadata, edited: true }),
137
+ });
138
+ }
139
+ async deleteMessage(id) {
140
+ // Delete any reactions that reference this message via metadata.originalMessageId
141
+ const reactions = await this.queries.messages.getReactionsByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID);
142
+ const toDelete = reactions.filter(row => {
143
+ if (!row.metadata)
144
+ return false;
145
+ try {
146
+ const meta = JSON.parse(row.metadata);
147
+ return meta?.originalMessageId === id;
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ });
153
+ for (const reaction of toDelete) {
154
+ await this.queries.messages.deleteById(reaction.id);
155
+ }
156
+ await this.queries.messages.deleteById(id);
157
+ }
158
+ async sendReaction(emoji, originalMessageDbId) {
159
+ const encryptedEmoji = await this.encryptContent(emoji);
160
+ const now = new Date();
161
+ const id = await this.queries.messages.insert({
162
+ ownerUserId: this.ownerUserId,
163
+ contactUserId: SELF_CONTACT_ID,
164
+ content: encryptedEmoji,
165
+ type: MessageType.REACTION,
166
+ direction: MessageDirection.OUTGOING,
167
+ status: MessageStatus.SENT,
168
+ timestamp: now,
169
+ metadata: JSON.stringify({ originalMessageId: originalMessageDbId }),
170
+ });
171
+ return { id, emoji, originalMessageId: originalMessageDbId };
172
+ }
173
+ async removeReaction(reactionId) {
174
+ await this.queries.messages.deleteById(reactionId);
175
+ }
176
+ async getReactions() {
177
+ const rows = await this.queries.messages.getReactionsByOwnerAndContact(this.ownerUserId, SELF_CONTACT_ID);
178
+ const result = [];
179
+ for (const row of rows) {
180
+ try {
181
+ const emoji = await this.decryptContent(row.content);
182
+ const meta = row.metadata ? JSON.parse(row.metadata) : null;
183
+ const originalMessageId = meta?.originalMessageId;
184
+ if (typeof originalMessageId === 'number') {
185
+ result.push({ id: row.id, emoji, originalMessageId });
186
+ }
187
+ }
188
+ catch {
189
+ // Skip reactions that cannot be decrypted
190
+ }
191
+ }
192
+ return result;
193
+ }
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massalabs/gossip-sdk",
3
- "version": "0.0.2-dev.20260319090011",
3
+ "version": "0.0.2-dev.20260323091132",
4
4
  "description": "Gossip SDK for automation, chatbot, and integration use cases",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",