@ermis-network/ermis-chat-sdk 2.0.0 → 2.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.
Files changed (54) hide show
  1. package/README.md +330 -0
  2. package/dist/encryption/index.browser.cjs +13045 -0
  3. package/dist/encryption/index.browser.cjs.map +1 -0
  4. package/dist/encryption/index.browser.mjs +12959 -0
  5. package/dist/encryption/index.browser.mjs.map +1 -0
  6. package/dist/encryption/index.cjs +13045 -0
  7. package/dist/encryption/index.cjs.map +1 -0
  8. package/dist/encryption/index.d.mts +3 -0
  9. package/dist/encryption/index.d.ts +3 -0
  10. package/dist/encryption/index.mjs +12959 -0
  11. package/dist/encryption/index.mjs.map +1 -0
  12. package/dist/index-CcvHIY5q.d.mts +4988 -0
  13. package/dist/index-CcvHIY5q.d.ts +4988 -0
  14. package/dist/index.browser.cjs +20192 -5766
  15. package/dist/index.browser.cjs.map +1 -1
  16. package/dist/index.browser.full-bundle.min.js +20 -16
  17. package/dist/index.browser.full-bundle.min.js.map +1 -1
  18. package/dist/index.browser.mjs +20106 -5731
  19. package/dist/index.browser.mjs.map +1 -1
  20. package/dist/index.cjs +20191 -5765
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.mts +15 -1337
  23. package/dist/index.d.ts +15 -1337
  24. package/dist/index.mjs +20106 -5731
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/wasm_worker.worker.mjs +8 -4
  27. package/dist/wasm_worker.worker.mjs.map +1 -1
  28. package/package.json +21 -6
  29. package/public/e2ee-media-stream-worker.js +627 -0
  30. package/public/openmls_wasm_bg.wasm +0 -0
  31. package/src/attachment_utils.ts +0 -148
  32. package/src/auth.ts +0 -352
  33. package/src/channel.ts +0 -1879
  34. package/src/channel_state.ts +0 -612
  35. package/src/client.ts +0 -1759
  36. package/src/client_state.ts +0 -55
  37. package/src/connection.ts +0 -587
  38. package/src/ermis_call_node.ts +0 -1046
  39. package/src/errors.ts +0 -60
  40. package/src/events.ts +0 -46
  41. package/src/hevc_decoder_config.ts +0 -305
  42. package/src/index.ts +0 -17
  43. package/src/media_stream_receiver.ts +0 -593
  44. package/src/media_stream_sender.ts +0 -465
  45. package/src/shims/empty.ts +0 -1
  46. package/src/signal_message.ts +0 -171
  47. package/src/system_message.ts +0 -259
  48. package/src/token_manager.ts +0 -48
  49. package/src/types.ts +0 -594
  50. package/src/utils.ts +0 -553
  51. package/src/wasm/ermis_call_node_wasm.d.ts +0 -156
  52. package/src/wasm/ermis_call_node_wasm.js +0 -1568
  53. package/src/wasm_worker.ts +0 -219
  54. package/src/wasm_worker_proxy.ts +0 -244
