@vibexnpm/talkx 8.6.0 → 8.7.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 +42 -11
- 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 +20 -9
- package/src/auth/AuthSession.js +27 -2
- package/src/chat/ChatClient.js +2 -2
- package/src/index.js +1 -0
- package/types/__typetests__/authSession.types.ts +23 -1
- package/types/index.d.ts +42 -11
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({
|
|
@@ -310,21 +311,28 @@ class TalkFlowClient extends EventEmitter {
|
|
|
310
311
|
* <p>Server Key 도 고객사 BFF 도 필요 없다. 응답의
|
|
311
312
|
* {@code clientKey}/{@code projectId}/{@code accessToken} 이 그대로 SDK 초기화 입력이다.</p>
|
|
312
313
|
*
|
|
313
|
-
* <p><b>회사
|
|
314
|
-
*
|
|
314
|
+
* <p><b>회사 선택</b>: 같은 이메일이 여러 회사에 계정을 가질 수 있다(다른 회사의 게스트 등). 비밀번호가 맞는 계정이
|
|
315
|
+
* 둘 이상이면 토큰 없이 {@code {workspaceSelectionRequired: true, workspaces: [{projectId, companyName, companyRole}]}}
|
|
316
|
+
* 를 돌려준다 — 저장된 세션은 그대로다. 고른 {@code projectId} 를 실어 같은 이메일·비밀번호로 다시 부른다.
|
|
317
|
+
* 계정이 하나면 지금처럼 바로 로그인된다.</p>
|
|
315
318
|
*
|
|
316
319
|
* @param {Object} options
|
|
317
320
|
* @param {string} options.serverUrl - 채팅 서버 base URL
|
|
318
321
|
* @param {string} options.email
|
|
319
322
|
* @param {string} options.password
|
|
320
|
-
* @
|
|
323
|
+
* @param {string} [options.projectId] - 회사 선택 응답에서 고른 회사
|
|
324
|
+
* @returns {Promise<Object>} `{accessToken, tokenType, expiresIn, projectId, clientKey, user}` 또는 회사 선택 결과
|
|
321
325
|
*
|
|
322
326
|
* @example
|
|
323
|
-
*
|
|
327
|
+
* let login = await TalkFlowClient.loginUser({
|
|
324
328
|
* serverUrl: 'https://chat.apiorbit.net',
|
|
325
329
|
* email: 'dan@example.com',
|
|
326
330
|
* password: '********'
|
|
327
331
|
* });
|
|
332
|
+
* if (login.workspaceSelectionRequired) {
|
|
333
|
+
* const picked = await showWorkspacePicker(login.workspaces); // 화면에서 고른 회사
|
|
334
|
+
* login = await TalkFlowClient.loginUser({ serverUrl, email, password, projectId: picked.projectId });
|
|
335
|
+
* }
|
|
328
336
|
*
|
|
329
337
|
* const client = new TalkFlowClient({
|
|
330
338
|
* apiKey: login.clientKey,
|
|
@@ -333,13 +341,13 @@ class TalkFlowClient extends EventEmitter {
|
|
|
333
341
|
* });
|
|
334
342
|
* await client.connect();
|
|
335
343
|
*/
|
|
336
|
-
static async loginUser({ serverUrl, email, password }) {
|
|
344
|
+
static async loginUser({ serverUrl, email, password, projectId }) {
|
|
337
345
|
if (!serverUrl) throw new Error('serverUrl is required');
|
|
338
346
|
if (!email) throw new Error('email is required');
|
|
339
347
|
if (!password) throw new Error('password is required');
|
|
340
348
|
|
|
341
349
|
// 로그인은 키를 «받기 위해» 부르는 경로라 X-API-KEY/X-PROJECT-ID 가 없다(서버에서 필터 제외).
|
|
342
|
-
return AuthSession.forServer(serverUrl).login({ email, password });
|
|
350
|
+
return AuthSession.forServer(serverUrl).login({ email, password, projectId });
|
|
343
351
|
}
|
|
344
352
|
|
|
345
353
|
/**
|
|
@@ -348,16 +356,19 @@ class TalkFlowClient extends EventEmitter {
|
|
|
348
356
|
* <p>이미 클라이언트가 있는 상태의 재로그인·계정 전환용이다. 최초 진입은 클라이언트 생성 전에
|
|
349
357
|
* 부르는 정적 {@link TalkFlowClient.loginUser} 를 쓴다 — 그쪽이 {@code clientKey} 까지 준다.</p>
|
|
350
358
|
*
|
|
359
|
+
* <p>회사 선택 응답이면 토큰을 설정하지 않고 그 결과를 돌려준다 — 정적 {@link TalkFlowClient.loginUser} 와 같다.</p>
|
|
360
|
+
*
|
|
351
361
|
* @param {Object} credentials
|
|
352
362
|
* @param {string} credentials.email
|
|
353
363
|
* @param {string} credentials.password
|
|
354
|
-
* @
|
|
364
|
+
* @param {string} [credentials.projectId] - 회사 선택 응답에서 고른 회사
|
|
365
|
+
* @returns {Promise<Object>} 로그인 응답 또는 회사 선택 결과
|
|
355
366
|
*/
|
|
356
|
-
async loginUser({ email, password }) {
|
|
367
|
+
async loginUser({ email, password, projectId }) {
|
|
357
368
|
if (!email || !password) throw new Error('email and password are required');
|
|
358
369
|
this._enableRefreshSession();
|
|
359
370
|
await this._authReady;
|
|
360
|
-
return this.authSession.login({ email, password });
|
|
371
|
+
return this.authSession.login({ email, password, projectId });
|
|
361
372
|
}
|
|
362
373
|
|
|
363
374
|
/**
|
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
|
@@ -2511,8 +2511,8 @@ class ChatClient extends EventEmitter {
|
|
|
2511
2511
|
* @param {string} roomId - 채팅방 ID
|
|
2512
2512
|
* @returns {Promise<void>}
|
|
2513
2513
|
*
|
|
2514
|
-
* @fires memberJoined - { roomId, members: [{ userId, nickname, profileUrl }], participantCount, timestamp }
|
|
2515
|
-
* @fires memberLeft - { roomId, members: [{ userId, nickname, profileUrl }], participantCount, timestamp }
|
|
2514
|
+
* @fires memberJoined - { roomId, members: [{ userId, nickname, profileUrl, companyRole? }], participantCount, timestamp }
|
|
2515
|
+
* @fires memberLeft - { roomId, members: [{ userId, nickname, profileUrl, companyRole? }], participantCount, timestamp }
|
|
2516
2516
|
*/
|
|
2517
2517
|
async subscribeRoom(roomId) {
|
|
2518
2518
|
if (this.subscribedRooms.has(roomId)) {
|
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',
|
|
@@ -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];
|
package/types/index.d.ts
CHANGED
|
@@ -1044,14 +1044,20 @@ export interface DecisionResponse {
|
|
|
1044
1044
|
optionId: string;
|
|
1045
1045
|
}
|
|
1046
1046
|
|
|
1047
|
+
/**
|
|
1048
|
+
* 회사 역할. `GUEST` 는 회사 밖에서 초대받아 그 회사 계정을 가진 사람이다 — 초대받은 방에서 채팅만 하고 PM·AI 버튼·조직도·
|
|
1049
|
+
* 방 만들기·초대 등은 서버가 막는다(403 `GUEST_FORBIDDEN`). 화면 노출 판단용이며 인가 근거는 아니다.
|
|
1050
|
+
*/
|
|
1051
|
+
export type CompanyRole = 'PARTNER' | 'MEMBER' | 'GUEST';
|
|
1052
|
+
|
|
1047
1053
|
/** 로그인한 회원. `userId` 는 메시지 발신자 식별과 같은 값이다. */
|
|
1048
1054
|
export interface LoginMember {
|
|
1049
1055
|
userId: string;
|
|
1050
1056
|
email?: string | null;
|
|
1051
1057
|
nickname?: string | null;
|
|
1052
1058
|
profileUrl?: string | null;
|
|
1053
|
-
/** 회사 역할 — 클라이언트가
|
|
1054
|
-
companyRole:
|
|
1059
|
+
/** 회사 역할 — 클라이언트가 파트너·게스트 화면 노출을 결정한다. 인가 근거는 아니다(서버가 재확인). */
|
|
1060
|
+
companyRole: CompanyRole;
|
|
1055
1061
|
preferredLanguage?: string | null;
|
|
1056
1062
|
/**
|
|
1057
1063
|
* 초기 비밀번호를 아직 안 바꿨는가 — `true` 면 **비밀번호 변경 화면으로 보내라**.
|
|
@@ -1063,11 +1069,11 @@ export interface LoginMember {
|
|
|
1063
1069
|
}
|
|
1064
1070
|
|
|
1065
1071
|
/**
|
|
1066
|
-
* `TalkFlowClient.loginUser` 응답.
|
|
1067
|
-
*
|
|
1068
|
-
* 회사 선택 단계가 없다 — 로그인 키가 전역 유일이라 이메일 하나로 회원과 회사가 함께 정해진다.
|
|
1072
|
+
* `TalkFlowClient.loginUser` 의 로그인 성공 응답. 회사 선택이 필요하면 대신 {@link WorkspaceSelection} 이 온다 —
|
|
1073
|
+
* `workspaceSelectionRequired` 로 가른다.
|
|
1069
1074
|
*/
|
|
1070
1075
|
export interface LoginResult {
|
|
1076
|
+
workspaceSelectionRequired?: false;
|
|
1071
1077
|
/** SDK가 저장·회전하는 갱신 자격. 직접 API를 쓰는 경우에만 응답에서 사용한다. */
|
|
1072
1078
|
refreshToken?: string;
|
|
1073
1079
|
/** 서버 현재 시각 및 refresh 만료 시각, Unix epoch 밀리초. */
|
|
@@ -1083,6 +1089,23 @@ export interface LoginResult {
|
|
|
1083
1089
|
user: LoginMember;
|
|
1084
1090
|
}
|
|
1085
1091
|
|
|
1092
|
+
/** 로그인 회사 선택지 하나. */
|
|
1093
|
+
export interface Workspace {
|
|
1094
|
+
projectId: string;
|
|
1095
|
+
companyName: string | null;
|
|
1096
|
+
/** 그 회사에서의 역할 — 선택 화면에 «게스트» 표시를 붙이는 데 쓴다. */
|
|
1097
|
+
companyRole: CompanyRole;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* 로그인 회사 선택 결과 — 같은 이메일의 여러 회사 계정에서 비밀번호가 둘 이상 맞았다. **토큰이 없고 저장된 세션은 그대로다.**
|
|
1102
|
+
* 고른 `projectId` 를 실어 같은 이메일·비밀번호로 `loginUser` 를 다시 부른다.
|
|
1103
|
+
*/
|
|
1104
|
+
export interface WorkspaceSelection {
|
|
1105
|
+
workspaceSelectionRequired: true;
|
|
1106
|
+
workspaces: Workspace[];
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1086
1109
|
/** 회사 조직도의 회원 — 초대 UI 에 필요한 것만. */
|
|
1087
1110
|
export interface DirectoryMember {
|
|
1088
1111
|
userId: string;
|
|
@@ -1124,7 +1147,7 @@ export interface MyProfile {
|
|
|
1124
1147
|
phone?: string | null;
|
|
1125
1148
|
profileUrl?: string | null;
|
|
1126
1149
|
/** 회사 역할 — 방 역할(ParticipantResponse.role)과 다른 축이다. */
|
|
1127
|
-
companyRole:
|
|
1150
|
+
companyRole: CompanyRole;
|
|
1128
1151
|
orgNodeId?: string | null;
|
|
1129
1152
|
orgName?: string | null;
|
|
1130
1153
|
preferredLanguage?: string | null;
|
|
@@ -1170,6 +1193,10 @@ export interface ParticipantResponse {
|
|
|
1170
1193
|
role: 'OWNER' | 'ADMIN' | 'MEMBER' | 'ASSISTANT';
|
|
1171
1194
|
/** 참가 시각(epoch millis). */
|
|
1172
1195
|
joinedAt: number;
|
|
1196
|
+
/**
|
|
1197
|
+
* 회사 역할 — 게스트 배지(`GUEST`)용. 방 역할({@link role})과 다른 축이다. PM(ASSISTANT)·정보를 못 찾은 참여자는 없다.
|
|
1198
|
+
*/
|
|
1199
|
+
companyRole?: CompanyRole;
|
|
1173
1200
|
}
|
|
1174
1201
|
|
|
1175
1202
|
/** 채팅방 last message preview. */
|
|
@@ -1280,6 +1307,8 @@ export interface RoomListMemberInfo {
|
|
|
1280
1307
|
userId: string;
|
|
1281
1308
|
nickname?: string | null;
|
|
1282
1309
|
profileUrl?: string | null;
|
|
1310
|
+
/** 회사 역할 — 방금 들어온 게스트에도 다시 읽지 않고 배지를 그린다. PM·역할을 모르는 경로는 없다. */
|
|
1311
|
+
companyRole?: CompanyRole;
|
|
1283
1312
|
}
|
|
1284
1313
|
|
|
1285
1314
|
export interface RoomListUpdateEvent {
|
|
@@ -1934,7 +1963,7 @@ export interface ChatMessageReceivedPayload {
|
|
|
1934
1963
|
|
|
1935
1964
|
export interface MemberChangePayload {
|
|
1936
1965
|
roomId: string;
|
|
1937
|
-
members: Array<{ userId: string; nickname?: string; profileUrl?: string | null }>;
|
|
1966
|
+
members: Array<{ userId: string; nickname?: string; profileUrl?: string | null; companyRole?: CompanyRole }>;
|
|
1938
1967
|
participantCount: number;
|
|
1939
1968
|
timestamp?: number;
|
|
1940
1969
|
}
|
|
@@ -2602,16 +2631,18 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
|
|
|
2602
2631
|
/**
|
|
2603
2632
|
* 회원 로그인 — 클라이언트 생성 **전에** 호출한다. Server Key 도 고객사 BFF 도 필요 없다.
|
|
2604
2633
|
* 응답의 `clientKey`/`projectId`/`accessToken` 이 그대로 SDK 초기화 입력이다.
|
|
2605
|
-
* 회사
|
|
2634
|
+
* 같은 이메일로 비밀번호가 맞는 회사 계정이 둘 이상이면 {@link WorkspaceSelection} 이 온다 — 고른 `projectId` 로 다시 부른다.
|
|
2606
2635
|
*/
|
|
2607
2636
|
static loginUser(options: {
|
|
2608
2637
|
serverUrl: string;
|
|
2609
2638
|
email: string;
|
|
2610
2639
|
password: string;
|
|
2611
|
-
|
|
2640
|
+
/** 회사 선택 응답에서 고른 회사. */
|
|
2641
|
+
projectId?: string;
|
|
2642
|
+
}): Promise<LoginResult | WorkspaceSelection>;
|
|
2612
2643
|
|
|
2613
|
-
/** 회원 로그인 (인스턴스) — 재로그인·계정 전환용. 성공 시 토큰을 자동 설정한다. */
|
|
2614
|
-
loginUser(credentials: { email: string; password: string }): Promise<LoginResult>;
|
|
2644
|
+
/** 회원 로그인 (인스턴스) — 재로그인·계정 전환용. 성공 시 토큰을 자동 설정한다. 회사 선택 결과면 토큰을 설정하지 않는다. */
|
|
2645
|
+
loginUser(credentials: { email: string; password: string; projectId?: string }): Promise<LoginResult | WorkspaceSelection>;
|
|
2615
2646
|
|
|
2616
2647
|
/** localStorage의 refresh로 복구. 세션이 없으면 null, 네트워크/서버 장애는 reject. */
|
|
2617
2648
|
static restoreSession(options: { serverUrl: string }): Promise<LoginResult | null>;
|