@massalabs/gossip-sdk 0.0.2-dev.20260319090011 → 0.0.2-dev.20260320093215
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.
- package/dist/db/queries/messages.d.ts +1 -0
- package/dist/db/queries/messages.js +5 -0
- package/dist/gossip.d.ts +4 -0
- package/dist/gossip.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/refresh.js +16 -1
- package/dist/services/selfMessage.d.ts +28 -0
- package/dist/services/selfMessage.js +194 -0
- package/package.json +1 -1
|
@@ -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
package/dist/index.js
CHANGED
package/dist/services/refresh.js
CHANGED
|
@@ -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
|
|
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