@massalabs/gossip-sdk 0.0.2-dev.20260506133713 → 0.0.2-dev.20260507142137

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,6 +10,7 @@ export declare enum SdkEventType {
10
10
  MESSAGE_FAILED = "messageFailed",
11
11
  MESSAGE_DELETED = "messageDeleted",
12
12
  MESSAGE_UPDATED = "messageUpdated",
13
+ SELF_MESSAGE_UPDATED = "selfMessageUpdated",
13
14
  SESSION_REQUESTED = "sessionRequested",
14
15
  SESSION_CREATED = "sessionCreated",
15
16
  SESSION_RENEWED = "sessionRenewed",
@@ -37,6 +38,9 @@ export type SdkEvents = {
37
38
  [SdkEventType.MESSAGE_UPDATED]: {
38
39
  messages: Message[];
39
40
  };
41
+ [SdkEventType.SELF_MESSAGE_UPDATED]: {
42
+ messages: Message[];
43
+ };
40
44
  [SdkEventType.SESSION_REQUESTED]: {
41
45
  discussion: Discussion;
42
46
  contact: Contact;
@@ -10,6 +10,7 @@ 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";
13
14
  SdkEventType["SESSION_REQUESTED"] = "sessionRequested";
14
15
  SdkEventType["SESSION_CREATED"] = "sessionCreated";
15
16
  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, encryptionKey);
334
+ this._selfMessage = new SelfMessageService(queries, session.userIdEncoded, this.eventEmitter);
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,6 +16,7 @@ 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';
19
20
  // ---------------------------------------------------------------------------
20
21
  // JSON serialization helpers for message fields stored as text in SQLite
21
22
  // ---------------------------------------------------------------------------
@@ -652,12 +653,14 @@ export class MessageService {
652
653
  log.info('queueing message', {
653
654
  messageType: message.type,
654
655
  });
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
- };
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
+ }
661
664
  }
662
665
  // Look up discussion
663
666
  const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId, parentTx);
@@ -1,16 +1,17 @@
1
1
  import { Queries } from '../db/queries/index.js';
2
- import type { EncryptionKey } from '../wasm/encryption.js';
3
2
  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 encryptionKey;
9
- constructor(queries: Queries, ownerUserId: string, encryptionKey: EncryptionKey);
8
+ private readonly eventEmitter;
9
+ constructor(queries: Queries, ownerUserId: string, eventEmitter: SdkEventEmitter);
10
10
  ensureDiscussionExists(): Promise<void>;
11
- private encryptContent;
12
- private decryptContent;
13
- send(content: string): Promise<Message>;
11
+ isSelfMessage(message: Message): boolean;
12
+ repliedMessageId(message: Message): number | null;
13
+ send(message: Message): Promise<Message>;
14
+ get(id: number): Promise<Message | undefined>;
14
15
  getMessages(): Promise<Message[]>;
15
16
  editMessage(id: number, newContent: string): Promise<void>;
16
17
  deleteMessage(id: number): Promise<void>;
@@ -1,11 +1,11 @@
1
- import { decryptAead, encryptAead, nonceFromBytes, } from '../wasm/encryption.js';
2
1
  import { MessageDirection, MessageStatus, MessageType, } from '../db/db.js';
3
- import { encodeToBase64, decodeFromBase64 } from '../utils/base64.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';
4
6
  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, encryptionKey) {
8
+ constructor(queries, ownerUserId, eventEmitter) {
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, "encryptionKey", {
21
+ Object.defineProperty(this, "eventEmitter", {
22
22
  enumerable: true,
23
23
  configurable: true,
24
24
  writable: true,
25
- value: encryptionKey
25
+ value: eventEmitter
26
26
  });
27
27
  }
28
28
  async ensureDiscussionExists() {
@@ -53,51 +53,64 @@ export class SelfMessageService {
53
53
  updatedAt: now,
54
54
  });
55
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);
56
+ isSelfMessage(message) {
57
+ return message.contactUserId === SELF_CONTACT_ID;
61
58
  }
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');
59
+ repliedMessageId(message) {
60
+ if (!this.isSelfMessage(message)) {
61
+ return null;
68
62
  }
69
- return new TextDecoder().decode(plaintextBytes);
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;
70
72
  }
71
- async send(content) {
72
- const encryptedContent = await this.encryptContent(content);
73
- const now = new Date();
73
+ async send(message) {
74
74
  const id = await this.queries.messages.insert({
75
+ ...message,
75
76
  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,
77
+ forwardOf: message.forwardOf ? JSON.stringify(message.forwardOf) : null,
78
+ metadata: message.metadata ? JSON.stringify(message.metadata) : null,
82
79
  });
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
- });
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
+ }
91
98
  }
