diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js index b6c987fad..55d73624e 100644 --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -1,518 +1,519 @@ // @flow import t from 'tcomb'; import type { TType } from 'tcomb'; import { baseLegalPolicies } from 'lib/facts/policies.js'; import type { PolicyType } from 'lib/facts/policies.js'; import type { Endpoint } from 'lib/types/endpoints.js'; import { endpointValidators } from 'lib/types/validators/endpoint-validators.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { updateActivityResponder, threadSetUnreadStatusResponder, setThreadUnreadStatusValidator, updateActivityResponderInputValidator, } from './responders/activity-responders.js'; import { deviceTokenUpdateResponder, deviceTokenUpdateRequestInputValidator, } from './responders/device-responders.js'; import { entryFetchResponder, entryRevisionFetchResponder, entryCreationResponder, entryUpdateResponder, entryDeletionResponder, entryRestorationResponder, calendarQueryUpdateResponder, createEntryRequestInputValidator, deleteEntryRequestInputValidator, entryQueryInputValidator, entryRevisionHistoryFetchInputValidator, newEntryQueryInputValidator, restoreEntryRequestInputValidator, saveEntryRequestInputValidator, } from './responders/entry-responders.js'; import { createOrUpdateFarcasterChannelTagResponder, deleteFarcasterChannelTagResponder, createOrUpdateFarcasterChannelTagInputValidator, deleteFarcasterChannelTagInputValidator, } from './responders/farcaster-channel-tag-responders.js'; import type { JSONResponder } from './responders/handlers.js'; import { createJSONResponder } from './responders/handlers.js'; import { getOlmSessionInitializationDataResponder } from './responders/keys-responders.js'; import { createOrUpdatePublicLinkResponder, disableInviteLinkResponder, fetchPrimaryInviteLinksResponder, inviteLinkVerificationResponder, createOrUpdatePublicLinkInputValidator, disableInviteLinkInputValidator, inviteLinkVerificationRequestInputValidator, } from './responders/link-responders.js'; import { messageReportCreationResponder, messageReportCreationRequestInputValidator, } from './responders/message-report-responder.js'; import { textMessageCreationResponder, messageFetchResponder, multimediaMessageCreationResponder, reactionMessageCreationResponder, editMessageCreationResponder, fetchPinnedMessagesResponder, searchMessagesResponder, sendMultimediaMessageRequestInputValidator, sendReactionMessageRequestInputValidator, editMessageRequestInputValidator, sendTextMessageRequestInputValidator, fetchMessageInfosRequestInputValidator, fetchPinnedMessagesResponderInputValidator, searchMessagesResponderInputValidator, } from './responders/message-responders.js'; import { getInitialReduxStateResponder, initialReduxStateRequestValidator, } from './responders/redux-state-responders.js'; import { updateRelationshipsResponder, updateRelationshipInputValidator, } from './responders/relationship-responders.js'; import { reportCreationResponder, reportMultiCreationResponder, errorReportFetchInfosResponder, reportCreationRequestInputValidator, fetchErrorReportInfosRequestInputValidator, reportMultiCreationRequestInputValidator, } from './responders/report-responders.js'; import { userSearchResponder, exactUserSearchResponder, exactUserSearchRequestInputValidator, userSearchRequestInputValidator, } from './responders/search-responders.js'; import { siweNonceResponder } from './responders/siwe-nonce-responders.js'; import { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadFetchMediaResponder, threadJoinResponder, toggleMessagePinResponder, roleModificationResponder, roleDeletionResponder, newThreadRequestInputValidator, threadDeletionRequestInputValidator, joinThreadRequestInputValidator, leaveThreadRequestInputValidator, threadFetchMediaRequestInputValidator, removeMembersRequestInputValidator, roleChangeRequestInputValidator, toggleMessagePinRequestInputValidator, updateThreadRequestInputValidator, roleDeletionRequestInputValidator, roleModificationRequestInputValidator, } from './responders/thread-responders.js'; import { keyserverAuthRequestInputValidator, keyserverAuthResponder, userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, registerRequestInputValidator, logInRequestInputValidator, policyAcknowledgmentRequestInputValidator, accountUpdateInputValidator, resetPasswordRequestInputValidator, siweAuthRequestInputValidator, subscriptionUpdateRequestInputValidator, updatePasswordRequestInputValidator, updateUserSettingsInputValidator, claimUsernameResponder, + claimUsernameRequestInputValidator, } from './responders/user-responders.js'; import { codeVerificationResponder, codeVerificationRequestInputValidator, } from './responders/verification-responders.js'; import { versionResponder } from './responders/version-responders.js'; import type { Viewer } from './session/viewer.js'; import { uploadMediaMetadataResponder, uploadDeletionResponder, UploadDeletionRequestInputValidator, uploadMediaMetadataInputValidator, } from './uploads/uploads.js'; const ignoredArgumentValidator = t.irreducible( 'Ignored argument', () => true, ); type EndpointData = { responder: (viewer: Viewer, input: any) => Promise<*>, inputValidator: TType<*>, policies: $ReadOnlyArray, }; const jsonEndpointsData: { +[id: Endpoint]: EndpointData } = { create_account: { responder: accountCreationResponder, inputValidator: registerRequestInputValidator, policies: [], }, create_entry: { responder: entryCreationResponder, inputValidator: createEntryRequestInputValidator, policies: baseLegalPolicies, }, create_error_report: { responder: reportCreationResponder, inputValidator: reportCreationRequestInputValidator, policies: [], }, create_message_report: { responder: messageReportCreationResponder, inputValidator: messageReportCreationRequestInputValidator, policies: baseLegalPolicies, }, create_multimedia_message: { responder: multimediaMessageCreationResponder, inputValidator: sendMultimediaMessageRequestInputValidator, policies: baseLegalPolicies, }, create_or_update_public_link: { responder: createOrUpdatePublicLinkResponder, inputValidator: createOrUpdatePublicLinkInputValidator, policies: baseLegalPolicies, }, create_reaction_message: { responder: reactionMessageCreationResponder, inputValidator: sendReactionMessageRequestInputValidator, policies: baseLegalPolicies, }, disable_invite_link: { responder: disableInviteLinkResponder, inputValidator: disableInviteLinkInputValidator, policies: baseLegalPolicies, }, edit_message: { responder: editMessageCreationResponder, inputValidator: editMessageRequestInputValidator, policies: baseLegalPolicies, }, create_report: { responder: reportCreationResponder, inputValidator: reportCreationRequestInputValidator, policies: [], }, create_reports: { responder: reportMultiCreationResponder, inputValidator: reportMultiCreationRequestInputValidator, policies: [], }, create_text_message: { responder: textMessageCreationResponder, inputValidator: sendTextMessageRequestInputValidator, policies: baseLegalPolicies, }, create_thread: { responder: threadCreationResponder, inputValidator: newThreadRequestInputValidator, policies: baseLegalPolicies, }, delete_account: { responder: accountDeletionResponder, inputValidator: ignoredArgumentValidator, policies: [], }, delete_entry: { responder: entryDeletionResponder, inputValidator: deleteEntryRequestInputValidator, policies: baseLegalPolicies, }, delete_community_role: { responder: roleDeletionResponder, inputValidator: roleDeletionRequestInputValidator, policies: baseLegalPolicies, }, delete_thread: { responder: threadDeletionResponder, inputValidator: threadDeletionRequestInputValidator, policies: baseLegalPolicies, }, delete_upload: { responder: uploadDeletionResponder, inputValidator: UploadDeletionRequestInputValidator, policies: baseLegalPolicies, }, exact_search_user: { responder: exactUserSearchResponder, inputValidator: exactUserSearchRequestInputValidator, policies: [], }, fetch_entries: { responder: entryFetchResponder, inputValidator: entryQueryInputValidator, policies: baseLegalPolicies, }, fetch_entry_revisions: { responder: entryRevisionFetchResponder, inputValidator: entryRevisionHistoryFetchInputValidator, policies: baseLegalPolicies, }, fetch_error_report_infos: { responder: errorReportFetchInfosResponder, inputValidator: fetchErrorReportInfosRequestInputValidator, policies: baseLegalPolicies, }, fetch_messages: { responder: messageFetchResponder, inputValidator: fetchMessageInfosRequestInputValidator, policies: baseLegalPolicies, }, fetch_pinned_messages: { responder: fetchPinnedMessagesResponder, inputValidator: fetchPinnedMessagesResponderInputValidator, policies: baseLegalPolicies, }, fetch_primary_invite_links: { responder: fetchPrimaryInviteLinksResponder, inputValidator: ignoredArgumentValidator, policies: baseLegalPolicies, }, fetch_thread_media: { responder: threadFetchMediaResponder, inputValidator: threadFetchMediaRequestInputValidator, policies: baseLegalPolicies, }, get_initial_redux_state: { responder: getInitialReduxStateResponder, inputValidator: initialReduxStateRequestValidator, policies: [], }, join_thread: { responder: threadJoinResponder, inputValidator: joinThreadRequestInputValidator, policies: baseLegalPolicies, }, keyserver_auth: { responder: keyserverAuthResponder, inputValidator: keyserverAuthRequestInputValidator, policies: [], }, leave_thread: { responder: threadLeaveResponder, inputValidator: leaveThreadRequestInputValidator, policies: baseLegalPolicies, }, log_in: { responder: logInResponder, inputValidator: logInRequestInputValidator, policies: [], }, log_out: { responder: logOutResponder, inputValidator: ignoredArgumentValidator, policies: [], }, modify_community_role: { responder: roleModificationResponder, inputValidator: roleModificationRequestInputValidator, policies: baseLegalPolicies, }, policy_acknowledgment: { responder: policyAcknowledgmentResponder, inputValidator: policyAcknowledgmentRequestInputValidator, policies: [], }, remove_members: { responder: memberRemovalResponder, inputValidator: removeMembersRequestInputValidator, policies: baseLegalPolicies, }, restore_entry: { responder: entryRestorationResponder, inputValidator: restoreEntryRequestInputValidator, policies: baseLegalPolicies, }, search_messages: { responder: searchMessagesResponder, inputValidator: searchMessagesResponderInputValidator, policies: baseLegalPolicies, }, search_users: { responder: userSearchResponder, inputValidator: userSearchRequestInputValidator, policies: baseLegalPolicies, }, send_password_reset_email: { responder: sendPasswordResetEmailResponder, inputValidator: resetPasswordRequestInputValidator, policies: [], }, send_verification_email: { responder: sendVerificationEmailResponder, inputValidator: ignoredArgumentValidator, policies: [], }, set_thread_unread_status: { responder: threadSetUnreadStatusResponder, inputValidator: setThreadUnreadStatusValidator, policies: baseLegalPolicies, }, toggle_message_pin: { responder: toggleMessagePinResponder, inputValidator: toggleMessagePinRequestInputValidator, policies: baseLegalPolicies, }, update_account: { responder: passwordUpdateResponder, inputValidator: accountUpdateInputValidator, policies: baseLegalPolicies, }, update_activity: { responder: updateActivityResponder, inputValidator: updateActivityResponderInputValidator, policies: baseLegalPolicies, }, update_calendar_query: { responder: calendarQueryUpdateResponder, inputValidator: newEntryQueryInputValidator, policies: baseLegalPolicies, }, update_user_settings: { responder: updateUserSettingsResponder, inputValidator: updateUserSettingsInputValidator, policies: baseLegalPolicies, }, update_device_token: { responder: deviceTokenUpdateResponder, inputValidator: deviceTokenUpdateRequestInputValidator, policies: [], }, update_entry: { responder: entryUpdateResponder, inputValidator: saveEntryRequestInputValidator, policies: baseLegalPolicies, }, update_password: { responder: oldPasswordUpdateResponder, inputValidator: updatePasswordRequestInputValidator, policies: baseLegalPolicies, }, update_relationships: { responder: updateRelationshipsResponder, inputValidator: updateRelationshipInputValidator, policies: baseLegalPolicies, }, update_role: { responder: roleUpdateResponder, inputValidator: roleChangeRequestInputValidator, policies: baseLegalPolicies, }, update_thread: { responder: threadUpdateResponder, inputValidator: updateThreadRequestInputValidator, policies: baseLegalPolicies, }, update_user_subscription: { responder: userSubscriptionUpdateResponder, inputValidator: subscriptionUpdateRequestInputValidator, policies: baseLegalPolicies, }, verify_code: { responder: codeVerificationResponder, inputValidator: codeVerificationRequestInputValidator, policies: baseLegalPolicies, }, verify_invite_link: { responder: inviteLinkVerificationResponder, inputValidator: inviteLinkVerificationRequestInputValidator, policies: baseLegalPolicies, }, siwe_nonce: { responder: siweNonceResponder, inputValidator: ignoredArgumentValidator, policies: [], }, siwe_auth: { responder: siweAuthResponder, inputValidator: siweAuthRequestInputValidator, policies: [], }, claim_username: { responder: claimUsernameResponder, - inputValidator: ignoredArgumentValidator, + inputValidator: claimUsernameRequestInputValidator, policies: [], }, update_user_avatar: { responder: updateUserAvatarResponder, inputValidator: updateUserAvatarRequestValidator, policies: baseLegalPolicies, }, upload_media_metadata: { responder: uploadMediaMetadataResponder, inputValidator: uploadMediaMetadataInputValidator, policies: baseLegalPolicies, }, get_olm_session_initialization_data: { responder: getOlmSessionInitializationDataResponder, inputValidator: ignoredArgumentValidator, policies: [], }, version: { responder: versionResponder, inputValidator: ignoredArgumentValidator, policies: [], }, create_or_update_farcaster_channel_tag: { responder: createOrUpdateFarcasterChannelTagResponder, inputValidator: createOrUpdateFarcasterChannelTagInputValidator, policies: baseLegalPolicies, }, delete_farcaster_channel_tag: { responder: deleteFarcasterChannelTagResponder, inputValidator: deleteFarcasterChannelTagInputValidator, policies: baseLegalPolicies, }, }; function createJSONResponders(obj: { +[Endpoint]: EndpointData }): { +[Endpoint]: JSONResponder, } { const result: { [Endpoint]: JSONResponder } = {}; Object.keys(obj).forEach((endpoint: Endpoint) => { const responder = createJSONResponder( obj[endpoint].responder, obj[endpoint].inputValidator, endpointValidators[endpoint].validator, obj[endpoint].policies, ); result[endpoint] = responder; }); return result; } const jsonEndpoints: { +[Endpoint]: JSONResponder } = createJSONResponders(jsonEndpointsData); export { jsonEndpoints }; diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js index 46a0728e3..6b7855879 100644 --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,993 +1,1020 @@ // @flow import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { getRustAPI } from 'rust-node-addon'; import { SiweErrorType, SiweMessage } from 'siwe'; import t, { type TInterface } from 'tcomb'; import bcrypt from 'twin-bcrypt'; import { baseLegalPolicies, policies, policyTypes, type PolicyType, } from 'lib/facts/policies.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { KeyserverAuthRequest, ResetPasswordRequest, LogOutResponse, RegisterResponse, RegisterRequest, ServerLogInResponse, LogInRequest, UpdatePasswordRequest, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, + ClaimUsernameRequest, ClaimUsernameResponse, } from 'lib/types/account-types.js'; import { userSettingsTypes, notificationTypeValues, authActionSources, } from 'lib/types/account-types.js'; import { type ClientAvatar, type UpdateUserAvatarResponse, type UpdateUserAvatarRequest, } from 'lib/types/avatar-types.js'; import type { ReservedUsernameMessage, IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; import type { DeviceType, DeviceTokenUpdateRequest, PlatformDetails, } from 'lib/types/device-types'; import { type CalendarQuery, type FetchEntryInfosBase, } from 'lib/types/entry-types.js'; import { defaultNumberPerThread } from 'lib/types/message-types.js'; import type { SIWEAuthRequest, SIWEMessage, SIWESocialProof, } from 'lib/types/siwe-types.js'; import { type SubscriptionUpdateRequest, type SubscriptionUpdateResponse, } from 'lib/types/subscription-types.js'; import { type PasswordUpdate } from 'lib/types/user-types.js'; import { identityKeysBlobValidator, signedIdentityKeysBlobValidator, } from 'lib/utils/crypto-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { ignorePromiseRejections } from 'lib/utils/promises.js'; import { getPublicKeyFromSIWEStatement, isValidSIWEMessage, isValidSIWEStatementWithPublicKey, primaryIdentityPublicKeyRegex, } from 'lib/utils/siwe-utils.js'; import { tShape, tPlatformDetails, tPassword, tEmail, tOldValidUsername, tRegex, tID, tUserID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, newEntryQueryInputValidator, normalizeCalendarQuery, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { createAndSendReservedUsernameMessage, sendMessagesOnAccountCreation, createAccount, } from '../creators/account-creator.js'; import createIDs from '../creators/id-creator.js'; import { createOlmSession, persistFreshOlmSession, } from '../creators/olm-session-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { deleteAccount } from '../deleters/account-deleters.js'; import { deleteCookie } from '../deleters/cookie-deleters.js'; import { checkAndInvalidateSIWENonceEntry } from '../deleters/siwe-nonce-deleters.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchKnownUserInfos, fetchLoggedInUserInfo, fetchUserIDForEthereumAddress, fetchUsername, } from '../fetchers/user-fetchers.js'; import { createNewAnonymousCookie, createNewUserCookie, setNewSession, } from '../session/cookies.js'; import type { Viewer } from '../session/viewer.js'; import { passwordUpdater, checkAndSendVerificationEmail, checkAndSendPasswordResetEmail, updatePassword, updateUserSettings, updateUserAvatar, } from '../updaters/account-updaters.js'; import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; import { userSubscriptionUpdater } from '../updaters/user-subscription-updaters.js'; import { viewerAcknowledgmentUpdater } from '../updaters/viewer-acknowledgment-updater.js'; import { verifyUserLoggedIn } from '../user/login.js'; import { getOlmUtility, getContentSigningKey } from '../utils/olm-utils.js'; export const subscriptionUpdateRequestInputValidator: TInterface = tShape({ threadID: tID, updatedFields: tShape({ pushNotifs: t.maybe(t.Boolean), home: t.maybe(t.Boolean), }), }); async function userSubscriptionUpdateResponder( viewer: Viewer, request: SubscriptionUpdateRequest, ): Promise { const threadSubscription = await userSubscriptionUpdater(viewer, request); return { threadSubscription, }; } export const accountUpdateInputValidator: TInterface = tShape({ updatedFields: tShape({ email: t.maybe(tEmail), password: t.maybe(tPassword), }), currentPassword: tPassword, }); async function passwordUpdateResponder( viewer: Viewer, request: PasswordUpdate, ): Promise { await passwordUpdater(viewer, request); } async function sendVerificationEmailResponder(viewer: Viewer): Promise { await checkAndSendVerificationEmail(viewer); } export const resetPasswordRequestInputValidator: TInterface = tShape({ usernameOrEmail: t.union([tEmail, tOldValidUsername]), }); async function sendPasswordResetEmailResponder( viewer: Viewer, request: ResetPasswordRequest, ): Promise { await checkAndSendPasswordResetEmail(request); } async function logOutResponder(viewer: Viewer): Promise { if (viewer.loggedIn) { const [anonymousViewerData] = await Promise.all([ createNewAnonymousCookie({ platformDetails: viewer.platformDetails, deviceToken: viewer.deviceToken, }), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(anonymousViewerData); } return { currentUserInfo: { anonymous: true, }, }; } async function accountDeletionResponder( viewer: Viewer, ): Promise { const result = await deleteAccount(viewer); invariant(result, 'deleteAccount should return result if handed request'); return result; } type OldDeviceTokenUpdateRequest = { +deviceType?: ?DeviceType, +deviceToken: string, }; const deviceTokenUpdateRequestInputValidator = tShape({ deviceType: t.maybe(t.enums.of(['ios', 'android'])), deviceToken: t.String, }); export const registerRequestInputValidator: TInterface = tShape({ username: t.String, email: t.maybe(tEmail), password: tPassword, calendarQuery: t.maybe(newEntryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, // We include `primaryIdentityPublicKey` to avoid breaking // old clients, but we no longer do anything with it. primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), initialNotificationsEncryptedMessage: t.maybe(t.String), }); async function accountCreationResponder( viewer: Viewer, request: RegisterRequest, ): Promise { const { signedIdentityKeysBlob } = request; if (signedIdentityKeysBlob) { const identityKeys: IdentityKeysBlob = JSON.parse( signedIdentityKeysBlob.payload, ); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } return await createAccount(viewer, request); } type ProcessSuccessfulLoginParams = { +viewer: Viewer, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +userID: string, +calendarQuery: ?CalendarQuery, +socialProof?: ?SIWESocialProof, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, +pickledContentOlmSession?: string, +shouldMarkPoliciesAsAcceptedAfterCookieCreation?: boolean, }; type ProcessSuccessfulLoginResult = | { +success: true, +newServerTime: number, } | { +success: false, +notAcknowledgedPolicies: $ReadOnlyArray, }; async function processSuccessfulLogin( params: ProcessSuccessfulLoginParams, ): Promise { const { viewer, deviceTokenUpdateRequest, platformDetails, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, pickledContentOlmSession, shouldMarkPoliciesAsAcceptedAfterCookieCreation, } = params; // Olm sessions have to be created before createNewUserCookie is called, // to avoid propagating a user cookie in case session creation fails const olmNotifSession = await (async () => { if (initialNotificationsEncryptedMessage && signedIdentityKeysBlob) { return await createOlmSession( initialNotificationsEncryptedMessage, 'notifications', ); } return null; })(); const newServerTime = Date.now(); const deviceToken = deviceTokenUpdateRequest ? deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const setNewCookiePromise = (async () => { const [userViewerData] = await Promise.all([ createNewUserCookie(userID, { platformDetails, deviceToken, socialProof, signedIdentityKeysBlob, }), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(userViewerData); })(); const policiesCheckAndUpdate = (async () => { if (shouldMarkPoliciesAsAcceptedAfterCookieCreation) { await setNewCookiePromise; await viewerAcknowledgmentUpdater( viewer, policyTypes.tosAndPrivacyPolicy, ); } return await fetchNotAcknowledgedPolicies(userID, baseLegalPolicies); })(); const [notAcknowledgedPolicies] = await Promise.all([ policiesCheckAndUpdate, setNewCookiePromise, ]); if ( notAcknowledgedPolicies.length && hasMinCodeVersion(viewer.platformDetails, { native: 181 }) ) { return { success: false, notAcknowledgedPolicies }; } if (calendarQuery) { await setNewSession(viewer, calendarQuery, newServerTime); } const persistOlmNotifSessionPromise = (async () => { if (olmNotifSession && viewer.cookieID) { await persistFreshOlmSession( olmNotifSession, 'notifications', viewer.cookieID, ); } })(); // `pickledContentOlmSession` is created in `keyserverAuthResponder(...)` in // order to authenticate the user. Here, we simply persist the session if it // exists. const persistOlmContentSessionPromise = (async () => { if (viewer.cookieID && pickledContentOlmSession) { await persistFreshOlmSession( pickledContentOlmSession, 'content', viewer.cookieID, ); } })(); await Promise.all([ persistOlmNotifSessionPromise, persistOlmContentSessionPromise, ]); return { success: true, newServerTime }; } type FetchLoginResponseParams = { +viewer: Viewer, +watchedIDs: $ReadOnlyArray, +calendarQuery: ?CalendarQuery, +newServerTime: number, }; async function fetchLoginResponse( params: FetchLoginResponseParams, ): Promise { const { viewer, watchedIDs, calendarQuery } = params; const threadCursors: { [string]: null } = {}; for (const watchedThreadID of watchedIDs) { threadCursors[watchedThreadID] = null; } const messageSelectionCriteria = { threadCursors, joinedThreads: true }; const entriesPromise: Promise = (async () => { if (!calendarQuery) { return undefined; } return await fetchEntryInfos(viewer, [calendarQuery]); })(); const [ threadsResult, messagesResult, entriesResult, userInfos, currentUserInfo, ] = await Promise.all([ fetchThreadInfos(viewer), fetchMessageInfos(viewer, messageSelectionCriteria, defaultNumberPerThread), entriesPromise, fetchKnownUserInfos(viewer), fetchLoggedInUserInfo(viewer), ]); const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null; const response: ServerLogInResponse = { currentUserInfo, rawMessageInfos: messagesResult.rawMessageInfos, truncationStatuses: messagesResult.truncationStatuses, serverTime: params.newServerTime, userInfos: values(userInfos), cookieChange: { threadInfos: threadsResult.threadInfos, userInfos: [], }, }; if (rawEntryInfos) { return { ...response, rawEntryInfos, }; } return response; } type HandleSuccessfulLoginResultParams = { +viewer: Viewer, +watchedIDs: $ReadOnlyArray, +calendarQuery: ?CalendarQuery, }; async function handleSuccessfulLoginResult( result: ProcessSuccessfulLoginResult, params: HandleSuccessfulLoginResultParams, ): Promise { const { viewer, watchedIDs, calendarQuery } = params; if (!result.success) { const currentUserInfo = await fetchLoggedInUserInfo(viewer); return { notAcknowledgedPolicies: result.notAcknowledgedPolicies, currentUserInfo: currentUserInfo, rawMessageInfos: [], truncationStatuses: {}, userInfos: [], rawEntryInfos: [], serverTime: 0, cookieChange: { threadInfos: {}, userInfos: [], }, }; } return await fetchLoginResponse({ viewer, watchedIDs, calendarQuery, newServerTime: result.newServerTime, }); } export const logInRequestInputValidator: TInterface = tShape({ username: t.maybe(t.String), usernameOrEmail: t.maybe(t.union([tEmail, tOldValidUsername])), password: tPassword, watchedIDs: t.list(tID), calendarQuery: t.maybe(entryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, source: t.maybe(t.enums.of(values(authActionSources))), // We include `primaryIdentityPublicKey` to avoid breaking // old clients, but we no longer do anything with it. primaryIdentityPublicKey: t.maybe(tRegex(primaryIdentityPublicKeyRegex)), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), initialNotificationsEncryptedMessage: t.maybe(t.String), }); async function logInResponder( viewer: Viewer, request: LogInRequest, ): Promise { let identityKeys: ?IdentityKeysBlob; const { signedIdentityKeysBlob, initialNotificationsEncryptedMessage } = request; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } const calendarQuery = request.calendarQuery ? normalizeCalendarQuery(request.calendarQuery) : null; const verifyCalendarQueryThreadIDsPromise = (async () => { if (calendarQuery) { await verifyCalendarQueryThreadIDs(calendarQuery); } })(); const username = request.username ?? request.usernameOrEmail; if (!username) { if (hasMinCodeVersion(viewer.platformDetails, { native: 150 })) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userQuery = SQL` SELECT id, hash, username FROM users WHERE LCASE(username) = LCASE(${username}) `; const userQueryPromise = dbQuery(userQuery); const [[userResult]] = await Promise.all([ userQueryPromise, verifyCalendarQueryThreadIDsPromise, ]); if (userResult.length === 0) { if (hasMinCodeVersion(viewer.platformDetails, { native: 150 })) { throw new ServerError('invalid_credentials'); } else { throw new ServerError('invalid_parameters'); } } const userRow = userResult[0]; if (!userRow.hash || !bcrypt.compareSync(request.password, userRow.hash)) { throw new ServerError('invalid_credentials'); } const id = userRow.id.toString(); const processSuccessfulLoginResult = await processSuccessfulLogin({ viewer, platformDetails: request.platformDetails, deviceTokenUpdateRequest: request.deviceTokenUpdateRequest, userID: id, calendarQuery, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, }); return await handleSuccessfulLoginResult(processSuccessfulLoginResult, { viewer, watchedIDs: request.watchedIDs, calendarQuery, }); } export const siweAuthRequestInputValidator: TInterface = tShape({ signature: t.String, message: t.String, calendarQuery: entryQueryInputValidator, deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, watchedIDs: t.list(tID), signedIdentityKeysBlob: t.maybe(signedIdentityKeysBlobValidator), initialNotificationsEncryptedMessage: t.maybe(t.String), doNotRegister: t.maybe(t.Boolean), }); async function siweAuthResponder( viewer: Viewer, request: SIWEAuthRequest, ): Promise { const { message, signature, deviceTokenUpdateRequest, platformDetails, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, doNotRegister, watchedIDs, } = request; const calendarQuery = normalizeCalendarQuery(request.calendarQuery); // 1. Ensure that `message` is a well formed Comm SIWE Auth message. const siweMessage: SIWEMessage = new SiweMessage(message); if (!isValidSIWEMessage(siweMessage)) { throw new ServerError('invalid_parameters'); } // 2. Check if there's already a user for this ETH address. // Verify calendarQuery. const [existingUserID] = await Promise.all([ fetchUserIDForEthereumAddress(siweMessage.address), verifyCalendarQueryThreadIDs(calendarQuery), ]); if (!existingUserID && doNotRegister) { throw new ServerError('account_does_not_exist'); } // 3. Ensure that the `nonce` exists in the `siwe_nonces` table // AND hasn't expired. If those conditions are met, delete the entry to // ensure that the same `nonce` can't be re-used in a future request. const wasNonceCheckedAndInvalidated = await checkAndInvalidateSIWENonceEntry( siweMessage.nonce, ); if (!wasNonceCheckedAndInvalidated) { throw new ServerError('invalid_parameters'); } // 4. Validate SIWEMessage signature and handle possible errors. try { await siweMessage.verify({ signature }); } catch (error) { if (error === SiweErrorType.EXPIRED_MESSAGE) { // Thrown when the `expirationTime` is present and in the past. throw new ServerError('expired_message'); } else if (error === SiweErrorType.INVALID_SIGNATURE) { // Thrown when the `validate()` function can't verify the message. throw new ServerError('invalid_signature'); } else { throw new ServerError('unknown_error'); } } // 5. Pull `primaryIdentityPublicKey` out from SIWEMessage `statement`. // We expect it to be included for BOTH native and web clients. const { statement } = siweMessage; const primaryIdentityPublicKey = statement && isValidSIWEStatementWithPublicKey(statement) ? getPublicKeyFromSIWEStatement(statement) : null; if (!primaryIdentityPublicKey) { throw new ServerError('invalid_siwe_statement_public_key'); } // 6. Verify `signedIdentityKeysBlob.payload` with included `signature` // if `signedIdentityKeysBlob` was included in the `SIWEAuthRequest`. let identityKeys: ?IdentityKeysBlob; if (signedIdentityKeysBlob) { identityKeys = JSON.parse(signedIdentityKeysBlob.payload); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } const olmUtil: OlmUtility = getOlmUtility(); try { olmUtil.ed25519_verify( identityKeys.primaryIdentityPublicKeys.ed25519, signedIdentityKeysBlob.payload, signedIdentityKeysBlob.signature, ); } catch (e) { throw new ServerError('invalid_signature'); } } // 7. Ensure that `primaryIdentityPublicKeys.ed25519` matches SIWE // statement `primaryIdentityPublicKey` if `identityKeys` exists. if ( identityKeys && identityKeys.primaryIdentityPublicKeys.ed25519 !== primaryIdentityPublicKey ) { throw new ServerError('primary_public_key_mismatch'); } // 8. Construct `SIWESocialProof` object with the stringified // SIWEMessage and the corresponding signature. const socialProof: SIWESocialProof = { siweMessage: siweMessage.toMessage(), siweMessageSignature: signature, }; // 9. Create account if address does not correspond to an existing user. const userID = await (async () => { if (existingUserID) { return existingUserID; } const time = Date.now(); const [id] = await createIDs('users', 1); const newUserRow = [id, siweMessage.address, siweMessage.address, time]; const newUserQuery = SQL` INSERT INTO users(id, username, ethereum_address, creation_time) VALUES ${[newUserRow]} `; await dbQuery(newUserQuery); return id; })(); // 10. Complete login with call to `processSuccessfulLogin(...)`. const processSuccessfulLoginResult = await processSuccessfulLogin({ viewer, platformDetails, deviceTokenUpdateRequest, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, shouldMarkPoliciesAsAcceptedAfterCookieCreation: !existingUserID, }); // 11. Create messages with call to `sendMessagesOnAccountCreation(...)`, // if the account has just been registered. Also, set the username as // reserved. if (!existingUserID) { await sendMessagesOnAccountCreation(viewer); ignorePromiseRejections( createAndSendReservedUsernameMessage([ { username: siweMessage.address, userID }, ]), ); } // 12. Fetch data from MariaDB for the response. return await handleSuccessfulLoginResult(processSuccessfulLoginResult, { viewer, watchedIDs, calendarQuery, }); } export const keyserverAuthRequestInputValidator: TInterface = tShape({ userID: tUserID, deviceID: t.String, calendarQuery: entryQueryInputValidator, deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, watchedIDs: t.list(tID), initialContentEncryptedMessage: t.String, initialNotificationsEncryptedMessage: t.String, doNotRegister: t.Boolean, source: t.maybe(t.enums.of(values(authActionSources))), }); async function keyserverAuthResponder( viewer: Viewer, request: KeyserverAuthRequest, ): Promise { const { userID, deviceID, initialContentEncryptedMessage, initialNotificationsEncryptedMessage, doNotRegister, } = request; const calendarQuery = normalizeCalendarQuery(request.calendarQuery); // 1. Check if there's already a user for this userID. Simultaneously, get // info for identity service auth. const [existingUsername, authDeviceID, identityInfo, rustAPI] = await Promise.all([ fetchUsername(userID), getContentSigningKey(), verifyUserLoggedIn(), getRustAPI(), verifyCalendarQueryThreadIDs(calendarQuery), ]); if (!existingUsername && doNotRegister) { throw new ServerError('account_does_not_exist'); } if (!identityInfo) { throw new ServerError('account_not_registered_on_identity_service'); } // 2. Get user's keys from identity service. let inboundKeysForUser; try { inboundKeysForUser = await rustAPI.getInboundKeysForUserDevice( identityInfo.userId, authDeviceID, identityInfo.accessToken, userID, deviceID, ); } catch (e) { console.log(e); throw new ServerError('failed_to_retrieve_inbound_keys'); } const username = inboundKeysForUser.username ? inboundKeysForUser.username : inboundKeysForUser.walletAddress; if (!username) { throw new ServerError('user_identifier_missing'); } const identityKeys: IdentityKeysBlob = JSON.parse(inboundKeysForUser.payload); if (!identityKeysBlobValidator.is(identityKeys)) { throw new ServerError('invalid_identity_keys_blob'); } // 3. Create content olm session. (The notif session was introduced first and // as such is created in legacy auth responders as well. It's factored out // into in the shared utility `processSuccessfulLogin(...)`.) const pickledContentOlmSessionPromise = createOlmSession( initialContentEncryptedMessage, 'content', identityKeys.primaryIdentityPublicKeys.curve25519, ); // 4. Create account if username does not correspond to an existing user. const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: inboundKeysForUser.payload, signature: inboundKeysForUser.payloadSignature, }; const olmAccountCreationPromise = (async () => { if (existingUsername) { return; } const time = Date.now(); const newUserRow = [ userID, username, inboundKeysForUser.walletAddress, time, ]; const newUserQuery = SQL` INSERT INTO users(id, username, ethereum_address, creation_time) VALUES ${[newUserRow]} `; await dbQuery(newUserQuery); })(); const [pickledContentOlmSession] = await Promise.all([ pickledContentOlmSessionPromise, olmAccountCreationPromise, ]); // 5. Complete login with call to `processSuccessfulLogin(...)`. const processSuccessfulLoginResult = await processSuccessfulLogin({ viewer, platformDetails: request.platformDetails, deviceTokenUpdateRequest: request.deviceTokenUpdateRequest, userID, calendarQuery, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, pickledContentOlmSession, shouldMarkPoliciesAsAcceptedAfterCookieCreation: !existingUsername, }); // 6. Create messages with call to `sendMessagesOnAccountCreation(...)`, // if the account has just been registered. if (!existingUsername) { await sendMessagesOnAccountCreation(viewer); } // 7. Fetch data from MariaDB for the response. return await handleSuccessfulLoginResult(processSuccessfulLoginResult, { viewer, watchedIDs: request.watchedIDs, calendarQuery, }); } export const updatePasswordRequestInputValidator: TInterface = tShape({ code: t.String, password: tPassword, watchedIDs: t.list(tID), calendarQuery: t.maybe(entryQueryInputValidator), deviceTokenUpdateRequest: t.maybe(deviceTokenUpdateRequestInputValidator), platformDetails: tPlatformDetails, }); async function oldPasswordUpdateResponder( viewer: Viewer, request: UpdatePasswordRequest, ): Promise { if (request.calendarQuery) { request.calendarQuery = normalizeCalendarQuery(request.calendarQuery); } return await updatePassword(viewer, request); } export const updateUserSettingsInputValidator: TInterface = tShape({ name: t.irreducible( userSettingsTypes.DEFAULT_NOTIFICATIONS, x => x === userSettingsTypes.DEFAULT_NOTIFICATIONS, ), data: t.enums.of(notificationTypeValues), }); async function updateUserSettingsResponder( viewer: Viewer, request: UpdateUserSettingsRequest, ): Promise { await updateUserSettings(viewer, request); } export const policyAcknowledgmentRequestInputValidator: TInterface = tShape({ policy: t.maybe(t.enums.of(policies)), }); async function policyAcknowledgmentResponder( viewer: Viewer, request: PolicyAcknowledgmentRequest, ): Promise { await viewerAcknowledgmentUpdater(viewer, request.policy); } async function updateUserAvatarResponder( viewer: Viewer, request: UpdateUserAvatarRequest, ): Promise { return await updateUserAvatar(viewer, request); } +export const claimUsernameRequestInputValidator: TInterface = + tShape({ + username: t.String, + password: tPassword, + }); + async function claimUsernameResponder( viewer: Viewer, + request: ClaimUsernameRequest, ): Promise { - const [username, accountInfo] = await Promise.all([ - fetchUsername(viewer.userID), + const username = request.username; + + const userQuery = SQL` + SELECT id, hash, username + FROM users + WHERE LCASE(username) = LCASE(${request.username}) + `; + const [[userResult], accountInfo] = await Promise.all([ + dbQuery(userQuery), fetchOlmAccount('content'), ]); - if (!username) { + if (userResult.length === 0) { throw new ServerError('invalid_credentials'); } + const userRow = userResult[0]; + + if (!userRow.hash) { + throw new ServerError('invalid_parameters'); + } + + if (!bcrypt.compareSync(request.password, userRow.hash)) { + throw new ServerError('invalid_credentials'); + } + + const userID = userRow.id; + const issuedAt = new Date().toISOString(); const reservedUsernameMessage: ReservedUsernameMessage = { statement: 'This user is the owner of the following username and user ID', payload: { username, - userID: viewer.userID, + userID, }, issuedAt, }; const message = JSON.stringify(reservedUsernameMessage); const signature = accountInfo.account.sign(message); return { message, signature }; } export { userSubscriptionUpdateResponder, passwordUpdateResponder, sendVerificationEmailResponder, sendPasswordResetEmailResponder, logOutResponder, accountDeletionResponder, accountCreationResponder, logInResponder, siweAuthResponder, oldPasswordUpdateResponder, updateUserSettingsResponder, policyAcknowledgmentResponder, updateUserAvatarResponder, claimUsernameResponder, keyserverAuthResponder, }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index 46f5dfd5e..13d0cd605 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,1010 +1,1012 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { usePreRequestUserState } from '../selectors/account-selectors.js'; import { getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, } from '../shared/crypto-utils.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import type { LegacyLogInInfo, LegacyLogInResult, LegacyRegisterResult, LegacyRegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, + ClaimUsernameRequest, ClaimUsernameResponse, LogInRequest, KeyserverAuthResult, KeyserverAuthInfo, KeyserverAuthRequest, ClientLogInResponse, KeyserverLogOutResult, LogOutResult, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; import type { IdentityAuthResult } from '../types/identity-service-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../types/message-types.js'; import type { GetOlmSessionInitializationDataResponse } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { PreRequestUserState } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types'; import type { CurrentUserInfo, UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import sleep from '../utils/sleep.js'; const loggedOutUserInfo: LoggedOutUserInfo = { anonymous: true, }; export type KeyserverLogOutInput = { +preRequestUserState: PreRequestUserState, +keyserverIDs?: $ReadOnlyArray, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const keyserverLogOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } let response = null; try { response = await Promise.race([ callKeyserverEndpoint('log_out', requests), (async () => { await sleep(500); throw new Error('keyserver log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? loggedOutUserInfo : null; return { currentUserInfo, preRequestUserState, keyserverIDs }; }; function useLogOut(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const callKeyserverLogOut = useKeyserverCall(keyserverLogOut); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback( async (keyserverIDs?: $ReadOnlyArray) => { const identityPromise = (async () => { if (!usingCommServicesAccessToken || !commServicesAccessToken) { return; } if (!identityClient) { throw new Error('Identity service client is not initialized'); } try { await Promise.race([ identityClient.logOut(), (async () => { await sleep(500); throw new Error('identity log_out took more than 500ms'); })(), ]); } catch {} })(); const [{ keyserverIDs: _, ...result }] = await Promise.all([ callKeyserverLogOut({ preRequestUserState, keyserverIDs, }), identityPromise, ]); return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }, [ callKeyserverLogOut, commServicesAccessToken, identityClient, preRequestUserState, ], ); } function useIdentityLogOut(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback(async () => { invariant( usingCommServicesAccessToken, 'identityLogOut can only be called when usingCommServicesAccessToken', ); if (!identityClient) { throw new Error('Identity service client is not initialized'); } try { await Promise.race([ identityClient.logOut(), (async () => { await sleep(500); throw new Error('identity log_out took more than 500ms'); })(), ]); } catch {} return { currentUserInfo: null, preRequestUserState: { ...preRequestUserState, commServicesAccessToken, }, }; }, [commServicesAccessToken, identityClient, preRequestUserState]); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); -const claimUsernameCallSingleKeyserverEndpointOptions = { timeout: 500 }; const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, - ): (() => Promise) => - async () => { - const requests = { [authoritativeKeyserverID()]: {} }; - const responses = await callKeyserverEndpoint('claim_username', requests, { - ...claimUsernameCallSingleKeyserverEndpointOptions, - }); + ): (( + claimUsernameRequest: ClaimUsernameRequest, + ) => Promise) => + async (claimUsernameRequest: ClaimUsernameRequest) => { + const requests = { [authoritativeKeyserverID()]: claimUsernameRequest }; + const responses = await callKeyserverEndpoint('claim_username', requests); const response = responses[authoritativeKeyserverID()]; return { message: response.message, signature: response.signature, }; }; -function useClaimUsername(): () => Promise { +function useClaimUsername(): ( + claimUsernameRequest: ClaimUsernameRequest, +) => Promise { return useKeyserverCall(claimUsername); } const deleteKeyserverAccountActionTypes = Object.freeze({ started: 'DELETE_KEYSERVER_ACCOUNT_STARTED', success: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', failed: 'DELETE_KEYSERVER_ACCOUNT_FAILED', }); const deleteKeyserverAccount = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState, keyserverIDs, }; }; function useDeleteKeyserverAccount(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const preRequestUserState = usePreRequestUserState(); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); return React.useCallback( (keyserverIDs?: $ReadOnlyArray) => callKeyserverDeleteAccount({ preRequestUserState, keyserverIDs }), [callKeyserverDeleteAccount, preRequestUserState], ); } const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); function useDeleteAccount(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback(async () => { const identityPromise = (async () => { if (!usingCommServicesAccessToken) { return undefined; } if (!identityClient) { throw new Error('Identity service client is not initialized'); } if (!identityClient.deleteWalletUser) { throw new Error('Delete wallet user method unimplemented'); } return await identityClient.deleteWalletUser(); })(); const [keyserverResult] = await Promise.all([ callKeyserverDeleteAccount({ preRequestUserState, }), identityPromise, ]); const { keyserverIDs: _, ...result } = keyserverResult; return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }, [ callKeyserverDeleteAccount, commServicesAccessToken, identityClient, preRequestUserState, ]); } // Unlike useDeleteAccount, we always dispatch a success here (never throw). // That's because useDeleteAccount is used in a scenario where the user is // visibly logged-in, and we don't want to log them out unless we succeeded in // deleting their account. On the other hand, useDeleteDiscardedIdentityAccount // is used in a scenario where the user is visibly logged-out, and it's only // used to reset state (eg. Redux, SQLite) to a logged-out state. The state // reset only occurs when a success action is dispatched, so we always dispatch // a success. function useDeleteDiscardedIdentityAccount(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback(async () => { invariant( usingCommServicesAccessToken, 'deleteDiscardedIdentityAccount can only be called when ' + 'usingCommServicesAccessToken', ); if (!identityClient) { throw new Error('Identity service client is not initialized'); } if (!identityClient.deleteWalletUser) { throw new Error('Delete wallet user method unimplemented'); } try { await Promise.race([ identityClient.deleteWalletUser(), (async () => { await sleep(500); throw new Error('identity delete_wallet_user took more than 500ms'); })(), ]); } catch {} return { currentUserInfo: null, preRequestUserState: { ...preRequestUserState, commServicesAccessToken, }, }; }, [commServicesAccessToken, identityClient, preRequestUserState]); } const legacyKeyserverRegisterActionTypes = Object.freeze({ started: 'LEGACY_KEYSERVER_REGISTER_STARTED', success: 'LEGACY_KEYSERVER_REGISTER_SUCCESS', failed: 'LEGACY_KEYSERVER_REGISTER_FAILED', }); const legacyKeyserverRegisterCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const legacyKeyserverRegister = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( registerInfo: LegacyRegisterInfo, options?: CallSingleKeyserverEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[authoritativeKeyserverID()]; const { preRequestUserInfo, ...rest } = registerInfo; const response = await callSingleKeyserverEndpoint( 'create_account', { ...rest, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...legacyKeyserverRegisterCallSingleKeyserverEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; export type KeyserverAuthInput = $ReadOnly<{ ...KeyserverAuthInfo, +preRequestUserInfo: ?CurrentUserInfo, }>; const keyserverAuthActionTypes = Object.freeze({ started: 'KEYSERVER_AUTH_STARTED', success: 'KEYSERVER_AUTH_SUCCESS', failed: 'KEYSERVER_AUTH_FAILED', }); const keyserverAuthCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const keyserverAuth = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: KeyserverAuthInput) => Promise) => async keyserverAuthInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { authActionSource, calendarQuery, keyserverData, deviceTokenUpdateInput, preRequestUserInfo, ...restLogInInfo } = keyserverAuthInfo; const keyserverIDs = Object.keys(keyserverData); const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: KeyserverAuthRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: deviceTokenUpdateInput[keyserverID], watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, initialContentEncryptedMessage: keyserverData[keyserverID].initialContentEncryptedMessage, initialNotificationsEncryptedMessage: keyserverData[keyserverID].initialNotificationsEncryptedMessage, source: authActionSource, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'keyserver_auth', requests, keyserverAuthCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: keyserverAuthInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[authoritativeKeyserverID()]?.currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, authActionSource: keyserverAuthInfo.authActionSource, notAcknowledgedPolicies: responses[authoritativeKeyserverID()]?.notAcknowledgedPolicies, preRequestUserInfo, }; }; const identityRegisterActionTypes = Object.freeze({ started: 'IDENTITY_REGISTER_STARTED', success: 'IDENTITY_REGISTER_SUCCESS', failed: 'IDENTITY_REGISTER_FAILED', }); function useIdentityPasswordRegister(): ( username: string, password: string, fid: ?string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerPasswordUser) { throw new Error('Register password user method unimplemented'); } return identityClient.registerPasswordUser; } function useIdentityWalletRegister(): ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerWalletUser) { throw new Error('Register wallet user method unimplemented'); } return identityClient.registerWalletUser; } const identityGenerateNonceActionTypes = Object.freeze({ started: 'IDENTITY_GENERATE_NONCE_STARTED', success: 'IDENTITY_GENERATE_NONCE_SUCCESS', failed: 'IDENTITY_GENERATE_NONCE_FAILED', }); function useIdentityGenerateNonce(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return identityClient.generateNonce; } function mergeUserInfos( ...userInfoArrays: Array<$ReadOnlyArray> ): UserInfo[] { const merged: { [string]: UserInfo } = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } type WritableGenericMessagesResult = { messageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatuses, watchedIDsAtRequestTime: string[], currentAsOf: { [keyserverID: string]: number }, }; type WritableCalendarResult = { rawEntryInfos: RawEntryInfo[], calendarQuery: CalendarQuery, }; const identityLogInActionTypes = Object.freeze({ started: 'IDENTITY_LOG_IN_STARTED', success: 'IDENTITY_LOG_IN_SUCCESS', failed: 'IDENTITY_LOG_IN_FAILED', }); function useIdentityPasswordLogIn(): ( username: string, password: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = useSelector(state => state.currentUserInfo); return React.useCallback( (username, password) => { if (!identityClient) { throw new Error('Identity service client is not initialized'); } return (async () => { const result = await identityClient.logInPasswordUser( username, password, ); return { ...result, preRequestUserState, }; })(); }, [identityClient, preRequestUserState], ); } function useIdentityWalletLogIn(): ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return identityClient.logInWalletUser; } function useIdentitySecondaryDeviceLogIn(): ( userID: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); const { generateNonce, uploadKeysForRegisteredDeviceAndLogIn } = identityClient; const { signMessage } = getConfig().olmAPI; return React.useCallback( async (userID: string) => { const nonce = await generateNonce(); const nonceSignature = await signMessage(nonce); return await uploadKeysForRegisteredDeviceAndLogIn(userID, { nonce, nonceSignature, }); }, [generateNonce, signMessage, uploadKeysForRegisteredDeviceAndLogIn], ); } const legacyLogInActionTypes = Object.freeze({ started: 'LEGACY_LOG_IN_STARTED', success: 'LEGACY_LOG_IN_SUCCESS', failed: 'LEGACY_LOG_IN_FAILED', }); const legacyLogInCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const legacyLogIn = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LegacyLogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { authActionSource, calendarQuery, keyserverIDs: inputKeyserverIDs, preRequestUserInfo, ...restLogInInfo } = logInInfo; // Eventually the list of keyservers will be fetched from the // identity service const keyserverIDs = inputKeyserverIDs ?? [authoritativeKeyserverID()]; const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: LogInRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: logInInfo.deviceTokenUpdateRequest[keyserverID], source: authActionSource, watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'log_in', requests, legacyLogInCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[authoritativeKeyserverID()].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, authActionSource: logInInfo.authActionSource, notAcknowledgedPolicies: responses[authoritativeKeyserverID()].notAcknowledgedPolicies, preRequestUserInfo, }; }; function useLegacyLogIn(): ( input: LegacyLogInInfo, ) => Promise { return useKeyserverCall(legacyLogIn); } const changeKeyserverUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', success: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', failed: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', }); const changeKeyserverUserPassword = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callSingleKeyserverEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callSingleKeyserverEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((username: string) => Promise) => async username => { const response = await callSingleKeyserverEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SubscriptionUpdateRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_user_subscription', requests, ); const response = responses[keyserverID]; return { threadID: input.threadID, subscription: response.threadSubscription, }; }; function useUpdateSubscription(): ( input: SubscriptionUpdateRequest, ) => Promise { return useKeyserverCall(updateSubscription); } const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: UpdateUserSettingsRequest) => Promise) => async input => { const requests: { [string]: UpdateUserSettingsRequest } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = input; } await callKeyserverEndpoint('update_user_settings', requests); }; function useSetUserSettings(): ( input: UpdateUserSettingsRequest, ) => Promise { return useKeyserverCall(setUserSettings); } const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( options?: ?CallSingleKeyserverEndpointOptions, ) => Promise) => async options => { const olmInitData = await callSingleKeyserverEndpoint( 'get_olm_session_initialization_data', {}, options, ); return { signedIdentityKeysBlob: olmInitData.signedIdentityKeysBlob, contentInitializationInfo: { ...olmInitData.contentInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.contentInitializationInfo.oneTimeKey, )[0], prekey: getPrekeyValueFromBlob( olmInitData.contentInitializationInfo.prekey, ), }, notifInitializationInfo: { ...olmInitData.notifInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.notifInitializationInfo.oneTimeKey, )[0], prekey: getPrekeyValueFromBlob( olmInitData.notifInitializationInfo.prekey, ), }, }; }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callSingleKeyserverEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callSingleKeyserverEndpoint('update_user_avatar', avatarDBContent); return { updates }; }; export { changeKeyserverUserPasswordActionTypes, changeKeyserverUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, legacyLogIn as legacyLogInRawAction, identityLogInActionTypes, useIdentityPasswordLogIn, useIdentityWalletLogIn, useIdentitySecondaryDeviceLogIn, useLegacyLogIn, legacyLogInActionTypes, useLogOut, useIdentityLogOut, logOutActionTypes, legacyKeyserverRegister, legacyKeyserverRegisterActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, deleteAccountActionTypes, useDeleteAccount, useDeleteDiscardedIdentityAccount, keyserverAuthActionTypes, keyserverAuth as keyserverAuthRawAction, identityRegisterActionTypes, useIdentityPasswordRegister, useIdentityWalletRegister, identityGenerateNonceActionTypes, useIdentityGenerateNonce, }; diff --git a/lib/types/account-types.js b/lib/types/account-types.js index 9b90ea936..5a2494ec8 100644 --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,301 +1,306 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import type { PlatformDetails } from './device-types.js'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types.js'; import { type RawMessageInfo, type MessageTruncationStatuses, type GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState, IdentityCallPreRequestUserState, } from './session-types.js'; import { type MixedRawThreadInfos, type RawThreadInfos, } from './thread-types.js'; import type { CurrentUserInfo, UserInfo, LoggedOutUserInfo, LoggedInUserInfo, } from './user-types'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; import { tShape } from '../utils/validation-utils.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, }; export type LogOutResult = { +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: IdentityCallPreRequestUserState, }; export type KeyserverLogOutResult = $ReadOnly<{ +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: PreRequestUserState, +keyserverIDs: $ReadOnlyArray, }>; export type LogOutResponse = { +currentUserInfo: LoggedOutUserInfo, }; export type LegacyRegisterInfo = { ...LegacyLogInExtraInfo, +username: string, +password: string, }; export type DeviceTokenUpdateRequest = { +deviceToken: string, }; type DeviceTokenUpdateInput = { +[keyserverID: string]: DeviceTokenUpdateRequest, }; export type RegisterRequest = { +username: string, +email?: empty, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type RegisterResponse = { +id: string, +rawMessageInfos: $ReadOnlyArray, +currentUserInfo: LoggedInUserInfo, +cookieChange: { +threadInfos: MixedRawThreadInfos, +userInfos: $ReadOnlyArray, }, }; export type LegacyRegisterResult = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +threadInfos: RawThreadInfos, +userInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export const recoveryFromReduxActionSources = Object.freeze({ cookieInvalidationResolutionAttempt: 'COOKIE_INVALIDATION_RESOLUTION_ATTEMPT', appStartCookieLoggedInButInvalidRedux: 'APP_START_COOKIE_LOGGED_IN_BUT_INVALID_REDUX', appStartReduxLoggedInButInvalidCookie: 'APP_START_REDUX_LOGGED_IN_BUT_INVALID_COOKIE', socketAuthErrorResolutionAttempt: 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT', refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT', socketNotLoggedIn: 'SOCKET_NOT_LOGGED_IN', }); export type RecoveryFromReduxActionSource = $Values< typeof recoveryFromReduxActionSources, >; export const recoveryFromDataHandlerActionSources = Object.freeze({ //sqliteOpFailure: 'SQLITE_OP_FAILURE', (DEPRECATED) sqliteLoadFailure: 'SQLITE_LOAD_FAILURE', corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION', }); export type RecoveryFromDataHandlerActionSource = $Values< typeof recoveryFromDataHandlerActionSources, >; export type RecoveryActionSource = | RecoveryFromReduxActionSource | RecoveryFromDataHandlerActionSource; export const logInActionSources = Object.freeze({ logInFromWebForm: 'LOG_IN_FROM_WEB_FORM', logInFromNativeForm: 'LOG_IN_FROM_NATIVE_FORM', logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE', keyserverAuthFromNative: 'KEYSERVER_AUTH_FROM_NATIVE', keyserverAuthFromWeb: 'KEYSERVER_AUTH_FROM_WEB', }); export type LogInActionSource = $Values; export const authActionSources = Object.freeze({ ...recoveryFromReduxActionSources, ...recoveryFromDataHandlerActionSources, ...logInActionSources, }); export type AuthActionSource = LogInActionSource | RecoveryActionSource; export type LegacyLogInStartingPayload = { +calendarQuery: CalendarQuery, +authActionSource?: AuthActionSource, }; export type LegacyLogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest: DeviceTokenUpdateInput, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, +preRequestUserInfo: ?CurrentUserInfo, }; export type LegacyLogInInfo = { ...LegacyLogInExtraInfo, +username: string, +password: string, +authActionSource: AuthActionSource, +keyserverIDs?: $ReadOnlyArray, }; export type LogInRequest = { +usernameOrEmail?: ?string, +username?: ?string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +source?: AuthActionSource, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type ServerLogInResponse = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +rawEntryInfos?: ?$ReadOnlyArray, +serverTime: number, +cookieChange: { +threadInfos: MixedRawThreadInfos, +userInfos: $ReadOnlyArray, }, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type ClientLogInResponse = $ReadOnly<{ ...ServerLogInResponse, +cookieChange: $ReadOnly<{ ...$PropertyType, threadInfos: RawThreadInfos, }>, }>; export type LegacyLogInResult = { +threadInfos: RawThreadInfos, +currentUserInfo: LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: { +[keyserverID: string]: number }, +authActionSource: AuthActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, +preRequestUserInfo: ?CurrentUserInfo, }; export type KeyserverAuthResult = { +threadInfos: RawThreadInfos, +currentUserInfo?: ?LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: { +[keyserverID: string]: number }, +authActionSource: AuthActionSource, +notAcknowledgedPolicies?: ?$ReadOnlyArray, +preRequestUserInfo: ?CurrentUserInfo, }; type KeyserverRequestData = { +initialContentEncryptedMessage: string, +initialNotificationsEncryptedMessage: string, }; export type KeyserverAuthInfo = { +userID: string, +deviceID: string, +doNotRegister: boolean, +calendarQuery: CalendarQuery, +deviceTokenUpdateInput: DeviceTokenUpdateInput, +authActionSource: AuthActionSource, +keyserverData: { +[keyserverID: string]: KeyserverRequestData }, }; export type KeyserverAuthRequest = $ReadOnly<{ ...KeyserverRequestData, +userID: string, +deviceID: string, +doNotRegister: boolean, +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +watchedIDs: $ReadOnlyArray, +platformDetails: PlatformDetails, +source?: AuthActionSource, }>; export type UpdatePasswordRequest = { code: string, password: string, calendarQuery?: ?CalendarQuery, deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, platformDetails: PlatformDetails, watchedIDs: $ReadOnlyArray, }; export type PolicyAcknowledgmentRequest = { +policy: PolicyType, }; export type EmailSubscriptionRequest = { +email: string, }; export type UpdateUserSettingsRequest = { +name: 'default_user_notifications', +data: NotificationTypes, }; export const userSettingsTypes = Object.freeze({ DEFAULT_NOTIFICATIONS: 'default_user_notifications', }); export const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', BACKGROUND: 'background', }); export type NotificationTypes = $Values; export const notificationTypeValues: $ReadOnlyArray = values(notificationTypes); export type DefaultNotificationPayload = { +default_user_notifications: ?NotificationTypes, }; export const defaultNotificationPayloadValidator: TInterface = tShape({ default_user_notifications: t.maybe(t.enums.of(notificationTypeValues)), }); +export type ClaimUsernameRequest = { + +username: string, + +password: string, +}; + export type ClaimUsernameResponse = { +message: string, +signature: string, };