@ethora/chat-component-rn 26.6.12 → 26.6.13
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/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/lib/commonjs/components/AuthForms/Login.js +2 -1
- package/lib/commonjs/components/AuthForms/Login.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +4 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +14 -13
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/LoginWrapper.js +2 -1
- package/lib/commonjs/components/MainComponents/LoginWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +17 -6
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +2 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +5 -1
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +10 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +4 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/commonjs/components/styled/AudioMessage.js +2 -1
- package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/MessageImage.js +34 -8
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +7 -1
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +44 -18
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/initXmppRooms.js +5 -1
- package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +126 -12
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
- package/lib/commonjs/helpers/secureFileUrl.js +101 -0
- package/lib/commonjs/helpers/secureFileUrl.js.map +1 -0
- package/lib/commonjs/hooks/useChatWrapperInit.js +3 -3
- package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/commonjs/hooks/useFileToken.js +10 -0
- package/lib/commonjs/hooks/useFileToken.js.map +1 -0
- package/lib/commonjs/hooks/useSendMessage.js +23 -13
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/main.js +25 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/api-requests/auth.api.js +99 -54
- package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/roomMembers.api.js +1 -1
- package/lib/commonjs/networking/api-requests/roomMembers.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +9 -9
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/user.api.js +9 -9
- package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
- package/lib/commonjs/networking/apiClient.js +62 -112
- package/lib/commonjs/networking/apiClient.js.map +1 -1
- package/lib/commonjs/networking/authRefresh.js +323 -0
- package/lib/commonjs/networking/authRefresh.js.map +1 -0
- package/lib/commonjs/networking/xmppClient.js +33 -5
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/chatSettingsSlice.js +20 -2
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/components/AuthForms/Login.js +2 -1
- package/lib/module/components/AuthForms/Login.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +4 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +14 -13
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/LoginWrapper.js +2 -1
- package/lib/module/components/MainComponents/LoginWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +17 -6
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +4 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +8 -7
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js +2 -1
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/module/components/RoomComponents/ChatRoomItem.js +5 -1
- package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js +10 -2
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +4 -1
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js.map +1 -1
- package/lib/module/components/styled/AudioMessage.js +2 -1
- package/lib/module/components/styled/AudioMessage.js.map +1 -1
- package/lib/module/components/styled/MessageImage.js +34 -8
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +7 -1
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +43 -18
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/initXmppRooms.js +5 -1
- package/lib/module/helpers/initXmppRooms.js.map +1 -1
- package/lib/module/helpers/resolveInitBeforeLoadUser.js +126 -11
- package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
- package/lib/module/helpers/secureFileUrl.js +91 -0
- package/lib/module/helpers/secureFileUrl.js.map +1 -0
- package/lib/module/hooks/useChatWrapperInit.js +3 -3
- package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/module/hooks/useFileToken.js +5 -0
- package/lib/module/hooks/useFileToken.js.map +1 -0
- package/lib/module/hooks/useSendMessage.js +24 -14
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/main.js +12 -1
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/api-requests/auth.api.js +96 -53
- package/lib/module/networking/api-requests/auth.api.js.map +1 -1
- package/lib/module/networking/api-requests/roomMembers.api.js +1 -1
- package/lib/module/networking/api-requests/roomMembers.api.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +9 -9
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/api-requests/user.api.js +8 -7
- package/lib/module/networking/api-requests/user.api.js.map +1 -1
- package/lib/module/networking/apiClient.js +61 -113
- package/lib/module/networking/apiClient.js.map +1 -1
- package/lib/module/networking/authRefresh.js +312 -0
- package/lib/module/networking/authRefresh.js.map +1 -0
- package/lib/module/networking/xmppClient.js +33 -5
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/chatSettingsSlice.js +20 -2
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/typescript/components/AuthForms/Login.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/LoginWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
- package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/MessageImage.d.ts +8 -0
- package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
- package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
- package/lib/typescript/helpers/secureFileUrl.d.ts +7 -0
- package/lib/typescript/helpers/secureFileUrl.d.ts.map +1 -0
- package/lib/typescript/hooks/useFileToken.d.ts +2 -0
- package/lib/typescript/hooks/useFileToken.d.ts.map +1 -0
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +6 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/auth.api.d.ts +3 -27
- package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
- package/lib/typescript/networking/apiClient.d.ts +12 -6
- package/lib/typescript/networking/apiClient.d.ts.map +1 -1
- package/lib/typescript/networking/authRefresh.d.ts +92 -0
- package/lib/typescript/networking/authRefresh.d.ts.map +1 -0
- package/lib/typescript/networking/xmppClient.d.ts +6 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts +16 -0
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +17 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AuthForms/Login.tsx +1 -0
- package/src/components/MainComponents/ChatHeader.tsx +4 -1
- package/src/components/MainComponents/ChatWrapper.tsx +14 -19
- package/src/components/MainComponents/LoginWrapper.tsx +1 -0
- package/src/components/MainComponents/MediaMessage.tsx +14 -4
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -1
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +8 -7
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -1
- package/src/components/RoomComponents/ChatRoomItem.tsx +9 -1
- package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
- package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +4 -1
- package/src/components/RoomComponents/styled/LastMessageVideo.tsx +4 -1
- package/src/components/styled/AudioMessage.tsx +2 -1
- package/src/components/styled/MessageImage.tsx +45 -6
- package/src/components/styled/VideoMessage.tsx +10 -1
- package/src/context/xmppProvider.tsx +45 -20
- package/src/helpers/initXmppRooms.tsx +5 -1
- package/src/helpers/resolveInitBeforeLoadUser.ts +123 -11
- package/src/helpers/secureFileUrl.ts +103 -0
- package/src/hooks/useChatWrapperInit.ts +3 -3
- package/src/hooks/useFileToken.ts +7 -0
- package/src/hooks/useSendMessage.tsx +28 -13
- package/src/main.ts +22 -1
- package/src/networking/api-requests/auth.api.ts +125 -52
- package/src/networking/api-requests/roomMembers.api.ts +1 -1
- package/src/networking/api-requests/rooms.api.ts +9 -9
- package/src/networking/api-requests/user.api.ts +8 -7
- package/src/networking/apiClient.ts +68 -123
- package/src/networking/authRefresh.ts +435 -0
- package/src/networking/xmppClient.ts +37 -5
- package/src/roomStore/chatSettingsSlice.ts +28 -3
- package/src/types/types.ts +17 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { setBaseURL } from '../networking/apiClient';
|
|
2
|
+
import { refreshAuthTokens, isRefreshFatalError } from '../networking/authRefresh';
|
|
2
3
|
import { loginViaJwt } from '../networking/api-requests/auth.api';
|
|
3
4
|
import { getMyUser } from '../networking/api-requests/user.api';
|
|
4
5
|
import { IConfig, User } from '../types/types';
|
|
@@ -46,15 +47,109 @@ const normalizeUserForXmpp = (user?: User | null): User | null => {
|
|
|
46
47
|
return { ...user, xmppUsername: normalizedXmppUsername };
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
// Hermes has no atob/Buffer; hand-decode just enough base64url to read a
|
|
51
|
+
// JWT's `iat`. Claims are ASCII so the byte-per-char string is fine.
|
|
52
|
+
const B64_ALPHABET =
|
|
53
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
54
|
+
|
|
55
|
+
const readJwtIat = (token?: string | null): number => {
|
|
56
|
+
if (!token || typeof token !== 'string') {return 0;}
|
|
57
|
+
const part = token.split('.')[1];
|
|
58
|
+
if (!part) {return 0;}
|
|
59
|
+
try {
|
|
60
|
+
const clean = part.replace(/-/g, '+').replace(/_/g, '/');
|
|
61
|
+
let out = '';
|
|
62
|
+
let buffer = 0;
|
|
63
|
+
let bits = 0;
|
|
64
|
+
for (let i = 0; i < clean.length; i++) {
|
|
65
|
+
const idx = B64_ALPHABET.indexOf(clean[i]);
|
|
66
|
+
if (idx === -1) {continue;}
|
|
67
|
+
buffer = (buffer << 6) | idx;
|
|
68
|
+
bits += 6;
|
|
69
|
+
if (bits >= 8) {
|
|
70
|
+
bits -= 8;
|
|
71
|
+
out += String.fromCharCode((buffer >> bits) & 0xff);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const iat = JSON.parse(out)?.iat;
|
|
75
|
+
return typeof iat === 'number' ? iat : 0;
|
|
76
|
+
} catch {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A `userLogin.user` is typically a snapshot the host captured at ITS
|
|
83
|
+
* login time and re-presents verbatim on every mount, while this SDK
|
|
84
|
+
* keeps rotating the session and persisting the newest pair to
|
|
85
|
+
* `ETHORA_USER`. Taking the snapshot as-is therefore discards the live
|
|
86
|
+
* session in favour of an ever-aging copy: media goes blank the moment
|
|
87
|
+
* the snapshot has no `fileToken` (secure-files URLs 401 without it),
|
|
88
|
+
* and once the snapshot's refresh token ages out entirely the account
|
|
89
|
+
* looks dead even though the SDK held a working session all along.
|
|
90
|
+
*
|
|
91
|
+
* So, for the SAME account only, prefer whichever refresh pair was
|
|
92
|
+
* minted later (JWT `iat` decides — a host that genuinely re-logged-in
|
|
93
|
+
* hands us newer tokens and wins), and fill in a missing `fileToken`
|
|
94
|
+
* from the persisted copy either way.
|
|
95
|
+
*/
|
|
96
|
+
const adoptFresherPersistedSession = async (candidate: User): Promise<User> => {
|
|
97
|
+
try {
|
|
98
|
+
const stored = await asyncLocalStorage<User>(
|
|
99
|
+
localStorageConstants.ETHORA_USER
|
|
100
|
+
).get();
|
|
101
|
+
if (!stored) {return candidate;}
|
|
102
|
+
|
|
103
|
+
const sameUser =
|
|
104
|
+
(candidate._id && stored._id && candidate._id === stored._id) ||
|
|
105
|
+
(candidate.xmppUsername &&
|
|
106
|
+
stored.xmppUsername &&
|
|
107
|
+
candidate.xmppUsername === stored.xmppUsername);
|
|
108
|
+
if (!sameUser) {return candidate;}
|
|
109
|
+
|
|
110
|
+
const merged = { ...candidate };
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
stored.refreshToken &&
|
|
114
|
+
readJwtIat(stored.refreshToken) > readJwtIat(candidate.refreshToken)
|
|
115
|
+
) {
|
|
116
|
+
merged.refreshToken = stored.refreshToken;
|
|
117
|
+
if (stored.token) {merged.token = stored.token;}
|
|
118
|
+
if (stored.fileToken) {merged.fileToken = stored.fileToken;}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!merged.fileToken && stored.fileToken) {
|
|
122
|
+
merged.fileToken = stored.fileToken;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return merged;
|
|
126
|
+
} catch {
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Bootstrap rotation.
|
|
133
|
+
*
|
|
134
|
+
* Was a second, independent `/users/login/refresh` caller that bypassed
|
|
135
|
+
* every lock in the SDK — and, worse, only wrote the rotated token to
|
|
136
|
+
* the store on the happy path, so several of the early-return branches
|
|
137
|
+
* below used to drop it. Under the backend's reuse detection a dropped
|
|
138
|
+
* rotation means the next launch presents a burned token and the
|
|
139
|
+
* session is killed.
|
|
140
|
+
*
|
|
141
|
+
* `refreshAuthTokens` persists the new pair before it resolves, so by
|
|
142
|
+
* the time this returns the rotation is safe no matter which branch the
|
|
143
|
+
* caller takes afterwards. The explicit token is required here: during
|
|
144
|
+
* bootstrap the candidate session isn't in the store yet.
|
|
145
|
+
*/
|
|
49
146
|
const refreshWithToken = async (refreshToken: string) => {
|
|
50
|
-
const
|
|
51
|
-
'/users/login/refresh',
|
|
52
|
-
{},
|
|
53
|
-
{ headers: { Authorization: refreshToken } }
|
|
54
|
-
);
|
|
147
|
+
const result = await refreshAuthTokens({ refreshToken });
|
|
55
148
|
return {
|
|
56
|
-
token:
|
|
57
|
-
refreshToken:
|
|
149
|
+
token: result.token,
|
|
150
|
+
refreshToken: result.refreshToken || refreshToken,
|
|
151
|
+
xmppPassword: result.xmppPassword || '',
|
|
152
|
+
fileToken: result.fileToken || '',
|
|
58
153
|
};
|
|
59
154
|
};
|
|
60
155
|
|
|
@@ -87,11 +182,15 @@ const tryHydrateViaMy = async (
|
|
|
87
182
|
|
|
88
183
|
let workingToken = candidate?.token || '';
|
|
89
184
|
let workingRefresh = candidate?.refreshToken || '';
|
|
185
|
+
let rotatedXmppPassword = '';
|
|
186
|
+
let workingFileToken = candidate?.fileToken || '';
|
|
90
187
|
|
|
91
188
|
const candidateWithCurrentTokens = (): User => ({
|
|
92
189
|
...candidate,
|
|
93
190
|
token: workingToken || candidate.token,
|
|
94
191
|
refreshToken: workingRefresh || candidate.refreshToken,
|
|
192
|
+
xmppPassword: rotatedXmppPassword || candidate.xmppPassword,
|
|
193
|
+
fileToken: workingFileToken || candidate.fileToken,
|
|
95
194
|
});
|
|
96
195
|
|
|
97
196
|
const fallbackWithCreds = (): User | null => {
|
|
@@ -107,6 +206,8 @@ const tryHydrateViaMy = async (
|
|
|
107
206
|
if (merged) {
|
|
108
207
|
merged.token = workingToken || merged.token;
|
|
109
208
|
merged.refreshToken = workingRefresh || merged.refreshToken;
|
|
209
|
+
merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
|
|
210
|
+
merged.fileToken = workingFileToken || merged.fileToken;
|
|
110
211
|
}
|
|
111
212
|
return merged;
|
|
112
213
|
} catch (error) {
|
|
@@ -127,6 +228,8 @@ const tryHydrateViaMy = async (
|
|
|
127
228
|
const refreshed = await refreshWithToken(workingRefresh);
|
|
128
229
|
workingToken = refreshed.token;
|
|
129
230
|
workingRefresh = refreshed.refreshToken;
|
|
231
|
+
rotatedXmppPassword = refreshed.xmppPassword || rotatedXmppPassword;
|
|
232
|
+
workingFileToken = refreshed.fileToken || workingFileToken;
|
|
130
233
|
|
|
131
234
|
try {
|
|
132
235
|
const myUser = await getMyUser({ token: workingToken, endpoint: myEndpoint });
|
|
@@ -136,6 +239,8 @@ const tryHydrateViaMy = async (
|
|
|
136
239
|
if (merged) {
|
|
137
240
|
merged.token = workingToken || merged.token;
|
|
138
241
|
merged.refreshToken = workingRefresh || merged.refreshToken;
|
|
242
|
+
merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
|
|
243
|
+
merged.fileToken = workingFileToken || merged.fileToken;
|
|
139
244
|
}
|
|
140
245
|
return merged;
|
|
141
246
|
} catch (myError) {
|
|
@@ -147,6 +252,11 @@ const tryHydrateViaMy = async (
|
|
|
147
252
|
}
|
|
148
253
|
} catch (error) {
|
|
149
254
|
if (signal?.aborted || isAbortError(error)) {return null;}
|
|
255
|
+
// A fatal refresh verdict (reuse detected / token not found / stale
|
|
256
|
+
// with nothing newer around) carries no HTTP response, so it would
|
|
257
|
+
// otherwise fall through to `throw` — the session is simply dead,
|
|
258
|
+
// which for bootstrap means "nothing to restore".
|
|
259
|
+
if (isRefreshFatalError(error)) {return null;}
|
|
150
260
|
if (isAuthError(error)) {return null;}
|
|
151
261
|
throw error;
|
|
152
262
|
}
|
|
@@ -162,13 +272,15 @@ export const resolveInitBeforeLoadUser = async (
|
|
|
162
272
|
setBaseURL(config.baseUrl, config.customAppToken);
|
|
163
273
|
}
|
|
164
274
|
|
|
165
|
-
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
|
|
275
|
+
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
|
|
166
276
|
|
|
167
277
|
// Priority 1: explicit userLogin
|
|
168
278
|
const explicitUser = config?.userLogin?.enabled ? config?.userLogin?.user : null;
|
|
169
279
|
if (explicitUser) {
|
|
170
280
|
const candidate = normalizeUserForXmpp(explicitUser);
|
|
171
|
-
if (candidate && hasXmppCredentials(candidate)) {
|
|
281
|
+
if (candidate && hasXmppCredentials(candidate)) {
|
|
282
|
+
return await adoptFresherPersistedSession(candidate);
|
|
283
|
+
}
|
|
172
284
|
|
|
173
285
|
const hydrated = await tryHydrateViaMy(explicitUser, myEndpoint, signal).catch(
|
|
174
286
|
() => null
|
|
@@ -275,7 +387,7 @@ export const refreshUserCredentialsForXmpp = async (
|
|
|
275
387
|
}
|
|
276
388
|
}
|
|
277
389
|
|
|
278
|
-
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
|
|
390
|
+
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
|
|
279
391
|
|
|
280
392
|
// Priority 2: redux store user (covers userLogin + customLogin +
|
|
281
393
|
// anything previously persisted into chatSettingStore.user). The
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { store } from '../roomStore';
|
|
2
|
+
import { getCurrentBaseURL } from '../networking/apiClient';
|
|
3
|
+
import { refreshAuthTokens } from '../networking/authRefresh';
|
|
4
|
+
|
|
5
|
+
const SECURE_FILES_HOST_PREFIX = 'secure-files.';
|
|
6
|
+
const SECURE_MEDIA_PATH_PREFIX = '/secure-media/';
|
|
7
|
+
|
|
8
|
+
const FILE_TOKEN_PARAM = 'ft';
|
|
9
|
+
|
|
10
|
+
export const resolveFileUrl = (url?: string | null): string => {
|
|
11
|
+
if (!url) {return '';}
|
|
12
|
+
// Any scheme at all — http(s), data, file, content, blob.
|
|
13
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {return url;}
|
|
14
|
+
if (!url.startsWith('/')) {return url;}
|
|
15
|
+
|
|
16
|
+
const base = getCurrentBaseURL().replace(/\/+$/, '');
|
|
17
|
+
return base ? `${base}${url}` : url;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const isSecureFileUrl = (url?: string | null): boolean => {
|
|
21
|
+
if (!url) {return false;}
|
|
22
|
+
|
|
23
|
+
const path = url.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]*/i, '');
|
|
24
|
+
if (path.startsWith(SECURE_MEDIA_PATH_PREFIX)) {return true;}
|
|
25
|
+
|
|
26
|
+
const match = /^https?:\/\/(?:[^@/]+@)?([^:/?#]+)/i.exec(url);
|
|
27
|
+
if (!match) {return false;}
|
|
28
|
+
return match[1].toLowerCase().startsWith(SECURE_FILES_HOST_PREFIX);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const appendFileToken = (
|
|
32
|
+
url: string | null | undefined,
|
|
33
|
+
fileToken: string | null | undefined
|
|
34
|
+
): string => {
|
|
35
|
+
if (!url) {return '';}
|
|
36
|
+
|
|
37
|
+
const resolved = resolveFileUrl(url);
|
|
38
|
+
if (!fileToken || !isSecureFileUrl(resolved)) {return resolved;}
|
|
39
|
+
url = resolved;
|
|
40
|
+
|
|
41
|
+
const hashIndex = url.indexOf('#');
|
|
42
|
+
const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
|
|
43
|
+
const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
44
|
+
|
|
45
|
+
const queryIndex = withoutHash.indexOf('?');
|
|
46
|
+
const base = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
|
|
47
|
+
const query = queryIndex === -1 ? '' : withoutHash.slice(queryIndex + 1);
|
|
48
|
+
|
|
49
|
+
const params = query
|
|
50
|
+
.split('&')
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.filter((pair) => pair.split('=')[0] !== FILE_TOKEN_PARAM);
|
|
53
|
+
|
|
54
|
+
params.push(`${FILE_TOKEN_PARAM}=${encodeURIComponent(fileToken)}`);
|
|
55
|
+
|
|
56
|
+
return `${base}?${params.join('&')}${hash}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const withFileToken = (url?: string | null): string =>
|
|
60
|
+
appendFileToken(
|
|
61
|
+
url,
|
|
62
|
+
store.getState().chatSettingStore?.user?.fileToken || ''
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const RECOVERY_COOLDOWN_MS = 30_000;
|
|
66
|
+
let recoveryInFlight: Promise<boolean> | null = null;
|
|
67
|
+
let lastRecoveryAt = 0;
|
|
68
|
+
|
|
69
|
+
export const requestFileTokenRecovery = (): Promise<boolean> => {
|
|
70
|
+
if (recoveryInFlight) {return recoveryInFlight;}
|
|
71
|
+
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (now - lastRecoveryAt < RECOVERY_COOLDOWN_MS) {
|
|
74
|
+
return Promise.resolve(false);
|
|
75
|
+
}
|
|
76
|
+
lastRecoveryAt = now;
|
|
77
|
+
|
|
78
|
+
const state = store.getState();
|
|
79
|
+
const refreshConfig = state.chatSettingStore?.config?.refreshTokens;
|
|
80
|
+
const user = state.chatSettingStore?.user;
|
|
81
|
+
|
|
82
|
+
if (!refreshConfig?.enabled) {return Promise.resolve(false);}
|
|
83
|
+
|
|
84
|
+
const run = async (): Promise<boolean> => {
|
|
85
|
+
if (!refreshConfig.refreshFunction && !user?.refreshToken) {return false;}
|
|
86
|
+
|
|
87
|
+
const tokens = await refreshAuthTokens();
|
|
88
|
+
return Boolean(tokens.fileToken);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
recoveryInFlight = run()
|
|
92
|
+
.catch(() => false)
|
|
93
|
+
.finally(() => {
|
|
94
|
+
recoveryInFlight = null;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return recoveryInFlight;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const __resetFileTokenRecoveryForTests = (): void => {
|
|
101
|
+
recoveryInFlight = null;
|
|
102
|
+
lastRecoveryAt = 0;
|
|
103
|
+
};
|
|
@@ -10,7 +10,7 @@ import { chatAutoEnterer } from '../helpers/chatAutoEnterer';
|
|
|
10
10
|
import { initRoomsPresence } from '../helpers/initRoomsPresence';
|
|
11
11
|
import { updatedChatLastTimestamps } from '../helpers/updatedChatLastTimestamps';
|
|
12
12
|
import { updateMessagesTillLast } from '../helpers/updateMessagesTillLast';
|
|
13
|
-
import {
|
|
13
|
+
import { refreshAuthTokensQuietly } from '../networking/authRefresh';
|
|
14
14
|
import { setLangSource, setConfig } from '../roomStore/chatSettingsSlice';
|
|
15
15
|
import { setIsLoading } from '../roomStore/roomsSlice';
|
|
16
16
|
import { useRoomState } from './useRoomState';
|
|
@@ -179,7 +179,7 @@ const useChatWrapperInit = ({
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
{
|
|
182
|
-
config?.refreshTokens?.enabled &&
|
|
182
|
+
config?.refreshTokens?.enabled && refreshAuthTokensQuietly();
|
|
183
183
|
}
|
|
184
184
|
} catch (error) {
|
|
185
185
|
console.error(' Error during initialization', error);
|
|
@@ -233,7 +233,7 @@ const useChatWrapperInit = ({
|
|
|
233
233
|
setConnectionLost(false);
|
|
234
234
|
}
|
|
235
235
|
{
|
|
236
|
-
config?.refreshTokens?.enabled &&
|
|
236
|
+
config?.refreshTokens?.enabled && refreshAuthTokensQuietly();
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
clearMessageFailure,
|
|
10
10
|
markMessageFailed,
|
|
11
11
|
} from '../roomStore/roomHeapSlice';
|
|
12
|
-
import {
|
|
12
|
+
import { uploadFileV2 } from '../networking/api-requests/auth.api';
|
|
13
13
|
import { enqueueOutboundSend } from '../networking/outboundQueue';
|
|
14
14
|
import { RootState, store } from '../roomStore';
|
|
15
15
|
import { getGlobalXmppClient } from '../utils/clientRegistry';
|
|
@@ -182,13 +182,34 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
182
182
|
// PENDING_WATCHDOG_MS, flip the bubble to "failed → tap to
|
|
183
183
|
// retry". We check at fire-time whether the message is still
|
|
184
184
|
// pending and not already marked failed by the catch path.
|
|
185
|
-
|
|
185
|
+
// One-shot extension for the case below. Declared outside the
|
|
186
|
+
// timer so the rescheduled run can't extend again.
|
|
187
|
+
let watchdogExtended = false;
|
|
188
|
+
const runWatchdog = () => {
|
|
186
189
|
const state = reduxStore.getState();
|
|
187
190
|
const roomMsgs = state.rooms?.rooms?.[activeRoomJID]?.messages || [];
|
|
188
191
|
const stillPending = roomMsgs.find(
|
|
189
192
|
(m: any) => m.id === optimisticId && m.pending
|
|
190
193
|
);
|
|
191
194
|
const alreadyFailed = state.roomHeapSlice?.failedMessages?.[optimisticId];
|
|
195
|
+
|
|
196
|
+
// Don't call it a failure while the stream is still coming back.
|
|
197
|
+
// A reconnect tears the old client down and builds a new one, and
|
|
198
|
+
// any echo in flight during that window is simply lost — the
|
|
199
|
+
// message may well have been delivered (it has been observed
|
|
200
|
+
// arriving twice on the receiving side). Failing it here shows a
|
|
201
|
+
// red "tap to retry" under a message that actually went through.
|
|
202
|
+
// Give the reconnect one more window to produce an echo, once.
|
|
203
|
+
const streamOnline = getGlobalXmppClient()?.status === 'online';
|
|
204
|
+
if (stillPending && !alreadyFailed && !streamOnline && !watchdogExtended) {
|
|
205
|
+
watchdogExtended = true;
|
|
206
|
+
console.warn(
|
|
207
|
+
`Send watchdog — ${optimisticId} still pending but the stream is not online; extending one window`
|
|
208
|
+
);
|
|
209
|
+
watchdogTimer = setTimeout(runWatchdog, PENDING_WATCHDOG_MS);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
192
213
|
if (stillPending && !alreadyFailed) {
|
|
193
214
|
console.warn(
|
|
194
215
|
`Send watchdog fired — message ${optimisticId} stuck pending > ${PENDING_WATCHDOG_MS}ms; marking failed`
|
|
@@ -211,7 +232,9 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
211
232
|
messageType: 'text',
|
|
212
233
|
});
|
|
213
234
|
}
|
|
214
|
-
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
let watchdogTimer = setTimeout(runWatchdog, PENDING_WATCHDOG_MS);
|
|
215
238
|
|
|
216
239
|
try {
|
|
217
240
|
// The single send closure, used both for the immediate send and for
|
|
@@ -467,19 +490,11 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
467
490
|
name: data?.name || data?.fileName || `media_${Date.now()}`,
|
|
468
491
|
};
|
|
469
492
|
|
|
470
|
-
// Mirror the web client: a single multipart POST to /files/ with
|
|
471
|
-
// the file under the plural "files" field. We send it via fetch
|
|
472
|
-
// (not axios) because RN's fetch sets the multipart boundary on the
|
|
473
|
-
// Content-Type header correctly. The previous axios fallback omitted
|
|
474
|
-
// a valid boundary, so the server mis-parsed one file as many and
|
|
475
|
-
// rejected the request with HTTP 413 "TOO_MANY_FILES". The only
|
|
476
|
-
// retry is the documented singular-"file" fallback for deployments
|
|
477
|
-
// that 500 on the plural field (bug #10).
|
|
478
493
|
const tryUpload = async () => {
|
|
479
494
|
const fd = new FormData();
|
|
480
495
|
fd.append('files', fileBlob as any);
|
|
481
496
|
try {
|
|
482
|
-
return await
|
|
497
|
+
return await uploadFileV2(fd, activeRoomJID);
|
|
483
498
|
} catch (err: any) {
|
|
484
499
|
if (err?.response?.status === 500) {
|
|
485
500
|
console.warn(
|
|
@@ -488,7 +503,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
488
503
|
);
|
|
489
504
|
const fd2 = new FormData();
|
|
490
505
|
fd2.append('file', fileBlob as any);
|
|
491
|
-
return await
|
|
506
|
+
return await uploadFileV2(fd2, activeRoomJID);
|
|
492
507
|
}
|
|
493
508
|
throw err;
|
|
494
509
|
}
|
package/src/main.ts
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* <Chat
|
|
9
9
|
* config={{
|
|
10
|
-
*
|
|
10
|
+
* // The API ROOT — the SDK versions every path itself
|
|
11
|
+
* // (`/v1/...`, `/v2/...`). A legacy `.../v1` is accepted and
|
|
12
|
+
* // normalised, so existing integrations keep working.
|
|
13
|
+
* baseUrl: 'https://api.chat.ethora.com',
|
|
11
14
|
* xmppSettings: { host: 'xmpp.chat.ethora.com' },
|
|
12
15
|
* jwtLogin: { enabled: true, token: '<paste JWT>' },
|
|
13
16
|
* initBeforeLoad: true,
|
|
@@ -66,3 +69,21 @@ export type {
|
|
|
66
69
|
} from './types/types';
|
|
67
70
|
export type { Iso639_1Codes } from './types/models/language.model';
|
|
68
71
|
export type { TranslateMode } from './utils/translateModePolicy';
|
|
72
|
+
|
|
73
|
+
// Auth-token rotation. Exposed so hosts can drive a refresh themselves
|
|
74
|
+
// (it is deduped and shares the SDK's lock) and, more importantly, so
|
|
75
|
+
// they can tell a dead session from a transient failure: only
|
|
76
|
+
// `RefreshFatalError` means "log the user out". Every other rejection —
|
|
77
|
+
// network, 5xx, a lost REFRESH_IN_PROGRESS race — must leave the
|
|
78
|
+
// session alone.
|
|
79
|
+
export {
|
|
80
|
+
refreshAuthTokens,
|
|
81
|
+
refreshAuthTokensQuietly,
|
|
82
|
+
RefreshFatalError,
|
|
83
|
+
isRefreshFatalError,
|
|
84
|
+
} from './networking/authRefresh';
|
|
85
|
+
export type {
|
|
86
|
+
RefreshErrorCode,
|
|
87
|
+
RefreshResult,
|
|
88
|
+
RefreshOptions,
|
|
89
|
+
} from './networking/authRefresh';
|