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