@@ -1,148 +0,0 @@
1
- import type { Attachment } from './types';
2
-
3
- /**
4
- * Normalize a file name for upload:
5
- * - Remove Vietnamese diacritics
6
- * - Replace đ/Đ with d/D
7
- * - Replace spaces with underscores
8
- * - Preserve extension
9
- */
10
- export function normalizeFileName(name: string): string {
11
- const dotIndex = name.lastIndexOf('.');
12
- const baseName = dotIndex > 0 ? name.slice(0, dotIndex) : name;
13
- const extension = dotIndex > 0 ? name.slice(dotIndex) : '';
14
-
15
- const normalized = baseName
16
- .normalize('NFD')
17
- .replace(/[\u0300-\u036f]/g, '') // strip diacritics
18
- .replace(/đ/g, 'd')
19
- .replace(/Đ/g, 'D')
20
- .replace(/\s+/g, '_');
21
-
22
- return normalized + extension;
23
- }
24
-
25
- /**
26
- * Check if a MIME type or file extension is HEIC/HEIF.
27
- */
28
- export function isHeicFile(file: File): boolean {
29
- const mime = file.type.toLowerCase();
30
- const ext = file.name.toLowerCase().split('.').pop() || '';
31
- return (
32
- mime === 'image/heic' ||
33
- mime === 'image/heif' ||
34
- ext === 'heic' ||
35
- ext === 'heif'
36
- );
37
- }
38
-
39
- /**
40
- * Categorize a file by MIME type.
41
- * HEIC/HEIF files are treated as 'file' (not 'image') since browsers can't render them.
42
- */
43
- export function getAttachmentCategory(
44
- mimeType: string,
45
- fileName?: string,
46
- ): 'image' | 'video' | 'audio' | 'file' {
47
- const mime = mimeType.toLowerCase();
48
- const ext = (fileName || '').toLowerCase().split('.').pop() || '';
49
-
50
- // HEIC/HEIF → file (not image)
51
- if (mime === 'image/heic' || mime === 'image/heif' || ext === 'heic' || ext === 'heif') {
52
- return 'file';
53
- }
54
-
55
- if (mime.startsWith('image/')) return 'image';
56
- if (mime.startsWith('video/')) return 'video';
57
- if (mime.startsWith('audio/')) return 'audio';
58
- return 'file';
59
- }
60
-
61
- /**
62
- * Check if a file is a video type that supports thumbnail extraction.
63
- */
64
- export function isVideoFile(file: File): boolean {
65
- return (
66
- file.type === 'video/mp4' ||
67
- file.type === 'video/webm' ||
68
- file.type === 'video/quicktime'
69
- );
70
- }
71
-
72
- /**
73
- * Metadata for voice recording attachments.
74
- */
75
- export type VoiceRecordingMeta = {
76
- waveform_data: number[];
77
- duration: number;
78
- };
79
-
80
- /**
81
- * Build a normalized attachment payload from an uploaded file.
82
- *
83
- * @param file - Original file object
84
- * @param uploadedUrl - URL returned by the upload API
85
- * @param thumbUrl - Optional thumbnail URL (for video)
86
- * @param voiceMeta - Optional voice recording metadata
87
- */
88
- export function buildAttachmentPayload(
89
- file: File,
90
- uploadedUrl: string,
91
- thumbUrl?: string,
92
- voiceMeta?: VoiceRecordingMeta,
93
- ): Attachment {
94
- const title = normalizeFileName(file.name);
95
- const mimeType = file.type || '';
96
- const category = getAttachmentCategory(mimeType, file.name);
97
-
98
- if (voiceMeta) {
99
- return {
100
- type: 'voiceRecording',
101
- asset_url: uploadedUrl,
102
- title,
103
- file_size: file.size,
104
- mime_type: mimeType,
105
- waveform_data: voiceMeta.waveform_data,
106
- duration: voiceMeta.duration,
107
- };
108
- }
109
-
110
- switch (category) {
111
- case 'image':
112
- return {
113
- type: 'image',
114
- image_url: uploadedUrl,
115
- title,
116
- file_size: file.size,
117
- mime_type: mimeType,
118
- };
119
-
120
- case 'video':
121
- return {
122
- type: 'video',
123
- asset_url: uploadedUrl,
124
- title,
125
- file_size: file.size,
126
- mime_type: mimeType,
127
- thumb_url: thumbUrl || '',
128
- };
129
-
130
- case 'audio':
131
- return {
132
- type: 'file',
133
- asset_url: uploadedUrl,
134
- title,
135
- file_size: file.size,
136
- mime_type: mimeType,
137
- };
138
-
139
- default:
140
- return {
141
- type: 'file',
142
- asset_url: uploadedUrl,
143
- title,
144
- file_size: file.size,
145
- mime_type: mimeType || '',
146
- };
147
- }
148
- }
package/src/auth.ts DELETED
@@ -1,352 +0,0 @@
1
- import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
2
- import { APIErrorResponse, ErmisChatOptions, ErrorFromResponse, Logger } from './types';
3
- import { chatCodes, isFunction, randomId, retryInterval, sleep } from './utils';
4
- import https from 'https';
5
- import { isErrorResponse } from './errors';
6
-
7
- export class ErmisAuthProvider {
8
- apiKey: string;
9
- baseURL?: string;
10
- options?: ErmisChatOptions;
11
- axiosInstance: AxiosInstance;
12
- disconnected: boolean;
13
- browser: boolean;
14
- node: boolean;
15
- logger: Logger;
16
- consecutiveFailures: number;
17
- userAgent?: string;
18
- /** Last identifier (phone or email) used for OTP */
19
- lastIdentifier?: string;
20
- /**
21
- * The last OTP method used ('Sms', 'Voice', or 'Email').
22
- * Used to verify OTP for the correct method.
23
- */
24
- lastMethod?: 'Sms' | 'Voice' | 'Email';
25
- /** Wallet address used for wallet authentication */
26
- address?: string;
27
-
28
- constructor(apiKey: string, baseURL: string, options?: ErmisChatOptions) {
29
- const inputOptions = options || {};
30
- this.apiKey = apiKey;
31
- this.baseURL = options?.userBaseURL || baseURL + '/uss/v1';
32
-
33
- this.browser = typeof inputOptions.browser !== 'undefined' ? inputOptions.browser : typeof window !== 'undefined';
34
- this.node = !this.browser;
35
- this.options = {
36
- // timeout: 3000,
37
- withCredentials: false, // making sure cookies are not sent
38
- warmUp: false,
39
- recoverStateOnReconnect: true,
40
- ...inputOptions,
41
- };
42
- if (this.node && !this.options.httpsAgent) {
43
- this.options.httpsAgent = new https.Agent({
44
- keepAlive: true,
45
- keepAliveMsecs: 3000,
46
- });
47
- }
48
- this.axiosInstance = axios.create(this.options);
49
- this.logger = isFunction(inputOptions.logger) ? inputOptions.logger : () => null;
50
- this.consecutiveFailures = 0;
51
- this.disconnected = false;
52
- }
53
-
54
- _logApiRequest(
55
- type: string,
56
- url: string,
57
- data: unknown,
58
- config: AxiosRequestConfig & {
59
- config?: AxiosRequestConfig & { maxBodyLength?: number };
60
- },
61
- ) {
62
- this.logger(
63
- 'info',
64
- `client: ${type} - Request - ${url}- ${JSON.stringify(data)} - ${JSON.stringify(config.params)}`,
65
- {
66
- tags: ['api', 'api_request', 'client'],
67
- url,
68
- payload: data,
69
- config,
70
- },
71
- );
72
- }
73
-
74
- _logApiResponse<T>(type: string, url: string, response: AxiosResponse<T>) {
75
- this.logger('info', `client:${type} - Response - url: ${url} > status ${response.status}`, {
76
- tags: ['api', 'api_response', 'client'],
77
- url,
78
- response,
79
- });
80
- }
81
-
82
- _logApiError(type: string, url: string, error: unknown, options: unknown) {
83
- this.logger(
84
- 'error',
85
- `client:${type} - Error: ${JSON.stringify(error)} - url: ${url} - options: ${JSON.stringify(options)}`,
86
- {
87
- tags: ['api', 'api_response', 'client'],
88
- url,
89
- error,
90
- },
91
- );
92
- }
93
-
94
- doAxiosRequest = async <T>(
95
- type: string,
96
- url: string,
97
- data?: unknown,
98
- options: AxiosRequestConfig & {
99
- config?: AxiosRequestConfig & { maxBodyLength?: number };
100
- } = {},
101
- ): Promise<T> => {
102
- const requestConfig = this._enrichAxiosOptions(options);
103
-
104
- try {
105
- let response: AxiosResponse<T>;
106
- this._logApiRequest(type, url, data, requestConfig);
107
- switch (type) {
108
- case 'get':
109
- response = await this.axiosInstance.get(url, requestConfig);
110
- break;
111
- case 'delete':
112
- response = await this.axiosInstance.delete(url, requestConfig);
113
- break;
114
- case 'post':
115
- response = await this.axiosInstance.post(url, data, requestConfig);
116
- break;
117
- case 'postForm':
118
- response = await this.axiosInstance.postForm(url, data, requestConfig);
119
- break;
120
- case 'put':
121
- response = await this.axiosInstance.put(url, data, requestConfig);
122
- break;
123
- case 'patch':
124
- response = await this.axiosInstance.patch(url, data, requestConfig);
125
- break;
126
- case 'options':
127
- response = await this.axiosInstance.options(url, requestConfig);
128
- break;
129
- default:
130
- throw new Error('Invalid request type');
131
- }
132
- this._logApiResponse<T>(type, url, response);
133
- this.consecutiveFailures = 0;
134
- return this.handleResponse(response);
135
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
- } catch (e: any /**TODO: generalize error types */) {
137
- e.client_request_id = requestConfig.headers?.['x-client-request-id'];
138
- this._logApiError(type, url, e, options);
139
- this.consecutiveFailures += 1;
140
- if (e.response) {
141
- /** connection_fallback depends on this token expiration logic */
142
- if (e.response.data.code === chatCodes.TOKEN_EXPIRED) {
143
- if (this.consecutiveFailures > 1) {
144
- await sleep(retryInterval(this.consecutiveFailures));
145
- }
146
- return await this.doAxiosRequest<T>(type, url, data, requestConfig);
147
- }
148
- return this.handleResponse(e.response);
149
- } else {
150
- throw e as AxiosError<APIErrorResponse>;
151
- }
152
- }
153
- };
154
-
155
- get<T>(url: string, params?: AxiosRequestConfig['params']) {
156
- return this.doAxiosRequest<T>('get', url, null, { params });
157
- }
158
-
159
- put<T>(url: string, data?: unknown) {
160
- return this.doAxiosRequest<T>('put', url, data);
161
- }
162
-
163
- post<T>(url: string, data?: unknown, params?: AxiosRequestConfig['params']) {
164
- return this.doAxiosRequest<T>('post', url, data, { params });
165
- }
166
-
167
- patch<T>(url: string, data?: unknown) {
168
- return this.doAxiosRequest<T>('patch', url, data);
169
- }
170
-
171
- delete<T>(url: string, params?: AxiosRequestConfig['params']) {
172
- return this.doAxiosRequest<T>('delete', url, null, { params });
173
- }
174
- errorFromResponse(response: AxiosResponse<APIErrorResponse>): ErrorFromResponse<APIErrorResponse> {
175
- let err: ErrorFromResponse<APIErrorResponse>;
176
- err = new ErrorFromResponse(`ErmisChat error HTTP code: ${response.status}`);
177
- if (response.data && response.data.code) {
178
- err = new Error(`ErmisChat error code ${response.data.code}: ${response.data.message}`);
179
- err.code = response.data.code;
180
- }
181
- err.response = response;
182
- err.status = response.status;
183
- return err;
184
- }
185
-
186
- handleResponse<T>(response: AxiosResponse<T>) {
187
- const data = response.data;
188
- if (isErrorResponse(response)) {
189
- throw this.errorFromResponse(response);
190
- }
191
- return data;
192
- }
193
-
194
- getUserAgent() {
195
- return (
196
- this.userAgent || `ermis-chat-sdk-javascript-client-${this.node ? 'node' : 'browser'}-${process.env.PKG_VERSION}`
197
- );
198
- }
199
- setUserAgent(userAgent: string) {
200
- this.userAgent = userAgent;
201
- }
202
- _enrichAxiosOptions(
203
- options: AxiosRequestConfig & { config?: AxiosRequestConfig } = {
204
- params: {},
205
- headers: {},
206
- config: {},
207
- },
208
- ): AxiosRequestConfig {
209
- let signal: AbortSignal | null = null;
210
-
211
- if (!options.headers?.['x-client-request-id']) {
212
- options.headers = {
213
- ...options.headers,
214
- 'x-client-request-id': randomId(),
215
- };
216
- }
217
- const {
218
- params: axiosRequestConfigParams,
219
- headers: axiosRequestConfigHeaders,
220
- ...axiosRequestConfigRest
221
- } = this.options?.axiosRequestConfig || {};
222
-
223
- let user_service_params = {
224
- // api_key: this.key,
225
- ...options.params,
226
- ...(axiosRequestConfigParams || {}),
227
- };
228
-
229
- return {
230
- params: user_service_params,
231
- headers: {
232
- 'X-Stream-Client': this.getUserAgent(),
233
- ...options.headers,
234
- ...(axiosRequestConfigHeaders || {}),
235
- },
236
- ...(signal ? { signal } : {}),
237
- ...options.config,
238
- ...(axiosRequestConfigRest || {}),
239
- };
240
- }
241
-
242
- /**
243
- * Send OTP to a phone number.
244
- * @param identifier Phone number
245
- * @param language Language code (e.g. 'En', 'Vi')
246
- * @param method Method type (e.g. 'Sms', 'Voice')
247
- */
248
- async sendOtpToPhone(identifier: string, method: 'Sms' | 'Voice'): Promise<{ success: boolean; message?: string }> {
249
- this.lastIdentifier = identifier;
250
- this.lastMethod = method;
251
- const data = {
252
- apikey: this.apiKey,
253
- identifier,
254
- language: 'Vi',
255
- method,
256
- otp_type: 'Login',
257
- };
258
- return this.post<{ success: boolean; message?: string }>(this.baseURL + '/auth/get_otp_new', data);
259
- }
260
-
261
- /**
262
- * Send OTP to a email.
263
- * @param identifier Email address
264
- * @param language Language code (e.g. 'En', 'Vi')
265
- * @param method Method type (e.g. 'Email')
266
- */
267
- async sendOtpToEmail(identifier: string): Promise<{ success: boolean; message?: string }> {
268
- this.lastIdentifier = identifier;
269
- this.lastMethod = 'Email';
270
- const data = {
271
- apikey: this.apiKey,
272
- identifier,
273
- language: 'Vi',
274
- method: 'Email',
275
- otp_type: 'Login',
276
- };
277
- return this.post<{ success: boolean; message?: string }>(this.baseURL + '/auth/get_otp_new', data);
278
- }
279
-
280
- /**
281
- * Verify OTP for phone or email.
282
- * @param otp OTP code
283
- */
284
- async verifyOtp(otp: string): Promise<{ success: boolean; message?: string }> {
285
- const data = {
286
- identifier: this.lastIdentifier,
287
- method: this.lastMethod,
288
- apikey: this.apiKey,
289
- otp,
290
- };
291
-
292
- return this.post<{ success: boolean; message?: string }>(this.baseURL + '/auth/otp_login', data);
293
- }
294
-
295
- /**
296
- * Login with Google.
297
- * @param token Google OAuth token
298
- * @param apikey API key
299
- */
300
- async loginWithGoogle(token: string): Promise<{ success: boolean; message?: string }> {
301
- const data = {
302
- token,
303
- apikey: this.apiKey,
304
- };
305
- return this.post<{ success: boolean; message?: string }>(this.baseURL + '/auth/google_login', data);
306
- }
307
-
308
- /**
309
- * Get challenge for wallet login.
310
- * @param address Wallet address
311
- * @param apiKey API key
312
- */
313
- async getWalletChallenge(address: string): Promise<any> {
314
- this.address = address;
315
- const response = await this.post<{ challenge: string }>(this.baseURL + '/auth/get_challenge', {
316
- address,
317
- apikey: this.apiKey,
318
- });
319
- const challenge = JSON.parse(response.challenge);
320
- return challenge;
321
- }
322
-
323
- private createNonce(length: number): string {
324
- let result = '';
325
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
326
- const charactersLength = characters.length;
327
- for (let i = 0; i < length; i++) {
328
- result += characters.charAt(Math.floor(Math.random() * charactersLength));
329
- }
330
- return result;
331
- }
332
-
333
- /**
334
- * Verify wallet signature after receiving the challenge.
335
- * @param address Wallet address
336
- * @param signature Signature generated by the wallet
337
- * @param nonce Nonce used in the challenge
338
- * @returns Verification result and token if successful
339
- */
340
- async verifyWalletSignature(signature: string): Promise<{ success: boolean; token?: string; message?: string }> {
341
- const data = {
342
- address: this.address,
343
- signature,
344
- nonce: this.createNonce(20),
345
- apikey: this.apiKey,
346
- };
347
- return this.post<{ success: boolean; token?: string; message?: string }>(
348
- this.baseURL + '/auth/verify_signature',
349
- data,
350
- );
351
- }
352
- }