@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/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 e, R as o, C as t, M as p, n as r, X as u, o as n, p as d, q as h, t as i, v as y, w as I, x as T, y as g, z as l, D as C, E as c, F as m } from "./main-Doq70DIE.js";
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
- e as AuthTestIds,
4
- o as Chat,
3
+ a as AuthTestIds,
4
+ r as Chat,
5
5
  t as ChatInputTestIds,
6
- p as MessageBubbleTestIds,
7
- r as RoomListTestIds,
6
+ o as MessageBubbleTestIds,
7
+ h as RefreshFatalError,
8
+ p as RoomListTestIds,
8
9
  u as XmppProvider,
9
10
  n as applyTypography,
10
- d as clearTypography,
11
- h as handleQRChatId,
12
- i as logoutService,
13
- y as resendMessage,
14
- I as useInAppNotifications,
15
- T as useIsUserOnline,
16
- g as usePushNotifications,
17
- l as useQRCodeChat,
18
- C as useRoomPresence,
19
- c as useTypography,
20
- m as useUnread
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
- export declare function refresh(): Promise<{
5
- data: {
6
- refreshToken: string;
7
- token: string;
8
- fileToken?: string;
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.6.1";
1
+ export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.06.04";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethora/chat-component",
3
- "version": "26.6.6",
3
+ "version": "26.7.1",
4
4
  "type": "module",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
@@ -1 +0,0 @@
1
- export declare const refreshHelper: (isEnabled: boolean, refreshFunc: any, logoutClb: any) => void;