@massalabs/gossip-sdk 0.0.2-dev.20260507154753 → 0.0.2-dev.20260507171428

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.
@@ -10,7 +10,6 @@ export declare enum SdkEventType {
10
10
  MESSAGE_FAILED = "messageFailed",
11
11
  MESSAGE_DELETED = "messageDeleted",
12
12
  MESSAGE_UPDATED = "messageUpdated",
13
- SELF_MESSAGE_UPDATED = "selfMessageUpdated",
14
13
  SESSION_REQUESTED = "sessionRequested",
15
14
  SESSION_CREATED = "sessionCreated",
16
15
  SESSION_RENEWED = "sessionRenewed",
@@ -38,9 +37,6 @@ export type SdkEvents = {
38
37
  [SdkEventType.MESSAGE_UPDATED]: {
39
38
  messages: Message[];
40
39
  };
41
- [SdkEventType.SELF_MESSAGE_UPDATED]: {
42
- messages: Message[];
43
- };
44
40
  [SdkEventType.SESSION_REQUESTED]: {
45
41
  discussion: Discussion;
46
42
  contact: Contact;
@@ -10,7 +10,6 @@ export var SdkEventType;
10
10
  SdkEventType["MESSAGE_FAILED"] = "messageFailed";
11
11
  SdkEventType["MESSAGE_DELETED"] = "messageDeleted";
12
12
  SdkEventType["MESSAGE_UPDATED"] = "messageUpdated";
13
- SdkEventType["SELF_MESSAGE_UPDATED"] = "selfMessageUpdated";
14
13
  SdkEventType["SESSION_REQUESTED"] = "sessionRequested";
15
14
  SdkEventType["SESSION_CREATED"] = "sessionCreated";
16
15
  SdkEventType["SESSION_RENEWED"] = "sessionRenewed";
package/dist/gossip.js CHANGED
@@ -331,7 +331,7 @@ class GossipSdk {
331
331
  this._announcement.setPersistFlusher(() => this.awaitPendingPersist());
332
332
  this._discussion.setMessageService(this._message);
333
333
  this._refresh = new RefreshService(this._message, this._discussion, this._announcement, session, this.eventEmitter, queries, this.config);
334
- this._selfMessage = new SelfMessageService(queries, session.userIdEncoded, this.eventEmitter);
334
+ this._selfMessage = new SelfMessageService(queries, session.userIdEncoded, encryptionKey);
335
335
  await this._selfMessage.ensureDiscussionExists();
336
336
  // Publish gossip ID (public key) on messageProtocol so the user is discoverable.
337
337
  // Non-blocking: login must succeed even when the API is unreachable.
@@ -16,7 +16,6 @@ import { defaultSdkConfig } from '../config/sdk.js';
16
16
  import { SdkEventType } from '../core/SdkEventEmitter.js';
17
17
  import { and, eq, sql } from 'drizzle-orm';
18
18
  import { POST_MESSAGE_TYPES } from '../utils/message.js';
19
- import { SELF_CONTACT_ID } from './selfMessage.js';
20
19
  // ---------------------------------------------------------------------------
21
20
  // JSON serialization helpers for message fields stored as text in SQLite
22
21
  // ---------------------------------------------------------------------------
@@ -653,14 +652,12 @@ export class MessageService {
653
652
  log.info('queueing message', {
654
653
  messageType: message.type,
655
654
  });
656
- if (message.contactUserId !== SELF_CONTACT_ID) {
657
- const peerId = decodeUserId(message.contactUserId);
658
- if (peerId.length !== 32) {
659
- return {
660
- success: false,
661
- error: 'Invalid contact userId (must be 32 bytes)',
662
- };
663
- }
655
+ const peerId = decodeUserId(message.contactUserId);
656
+ if (peerId.length !== 32) {
657
+ return {
658
+ success: false,
659
+ error: 'Invalid contact userId (must be 32 bytes)',
660
+ };
664
661
  }
665
662
  // Look up discussion
666
663
  const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId, parentTx);
@@ -1,17 +1,16 @@
1
1
  import { Queries } from '../db/queries/index.js';
2
+ import type { EncryptionKey } from '../wasm/encryption.js';
2
3
  import { type Message } from '../db/db.js';
3
- import { SdkEventEmitter } from '../core/SdkEventEmitter.js';
4
4
  export declare const SELF_CONTACT_ID = "__self__";
5
5
  export declare class SelfMessageService {
6
6
  private readonly queries;
7
7
  private readonly ownerUserId;
8
- private readonly eventEmitter;
9
- constructor(queries: Queries, ownerUserId: string, eventEmitter: SdkEventEmitter);
8
+ private readonly encryptionKey;
9
+ constructor(queries: Queries, ownerUserId: string, encryptionKey: EncryptionKey);
10
10
  ensureDiscussionExists(): Promise<void>;
11
- isSelfMessage(message: Message): boolean;
12
- repliedMessageId(message: Message): number | null;
13
- send(message: Message): Promise<Message>;
14
- get(id: number): Promise<Message | undefined>;
11
+ private encryptContent;
12
+ private decryptContent;
13
+ send(content: string): Promise<Message>;
15
14
  getMessages(): Promise<Message[]>;
16
15
  editMessage(id: number, newContent: string): Promise<void>;
17
16
  deleteMessage(id: number): Promise<void>;
@@ -1,11 +1,11 @@
1
+ import { decryptAead, encryptAead, nonceFromBytes, } from '../wasm/encryption.js';
1
2
  import { MessageDirection, MessageStatus, MessageType, } from '../db/db.js';
2
- import { messages } from '../db/schema/index.js';
3
- import { or, eq, sql, and } from 'drizzle-orm';
4
- import { SdkEventType } from '../core/SdkEventEmitter.js';
5
- import { rowToMessage } from './message.js';
3
+ import { encodeToBase64, decodeFromBase64 } from '../utils/base64.js';
6
4
  export const SELF_CONTACT_ID = '__self__';
5
+ const AAD_EMPTY = new Uint8Array(0);
6
+ const ZERO_NONCE_BYTES = new Uint8Array(16);
7
7
  export class SelfMessageService {
8
- constructor(queries, ownerUserId, eventEmitter) {
8
+ constructor(queries, ownerUserId, encryptionKey) {
9
9
  Object.defineProperty(this, "queries", {
10
10
  enumerable: true,
11
11
  configurable: true,
@@ -18,11 +18,11 @@ export class SelfMessageService {
18
18
  writable: true,
19
19
  value: ownerUserId
20
20
  });
21
- Object.defineProperty(this, "eventEmitter", {
21
+ Object.defineProperty(this, "encryptionKey", {
22
22
  enumerable: true,
23
23
  configurable: true,
24
24
  writable: true,
25
- value: eventEmitter
25
+ value: encryptionKey
26
26
  });
27
27
  }
28
28
  async ensureDiscussionExists() {
@@ -53,64 +53,51 @@ export class SelfMessageService {
53
53
  updatedAt: now,
54
54
  });
55
55
  }
56
- isSelfMessage(message) {
57
- return message.contactUserId === SELF_CONTACT_ID;
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);
58
61
  }
59
- repliedMessageId(message) {
60
- if (!this.isSelfMessage(message)) {
61
- return null;
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');
62
68
  }
63
- const value = message.metadata?.originalMessageId;
64
- if (typeof value === 'number' && Number.isFinite(value)) {
65
- return value;
66
- }
67
- if (typeof value === 'string') {
68
- const parsed = Number.parseInt(value, 10);
69
- return Number.isFinite(parsed) ? parsed : null;
70
- }
71
- return null;
69
+ return new TextDecoder().decode(plaintextBytes);
72
70
  }
73
- async send(message) {
71
+ async send(content) {
72
+ const encryptedContent = await this.encryptContent(content);
73
+ const now = new Date();
74
74
  const id = await this.queries.messages.insert({
75
- ...message,
76
75
  ownerUserId: this.ownerUserId,
77
- forwardOf: message.forwardOf ? JSON.stringify(message.forwardOf) : null,
78
- metadata: message.metadata ? JSON.stringify(message.metadata) : null,
76
+ contactUserId: SELF_CONTACT_ID,
77
+ content: encryptedContent,
78
+ type: MessageType.TEXT,
79
+ direction: MessageDirection.OUTGOING,
80
+ status: MessageStatus.SENT,
81
+ timestamp: now,
79
82
  });
80
- const retrievedMessage = await this.get(id);
81
- if (!retrievedMessage) {
82
- throw new Error('Failed to send message');
83
- }
84
- return retrievedMessage;
85
- }
86
- async get(id) {
87
- const row = await this.queries.messages.getById(id);
88
- if (!row)
89
- return undefined;
90
- let metadata;
91
- if (row.metadata) {
92
- try {
93
- metadata = JSON.parse(row.metadata);
94
- }
95
- catch {
96
- metadata = undefined;
97
- }
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
+ });
98
91
  }
99
92
  return {
100
- id: row.id,
101
- ownerUserId: row.ownerUserId,
102
- contactUserId: row.contactUserId,
103
- content: row.content,
104
- type: row.type,
93
+ id,
94
+ ownerUserId: this.ownerUserId,
95
+ contactUserId: SELF_CONTACT_ID,
96
+ content,
97
+ type: MessageType.TEXT,
105
98
  direction: MessageDirection.OUTGOING,
106
- status: row.status,
107
- timestamp: row.timestamp,
108
- forwardOf: row.forwardOf
109
- ? JSON.parse(row.forwardOf)
110
- : undefined,
111
- deleteOf: row.deleteOf ? JSON.parse(row.deleteOf) : undefined,
112
- editOf: row.editOf ? JSON.parse(row.editOf) : undefined,
113
- metadata,
99
+ status: MessageStatus.SENT,
100
+ timestamp: now,
114
101
  };
115
102
  }
116
103
  async getMessages() {
@@ -118,28 +105,16 @@ export class SelfMessageService {
118
105
  const result = [];
119
106
  for (const row of rows) {
120
107
  try {
121
- let metadata;
122
- if (row.metadata) {
123
- try {
124
- metadata = JSON.parse(row.metadata);
125
- }
126
- catch {
127
- metadata = undefined;
128
- }
129
- }
108
+ const plaintext = await this.decryptContent(row.content);
130
109
  result.push({
131
110
  id: row.id,
132
111
  ownerUserId: row.ownerUserId,
133
112
  contactUserId: row.contactUserId,
134
- content: row.content,
113
+ content: plaintext,
135
114
  type: row.type,
136
- forwardOf: row.forwardOf
137
- ? JSON.parse(row.forwardOf)
138
- : undefined,
139
115
  direction: MessageDirection.OUTGOING,
140
116
  status: row.status,
141
117
  timestamp: row.timestamp,
142
- metadata,
143
118
  });
144
119
  }
145
120
  catch {
@@ -152,39 +127,41 @@ export class SelfMessageService {
152
127
  const row = await this.queries.messages.getById(id);
153
128
  if (!row)
154
129
  return;
130
+ const encryptedContent = await this.encryptContent(newContent);
155
131
  const existingMetadata = row.metadata
156
132
  ? JSON.parse(row.metadata)
157
133
  : {};
158
134
  await this.queries.messages.updateById(id, {
159
- content: newContent,
135
+ content: encryptedContent,
160
136
  metadata: JSON.stringify({ ...existingMetadata, edited: true }),
161
137
  });
162
138
  }
163
139
  async deleteMessage(id) {
164
- await this.queries.conn.db
165
- .delete(messages)
166
- .where(or(eq(messages.id, id), and(eq(messages.type, MessageType.REACTION), sql `json_extract(${messages.metadata}, '$.originalMessageId') = ${id}`)));
167
- // Remove $.originalMessageId from metadata of any message that references the deleted message
168
- const updatedRows = await this.queries.conn.db
169
- .update(messages)
170
- .set({
171
- metadata: sql `json_remove(${messages.metadata}, '$.originalMessageId')`,
172
- })
173
- .where(sql `json_extract(${messages.metadata}, '$.originalMessageId') = ${id}`)
174
- .returning();
175
- const updatedMessages = updatedRows.map(rowToMessage);
176
- if (updatedMessages.length > 0) {
177
- this.eventEmitter.emit(SdkEventType.SELF_MESSAGE_UPDATED, {
178
- messages: updatedMessages,
179
- });
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);
180
155
  }
156
+ await this.queries.messages.deleteById(id);
181
157
  }
182
158
  async sendReaction(emoji, originalMessageDbId) {
159
+ const encryptedEmoji = await this.encryptContent(emoji);
183
160
  const now = new Date();
184
161
  const id = await this.queries.messages.insert({
185
162
  ownerUserId: this.ownerUserId,
186
163
  contactUserId: SELF_CONTACT_ID,
187
- content: emoji,
164
+ content: encryptedEmoji,
188
165
  type: MessageType.REACTION,
189
166
  direction: MessageDirection.OUTGOING,
190
167
  status: MessageStatus.SENT,
@@ -215,10 +192,11 @@ export class SelfMessageService {
215
192
  const result = [];
216
193
  for (const row of rows) {
217
194
  try {
195
+ const emoji = await this.decryptContent(row.content);
218
196
  const meta = row.metadata ? JSON.parse(row.metadata) : null;
219
197
  const originalMessageId = meta?.originalMessageId;
220
198
  if (typeof originalMessageId === 'number') {
221
- result.push({ id: row.id, emoji: row.content, originalMessageId });
199
+ result.push({ id: row.id, emoji, originalMessageId });
222
200
  }
223
201
  }
224
202
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massalabs/gossip-sdk",
3
- "version": "0.0.2-dev.20260507154753",
3
+ "version": "0.0.2-dev.20260507171428",
4
4
  "description": "Gossip SDK for automation, chatbot, and integration use cases",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",