@vibexnpm/talkx 2.3.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/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@vibexnpm/talkx",
3
+ "version": "2.3.1",
4
+ "type": "module",
5
+ "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
+ "main": "dist/talkflow-sdk.umd.js",
7
+ "module": "dist/talkflow-sdk.esm.js",
8
+ "browser": "dist/talkflow-sdk.standalone.js",
9
+ "types": "types/index.d.ts",
10
+ "files": [
11
+ "dist",
12
+ "src",
13
+ "types"
14
+ ],
15
+ "scripts": {
16
+ "build": "rollup -c && npm run build:types",
17
+ "build:types": "copy types\\index.d.ts dist\\index.d.ts",
18
+ "dev": "rollup -c -w",
19
+ "lint": "eslint src/",
20
+ "test": "jest"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/talkflow/talkflow-sdk.git"
25
+ },
26
+ "keywords": [
27
+ "talkflow",
28
+ "chat",
29
+ "webrtc",
30
+ "websocket",
31
+ "stomp",
32
+ "realtime"
33
+ ],
34
+ "author": "vibexnpm",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@stomp/stompjs": "^7.0.0",
38
+ "firebase": "^11.8.1",
39
+ "sockjs-client": "^1.6.0"
40
+ },
41
+ "devDependencies": {
42
+ "@babel/core": "^7.24.0",
43
+ "@babel/preset-env": "^7.24.0",
44
+ "@rollup/plugin-babel": "^6.0.4",
45
+ "@rollup/plugin-commonjs": "^29.0.0",
46
+ "@rollup/plugin-node-resolve": "^16.0.3",
47
+ "@rollup/plugin-replace": "^6.0.3",
48
+ "@rollup/plugin-terser": "^0.4.4",
49
+ "rollup": "^4.12.0"
50
+ }
51
+ }
@@ -0,0 +1,481 @@
1
+ /**
2
+ * TalkFlowClient
3
+ * 채팅 + WebRTC 통합 클라이언트
4
+ */
5
+
6
+ import EventEmitter from './utils/EventEmitter.js';
7
+ import Logger from './utils/Logger.js';
8
+ import ApiClient from './utils/ApiClient.js';
9
+ import { extractUserIdFromJWT } from './utils/jwtUtils.js';
10
+ import { ConnectionState, ErrorTypes, DefaultConfig, LogLevel, Environment, getServerUrl } from './constants.js';
11
+ import { initializeSubClients, applySessionMethods } from './talkflow/session.js';
12
+ import { setupEventForwarding } from './talkflow/eventForwarding.js';
13
+ import { applyDelegatedMethods } from './talkflow/delegates.js';
14
+
15
+ // noinspection JSUnresolvedReference
16
+ class TalkFlowClient extends EventEmitter {
17
+ /**
18
+ * TalkFlowClient 생성
19
+ *
20
+ * <p>JWT 토큰은 고객사 backend 에서 발급받아서 SDK 에 전달해야 합니다.
21
+ * 브라우저에서 직접 JWT 를 발급하는 기능은 제공하지 않습니다 (보안상).
22
+ * 자세한 내용은 README "프로덕션 인증 플로우" 섹션 참고.</p>
23
+ *
24
+ * @param {Object} options - 설정 옵션
25
+ * @param {string} options.apiKey - Client API 키 (필수, 브라우저 노출 안전)
26
+ * @param {string} options.projectId - 프로젝트 ID (필수)
27
+ * @param {string} options.jwtToken - JWT 토큰 (필수, 고객사 backend 에서 발급)
28
+ * @param {string} [options.env='production'] - 환경 ('development' | 'staging' | 'production')
29
+ * @param {string} [options.serverUrl] - 서버 URL (직접 지정 시 env 무시)
30
+ * @param {boolean} [options.useSockJS=true] - SockJS 사용 여부
31
+ * @param {number} [options.reconnectDelay=5000] - 재연결 지연 (ms)
32
+ * @param {number} [options.maxReconnectAttempts=10] - 최대 재연결 시도
33
+ * @param {Object[]} [options.iceServers] - ICE 서버 설정
34
+ * @param {boolean} [options.autoSubscribeRoomList=true] - connect() 시 채팅방 리스트 자동 구독 여부 (카톡 스타일)
35
+ * @param {number} [options.logLevel=LogLevel.WARN] - 로그 레벨
36
+ *
37
+ * @example
38
+ * // 1단계: 고객사 backend 가 talkflow JWT 를 발급받음 (Server API Key 사용)
39
+ * // POST https://chat.apiorbit.net/api/v1/users/auth
40
+ * // Headers: X-API-KEY: <SERVER_KEY>, X-PROJECT-ID: <project-id>
41
+ * // Body: { userId: 'user-123', nickname: '홍길동' }
42
+ * // Response: { accessToken, refreshToken }
43
+ *
44
+ * // 2단계: 고객사 backend 가 최종 사용자 브라우저에 JWT 전달 (자사 API 응답 등)
45
+ *
46
+ * // 3단계: 브라우저에서 SDK 초기화
47
+ * const client = new TalkFlowClient({
48
+ * apiKey: 'CLIENT_KEY', // 브라우저에 노출되는 Client Key
49
+ * projectId: 'your-project-id',
50
+ * jwtToken: receivedJwt, // 고객사 backend 로부터 받은 JWT
51
+ * env: 'production'
52
+ * });
53
+ *
54
+ * await client.connect();
55
+ */
56
+ constructor(options) {
57
+ super();
58
+
59
+ // 필수 옵션 검증
60
+ this._validateOptions(options);
61
+
62
+ // 환경에 따른 서버 URL 결정
63
+ // serverUrl이 직접 지정되면 우선 사용, 아니면 env에 따라 자동 결정
64
+ const env = options.env || DefaultConfig.environment;
65
+ const serverUrl = (options.serverUrl || getServerUrl(env)).replace(/\/$/, '');
66
+
67
+ this.options = {
68
+ serverUrl,
69
+ env,
70
+ apiKey: options.apiKey,
71
+ projectId: options.projectId,
72
+ jwtToken: options.jwtToken || null,
73
+ useSockJS: options.useSockJS !== false,
74
+ reconnectDelay: options.reconnectDelay || DefaultConfig.reconnectDelay,
75
+ maxReconnectAttempts: options.maxReconnectAttempts || DefaultConfig.maxReconnectAttempts,
76
+ iceServers: options.iceServers || DefaultConfig.iceServers,
77
+ // connect() 시 채팅방 리스트 자동 구독 여부 (카톡 스타일).
78
+ // 기본 true — 앱 어디서든 리스트 이벤트를 놓치지 않음 (배지/알림 등).
79
+ // false 로 설정 시 수동으로 client.chat.subscribeRoomList() 호출 필요.
80
+ autoSubscribeRoomList: options.autoSubscribeRoomList !== false,
81
+ logLevel: options.logLevel !== undefined ? options.logLevel : LogLevel.WARN
82
+ };
83
+
84
+ // 로거 초기화
85
+ this.logger = new Logger(this.options.logLevel, 'TalkFlowClient');
86
+
87
+ // 상태
88
+ this._initialized = false;
89
+ this._state = ConnectionState.DISCONNECTED;
90
+
91
+ // public 읽기 전용 surface 의 backing field.
92
+ // types/index.d.ts 는 chat/webrtc/pushManager/userId 를 readonly 로 선언 —
93
+ // 외부 계약(readonly) 과 내부 재할당 필요성을 동시에 만족시키기 위해 private
94
+ // backing field 에 저장하고 동명 getter 로 공개한다.
95
+ this._userId = null;
96
+
97
+ // JWT가 있으면 userId 추출
98
+ if (this.options.jwtToken) {
99
+ this._userId = extractUserIdFromJWT(this.options.jwtToken);
100
+ }
101
+
102
+ // API 클라이언트 초기화 (JWT 없이도 동작)
103
+ this.apiClient = new ApiClient({
104
+ baseUrl: this.options.serverUrl,
105
+ apiKey: this.options.apiKey,
106
+ projectId: this.options.projectId,
107
+ jwtToken: this.options.jwtToken,
108
+ logLevel: this.options.logLevel
109
+ });
110
+
111
+ // ConnectionManager, ChatClient, WebRTCClient는 지연 초기화
112
+ this.connectionManager = null;
113
+ this._chat = null;
114
+ this._webrtc = null;
115
+ this._pushManager = null;
116
+ this._pushEnablePromise = null;
117
+
118
+ // JWT가 있으면 즉시 서브 클라이언트 초기화
119
+ if (this.options.jwtToken && this._userId) {
120
+ this._initializeSubClients();
121
+ }
122
+
123
+ this._initialized = true;
124
+ this.logger.info('TalkFlowClient initialized', {
125
+ userId: this._userId,
126
+ hasToken: !!this.options.jwtToken
127
+ });
128
+ }
129
+
130
+ // ==================== Public readonly surface (getter) ====================
131
+
132
+ /** @returns {ChatClient|null} */
133
+ get chat() { return this._chat; }
134
+
135
+ /** @returns {WebRTCClient|null} */
136
+ get webrtc() { return this._webrtc; }
137
+
138
+ /** @returns {PushManager|null} */
139
+ get pushManager() { return this._pushManager; }
140
+
141
+ /** @returns {string|null} */
142
+ get userId() { return this._userId; }
143
+
144
+ /**
145
+ * 옵션 검증
146
+ * @private
147
+ * @param {Object} options - 설정 옵션
148
+ * @throws {Error} 필수 옵션이 없는 경우
149
+ */
150
+ _validateOptions(options) {
151
+ if (!options) {
152
+ throw new Error('Options are required');
153
+ }
154
+ if (!options.apiKey) {
155
+ throw new Error('apiKey is required');
156
+ }
157
+ if (!options.projectId) {
158
+ throw new Error('projectId is required');
159
+ }
160
+
161
+ // Server Key (sk-) 브라우저 사용 경고
162
+ // - development: 경고만 (registerUser 빠른 테스트 허용)
163
+ // - staging/production: 경고 + 강력 안내
164
+ if (options.apiKey.startsWith('sk-')) {
165
+ const env = options.env || 'production';
166
+ if (env === 'development') {
167
+ console.warn(
168
+ '⚠️ [TalkFlow] Server Key (sk-) 를 개발 모드에서 사용 중입니다. ' +
169
+ '프로덕션 배포 전에 반드시 Client Key (ck-) 로 교체하세요.'
170
+ );
171
+ } else {
172
+ console.error(
173
+ '🚨 [TalkFlow] Server Key (sk-) 가 비개발 환경 (' + env + ') 에서 감지되었습니다. ' +
174
+ '보안 위험: Server Key 를 브라우저에 포함하면 공격자가 임의 사용자로 JWT 를 발급받을 수 있습니다. ' +
175
+ '반드시 Client Key (ck-) 로 교체하세요. ' +
176
+ '서버에서도 프로덕션 모드에서 Server Key 의 브라우저 호출을 차단합니다.'
177
+ );
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 서브 클라이언트 초기화
184
+ * @private
185
+ */
186
+ _initializeSubClients() {
187
+ initializeSubClients(this);
188
+ }
189
+
190
+ /**
191
+ * 이벤트 포워딩 설정
192
+ * @private
193
+ */
194
+ _setupEventForwarding() {
195
+ setupEventForwarding(this);
196
+ }
197
+
198
+ /**
199
+ * 연결 상태 확인
200
+ * @returns {boolean}
201
+ */
202
+ isConnected() {
203
+ return this.connectionManager ? this.connectionManager.isConnected() : false;
204
+ }
205
+
206
+ /**
207
+ * 현재 연결 상태
208
+ * @returns {string}
209
+ */
210
+ getState() {
211
+ return this._state;
212
+ }
213
+
214
+ /**
215
+ * 토큰 보유 여부
216
+ * @returns {boolean}
217
+ */
218
+ hasToken() {
219
+ return !!this.options.jwtToken;
220
+ }
221
+
222
+ // ==================== 사용자 API ====================
223
+
224
+ /**
225
+ * 사용자 인증 (등록/로그인).
226
+ * 인증 성공 시 반환된 JWT 토큰을 자동으로 설정합니다.
227
+ *
228
+ * <h3>⚠️ Server API Key 전용</h3>
229
+ * <p>이 메서드는 JWT 를 발급하는 엔드포인트 ({@code POST /api/v1/users/auth}) 를 호출합니다.
230
+ * <strong>프로덕션 환경에서는 Client API Key 로 이 메서드를 호출하면 서버가 403 을 반환합니다.</strong></p>
231
+ *
232
+ * <ul>
233
+ * <li><strong>개발/프로토타입</strong>: Server Key 로 SDK 를 초기화한 뒤 이 메서드를 사용하면 빠르게 테스트 가능</li>
234
+ * <li><strong>프로덕션</strong>: 고객사 backend 에서 Server Key 로 직접 {@code POST /api/v1/users/auth} 를 호출 →
235
+ * JWT 를 받아 SDK 에 {@code setToken(jwt)} 으로 전달. 이 메서드를 브라우저에서 쓰지 마세요.</li>
236
+ * </ul>
237
+ *
238
+ * <p>자세한 내용은 README 의 "프로덕션 인증 플로우" 섹션 참고.</p>
239
+ *
240
+ * @param {Object} userData
241
+ * @param {string} userData.userId - 사용자 ID (필수, 고객사 회원 ID)
242
+ * @param {string} userData.nickname - 닉네임 (필수, max 100자)
243
+ * @param {string} [userData.profileImageUrl] - 프로필 이미지 URL (max 500자)
244
+ * @param {Object} [userData.metadata] - 추가 메타데이터
245
+ * @returns {Promise<Object>} 인증 결과 (토큰 포함)
246
+ *
247
+ * @example
248
+ * // 개발 환경 — Server Key 로 빠른 테스트
249
+ * const client = new TalkFlowClient({
250
+ * apiKey: 'SERVER_KEY',
251
+ * projectId: 'your-project-id',
252
+ * env: 'development'
253
+ * });
254
+ * await client.registerUser({ userId: 'user-123', nickname: '홍길동' });
255
+ * await client.connect();
256
+ */
257
+ async registerUser(userData) {
258
+ // 다국어: navigator.language 자동 시드 — 호출자가 preferredLanguage 를 명시하지 않은 경우에만.
259
+ // 서버 /users/auth 는 seed-if-absent (기존 값 비클로버) 라 매 인증마다 덮어쓰지 않아 안전.
260
+ const payload = { ...userData };
261
+ if (payload.preferredLanguage === undefined) {
262
+ const detected = TalkFlowClient.detectBrowserLanguage();
263
+ if (detected) {
264
+ payload.preferredLanguage = detected;
265
+ }
266
+ }
267
+
268
+ const response = await this.apiClient.post('/api/v1/users/auth', payload);
269
+
270
+ if (response.data && response.data.accessToken) {
271
+ await this.setToken(response.data.accessToken);
272
+ this.logger.info('User authenticated, token auto-set');
273
+ }
274
+
275
+ return response;
276
+ }
277
+
278
+ /**
279
+ * 사용자 정보 수정
280
+ * @param {Object} userData
281
+ * @param {string} [userData.nickname] - 닉네임 (max 100자)
282
+ * @param {string} [userData.profileImageUrl] - 프로필 이미지 URL (max 500자)
283
+ * @param {Object} [userData.metadata] - 메타데이터 (전체 교체)
284
+ * @param {string|null} [userData.preferredLanguage] - 기본 선호 언어 (BCP-47 / 'OFF'(원문) / null). 다국어.
285
+ * @returns {Promise<Object>}
286
+ */
287
+ async updateMyInfo(userData) {
288
+ this._checkToken();
289
+ return this.apiClient.put('/api/v1/users/update', userData);
290
+ }
291
+
292
+ /**
293
+ * 내 기본 선호 언어 설정 (전 방 공통, 다국어). 명시 변경 — 방별 오버라이드는 {@link #setMyRoomLanguage}.
294
+ * @param {string|null} language - BCP-47 코드(예 'ko','en') / 'OFF'(원문) / null·''(미설정)
295
+ * @returns {Promise<Object>}
296
+ */
297
+ async setPreferredLanguage(language) {
298
+ return this.updateMyInfo({ preferredLanguage: language });
299
+ }
300
+
301
+ /**
302
+ * 브라우저 locale 감지 — {@code navigator.language} 를 BCP-47 소문자로 정규화. 비브라우저/미가용 시 null.
303
+ * <p>{@link #registerUser} 자동 시드에 사용되며, 고객사 BFF 의 {@code /users/auth} 시드에도 쓸 수 있다.</p>
304
+ * @returns {string|null}
305
+ */
306
+ static detectBrowserLanguage() {
307
+ if (typeof navigator === 'undefined' || !navigator.language) {
308
+ return null;
309
+ }
310
+ return navigator.language.trim().toLowerCase() || null;
311
+ }
312
+
313
+ /**
314
+ * 표시 텍스트 헬퍼 (다국어) — 내 언어 번역이 있으면 그것, 없으면 원문.
315
+ * <p>{@code myLang} 은 내 effectiveLanguage (방 participant.language ?? user.preferredLanguage).
316
+ * {@code messageTranslated} 이벤트로 갱신된 message 에 적용.</p>
317
+ * @param {Object} message - 메시지 (translations 맵 포함 가능)
318
+ * @param {string} [myLang] - 내 언어 코드
319
+ * @returns {string|null}
320
+ */
321
+ static displayText(message, myLang) {
322
+ if (message && message.translations && myLang && message.translations[myLang]) {
323
+ return message.translations[myLang];
324
+ }
325
+ return message ? message.content : null;
326
+ }
327
+
328
+ /**
329
+ * 사용자 존재 여부 확인
330
+ * @param {string} projectUserId - 프로젝트 사용자 ID
331
+ * @returns {Promise<Object>}
332
+ */
333
+ async checkUserExists(projectUserId) {
334
+ return this.apiClient.get(`/api/v1/users/${projectUserId}/exists`);
335
+ }
336
+
337
+ /**
338
+ * 사용자 목록 조회
339
+ * @param {Object} [params]
340
+ * @param {number} [params.size=50] - 페이지 크기 (1-100)
341
+ * @param {string} [params.lastId] - 마지막 사용자 ID
342
+ * @param {number} [params.lastSortValue] - 마지막 정렬값
343
+ * @returns {Promise<Object>}
344
+ */
345
+ async getUsers(params = {}) {
346
+ this._checkToken();
347
+ const { size = 50, lastId, lastSortValue } = params;
348
+ return this.apiClient.get('/api/v1/users', { size, lastId, lastSortValue });
349
+ }
350
+
351
+ /**
352
+ * 사용자 검색
353
+ * @param {Object} params
354
+ * @param {string} params.keyword - 검색 키워드 (필수, max 50자)
355
+ * @param {number} [params.limit=20] - 결과 수 (1-50)
356
+ * @returns {Promise<Object>}
357
+ */
358
+ async searchUsers(params) {
359
+ this._checkToken();
360
+ const { keyword, limit = 20 } = params;
361
+ return this.apiClient.get('/api/v1/users/search', { keyword, limit });
362
+ }
363
+
364
+ /**
365
+ * 토큰 필요 여부 확인
366
+ * @private
367
+ * @throws {Error} 토큰이 없는 경우
368
+ */
369
+ _checkToken() {
370
+ if (!this.options.jwtToken) {
371
+ throw new Error(
372
+ 'JWT token is required for this operation. Obtain it from your backend ' +
373
+ '(POST /api/v1/users/auth with Server API Key) and pass it via setToken(). ' +
374
+ 'See README "프로덕션 인증 플로우".'
375
+ );
376
+ }
377
+ }
378
+
379
+ // ==================== 유틸리티 ====================
380
+
381
+ /**
382
+ * 현재 사용자 ID
383
+ * @returns {string|null}
384
+ */
385
+ getUserId() {
386
+ return this.userId;
387
+ }
388
+
389
+ /**
390
+ * 초기화 상태 확인
391
+ * @returns {boolean}
392
+ */
393
+ isInitialized() {
394
+ return this._initialized;
395
+ }
396
+
397
+ /**
398
+ * 서브 클라이언트 초기화 상태 확인
399
+ * @returns {boolean}
400
+ */
401
+ isReady() {
402
+ return this._initialized && !!this.connectionManager;
403
+ }
404
+
405
+ /**
406
+ * 로그 레벨 설정
407
+ * @param {number} level - LogLevel 값
408
+ */
409
+ setLogLevel(level) {
410
+ this.options.logLevel = level;
411
+ this.logger.setLevel(level);
412
+ this.apiClient.logger?.setLevel(level);
413
+
414
+ if (this.connectionManager) {
415
+ this.connectionManager.setLogLevel(level);
416
+ }
417
+ if (this.chat) {
418
+ this.chat.setLogLevel(level);
419
+ }
420
+ if (this.webrtc) {
421
+ this.webrtc.setLogLevel(level);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * 전체 상태 요약
427
+ * @returns {Object}
428
+ */
429
+ getStatus() {
430
+ return {
431
+ initialized: this._initialized,
432
+ ready: this.isReady(),
433
+ hasToken: this.hasToken(),
434
+ connectionState: this._state,
435
+ isConnected: this.isConnected(),
436
+ userId: this.userId,
437
+ chat: this.chat ? {
438
+ subscribedRooms: this.chat.getSubscribedRooms()
439
+ } : null,
440
+ webrtc: this.webrtc ? {
441
+ isInCall: this.webrtc.isInCall(),
442
+ currentRoom: this.webrtc.getCurrentRoom(),
443
+ participants: this.webrtc.getParticipants(),
444
+ mediaState: this.webrtc.getMediaState()
445
+ } : null
446
+ };
447
+ }
448
+
449
+ /**
450
+ * 리소스 정리
451
+ */
452
+ async destroy() {
453
+ await this.disconnect();
454
+
455
+ if (this.chat) {
456
+ this.chat.destroy();
457
+ }
458
+ if (this.webrtc) {
459
+ this.webrtc.destroy();
460
+ }
461
+ if (this.connectionManager) {
462
+ await this.connectionManager.destroy();
463
+ }
464
+
465
+ this.removeAllListeners();
466
+
467
+ this._initialized = false;
468
+ this.logger.info('TalkFlowClient destroyed');
469
+ }
470
+ }
471
+
472
+ // 정적 속성으로 상수 노출
473
+ TalkFlowClient.ConnectionState = ConnectionState;
474
+ TalkFlowClient.ErrorTypes = ErrorTypes;
475
+ TalkFlowClient.LogLevel = LogLevel;
476
+ TalkFlowClient.Environment = Environment;
477
+
478
+ applySessionMethods(TalkFlowClient);
479
+ applyDelegatedMethods(TalkFlowClient);
480
+
481
+ export default TalkFlowClient;