@ethora/chat-component-rn 26.6.12 → 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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/lib/commonjs/components/AuthForms/Login.js +4 -4
- 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/MainComponents/ProfileImagePlaceholder.js +9 -8
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.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/Modals/NewChatModal/ModalContent.js +2 -1
- package/lib/commonjs/components/Modals/NewChatModal/ModalContent.js.map +1 -1
- package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js +8 -0
- package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/EditUserModal.js +7 -21
- package/lib/commonjs/components/Modals/UserProfileModal/EditUserModal.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 +46 -21
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/ensureScopedChatCache.js +2 -2
- package/lib/commonjs/helpers/ensureScopedChatCache.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/persistCrypto.js +122 -0
- package/lib/commonjs/helpers/persistCrypto.js.map +1 -0
- package/lib/commonjs/helpers/pickImageAsset.js +68 -0
- package/lib/commonjs/helpers/pickImageAsset.js.map +1 -0
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +130 -17
- 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/helpers/secureKeyValue.js +83 -0
- package/lib/commonjs/helpers/secureKeyValue.js.map +1 -0
- package/lib/commonjs/helpers/secureStoreRuntime.js +59 -0
- package/lib/commonjs/helpers/secureStoreRuntime.js.map +1 -0
- package/lib/commonjs/helpers/secureUserStorage.js +75 -0
- package/lib/commonjs/helpers/secureUserStorage.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/useLogout.js +15 -7
- package/lib/commonjs/hooks/useLogout.js.map +1 -1
- 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 +322 -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 +23 -6
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/commonjs/roomStore/persistence.js +29 -15
- package/lib/commonjs/roomStore/persistence.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +2 -0
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/components/AuthForms/Login.js +4 -4
- 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/MainComponents/ProfileImagePlaceholder.js +9 -8
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.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/Modals/NewChatModal/ModalContent.js +2 -1
- package/lib/module/components/Modals/NewChatModal/ModalContent.js.map +1 -1
- package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js +8 -0
- package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
- package/lib/module/components/Modals/UserProfileModal/EditUserModal.js +9 -23
- package/lib/module/components/Modals/UserProfileModal/EditUserModal.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 +45 -21
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/ensureScopedChatCache.js +2 -2
- package/lib/module/helpers/ensureScopedChatCache.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/persistCrypto.js +115 -0
- package/lib/module/helpers/persistCrypto.js.map +1 -0
- package/lib/module/helpers/pickImageAsset.js +63 -0
- package/lib/module/helpers/pickImageAsset.js.map +1 -0
- package/lib/module/helpers/resolveInitBeforeLoadUser.js +130 -16
- 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/helpers/secureKeyValue.js +75 -0
- package/lib/module/helpers/secureKeyValue.js.map +1 -0
- package/lib/module/helpers/secureStoreRuntime.js +52 -0
- package/lib/module/helpers/secureStoreRuntime.js.map +1 -0
- package/lib/module/helpers/secureUserStorage.js +72 -0
- package/lib/module/helpers/secureUserStorage.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/useLogout.js +15 -7
- package/lib/module/hooks/useLogout.js.map +1 -1
- 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 +311 -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 +23 -6
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/roomStore/persistence.js +30 -15
- package/lib/module/roomStore/persistence.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +2 -0
- package/lib/module/roomStore/roomsSlice.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/MainComponents/ProfileImagePlaceholder.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/Modals/NewChatModal/ModalContent.d.ts +4 -2
- package/lib/typescript/components/Modals/NewChatModal/ModalContent.d.ts.map +1 -1
- package/lib/typescript/components/Modals/NewChatModal/UpdNewChatModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserProfileModal/EditUserModal.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/persistCrypto.d.ts +13 -0
- package/lib/typescript/helpers/persistCrypto.d.ts.map +1 -0
- package/lib/typescript/helpers/pickImageAsset.d.ts +27 -0
- package/lib/typescript/helpers/pickImageAsset.d.ts.map +1 -0
- 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/helpers/secureKeyValue.d.ts +13 -0
- package/lib/typescript/helpers/secureKeyValue.d.ts.map +1 -0
- package/lib/typescript/helpers/secureStoreRuntime.d.ts +29 -0
- package/lib/typescript/helpers/secureStoreRuntime.d.ts.map +1 -0
- package/lib/typescript/helpers/secureUserStorage.d.ts +23 -0
- package/lib/typescript/helpers/secureUserStorage.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/useLogout.d.ts.map +1 -1
- 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/roomStore/persistence.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.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 +8 -1
- package/src/components/AuthForms/Login.tsx +3 -3
- 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/MainComponents/ProfileImagePlaceholder.tsx +16 -9
- 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/Modals/NewChatModal/ModalContent.tsx +6 -3
- package/src/components/Modals/NewChatModal/UpdNewChatModal.tsx +12 -1
- package/src/components/Modals/UserProfileModal/EditUserModal.tsx +9 -25
- 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 +47 -23
- package/src/helpers/ensureScopedChatCache.ts +2 -2
- package/src/helpers/initXmppRooms.tsx +5 -1
- package/src/helpers/persistCrypto.ts +127 -0
- package/src/helpers/pickImageAsset.ts +68 -0
- package/src/helpers/resolveInitBeforeLoadUser.ts +125 -18
- package/src/helpers/secureFileUrl.ts +103 -0
- package/src/helpers/secureKeyValue.ts +80 -0
- package/src/helpers/secureStoreRuntime.ts +62 -0
- package/src/helpers/secureUserStorage.ts +80 -0
- package/src/hooks/useChatWrapperInit.ts +3 -3
- package/src/hooks/useFileToken.ts +7 -0
- package/src/hooks/useLogout.tsx +15 -9
- 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 +430 -0
- package/src/networking/xmppClient.ts +37 -5
- package/src/roomStore/chatSettingsSlice.ts +31 -9
- package/src/roomStore/persistence.ts +39 -19
- package/src/roomStore/roomsSlice.ts +2 -0
- package/src/types/types.ts +17 -0
|
@@ -1,12 +1,12 @@
|
|
|
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';
|
|
5
6
|
import { store } from '../roomStore';
|
|
6
7
|
import { setUser } from '../roomStore/chatSettingsSlice';
|
|
7
8
|
import { walletToUsername } from './walletUsername';
|
|
8
|
-
import {
|
|
9
|
-
import { localStorageConstants } from './constants/LOCAL_STORAGE';
|
|
9
|
+
import { secureUserStorage } from './secureUserStorage';
|
|
10
10
|
|
|
11
11
|
interface ResolveInitBeforeLoadUserOptions {
|
|
12
12
|
config?: IConfig;
|
|
@@ -46,15 +46,107 @@ const normalizeUserForXmpp = (user?: User | null): User | null => {
|
|
|
46
46
|
return { ...user, xmppUsername: normalizedXmppUsername };
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
// Hermes has no atob/Buffer; hand-decode just enough base64url to read a
|
|
50
|
+
// JWT's `iat`. Claims are ASCII so the byte-per-char string is fine.
|
|
51
|
+
const B64_ALPHABET =
|
|
52
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
53
|
+
|
|
54
|
+
const readJwtIat = (token?: string | null): number => {
|
|
55
|
+
if (!token || typeof token !== 'string') {return 0;}
|
|
56
|
+
const part = token.split('.')[1];
|
|
57
|
+
if (!part) {return 0;}
|
|
58
|
+
try {
|
|
59
|
+
const clean = part.replace(/-/g, '+').replace(/_/g, '/');
|
|
60
|
+
let out = '';
|
|
61
|
+
let buffer = 0;
|
|
62
|
+
let bits = 0;
|
|
63
|
+
for (let i = 0; i < clean.length; i++) {
|
|
64
|
+
const idx = B64_ALPHABET.indexOf(clean[i]);
|
|
65
|
+
if (idx === -1) {continue;}
|
|
66
|
+
buffer = (buffer << 6) | idx;
|
|
67
|
+
bits += 6;
|
|
68
|
+
if (bits >= 8) {
|
|
69
|
+
bits -= 8;
|
|
70
|
+
out += String.fromCharCode((buffer >> bits) & 0xff);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const iat = JSON.parse(out)?.iat;
|
|
74
|
+
return typeof iat === 'number' ? iat : 0;
|
|
75
|
+
} catch {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A `userLogin.user` is typically a snapshot the host captured at ITS
|
|
82
|
+
* login time and re-presents verbatim on every mount, while this SDK
|
|
83
|
+
* keeps rotating the session and persisting the newest pair to
|
|
84
|
+
* `ETHORA_USER`. Taking the snapshot as-is therefore discards the live
|
|
85
|
+
* session in favour of an ever-aging copy: media goes blank the moment
|
|
86
|
+
* the snapshot has no `fileToken` (secure-files URLs 401 without it),
|
|
87
|
+
* and once the snapshot's refresh token ages out entirely the account
|
|
88
|
+
* looks dead even though the SDK held a working session all along.
|
|
89
|
+
*
|
|
90
|
+
* So, for the SAME account only, prefer whichever refresh pair was
|
|
91
|
+
* minted later (JWT `iat` decides — a host that genuinely re-logged-in
|
|
92
|
+
* hands us newer tokens and wins), and fill in a missing `fileToken`
|
|
93
|
+
* from the persisted copy either way.
|
|
94
|
+
*/
|
|
95
|
+
const adoptFresherPersistedSession = async (candidate: User): Promise<User> => {
|
|
96
|
+
try {
|
|
97
|
+
const stored = await secureUserStorage().get();
|
|
98
|
+
if (!stored) {return candidate;}
|
|
99
|
+
|
|
100
|
+
const sameUser =
|
|
101
|
+
(candidate._id && stored._id && candidate._id === stored._id) ||
|
|
102
|
+
(candidate.xmppUsername &&
|
|
103
|
+
stored.xmppUsername &&
|
|
104
|
+
candidate.xmppUsername === stored.xmppUsername);
|
|
105
|
+
if (!sameUser) {return candidate;}
|
|
106
|
+
|
|
107
|
+
const merged = { ...candidate };
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
stored.refreshToken &&
|
|
111
|
+
readJwtIat(stored.refreshToken) > readJwtIat(candidate.refreshToken)
|
|
112
|
+
) {
|
|
113
|
+
merged.refreshToken = stored.refreshToken;
|
|
114
|
+
if (stored.token) {merged.token = stored.token;}
|
|
115
|
+
if (stored.fileToken) {merged.fileToken = stored.fileToken;}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!merged.fileToken && stored.fileToken) {
|
|
119
|
+
merged.fileToken = stored.fileToken;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return merged;
|
|
123
|
+
} catch {
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Bootstrap rotation.
|
|
130
|
+
*
|
|
131
|
+
* Was a second, independent `/users/login/refresh` caller that bypassed
|
|
132
|
+
* every lock in the SDK — and, worse, only wrote the rotated token to
|
|
133
|
+
* the store on the happy path, so several of the early-return branches
|
|
134
|
+
* below used to drop it. Under the backend's reuse detection a dropped
|
|
135
|
+
* rotation means the next launch presents a burned token and the
|
|
136
|
+
* session is killed.
|
|
137
|
+
*
|
|
138
|
+
* `refreshAuthTokens` persists the new pair before it resolves, so by
|
|
139
|
+
* the time this returns the rotation is safe no matter which branch the
|
|
140
|
+
* caller takes afterwards. The explicit token is required here: during
|
|
141
|
+
* bootstrap the candidate session isn't in the store yet.
|
|
142
|
+
*/
|
|
49
143
|
const refreshWithToken = async (refreshToken: string) => {
|
|
50
|
-
const
|
|
51
|
-
'/users/login/refresh',
|
|
52
|
-
{},
|
|
53
|
-
{ headers: { Authorization: refreshToken } }
|
|
54
|
-
);
|
|
144
|
+
const result = await refreshAuthTokens({ refreshToken });
|
|
55
145
|
return {
|
|
56
|
-
token:
|
|
57
|
-
refreshToken:
|
|
146
|
+
token: result.token,
|
|
147
|
+
refreshToken: result.refreshToken || refreshToken,
|
|
148
|
+
xmppPassword: result.xmppPassword || '',
|
|
149
|
+
fileToken: result.fileToken || '',
|
|
58
150
|
};
|
|
59
151
|
};
|
|
60
152
|
|
|
@@ -87,11 +179,15 @@ const tryHydrateViaMy = async (
|
|
|
87
179
|
|
|
88
180
|
let workingToken = candidate?.token || '';
|
|
89
181
|
let workingRefresh = candidate?.refreshToken || '';
|
|
182
|
+
let rotatedXmppPassword = '';
|
|
183
|
+
let workingFileToken = candidate?.fileToken || '';
|
|
90
184
|
|
|
91
185
|
const candidateWithCurrentTokens = (): User => ({
|
|
92
186
|
...candidate,
|
|
93
187
|
token: workingToken || candidate.token,
|
|
94
188
|
refreshToken: workingRefresh || candidate.refreshToken,
|
|
189
|
+
xmppPassword: rotatedXmppPassword || candidate.xmppPassword,
|
|
190
|
+
fileToken: workingFileToken || candidate.fileToken,
|
|
95
191
|
});
|
|
96
192
|
|
|
97
193
|
const fallbackWithCreds = (): User | null => {
|
|
@@ -107,6 +203,8 @@ const tryHydrateViaMy = async (
|
|
|
107
203
|
if (merged) {
|
|
108
204
|
merged.token = workingToken || merged.token;
|
|
109
205
|
merged.refreshToken = workingRefresh || merged.refreshToken;
|
|
206
|
+
merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
|
|
207
|
+
merged.fileToken = workingFileToken || merged.fileToken;
|
|
110
208
|
}
|
|
111
209
|
return merged;
|
|
112
210
|
} catch (error) {
|
|
@@ -127,6 +225,8 @@ const tryHydrateViaMy = async (
|
|
|
127
225
|
const refreshed = await refreshWithToken(workingRefresh);
|
|
128
226
|
workingToken = refreshed.token;
|
|
129
227
|
workingRefresh = refreshed.refreshToken;
|
|
228
|
+
rotatedXmppPassword = refreshed.xmppPassword || rotatedXmppPassword;
|
|
229
|
+
workingFileToken = refreshed.fileToken || workingFileToken;
|
|
130
230
|
|
|
131
231
|
try {
|
|
132
232
|
const myUser = await getMyUser({ token: workingToken, endpoint: myEndpoint });
|
|
@@ -136,6 +236,8 @@ const tryHydrateViaMy = async (
|
|
|
136
236
|
if (merged) {
|
|
137
237
|
merged.token = workingToken || merged.token;
|
|
138
238
|
merged.refreshToken = workingRefresh || merged.refreshToken;
|
|
239
|
+
merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
|
|
240
|
+
merged.fileToken = workingFileToken || merged.fileToken;
|
|
139
241
|
}
|
|
140
242
|
return merged;
|
|
141
243
|
} catch (myError) {
|
|
@@ -147,6 +249,11 @@ const tryHydrateViaMy = async (
|
|
|
147
249
|
}
|
|
148
250
|
} catch (error) {
|
|
149
251
|
if (signal?.aborted || isAbortError(error)) {return null;}
|
|
252
|
+
// A fatal refresh verdict (reuse detected / token not found / stale
|
|
253
|
+
// with nothing newer around) carries no HTTP response, so it would
|
|
254
|
+
// otherwise fall through to `throw` — the session is simply dead,
|
|
255
|
+
// which for bootstrap means "nothing to restore".
|
|
256
|
+
if (isRefreshFatalError(error)) {return null;}
|
|
150
257
|
if (isAuthError(error)) {return null;}
|
|
151
258
|
throw error;
|
|
152
259
|
}
|
|
@@ -162,13 +269,15 @@ export const resolveInitBeforeLoadUser = async (
|
|
|
162
269
|
setBaseURL(config.baseUrl, config.customAppToken);
|
|
163
270
|
}
|
|
164
271
|
|
|
165
|
-
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
|
|
272
|
+
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
|
|
166
273
|
|
|
167
274
|
// Priority 1: explicit userLogin
|
|
168
275
|
const explicitUser = config?.userLogin?.enabled ? config?.userLogin?.user : null;
|
|
169
276
|
if (explicitUser) {
|
|
170
277
|
const candidate = normalizeUserForXmpp(explicitUser);
|
|
171
|
-
if (candidate && hasXmppCredentials(candidate)) {
|
|
278
|
+
if (candidate && hasXmppCredentials(candidate)) {
|
|
279
|
+
return await adoptFresherPersistedSession(candidate);
|
|
280
|
+
}
|
|
172
281
|
|
|
173
282
|
const hydrated = await tryHydrateViaMy(explicitUser, myEndpoint, signal).catch(
|
|
174
283
|
() => null
|
|
@@ -217,13 +326,13 @@ export const resolveInitBeforeLoadUser = async (
|
|
|
217
326
|
|
|
218
327
|
// Priority 5: persisted user in AsyncStorage
|
|
219
328
|
try {
|
|
220
|
-
const storedUser = await
|
|
329
|
+
const storedUser = await secureUserStorage().get();
|
|
221
330
|
if (storedUser && (storedUser.token || storedUser.refreshToken || storedUser.xmppPassword)) {
|
|
222
331
|
const hydrated = await tryHydrateViaMy(storedUser, myEndpoint, signal).catch(
|
|
223
332
|
() => null
|
|
224
333
|
);
|
|
225
334
|
if (hydrated && hasXmppCredentials(hydrated)) {return hydrated;}
|
|
226
|
-
await
|
|
335
|
+
await secureUserStorage().remove();
|
|
227
336
|
}
|
|
228
337
|
} catch {
|
|
229
338
|
// ignore storage errors
|
|
@@ -275,7 +384,7 @@ export const refreshUserCredentialsForXmpp = async (
|
|
|
275
384
|
}
|
|
276
385
|
}
|
|
277
386
|
|
|
278
|
-
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
|
|
387
|
+
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
|
|
279
388
|
|
|
280
389
|
// Priority 2: redux store user (covers userLogin + customLogin +
|
|
281
390
|
// anything previously persisted into chatSettingStore.user). The
|
|
@@ -295,9 +404,7 @@ export const refreshUserCredentialsForXmpp = async (
|
|
|
295
404
|
// should already mirror this, but if the redux state got cleared
|
|
296
405
|
// mid-session (logout race) this gives us one last shot.
|
|
297
406
|
try {
|
|
298
|
-
const storedUser = await
|
|
299
|
-
localStorageConstants.ETHORA_USER
|
|
300
|
-
).get();
|
|
407
|
+
const storedUser = await secureUserStorage().get();
|
|
301
408
|
if (
|
|
302
409
|
storedUser &&
|
|
303
410
|
(storedUser.token || storedUser.refreshToken || storedUser.xmppPassword)
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import { loadSecureStore } from './secureStoreRuntime';
|
|
3
|
+
import { pushLog } from '../utils/devLogger';
|
|
4
|
+
|
|
5
|
+
// Fallback keys are prefixed and namespaced away from every other
|
|
6
|
+
// AsyncStorage key this SDK writes, so a host that never installs
|
|
7
|
+
// expo-secure-store can still be told apart from one that used to lack
|
|
8
|
+
// it and has since added it (see `secureSet`'s stale-fallback cleanup).
|
|
9
|
+
const FALLBACK_PREFIX = '@ethora/secure-fallback:';
|
|
10
|
+
|
|
11
|
+
let warnedFallback = false;
|
|
12
|
+
const warnFallbackOnce = () => {
|
|
13
|
+
if (warnedFallback) {return;}
|
|
14
|
+
warnedFallback = true;
|
|
15
|
+
pushLog(
|
|
16
|
+
'warn',
|
|
17
|
+
'expo-secure-store is not installed — auth tokens, the XMPP ' +
|
|
18
|
+
'password, and the message-history cipher key are falling back ' +
|
|
19
|
+
'to plain AsyncStorage instead of the platform Keychain/Keystore. ' +
|
|
20
|
+
'Add the optional peer dependency `expo-secure-store` to your app ' +
|
|
21
|
+
'to store them securely.'
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Small-value secure KV store: platform Keychain/Keystore via
|
|
27
|
+
* expo-secure-store when the host has it installed, plain (but still
|
|
28
|
+
* namespaced) AsyncStorage otherwise. NOT for bulk data — see
|
|
29
|
+
* `persistCrypto.ts`'s size note on why message history goes through
|
|
30
|
+
* AES instead of straight into this store.
|
|
31
|
+
*/
|
|
32
|
+
export async function secureGet(key: string): Promise<string | null> {
|
|
33
|
+
const store = loadSecureStore();
|
|
34
|
+
if (store) {
|
|
35
|
+
try {
|
|
36
|
+
return await store.getItemAsync(key);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
pushLog('warn', `secureStore: getItemAsync(${key}) failed`, e);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
warnFallbackOnce();
|
|
43
|
+
return AsyncStorage.getItem(FALLBACK_PREFIX + key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function secureSet(key: string, value: string): Promise<void> {
|
|
47
|
+
const store = loadSecureStore();
|
|
48
|
+
if (store) {
|
|
49
|
+
try {
|
|
50
|
+
await store.setItemAsync(key, value);
|
|
51
|
+
// Clean up a stale plaintext fallback copy from before this host
|
|
52
|
+
// added expo-secure-store — otherwise it sits on disk forever.
|
|
53
|
+
AsyncStorage.removeItem(FALLBACK_PREFIX + key).catch(() => undefined);
|
|
54
|
+
return;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
pushLog('warn', `secureStore: setItemAsync(${key}) failed`, e);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
warnFallbackOnce();
|
|
61
|
+
await AsyncStorage.setItem(FALLBACK_PREFIX + key, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function secureRemove(key: string): Promise<void> {
|
|
65
|
+
const store = loadSecureStore();
|
|
66
|
+
if (store) {
|
|
67
|
+
try {
|
|
68
|
+
await store.deleteItemAsync(key);
|
|
69
|
+
} catch {
|
|
70
|
+
// Key may simply not exist — deleteItemAsync throwing here isn't
|
|
71
|
+
// actionable either way.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await AsyncStorage.removeItem(FALLBACK_PREFIX + key).catch(() => undefined);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Test seam only. */
|
|
78
|
+
export const __resetSecureKeyValueWarningForTests = (): void => {
|
|
79
|
+
warnedFallback = false;
|
|
80
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional-dependency bridge for `expo-secure-store` — the Keychain
|
|
3
|
+
* (iOS) / Keystore-backed EncryptedSharedPreferences (Android) API.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `components/VideoCalls/livekitRuntime.ts`: a plain top-level
|
|
6
|
+
* `import ... from 'expo-secure-store'` is not allowed anywhere in the
|
|
7
|
+
* SDK, since Metro resolves imports at bundle time and a host that
|
|
8
|
+
* hasn't added this optional peer dependency would fail to build over
|
|
9
|
+
* a feature it never opted into. Everything goes through
|
|
10
|
+
* `loadSecureStore()`, which requires the module lazily and returns
|
|
11
|
+
* null when it isn't installed — callers fall back to plain
|
|
12
|
+
* AsyncStorage rather than crash (see `secureKeyValue.ts`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface SecureStoreRuntime {
|
|
16
|
+
getItemAsync: (key: string) => Promise<string | null>;
|
|
17
|
+
setItemAsync: (key: string, value: string) => Promise<void>;
|
|
18
|
+
deleteItemAsync: (key: string) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let cached: SecureStoreRuntime | null | undefined;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the SecureStore runtime, or null when the optional native
|
|
25
|
+
* package isn't installed in the host app. Result is memoised
|
|
26
|
+
* (including the failure) so a missing dependency doesn't re-throw on
|
|
27
|
+
* every read/write.
|
|
28
|
+
*/
|
|
29
|
+
export const loadSecureStore = (): SecureStoreRuntime | null => {
|
|
30
|
+
if (cached !== undefined) {
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
+
const mod = require('expo-secure-store');
|
|
37
|
+
if (
|
|
38
|
+
typeof mod?.getItemAsync !== 'function' ||
|
|
39
|
+
typeof mod?.setItemAsync !== 'function' ||
|
|
40
|
+
typeof mod?.deleteItemAsync !== 'function'
|
|
41
|
+
) {
|
|
42
|
+
cached = null;
|
|
43
|
+
} else {
|
|
44
|
+
cached = {
|
|
45
|
+
getItemAsync: mod.getItemAsync,
|
|
46
|
+
setItemAsync: mod.setItemAsync,
|
|
47
|
+
deleteItemAsync: mod.deleteItemAsync,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
cached = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return cached;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const isSecureStoreAvailable = (): boolean => loadSecureStore() !== null;
|
|
58
|
+
|
|
59
|
+
/** Test seam only. */
|
|
60
|
+
export const __resetSecureStoreRuntimeForTests = (): void => {
|
|
61
|
+
cached = undefined;
|
|
62
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { User } from '../types/types';
|
|
2
|
+
import { asyncLocalStorage } from '../hooks/useLocalStorage';
|
|
3
|
+
import { localStorageConstants } from './constants/LOCAL_STORAGE';
|
|
4
|
+
import { secureGet, secureSet, secureRemove } from './secureKeyValue';
|
|
5
|
+
|
|
6
|
+
// Keychain (iOS) / Keystore-backed EncryptedSharedPreferences (Android)
|
|
7
|
+
// keys for the four live bearer credentials on `User`. Deliberately one
|
|
8
|
+
// key per field rather than one JSON blob: expo-secure-store historically
|
|
9
|
+
// caps a single value's size on Android, and four short JWTs each stay
|
|
10
|
+
// comfortably under that ceiling where one combined blob might not.
|
|
11
|
+
const SECRET_KEYS = {
|
|
12
|
+
token: 'ethora_auth_token',
|
|
13
|
+
refreshToken: 'ethora_auth_refresh_token',
|
|
14
|
+
xmppPassword: 'ethora_auth_xmpp_password',
|
|
15
|
+
fileToken: 'ethora_auth_file_token',
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
type SecretField = keyof typeof SECRET_KEYS;
|
|
19
|
+
const SECRET_FIELDS = Object.keys(SECRET_KEYS) as SecretField[];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Persisted-user storage, split across two backends behind the exact
|
|
23
|
+
* same `{ get, set, remove }` shape `asyncLocalStorage<User>(...)`
|
|
24
|
+
* already exposed — every existing call site swaps in by changing only
|
|
25
|
+
* the constructor call, `get()`/`set()`/`remove()` keep returning/taking
|
|
26
|
+
* a complete `User`.
|
|
27
|
+
*
|
|
28
|
+
* The four live credentials (REST access + refresh token, XMPP
|
|
29
|
+
* password, secure-media fileToken) never touch plain AsyncStorage:
|
|
30
|
+
* they go through `secureGet`/`secureSet`/`secureRemove` (platform
|
|
31
|
+
* Keychain/Keystore, falling back to namespaced AsyncStorage only when
|
|
32
|
+
* the host hasn't installed the optional `expo-secure-store` peer dep).
|
|
33
|
+
* Everything else on `User` — name, avatar, ids, xmppUsername, wallet
|
|
34
|
+
* address, none of it a bearer credential — keeps living under the
|
|
35
|
+
* existing `ETHORA_USER` AsyncStorage key, unchanged.
|
|
36
|
+
*/
|
|
37
|
+
export function secureUserStorage() {
|
|
38
|
+
const get = async (): Promise<User | null> => {
|
|
39
|
+
const profile = await asyncLocalStorage<User>(
|
|
40
|
+
localStorageConstants.ETHORA_USER
|
|
41
|
+
).get();
|
|
42
|
+
if (!profile) {return null;}
|
|
43
|
+
|
|
44
|
+
const secretValues = await Promise.all(
|
|
45
|
+
SECRET_FIELDS.map((field) => secureGet(SECRET_KEYS[field]))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const merged: User = { ...profile };
|
|
49
|
+
SECRET_FIELDS.forEach((field, i) => {
|
|
50
|
+
(merged as any)[field] = secretValues[i] || '';
|
|
51
|
+
});
|
|
52
|
+
return merged;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const set = async (user: User): Promise<void> => {
|
|
56
|
+
const profile: any = { ...user };
|
|
57
|
+
for (const field of SECRET_FIELDS) {
|
|
58
|
+
profile[field] = '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await Promise.all([
|
|
62
|
+
asyncLocalStorage<User>(localStorageConstants.ETHORA_USER).set(profile),
|
|
63
|
+
...SECRET_FIELDS.map((field) => {
|
|
64
|
+
const value = user?.[field] as string | undefined;
|
|
65
|
+
return value
|
|
66
|
+
? secureSet(SECRET_KEYS[field], value)
|
|
67
|
+
: secureRemove(SECRET_KEYS[field]);
|
|
68
|
+
}),
|
|
69
|
+
]);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const remove = async (): Promise<void> => {
|
|
73
|
+
await Promise.all([
|
|
74
|
+
asyncLocalStorage(localStorageConstants.ETHORA_USER).remove(),
|
|
75
|
+
...SECRET_FIELDS.map((field) => secureRemove(SECRET_KEYS[field])),
|
|
76
|
+
]);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return { get, set, remove };
|
|
80
|
+
}
|
|
@@ -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
|
}
|
package/src/hooks/useLogout.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { clearHeap } from '../roomStore/roomHeapSlice';
|
|
|
9
9
|
import { pushSubscriptionService } from '../services/pushSubscriptionService';
|
|
10
10
|
import { clearRoomsRestCache } from '../networking/api-requests/rooms.api';
|
|
11
11
|
import { clearPersistedState } from '../roomStore/persistence';
|
|
12
|
-
import {
|
|
12
|
+
import { secureUserStorage } from '../helpers/secureUserStorage';
|
|
13
13
|
|
|
14
14
|
// AsyncStorage keys the library writes but that aren't cleared by any
|
|
15
15
|
// slice reducer. Listed here so a single logout call wipes the full
|
|
@@ -120,17 +120,23 @@ const logoutService = {
|
|
|
120
120
|
|
|
121
121
|
// 6. Stray keys the slices don't touch.
|
|
122
122
|
try {
|
|
123
|
-
await AsyncStorage.multiRemove(
|
|
124
|
-
...LIBRARY_STRAY_KEYS,
|
|
125
|
-
// Belt-and-suspenders: the chat slice's `logout` reducer
|
|
126
|
-
// schedules an async remove of ETHORA_USER fire-and-forget;
|
|
127
|
-
// re-issue it here so the disk is provably clean before this
|
|
128
|
-
// Promise resolves.
|
|
129
|
-
localStorageConstants.ETHORA_USER,
|
|
130
|
-
]);
|
|
123
|
+
await AsyncStorage.multiRemove(LIBRARY_STRAY_KEYS);
|
|
131
124
|
} catch (e) {
|
|
132
125
|
console.warn('logoutService: stray-key clear failed', e);
|
|
133
126
|
}
|
|
127
|
+
|
|
128
|
+
// 7. Belt-and-suspenders: the chat slice's `logout` reducer already
|
|
129
|
+
// fires an async ETHORA_USER removal, but re-issue it here (via
|
|
130
|
+
// the same secureUserStorage split the reducer uses) so BOTH the
|
|
131
|
+
// plain-AsyncStorage profile half AND the Keychain/Keystore
|
|
132
|
+
// secrets are provably gone before this Promise resolves — a bare
|
|
133
|
+
// `AsyncStorage.removeItem(ETHORA_USER)` here would only clear the
|
|
134
|
+
// profile half and leave the tokens behind.
|
|
135
|
+
try {
|
|
136
|
+
await secureUserStorage().remove();
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.warn('logoutService: secure user storage clear failed', e);
|
|
139
|
+
}
|
|
134
140
|
},
|
|
135
141
|
};
|
|
136
142
|
|