diff --git a/lib/types/identity-service-types.js b/lib/types/identity-service-types.js index 7ab08b468..14b49e53d 100644 --- a/lib/types/identity-service-types.js +++ b/lib/types/identity-service-types.js @@ -1,366 +1,369 @@ // @flow import t, { type TInterface, type TList, type TDict, type TEnums } from 'tcomb'; import { identityKeysBlobValidator, type IdentityKeysBlob, signedPrekeysValidator, type SignedPrekeys, type OneTimeKeysResultValues, } from './crypto-types.js'; import { type OlmSessionInitializationInfo, olmSessionInitializationInfoValidator, } from './request-types.js'; import { currentUserInfoValidator, type CurrentUserInfo, } from './user-types.js'; import { values } from '../utils/objects.js'; import { tUserID, tShape } from '../utils/validation-utils.js'; export type UserAuthMetadata = { +userID: string, +accessToken: string, }; // This type should not be altered without also updating OutboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type OutboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +oneTimeContentPrekey: ?string, +oneTimeNotifPrekey: ?string, }; // This type should not be altered without also updating InboundKeyInfoResponse // in native/native_rust_library/src/identity/x3dh.rs export type InboundKeyInfoResponse = { +payload: string, +payloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +username?: ?string, +walletAddress?: ?string, }; export type DeviceOlmOutboundKeys = { +identityKeysBlob: IdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, +payloadSignature: string, }; export const deviceOlmOutboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, payloadSignature: t.String, }); export type UserDevicesOlmOutboundKeys = { +deviceID: string, +keys: ?DeviceOlmOutboundKeys, }; export type DeviceOlmInboundKeys = { +identityKeysBlob: IdentityKeysBlob, +signedPrekeys: SignedPrekeys, +payloadSignature: string, }; export const deviceOlmInboundKeysValidator: TInterface = tShape({ identityKeysBlob: identityKeysBlobValidator, signedPrekeys: signedPrekeysValidator, payloadSignature: t.String, }); export type UserDevicesOlmInboundKeys = { +keys: { +[deviceID: string]: ?DeviceOlmInboundKeys, }, +username?: ?string, +walletAddress?: ?string, }; // This type should not be altered without also updating FarcasterUser in // keyserver/addons/rust-node-addon/src/identity_client/get_farcaster_users.rs export type FarcasterUser = { +userID: string, +username: string, +farcasterID: string, }; export const farcasterUserValidator: TInterface = tShape({ userID: tUserID, username: t.String, farcasterID: t.String, }); export const farcasterUsersValidator: TList> = t.list( farcasterUserValidator, ); export const userDeviceOlmInboundKeysValidator: TInterface = tShape({ keys: t.dict(t.String, t.maybe(deviceOlmInboundKeysValidator)), username: t.maybe(t.String), walletAddress: t.maybe(t.String), }); export interface IdentityServiceClient { // Only a primary device can initiate account deletion, and web cannot be a // primary device +deleteWalletUser?: () => Promise; // Only a primary device can initiate account deletion, and web cannot be a // primary device +deletePasswordUser?: (password: string) => Promise; +logOut: () => Promise; +logOutSecondaryDevice: () => Promise; +getKeyserverKeys: string => Promise; // Users cannot register from web +registerPasswordUser?: ( username: string, password: string, fid: ?string, ) => Promise; // Users cannot register from web +registerReservedPasswordUser?: ( username: string, password: string, keyserverMessage: string, keyserverSignature: string, ) => Promise; +logInPasswordUser: ( username: string, password: string, ) => Promise; +getOutboundKeysForUser: ( userID: string, ) => Promise; +getInboundKeysForUser: ( userID: string, ) => Promise; +uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise; +generateNonce: () => Promise; // Users cannot register from web +registerWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => Promise; // Users cannot register from web +registerReservedWalletUser?: ( walletAddress: string, siweMessage: string, siweSignature: string, keyserverMessage: string, keyserverSignature: string, ) => Promise; +logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise; // on native, publishing prekeys to Identity is called directly from C++, // there is no need to expose it to JS +publishWebPrekeys?: (prekeys: SignedPrekeys) => Promise; +getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray>; +getDeviceListsForUsers: ( userIDs: $ReadOnlyArray, ) => Promise; // updating device list is possible only on Native // web cannot be a primary device, so there's no need to expose it to JS +updateDeviceList?: (newDeviceList: SignedDeviceList) => Promise; +syncPlatformDetails: () => Promise; +uploadKeysForRegisteredDeviceAndLogIn: ( userID: string, signedNonce: SignedNonce, ) => Promise; +getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray>; +linkFarcasterAccount: (farcasterID: string) => Promise; +unlinkFarcasterAccount: () => Promise; +findUserIdentities: (userIDs: $ReadOnlyArray) => Promise; + // We are introducing this on web temporarily to make sure the identity + // service is reachable in production + +ping?: () => Promise; } export type IdentityServiceAuthLayer = { +userID: string, +deviceID: string, +commServicesAccessToken: string, }; export type IdentityAuthResult = { +userID: string, +accessToken: string, +username: string, +preRequestUserState?: ?CurrentUserInfo, }; export const identityAuthResultValidator: TInterface = tShape({ userID: tUserID, accessToken: t.String, username: t.String, preRequestUserState: t.maybe(currentUserInfoValidator), }); export type IdentityNewDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, +contentOneTimeKeys: $ReadOnlyArray, +notifOneTimeKeys: $ReadOnlyArray, }; export type IdentityExistingDeviceKeyUpload = { +keyPayload: string, +keyPayloadSignature: string, +contentPrekey: string, +contentPrekeySignature: string, +notifPrekey: string, +notifPrekeySignature: string, }; // Device list types export type RawDeviceList = { +devices: $ReadOnlyArray, +timestamp: number, }; export const rawDeviceListValidator: TInterface = tShape({ devices: t.list(t.String), timestamp: t.Number, }); export type UsersRawDeviceLists = { +[userID: string]: RawDeviceList, }; // User Identity types export type EthereumIdentity = { walletAddress: string, siweMessage: string, siweSignature: string, }; export type Identity = { +username: string, +ethIdentity: ?EthereumIdentity, +farcasterID: ?string, }; export type Identities = { +[userID: string]: Identity, }; export const ethereumIdentityValidator: TInterface = tShape({ walletAddress: t.String, siweMessage: t.String, siweSignature: t.String, }); export const identityValidator: TInterface = tShape({ username: t.String, ethIdentity: t.maybe(ethereumIdentityValidator), farcasterID: t.maybe(t.String), }); export const identitiesValidator: TDict = t.dict( t.String, identityValidator, ); export type SignedDeviceList = { // JSON-stringified RawDeviceList +rawDeviceList: string, // Current primary device signature. Absent for Identity Service generated // device lists. +curPrimarySignature?: string, // Previous primary device signature. Present only if primary device // has changed since last update. +lastPrimarySignature?: string, }; export const signedDeviceListValidator: TInterface = tShape({ rawDeviceList: t.String, curPrimarySignature: t.maybe(t.String), lastPrimarySignature: t.maybe(t.String), }); export const signedDeviceListHistoryValidator: TList> = t.list(signedDeviceListValidator); export type UsersSignedDeviceLists = { +[userID: string]: SignedDeviceList, }; export const usersSignedDeviceListsValidator: TDict = t.dict(t.String, signedDeviceListValidator); export type SignedNonce = { +nonce: string, +nonceSignature: string, }; export const ONE_TIME_KEYS_NUMBER = 10; export const identityDeviceTypes = Object.freeze({ KEYSERVER: 0, WEB: 1, IOS: 2, ANDROID: 3, WINDOWS: 4, MAC_OS: 5, }); export type IdentityDeviceType = $Values; export const identityDeviceTypeValidator: TEnums = t.enums.of( values(identityDeviceTypes), ); export type IdentityPlatformDetails = { +deviceType: IdentityDeviceType, +codeVersion: number, +stateVersion?: number, +majorDesktopVersion?: number, }; export const identityPlatformDetailsValidator: TInterface = tShape({ deviceType: identityDeviceTypeValidator, codeVersion: t.Number, stateVersion: t.maybe(t.Number), majorDesktopVersion: t.maybe(t.Number), }); export type UserDevicesPlatformDetails = { +[deviceID: string]: IdentityPlatformDetails, }; export const userDevicesPlatformDetailsValidator: TDict = t.dict(t.String, identityPlatformDetailsValidator); export type UsersDevicesPlatformDetails = { +[userID: string]: UserDevicesPlatformDetails, }; export const usersDevicesPlatformDetailsValidator: TDict = t.dict(t.String, userDevicesPlatformDetailsValidator); export type PeersDeviceLists = { +usersSignedDeviceLists: UsersSignedDeviceLists, +usersDevicesPlatformDetails: UsersDevicesPlatformDetails, }; export const peersDeviceListsValidator: TInterface = tShape({ usersSignedDeviceLists: usersSignedDeviceListsValidator, usersDevicesPlatformDetails: usersDevicesPlatformDetailsValidator, }); diff --git a/web/app.react.js b/web/app.react.js index 6bb3ec376..2f36f46c4 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,572 +1,574 @@ // @flow import 'basscss/css/basscss.min.css'; import './theme.css'; import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import classnames from 'classnames'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { fetchEntriesActionTypes, updateCalendarQueryActionTypes, } from 'lib/actions/entry-actions.js'; import { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js'; import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ModalProvider, useModalContext, } from 'lib/components/modal-provider.react.js'; import { NeynarClientProvider } from 'lib/components/neynar-client-provider.react.js'; import PlatformDetailsSynchronizer from 'lib/components/platform-details-synchronizer.react.js'; import { StaffContextProvider } from 'lib/components/staff-provider.react.js'; import { UserInfosHandler } from 'lib/handlers/user-infos-handler.react.js'; import { IdentitySearchProvider } from 'lib/identity-search/identity-search-context.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { extractMajorDesktopVersion } from 'lib/shared/version-utils.js'; import type { SecondaryTunnelbrokerConnection } from 'lib/tunnelbroker/secondary-tunnelbroker-connection.js'; import { TunnelbrokerProvider } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { WebNavInfo } from 'lib/types/nav-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import type { MessageToDeviceRequest } from 'lib/types/tunnelbroker/message-to-device-request-types.js'; import { getConfig, registerConfig } from 'lib/utils/config.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { AlchemyENSCacheProvider } from 'lib/utils/wagmi-utils.js'; import QRCodeLogin from './account/qr-code-login.react.js'; import AppThemeWrapper from './app-theme-wrapper.react.js'; import { authoritativeKeyserverID } from './authoritative-keyserver.js'; import WebEditThreadAvatarProvider from './avatars/web-edit-thread-avatar-provider.react.js'; import Calendar from './calendar/calendar.react.js'; import Chat from './chat/chat.react.js'; import { EditModalProvider } from './chat/edit-message-provider.js'; import { MemberListSidebarProvider } from './chat/member-list-sidebar/member-list-sidebar-provider.react.js'; import CommunitiesRefresher from './components/communities-refresher.react.js'; import { DBOpsHandler } from './components/db-ops-handler.react.js'; +import IdentityPing from './components/identity-ping.react.js'; import LogOutIfMissingCSATHandler from './components/log-out-if-missing-csat-handler.react.js'; import NavigationArrows from './components/navigation-arrows.react.js'; import MinVersionHandler from './components/version-handler.react.js'; import { olmAPI } from './crypto/olm-api.js'; import { sqliteAPI } from './database/sqlite-api.js'; import electron from './electron.js'; import InputStateContainer from './input/input-state-container.react.js'; import InviteLinkHandler from './invite-links/invite-link-handler.react.js'; import InviteLinksRefresher from './invite-links/invite-links-refresher.react.js'; import LoadingIndicator from './loading-indicator.react.js'; import { MenuProvider } from './menu-provider.react.js'; import UpdateModalHandler from './modals/update-modal.react.js'; import SettingsSwitcher from './navigation-panels/settings-switcher.react.js'; import Topbar from './navigation-panels/topbar.react.js'; import useBadgeHandler from './push-notif/badge-handler.react.js'; import { PushNotificationsHandler } from './push-notif/push-notifs-handler.js'; import { updateNavInfoActionType } from './redux/action-types.js'; import DisconnectedBar from './redux/disconnected-bar.js'; import FocusHandler from './redux/focus-handler.react.js'; import { KeyserverReachabilityHandler } from './redux/keyserver-reachability-handler.js'; import { persistConfig } from './redux/persist.js'; import PolicyAcknowledgmentHandler from './redux/policy-acknowledgment-handler.js'; import { useSelector } from './redux/redux-utils.js'; import VisibilityHandler from './redux/visibility-handler.react.js'; import history from './router-history.js'; import { MessageSearchStateProvider } from './search/message-search-state-provider.react.js'; import AccountSettings from './settings/account-settings.react.js'; import DangerZone from './settings/danger-zone.react.js'; import KeyserverSelectionList from './settings/keyserver-selection-list.react.js'; import { getCommSharedWorker } from './shared-worker/shared-worker-provider.js'; import CommunityPicker from './sidebar/community-picker.react.js'; import Splash from './splash/splash.react.js'; import './typography.css'; import css from './style.css'; import { TooltipProvider } from './tooltips/tooltip-provider.js'; import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js'; import { useWebLock, TUNNELBROKER_LOCK_NAME } from './web-lock.js'; // We want Webpack's css-loader and style-loader to handle the Fontawesome CSS, // so we disable the autoAddCss logic and import the CSS file. Otherwise every // icon flashes huge for a second before the CSS is loaded. import '@fortawesome/fontawesome-svg-core/styles.css'; faConfig.autoAddCss = false; const desktopDetails = electron?.version ? { majorDesktopVersion: extractMajorDesktopVersion(electron?.version) } : null; registerConfig({ // We can't securely cache credentials on web resolveKeyserverSessionInvalidationUsingNativeCredentials: null, setSessionIDOnRequest: true, // Never reset the calendar range calendarRangeInactivityLimit: null, platformDetails: { platform: electron?.platform ?? 'web', codeVersion: 87, stateVersion: persistConfig.version, ...desktopDetails, }, authoritativeKeyserverID, olmAPI, sqliteAPI, }); const versionBroadcast = new BroadcastChannel('comm_version'); versionBroadcast.postMessage(getConfig().platformDetails.codeVersion); versionBroadcast.onmessage = (event: MessageEvent) => { if (event.data && event.data !== getConfig().platformDetails.codeVersion) { location.reload(); } }; // Start initializing the shared worker immediately void getCommSharedWorker(); type BaseProps = { +location: { +pathname: string, ... }, }; type Props = { ...BaseProps, // Redux state +navInfo: WebNavInfo, +entriesLoadingStatus: LoadingStatus, +loggedIn: boolean, +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, +modals: $ReadOnlyArray, }; class App extends React.PureComponent { componentDidMount() { const { navInfo, location: { pathname }, loggedIn, } = this.props; const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (pathname !== newURL) { history.replace(newURL); } } componentDidUpdate(prevProps: Props) { const { navInfo, location: { pathname }, loggedIn, } = this.props; if (!_isEqual(navInfo)(prevProps.navInfo)) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.push(newURL); } } else if (pathname !== prevProps.location.pathname) { const urlInfo = infoFromURL(pathname); const newNavInfo = navInfoFromURL(urlInfo, { navInfo }); if (!_isEqual(newNavInfo)(navInfo)) { this.props.dispatch({ type: updateNavInfoActionType, payload: newNavInfo, }); } } else if (loggedIn !== prevProps.loggedIn) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.replace(newURL); } } if (loggedIn !== prevProps.loggedIn) { electron?.clearHistory(); } } onWordmarkClicked = () => { this.props.dispatch({ type: updateNavInfoActionType, payload: { tab: 'chat' }, }); }; render(): React.Node { let content; if (this.props.loggedIn) { content = ( <> {this.renderMainContent()} {this.props.modals} ); } else { content = ( <> {this.renderLoginPage()} {this.props.modals} ); } return ( + {content} ); } onHeaderDoubleClick = (): void => electron?.doubleClickTopBar(); stopDoubleClickPropagation: ?(SyntheticEvent) => void = electron ? e => e.stopPropagation() : null; renderLoginPage(): React.Node { const { loginMethod } = this.props.navInfo; if (loginMethod === 'qr-code') { return ; } return ; } renderMainContent(): React.Node { const mainContent = this.getMainContentWithSwitcher(); let navigationArrows = null; if (electron) { navigationArrows = ; } const headerClasses = classnames({ [css.header]: true, [css['electron-draggable']]: electron, }); const wordmarkClasses = classnames({ [css.wordmark]: true, [css['electron-non-draggable']]: electron, [css['wordmark-macos']]: electron?.platform === 'macos', }); return (

