@ethora/chat-component 26.6.6 → 26.7.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/dist/{VideoCallOverlay-gE1OvEiT.js → VideoCallOverlay-Q6-aYX0q.js} +2 -2
- package/dist/{VideoCallOverlay-gE1OvEiT.js.map → VideoCallOverlay-Q6-aYX0q.js.map} +1 -1
- package/dist/{main-Doq70DIE.js → main-JguHZ6Rn.js} +14124 -13884
- package/dist/{main-Doq70DIE.js.map → main-JguHZ6Rn.js.map} +1 -1
- package/dist/main.d.ts +2 -0
- package/dist/main.js +20 -16
- package/dist/networking/apiClient.d.ts +15 -7
- package/dist/networking/authRefresh.d.ts +108 -0
- package/dist/networking/xmppClient.d.ts +49 -0
- package/dist/roomStore/chatSettingsSlice.d.ts +1 -0
- package/dist/types/models/config.model.d.ts +15 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/networking/helpers/refreshChecker.d.ts +0 -1
package/dist/main.d.ts
CHANGED
|
@@ -14,3 +14,5 @@ export { applyTypography, clearTypography } from './helpers/applyTypography';
|
|
|
14
14
|
export { useTypography } from './hooks/useTypography';
|
|
15
15
|
export type { TypographyConfig, FontFaceSource, } from './types/models/config.model';
|
|
16
16
|
export { ChatInputTestIds, MessageBubbleTestIds, RoomListTestIds, AuthTestIds, } from './testIds';
|
|
17
|
+
export { refreshAuthTokens, refreshAuthTokensQuietly, RefreshFatalError, isRefreshFatalError, } from './networking/authRefresh';
|
|
18
|
+
export type { RefreshErrorCode, RefreshResult, RefreshOptions, } from './networking/authRefresh';
|
package/dist/main.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import { m as
|
|
1
|
+
import { m as a, R as r, C as t, M as o, n as h, o as p, X as u, p as n, q as i, t as d, v as l, w as y, x as I, y as T, z as f, D as R, E as g, F as C, G as c, I as m, J as A, K as v } from "./main-JguHZ6Rn.js";
|
|
2
2
|
export {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
a as AuthTestIds,
|
|
4
|
+
r as Chat,
|
|
5
5
|
t as ChatInputTestIds,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
o as MessageBubbleTestIds,
|
|
7
|
+
h as RefreshFatalError,
|
|
8
|
+
p as RoomListTestIds,
|
|
8
9
|
u as XmppProvider,
|
|
9
10
|
n as applyTypography,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
y as
|
|
14
|
-
I as
|
|
15
|
-
T as
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
i as clearTypography,
|
|
12
|
+
d as handleQRChatId,
|
|
13
|
+
l as isRefreshFatalError,
|
|
14
|
+
y as logoutService,
|
|
15
|
+
I as refreshAuthTokens,
|
|
16
|
+
T as refreshAuthTokensQuietly,
|
|
17
|
+
f as resendMessage,
|
|
18
|
+
R as useInAppNotifications,
|
|
19
|
+
g as useIsUserOnline,
|
|
20
|
+
C as usePushNotifications,
|
|
21
|
+
c as useQRCodeChat,
|
|
22
|
+
m as useRoomPresence,
|
|
23
|
+
A as useTypography,
|
|
24
|
+
v as useUnread
|
|
21
25
|
};
|
|
22
26
|
//# sourceMappingURL=main.js.map
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import { RefreshResult } from './authRefresh';
|
|
1
2
|
declare const http: import('axios').AxiosInstance;
|
|
2
3
|
declare let appToken: string;
|
|
3
4
|
export declare function setBaseURL(newBaseURL?: string, customAppToken?: string): void;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Import `refreshAuthTokens` from `./authRefresh` instead.
|
|
7
|
+
*
|
|
8
|
+
* Kept as a thin forwarder so existing call sites keep working while
|
|
9
|
+
* they migrate. Declared as a function (not `const refresh =
|
|
10
|
+
* refreshAuthTokens`) on purpose: this module and `authRefresh` import
|
|
11
|
+
* each other, and a const would capture the binding before the other
|
|
12
|
+
* module finished evaluating.
|
|
13
|
+
*
|
|
14
|
+
* Behaviour change vs. the old implementation: it no longer dispatches
|
|
15
|
+
* `logout()` on every failure. Only a fatal verdict from the backend
|
|
16
|
+
* ends the session - see the interceptor below.
|
|
17
|
+
*/
|
|
18
|
+
export declare function refresh(): Promise<RefreshResult>;
|
|
11
19
|
export default http;
|
|
12
20
|
export { appToken };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { User } from '../types/types';
|
|
2
|
+
/**
|
|
3
|
+
* THE single refresh-token rotation point for the SDK.
|
|
4
|
+
*
|
|
5
|
+
* The backend now runs refresh-token ROTATION with REUSE DETECTION:
|
|
6
|
+
* every successful `/v1/users/login/refresh` burns the presented
|
|
7
|
+
* refresh token and issues a new one. Presenting an already-rotated
|
|
8
|
+
* token is indistinguishable, server-side, from a stolen token - so it
|
|
9
|
+
* is treated as theft and (once the backend leaves monitor mode and
|
|
10
|
+
* enables enforcing mode) kills the whole token family.
|
|
11
|
+
*
|
|
12
|
+
* That puts three hard obligations on the client:
|
|
13
|
+
*
|
|
14
|
+
* 1. Persist the NEW refreshToken from every response, immediately,
|
|
15
|
+
* before any other logic can throw and drop it on the floor.
|
|
16
|
+
* 2. Never rotate concurrently - including ACROSS TABS. Two parallel
|
|
17
|
+
* refreshes both start from the same token; the loser presents a
|
|
18
|
+
* burned one and looks like an attacker. Hence Web Locks below,
|
|
19
|
+
* plus a same-tab in-flight promise.
|
|
20
|
+
* 3. Route EVERY refresh path through here. One "side" refresh
|
|
21
|
+
* elsewhere (service worker, poller, host app) breaks the scheme
|
|
22
|
+
* for everyone.
|
|
23
|
+
*
|
|
24
|
+
* The subtle part is (2): serialising the requests is NOT enough. A tab
|
|
25
|
+
* that waited on the lock still holds the token it read before waiting,
|
|
26
|
+
* which the winner has since burned. The token must be RE-READ from
|
|
27
|
+
* cross-tab storage inside the lock - see `performRefresh`.
|
|
28
|
+
*
|
|
29
|
+
* Callers must also stop treating 401 as "log out". Two of the four
|
|
30
|
+
* refresh error codes are race signals, not auth failures - see
|
|
31
|
+
* `RefreshErrorCode` below and `RefreshFatalError`.
|
|
32
|
+
*/
|
|
33
|
+
export type RefreshErrorCode = 'REFRESH_IN_PROGRESS' | 'REFRESH_TOKEN_ALREADY_ROTATED' | 'REFRESH_TOKEN_REUSE_DETECTED' | 'REFRESH_TOKEN_NOT_FOUND';
|
|
34
|
+
export interface RefreshResult {
|
|
35
|
+
token: string;
|
|
36
|
+
refreshToken: string;
|
|
37
|
+
fileToken?: string;
|
|
38
|
+
xmppPassword?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Thrown when the session is genuinely dead and the caller must do a
|
|
42
|
+
* hard logout (clear storage, drop XMPP, send the user to login).
|
|
43
|
+
*
|
|
44
|
+
* Anything else this module rejects with - network errors, 5xx, an
|
|
45
|
+
* unrecognised 401 - is NOT fatal and must NOT trigger a logout.
|
|
46
|
+
*/
|
|
47
|
+
export declare class RefreshFatalError extends Error {
|
|
48
|
+
code: RefreshErrorCode;
|
|
49
|
+
constructor(code: RefreshErrorCode, message?: string);
|
|
50
|
+
}
|
|
51
|
+
export declare const isRefreshFatalError: (error: unknown) => error is RefreshFatalError;
|
|
52
|
+
/**
|
|
53
|
+
* All four codes come back as HTTP 401, so the status alone tells us
|
|
54
|
+
* nothing - the code in the body is the only signal.
|
|
55
|
+
*
|
|
56
|
+
* TODO(backend-confirm): the exact location of `code` in the payload is
|
|
57
|
+
* not documented yet. Until it is, probe every shape the API uses
|
|
58
|
+
* elsewhere and take the first hit. Once confirmed, collapse this to
|
|
59
|
+
* the single real path.
|
|
60
|
+
*/
|
|
61
|
+
export declare const parseRefreshErrorCode: (error: unknown) => RefreshErrorCode | null;
|
|
62
|
+
/**
|
|
63
|
+
* Is there anything left to rotate?
|
|
64
|
+
*
|
|
65
|
+
* Callers used to answer this by reading the redux copy, which is wrong
|
|
66
|
+
* in exactly the case that matters: `scrubSensitiveChatStateTransform`
|
|
67
|
+
* (roomStore/index.ts) blanks token/refreshToken before redux-persist
|
|
68
|
+
* writes them, so a freshly rehydrated second tab holds an EMPTY
|
|
69
|
+
* refresh token while the real one sits in `ETHORA_USER_SESSION`. Every
|
|
70
|
+
* such gate therefore has to go through storage first, same as the
|
|
71
|
+
* rotation itself.
|
|
72
|
+
*/
|
|
73
|
+
export declare const hasRotatableSession: () => boolean;
|
|
74
|
+
export interface RefreshOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Rotate THIS token instead of the one in the store/storage.
|
|
77
|
+
*
|
|
78
|
+
* Only for bootstrap, where the session being restored isn't in the
|
|
79
|
+
* store yet (a host-supplied `userLogin.user`, a persisted record).
|
|
80
|
+
* Everywhere else, omitting it is the correct and safer choice: the
|
|
81
|
+
* module then always presents the newest token it knows about.
|
|
82
|
+
*/
|
|
83
|
+
refreshToken?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Rotate the tokens. Concurrent callers in this tab share ONE request;
|
|
87
|
+
* concurrent tabs are serialised by the Web Lock. This is the lock the
|
|
88
|
+
* new backend scheme requires.
|
|
89
|
+
*
|
|
90
|
+
* Rejects with `RefreshFatalError` when the session is dead (caller
|
|
91
|
+
* should hard-logout) and with a plain error otherwise (caller should
|
|
92
|
+
* surface the failure and leave the session alone).
|
|
93
|
+
*/
|
|
94
|
+
export declare function refreshAuthTokens(options?: RefreshOptions): Promise<RefreshResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Fire-and-forget variant for call sites that only want "make sure the
|
|
97
|
+
* tokens are fresh" and have no meaningful error handling - chat
|
|
98
|
+
* bootstrap, XMPP reconnect. Never rejects.
|
|
99
|
+
*/
|
|
100
|
+
export declare function refreshAuthTokensQuietly(): Promise<RefreshResult | null>;
|
|
101
|
+
export declare function startCrossTabTokenSync(): () => void;
|
|
102
|
+
type CredentialLike = Partial<Pick<User, 'xmppUsername' | 'refreshToken' | 'token'>> | null | undefined;
|
|
103
|
+
export declare function markCurrentSessionDead(): void;
|
|
104
|
+
export declare function isSessionDead(candidate: CredentialLike): boolean;
|
|
105
|
+
export declare function clearDeadSessionLatch(): void;
|
|
106
|
+
/** Test seam: drops any shared in-flight promise between cases. */
|
|
107
|
+
export declare function __resetAuthRefreshStateForTests(): void;
|
|
108
|
+
export {};
|
|
@@ -2,6 +2,10 @@ import { Client } from '@xmpp/client';
|
|
|
2
2
|
import { Iso639_1Codes, XmppClientInterface, xmppSettingsInterface, IMessage } from '../types/types';
|
|
3
3
|
import { AllRoomPresenceSummary } from './xmpp/allRoomPresences.xmpp';
|
|
4
4
|
type HistorySource = 'active' | 'send_ack' | 'background' | 'default';
|
|
5
|
+
export type XmppCredentialsProvider = () => Promise<{
|
|
6
|
+
username: string;
|
|
7
|
+
password: string;
|
|
8
|
+
}>;
|
|
5
9
|
export declare class XmppClient implements XmppClientInterface {
|
|
6
10
|
client: Client;
|
|
7
11
|
devServer: string | undefined;
|
|
@@ -81,6 +85,51 @@ export declare class XmppClient implements XmppClientInterface {
|
|
|
81
85
|
private pausedDueToOfflineCap;
|
|
82
86
|
private authFailureDetected;
|
|
83
87
|
private intentionalLogout;
|
|
88
|
+
/**
|
|
89
|
+
* Ported from the RN client (bug #17). The XMPP password now expires
|
|
90
|
+
* after an hour, so a SASL `not-authorized` is no longer a dead end -
|
|
91
|
+
* it is the normal, expected end of a credential's life.
|
|
92
|
+
*
|
|
93
|
+
* Note the shape of the failure, confirmed with the backend: an
|
|
94
|
+
* ALREADY-ESTABLISHED connection is NOT dropped when its password
|
|
95
|
+
* expires - authentication happens once, at SASL bind. So a tab that
|
|
96
|
+
* simply stays open keeps working indefinitely, and the expiry only
|
|
97
|
+
* bites on the next (re)connect: a network blip, a laptop waking from
|
|
98
|
+
* sleep, a server restart. That makes it rare, intermittent and easy
|
|
99
|
+
* to misdiagnose - and before this hook it was permanent, because the
|
|
100
|
+
* client latched `authFailureDetected` and refused to reconnect for
|
|
101
|
+
* the rest of the page's life. Only a reload brought the chat back.
|
|
102
|
+
*/
|
|
103
|
+
private credentialsProvider;
|
|
104
|
+
private credentialsRefreshInFlight;
|
|
105
|
+
/**
|
|
106
|
+
* Bounded so a genuinely dead session (revoked account, wrong host)
|
|
107
|
+
* can't spin refresh -> reconnect -> auth-fail forever. Reset on every
|
|
108
|
+
* successful 'online'.
|
|
109
|
+
*/
|
|
110
|
+
private authRecoveryAttempts;
|
|
111
|
+
private maxAuthRecoveryAttempts;
|
|
112
|
+
/**
|
|
113
|
+
* Inject (or replace) the callback used to fetch fresh XMPP
|
|
114
|
+
* credentials after a SASL `not-authorized`. The provider refreshes
|
|
115
|
+
* whatever upstream credential is needed (the REST session, which now
|
|
116
|
+
* re-issues the hourly XMPP password) and returns username+password.
|
|
117
|
+
*/
|
|
118
|
+
setCredentialsProvider(provider: XmppCredentialsProvider | null): void;
|
|
119
|
+
updateCredentials(username: string, password: string): void;
|
|
120
|
+
/**
|
|
121
|
+
* Single-flight wrapper around the provider. Several reconnect paths
|
|
122
|
+
* (error handler, watchdog, visibility) can fire at once; they share
|
|
123
|
+
* one request and update the credentials exactly once.
|
|
124
|
+
*/
|
|
125
|
+
private refreshCredentialsOnce;
|
|
126
|
+
/**
|
|
127
|
+
* A SASL failure used to be terminal here. With an hourly XMPP
|
|
128
|
+
* password it is routine, so try to re-mint credentials and come back
|
|
129
|
+
* instead of latching the connection off for the rest of the page's
|
|
130
|
+
* life. Returns true when a retry was started.
|
|
131
|
+
*/
|
|
132
|
+
private recoverFromAuthFailure;
|
|
84
133
|
private isTerminalAuthFailure;
|
|
85
134
|
constructor(username: string, password: string, xmppSettings?: xmppSettingsInterface);
|
|
86
135
|
initializeClient(): Promise<void>;
|
|
@@ -37,6 +37,7 @@ export declare const setUser: import('@reduxjs/toolkit').ActionCreatorWithOption
|
|
|
37
37
|
token: string;
|
|
38
38
|
refreshToken: string;
|
|
39
39
|
fileToken?: string;
|
|
40
|
+
xmppPassword?: string;
|
|
40
41
|
}, "chatSettingStore/refreshTokens">, logout: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"chatSettingStore/logout">, setActiveModal: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "chatSettingStore/setActiveModal">, setDeleteModal: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<DeleteModal, "chatSettingStore/setDeleteModal">, setSelectedUser: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<IUser, "chatSettingStore/setSelectedUser">, updateUser: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
41
42
|
updates: Partial<User>;
|
|
42
43
|
}, "chatSettingStore/updateUser">, setActiveFile: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<ModalFile, "chatSettingStore/setActiveFile">, setLangSource: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<Iso639_1Codes, "chatSettingStore/setLangSource">, setTranslateSendEnabled: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<boolean, "chatSettingStore/setTranslateSendEnabled">, setTranslateMode: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<"auto" | "manual", "chatSettingStore/setTranslateMode">;
|
|
@@ -201,10 +201,25 @@ export interface IConfig {
|
|
|
201
201
|
defaultRooms?: ConfigRoom[];
|
|
202
202
|
refreshTokens?: {
|
|
203
203
|
enabled: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Host-owned rotation. When set, the SDK calls this INSTEAD of
|
|
206
|
+
* `/v1/users/login/refresh` - always, including during bootstrap -
|
|
207
|
+
* and never rotates the Ethora refresh token itself. Serialised
|
|
208
|
+
* through the same lock (and the same Web Lock) as the built-in path.
|
|
209
|
+
*
|
|
210
|
+
* Return `refreshToken` whenever the host's flow actually rotates an
|
|
211
|
+
* Ethora refresh token: the backend now treats a re-presented one as
|
|
212
|
+
* theft, so omitting it leaves a burned token in storage that ends
|
|
213
|
+
* the session the next time it is used. It stays optional because
|
|
214
|
+
* some hosts re-mint an access token from a different authority
|
|
215
|
+
* (SuperTokens, an owner-session endpoint) and hold no Ethora
|
|
216
|
+
* refresh token at all.
|
|
217
|
+
*/
|
|
204
218
|
refreshFunction?: () => Promise<{
|
|
205
219
|
accessToken: string;
|
|
206
220
|
refreshToken?: string;
|
|
207
221
|
fileToken?: string;
|
|
222
|
+
xmppPassword?: string;
|
|
208
223
|
} | null>;
|
|
209
224
|
};
|
|
210
225
|
backgroundChat?: {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.
|
|
1
|
+
export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.06.04";
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const refreshHelper: (isEnabled: boolean, refreshFunc: any, logoutClb: any) => void;
|