@vibexnpm/talkx 8.6.0 → 8.9.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/dist/index.d.ts +64 -15
- package/dist/talkflow-sdk.esm.js +1 -1
- package/dist/talkflow-sdk.esm.js.map +1 -1
- package/dist/talkflow-sdk.standalone.js +1 -1
- package/dist/talkflow-sdk.standalone.js.map +1 -1
- package/dist/talkflow-sdk.umd.js +1 -1
- package/dist/talkflow-sdk.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/TalkFlowClient.js +29 -9
- package/src/auth/AuthSession.js +27 -2
- package/src/chat/ChatClient.js +3 -2
- package/src/constants.js +2 -0
- package/src/core/ConnectionManager.js +68 -6
- package/src/index.js +1 -0
- package/src/talkflow/eventForwarding.js +2 -0
- package/types/__typetests__/authSession.types.ts +23 -1
- package/types/__typetests__/webReading.types.ts +18 -0
- package/types/index.d.ts +64 -15
package/package.json
CHANGED
package/src/TalkFlowClient.js
CHANGED
|
@@ -39,6 +39,7 @@ class TalkFlowClient extends EventEmitter {
|
|
|
39
39
|
* // 1단계: 로그인 — 클라이언트 생성 «전» 에 부른다. ck/projectId/JWT 를 한 번에 받는다.
|
|
40
40
|
* // const { clientKey, projectId, accessToken } = await TalkFlowClient.loginUser({
|
|
41
41
|
* // serverUrl, email, password });
|
|
42
|
+
* // (회사 계정이 둘 이상이면 먼저 workspaceSelectionRequired 결과 → projectId 를 실어 다시 부른다)
|
|
42
43
|
*
|
|
43
44
|
* // 2단계: 받은 값으로 SDK 초기화
|
|
44
45
|
* const client = new TalkFlowClient({
|
|
@@ -210,6 +211,15 @@ class TalkFlowClient extends EventEmitter {
|
|
|
210
211
|
return this.connectionManager ? this.connectionManager.isConnected() : false;
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
/**
|
|
215
|
+
* 회사 구독 대기 중인가 — 서버가 웹소켓을 NOT_SUBSCRIBED 로 거부해 연결을 멈췄다(결제 전·해지 회사).
|
|
216
|
+
* 결제가 확인되면(`GET /api/v1/company/billing` 의 `subscription === 'ACTIVE'`) 같은 토큰으로 {@link #connect} 를 다시 부른다.
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
isSubscriptionRequired() {
|
|
220
|
+
return this.connectionManager ? this.connectionManager.isSubscriptionRequired() : false;
|
|
221
|
+
}
|
|
222
|
+
|
|
213
223
|
_enableRefreshSession() {
|
|
214
224
|
if (this.authSession) return;
|
|
215
225
|
this.options.sessionMode = 'refresh';
|
|
@@ -310,21 +320,28 @@ class TalkFlowClient extends EventEmitter {
|
|
|
310
320
|
* <p>Server Key 도 고객사 BFF 도 필요 없다. 응답의
|
|
311
321
|
* {@code clientKey}/{@code projectId}/{@code accessToken} 이 그대로 SDK 초기화 입력이다.</p>
|
|
312
322
|
*
|
|
313
|
-
* <p><b>회사
|
|
314
|
-
*
|
|
323
|
+
* <p><b>회사 선택</b>: 같은 이메일이 여러 회사에 계정을 가질 수 있다(다른 회사의 게스트 등). 비밀번호가 맞는 계정이
|
|
324
|
+
* 둘 이상이면 토큰 없이 {@code {workspaceSelectionRequired: true, workspaces: [{projectId, companyName, companyRole}]}}
|
|
325
|
+
* 를 돌려준다 — 저장된 세션은 그대로다. 고른 {@code projectId} 를 실어 같은 이메일·비밀번호로 다시 부른다.
|
|
326
|
+
* 계정이 하나면 지금처럼 바로 로그인된다.</p>
|
|
315
327
|
*
|
|
316
328
|
* @param {Object} options
|
|
317
329
|
* @param {string} options.serverUrl - 채팅 서버 base URL
|
|
318
330
|
* @param {string} options.email
|
|
319
331
|
* @param {string} options.password
|
|
320
|
-
* @
|
|
332
|
+
* @param {string} [options.projectId] - 회사 선택 응답에서 고른 회사
|
|
333
|
+
* @returns {Promise<Object>} `{accessToken, tokenType, expiresIn, projectId, clientKey, user}` 또는 회사 선택 결과
|
|
321
334
|
*
|
|
322
335
|
* @example
|
|
323
|
-
*
|
|
336
|
+
* let login = await TalkFlowClient.loginUser({
|
|
324
337
|
* serverUrl: 'https://chat.apiorbit.net',
|
|
325
338
|
* email: 'dan@example.com',
|
|
326
339
|
* password: '********'
|
|
327
340
|
* });
|
|
341
|
+
* if (login.workspaceSelectionRequired) {
|
|
342
|
+
* const picked = await showWorkspacePicker(login.workspaces); // 화면에서 고른 회사
|
|
343
|
+
* login = await TalkFlowClient.loginUser({ serverUrl, email, password, projectId: picked.projectId });
|
|
344
|
+
* }
|
|
328
345
|
*
|
|
329
346
|
* const client = new TalkFlowClient({
|
|
330
347
|
* apiKey: login.clientKey,
|
|
@@ -333,13 +350,13 @@ class TalkFlowClient extends EventEmitter {
|
|
|
333
350
|
* });
|
|
334
351
|
* await client.connect();
|
|
335
352
|
*/
|
|
336
|
-
static async loginUser({ serverUrl, email, password }) {
|
|
353
|
+
static async loginUser({ serverUrl, email, password, projectId }) {
|
|
337
354
|
if (!serverUrl) throw new Error('serverUrl is required');
|
|
338
355
|
if (!email) throw new Error('email is required');
|
|
339
356
|
if (!password) throw new Error('password is required');
|
|
340
357
|
|
|
341
358
|
// 로그인은 키를 «받기 위해» 부르는 경로라 X-API-KEY/X-PROJECT-ID 가 없다(서버에서 필터 제외).
|
|
342
|
-
return AuthSession.forServer(serverUrl).login({ email, password });
|
|
359
|
+
return AuthSession.forServer(serverUrl).login({ email, password, projectId });
|
|
343
360
|
}
|
|
344
361
|
|
|
345
362
|
/**
|
|
@@ -348,16 +365,19 @@ class TalkFlowClient extends EventEmitter {
|
|
|
348
365
|
* <p>이미 클라이언트가 있는 상태의 재로그인·계정 전환용이다. 최초 진입은 클라이언트 생성 전에
|
|
349
366
|
* 부르는 정적 {@link TalkFlowClient.loginUser} 를 쓴다 — 그쪽이 {@code clientKey} 까지 준다.</p>
|
|
350
367
|
*
|
|
368
|
+
* <p>회사 선택 응답이면 토큰을 설정하지 않고 그 결과를 돌려준다 — 정적 {@link TalkFlowClient.loginUser} 와 같다.</p>
|
|
369
|
+
*
|
|
351
370
|
* @param {Object} credentials
|
|
352
371
|
* @param {string} credentials.email
|
|
353
372
|
* @param {string} credentials.password
|
|
354
|
-
* @
|
|
373
|
+
* @param {string} [credentials.projectId] - 회사 선택 응답에서 고른 회사
|
|
374
|
+
* @returns {Promise<Object>} 로그인 응답 또는 회사 선택 결과
|
|
355
375
|
*/
|
|
356
|
-
async loginUser({ email, password }) {
|
|
376
|
+
async loginUser({ email, password, projectId }) {
|
|
357
377
|
if (!email || !password) throw new Error('email and password are required');
|
|
358
378
|
this._enableRefreshSession();
|
|
359
379
|
await this._authReady;
|
|
360
|
-
return this.authSession.login({ email, password });
|
|
380
|
+
return this.authSession.login({ email, password, projectId });
|
|
361
381
|
}
|
|
362
382
|
|
|
363
383
|
/**
|
package/src/auth/AuthSession.js
CHANGED
|
@@ -4,6 +4,21 @@ const sessions = new Map();
|
|
|
4
4
|
const TERMINAL_CODES = new Set(['REFRESH_INVALID', 'REFRESH_REUSED', 'SESSION_REVOKED', 'ACCOUNT_BLOCKED', 'LOGIN_FAILED']);
|
|
5
5
|
export const isTerminalAuthError = error => TERMINAL_CODES.has(error?.response?.code || error?.code);
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 로그인 회사 선택 결과 — 토큰이 없다. 목록이 비었거나 회사 id 가 없는 응답은 계약 위반이라 인증 불가로 끊는다(빈 선택 화면을 띄우지 않게).
|
|
9
|
+
*/
|
|
10
|
+
function workspaceSelection(data) {
|
|
11
|
+
const workspaces = Array.isArray(data.workspaces) ? data.workspaces : [];
|
|
12
|
+
if (workspaces.length === 0 || workspaces.some(workspace => !workspace?.projectId)) {
|
|
13
|
+
const error = new Error('Invalid authentication response'); error.code = 'AUTH_UNAVAILABLE'; throw error;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
workspaceSelectionRequired: true,
|
|
17
|
+
workspaces: workspaces.map(({ projectId, companyName, companyRole }) =>
|
|
18
|
+
({ projectId, companyName: companyName ?? null, companyRole }))
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
export function tokenClaims(token) {
|
|
8
23
|
try {
|
|
9
24
|
const value = token.replace(/^Bearer\s+/i, '').split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
@@ -240,13 +255,23 @@ export default class AuthSession extends EventEmitter {
|
|
|
240
255
|
} finally { clearTimeout(timeout); }
|
|
241
256
|
}
|
|
242
257
|
|
|
243
|
-
|
|
258
|
+
/**
|
|
259
|
+
* 같은 이메일로 여러 회사 계정이 있고 비밀번호가 둘 이상 맞으면 서버는 토큰 없이 회사 목록만 준다. 그 응답은
|
|
260
|
+
* {@link workspaceSelection} 결과로 돌려준다. 저장소·인증 상태·이전 세션은 건드리지 않는다 — 서버도 이 단계에서는
|
|
261
|
+
* 이전 세션을 끊지 않는다. 호출자는 고른 {@code projectId} 를 실어 같은 자격으로 다시 부른다(서버가 비밀번호를 다시 검증).
|
|
262
|
+
*/
|
|
263
|
+
async login({ email, password, projectId }) {
|
|
244
264
|
const generation = ++this.generation;
|
|
245
265
|
return this._locked(async () => {
|
|
246
266
|
const previous = this._readMarker();
|
|
247
267
|
this._writeMarker(previous);
|
|
248
268
|
if (previous.pendingRefreshToken) await this._request('/v1/auth/signout', { refreshToken: previous.pendingRefreshToken, deviceId: previous.deviceId });
|
|
249
|
-
const data = await this._request('/api/v1/auth/login',
|
|
269
|
+
const data = await this._request('/api/v1/auth/login',
|
|
270
|
+
{ email, password, previousRefreshToken: previous.refreshToken, ...(projectId ? { projectId } : {}) });
|
|
271
|
+
if (data?.workspaceSelectionRequired === true) {
|
|
272
|
+
if (generation !== this.generation) throw this._cancelled();
|
|
273
|
+
return workspaceSelection(data);
|
|
274
|
+
}
|
|
250
275
|
if (!data?.refreshToken) {
|
|
251
276
|
const error = new Error('Server does not support the refresh session contract');
|
|
252
277
|
error.code = 'AUTH_PROTOCOL_UNSUPPORTED';
|
package/src/chat/ChatClient.js
CHANGED
|
@@ -147,6 +147,7 @@ class ChatClient extends EventEmitter {
|
|
|
147
147
|
PLANNING: '기획 중…',
|
|
148
148
|
DOCUMENT_ANALYSIS: '문서 분석 중…',
|
|
149
149
|
VIDEO_ANALYSIS: '영상 분석 중…',
|
|
150
|
+
WEB_READING: '페이지 읽는 중…',
|
|
150
151
|
WRITING: '작성 중…'
|
|
151
152
|
};
|
|
152
153
|
// PHASE 의 expiresAt 누락/이상 시 fallback TTL (ms) — 서버 ttl(최소 30s) 보다 약간 길게.
|
|
@@ -2511,8 +2512,8 @@ class ChatClient extends EventEmitter {
|
|
|
2511
2512
|
* @param {string} roomId - 채팅방 ID
|
|
2512
2513
|
* @returns {Promise<void>}
|
|
2513
2514
|
*
|
|
2514
|
-
* @fires memberJoined - { roomId, members: [{ userId, nickname, profileUrl }], participantCount, timestamp }
|
|
2515
|
-
* @fires memberLeft - { roomId, members: [{ userId, nickname, profileUrl }], participantCount, timestamp }
|
|
2515
|
+
* @fires memberJoined - { roomId, members: [{ userId, nickname, profileUrl, companyRole? }], participantCount, timestamp }
|
|
2516
|
+
* @fires memberLeft - { roomId, members: [{ userId, nickname, profileUrl, companyRole? }], participantCount, timestamp }
|
|
2516
2517
|
*/
|
|
2517
2518
|
async subscribeRoom(roomId) {
|
|
2518
2519
|
if (this.subscribedRooms.has(roomId)) {
|
package/src/constants.js
CHANGED
|
@@ -22,6 +22,8 @@ export const ErrorTypes = {
|
|
|
22
22
|
CONNECTION_FAILED: 'CONNECTION_FAILED',
|
|
23
23
|
CONNECTION_LOST: 'CONNECTION_LOST',
|
|
24
24
|
CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT',
|
|
25
|
+
// 회사 구독 없음 — 서버가 NOT_SUBSCRIBED 로 거부했다(결제 전·해지). 재연결하지 않고 connect() 를 기다린다.
|
|
26
|
+
SUBSCRIPTION_REQUIRED: 'SUBSCRIPTION_REQUIRED',
|
|
25
27
|
|
|
26
28
|
// Authentication errors
|
|
27
29
|
JWT_INVALID: 'JWT_INVALID',
|
|
@@ -10,6 +10,11 @@ import { ConnectionState, ErrorTypes, WebSocketPaths, DefaultConfig, LogLevel }
|
|
|
10
10
|
import { Client as StompClient, ReconnectionTimeMode, StompSocketState } from '@stomp/stompjs';
|
|
11
11
|
import SockJS from 'sockjs-client';
|
|
12
12
|
|
|
13
|
+
/** 서버 구독 관문의 거부 코드(STOMP ERROR 본문 code · HTTP 402) — 결제 전·해지 회사. */
|
|
14
|
+
const SUBSCRIPTION_REQUIRED_CODE = 'NOT_SUBSCRIBED';
|
|
15
|
+
/** 서버가 구독 상태를 잠시 확인하지 못했다(503) — 대기가 아니라 일시 실패다. */
|
|
16
|
+
const SUBSCRIPTION_CHECK_UNAVAILABLE_CODE = 'SUBSCRIPTION_CHECK_UNAVAILABLE';
|
|
17
|
+
|
|
13
18
|
// STOMP Client 가져오기 (CDN 사용 시 전역 객체 우선)
|
|
14
19
|
const getStompClient = () => {
|
|
15
20
|
// noinspection JSUnresolvedReference
|
|
@@ -82,6 +87,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
82
87
|
this._recoveryTimer = null;
|
|
83
88
|
this._terminalAuthError = false;
|
|
84
89
|
this._recoveryEligible = false;
|
|
90
|
+
// 회사 구독 대기 — 서버가 CONNECT·SUBSCRIBE·SEND 를 NOT_SUBSCRIBED 로 거부했다(결제 전·해지 회사).
|
|
91
|
+
// 인증 거부와 다르다: 토큰은 그대로 유효하고, 결제가 끝나면 앱이 같은 토큰으로 connect() 를 다시 부른다.
|
|
92
|
+
this._subscriptionRequired = false;
|
|
85
93
|
|
|
86
94
|
this._installLifecycleListeners();
|
|
87
95
|
}
|
|
@@ -153,7 +161,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
153
161
|
* @private
|
|
154
162
|
*/
|
|
155
163
|
_onResume(reason) {
|
|
156
|
-
if (!this.resumeOnVisibility || this._intentionalDisconnect || this._terminalAuthError) {
|
|
164
|
+
if (!this.resumeOnVisibility || this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) {
|
|
157
165
|
return;
|
|
158
166
|
}
|
|
159
167
|
|
|
@@ -248,7 +256,10 @@ class ConnectionManager extends EventEmitter {
|
|
|
248
256
|
this.stompClient = null;
|
|
249
257
|
}
|
|
250
258
|
|
|
251
|
-
|
|
259
|
+
// 구독 대기면 다시 붙지 않는다 — refresh 세션의 토큰 갱신(AuthSession 'access-token-renewed')·저빈도 복구·탭 복귀가 모두
|
|
260
|
+
// 이리로 오고, 정리를 기다리는 동안 옛 클라이언트가 NOT_SUBSCRIBED 로 대기에 들어간 경우도 여기서 걸린다. 대기는 앱이 connect() 를
|
|
261
|
+
// 다시 불러야 풀린다.
|
|
262
|
+
if (this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) return;
|
|
252
263
|
|
|
253
264
|
// connect() 가 CONNECTED/CONNECTING 이면 조기 반환하므로 상태를 먼저 내려둔다.
|
|
254
265
|
this._setState(ConnectionState.DISCONNECTED);
|
|
@@ -407,6 +418,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
407
418
|
}
|
|
408
419
|
this._cancelReconnectRecovery();
|
|
409
420
|
this._terminalAuthError = false;
|
|
421
|
+
// 구독 대기는 앱이 connect() 를 다시 불러야 풀린다(결제 확인 뒤). 아직 구독이 없으면 서버가 다시 거부해 대기로 돌아온다.
|
|
422
|
+
this._subscriptionRequired = false;
|
|
410
423
|
this._setState(ConnectionState.CONNECTING);
|
|
411
424
|
this.reconnectAttempts = 0;
|
|
412
425
|
this._intentionalDisconnect = false;
|
|
@@ -476,10 +489,20 @@ class ConnectionManager extends EventEmitter {
|
|
|
476
489
|
* @private
|
|
477
490
|
*/
|
|
478
491
|
_onStompError(frame) {
|
|
492
|
+
let code = null;
|
|
493
|
+
let serverMessage = null;
|
|
479
494
|
try {
|
|
480
495
|
const payload = JSON.parse(frame.body || frame.headers?.message || '{}');
|
|
481
|
-
|
|
496
|
+
code = payload.code ?? null;
|
|
497
|
+
serverMessage = typeof payload.message === 'string' ? payload.message : null;
|
|
498
|
+
this._authRejected = ['ACCESS_UNAUTHORIZED', 'SESSION_REVOKED', 'TOKEN_INVALID'].includes(code);
|
|
482
499
|
} catch { /* Non-authentication STOMP errors keep the normal reconnect policy. */ }
|
|
500
|
+
// 회사 구독 없음 — 재연결해도 같은 거부다. 인증 거부로 기록하지 않고 «구독 대기» 로 멈춘다.
|
|
501
|
+
// 구독 확인 일시 실패(SUBSCRIPTION_CHECK_UNAVAILABLE)는 아래 기본 경로 — 평소처럼 다시 붙는다.
|
|
502
|
+
if (code === SUBSCRIPTION_REQUIRED_CODE) {
|
|
503
|
+
this._stopForSubscription(serverMessage);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
483
506
|
this.logger.error('STOMP error:', frame);
|
|
484
507
|
this._setState(ConnectionState.ERROR);
|
|
485
508
|
|
|
@@ -490,7 +513,9 @@ class ConnectionManager extends EventEmitter {
|
|
|
490
513
|
};
|
|
491
514
|
|
|
492
515
|
if (this._authRejected) this._recordAuthRejection();
|
|
493
|
-
|
|
516
|
+
// 구독 확인 일시 실패는 첫 연결이라도 대기 중인 connect() 를 끝내지 않는다 — STOMP 가 곧 다시 붙는다. 여기서 reject 하면
|
|
517
|
+
// 앱은 실패로 알고 방 목록 구독 등 후속 초기화를 건너뛰는데 연결은 붙어 «반쪽 연결» 이 된다.
|
|
518
|
+
const terminal = code !== SUBSCRIPTION_CHECK_UNAVAILABLE_CODE && !this._canRecoverConnection();
|
|
494
519
|
if (terminal) this._drainPendingSubscriptions(`STOMP error: ${error.message}`);
|
|
495
520
|
|
|
496
521
|
this.emit('error', error);
|
|
@@ -512,7 +537,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
512
537
|
|
|
513
538
|
// forceReconnect 의 의도적 teardown 이면 재연결 시도로 집계하지 않는다 —
|
|
514
539
|
// 카운터가 오염되면 복귀 직후 다시 max 를 소진해버린다.
|
|
515
|
-
if (this._tearingDown || this._intentionalDisconnect || this._terminalAuthError) {
|
|
540
|
+
if (this._tearingDown || this._intentionalDisconnect || this._terminalAuthError || this._subscriptionRequired) {
|
|
516
541
|
this.logger.debug('WebSocket closed during intentional teardown — not counting as reconnect');
|
|
517
542
|
return;
|
|
518
543
|
}
|
|
@@ -618,7 +643,43 @@ class ConnectionManager extends EventEmitter {
|
|
|
618
643
|
|
|
619
644
|
_canRecoverConnection() {
|
|
620
645
|
return this._recoveryEligible && !this._intentionalDisconnect && !this._terminalAuthError
|
|
621
|
-
&& this.reconnectRecoveryDelay > 0;
|
|
646
|
+
&& !this._subscriptionRequired && this.reconnectRecoveryDelay > 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** 회사 구독 대기 중인가 — 서버가 NOT_SUBSCRIBED 로 거부해 연결을 멈췄다. 앱이 connect() 를 다시 부르면 풀린다. */
|
|
650
|
+
isSubscriptionRequired() {
|
|
651
|
+
return this._subscriptionRequired;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* «구독 대기» 로 멈춘다 (서버 구독 관문 설계 v3 §7).
|
|
656
|
+
*
|
|
657
|
+
* <p>인증 정지({@link #_stopForAuthError})와 같은 방식으로 STOMP 재연결 루프·저빈도 복구·탭 복귀 재연결을 끄되, 토큰은
|
|
658
|
+
* 거부된 토큰으로 기록하지 않는다 — 토큰 갱신·로그인은 그대로 둔다. 대기 중인 connect()·구독은 reject 하고, 한 번 들어올 때
|
|
659
|
+
* {@code subscriptionRequired} 를 알린다(일반 {@code error} 는 내지 않는다 — 연결 오류 안내가 결제 안내를 가리지 않게).
|
|
660
|
+
* 연결 중에 해지되면 다음 SUBSCRIBE·SEND 의 ERROR 로 같은 경로에 온다.</p>
|
|
661
|
+
*
|
|
662
|
+
* @private
|
|
663
|
+
*/
|
|
664
|
+
_stopForSubscription(serverMessage) {
|
|
665
|
+
const entering = !this._subscriptionRequired;
|
|
666
|
+
this._subscriptionRequired = true;
|
|
667
|
+
this._cancelReconnectRecovery();
|
|
668
|
+
this._setState(ConnectionState.ERROR);
|
|
669
|
+
// 본문의 안내 문구 — message 헤더는 오류 JSON 전체가 이스케이프된 채 오므로 쓰지 않는다.
|
|
670
|
+
const message = serverMessage || 'Company subscription required';
|
|
671
|
+
const error = Object.assign(new Error('Company subscription required'),
|
|
672
|
+
{ code: SUBSCRIPTION_REQUIRED_CODE, type: ErrorTypes.SUBSCRIPTION_REQUIRED });
|
|
673
|
+
this._connectReject?.(error);
|
|
674
|
+
this._connectResolve = this._connectReject = null;
|
|
675
|
+
this._drainPendingSubscriptions('Company subscription required');
|
|
676
|
+
const client = this.stompClient;
|
|
677
|
+
Promise.resolve(client?.deactivate()).catch(failure => this.logger.warn('Subscription teardown failed:', failure));
|
|
678
|
+
this.logger.warn('Company subscription required — connection stopped until connect() is called again');
|
|
679
|
+
if (entering) {
|
|
680
|
+
// 정지 전이를 마친 뒤 알린다 — 처리기가 곧바로 connect() 를 불러도 정리 중인 연결과 섞이지 않는다.
|
|
681
|
+
Promise.resolve().then(() => this.emit('subscriptionRequired', { code: SUBSCRIPTION_REQUIRED_CODE, message }));
|
|
682
|
+
}
|
|
622
683
|
}
|
|
623
684
|
|
|
624
685
|
/** 기존에 연결됐던 클라이언트만 긴 장애 후 탭 이벤트 없이 다시 시도한다. */
|
|
@@ -795,6 +856,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
795
856
|
// 명시적 해제 — 포그라운드 복귀가 이걸 되살리면 안 된다.
|
|
796
857
|
this._intentionalDisconnect = true;
|
|
797
858
|
this._recoveryEligible = false;
|
|
859
|
+
this._subscriptionRequired = false;
|
|
798
860
|
this._authRejected = false;
|
|
799
861
|
this._rejectedToken = null;
|
|
800
862
|
this._cancelReconnectRecovery();
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* import TalkFlowClient from '@talkflow/sdk';
|
|
15
15
|
*
|
|
16
16
|
* // 1단계: 로그인. 클라이언트 생성 «전» 에 부른다 — ck/projectId/JWT 를 한 번에 받는다.
|
|
17
|
+
* // 회사 계정이 둘 이상이면 먼저 { workspaceSelectionRequired, workspaces } 가 온다 → projectId 를 실어 다시 부른다.
|
|
17
18
|
* const { clientKey, projectId, accessToken } = await TalkFlowClient.loginUser({
|
|
18
19
|
* serverUrl: 'https://chat.apiorbit.net',
|
|
19
20
|
* email: 'me@example.com',
|
|
@@ -7,6 +7,8 @@ const CONNECTION_EVENT_MAP = [
|
|
|
7
7
|
// 재연결 '성공' + 포그라운드 복귀 감지 (RECONNECT_SYNC_DESIGN §6.1/§6.4)
|
|
8
8
|
['reconnected', 'reconnected'],
|
|
9
9
|
['resumed', 'resumed'],
|
|
10
|
+
// 회사 구독 대기(서버 구독 관문) — 연결 오류와 따로 알린다. 결제 뒤 connect() 를 다시 부르면 풀린다.
|
|
11
|
+
['subscriptionRequired', 'subscriptionRequired'],
|
|
10
12
|
['error', 'connectionError']
|
|
11
13
|
];
|
|
12
14
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import TalkFlowClient, { AuthState, LoginResult } from '../index';
|
|
1
|
+
import TalkFlowClient, { AuthState, LoginResult, WorkspaceSelection, CompanyRole, ParticipantResponse, RoomListMemberInfo } from '../index';
|
|
2
2
|
const client = new TalkFlowClient({ serverUrl: 'https://api.example.com', sessionMode: 'refresh' });
|
|
3
3
|
const restored: Promise<LoginResult | null> = client.restoreSession();
|
|
4
4
|
const state: AuthState = client.getAuthState();
|
|
@@ -6,3 +6,25 @@ const signedIn: boolean = client.isLoggedIn();
|
|
|
6
6
|
const access: Promise<string> = client.getAccessToken({ forceRefresh: true, rejectedToken: 'old' });
|
|
7
7
|
client.on('authStateChanged', ({ state }) => { const typed: AuthState = state; void typed; });
|
|
8
8
|
void [restored, state, signedIn, access];
|
|
9
|
+
|
|
10
|
+
// 회사 선택 — 결과를 workspaceSelectionRequired 로 가른 뒤에만 토큰 칸을 쓴다.
|
|
11
|
+
async function loginWithSelection(): Promise<LoginResult> {
|
|
12
|
+
const first = await TalkFlowClient.loginUser({ serverUrl: 'https://api.example.com', email: 'a@b.c', password: 'pw' });
|
|
13
|
+
if (first.workspaceSelectionRequired) {
|
|
14
|
+
const picked: WorkspaceSelection['workspaces'][number] = first.workspaces[0];
|
|
15
|
+
const role: CompanyRole = picked.companyRole;
|
|
16
|
+
void role;
|
|
17
|
+
const second = await client.loginUser({ email: 'a@b.c', password: 'pw', projectId: picked.projectId });
|
|
18
|
+
if (second.workspaceSelectionRequired) throw new Error('unexpected');
|
|
19
|
+
return second;
|
|
20
|
+
}
|
|
21
|
+
const key: string = first.clientKey;
|
|
22
|
+
void key;
|
|
23
|
+
return first;
|
|
24
|
+
}
|
|
25
|
+
// @ts-expect-error 가르기 전에는 clientKey 가 없을 수 있다
|
|
26
|
+
TalkFlowClient.loginUser({ serverUrl: 'x', email: 'a', password: 'b' }).then(result => result.clientKey.length);
|
|
27
|
+
const guestRole: CompanyRole = 'GUEST';
|
|
28
|
+
const participantRole: ParticipantResponse['companyRole'] = 'GUEST';
|
|
29
|
+
const memberRole: RoomListMemberInfo['companyRole'] = undefined;
|
|
30
|
+
void [loginWithSelection, guestRole, participantRole, memberRole];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 웹페이지 읽기 진행 단계(8.9.0) — `WEB_READING` 이 phase 유니온에 있다는 것을 tsc 로 고정한다.
|
|
3
|
+
*
|
|
4
|
+
* <p>phase 를 직접 분기하는 화면이 이 값을 쓸 수 있어야 한다. 기본 라벨은 «페이지 읽는 중…» 이다.</p>
|
|
5
|
+
*/
|
|
6
|
+
import type { AssistantProgressEvent, AssistantRoomProgressEvent, AssistantProgressStreamSnapshot } from '../index';
|
|
7
|
+
|
|
8
|
+
export function isWebReading(event: AssistantProgressEvent): boolean {
|
|
9
|
+
return event.active && event.phase === 'WEB_READING';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function roomIsWebReading(event: AssistantRoomProgressEvent): boolean {
|
|
13
|
+
return event.phase === 'WEB_READING';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function snapshotIsWebReading(snapshot: AssistantProgressStreamSnapshot): boolean {
|
|
17
|
+
return snapshot.phase === 'WEB_READING';
|
|
18
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export const ErrorTypes: {
|
|
|
26
26
|
readonly CONNECTION_FAILED: 'CONNECTION_FAILED';
|
|
27
27
|
readonly CONNECTION_LOST: 'CONNECTION_LOST';
|
|
28
28
|
readonly CONNECTION_TIMEOUT: 'CONNECTION_TIMEOUT';
|
|
29
|
+
/** 회사 구독 없음(결제 전·해지) — 웹소켓이 멈췄다. 결제 확인 뒤 `connect()` 를 다시 부른다. */
|
|
30
|
+
readonly SUBSCRIPTION_REQUIRED: 'SUBSCRIPTION_REQUIRED';
|
|
29
31
|
readonly JWT_INVALID: 'JWT_INVALID';
|
|
30
32
|
readonly JWT_EXPIRED: 'JWT_EXPIRED';
|
|
31
33
|
readonly JWT_PARSE_FAILED: 'JWT_PARSE_FAILED';
|
|
@@ -1044,14 +1046,20 @@ export interface DecisionResponse {
|
|
|
1044
1046
|
optionId: string;
|
|
1045
1047
|
}
|
|
1046
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* 회사 역할. `GUEST` 는 회사 밖에서 초대받아 그 회사 계정을 가진 사람이다 — 초대받은 방에서 채팅만 하고 PM·AI 버튼·조직도·
|
|
1051
|
+
* 방 만들기·초대 등은 서버가 막는다(403 `GUEST_FORBIDDEN`). 화면 노출 판단용이며 인가 근거는 아니다.
|
|
1052
|
+
*/
|
|
1053
|
+
export type CompanyRole = 'PARTNER' | 'MEMBER' | 'GUEST';
|
|
1054
|
+
|
|
1047
1055
|
/** 로그인한 회원. `userId` 는 메시지 발신자 식별과 같은 값이다. */
|
|
1048
1056
|
export interface LoginMember {
|
|
1049
1057
|
userId: string;
|
|
1050
1058
|
email?: string | null;
|
|
1051
1059
|
nickname?: string | null;
|
|
1052
1060
|
profileUrl?: string | null;
|
|
1053
|
-
/** 회사 역할 — 클라이언트가
|
|
1054
|
-
companyRole:
|
|
1061
|
+
/** 회사 역할 — 클라이언트가 파트너·게스트 화면 노출을 결정한다. 인가 근거는 아니다(서버가 재확인). */
|
|
1062
|
+
companyRole: CompanyRole;
|
|
1055
1063
|
preferredLanguage?: string | null;
|
|
1056
1064
|
/**
|
|
1057
1065
|
* 초기 비밀번호를 아직 안 바꿨는가 — `true` 면 **비밀번호 변경 화면으로 보내라**.
|
|
@@ -1063,11 +1071,11 @@ export interface LoginMember {
|
|
|
1063
1071
|
}
|
|
1064
1072
|
|
|
1065
1073
|
/**
|
|
1066
|
-
* `TalkFlowClient.loginUser` 응답.
|
|
1067
|
-
*
|
|
1068
|
-
* 회사 선택 단계가 없다 — 로그인 키가 전역 유일이라 이메일 하나로 회원과 회사가 함께 정해진다.
|
|
1074
|
+
* `TalkFlowClient.loginUser` 의 로그인 성공 응답. 회사 선택이 필요하면 대신 {@link WorkspaceSelection} 이 온다 —
|
|
1075
|
+
* `workspaceSelectionRequired` 로 가른다.
|
|
1069
1076
|
*/
|
|
1070
1077
|
export interface LoginResult {
|
|
1078
|
+
workspaceSelectionRequired?: false;
|
|
1071
1079
|
/** SDK가 저장·회전하는 갱신 자격. 직접 API를 쓰는 경우에만 응답에서 사용한다. */
|
|
1072
1080
|
refreshToken?: string;
|
|
1073
1081
|
/** 서버 현재 시각 및 refresh 만료 시각, Unix epoch 밀리초. */
|
|
@@ -1083,6 +1091,23 @@ export interface LoginResult {
|
|
|
1083
1091
|
user: LoginMember;
|
|
1084
1092
|
}
|
|
1085
1093
|
|
|
1094
|
+
/** 로그인 회사 선택지 하나. */
|
|
1095
|
+
export interface Workspace {
|
|
1096
|
+
projectId: string;
|
|
1097
|
+
companyName: string | null;
|
|
1098
|
+
/** 그 회사에서의 역할 — 선택 화면에 «게스트» 표시를 붙이는 데 쓴다. */
|
|
1099
|
+
companyRole: CompanyRole;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* 로그인 회사 선택 결과 — 같은 이메일의 여러 회사 계정에서 비밀번호가 둘 이상 맞았다. **토큰이 없고 저장된 세션은 그대로다.**
|
|
1104
|
+
* 고른 `projectId` 를 실어 같은 이메일·비밀번호로 `loginUser` 를 다시 부른다.
|
|
1105
|
+
*/
|
|
1106
|
+
export interface WorkspaceSelection {
|
|
1107
|
+
workspaceSelectionRequired: true;
|
|
1108
|
+
workspaces: Workspace[];
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1086
1111
|
/** 회사 조직도의 회원 — 초대 UI 에 필요한 것만. */
|
|
1087
1112
|
export interface DirectoryMember {
|
|
1088
1113
|
userId: string;
|
|
@@ -1124,7 +1149,7 @@ export interface MyProfile {
|
|
|
1124
1149
|
phone?: string | null;
|
|
1125
1150
|
profileUrl?: string | null;
|
|
1126
1151
|
/** 회사 역할 — 방 역할(ParticipantResponse.role)과 다른 축이다. */
|
|
1127
|
-
companyRole:
|
|
1152
|
+
companyRole: CompanyRole;
|
|
1128
1153
|
orgNodeId?: string | null;
|
|
1129
1154
|
orgName?: string | null;
|
|
1130
1155
|
preferredLanguage?: string | null;
|
|
@@ -1170,6 +1195,10 @@ export interface ParticipantResponse {
|
|
|
1170
1195
|
role: 'OWNER' | 'ADMIN' | 'MEMBER' | 'ASSISTANT';
|
|
1171
1196
|
/** 참가 시각(epoch millis). */
|
|
1172
1197
|
joinedAt: number;
|
|
1198
|
+
/**
|
|
1199
|
+
* 회사 역할 — 게스트 배지(`GUEST`)용. 방 역할({@link role})과 다른 축이다. PM(ASSISTANT)·정보를 못 찾은 참여자는 없다.
|
|
1200
|
+
*/
|
|
1201
|
+
companyRole?: CompanyRole;
|
|
1173
1202
|
}
|
|
1174
1203
|
|
|
1175
1204
|
/** 채팅방 last message preview. */
|
|
@@ -1280,6 +1309,8 @@ export interface RoomListMemberInfo {
|
|
|
1280
1309
|
userId: string;
|
|
1281
1310
|
nickname?: string | null;
|
|
1282
1311
|
profileUrl?: string | null;
|
|
1312
|
+
/** 회사 역할 — 방금 들어온 게스트에도 다시 읽지 않고 배지를 그린다. PM·역할을 모르는 경로는 없다. */
|
|
1313
|
+
companyRole?: CompanyRole;
|
|
1283
1314
|
}
|
|
1284
1315
|
|
|
1285
1316
|
export interface RoomListUpdateEvent {
|
|
@@ -1478,7 +1509,7 @@ export interface AssistantProgressEvent {
|
|
|
1478
1509
|
/** 진행 주체 페르소나 ID. */
|
|
1479
1510
|
personaId: string | null;
|
|
1480
1511
|
/** 진행 단계 — `active:true` 일 때. 종료 시 null. */
|
|
1481
|
-
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
|
|
1512
|
+
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
|
|
1482
1513
|
/** DOCUMENT_ANALYSIS의 실측 진행률. 같은 phase heartbeat 사이와 재입장 후에도 보존된다. */
|
|
1483
1514
|
progress?: DocumentAnalysisProgress | null;
|
|
1484
1515
|
/** phase 의 기본 표시 라벨(예: "생각 중…"). SDK 매핑 — 커스텀하려면 `phase` 사용. 종료 시 null. */
|
|
@@ -1549,7 +1580,7 @@ export interface AssistantRoomProgressEvent {
|
|
|
1549
1580
|
/** 표시 기준이 된 stream (가장 최근 갱신). 없으면 null. */
|
|
1550
1581
|
streamId: string | null;
|
|
1551
1582
|
personaId: string | null;
|
|
1552
|
-
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
|
|
1583
|
+
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
|
|
1553
1584
|
progress?: DocumentAnalysisProgress | null;
|
|
1554
1585
|
label: string | null;
|
|
1555
1586
|
/** 표시 기준 stream 의 누적 본문. `textAvailable:false` 거나 진행 중이 아니면 null. */
|
|
@@ -1562,7 +1593,7 @@ export interface AssistantRoomProgressEvent {
|
|
|
1562
1593
|
export interface AssistantProgressStreamSnapshot {
|
|
1563
1594
|
streamId: string;
|
|
1564
1595
|
personaId: string | null;
|
|
1565
|
-
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WRITING' | null;
|
|
1596
|
+
phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'VIDEO_ANALYSIS' | 'WEB_READING' | 'WRITING' | null;
|
|
1566
1597
|
progress?: DocumentAnalysisProgress | null;
|
|
1567
1598
|
label: string | null;
|
|
1568
1599
|
text: string | null;
|
|
@@ -1934,7 +1965,7 @@ export interface ChatMessageReceivedPayload {
|
|
|
1934
1965
|
|
|
1935
1966
|
export interface MemberChangePayload {
|
|
1936
1967
|
roomId: string;
|
|
1937
|
-
members: Array<{ userId: string; nickname?: string; profileUrl?: string | null }>;
|
|
1968
|
+
members: Array<{ userId: string; nickname?: string; profileUrl?: string | null; companyRole?: CompanyRole }>;
|
|
1938
1969
|
participantCount: number;
|
|
1939
1970
|
timestamp?: number;
|
|
1940
1971
|
}
|
|
@@ -1968,7 +1999,7 @@ export interface RetentionCleanupPayload {
|
|
|
1968
1999
|
/**
|
|
1969
2000
|
* TalkFlowClient 이벤트 맵 — 런타임 {@code TalkFlowClient.js} 의 {@code this.emit(...)} 호출 기반.
|
|
1970
2001
|
*
|
|
1971
|
-
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, connectionError, tokenSet, loggedOut</p>
|
|
2002
|
+
* <p><b>Connection 관련</b>: stateChange, connected, disconnected, reconnecting, reconnected, resumed, subscriptionRequired, connectionError, tokenSet, loggedOut</p>
|
|
1972
2003
|
* <p><b>Chat 관련 (ChatClient 이벤트를 이 이름으로 re-emit)</b>: chatMessage, newChatMessage, messageUpdated, messageDeleted, reactionChanged, linkPreviewAttached, messageRead, typing, assistantProgress, memberJoined, memberLeft, roomSubscribed, roomUnsubscribed, roomListSubscribed, roomListUnsubscribed, roomListUpdate, roomListMessage, roomListCreated, roomListJoined, roomListLeft, roomListSelfLeft, roomListRoomUpdated, retentionCleanup</p>
|
|
1973
2004
|
* <p><b>Push 관련</b>: pushEnabled, pushFailed, pushNotification</p>
|
|
1974
2005
|
* <p><b>WebRTC 관련</b>: localStreamStarted, localStreamStopped, remoteTrack, screenShareStarted, screenShareEnded, deviceChange, mediaStateChanged, callStarted, callEnded, callRequested, callAccepted, callRejected, callCancelled, callInvitation, callBusy, incomingCall, incomingCallWhileBusy, userJoined, userLeft, participantLeft, participantMediaState, peerConnected, peerDisconnected, peerClosed, webrtcError</p>
|
|
@@ -1993,6 +2024,15 @@ export interface TalkFlowClientEvents {
|
|
|
1993
2024
|
* 발행된다(두 이벤트가 겹치지 않아 재동기화가 중복 실행되지 않는다).</p>
|
|
1994
2025
|
*/
|
|
1995
2026
|
resumed: { reason: 'visibilitychange' | 'pageshow' | 'online' };
|
|
2027
|
+
/**
|
|
2028
|
+
* 회사 구독 없음 — 서버가 웹소켓 연결·구독·전송을 `NOT_SUBSCRIBED` 로 거부했다(결제 전·해지 회사). SDK 는 재연결하지 않고
|
|
2029
|
+
* 토큰도 거부된 것으로 기록하지 않는다(로그인·토큰 갱신은 그대로). 대기에 들어갈 때 한 번 발행되며 `connectionError` 는 내지 않는다.
|
|
2030
|
+
*
|
|
2031
|
+
* <p>앱: 파트너면 결제 화면, 그 밖은 «회사 구독이 필요합니다. 관리자에게 문의하세요.» 안내. 결제가 확인되면
|
|
2032
|
+
* (`subscription === 'ACTIVE'`) 같은 토큰으로 `connect()` 를 다시 부른다. 구독 확인 일시 실패(`SUBSCRIPTION_CHECK_UNAVAILABLE`)는
|
|
2033
|
+
* 이 이벤트가 아니다 — 평소처럼 재연결한다.</p>
|
|
2034
|
+
*/
|
|
2035
|
+
subscriptionRequired: { code: 'NOT_SUBSCRIBED'; message: string };
|
|
1996
2036
|
connectionError: unknown;
|
|
1997
2037
|
tokenSet: { userId: string };
|
|
1998
2038
|
loggedOut: void;
|
|
@@ -2602,16 +2642,18 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2602
2642
|
/**
|
|
2603
2643
|
* 회원 로그인 — 클라이언트 생성 **전에** 호출한다. Server Key 도 고객사 BFF 도 필요 없다.
|
|
2604
2644
|
* 응답의 `clientKey`/`projectId`/`accessToken` 이 그대로 SDK 초기화 입력이다.
|
|
2605
|
-
* 회사
|
|
2645
|
+
* 같은 이메일로 비밀번호가 맞는 회사 계정이 둘 이상이면 {@link WorkspaceSelection} 이 온다 — 고른 `projectId` 로 다시 부른다.
|
|
2606
2646
|
*/
|
|
2607
2647
|
static loginUser(options: {
|
|
2608
2648
|
serverUrl: string;
|
|
2609
2649
|
email: string;
|
|
2610
2650
|
password: string;
|
|
2611
|
-
|
|
2651
|
+
/** 회사 선택 응답에서 고른 회사. */
|
|
2652
|
+
projectId?: string;
|
|
2653
|
+
}): Promise<LoginResult | WorkspaceSelection>;
|
|
2612
2654
|
|
|
2613
|
-
/** 회원 로그인 (인스턴스) — 재로그인·계정 전환용. 성공 시 토큰을 자동 설정한다. */
|
|
2614
|
-
loginUser(credentials: { email: string; password: string }): Promise<LoginResult>;
|
|
2655
|
+
/** 회원 로그인 (인스턴스) — 재로그인·계정 전환용. 성공 시 토큰을 자동 설정한다. 회사 선택 결과면 토큰을 설정하지 않는다. */
|
|
2656
|
+
loginUser(credentials: { email: string; password: string; projectId?: string }): Promise<LoginResult | WorkspaceSelection>;
|
|
2615
2657
|
|
|
2616
2658
|
/** localStorage의 refresh로 복구. 세션이 없으면 null, 네트워크/서버 장애는 reject. */
|
|
2617
2659
|
static restoreSession(options: { serverUrl: string }): Promise<LoginResult | null>;
|
|
@@ -2648,6 +2690,8 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2648
2690
|
/**
|
|
2649
2691
|
* 연결 완료까지 기다립니다. 이전 연결의 자동 복구가 이어지는 동안 재시도 한도 소진으로 reject하지 않습니다.
|
|
2650
2692
|
* 복구 연결 시 resolve하며, 명시적 disconnect·인증 종료·최초 연결 실패 시 reject합니다.
|
|
2693
|
+
* 회사 구독이 없으면(서버 `NOT_SUBSCRIBED`) `code: 'NOT_SUBSCRIBED'`·`type: 'SUBSCRIPTION_REQUIRED'` 로 reject 하고
|
|
2694
|
+
* 재연결하지 않습니다 — {@link TalkFlowClientEvents.subscriptionRequired} 참고.
|
|
2651
2695
|
*/
|
|
2652
2696
|
connect(jwt?: string, options?: ConnectOptions): Promise<void>;
|
|
2653
2697
|
disconnect(): Promise<void>;
|
|
@@ -2655,6 +2699,11 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2655
2699
|
setToken(token: string): Promise<void>;
|
|
2656
2700
|
updateToken(newToken: string): Promise<void>;
|
|
2657
2701
|
isConnected(): boolean;
|
|
2702
|
+
/**
|
|
2703
|
+
* 회사 구독 대기 중인가 — 서버가 웹소켓을 `NOT_SUBSCRIBED` 로 거부해 연결을 멈췄다(결제 전·해지 회사).
|
|
2704
|
+
* 결제가 확인되면(`GET /api/v1/company/billing` 의 `subscription === 'ACTIVE'`) 같은 토큰으로 `connect()` 를 다시 부른다.
|
|
2705
|
+
*/
|
|
2706
|
+
isSubscriptionRequired(): boolean;
|
|
2658
2707
|
getState(): ConnectionStateValue;
|
|
2659
2708
|
hasToken(): boolean;
|
|
2660
2709
|
|