chatly-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/src/index.ts ADDED
@@ -0,0 +1,212 @@
1
+ import type { User, StoredUser } from "./models/user.js";
2
+ import type { Message } from "./models/message.js";
3
+ import type { Group } from "./models/group.js";
4
+ import type {
5
+ UserStoreAdapter,
6
+ MessageStoreAdapter,
7
+ GroupStoreAdapter,
8
+ } from "./stores/adapters.js";
9
+ import type { TransportAdapter } from "./transport/adapters.js";
10
+ import { ChatSession } from "./chat/ChatSession.js";
11
+ import { GroupSession } from "./chat/GroupSession.js";
12
+ import { generateIdentityKeyPair } from "./crypto/keys.js";
13
+ import { generateUUID } from "./crypto/uuid.js";
14
+
15
+ export interface ChatSDKConfig {
16
+ userStore: UserStoreAdapter;
17
+ messageStore: MessageStoreAdapter;
18
+ groupStore: GroupStoreAdapter;
19
+ transport?: TransportAdapter;
20
+ }
21
+
22
+ /**
23
+ * Main ChatSDK class - production-ready WhatsApp-style chat SDK
24
+ */
25
+ export class ChatSDK {
26
+ private config: ChatSDKConfig;
27
+ private currentUser: User | null = null;
28
+
29
+ constructor(config: ChatSDKConfig) {
30
+ this.config = config;
31
+ }
32
+
33
+ /**
34
+ * Create a new user with generated identity keys
35
+ */
36
+ async createUser(username: string): Promise<User> {
37
+ const keyPair = generateIdentityKeyPair();
38
+ const user: User = {
39
+ id: generateUUID(),
40
+ username,
41
+ identityKey: keyPair.publicKey,
42
+ publicKey: keyPair.publicKey,
43
+ privateKey: keyPair.privateKey,
44
+ };
45
+
46
+ await this.config.userStore.create(user);
47
+ return user;
48
+ }
49
+
50
+ /**
51
+ * Import an existing user from stored data
52
+ */
53
+ async importUser(userData: StoredUser): Promise<User> {
54
+ await this.config.userStore.save(userData);
55
+ return userData;
56
+ }
57
+
58
+ /**
59
+ * Set the current active user
60
+ */
61
+ setCurrentUser(user: User): void {
62
+ this.currentUser = user;
63
+ if (this.config.transport) {
64
+ this.config.transport.connect(user.id);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get the current active user
70
+ */
71
+ getCurrentUser(): User | null {
72
+ return this.currentUser;
73
+ }
74
+
75
+ /**
76
+ * Start a 1:1 chat session between two users
77
+ */
78
+ async startSession(userA: User, userB: User): Promise<ChatSession> {
79
+ // Create consistent session ID regardless of user order
80
+ const ids = [userA.id, userB.id].sort();
81
+ const sessionId = `${ids[0]}-${ids[1]}`;
82
+ const session = new ChatSession(sessionId, userA, userB);
83
+ await session.initialize();
84
+ return session;
85
+ }
86
+
87
+ /**
88
+ * Create a new group with members
89
+ */
90
+ async createGroup(name: string, members: User[]): Promise<GroupSession> {
91
+ if (members.length < 2) {
92
+ throw new Error("Group must have at least 2 members");
93
+ }
94
+
95
+ const group: Group = {
96
+ id: generateUUID(),
97
+ name,
98
+ members,
99
+ createdAt: Date.now(),
100
+ };
101
+
102
+ await this.config.groupStore.create(group);
103
+ const session = new GroupSession(group);
104
+ await session.initialize();
105
+ return session;
106
+ }
107
+
108
+ /**
109
+ * Load an existing group by ID
110
+ */
111
+ async loadGroup(id: string): Promise<GroupSession> {
112
+ const group = await this.config.groupStore.findById(id);
113
+ if (!group) {
114
+ throw new Error(`Group not found: ${id}`);
115
+ }
116
+
117
+ const session = new GroupSession(group);
118
+ await session.initialize();
119
+ return session;
120
+ }
121
+
122
+ /**
123
+ * Send a message in a chat session (1:1 or group)
124
+ */
125
+ async sendMessage(
126
+ session: ChatSession | GroupSession,
127
+ plaintext: string
128
+ ): Promise<Message> {
129
+ if (!this.currentUser) {
130
+ throw new Error("No current user set. Call setCurrentUser() first.");
131
+ }
132
+
133
+ let message: Message;
134
+ if (session instanceof ChatSession) {
135
+ message = await session.encrypt(plaintext, this.currentUser.id);
136
+ } else {
137
+ message = await session.encrypt(plaintext, this.currentUser.id);
138
+ }
139
+
140
+ // Store the message
141
+ await this.config.messageStore.create(message);
142
+
143
+ // Send via transport if available
144
+ if (this.config.transport) {
145
+ await this.config.transport.send(message);
146
+ }
147
+
148
+ return message;
149
+ }
150
+
151
+ /**
152
+ * Decrypt a message
153
+ */
154
+ async decryptMessage(message: Message, user: User): Promise<string> {
155
+ if (message.groupId) {
156
+ // Group message
157
+ const group = await this.config.groupStore.findById(message.groupId);
158
+ if (!group) {
159
+ throw new Error(`Group not found: ${message.groupId}`);
160
+ }
161
+ const session = new GroupSession(group);
162
+ await session.initialize();
163
+ return await session.decrypt(message);
164
+ } else {
165
+ // 1:1 message - need to find the session
166
+ const otherUserId =
167
+ message.senderId === user.id ? message.receiverId : message.senderId;
168
+ if (!otherUserId) {
169
+ throw new Error("Invalid message: missing receiver/sender");
170
+ }
171
+
172
+ const otherUser = await this.config.userStore.findById(otherUserId);
173
+ if (!otherUser) {
174
+ throw new Error(`User not found: ${otherUserId}`);
175
+ }
176
+
177
+ // Create consistent session ID
178
+ const ids = [user.id, otherUser.id].sort();
179
+ const sessionId = `${ids[0]}-${ids[1]}`;
180
+ const session = new ChatSession(sessionId, user, otherUser);
181
+ await session.initializeForUser(user);
182
+ return await session.decrypt(message, user);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Get messages for a user
188
+ */
189
+ async getMessagesForUser(userId: string): Promise<Message[]> {
190
+ return await this.config.messageStore.listByUser(userId);
191
+ }
192
+
193
+ /**
194
+ * Get messages for a group
195
+ */
196
+ async getMessagesForGroup(groupId: string): Promise<Message[]> {
197
+ return await this.config.messageStore.listByGroup(groupId);
198
+ }
199
+ }
200
+
201
+ // Export adapters and implementations
202
+ export * from "./stores/adapters.js";
203
+ export * from "./stores/memory/userStore.js";
204
+ export * from "./stores/memory/messageStore.js";
205
+ export * from "./stores/memory/groupStore.js";
206
+ export * from "./transport/adapters.js";
207
+ export * from "./transport/memoryTransport.js";
208
+ export * from "./models/user.js";
209
+ export * from "./models/message.js";
210
+ export * from "./models/group.js";
211
+ export * from "./chat/ChatSession.js";
212
+ export * from "./chat/GroupSession.js";
@@ -0,0 +1,6 @@
1
+
2
+ import { Message } from './Message';
3
+
4
+ export interface ImageMessage extends Message {
5
+ imageUrl: string;
6
+ }
@@ -0,0 +1,6 @@
1
+
2
+ export interface ReadReceipt {
3
+ messageId: string;
4
+ readerId: string;
5
+ timestamp: number;
6
+ }
@@ -0,0 +1,8 @@
1
+ import type { User } from "./user.js";
2
+
3
+ export interface Group {
4
+ id: string;
5
+ name: string;
6
+ members: User[];
7
+ createdAt: number;
8
+ }
@@ -0,0 +1,13 @@
1
+ export type MessageType = "text" | "system";
2
+
3
+ export interface Message {
4
+ id: string;
5
+ senderId: string;
6
+ receiverId?: string;
7
+ groupId?: string;
8
+ ciphertext: string;
9
+ iv: string;
10
+ timestamp: number;
11
+ type: MessageType;
12
+ }
13
+
@@ -0,0 +1,11 @@
1
+ export interface User {
2
+ id: string;
3
+ username: string;
4
+ identityKey: string;
5
+ publicKey: string;
6
+ privateKey: string;
7
+ }
8
+
9
+ export interface StoredUser extends User {
10
+ createdAt: number;
11
+ }
@@ -0,0 +1,22 @@
1
+ import type { User, StoredUser } from "../models/user.js";
2
+ import type { Message } from "../models/message.js";
3
+ import type { Group } from "../models/group.js";
4
+
5
+ export interface UserStoreAdapter {
6
+ create(user: User): Promise<User>;
7
+ findById(id: string): Promise<User | undefined>;
8
+ save(user: StoredUser): Promise<void>;
9
+ list(): Promise<User[]>;
10
+ }
11
+
12
+ export interface MessageStoreAdapter {
13
+ create(message: Message): Promise<Message>;
14
+ listByUser(userId: string): Promise<Message[]>;
15
+ listByGroup(groupId: string): Promise<Message[]>;
16
+ }
17
+
18
+ export interface GroupStoreAdapter {
19
+ create(group: Group): Promise<Group>;
20
+ findById(id: string): Promise<Group | undefined>;
21
+ list(): Promise<Group[]>;
22
+ }
@@ -0,0 +1,19 @@
1
+ import type { Group } from "../../models/group.js";
2
+ import type { GroupStoreAdapter } from "../adapters.js";
3
+
4
+ export class InMemoryGroupStore implements GroupStoreAdapter {
5
+ private groups = new Map<string, Group>();
6
+
7
+ async create(group: Group): Promise<Group> {
8
+ this.groups.set(group.id, group);
9
+ return group;
10
+ }
11
+
12
+ async findById(id: string): Promise<Group | undefined> {
13
+ return this.groups.get(id);
14
+ }
15
+
16
+ async list(): Promise<Group[]> {
17
+ return Array.from(this.groups.values());
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import type { Message } from "../../models/message.js";
2
+ import type { MessageStoreAdapter } from "../adapters.js";
3
+
4
+ export class InMemoryMessageStore implements MessageStoreAdapter {
5
+ private messages: Message[] = [];
6
+
7
+ async create(message: Message): Promise<Message> {
8
+ this.messages.push(message);
9
+ return message;
10
+ }
11
+
12
+ async listByUser(userId: string): Promise<Message[]> {
13
+ return this.messages.filter(
14
+ (msg) => msg.senderId === userId || msg.receiverId === userId
15
+ );
16
+ }
17
+
18
+ async listByGroup(groupId: string): Promise<Message[]> {
19
+ return this.messages.filter((msg) => msg.groupId === groupId);
20
+ }
21
+ }
@@ -0,0 +1,24 @@
1
+ import type { User, StoredUser } from "../../models/user.js";
2
+ import type { UserStoreAdapter } from "../adapters.js";
3
+
4
+ export class InMemoryUserStore implements UserStoreAdapter {
5
+ private users = new Map<string, StoredUser>();
6
+
7
+ async create(user: User): Promise<User> {
8
+ const stored: StoredUser = { ...user, createdAt: Date.now() };
9
+ this.users.set(stored.id, stored);
10
+ return stored;
11
+ }
12
+
13
+ async findById(id: string): Promise<User | undefined> {
14
+ return this.users.get(id);
15
+ }
16
+
17
+ async save(user: StoredUser): Promise<void> {
18
+ this.users.set(user.id, user);
19
+ }
20
+
21
+ async list(): Promise<User[]> {
22
+ return Array.from(this.users.values());
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import type { Message } from "../models/message.js";
2
+
3
+ export interface TransportAdapter {
4
+ connect(userId: string): Promise<void>;
5
+ send(message: Message): Promise<void>;
6
+ onMessage(handler: (message: Message) => void): void;
7
+ }
@@ -0,0 +1,24 @@
1
+ import type { Message } from "../models/message.js";
2
+ import type { TransportAdapter } from "./adapters.js";
3
+
4
+ type MessageHandler = (message: Message) => void;
5
+
6
+ export class InMemoryTransport implements TransportAdapter {
7
+ private handler?: MessageHandler;
8
+ private connected = false;
9
+
10
+ async connect(_userId: string): Promise<void> {
11
+ this.connected = true;
12
+ }
13
+
14
+ async send(message: Message): Promise<void> {
15
+ if (!this.connected) {
16
+ throw new Error("Transport not connected");
17
+ }
18
+ this.handler?.(message);
19
+ }
20
+
21
+ onMessage(handler: MessageHandler): void {
22
+ this.handler = handler;
23
+ }
24
+ }
@@ -0,0 +1,37 @@
1
+
2
+ import WebSocket from 'ws';
3
+
4
+ export class ChatClient {
5
+ private ws: WebSocket | null = null;
6
+ private messageHandlers: ((message: Buffer) => void)[] = [];
7
+
8
+ constructor(private url: string) {}
9
+
10
+ connect(): void {
11
+ this.ws = new WebSocket(this.url);
12
+
13
+ this.ws.on('open', () => {
14
+ console.log('Connected to WebSocket server');
15
+ });
16
+
17
+ this.ws.on('message', (message: Buffer) => {
18
+ this.messageHandlers.forEach(handler => handler(message));
19
+ });
20
+
21
+ this.ws.on('close', () => {
22
+ console.log('Disconnected from WebSocket server');
23
+ });
24
+ }
25
+
26
+ sendMessage(message: string | Buffer): void {
27
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
28
+ this.ws.send(message);
29
+ } else {
30
+ console.error('WebSocket is not connected.');
31
+ }
32
+ }
33
+
34
+ onMessage(handler: (message: Buffer) => void): void {
35
+ this.messageHandlers.push(handler);
36
+ }
37
+ }
@@ -0,0 +1,33 @@
1
+
2
+ import { WebSocketServer, WebSocket } from 'ws';
3
+
4
+ export class ChatServer {
5
+ private wss: WebSocketServer;
6
+
7
+ constructor(port: number) {
8
+ this.wss = new WebSocketServer({ port });
9
+ this.initialize();
10
+ }
11
+
12
+ private initialize(): void {
13
+ this.wss.on('connection', (ws: WebSocket) => {
14
+ console.log('Client connected');
15
+
16
+ ws.on('message', (message: Buffer) => {
17
+ console.log('Received message:', message.toString());
18
+ // Broadcast the message to all other clients
19
+ this.wss.clients.forEach((client) => {
20
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
21
+ client.send(message);
22
+ }
23
+ });
24
+ });
25
+
26
+ ws.on('close', () => {
27
+ console.log('Client disconnected');
28
+ });
29
+ });
30
+
31
+ console.log(`WebSocket server started on port ${this.wss.options.port}`);
32
+ }
33
+ }
@@ -0,0 +1,45 @@
1
+
2
+ import assert from 'assert';
3
+ import { KeyManager } from '../src/crypto/keyManager';
4
+ import { encrypt } from '../src/crypto/encrypt';
5
+ import { decrypt } from '../src/crypto/decrypt';
6
+
7
+ async function testCrypto() {
8
+ console.log('Running crypto tests...');
9
+
10
+ // Test KeyManager
11
+ const keyManager1 = new KeyManager();
12
+ const keyManager2 = new KeyManager();
13
+
14
+ await keyManager1.generateKeys();
15
+ await keyManager2.generateKeys();
16
+
17
+ const publicKey1 = keyManager1.getPublicKey();
18
+ const privateKey1 = keyManager1.getPrivateKey();
19
+ const publicKey2 = keyManager2.getPublicKey();
20
+ const privateKey2 = keyManager2.getPrivateKey();
21
+
22
+ assert(publicKey1, 'publicKey1 should not be null');
23
+ assert(privateKey1, 'privateKey1 should not be null');
24
+ assert(publicKey2, 'publicKey2 should not be null');
25
+ assert(privateKey2, 'privateKey2 should not be null');
26
+
27
+ console.log('KeyManager test passed.');
28
+
29
+ // Test encrypt and decrypt
30
+ const message = 'This is a secret message.';
31
+ if (publicKey1 && privateKey1 && publicKey2 && privateKey2) {
32
+ const encryptedMessage = await encrypt(message, publicKey2, privateKey1);
33
+ const decryptedMessage = await decrypt(encryptedMessage, publicKey1, privateKey2);
34
+
35
+ assert.strictEqual(decryptedMessage, message, 'Decrypted message should match original message.');
36
+ } else {
37
+ assert.fail('Keys should not be null');
38
+ }
39
+
40
+ console.log('Encrypt/decrypt test passed.');
41
+
42
+ console.log('All crypto tests passed.');
43
+ }
44
+
45
+ testCrypto().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ // "rootDir": "./src",
6
+ // "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "nodenext",
11
+ "target": "esnext",
12
+
13
+ // For nodejs:
14
+ // "lib": ["esnext"],
15
+ "types": ["node"],
16
+ // and npm install -D @types/node
17
+
18
+ // Other Outputs
19
+ "sourceMap": true,
20
+ "declaration": true,
21
+ "declarationMap": true,
22
+
23
+ // Stricter Typechecking Options
24
+ "noUncheckedIndexedAccess": true,
25
+ "exactOptionalPropertyTypes": true,
26
+
27
+ // Style Options
28
+ // "noImplicitReturns": true,
29
+ // "noImplicitOverride": true,
30
+ // "noUnusedLocals": true,
31
+ // "noUnusedParameters": true,
32
+ // "noFallthroughCasesInSwitch": true,
33
+ // "noPropertyAccessFromIndexSignature": true,
34
+
35
+ // Recommended Options
36
+ "strict": true,
37
+ "jsx": "react-jsx",
38
+ "verbatimModuleSyntax": false,
39
+ "isolatedModules": true,
40
+ "noUncheckedSideEffectImports": true,
41
+ "moduleDetection": "force",
42
+ "skipLibCheck": true,
43
+ }
44
+ }