@massalabs/gossip-sdk 0.0.1
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/README.md +484 -0
- package/package.json +41 -0
- package/src/api/messageProtocol/index.ts +53 -0
- package/src/api/messageProtocol/mock.ts +13 -0
- package/src/api/messageProtocol/rest.ts +209 -0
- package/src/api/messageProtocol/types.ts +70 -0
- package/src/config/protocol.ts +97 -0
- package/src/config/sdk.ts +131 -0
- package/src/contacts.ts +210 -0
- package/src/core/SdkEventEmitter.ts +91 -0
- package/src/core/SdkPolling.ts +134 -0
- package/src/core/index.ts +9 -0
- package/src/crypto/bip39.ts +84 -0
- package/src/crypto/encryption.ts +77 -0
- package/src/db.ts +465 -0
- package/src/gossipSdk.ts +994 -0
- package/src/index.ts +211 -0
- package/src/services/announcement.ts +653 -0
- package/src/services/auth.ts +95 -0
- package/src/services/discussion.ts +380 -0
- package/src/services/message.ts +1055 -0
- package/src/services/refresh.ts +234 -0
- package/src/sw.ts +17 -0
- package/src/types/events.ts +108 -0
- package/src/types.ts +70 -0
- package/src/utils/base64.ts +39 -0
- package/src/utils/contacts.ts +161 -0
- package/src/utils/discussions.ts +55 -0
- package/src/utils/logs.ts +86 -0
- package/src/utils/messageSerialization.ts +257 -0
- package/src/utils/queue.ts +106 -0
- package/src/utils/type.ts +7 -0
- package/src/utils/userId.ts +114 -0
- package/src/utils/validation.ts +144 -0
- package/src/utils.ts +47 -0
- package/src/wasm/encryption.ts +108 -0
- package/src/wasm/index.ts +20 -0
- package/src/wasm/loader.ts +123 -0
- package/src/wasm/session.ts +276 -0
- package/src/wasm/userKeys.ts +31 -0
- package/test/config/protocol.spec.ts +31 -0
- package/test/config/sdk.spec.ts +163 -0
- package/test/db/helpers.spec.ts +142 -0
- package/test/db/operations.spec.ts +128 -0
- package/test/db/states.spec.ts +535 -0
- package/test/integration/discussion-flow.spec.ts +422 -0
- package/test/integration/messaging-flow.spec.ts +708 -0
- package/test/integration/sdk-lifecycle.spec.ts +325 -0
- package/test/mocks/index.ts +9 -0
- package/test/mocks/mockMessageProtocol.ts +100 -0
- package/test/services/auth.spec.ts +311 -0
- package/test/services/discussion.spec.ts +279 -0
- package/test/services/message-deduplication.spec.ts +299 -0
- package/test/services/message-startup.spec.ts +331 -0
- package/test/services/message.spec.ts +817 -0
- package/test/services/refresh.spec.ts +199 -0
- package/test/services/session-status.spec.ts +349 -0
- package/test/session/wasm.spec.ts +227 -0
- package/test/setup.ts +52 -0
- package/test/utils/contacts.spec.ts +156 -0
- package/test/utils/discussions.spec.ts +66 -0
- package/test/utils/queue.spec.ts +52 -0
- package/test/utils/serialization.spec.ts +120 -0
- package/test/utils/userId.spec.ts +120 -0
- package/test/utils/validation.spec.ts +223 -0
- package/test/utils.ts +212 -0
- package/tsconfig.json +26 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API implementation of the message protocol
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BulletinItem,
|
|
7
|
+
EncryptedMessage,
|
|
8
|
+
IMessageProtocol,
|
|
9
|
+
MessageProtocolResponse,
|
|
10
|
+
} from './types';
|
|
11
|
+
import { encodeToBase64, decodeFromBase64 } from '../../utils/base64';
|
|
12
|
+
|
|
13
|
+
const BULLETIN_ENDPOINT = '/bulletin';
|
|
14
|
+
const MESSAGES_ENDPOINT = '/messages';
|
|
15
|
+
|
|
16
|
+
export type BulletinsPage = {
|
|
17
|
+
counter: string;
|
|
18
|
+
data: string;
|
|
19
|
+
}[];
|
|
20
|
+
|
|
21
|
+
type FetchMessagesResponse = {
|
|
22
|
+
key: string;
|
|
23
|
+
value: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class RestMessageProtocol implements IMessageProtocol {
|
|
27
|
+
constructor(
|
|
28
|
+
private baseUrl: string,
|
|
29
|
+
private timeout: number = 10000,
|
|
30
|
+
private retryAttempts: number = 3
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
// TODO: Implement a fetch with pagination to avoid fetching all messages at once
|
|
34
|
+
async fetchMessages(seekers: Uint8Array[]): Promise<EncryptedMessage[]> {
|
|
35
|
+
const url = `${this.baseUrl}${MESSAGES_ENDPOINT}/fetch`;
|
|
36
|
+
|
|
37
|
+
const response = await this.makeRequest<FetchMessagesResponse[]>(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ seekers: seekers.map(encodeToBase64) }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.success || !response.data) {
|
|
44
|
+
throw new Error(response.error || 'Failed to fetch messages');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return response.data.map((item: FetchMessagesResponse) => {
|
|
48
|
+
const seeker = decodeFromBase64(item.key);
|
|
49
|
+
const ciphertext = decodeFromBase64(item.value);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
seeker,
|
|
53
|
+
ciphertext,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async sendMessage(message: EncryptedMessage): Promise<void> {
|
|
59
|
+
const url = `${this.baseUrl}${MESSAGES_ENDPOINT}/`;
|
|
60
|
+
|
|
61
|
+
const response = await this.makeRequest<void>(url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
key: encodeToBase64(message.seeker),
|
|
66
|
+
value: encodeToBase64(message.ciphertext),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!response.success) {
|
|
71
|
+
throw new Error(response.error || 'Failed to send message');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async sendAnnouncement(announcement: Uint8Array): Promise<string> {
|
|
76
|
+
const url = `${this.baseUrl}${BULLETIN_ENDPOINT}`;
|
|
77
|
+
|
|
78
|
+
const response = await this.makeRequest<{ counter: string }>(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
data: encodeToBase64(announcement),
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!response.success || !response.data) {
|
|
87
|
+
throw new Error(response.error || 'Failed to broadcast outgoing session');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return response.data.counter;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async fetchAnnouncements(
|
|
94
|
+
limit: number = 50,
|
|
95
|
+
cursor?: string
|
|
96
|
+
): Promise<BulletinItem[]> {
|
|
97
|
+
const params = new URLSearchParams();
|
|
98
|
+
|
|
99
|
+
params.set('limit', limit.toString());
|
|
100
|
+
// Always pass 'after' parameter. If cursor is undefined, use '0' to fetch from the beginning.
|
|
101
|
+
// This ensures pagination works correctly: after=0 gets counters 1-20, after=20 gets 21-40, etc.
|
|
102
|
+
params.set('after', cursor ?? '0');
|
|
103
|
+
|
|
104
|
+
const url = `${this.baseUrl}${BULLETIN_ENDPOINT}?${params.toString()}`;
|
|
105
|
+
|
|
106
|
+
const response = await this.makeRequest<BulletinsPage>(url, {
|
|
107
|
+
method: 'GET',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!response.success || response.data == null) {
|
|
111
|
+
throw new Error(response.error || 'Failed to fetch announcements');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return response.data.map(item => ({
|
|
115
|
+
counter: item.counter,
|
|
116
|
+
data: decodeFromBase64(item.data),
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async fetchPublicKeyByUserId(userId: Uint8Array): Promise<string> {
|
|
121
|
+
const response = await this.makeRequest<{ value: string }>(
|
|
122
|
+
`${this.baseUrl}/auth/retrieve`,
|
|
123
|
+
{
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({ key: encodeToBase64(userId) }),
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!response.success || !response.data) {
|
|
131
|
+
throw new Error(response.error || 'Failed to fetch public key');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!response.data.value) {
|
|
135
|
+
throw new Error('Public key not found');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return response.data.value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async postPublicKey(base64PublicKeys: string): Promise<string> {
|
|
142
|
+
const url = `${this.baseUrl}/auth`;
|
|
143
|
+
|
|
144
|
+
const response = await this.makeRequest<{ value: string }>(url, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({ value: base64PublicKeys }),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!response.success || !response.data) {
|
|
151
|
+
const errorMessage = response.error || 'Failed to store public key';
|
|
152
|
+
throw new Error(errorMessage);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return response.data.value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async makeRequest<T>(
|
|
159
|
+
url: string,
|
|
160
|
+
options: RequestInit
|
|
161
|
+
): Promise<MessageProtocolResponse<T>> {
|
|
162
|
+
let lastError: Error | null = null;
|
|
163
|
+
|
|
164
|
+
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
165
|
+
try {
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
168
|
+
|
|
169
|
+
const response = await fetch(url, {
|
|
170
|
+
...options,
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
clearTimeout(timeoutId);
|
|
175
|
+
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const data = await response.json();
|
|
181
|
+
return { success: true, data };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
lastError = error as Error;
|
|
184
|
+
console.warn(`Request attempt ${attempt} failed:`, error);
|
|
185
|
+
|
|
186
|
+
if (attempt < this.retryAttempts) {
|
|
187
|
+
// Exponential backoff
|
|
188
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: lastError?.message || 'Request failed after all retry attempts',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async changeNode(nodeUrl?: string): Promise<MessageProtocolResponse> {
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
data:
|
|
204
|
+
'This message protocol provider use a single node, so changing the node to ' +
|
|
205
|
+
nodeUrl +
|
|
206
|
+
' is not supported',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Protocol Types and Interfaces
|
|
3
|
+
*
|
|
4
|
+
* Defines the core types and interfaces for message protocol operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type BulletinItem = {
|
|
8
|
+
counter: string;
|
|
9
|
+
data: Uint8Array;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface EncryptedMessage {
|
|
13
|
+
seeker: Uint8Array;
|
|
14
|
+
ciphertext: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MessageProtocolResponse<T = unknown> {
|
|
18
|
+
success: boolean;
|
|
19
|
+
data?: T;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Abstract interface for message protocol operations
|
|
25
|
+
*/
|
|
26
|
+
export interface IMessageProtocol {
|
|
27
|
+
/**
|
|
28
|
+
* Fetch encrypted messages for the provided set of seeker read keys
|
|
29
|
+
*/
|
|
30
|
+
fetchMessages(seekers: Uint8Array[]): Promise<EncryptedMessage[]>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Send an encrypted message to the key-value store
|
|
34
|
+
*/
|
|
35
|
+
sendMessage(message: EncryptedMessage): Promise<void>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Broadcast an outgoing session announcement produced by WASM.
|
|
39
|
+
* Returns the bulletin counter provided by the API.
|
|
40
|
+
*/
|
|
41
|
+
sendAnnouncement(announcement: Uint8Array): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Fetch incoming discussion announcements from the bulletin storage.
|
|
44
|
+
* Returns raw announcement bytes as provided by the API.
|
|
45
|
+
* @param limit - Maximum number of announcements to fetch (default: 20)
|
|
46
|
+
* @param cursor - Optional cursor (counter) to fetch announcements after this value
|
|
47
|
+
*/
|
|
48
|
+
fetchAnnouncements(limit?: number, cursor?: string): Promise<BulletinItem[]>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fetch public key by userId hash (base64 string)
|
|
52
|
+
* @param userId - Decoded userId bytes
|
|
53
|
+
* @returns Base64-encoded public keys
|
|
54
|
+
*/
|
|
55
|
+
fetchPublicKeyByUserId(userId: Uint8Array): Promise<string>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Store public key in the auth API
|
|
59
|
+
* @param base64PublicKeys - Base64-encoded public keys
|
|
60
|
+
* @returns The hash key (hex string) returned by the API
|
|
61
|
+
*/
|
|
62
|
+
postPublicKey(base64PublicKeys: string): Promise<string>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Change the current node provider
|
|
66
|
+
* @param nodeUrl - The URL of the new node
|
|
67
|
+
* @returns MessageProtocolResponse with the new node information
|
|
68
|
+
*/
|
|
69
|
+
changeNode(nodeUrl?: string): Promise<MessageProtocolResponse>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol API Configuration
|
|
3
|
+
*
|
|
4
|
+
* Centralized configuration for the message protocol API endpoints.
|
|
5
|
+
* This allows easy switching between different protocol implementations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ProtocolConfig {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
timeout: number;
|
|
11
|
+
retryAttempts: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Default API URL for the hosted REST protocol.
|
|
15
|
+
// Override via setProtocolBaseUrl() or environment variables.
|
|
16
|
+
const DEFAULT_API_URL = 'https://api.usegossip.com';
|
|
17
|
+
|
|
18
|
+
// Mutable config that can be updated at runtime
|
|
19
|
+
let currentBaseUrl: string | null = null;
|
|
20
|
+
|
|
21
|
+
function buildProtocolApiBaseUrl(): string {
|
|
22
|
+
// If runtime override is set, use it
|
|
23
|
+
if (currentBaseUrl !== null) {
|
|
24
|
+
return currentBaseUrl;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try to get from environment variable (Vite)
|
|
28
|
+
let apiUrl: string | undefined;
|
|
29
|
+
try {
|
|
30
|
+
// Check if import.meta.env is available (Vite environment)
|
|
31
|
+
if (
|
|
32
|
+
typeof import.meta !== 'undefined' &&
|
|
33
|
+
import.meta.env?.VITE_GOSSIP_API_URL
|
|
34
|
+
) {
|
|
35
|
+
apiUrl = import.meta.env.VITE_GOSSIP_API_URL;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// import.meta.env not available (Node.js without Vite)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check process.env for Node.js environment
|
|
42
|
+
if (
|
|
43
|
+
!apiUrl &&
|
|
44
|
+
typeof process !== 'undefined' &&
|
|
45
|
+
process.env?.GOSSIP_API_URL
|
|
46
|
+
) {
|
|
47
|
+
apiUrl = process.env.GOSSIP_API_URL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fall back to default
|
|
51
|
+
if (!apiUrl) apiUrl = DEFAULT_API_URL;
|
|
52
|
+
|
|
53
|
+
// Normalize trailing slashes to avoid `//api`
|
|
54
|
+
const trimmed = apiUrl.replace(/\/+$/, '');
|
|
55
|
+
return `${trimmed}/api`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const protocolConfig: ProtocolConfig = {
|
|
59
|
+
get baseUrl() {
|
|
60
|
+
return buildProtocolApiBaseUrl();
|
|
61
|
+
},
|
|
62
|
+
timeout: 10000,
|
|
63
|
+
retryAttempts: 3,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export enum MessageProtocolType {
|
|
67
|
+
REST = 'rest',
|
|
68
|
+
MOCK = 'mock',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const defaultMessageProtocol: MessageProtocolType =
|
|
72
|
+
MessageProtocolType.REST;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set the base URL for the protocol API at runtime.
|
|
76
|
+
* This overrides environment variables and defaults.
|
|
77
|
+
*
|
|
78
|
+
* @param baseUrl - The base URL to use (e.g., 'https://api.example.com/api')
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* import { setProtocolBaseUrl } from 'gossip-sdk';
|
|
83
|
+
*
|
|
84
|
+
* // Set custom API endpoint
|
|
85
|
+
* setProtocolBaseUrl('https://my-server.com/api');
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function setProtocolBaseUrl(baseUrl: string): void {
|
|
89
|
+
currentBaseUrl = baseUrl;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Reset the base URL to use environment variables or defaults.
|
|
94
|
+
*/
|
|
95
|
+
export function resetProtocolBaseUrl(): void {
|
|
96
|
+
currentBaseUrl = null;
|
|
97
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Configuration
|
|
3
|
+
*
|
|
4
|
+
* Centralized configuration for the Gossip SDK.
|
|
5
|
+
* All values have sensible defaults that can be overridden.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Protocol configuration for network requests
|
|
10
|
+
*/
|
|
11
|
+
export interface ProtocolConfig {
|
|
12
|
+
/** API base URL (default: from environment or https://api.usegossip.com) */
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
/** Request timeout in milliseconds (default: 10000) */
|
|
15
|
+
timeout: number;
|
|
16
|
+
/** Number of retry attempts for failed requests (default: 3) */
|
|
17
|
+
retryAttempts: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Polling configuration for automatic message/announcement fetching
|
|
22
|
+
*/
|
|
23
|
+
export interface PollingConfig {
|
|
24
|
+
/** Enable automatic polling (default: false) */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** Interval for fetching messages in milliseconds (default: 5000) */
|
|
27
|
+
messagesIntervalMs: number;
|
|
28
|
+
/** Interval for fetching announcements in milliseconds (default: 10000) */
|
|
29
|
+
announcementsIntervalMs: number;
|
|
30
|
+
/** Interval for session refresh/keep-alive in milliseconds (default: 30000) */
|
|
31
|
+
sessionRefreshIntervalMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Message fetching configuration
|
|
36
|
+
*/
|
|
37
|
+
export interface MessagesConfig {
|
|
38
|
+
/** Delay between fetch iterations in milliseconds (default: 100) */
|
|
39
|
+
fetchDelayMs: number;
|
|
40
|
+
/** Maximum number of fetch iterations per call (default: 30) */
|
|
41
|
+
maxFetchIterations: number;
|
|
42
|
+
/**
|
|
43
|
+
* Time window in milliseconds for duplicate detection (default: 30000 = 30 seconds).
|
|
44
|
+
* Messages with same content from same sender within this window are considered duplicates.
|
|
45
|
+
* This handles edge case where app crashes after network send but before DB update,
|
|
46
|
+
* resulting in message being re-sent on restart.
|
|
47
|
+
*/
|
|
48
|
+
deduplicationWindowMs: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Announcement configuration
|
|
53
|
+
*/
|
|
54
|
+
export interface AnnouncementsConfig {
|
|
55
|
+
/** Maximum announcements to fetch per request (default: 500) */
|
|
56
|
+
fetchLimit: number;
|
|
57
|
+
/** Time before marking failed announcements as broken in ms (default: 3600000 = 1 hour) */
|
|
58
|
+
brokenThresholdMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Complete SDK configuration
|
|
63
|
+
*/
|
|
64
|
+
export interface SdkConfig {
|
|
65
|
+
/** Network/protocol settings */
|
|
66
|
+
protocol: ProtocolConfig;
|
|
67
|
+
/** Automatic polling settings */
|
|
68
|
+
polling: PollingConfig;
|
|
69
|
+
/** Message fetching settings */
|
|
70
|
+
messages: MessagesConfig;
|
|
71
|
+
/** Announcement settings */
|
|
72
|
+
announcements: AnnouncementsConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Default SDK configuration values
|
|
77
|
+
*/
|
|
78
|
+
export const defaultSdkConfig: SdkConfig = {
|
|
79
|
+
protocol: {
|
|
80
|
+
timeout: 10000,
|
|
81
|
+
retryAttempts: 3,
|
|
82
|
+
},
|
|
83
|
+
polling: {
|
|
84
|
+
enabled: false,
|
|
85
|
+
messagesIntervalMs: 5000,
|
|
86
|
+
announcementsIntervalMs: 10000,
|
|
87
|
+
sessionRefreshIntervalMs: 30000,
|
|
88
|
+
},
|
|
89
|
+
messages: {
|
|
90
|
+
fetchDelayMs: 100,
|
|
91
|
+
maxFetchIterations: 30,
|
|
92
|
+
deduplicationWindowMs: 30000, // 30 seconds
|
|
93
|
+
},
|
|
94
|
+
announcements: {
|
|
95
|
+
fetchLimit: 500,
|
|
96
|
+
brokenThresholdMs: 60 * 60 * 1000, // 1 hour
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Deep merge partial config with defaults
|
|
102
|
+
*/
|
|
103
|
+
export function mergeConfig(partial?: DeepPartial<SdkConfig>): SdkConfig {
|
|
104
|
+
if (!partial) return { ...defaultSdkConfig };
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
protocol: {
|
|
108
|
+
...defaultSdkConfig.protocol,
|
|
109
|
+
...partial.protocol,
|
|
110
|
+
},
|
|
111
|
+
polling: {
|
|
112
|
+
...defaultSdkConfig.polling,
|
|
113
|
+
...partial.polling,
|
|
114
|
+
},
|
|
115
|
+
messages: {
|
|
116
|
+
...defaultSdkConfig.messages,
|
|
117
|
+
...partial.messages,
|
|
118
|
+
},
|
|
119
|
+
announcements: {
|
|
120
|
+
...defaultSdkConfig.announcements,
|
|
121
|
+
...partial.announcements,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Helper type for deep partial objects
|
|
128
|
+
*/
|
|
129
|
+
export type DeepPartial<T> = {
|
|
130
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
131
|
+
};
|
package/src/contacts.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Management SDK
|
|
3
|
+
*
|
|
4
|
+
* Functions for managing contacts including CRUD operations.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { getContacts, addContact, deleteContact } from 'gossip-sdk';
|
|
9
|
+
*
|
|
10
|
+
* // Get all contacts
|
|
11
|
+
* const contacts = await getContacts(userId);
|
|
12
|
+
*
|
|
13
|
+
* // Add a new contact
|
|
14
|
+
* const result = await addContact(userId, contactUserId, 'Alice', publicKeys);
|
|
15
|
+
*
|
|
16
|
+
* // Delete a contact
|
|
17
|
+
* await deleteContact(userId, contactUserId);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
updateContactName as updateContactNameUtil,
|
|
23
|
+
deleteContact as deleteContactUtil,
|
|
24
|
+
} from './utils/contacts';
|
|
25
|
+
import { type Contact, type GossipDatabase } from './db';
|
|
26
|
+
import type {
|
|
27
|
+
UpdateContactNameResult,
|
|
28
|
+
DeleteContactResult,
|
|
29
|
+
} from './utils/contacts';
|
|
30
|
+
import type { UserPublicKeys } from './assets/generated/wasm/gossip_wasm';
|
|
31
|
+
import type { SessionModule } from './wasm/session';
|
|
32
|
+
|
|
33
|
+
// Re-export result types
|
|
34
|
+
export type { UpdateContactNameResult, DeleteContactResult };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get all contacts for an owner.
|
|
38
|
+
*
|
|
39
|
+
* @param ownerUserId - The user ID of the contact owner
|
|
40
|
+
* @param db - Database instance
|
|
41
|
+
* @returns Array of contacts
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const contacts = await getContacts(myUserId, db);
|
|
46
|
+
* contacts.forEach(c => console.log(c.name, c.userId));
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export async function getContacts(
|
|
50
|
+
ownerUserId: string,
|
|
51
|
+
db: GossipDatabase
|
|
52
|
+
): Promise<Contact[]> {
|
|
53
|
+
try {
|
|
54
|
+
return await db.getContactsByOwner(ownerUserId);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error getting contacts:', error);
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get a specific contact by owner and contact user IDs.
|
|
63
|
+
*
|
|
64
|
+
* @param ownerUserId - The user ID of the contact owner
|
|
65
|
+
* @param contactUserId - The user ID of the contact
|
|
66
|
+
* @param db - Database instance
|
|
67
|
+
* @returns Contact or null if not found
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const contact = await getContact(myUserId, theirUserId, db);
|
|
72
|
+
* if (contact) {
|
|
73
|
+
* console.log('Found contact:', contact.name);
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export async function getContact(
|
|
78
|
+
ownerUserId: string,
|
|
79
|
+
contactUserId: string,
|
|
80
|
+
db: GossipDatabase
|
|
81
|
+
): Promise<Contact | null> {
|
|
82
|
+
try {
|
|
83
|
+
const contact = await db.getContactByOwnerAndUserId(
|
|
84
|
+
ownerUserId,
|
|
85
|
+
contactUserId
|
|
86
|
+
);
|
|
87
|
+
return contact ?? null;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Error getting contact:', error);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Add a new contact.
|
|
96
|
+
*
|
|
97
|
+
* @param ownerUserId - The user ID of the contact owner
|
|
98
|
+
* @param userId - The user ID of the contact (Bech32-encoded)
|
|
99
|
+
* @param name - Display name for the contact
|
|
100
|
+
* @param publicKeys - The contact's public keys
|
|
101
|
+
* @param db - Database instance
|
|
102
|
+
* @returns Result with success status and optional contact
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const result = await addContact(
|
|
107
|
+
* myUserId,
|
|
108
|
+
* 'gossip1abc...',
|
|
109
|
+
* 'Alice',
|
|
110
|
+
* alicePublicKeys,
|
|
111
|
+
* db
|
|
112
|
+
* );
|
|
113
|
+
* if (result.success) {
|
|
114
|
+
* console.log('Contact added:', result.contact?.name);
|
|
115
|
+
* } else if (result.error === 'Contact already exists') {
|
|
116
|
+
* console.log('Contact already exists');
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export async function addContact(
|
|
121
|
+
ownerUserId: string,
|
|
122
|
+
userId: string,
|
|
123
|
+
name: string,
|
|
124
|
+
publicKeys: UserPublicKeys,
|
|
125
|
+
db: GossipDatabase
|
|
126
|
+
): Promise<{ success: boolean; error?: string; contact?: Contact }> {
|
|
127
|
+
try {
|
|
128
|
+
// Check if contact already exists
|
|
129
|
+
const existing = await db.getContactByOwnerAndUserId(ownerUserId, userId);
|
|
130
|
+
if (existing) {
|
|
131
|
+
return { success: false, error: 'Contact already exists' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const contact: Contact = {
|
|
135
|
+
ownerUserId,
|
|
136
|
+
userId,
|
|
137
|
+
name,
|
|
138
|
+
publicKeys: publicKeys.to_bytes(),
|
|
139
|
+
isOnline: false,
|
|
140
|
+
lastSeen: new Date(),
|
|
141
|
+
createdAt: new Date(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const id = await db.contacts.add(contact);
|
|
145
|
+
const newContact = await db.contacts.get(id);
|
|
146
|
+
return { success: true, contact: newContact };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Error adding contact:', error);
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Update contact name.
|
|
158
|
+
*
|
|
159
|
+
* @param ownerUserId - The user ID of the contact owner
|
|
160
|
+
* @param contactUserId - The user ID of the contact
|
|
161
|
+
* @param newName - New name for the contact
|
|
162
|
+
* @param db - Database instance
|
|
163
|
+
* @returns Result with success status and trimmed name
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* const result = await updateContactName(myUserId, theirUserId, 'Alice Smith', db);
|
|
168
|
+
* if (result.ok) {
|
|
169
|
+
* console.log('Updated to:', result.trimmedName);
|
|
170
|
+
* } else {
|
|
171
|
+
* console.error('Failed:', result.message);
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export async function updateContactName(
|
|
176
|
+
ownerUserId: string,
|
|
177
|
+
contactUserId: string,
|
|
178
|
+
newName: string,
|
|
179
|
+
db: GossipDatabase
|
|
180
|
+
): Promise<UpdateContactNameResult> {
|
|
181
|
+
return await updateContactNameUtil(ownerUserId, contactUserId, newName, db);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Delete a contact and all associated discussions and messages.
|
|
186
|
+
*
|
|
187
|
+
* @param ownerUserId - The user ID of the contact owner
|
|
188
|
+
* @param contactUserId - The user ID of the contact to delete
|
|
189
|
+
* @param db - Database instance
|
|
190
|
+
* @param session - Session module for peer management
|
|
191
|
+
* @returns Result with success status
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* const result = await deleteContact(myUserId, theirUserId, db, session);
|
|
196
|
+
* if (result.ok) {
|
|
197
|
+
* console.log('Contact deleted');
|
|
198
|
+
* } else {
|
|
199
|
+
* console.error('Failed:', result.message);
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export async function deleteContact(
|
|
204
|
+
ownerUserId: string,
|
|
205
|
+
contactUserId: string,
|
|
206
|
+
db: GossipDatabase,
|
|
207
|
+
session: SessionModule
|
|
208
|
+
): Promise<DeleteContactResult> {
|
|
209
|
+
return await deleteContactUtil(ownerUserId, contactUserId, db, session);
|
|
210
|
+
}
|