chatly-sdk 1.0.0 → 2.0.0
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/CONTRIBUTING.md +658 -0
- package/IMPROVEMENTS.md +402 -0
- package/LICENSE +21 -0
- package/README.md +1576 -162
- package/dist/index.d.ts +502 -11
- package/dist/index.js +1619 -66
- package/examples/01-basic-chat/README.md +61 -0
- package/examples/01-basic-chat/index.js +58 -0
- package/examples/01-basic-chat/package.json +13 -0
- package/examples/02-group-chat/README.md +78 -0
- package/examples/02-group-chat/index.js +76 -0
- package/examples/02-group-chat/package.json +13 -0
- package/examples/03-offline-messaging/README.md +73 -0
- package/examples/03-offline-messaging/index.js +80 -0
- package/examples/03-offline-messaging/package.json +13 -0
- package/examples/04-live-chat/README.md +80 -0
- package/examples/04-live-chat/index.js +114 -0
- package/examples/04-live-chat/package.json +13 -0
- package/examples/05-hybrid-messaging/README.md +71 -0
- package/examples/05-hybrid-messaging/index.js +106 -0
- package/examples/05-hybrid-messaging/package.json +13 -0
- package/examples/06-postgresql-integration/README.md +101 -0
- package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
- package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
- package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
- package/examples/06-postgresql-integration/index.js +92 -0
- package/examples/06-postgresql-integration/package.json +14 -0
- package/examples/06-postgresql-integration/schema.sql +58 -0
- package/examples/08-customer-support/README.md +70 -0
- package/examples/08-customer-support/index.js +104 -0
- package/examples/08-customer-support/package.json +13 -0
- package/examples/README.md +105 -0
- package/jest.config.cjs +28 -0
- package/package.json +15 -6
- package/src/chat/ChatSession.ts +160 -3
- package/src/chat/GroupSession.ts +108 -1
- package/src/constants.ts +61 -0
- package/src/crypto/e2e.ts +9 -20
- package/src/crypto/utils.ts +3 -1
- package/src/index.ts +530 -63
- package/src/models/mediaTypes.ts +62 -0
- package/src/models/message.ts +4 -1
- package/src/storage/adapters.ts +36 -0
- package/src/storage/localStorage.ts +49 -0
- package/src/storage/s3Storage.ts +84 -0
- package/src/stores/adapters.ts +2 -0
- package/src/stores/memory/messageStore.ts +8 -0
- package/src/transport/adapters.ts +51 -1
- package/src/transport/memoryTransport.ts +75 -13
- package/src/transport/websocketClient.ts +269 -21
- package/src/transport/websocketServer.ts +26 -26
- package/src/utils/errors.ts +97 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/mediaUtils.ts +235 -0
- package/src/utils/messageQueue.ts +162 -0
- package/src/utils/validation.ts +99 -0
- package/test/crypto.test.ts +122 -35
- package/test/sdk.test.ts +276 -0
- package/test/validation.test.ts +64 -0
- package/tsconfig.json +11 -10
- package/tsconfig.test.json +11 -0
- package/src/ChatManager.ts +0 -103
- package/src/crypto/keyManager.ts +0 -28
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media types supported by the SDK
|
|
3
|
+
*/
|
|
4
|
+
export enum MediaType {
|
|
5
|
+
IMAGE = 'image',
|
|
6
|
+
AUDIO = 'audio',
|
|
7
|
+
VIDEO = 'video',
|
|
8
|
+
DOCUMENT = 'document',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Media metadata
|
|
13
|
+
*/
|
|
14
|
+
export interface MediaMetadata {
|
|
15
|
+
filename: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
size: number;
|
|
18
|
+
width?: number; // For images/videos
|
|
19
|
+
height?: number; // For images/videos
|
|
20
|
+
duration?: number; // For audio/video (in seconds)
|
|
21
|
+
thumbnail?: string; // Base64 thumbnail for images/videos
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Media attachment
|
|
26
|
+
*/
|
|
27
|
+
export interface MediaAttachment {
|
|
28
|
+
type: MediaType;
|
|
29
|
+
data?: string | undefined; // Base64 encoded file data (optional if stored remotely)
|
|
30
|
+
iv?: string | undefined; // Separate IV for media data encryption
|
|
31
|
+
metadata: MediaMetadata;
|
|
32
|
+
storage?: 'local' | 's3' | undefined; // Storage provider used
|
|
33
|
+
storageKey?: string | undefined; // Key/path in the storage provider
|
|
34
|
+
url?: string | undefined; // Public URL if available
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Supported MIME types
|
|
39
|
+
*/
|
|
40
|
+
export const SUPPORTED_MIME_TYPES = {
|
|
41
|
+
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
42
|
+
audio: ['audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/wav', 'audio/webm'],
|
|
43
|
+
video: ['video/mp4', 'video/webm', 'video/ogg'],
|
|
44
|
+
document: [
|
|
45
|
+
'application/pdf',
|
|
46
|
+
'application/msword',
|
|
47
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
48
|
+
'application/vnd.ms-excel',
|
|
49
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
50
|
+
'text/plain',
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* File size limits (in bytes)
|
|
56
|
+
*/
|
|
57
|
+
export const FILE_SIZE_LIMITS = {
|
|
58
|
+
image: 10 * 1024 * 1024, // 10 MB
|
|
59
|
+
audio: 16 * 1024 * 1024, // 16 MB
|
|
60
|
+
video: 100 * 1024 * 1024, // 100 MB
|
|
61
|
+
document: 100 * 1024 * 1024, // 100 MB
|
|
62
|
+
};
|
package/src/models/message.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { MediaAttachment } from './mediaTypes.js';
|
|
2
|
+
|
|
3
|
+
export type MessageType = "text" | "media" | "system";
|
|
2
4
|
|
|
3
5
|
export interface Message {
|
|
4
6
|
id: string;
|
|
@@ -9,5 +11,6 @@ export interface Message {
|
|
|
9
11
|
iv: string;
|
|
10
12
|
timestamp: number;
|
|
11
13
|
type: MessageType;
|
|
14
|
+
media?: MediaAttachment; // Optional media attachment
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface StorageUploadResult {
|
|
2
|
+
storageKey: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface StorageProvider {
|
|
7
|
+
/**
|
|
8
|
+
* Name of the storage provider (e.g., 'local', 's3')
|
|
9
|
+
*/
|
|
10
|
+
readonly name: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Upload data to storage
|
|
14
|
+
* @param data Base64 encoded data or Buffer
|
|
15
|
+
* @param filename Desired filename or path
|
|
16
|
+
* @param mimeType MIME type of the file
|
|
17
|
+
*/
|
|
18
|
+
upload(
|
|
19
|
+
data: string | Buffer,
|
|
20
|
+
filename: string,
|
|
21
|
+
mimeType: string
|
|
22
|
+
): Promise<StorageUploadResult>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Download data from storage
|
|
26
|
+
* @param storageKey Key/path of the file in storage
|
|
27
|
+
* @returns Base64 encoded data
|
|
28
|
+
*/
|
|
29
|
+
download(storageKey: string): Promise<string>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Delete data from storage
|
|
33
|
+
* @param storageKey Key/path of the file in storage
|
|
34
|
+
*/
|
|
35
|
+
delete(storageKey: string): Promise<void>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { StorageProvider, StorageUploadResult } from './adapters.js';
|
|
4
|
+
|
|
5
|
+
export class LocalStorageProvider implements StorageProvider {
|
|
6
|
+
public readonly name = 'local';
|
|
7
|
+
private storageDir: string;
|
|
8
|
+
|
|
9
|
+
constructor(storageDir: string = './storage') {
|
|
10
|
+
this.storageDir = path.resolve(storageDir);
|
|
11
|
+
// Ensure storage directory exists
|
|
12
|
+
fs.mkdir(this.storageDir, { recursive: true }).catch(() => {});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async upload(
|
|
16
|
+
data: string | Buffer,
|
|
17
|
+
filename: string,
|
|
18
|
+
mimeType: string
|
|
19
|
+
): Promise<StorageUploadResult> {
|
|
20
|
+
const buffer = typeof data === 'string' ? Buffer.from(data, 'base64') : data;
|
|
21
|
+
const filePath = path.join(this.storageDir, filename);
|
|
22
|
+
|
|
23
|
+
// Ensure parent directories exist
|
|
24
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
25
|
+
await fs.writeFile(filePath, buffer);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
storageKey: filename,
|
|
29
|
+
url: `file://${filePath}`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async download(storageKey: string): Promise<string> {
|
|
34
|
+
const filePath = path.join(this.storageDir, storageKey);
|
|
35
|
+
const buffer = await fs.readFile(filePath);
|
|
36
|
+
return buffer.toString('base64');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async delete(storageKey: string): Promise<void> {
|
|
40
|
+
const filePath = path.join(this.storageDir, storageKey);
|
|
41
|
+
try {
|
|
42
|
+
await fs.unlink(filePath);
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
if (error.code !== 'ENOENT') {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { StorageProvider, StorageUploadResult } from './adapters.js';
|
|
3
|
+
|
|
4
|
+
export interface S3Config {
|
|
5
|
+
region: string;
|
|
6
|
+
bucket: string;
|
|
7
|
+
credentials?: {
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
};
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
forcePathStyle?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class S3StorageProvider implements StorageProvider {
|
|
16
|
+
public readonly name = 's3';
|
|
17
|
+
private client: S3Client;
|
|
18
|
+
private bucket: string;
|
|
19
|
+
|
|
20
|
+
constructor(config: S3Config) {
|
|
21
|
+
const s3Config: any = {
|
|
22
|
+
region: config.region,
|
|
23
|
+
forcePathStyle: config.forcePathStyle,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (config.credentials) {
|
|
27
|
+
s3Config.credentials = config.credentials;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (config.endpoint) {
|
|
31
|
+
s3Config.endpoint = config.endpoint;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.client = new S3Client(s3Config);
|
|
35
|
+
this.bucket = config.bucket;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async upload(
|
|
39
|
+
data: string | Buffer,
|
|
40
|
+
filename: string,
|
|
41
|
+
mimeType: string
|
|
42
|
+
): Promise<StorageUploadResult> {
|
|
43
|
+
const body = typeof data === 'string' ? Buffer.from(data, 'base64') : data;
|
|
44
|
+
|
|
45
|
+
await this.client.send(
|
|
46
|
+
new PutObjectCommand({
|
|
47
|
+
Bucket: this.bucket,
|
|
48
|
+
Key: filename,
|
|
49
|
+
Body: body,
|
|
50
|
+
ContentType: mimeType,
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
storageKey: filename,
|
|
56
|
+
url: `https://${this.bucket}.s3.amazonaws.com/${filename}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async download(storageKey: string): Promise<string> {
|
|
61
|
+
const response = await this.client.send(
|
|
62
|
+
new GetObjectCommand({
|
|
63
|
+
Bucket: this.bucket,
|
|
64
|
+
Key: storageKey,
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!response.Body) {
|
|
69
|
+
throw new Error('S3 download failed: empty body');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const bytes = await response.Body.transformToByteArray();
|
|
73
|
+
return Buffer.from(bytes).toString('base64');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async delete(storageKey: string): Promise<void> {
|
|
77
|
+
await this.client.send(
|
|
78
|
+
new DeleteObjectCommand({
|
|
79
|
+
Bucket: this.bucket,
|
|
80
|
+
Key: storageKey,
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/stores/adapters.ts
CHANGED
|
@@ -11,8 +11,10 @@ export interface UserStoreAdapter {
|
|
|
11
11
|
|
|
12
12
|
export interface MessageStoreAdapter {
|
|
13
13
|
create(message: Message): Promise<Message>;
|
|
14
|
+
findById(id: string): Promise<Message | undefined>;
|
|
14
15
|
listByUser(userId: string): Promise<Message[]>;
|
|
15
16
|
listByGroup(groupId: string): Promise<Message[]>;
|
|
17
|
+
delete(id: string): Promise<void>;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface GroupStoreAdapter {
|
|
@@ -9,6 +9,10 @@ export class InMemoryMessageStore implements MessageStoreAdapter {
|
|
|
9
9
|
return message;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
async findById(id: string): Promise<Message | undefined> {
|
|
13
|
+
return this.messages.find((msg) => msg.id === id);
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
async listByUser(userId: string): Promise<Message[]> {
|
|
13
17
|
return this.messages.filter(
|
|
14
18
|
(msg) => msg.senderId === userId || msg.receiverId === userId
|
|
@@ -18,4 +22,8 @@ export class InMemoryMessageStore implements MessageStoreAdapter {
|
|
|
18
22
|
async listByGroup(groupId: string): Promise<Message[]> {
|
|
19
23
|
return this.messages.filter((msg) => msg.groupId === groupId);
|
|
20
24
|
}
|
|
25
|
+
|
|
26
|
+
async delete(id: string): Promise<void> {
|
|
27
|
+
this.messages = this.messages.filter((msg) => msg.id !== id);
|
|
28
|
+
}
|
|
21
29
|
}
|
|
@@ -1,7 +1,57 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Message } from "../models/message.js";
|
|
2
|
+
import { ConnectionState } from "../constants.js";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Transport adapter interface for network communication
|
|
6
|
+
*/
|
|
3
7
|
export interface TransportAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* Connect to the transport
|
|
10
|
+
* @param userId - User ID to connect as
|
|
11
|
+
*/
|
|
4
12
|
connect(userId: string): Promise<void>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Disconnect from the transport
|
|
16
|
+
*/
|
|
17
|
+
disconnect(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reconnect to the transport
|
|
21
|
+
*/
|
|
22
|
+
reconnect(): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send a message
|
|
26
|
+
* @param message - Message to send
|
|
27
|
+
*/
|
|
5
28
|
send(message: Message): Promise<void>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a message handler
|
|
32
|
+
* @param handler - Function to call when a message is received
|
|
33
|
+
*/
|
|
6
34
|
onMessage(handler: (message: Message) => void): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register a connection state change handler
|
|
38
|
+
* @param handler - Function to call when connection state changes
|
|
39
|
+
*/
|
|
40
|
+
onConnectionStateChange?(handler: (state: ConnectionState) => void): void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register an error handler
|
|
44
|
+
* @param handler - Function to call when an error occurs
|
|
45
|
+
*/
|
|
46
|
+
onError?(handler: (error: Error) => void): void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the current connection state
|
|
50
|
+
*/
|
|
51
|
+
getConnectionState(): ConnectionState;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if transport is connected
|
|
55
|
+
*/
|
|
56
|
+
isConnected(): boolean;
|
|
7
57
|
}
|
|
@@ -1,24 +1,86 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
type MessageHandler = (message: Message) => void;
|
|
1
|
+
import { Message } from "../models/message.js";
|
|
2
|
+
import { TransportAdapter } from "./adapters.js";
|
|
3
|
+
import { ConnectionState } from "../constants.js";
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* In-memory transport for testing (no actual network communication)
|
|
7
|
+
*/
|
|
6
8
|
export class InMemoryTransport implements TransportAdapter {
|
|
7
|
-
private
|
|
8
|
-
private
|
|
9
|
+
private messageHandler: ((message: Message) => void) | null = null;
|
|
10
|
+
private connectionState: ConnectionState = ConnectionState.DISCONNECTED;
|
|
11
|
+
private stateHandler: ((state: ConnectionState) => void) | null = null;
|
|
12
|
+
private errorHandler: ((error: Error) => void) | null = null;
|
|
13
|
+
|
|
14
|
+
async connect(userId: string): Promise<void> {
|
|
15
|
+
this.connectionState = ConnectionState.CONNECTED;
|
|
16
|
+
if (this.stateHandler) {
|
|
17
|
+
this.stateHandler(this.connectionState);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async disconnect(): Promise<void> {
|
|
22
|
+
this.connectionState = ConnectionState.DISCONNECTED;
|
|
23
|
+
if (this.stateHandler) {
|
|
24
|
+
this.stateHandler(this.connectionState);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
|
|
10
|
-
async
|
|
11
|
-
this.
|
|
28
|
+
async reconnect(): Promise<void> {
|
|
29
|
+
this.connectionState = ConnectionState.CONNECTING;
|
|
30
|
+
if (this.stateHandler) {
|
|
31
|
+
this.stateHandler(this.connectionState);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Simulate reconnection delay
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
36
|
+
|
|
37
|
+
this.connectionState = ConnectionState.CONNECTED;
|
|
38
|
+
if (this.stateHandler) {
|
|
39
|
+
this.stateHandler(this.connectionState);
|
|
40
|
+
}
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
async send(message: Message): Promise<void> {
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
// In-memory transport just echoes back the message
|
|
45
|
+
if (this.messageHandler) {
|
|
46
|
+
// Simulate async delivery
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
this.messageHandler!(message);
|
|
49
|
+
}, 10);
|
|
17
50
|
}
|
|
18
|
-
this.handler?.(message);
|
|
19
51
|
}
|
|
20
52
|
|
|
21
|
-
onMessage(handler:
|
|
22
|
-
this.
|
|
53
|
+
onMessage(handler: (message: Message) => void): void {
|
|
54
|
+
this.messageHandler = handler;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onConnectionStateChange(handler: (state: ConnectionState) => void): void {
|
|
58
|
+
this.stateHandler = handler;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onError(handler: (error: Error) => void): void {
|
|
62
|
+
this.errorHandler = handler;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getConnectionState(): ConnectionState {
|
|
66
|
+
return this.connectionState;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isConnected(): boolean {
|
|
70
|
+
return this.connectionState === ConnectionState.CONNECTED;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Test helper to simulate receiving a message
|
|
74
|
+
simulateReceive(message: Message): void {
|
|
75
|
+
if (this.messageHandler) {
|
|
76
|
+
this.messageHandler(message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Test helper to simulate an error
|
|
81
|
+
simulateError(error: Error): void {
|
|
82
|
+
if (this.errorHandler) {
|
|
83
|
+
this.errorHandler(error);
|
|
84
|
+
}
|
|
23
85
|
}
|
|
24
86
|
}
|