92
99
  return {
93
- id,
94
- ownerUserId: this.ownerUserId,
95
- contactUserId: SELF_CONTACT_ID,
96
- content,
97
- type: MessageType.TEXT,
100
+ id: row.id,
101
+ ownerUserId: row.ownerUserId,
102
+ contactUserId: row.contactUserId,
103
+ content: row.content,
104
+ type: row.type,
98
105
  direction: MessageDirection.OUTGOING,
99
- status: MessageStatus.SENT,
100
- timestamp: now,
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,
101
114
  };
102
115
  }
103
116
  async getMessages() {
@@ -105,16 +118,28 @@ export class SelfMessageService {
105
118
  const result = [];
106
119
  for (const row of rows) {
107
120
  try {
108
- const plaintext = await this.decryptContent(row.content);
121
+ let metadata;
122
+ if (row.metadata) {
123
+ try {
124
+ metadata = JSON.parse(row.metadata);
125
+ }
126
+ catch {
127
+ metadata = undefined;
128
+ }
129
+ }
109
130
  result.push({
110
131
  id: row.id,
111
132
  ownerUserId: row.ownerUserId,
112
133
  contactUserId: row.contactUserId,
113
- content: plaintext,
134
+ content: row.content,
114
135
  type: row.type,
136
+ forwardOf: row.forwardOf
137
+ ? JSON.parse(row.forwardOf)
138
+ : undefined,
115
139
  direction: MessageDirection.OUTGOING,
116
140
  status: row.status,
117
141
  timestamp: row.timestamp,
142
+ metadata,
118
143
  });
119
144
  }
120
145
  catch {
@@ -127,41 +152,39 @@ export class SelfMessageService {
127
152
  const row = await this.queries.messages.getById(id);
128
153
  if (!row)
129
154
  return;
130
- const encryptedContent = await this.encryptContent(newContent);
131
155
  const existingMetadata = row.metadata
132
156
  ? JSON.parse(row.metadata)
133
157
  : {};
134
158
  await this.queries.messages.updateById(id, {
135
- content: encryptedContent,
159
+ content: newContent,
136
160
  metadata: JSON.stringify({ ...existingMetadata, edited: true }),
137
161
  });
138
162
  }
139
163
  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);
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
+ });
155
180
  }
156
- await this.queries.messages.deleteById(id);
157
181
  }
158
182
  async sendReaction(emoji, originalMessageDbId) {
159
- const encryptedEmoji = await this.encryptContent(emoji);
160
183
  const now = new Date();
161
184
  const id = await this.queries.messages.insert({
162
185
  ownerUserId: this.ownerUserId,
163
186
  contactUserId: SELF_CONTACT_ID,
164
- content: encryptedEmoji,
187
+ content: emoji,
165
188
  type: MessageType.REACTION,
166
189
  direction: MessageDirection.OUTGOING,
167
190
  status: MessageStatus.SENT,
@@ -192,11 +215,10 @@ export class SelfMessageService {
192
215
  const result = [];
193
216
  for (const row of rows) {
194
217
  try {
195
- const emoji = await this.decryptContent(row.content);
196
218
  const meta = row.metadata ? JSON.parse(row.metadata) : null;
197
219
  const originalMessageId = meta?.originalMessageId;
198
220
  if (typeof originalMessageId === 'number') {
199
- result.push({ id: row.id, emoji, originalMessageId });
221
+ result.push({ id: row.id, emoji: row.content, originalMessageId });
200
222
  }
201
223
  }
202
224
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massalabs/gossip-sdk",
3
- "version": "0.0.2-dev.20260506133713",
3
+ "version": "0.0.2-dev.20260507142137",
4
4
  "description": "Gossip SDK for automation, chatbot, and integration use cases",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",