@vibexnpm/talkx 7.2.0 → 8.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/README.md +2 -0
- package/dist/index.d.ts +28 -3
- 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 +106 -16
- package/src/auth/AuthSession.js +409 -0
- package/src/core/ConnectionManager.js +42 -0
- package/src/push/PushManager.js +2 -0
- package/src/talkflow/session.js +20 -1
- package/src/utils/ApiClient.js +45 -4
- package/src/utils/jwtUtils.js +2 -2
- package/types/__typetests__/authSession.types.ts +8 -0
- package/types/index.d.ts +28 -3
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* WebSocket/STOMP 연결 관리 (Chat, WebRTC 공유)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { isTerminalAuthError } from '../auth/AuthSession.js';
|
|
6
7
|
import EventEmitter from '../utils/EventEmitter.js';
|
|
7
8
|
import Logger from '../utils/Logger.js';
|
|
8
9
|
import { ConnectionState, ErrorTypes, WebSocketPaths, DefaultConfig, LogLevel } from '../constants.js';
|
|
@@ -289,6 +290,36 @@ class ConnectionManager extends EventEmitter {
|
|
|
289
290
|
const wsUrl = this._getWebSocketUrl();
|
|
290
291
|
|
|
291
292
|
const config = {
|
|
293
|
+
beforeConnect: async (client) => {
|
|
294
|
+
if (!this.getAccessToken) return;
|
|
295
|
+
let attempt = 0;
|
|
296
|
+
while (!this._intentionalDisconnect && (!client || client.active)) {
|
|
297
|
+
try {
|
|
298
|
+
const token = await this.getAccessToken({ forceRefresh: this._authRejected === true });
|
|
299
|
+
if (this._intentionalDisconnect || (client && !client.active)) return;
|
|
300
|
+
this.updateToken(token);
|
|
301
|
+
this._authRejected = false;
|
|
302
|
+
return;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
this.emit('error', { type: ErrorTypes.CONNECTION_FAILED, message: error.message, error });
|
|
305
|
+
if (isTerminalAuthError(error)) {
|
|
306
|
+
this._setState(ConnectionState.ERROR);
|
|
307
|
+
this._connectReject?.(error);
|
|
308
|
+
this._connectResolve = this._connectReject = null;
|
|
309
|
+
await (client || this.stompClient)?.deactivate();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// beforeConnect의 예외에는 STOMP 소켓 종료 백오프가 적용되지 않는다.
|
|
313
|
+
const wait = Math.min(30000, 1000 * 2 ** Math.min(attempt++, 5)) * (0.8 + Math.random() * 0.4);
|
|
314
|
+
await new Promise(resolve => {
|
|
315
|
+
this._cancelAuthRetry = () => { clearTimeout(this._authRetryTimer); resolve(); };
|
|
316
|
+
this._authRetryTimer = setTimeout(resolve, wait);
|
|
317
|
+
this._authRetryTimer.unref?.();
|
|
318
|
+
});
|
|
319
|
+
this._cancelAuthRetry = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
292
323
|
connectHeaders: {
|
|
293
324
|
'Authorization': this.jwtToken.startsWith('Bearer ')
|
|
294
325
|
? this.jwtToken
|
|
@@ -369,6 +400,7 @@ class ConnectionManager extends EventEmitter {
|
|
|
369
400
|
* @private
|
|
370
401
|
*/
|
|
371
402
|
_onConnect(frame) {
|
|
403
|
+
this._authenticatedToken = this.jwtToken;
|
|
372
404
|
// 재연결 여부 판정은 카운터 리셋 전에 — 갭 복구(S4)가 이 신호로 동기화를 건다.
|
|
373
405
|
const attempts = this.reconnectAttempts;
|
|
374
406
|
const isReconnect = attempts > 0 || this._hasConnectedBefore === true;
|
|
@@ -413,6 +445,10 @@ class ConnectionManager extends EventEmitter {
|
|
|
413
445
|
* @private
|
|
414
446
|
*/
|
|
415
447
|
_onStompError(frame) {
|
|
448
|
+
try {
|
|
449
|
+
const payload = JSON.parse(frame.body || frame.headers?.message || '{}');
|
|
450
|
+
this._authRejected = ['ACCESS_UNAUTHORIZED', 'SESSION_REVOKED', 'TOKEN_INVALID'].includes(payload.code);
|
|
451
|
+
} catch { /* Non-authentication STOMP errors keep the normal reconnect policy. */ }
|
|
416
452
|
this.logger.error('STOMP error:', frame);
|
|
417
453
|
this._setState(ConnectionState.ERROR);
|
|
418
454
|
|
|
@@ -439,6 +475,8 @@ class ConnectionManager extends EventEmitter {
|
|
|
439
475
|
* @private
|
|
440
476
|
*/
|
|
441
477
|
_onWebSocketClose(event) {
|
|
478
|
+
if (this.stompClient && event?.code === 1012) this.stompClient.reconnectDelay = this.reconnectDelay * (0.8 + Math.random() * 0.4);
|
|
479
|
+
if (event?.code === 4001) this._authRejected = true;
|
|
442
480
|
this.logger.warn('WebSocket closed:', event);
|
|
443
481
|
|
|
444
482
|
// forceReconnect 의 의도적 teardown 이면 재연결 시도로 집계하지 않는다 —
|
|
@@ -657,6 +695,10 @@ class ConnectionManager extends EventEmitter {
|
|
|
657
695
|
async disconnect() {
|
|
658
696
|
// 명시적 해제 — 포그라운드 복귀가 이걸 되살리면 안 된다.
|
|
659
697
|
this._intentionalDisconnect = true;
|
|
698
|
+
this._connectRequested = false;
|
|
699
|
+
this._cancelAuthRetry?.();
|
|
700
|
+
this._connectReject?.(Object.assign(new Error('Connection cancelled'), { code: 'AUTH_OPERATION_CANCELLED' }));
|
|
701
|
+
this._connectResolve = this._connectReject = null;
|
|
660
702
|
this._drainPendingSubscriptions('Disconnected before subscription completed');
|
|
661
703
|
|
|
662
704
|
if (this.stompClient) {
|
package/src/push/PushManager.js
CHANGED
|
@@ -537,6 +537,7 @@ class PushManager {
|
|
|
537
537
|
async _registerTokenToServer(token) {
|
|
538
538
|
try {
|
|
539
539
|
const deviceId = await this._getDeviceId();
|
|
540
|
+
this.apiClient?.authProvider?.setDeviceId(deviceId);
|
|
540
541
|
await this.apiClient.post('/api/v1/push/devices', {
|
|
541
542
|
deviceToken: token,
|
|
542
543
|
deviceType: 'WEB',
|
|
@@ -623,6 +624,7 @@ class PushManager {
|
|
|
623
624
|
}
|
|
624
625
|
|
|
625
626
|
this._deviceIdCache = deviceId;
|
|
627
|
+
this.apiClient?.authProvider?.setDeviceId(deviceId);
|
|
626
628
|
return deviceId;
|
|
627
629
|
} catch (error) {
|
|
628
630
|
// IndexedDB 장애 (open / read / transaction throw 등) 시 fallback.
|
package/src/talkflow/session.js
CHANGED
|
@@ -27,6 +27,10 @@ export function initializeSubClients(client) {
|
|
|
27
27
|
resumeOnVisibility: client.options.resumeOnVisibility,
|
|
28
28
|
logLevel: client.options.logLevel
|
|
29
29
|
});
|
|
30
|
+
client.connectionManager.getAccessToken = async ({ forceRefresh = false } = {}) => {
|
|
31
|
+
if (!client.authSession) return client.options.jwtToken;
|
|
32
|
+
return client.authSession.getAccessToken({ forceRefresh, rejectedToken: client.connectionManager?.jwtToken });
|
|
33
|
+
};
|
|
30
34
|
|
|
31
35
|
client._chat = new ChatClient({
|
|
32
36
|
connectionManager: client.connectionManager,
|
|
@@ -60,6 +64,14 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
60
64
|
await this.setToken(jwt);
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
if (this.authSession) {
|
|
68
|
+
await this._authReady;
|
|
69
|
+
this._authConnectionRequested = true;
|
|
70
|
+
const token = await this.authSession.getAccessToken();
|
|
71
|
+
await this.setToken(token);
|
|
72
|
+
this.authSession.schedule();
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
if (!this.options.jwtToken) {
|
|
64
76
|
throw new Error(
|
|
65
77
|
'JWT token is required. Sign in with TalkFlowClient.loginUser({ serverUrl, ' +
|
|
@@ -68,7 +80,7 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
68
80
|
);
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
if (isJWTExpired(this.options.jwtToken)) {
|
|
83
|
+
if (!this.authSession && isJWTExpired(this.options.jwtToken)) {
|
|
72
84
|
throw new Error('JWT token has expired. Please update the token before connecting.');
|
|
73
85
|
}
|
|
74
86
|
|
|
@@ -98,6 +110,8 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
98
110
|
};
|
|
99
111
|
|
|
100
112
|
TalkFlowClient.prototype.disconnect = async function disconnect() {
|
|
113
|
+
this._authConnectionRequested = false;
|
|
114
|
+
this.authSession?.schedule();
|
|
101
115
|
if (!this.connectionManager) {
|
|
102
116
|
return;
|
|
103
117
|
}
|
|
@@ -120,6 +134,7 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
120
134
|
};
|
|
121
135
|
|
|
122
136
|
TalkFlowClient.prototype.logout = async function logout() {
|
|
137
|
+
if (this.authSession) return this.authSession.logout();
|
|
123
138
|
try {
|
|
124
139
|
await this.apiClient.post('/v1/auth/signout');
|
|
125
140
|
this.logger.info('Logged out from server');
|
|
@@ -339,6 +354,8 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
339
354
|
|
|
340
355
|
if (needReinitialize && this.connectionManager) {
|
|
341
356
|
await this.disconnect();
|
|
357
|
+
this._chat?.destroy();
|
|
358
|
+
this._webrtc?.destroy();
|
|
342
359
|
// destroy() 로 버려야 lifecycle listener(visibilitychange/pageshow/online)가 해제된다.
|
|
343
360
|
// disconnect() 만 하고 참조를 끊으면 죽은 매니저가 window 에 매달린 채 남아,
|
|
344
361
|
// 사용자 전환이 잦은 앱에서 복귀 이벤트마다 유령 매니저가 함께 깨어난다.
|
|
@@ -346,6 +363,7 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
346
363
|
if (this.pushManager) {
|
|
347
364
|
this.pushManager.reset();
|
|
348
365
|
this._pushEnablePromise = null;
|
|
366
|
+
this._pushManager = null;
|
|
349
367
|
}
|
|
350
368
|
this.connectionManager = null;
|
|
351
369
|
this._chat = null;
|
|
@@ -359,6 +377,7 @@ export function applySessionMethods(TalkFlowClient) {
|
|
|
359
377
|
}
|
|
360
378
|
|
|
361
379
|
this.logger.info('JWT token set', { userId });
|
|
380
|
+
this.authSession?.replaceAccessToken(token);
|
|
362
381
|
this.emit('tokenSet', { userId });
|
|
363
382
|
};
|
|
364
383
|
|
package/src/utils/ApiClient.js
CHANGED
|
@@ -67,6 +67,26 @@ class ApiClient {
|
|
|
67
67
|
* @returns {Promise<Object>}
|
|
68
68
|
*/
|
|
69
69
|
async request(method, path, options = {}) {
|
|
70
|
+
if (!this.authProvider || options.skipAuth) return this._request(method, path, options);
|
|
71
|
+
this.jwtToken = await this.authProvider.getAccessToken();
|
|
72
|
+
const sentToken = this.jwtToken;
|
|
73
|
+
const generation = this.authProvider.generation;
|
|
74
|
+
try { return await this._request(method, path, options); }
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.status === 401 && ['ACCESS_UNAUTHORIZED', 'TOKEN_INVALID'].includes(error.response?.code)) {
|
|
77
|
+
if (generation !== this.authProvider.generation) throw error;
|
|
78
|
+
this.jwtToken = await this.authProvider.getAccessToken({ forceRefresh: true, rejectedToken: sentToken });
|
|
79
|
+
const retryToken = this.jwtToken;
|
|
80
|
+
const retryGeneration = this.authProvider.generation;
|
|
81
|
+
try { return await this._request(method, path, options); }
|
|
82
|
+
catch (retryError) { await this.authProvider.authenticationFailed(retryError, { rejectedToken: retryToken, generation: retryGeneration }); throw retryError; }
|
|
83
|
+
}
|
|
84
|
+
await this.authProvider.authenticationFailed(error, { rejectedToken: sentToken, generation });
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async _request(method, path, options = {}) {
|
|
70
90
|
const { body, params, headers } = options;
|
|
71
91
|
|
|
72
92
|
// URL 빌드
|
|
@@ -90,11 +110,12 @@ class ApiClient {
|
|
|
90
110
|
|
|
91
111
|
let response;
|
|
92
112
|
try {
|
|
93
|
-
this.logger.debug(`${method} ${url}
|
|
113
|
+
this.logger.debug(`${method} ${url}`);
|
|
94
114
|
|
|
95
115
|
response = await fetch(url, {
|
|
96
116
|
method,
|
|
97
|
-
headers:
|
|
117
|
+
headers: { ...this._getHeaders(), ...headers },
|
|
118
|
+
credentials: this.authProvider ? 'omit' : 'same-origin',
|
|
98
119
|
body: body ? JSON.stringify(body) : undefined,
|
|
99
120
|
signal: controller.signal
|
|
100
121
|
});
|
|
@@ -133,7 +154,7 @@ class ApiClient {
|
|
|
133
154
|
throw error;
|
|
134
155
|
}
|
|
135
156
|
|
|
136
|
-
this.logger.debug(`Response ${response.status}
|
|
157
|
+
this.logger.debug(`Response ${response.status}`);
|
|
137
158
|
return data;
|
|
138
159
|
}
|
|
139
160
|
|
|
@@ -207,7 +228,27 @@ class ApiClient {
|
|
|
207
228
|
* @param {number} [options.timeout=600000] - 타임아웃 (ms). 기본 10분.
|
|
208
229
|
* @returns {Promise<Object>} 서버 응답 JSON (SuccessResponse 래퍼 포함)
|
|
209
230
|
*/
|
|
210
|
-
upload(path, file, options = {}) {
|
|
231
|
+
async upload(path, file, options = {}) {
|
|
232
|
+
if (!this.authProvider) return this._upload(path, file, options);
|
|
233
|
+
this.jwtToken = await this.authProvider.getAccessToken();
|
|
234
|
+
const sentToken = this.jwtToken;
|
|
235
|
+
const generation = this.authProvider.generation;
|
|
236
|
+
try { return await this._upload(path, file, options); }
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (error.status === 401 && ['ACCESS_UNAUTHORIZED', 'TOKEN_INVALID'].includes(error.response?.code)) {
|
|
239
|
+
if (generation !== this.authProvider.generation) throw error;
|
|
240
|
+
this.jwtToken = await this.authProvider.getAccessToken({ forceRefresh: true, rejectedToken: sentToken });
|
|
241
|
+
const retryToken = this.jwtToken;
|
|
242
|
+
const retryGeneration = this.authProvider.generation;
|
|
243
|
+
try { return await this._upload(path, file, options); }
|
|
244
|
+
catch (retryError) { await this.authProvider.authenticationFailed(retryError, { rejectedToken: retryToken, generation: retryGeneration }); throw retryError; }
|
|
245
|
+
}
|
|
246
|
+
await this.authProvider.authenticationFailed(error, { rejectedToken: sentToken, generation });
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_upload(path, file, options = {}) {
|
|
211
252
|
const {
|
|
212
253
|
fieldName = 'file',
|
|
213
254
|
onProgress,
|
package/src/utils/jwtUtils.js
CHANGED
|
@@ -45,7 +45,7 @@ function base64UrlDecode(str) {
|
|
|
45
45
|
* @throws {Error}
|
|
46
46
|
*/
|
|
47
47
|
export function validateAndParseJWT(jwtToken, options = {}) {
|
|
48
|
-
const { bufferSeconds = 30, validateExpiry = true } = options;
|
|
48
|
+
const { bufferSeconds = 30, validateExpiry = true, validateIssuedAt = false } = options;
|
|
49
49
|
|
|
50
50
|
if (!jwtToken || typeof jwtToken !== 'string') {
|
|
51
51
|
const error = new Error('JWT token is required');
|
|
@@ -89,7 +89,7 @@ export function validateAndParseJWT(jwtToken, options = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
if (payload.iat) {
|
|
92
|
+
if (validateIssuedAt && payload.iat) {
|
|
93
93
|
const issuedAt = payload.iat * 1000;
|
|
94
94
|
const clockSkew = 60 * 1000;
|
|
95
95
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import TalkFlowClient, { AuthState, LoginResult } from '../index';
|
|
2
|
+
const client = new TalkFlowClient({ serverUrl: 'https://api.example.com', sessionMode: 'refresh' });
|
|
3
|
+
const restored: Promise<LoginResult | null> = client.restoreSession();
|
|
4
|
+
const state: AuthState = client.getAuthState();
|
|
5
|
+
const signedIn: boolean = client.isLoggedIn();
|
|
6
|
+
const access: Promise<string> = client.getAccessToken({ forceRefresh: true, rejectedToken: 'old' });
|
|
7
|
+
client.on('authStateChanged', ({ state }) => { const typed: AuthState = state; void typed; });
|
|
8
|
+
void [restored, state, signedIn, access];
|
package/types/index.d.ts
CHANGED
|
@@ -1036,8 +1036,14 @@ export interface LoginMember {
|
|
|
1036
1036
|
* 회사 선택 단계가 없다 — 로그인 키가 전역 유일이라 이메일 하나로 회원과 회사가 함께 정해진다.
|
|
1037
1037
|
*/
|
|
1038
1038
|
export interface LoginResult {
|
|
1039
|
+
/** SDK가 저장·회전하는 갱신 자격. 직접 API를 쓰는 경우에만 응답에서 사용한다. */
|
|
1040
|
+
refreshToken?: string;
|
|
1041
|
+
/** 서버 현재 시각 및 refresh 만료 시각, Unix epoch 밀리초. */
|
|
1042
|
+
serverTime?: number;
|
|
1043
|
+
refreshExpiresAt?: number;
|
|
1039
1044
|
accessToken: string;
|
|
1040
1045
|
tokenType: string;
|
|
1046
|
+
/** Access 만료 시각, Unix epoch 초(남은 시간 아님). */
|
|
1041
1047
|
expiresIn?: number;
|
|
1042
1048
|
projectId: string;
|
|
1043
1049
|
/** 회사의 활성 ck- 키. SDK 초기화의 `apiKey` 로 그대로 넣는다. */
|
|
@@ -1464,11 +1470,16 @@ export interface AssistantProgressSnapshot extends AssistantRoomProgressEvent {
|
|
|
1464
1470
|
// Options — constructor & method
|
|
1465
1471
|
// ============================================================================
|
|
1466
1472
|
|
|
1473
|
+
export type AuthState = 'checking' | 'authenticated' | 'unauthenticated' | 'unavailable';
|
|
1474
|
+
export interface AccessTokenOptions { forceRefresh?: boolean; rejectedToken?: string; }
|
|
1475
|
+
|
|
1467
1476
|
export interface TalkFlowClientOptions {
|
|
1468
1477
|
/** Client API 키 (브라우저 노출 안전). */
|
|
1469
|
-
apiKey
|
|
1478
|
+
apiKey?: string;
|
|
1470
1479
|
/** 프로젝트 ID. */
|
|
1471
|
-
projectId
|
|
1480
|
+
projectId?: string;
|
|
1481
|
+
/** refresh: 키 없이 생성 후 restoreSession/loginUser 가능. 기존 JWT만 쓰면 legacy. */
|
|
1482
|
+
sessionMode?: 'refresh' | 'legacy';
|
|
1472
1483
|
/** JWT 토큰 — 고객사 backend 에서 발급 받아 전달. */
|
|
1473
1484
|
jwtToken?: string;
|
|
1474
1485
|
/** 환경. 기본 'production'. */
|
|
@@ -1867,6 +1878,8 @@ export interface TalkFlowClientEvents {
|
|
|
1867
1878
|
connectionError: unknown;
|
|
1868
1879
|
tokenSet: { userId: string };
|
|
1869
1880
|
loggedOut: void;
|
|
1881
|
+
authStateChanged: { state: AuthState; previous: AuthState; error?: unknown };
|
|
1882
|
+
authError: unknown;
|
|
1870
1883
|
|
|
1871
1884
|
// Chat (re-emit from ChatClient with prefixed names)
|
|
1872
1885
|
chatMessage: ChatMessageReceivedPayload;
|
|
@@ -2432,6 +2445,18 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2432
2445
|
/** 회원 로그인 (인스턴스) — 재로그인·계정 전환용. 성공 시 토큰을 자동 설정한다. */
|
|
2433
2446
|
loginUser(credentials: { email: string; password: string }): Promise<LoginResult>;
|
|
2434
2447
|
|
|
2448
|
+
/** localStorage의 refresh로 복구. 세션이 없으면 null, 네트워크/서버 장애는 reject. */
|
|
2449
|
+
static restoreSession(options: { serverUrl: string }): Promise<LoginResult | null>;
|
|
2450
|
+
restoreSession(): Promise<LoginResult | null>;
|
|
2451
|
+
/** 캐시된 UI 상태. 호출 자체는 통신하지 않으며 서버 권한 판정은 요청 시 수행된다. */
|
|
2452
|
+
getAuthState(): AuthState;
|
|
2453
|
+
isLoggedIn(): boolean;
|
|
2454
|
+
/** 유효한 메모리 토큰 반환. 필요할 때만 갱신하며 동시 요청은 하나로 합친다. */
|
|
2455
|
+
getAccessToken(options?: AccessTokenOptions): Promise<string>;
|
|
2456
|
+
/** 관리/캘린더 등 SDK 밖 호출용. 토큰 준비와 인증 오류 1회 재시도를 공통 적용한다. */
|
|
2457
|
+
request<T = unknown>(method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', path: string,
|
|
2458
|
+
options?: { body?: unknown; params?: Record<string, unknown>; headers?: Record<string, string> }): Promise<T>;
|
|
2459
|
+
|
|
2435
2460
|
/** 내 정보 조회. */
|
|
2436
2461
|
getMyProfile(): Promise<MyProfile>;
|
|
2437
2462
|
|
|
@@ -2448,7 +2473,7 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2448
2473
|
timeZone?: string;
|
|
2449
2474
|
}): Promise<MyProfile>;
|
|
2450
2475
|
|
|
2451
|
-
/** 비밀번호 변경 —
|
|
2476
|
+
/** 비밀번호 변경 — 기존 세션을 모두 폐기하고 이 브라우저에 새 세션을 자동 채택한다. */
|
|
2452
2477
|
changeMyPassword(params: { currentPassword: string; newPassword: string }): Promise<unknown>;
|
|
2453
2478
|
|
|
2454
2479
|
// Connection lifecycle
|