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.
Files changed (63) hide show
  1. package/CONTRIBUTING.md +658 -0
  2. package/IMPROVEMENTS.md +402 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1576 -162
  5. package/dist/index.d.ts +502 -11
  6. package/dist/index.js +1619 -66
  7. package/examples/01-basic-chat/README.md +61 -0
  8. package/examples/01-basic-chat/index.js +58 -0
  9. package/examples/01-basic-chat/package.json +13 -0
  10. package/examples/02-group-chat/README.md +78 -0
  11. package/examples/02-group-chat/index.js +76 -0
  12. package/examples/02-group-chat/package.json +13 -0
  13. package/examples/03-offline-messaging/README.md +73 -0
  14. package/examples/03-offline-messaging/index.js +80 -0
  15. package/examples/03-offline-messaging/package.json +13 -0
  16. package/examples/04-live-chat/README.md +80 -0
  17. package/examples/04-live-chat/index.js +114 -0
  18. package/examples/04-live-chat/package.json +13 -0
  19. package/examples/05-hybrid-messaging/README.md +71 -0
  20. package/examples/05-hybrid-messaging/index.js +106 -0
  21. package/examples/05-hybrid-messaging/package.json +13 -0
  22. package/examples/06-postgresql-integration/README.md +101 -0
  23. package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
  24. package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
  25. package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
  26. package/examples/06-postgresql-integration/index.js +92 -0
  27. package/examples/06-postgresql-integration/package.json +14 -0
  28. package/examples/06-postgresql-integration/schema.sql +58 -0
  29. package/examples/08-customer-support/README.md +70 -0
  30. package/examples/08-customer-support/index.js +104 -0
  31. package/examples/08-customer-support/package.json +13 -0
  32. package/examples/README.md +105 -0
  33. package/jest.config.cjs +28 -0
  34. package/package.json +15 -6
  35. package/src/chat/ChatSession.ts +160 -3
  36. package/src/chat/GroupSession.ts +108 -1
  37. package/src/constants.ts +61 -0
  38. package/src/crypto/e2e.ts +9 -20
  39. package/src/crypto/utils.ts +3 -1
  40. package/src/index.ts +530 -63
  41. package/src/models/mediaTypes.ts +62 -0
  42. package/src/models/message.ts +4 -1
  43. package/src/storage/adapters.ts +36 -0
  44. package/src/storage/localStorage.ts +49 -0
  45. package/src/storage/s3Storage.ts +84 -0
  46. package/src/stores/adapters.ts +2 -0
  47. package/src/stores/memory/messageStore.ts +8 -0
  48. package/src/transport/adapters.ts +51 -1
  49. package/src/transport/memoryTransport.ts +75 -13
  50. package/src/transport/websocketClient.ts +269 -21
  51. package/src/transport/websocketServer.ts +26 -26
  52. package/src/utils/errors.ts +97 -0
  53. package/src/utils/logger.ts +96 -0
  54. package/src/utils/mediaUtils.ts +235 -0
  55. package/src/utils/messageQueue.ts +162 -0
  56. package/src/utils/validation.ts +99 -0
  57. package/test/crypto.test.ts +122 -35
  58. package/test/sdk.test.ts +276 -0
  59. package/test/validation.test.ts +64 -0
  60. package/tsconfig.json +11 -10
  61. package/tsconfig.test.json +11 -0
  62. package/src/ChatManager.ts +0 -103
  63. 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
+ };
@@ -1,4 +1,6 @@
1
- export type MessageType = "text" | "system";
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
+ }
@@ -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 type { Message } from "../models/message.js";
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 type { Message } from "../models/message.js";
2
- import type { TransportAdapter } from "./adapters.js";
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 handler?: MessageHandler;
8
- private connected = false;
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 connect(_userId: string): Promise<void> {
11
- this.connected = true;
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
- if (!this.connected) {
16
- throw new Error("Transport not connected");
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: MessageHandler): void {
22
- this.handler = handler;
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
  }