Comm

{navigationArrows}
{mainContent}
); } getMainContentWithSwitcher(): React.Node { const { tab, settingsSection } = this.props.navInfo; let mainContent: React.Node; if (tab === 'settings') { if (settingsSection === 'account') { mainContent = ; } else if (settingsSection === 'friend-list') { mainContent = null; } else if (settingsSection === 'block-list') { mainContent = null; } else if (settingsSection === 'keyservers') { mainContent = ; } else if (settingsSection === 'build-info') { mainContent = null; } else if (settingsSection === 'danger-zone') { mainContent = ; } return (
{mainContent}
); } if (tab === 'calendar') { mainContent = ; } else if (tab === 'chat') { mainContent = ; } const mainContentClass = classnames( css['main-content-container'], css['main-content-container-column'], ); return (
{mainContent}
); } } const WEB_TUNNELBROKER_CHANNEL = new BroadcastChannel('shared-tunnelbroker'); const WEB_TUNNELBROKER_MESSAGE_TYPES = Object.freeze({ SEND_MESSAGE: 'send-message', MESSAGE_STATUS: 'message-status', }); function useOtherTabsTunnelbrokerConnection(): SecondaryTunnelbrokerConnection { const onSendMessageCallbacks = React.useRef< Set<(MessageToDeviceRequest) => mixed>, >(new Set()); const onMessageStatusCallbacks = React.useRef< Set<(messageID: string, error: ?string) => mixed>, >(new Set()); React.useEffect(() => { const messageHandler = (event: MessageEvent) => { if (typeof event.data !== 'object' || !event.data) { console.log( 'Invalid message received from shared ' + 'tunnelbroker broadcast channel', event.data, ); return; } const data = event.data; if (data.type === WEB_TUNNELBROKER_MESSAGE_TYPES.SEND_MESSAGE) { if (typeof data.message !== 'object' || !data.message) { console.log( 'Invalid tunnelbroker message request received ' + 'from shared tunnelbroker broadcast channel', event.data, ); return; } // We know that the input was already validated const message: MessageToDeviceRequest = (data.message: any); for (const callback of onSendMessageCallbacks.current) { callback(message); } } else if (data.type === WEB_TUNNELBROKER_MESSAGE_TYPES.MESSAGE_STATUS) { if (typeof data.messageID !== 'string') { console.log( 'Missing message id in message status message ' + 'from shared tunnelbroker broadcast channel', ); return; } const messageID = data.messageID; if ( typeof data.error !== 'string' && data.error !== null && data.error !== undefined ) { console.log( 'Invalid error in message status message ' + 'from shared tunnelbroker broadcast channel', data.error, ); return; } const error = data.error; for (const callback of onMessageStatusCallbacks.current) { callback(messageID, error); } } else { console.log( 'Invalid message type ' + 'from shared tunnelbroker broadcast channel', data, ); } }; WEB_TUNNELBROKER_CHANNEL.addEventListener('message', messageHandler); return () => WEB_TUNNELBROKER_CHANNEL.removeEventListener('message', messageHandler); }, [onMessageStatusCallbacks, onSendMessageCallbacks]); return React.useMemo( () => ({ sendMessage: message => WEB_TUNNELBROKER_CHANNEL.postMessage({ type: WEB_TUNNELBROKER_MESSAGE_TYPES.SEND_MESSAGE, message, }), onSendMessage: callback => { onSendMessageCallbacks.current.add(callback); return () => { onSendMessageCallbacks.current.delete(callback); }; }, setMessageStatus: (messageID, error) => { WEB_TUNNELBROKER_CHANNEL.postMessage({ type: WEB_TUNNELBROKER_MESSAGE_TYPES.MESSAGE_STATUS, messageID, error, }); }, onMessageStatus: callback => { onMessageStatusCallbacks.current.add(callback); return () => { onMessageStatusCallbacks.current.delete(callback); }; }, }), [onMessageStatusCallbacks, onSendMessageCallbacks], ); } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); const ConnectedApp: React.ComponentType = React.memo( function ConnectedApp(props) { const activeChatThreadID = useSelector( state => state.navInfo.activeChatThreadID, ); const navInfo = useSelector(state => state.navInfo); const fetchEntriesLoadingStatus = useSelector( fetchEntriesLoadingStatusSelector, ); const updateCalendarQueryLoadingStatus = useSelector( updateCalendarQueryLoadingStatusSelector, ); const entriesLoadingStatus = combineLoadingStatuses( fetchEntriesLoadingStatus, updateCalendarQueryLoadingStatus, ); const loggedIn = useSelector(isLoggedIn); const activeThreadCurrentlyUnread = useSelector( state => !activeChatThreadID || !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); useBadgeHandler(); const dispatch = useDispatch(); const modalContext = useModalContext(); const modals = React.useMemo( () => modalContext.modals.map(([modal, key]) => ( {modal} )), [modalContext.modals], ); const { lockStatus, releaseLockOrAbortRequest } = useWebLock( TUNNELBROKER_LOCK_NAME, ); const secondaryTunnelbrokerConnection: SecondaryTunnelbrokerConnection = useOtherTabsTunnelbrokerConnection(); return ( ); }, ); function AppWithProvider(props: BaseProps): React.Node { return ( ); } export default AppWithProvider; diff --git a/web/components/identity-ping.react.js b/web/components/identity-ping.react.js new file mode 100644 index 000000000..62db0013e --- /dev/null +++ b/web/components/identity-ping.react.js @@ -0,0 +1,40 @@ +// @flow + +import * as React from 'react'; + +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import { useIsCurrentUserStaff } from 'lib/shared/staff-utils.js'; + +function IdentityPing(): React.Node { + const identityContext = React.useContext(IdentityClientContext); + const isCurrentUserStaff = useIsCurrentUserStaff(); + + const ping = React.useCallback(async () => { + try { + if (!identityContext) { + console.log('Identity context not available'); + return; + } + const identityClient = identityContext.identityClient; + const pingCall = identityClient.ping; + if (!pingCall) { + console.log('Ping method unimplemented'); + return; + } + await pingCall(); + if (isCurrentUserStaff) { + console.log('Identity ping successful'); + } + } catch (error) { + console.log('Error pinging identity service:', error); + } + }, [identityContext, isCurrentUserStaff]); + + React.useEffect(() => { + void ping(); + }, [ping]); + + return null; +} + +export default IdentityPing; diff --git a/web/grpc/identity-service-client-wrapper.js b/web/grpc/identity-service-client-wrapper.js index 7e76bed83..9ad2adbb3 100644 --- a/web/grpc/identity-service-client-wrapper.js +++ b/web/grpc/identity-service-client-wrapper.js @@ -1,776 +1,781 @@ // @flow import { Login } from '@commapp/opaque-ke-wasm'; import identityServiceConfig from 'lib/facts/identity-service.js'; import type { OneTimeKeysResultValues, SignedPrekeys, } from 'lib/types/crypto-types.js'; import type { PlatformDetails } from 'lib/types/device-types.js'; import { type SignedDeviceList, signedDeviceListHistoryValidator, type SignedNonce, type IdentityServiceAuthLayer, type IdentityServiceClient, type DeviceOlmOutboundKeys, deviceOlmOutboundKeysValidator, type UserDevicesOlmOutboundKeys, type IdentityAuthResult, type IdentityNewDeviceKeyUpload, type IdentityExistingDeviceKeyUpload, identityDeviceTypes, identityAuthResultValidator, type UserDevicesOlmInboundKeys, type DeviceOlmInboundKeys, deviceOlmInboundKeysValidator, userDeviceOlmInboundKeysValidator, type FarcasterUser, farcasterUsersValidator, type UsersSignedDeviceLists, type Identities, identitiesValidator, type PeersDeviceLists, peersDeviceListsValidator, type IdentityPlatformDetails, } from 'lib/types/identity-service-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { VersionInterceptor, AuthInterceptor } from './interceptor.js'; import * as IdentityAuthClient from '../protobufs/identity-auth-client.cjs'; import * as IdentityAuthStructs from '../protobufs/identity-auth-structs.cjs'; import * as identityUnauthStructs from '../protobufs/identity-unauth-structs.cjs'; import { DeviceKeyUpload, Empty, IdentityKeyInfo, OpaqueLoginFinishRequest, OpaqueLoginStartRequest, Prekey, WalletAuthRequest, SecondaryDeviceKeysUploadRequest, GetFarcasterUsersRequest, } from '../protobufs/identity-unauth-structs.cjs'; import * as IdentityUnauthClient from '../protobufs/identity-unauth.cjs'; import { initOpaque } from '../shared-worker/utils/opaque-utils.js'; class IdentityServiceClientWrapper implements IdentityServiceClient { overridedOpaqueFilepath: string; authClient: ?IdentityAuthClient.IdentityClientServicePromiseClient; unauthClient: IdentityUnauthClient.IdentityClientServicePromiseClient; getNewDeviceKeyUpload: () => Promise; getExistingDeviceKeyUpload: () => Promise; constructor( platformDetails: PlatformDetails, overridedOpaqueFilepath: string, authLayer: ?IdentityServiceAuthLayer, getNewDeviceKeyUpload: () => Promise, getExistingDeviceKeyUpload: () => Promise, ) { this.overridedOpaqueFilepath = overridedOpaqueFilepath; if (authLayer) { this.authClient = IdentityServiceClientWrapper.createAuthClient( platformDetails, authLayer, ); } this.unauthClient = IdentityServiceClientWrapper.createUnauthClient(platformDetails); this.getNewDeviceKeyUpload = getNewDeviceKeyUpload; this.getExistingDeviceKeyUpload = getExistingDeviceKeyUpload; } static determineSocketAddr(): string { return process.env.IDENTITY_SOCKET_ADDR ?? identityServiceConfig.defaultURL; } static createAuthClient( platformDetails: PlatformDetails, authLayer: IdentityServiceAuthLayer, ): IdentityAuthClient.IdentityClientServicePromiseClient { const { userID, deviceID, commServicesAccessToken } = authLayer; const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor( platformDetails, ); const authInterceptor = new AuthInterceptor( userID, deviceID, commServicesAccessToken, ); const authClientOpts = { unaryInterceptors: [versionInterceptor, authInterceptor], }; return new IdentityAuthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, authClientOpts, ); } static createUnauthClient( platformDetails: PlatformDetails, ): IdentityUnauthClient.IdentityClientServicePromiseClient { const identitySocketAddr = IdentityServiceClientWrapper.determineSocketAddr(); const versionInterceptor = new VersionInterceptor( platformDetails, ); const unauthClientOpts = { unaryInterceptors: [versionInterceptor], }; return new IdentityUnauthClient.IdentityClientServicePromiseClient( identitySocketAddr, null, unauthClientOpts, ); } logOut: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.logOutUser(new Empty()); }; logOutSecondaryDevice: () => Promise = async () => { if (!this.authClient) { throw new Error('Identity service client is not initialized'); } await this.authClient.logOutSecondaryDevice(new Empty()); }; getKeyserverKeys: (keyserverID: string) => Promise = async (keyserverID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); request.setUserId(keyserverID); const response = await client.getKeyserverKeys(request); const keyserverInfo = response.getKeyserverInfo(); const identityInfo = keyserverInfo?.getIdentityInfo(); const contentPreKey = keyserverInfo?.getContentPrekey(); const notifPreKey = keyserverInfo?.getNotifPrekey(); const payload = identityInfo?.getPayload(); const keyserverKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: contentPreKey?.getPrekey(), prekeySignature: contentPreKey?.getPrekeySignature(), oneTimeKey: keyserverInfo?.getOneTimeContentPrekey(), }, notifInitializationInfo: { prekey: notifPreKey?.getPrekey(), prekeySignature: notifPreKey?.getPrekeySignature(), oneTimeKey: keyserverInfo?.getOneTimeNotifPrekey(), }, payloadSignature: identityInfo?.getPayloadSignature(), }; return assertWithValidator(keyserverKeys, deviceOlmOutboundKeysValidator); }; getOutboundKeysForUser: ( userID: string, ) => Promise = async (userID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.OutboundKeysForUserRequest(); request.setUserId(userID); const response = await client.getOutboundKeysForUser(request); const devicesMap = response.toObject()?.devicesMap; if (!devicesMap || !Array.isArray(devicesMap)) { throw new Error('Invalid devicesMap'); } const devicesKeys: (?UserDevicesOlmOutboundKeys)[] = devicesMap.map( ([deviceID, outboundKeysInfo]) => { const identityInfo = outboundKeysInfo?.identityInfo; const payload = identityInfo?.payload; const contentPreKey = outboundKeysInfo?.contentPrekey; const notifPreKey = outboundKeysInfo?.notifPrekey; if (typeof deviceID !== 'string') { console.log(`Invalid deviceID in devicesMap: ${deviceID}`); return null; } const deviceKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, contentInitializationInfo: { prekey: contentPreKey?.prekey, prekeySignature: contentPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeContentPrekey, }, notifInitializationInfo: { prekey: notifPreKey?.prekey, prekeySignature: notifPreKey?.prekeySignature, oneTimeKey: outboundKeysInfo.oneTimeNotifPrekey, }, payloadSignature: identityInfo?.payloadSignature, }; try { const validatedKeys = assertWithValidator( deviceKeys, deviceOlmOutboundKeysValidator, ); return { deviceID, keys: validatedKeys, }; } catch (e) { console.log(e); return { deviceID, keys: null, }; } }, ); return devicesKeys.filter(Boolean); }; getInboundKeysForUser: ( userID: string, ) => Promise = async (userID: string) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.InboundKeysForUserRequest(); request.setUserId(userID); const response = await client.getInboundKeysForUser(request); const devicesMap = response.toObject()?.devicesMap; if (!devicesMap || !Array.isArray(devicesMap)) { throw new Error('Invalid devicesMap'); } const devicesKeys: { [deviceID: string]: ?DeviceOlmInboundKeys, } = {}; devicesMap.forEach(([deviceID, inboundKeys]) => { const identityInfo = inboundKeys?.identityInfo; const payload = identityInfo?.payload; const contentPreKey = inboundKeys?.contentPrekey; const notifPreKey = inboundKeys?.notifPrekey; if (typeof deviceID !== 'string') { console.log(`Invalid deviceID in devicesMap: ${deviceID}`); return; } const deviceKeys = { identityKeysBlob: payload ? JSON.parse(payload) : null, signedPrekeys: { contentPrekey: contentPreKey?.prekey, contentPrekeySignature: contentPreKey?.prekeySignature, notifPrekey: notifPreKey?.prekey, notifPrekeySignature: notifPreKey?.prekeySignature, }, payloadSignature: identityInfo?.payloadSignature, }; try { devicesKeys[deviceID] = assertWithValidator( deviceKeys, deviceOlmInboundKeysValidator, ); } catch (e) { console.log(e); devicesKeys[deviceID] = null; } }); const identityInfo = response?.getIdentity(); const inboundUserKeys = { keys: devicesKeys, username: identityInfo?.getUsername(), walletAddress: identityInfo?.getEthIdentity()?.getWalletAddress(), }; return assertWithValidator( inboundUserKeys, userDeviceOlmInboundKeysValidator, ); }; uploadOneTimeKeys: (oneTimeKeys: OneTimeKeysResultValues) => Promise = async (oneTimeKeys: OneTimeKeysResultValues) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const contentOneTimeKeysArray = [...oneTimeKeys.contentOneTimeKeys]; const notifOneTimeKeysArray = [...oneTimeKeys.notificationsOneTimeKeys]; const request = new IdentityAuthStructs.UploadOneTimeKeysRequest(); request.setContentOneTimePrekeysList(contentOneTimeKeysArray); request.setNotifOneTimePrekeysList(notifOneTimeKeysArray); await client.uploadOneTimeKeys(request); }; logInPasswordUser: ( username: string, password: string, ) => Promise = async ( username: string, password: string, ) => { const client = this.unauthClient; if (!client) { throw new Error('Identity service client is not initialized'); } const [identityDeviceKeyUpload] = await Promise.all([ this.getExistingDeviceKeyUpload(), initOpaque(this.overridedOpaqueFilepath), ]); const opaqueLogin = new Login(); const startRequestBytes = opaqueLogin.start(password); const deviceKeyUpload = authExistingDeviceKeyUpload( identityDeviceKeyUpload, ); const loginStartRequest = new OpaqueLoginStartRequest(); loginStartRequest.setUsername(username); loginStartRequest.setOpaqueLoginRequest(startRequestBytes); loginStartRequest.setDeviceKeyUpload(deviceKeyUpload); let loginStartResponse; try { loginStartResponse = await client.logInPasswordUserStart(loginStartRequest); } catch (e) { console.log( 'Error calling logInPasswordUserStart:', getMessageForException(e) ?? 'unknown', ); throw e; } const finishRequestBytes = opaqueLogin.finish( loginStartResponse.getOpaqueLoginResponse_asU8(), ); const loginFinishRequest = new OpaqueLoginFinishRequest(); loginFinishRequest.setSessionId(loginStartResponse.getSessionId()); loginFinishRequest.setOpaqueLoginUpload(finishRequestBytes); let loginFinishResponse; try { loginFinishResponse = await client.logInPasswordUserFinish(loginFinishRequest); } catch (e) { console.log( 'Error calling logInPasswordUserFinish:', getMessageForException(e) ?? 'unknown', ); throw e; } const userID = loginFinishResponse.getUserId(); const accessToken = loginFinishResponse.getAccessToken(); const usernameResponse = loginFinishResponse.getUsername(); const identityAuthResult = { accessToken, userID, username: usernameResponse, }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; logInWalletUser: ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise = async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { const identityDeviceKeyUpload = await this.getExistingDeviceKeyUpload(); const deviceKeyUpload = authExistingDeviceKeyUpload( identityDeviceKeyUpload, ); const loginRequest = new WalletAuthRequest(); loginRequest.setSiweMessage(siweMessage); loginRequest.setSiweSignature(siweSignature); loginRequest.setDeviceKeyUpload(deviceKeyUpload); let loginResponse; try { loginResponse = await this.unauthClient.logInWalletUser(loginRequest); } catch (e) { console.log( 'Error calling logInWalletUser:', getMessageForException(e) ?? 'unknown', ); throw e; } const userID = loginResponse.getUserId(); const accessToken = loginResponse.getAccessToken(); const username = loginResponse.getUsername(); const identityAuthResult = { accessToken, userID, username }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; uploadKeysForRegisteredDeviceAndLogIn: ( ownerUserID: string, nonceChallengeResponse: SignedNonce, ) => Promise = async ( ownerUserID, nonceChallengeResponse, ) => { const identityDeviceKeyUpload = await this.getNewDeviceKeyUpload(); const deviceKeyUpload = authNewDeviceKeyUpload(identityDeviceKeyUpload); const { nonce, nonceSignature } = nonceChallengeResponse; const request = new SecondaryDeviceKeysUploadRequest(); request.setUserId(ownerUserID); request.setNonce(nonce); request.setNonceSignature(nonceSignature); request.setDeviceKeyUpload(deviceKeyUpload); let response; try { response = await this.unauthClient.uploadKeysForRegisteredDeviceAndLogIn(request); } catch (e) { console.log( 'Error calling uploadKeysForRegisteredDeviceAndLogIn:', getMessageForException(e) ?? 'unknown', ); throw e; } const userID = response.getUserId(); const accessToken = response.getAccessToken(); const username = response.getUsername(); const identityAuthResult = { accessToken, userID, username }; return assertWithValidator(identityAuthResult, identityAuthResultValidator); }; generateNonce: () => Promise = async () => { const result = await this.unauthClient.generateNonce(new Empty()); return result.getNonce(); }; publishWebPrekeys: (prekeys: SignedPrekeys) => Promise = async ( prekeys: SignedPrekeys, ) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const contentPrekeyUpload = new Prekey(); contentPrekeyUpload.setPrekey(prekeys.contentPrekey); contentPrekeyUpload.setPrekeySignature(prekeys.contentPrekeySignature); const notifPrekeyUpload = new Prekey(); notifPrekeyUpload.setPrekey(prekeys.notifPrekey); notifPrekeyUpload.setPrekeySignature(prekeys.notifPrekeySignature); const request = new IdentityAuthStructs.RefreshUserPrekeysRequest(); request.setNewContentPrekeys(contentPrekeyUpload); request.setNewNotifPrekeys(notifPrekeyUpload); await client.refreshUserPrekeys(request); }; getDeviceListHistoryForUser: ( userID: string, sinceTimestamp?: number, ) => Promise<$ReadOnlyArray> = async ( userID, sinceTimestamp, ) => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.GetDeviceListRequest(); request.setUserId(userID); if (sinceTimestamp) { request.setSinceTimestamp(sinceTimestamp); } const response = await client.getDeviceListForUser(request); const rawPayloads = response.getDeviceListUpdatesList(); const deviceListUpdates: SignedDeviceList[] = rawPayloads.map(payload => JSON.parse(payload), ); return assertWithValidator( deviceListUpdates, signedDeviceListHistoryValidator, ); }; getDeviceListsForUsers: ( userIDs: $ReadOnlyArray, ) => Promise = async userIDs => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.PeersDeviceListsRequest(); request.setUserIdsList([...userIDs]); const response = await client.getDeviceListsForUsers(request); const rawPayloads = response.toObject()?.usersDeviceListsMap; const rawUsersDevicesPlatformDetails = response.toObject()?.usersDevicesPlatformDetailsMap; let usersDeviceLists: UsersSignedDeviceLists = {}; rawPayloads.forEach(([userID, rawPayload]) => { usersDeviceLists = { ...usersDeviceLists, [userID]: JSON.parse(rawPayload), }; }); const usersDevicesPlatformDetails: { [userID: string]: { +[deviceID: string]: IdentityPlatformDetails }, } = {}; for (const [ userID, rawUserDevicesPlatformDetails, ] of rawUsersDevicesPlatformDetails) { usersDevicesPlatformDetails[userID] = Object.fromEntries( rawUserDevicesPlatformDetails.devicesPlatformDetailsMap, ); } const peersDeviceLists = { usersSignedDeviceLists: usersDeviceLists, usersDevicesPlatformDetails, }; return assertWithValidator(peersDeviceLists, peersDeviceListsValidator); }; syncPlatformDetails: () => Promise = async () => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } await client.syncPlatformDetails(new identityUnauthStructs.Empty()); }; getFarcasterUsers: ( farcasterIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = async farcasterIDs => { const getFarcasterUsersRequest = new GetFarcasterUsersRequest(); getFarcasterUsersRequest.setFarcasterIdsList([...farcasterIDs]); let getFarcasterUsersResponse; try { getFarcasterUsersResponse = await this.unauthClient.getFarcasterUsers( getFarcasterUsersRequest, ); } catch (e) { console.log( 'Error calling getFarcasterUsers:', getMessageForException(e) ?? 'unknown', ); throw e; } const farcasterUsersList = getFarcasterUsersResponse.getFarcasterUsersList(); const returnList = []; for (const user of farcasterUsersList) { returnList.push({ userID: user.getUserId(), username: user.getUsername(), farcasterID: user.getFarcasterId(), }); } return assertWithValidator(returnList, farcasterUsersValidator); }; linkFarcasterAccount: (farcasterID: string) => Promise = async farcasterID => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const linkFarcasterAccountRequest = new IdentityAuthStructs.LinkFarcasterAccountRequest(); linkFarcasterAccountRequest.setFarcasterId(farcasterID); await client.linkFarcasterAccount(linkFarcasterAccountRequest); }; unlinkFarcasterAccount: () => Promise = async () => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } await client.unlinkFarcasterAccount(new Empty()); }; findUserIdentities: (userIDs: $ReadOnlyArray) => Promise = async userIDs => { const client = this.authClient; if (!client) { throw new Error('Identity service client is not initialized'); } const request = new IdentityAuthStructs.UserIdentitiesRequest(); request.setUserIdsList([...userIDs]); const response = await client.findUserIdentities(request); const identityObjects = response.toObject()?.identitiesMap; let identities: Identities = {}; identityObjects.forEach(([userID, identityObject]) => { identities = { ...identities, [userID]: { ethIdentity: identityObject.ethIdentity, username: identityObject.username, farcasterID: identityObject.farcasterId, }, }; }); return assertWithValidator(identities, identitiesValidator); }; + + ping: () => Promise = async () => { + const client = this.unauthClient; + await client.ping(new Empty()); + }; } function authNewDeviceKeyUpload( uploadData: IdentityNewDeviceKeyUpload, ): DeviceKeyUpload { const { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, contentOneTimeKeys, notifOneTimeKeys, } = uploadData; const identityKeyInfo = createIdentityKeyInfo( keyPayload, keyPayloadSignature, ); const contentPrekeyUpload = createPrekey( contentPrekey, contentPrekeySignature, ); const notifPrekeyUpload = createPrekey(notifPrekey, notifPrekeySignature); const deviceKeyUpload = createDeviceKeyUpload( identityKeyInfo, contentPrekeyUpload, notifPrekeyUpload, contentOneTimeKeys, notifOneTimeKeys, ); return deviceKeyUpload; } function authExistingDeviceKeyUpload( uploadData: IdentityExistingDeviceKeyUpload, ): DeviceKeyUpload { const { keyPayload, keyPayloadSignature, contentPrekey, contentPrekeySignature, notifPrekey, notifPrekeySignature, } = uploadData; const identityKeyInfo = createIdentityKeyInfo( keyPayload, keyPayloadSignature, ); const contentPrekeyUpload = createPrekey( contentPrekey, contentPrekeySignature, ); const notifPrekeyUpload = createPrekey(notifPrekey, notifPrekeySignature); const deviceKeyUpload = createDeviceKeyUpload( identityKeyInfo, contentPrekeyUpload, notifPrekeyUpload, ); return deviceKeyUpload; } function createIdentityKeyInfo( keyPayload: string, keyPayloadSignature: string, ): IdentityKeyInfo { const identityKeyInfo = new IdentityKeyInfo(); identityKeyInfo.setPayload(keyPayload); identityKeyInfo.setPayloadSignature(keyPayloadSignature); return identityKeyInfo; } function createPrekey(prekey: string, prekeySignature: string): Prekey { const prekeyUpload = new Prekey(); prekeyUpload.setPrekey(prekey); prekeyUpload.setPrekeySignature(prekeySignature); return prekeyUpload; } function createDeviceKeyUpload( identityKeyInfo: IdentityKeyInfo, contentPrekeyUpload: Prekey, notifPrekeyUpload: Prekey, contentOneTimeKeys: $ReadOnlyArray = [], notifOneTimeKeys: $ReadOnlyArray = [], ): DeviceKeyUpload { const deviceKeyUpload = new DeviceKeyUpload(); deviceKeyUpload.setDeviceKeyInfo(identityKeyInfo); deviceKeyUpload.setContentUpload(contentPrekeyUpload); deviceKeyUpload.setNotifUpload(notifPrekeyUpload); deviceKeyUpload.setOneTimeContentPrekeysList([...contentOneTimeKeys]); deviceKeyUpload.setOneTimeNotifPrekeysList([...notifOneTimeKeys]); deviceKeyUpload.setDeviceType(identityDeviceTypes.WEB); return deviceKeyUpload; } export { IdentityServiceClientWrapper }; diff --git a/web/grpc/identity-service-context-provider.react.js b/web/grpc/identity-service-context-provider.react.js index ba3bb62c7..ec082e31e 100644 --- a/web/grpc/identity-service-context-provider.react.js +++ b/web/grpc/identity-service-context-provider.react.js @@ -1,160 +1,161 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { IdentityClientContext, type AuthMetadata, } from 'lib/shared/identity-client-context.js'; import type { IdentityServiceClient, IdentityServiceAuthLayer, } from 'lib/types/identity-service-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { useSelector } from '../redux/redux-utils.js'; import { getCommSharedWorker } from '../shared-worker/shared-worker-provider.js'; import { getOpaqueWasmPath } from '../shared-worker/utils/constants.js'; import { workerRequestMessageTypes, workerResponseMessageTypes, } from '../types/worker-types.js'; type CreateMethodWorkerProxy = ( method: $Keys, ) => (...args: $ReadOnlyArray) => Promise; type Props = { +children: React.Node, }; function IdentityServiceContextProvider(props: Props): React.Node { const { children } = props; const userID = useSelector(state => state.currentUserInfo?.id); const accessToken = useSelector(state => state.commServicesAccessToken); const getAuthMetadata = React.useCallback< () => Promise, >(async () => { const contentSigningKey = await getContentSigningKey(); return { userID, deviceID: contentSigningKey, accessToken, }; }, [accessToken, userID]); const workerClientAuthMetadata = React.useRef(null); const ensureThatWorkerClientAuthMetadataIsCurrent = React.useCallback(async () => { const [sharedWorker, authMetadata] = await Promise.all([ getCommSharedWorker(), getAuthMetadata(), ]); if (_isEqual(authMetadata, workerClientAuthMetadata.current)) { return; } workerClientAuthMetadata.current = authMetadata; let authLayer: ?IdentityServiceAuthLayer = null; if ( authMetadata.userID && authMetadata.deviceID && authMetadata.accessToken ) { authLayer = { userID: authMetadata.userID, deviceID: authMetadata.deviceID, commServicesAccessToken: authMetadata.accessToken, }; } await sharedWorker.schedule({ type: workerRequestMessageTypes.CREATE_IDENTITY_SERVICE_CLIENT, opaqueWasmPath: getOpaqueWasmPath(), authLayer, }); }, [getAuthMetadata]); React.useEffect(() => { void ensureThatWorkerClientAuthMetadataIsCurrent(); }, [ensureThatWorkerClientAuthMetadataIsCurrent]); const proxyMethodToWorker: CreateMethodWorkerProxy = React.useCallback( method => async (...args: $ReadOnlyArray) => { await ensureThatWorkerClientAuthMetadataIsCurrent(); const sharedWorker = await getCommSharedWorker(); const result = await sharedWorker.schedule({ type: workerRequestMessageTypes.CALL_IDENTITY_CLIENT_METHOD, method, args, }); if (!result) { throw new Error( `Worker identity call didn't return expected message`, ); } else if ( result.type !== workerResponseMessageTypes.CALL_IDENTITY_CLIENT_METHOD ) { throw new Error( `Worker identity call didn't return expected message. Instead got: ${JSON.stringify( result, )}`, ); } // Worker should return a message with the corresponding return type return (result.result: any); }, [ensureThatWorkerClientAuthMetadataIsCurrent], ); const client = React.useMemo(() => { return { logOut: proxyMethodToWorker('logOut'), logOutSecondaryDevice: proxyMethodToWorker('logOutSecondaryDevice'), getKeyserverKeys: proxyMethodToWorker('getKeyserverKeys'), getOutboundKeysForUser: proxyMethodToWorker('getOutboundKeysForUser'), getInboundKeysForUser: proxyMethodToWorker('getInboundKeysForUser'), uploadOneTimeKeys: proxyMethodToWorker('uploadOneTimeKeys'), logInPasswordUser: proxyMethodToWorker('logInPasswordUser'), logInWalletUser: proxyMethodToWorker('logInWalletUser'), uploadKeysForRegisteredDeviceAndLogIn: proxyMethodToWorker( 'uploadKeysForRegisteredDeviceAndLogIn', ), generateNonce: proxyMethodToWorker('generateNonce'), publishWebPrekeys: proxyMethodToWorker('publishWebPrekeys'), getDeviceListHistoryForUser: proxyMethodToWorker( 'getDeviceListHistoryForUser', ), getDeviceListsForUsers: proxyMethodToWorker('getDeviceListsForUsers'), syncPlatformDetails: proxyMethodToWorker('syncPlatformDetails'), getFarcasterUsers: proxyMethodToWorker('getFarcasterUsers'), linkFarcasterAccount: proxyMethodToWorker('linkFarcasterAccount'), unlinkFarcasterAccount: proxyMethodToWorker('unlinkFarcasterAccount'), findUserIdentities: proxyMethodToWorker('findUserIdentities'), + ping: proxyMethodToWorker('ping'), }; }, [proxyMethodToWorker]); const value = React.useMemo( () => ({ identityClient: client, getAuthMetadata, }), [client, getAuthMetadata], ); return ( {children} ); } export default IdentityServiceContextProvider;