diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js index 87b7a6e08..34d04ebef 100644 --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -1,339 +1,339 @@ // @flow import t from 'tcomb'; import type { TInterface, TUnion } from 'tcomb'; +import { rawThreadInfoValidator } from 'lib/permissions/minimally-encoded-thread-permissions-validators.js'; import { mediaValidator } from 'lib/types/media-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from 'lib/types/message-types.js'; import { userSurfacedPermissionValidator } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadDeletionRequest, type RoleChangeRequest, type ChangeThreadSettingsResult, type RemoveMembersRequest, type LeaveThreadRequest, type LeaveThreadResult, type UpdateThreadRequest, type ServerNewThreadRequest, type NewThreadResponse, type ServerThreadJoinRequest, type ThreadJoinResult, type ThreadFetchMediaResult, type ThreadFetchMediaRequest, type ToggleMessagePinRequest, type ToggleMessagePinResult, type RoleModificationRequest, type RoleModificationResult, type RoleDeletionRequest, type RoleDeletionResult, - legacyRawThreadInfoValidator, } from 'lib/types/thread-types.js'; import { serverUpdateInfoValidator } from 'lib/types/update-types.js'; import { userInfosValidator } from 'lib/types/user-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { values } from 'lib/utils/objects.js'; import { tShape, tNumEnum, tColor, tPassword, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { modifyRole } from '../creators/role-creator.js'; import { createThread } from '../creators/thread-creator.js'; import { deleteRole } from '../deleters/role-deleters.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { fetchMediaForThread } from '../fetchers/upload-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateRole, removeMembers, leaveThread, updateThread, joinThread, toggleMessagePinForThread, } from '../updaters/thread-updaters.js'; export const threadDeletionRequestInputValidator: TInterface = tShape({ threadID: tID, accountPassword: t.maybe(tPassword), }); export const leaveThreadResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function threadDeletionResponder( viewer: Viewer, request: ThreadDeletionRequest, ): Promise { return await deleteThread(viewer, request); } export const roleChangeRequestInputValidator: TInterface = tShape({ threadID: tID, memberIDs: t.list(t.String), role: t.refinement(tID, str => { if (str.indexOf('|') !== -1) { str = str.split('|')[1]; } const int = parseInt(str, 10); return String(int) === str && int > 0; }), }); export const changeThreadSettingsResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleUpdateResponder( viewer: Viewer, request: RoleChangeRequest, ): Promise { return await updateRole(viewer, request); } export const removeMembersRequestInputValidator: TInterface = tShape({ threadID: tID, memberIDs: t.list(t.String), }); async function memberRemovalResponder( viewer: Viewer, request: RemoveMembersRequest, ): Promise { return await removeMembers(viewer, request); } export const leaveThreadRequestInputValidator: TInterface = tShape({ threadID: tID, }); async function threadLeaveResponder( viewer: Viewer, request: LeaveThreadRequest, ): Promise { return await leaveThread(viewer, request); } export const updateThreadRequestInputValidator: TInterface = tShape({ threadID: tID, changes: tShape({ type: t.maybe(tNumEnum(values(threadTypes))), name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), newMemberIDs: t.maybe(t.list(t.String)), avatar: t.maybe(updateUserAvatarRequestValidator), }), accountPassword: t.maybe(tPassword), }); async function threadUpdateResponder( viewer: Viewer, request: UpdateThreadRequest, ): Promise { return await updateThread(viewer, request); } const threadRequestValidationShape = { name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), initialMemberIDs: t.maybe(t.list(t.String)), calendarQuery: t.maybe(entryQueryInputValidator), }; const newThreadRequestInputValidator: TUnion = t.union([ tShape({ type: tNumEnum([threadTypes.SIDEBAR]), sourceMessageID: tID, ...threadRequestValidationShape, }), tShape({ type: tNumEnum([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.PERSONAL, threadTypes.LOCAL, threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]), ...threadRequestValidationShape, }), ]); export const newThreadResponseValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), newMessageInfos: t.list(rawMessageInfoValidator), userInfos: userInfosValidator, newThreadID: tID, }); async function threadCreationResponder( viewer: Viewer, request: ServerNewThreadRequest, ): Promise { return await createThread(viewer, request, { silentlyFailMembers: request.type === threadTypes.SIDEBAR, }); } export const joinThreadRequestInputValidator: TInterface = tShape({ threadID: tID, calendarQuery: t.maybe(entryQueryInputValidator), inviteLinkSecret: t.maybe(t.String), }); export const threadJoinResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, }); async function threadJoinResponder( viewer: Viewer, request: ServerThreadJoinRequest, ): Promise { if (request.calendarQuery) { await verifyCalendarQueryThreadIDs(request.calendarQuery); } return await joinThread(viewer, request); } export const threadFetchMediaRequestInputValidator: TInterface = tShape({ threadID: tID, limit: t.Number, offset: t.Number, }); export const threadFetchMediaResultValidator: TInterface = tShape({ media: t.list(mediaValidator) }); async function threadFetchMediaResponder( viewer: Viewer, request: ThreadFetchMediaRequest, ): Promise { return await fetchMediaForThread(viewer, request); } export const toggleMessagePinRequestInputValidator: TInterface = tShape({ messageID: tID, action: t.enums.of(['pin', 'unpin']), }); export const toggleMessagePinResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, }); async function toggleMessagePinResponder( viewer: Viewer, request: ToggleMessagePinRequest, ): Promise { return await toggleMessagePinForThread(viewer, request); } export const roleModificationRequestInputValidator: TUnion = t.union([ tShape({ community: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['create_role']), }), tShape({ community: tID, existingRoleID: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['edit_role']), }), ]); export const roleModificationResultValidator: TInterface = tShape({ - threadInfo: t.maybe(legacyRawThreadInfoValidator), + threadInfo: t.maybe(rawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleModificationResponder( viewer: Viewer, request: RoleModificationRequest, ): Promise { return await modifyRole(viewer, request); } export const roleDeletionRequestInputValidator: TInterface = tShape({ community: tID, roleID: tID, }); export const roleDeletionResultValidator: TInterface = tShape({ - threadInfo: t.maybe(legacyRawThreadInfoValidator), + threadInfo: t.maybe(rawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleDeletionResponder( viewer: Viewer, request: RoleDeletionRequest, ): Promise { return await deleteRole(viewer, request); } export { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadJoinResponder, threadFetchMediaResponder, newThreadRequestInputValidator, toggleMessagePinResponder, roleModificationResponder, roleDeletionResponder, }; diff --git a/keyserver/src/responders/user-responders.js b/keyserver/src/responders/user-responders.js index 656e78ef6..b86c0619f 100644 --- a/keyserver/src/responders/user-responders.js +++ b/keyserver/src/responders/user-responders.js @@ -1,779 +1,778 @@ // @flow import type { Utility as OlmUtility } from '@commapp/olm'; import invariant from 'invariant'; import { ErrorTypes, SiweMessage } from 'siwe'; import t, { type TInterface, type TUnion, type TEnums } from 'tcomb'; import bcrypt from 'twin-bcrypt'; import { baseLegalPolicies, policies, policyTypeValidator, } from 'lib/facts/policies.js'; import { rawThreadInfoValidator } from 'lib/permissions/minimally-encoded-thread-permissions-validators.js'; import { hasMinCodeVersion } from 'lib/shared/version-utils.js'; import type { ResetPasswordRequest, LogOutResponse, RegisterResponse, RegisterRequest, LogInResponse, LogInRequest, UpdatePasswordRequest, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, } from 'lib/types/account-types.js'; import { userSettingsTypes, notificationTypeValues, logInActionSources, } from 'lib/types/account-types.js'; import { type ClientAvatar, clientAvatarValidator, type UpdateUserAvatarResponse, type UpdateUserAvatarRequest, } from 'lib/types/avatar-types.js'; import type { ReservedUsernameMessage, IdentityKeysBlob, SignedIdentityKeysBlob, } from 'lib/types/crypto-types.js'; import type { DeviceType } from 'lib/types/device-types'; import { type CalendarQuery, rawEntryInfoValidator, type FetchEntryInfosBase, } from 'lib/types/entry-types.js'; import { defaultNumberPerThread, rawMessageInfoValidator, messageTruncationStatusesValidator, } from 'lib/types/message-types.js'; import type { SIWEAuthRequest, SIWEMessage, SIWESocialProof, } from 'lib/types/siwe-types.js'; import { type SubscriptionUpdateRequest, type SubscriptionUpdateResponse, threadSubscriptionValidator, } from 'lib/types/subscription-types.js'; -import { legacyRawThreadInfoValidator } from 'lib/types/thread-types.js'; import { createUpdatesResultValidator } from 'lib/types/update-types.js'; import { type PasswordUpdate, loggedOutUserInfoValidator, loggedInUserInfoValidator, userInfoValidator, } 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 { getPublicKeyFromSIWEStatement, isValidSIWEMessage, isValidSIWEStatementWithPublicKey, primaryIdentityPublicKeyRegex, } from 'lib/utils/siwe-utils.js'; import { tShape, tPlatformDetails, tPassword, tEmail, tOldValidUsername, tRegex, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, newEntryQueryInputValidator, normalizeCalendarQuery, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { createAccount, processSIWEAccountCreation, } from '../creators/account-creator.js'; import { createOlmSession } 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 { accountUpdater, 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 { getOlmUtility } from '../utils/olm-utils.js'; export const subscriptionUpdateRequestInputValidator: TInterface = tShape({ threadID: tID, updatedFields: tShape({ pushNotifs: t.maybe(t.Boolean), home: t.maybe(t.Boolean), }), }); export const subscriptionUpdateResponseValidator: TInterface = tShape({ threadSubscription: threadSubscriptionValidator, }); 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 accountUpdater(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); } export const logOutResponseValidator: TInterface = tShape({ currentUserInfo: loggedOutUserInfoValidator, }); 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), }); export const registerResponseValidator: TInterface = tShape({ id: t.String, rawMessageInfos: t.list(rawMessageInfoValidator), currentUserInfo: loggedInUserInfoValidator, cookieChange: tShape({ - threadInfos: t.dict(tID, legacyRawThreadInfoValidator), + threadInfos: t.dict(tID, rawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), }); 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, +input: any, +userID: string, +calendarQuery: ?CalendarQuery, +socialProof?: ?SIWESocialProof, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; async function processSuccessfulLogin( params: ProcessSuccessfulLoginParams, ): Promise { const { viewer, input, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, } = params; const request: LogInRequest = input; const newServerTime = Date.now(); const deviceToken = request.deviceTokenUpdateRequest ? request.deviceTokenUpdateRequest.deviceToken : viewer.deviceToken; const [userViewerData, notAcknowledgedPolicies] = await Promise.all([ createNewUserCookie(userID, { platformDetails: request.platformDetails, deviceToken, socialProof, signedIdentityKeysBlob, }), fetchNotAcknowledgedPolicies(userID, baseLegalPolicies), deleteCookie(viewer.cookieID), ]); viewer.setNewCookie(userViewerData); if ( notAcknowledgedPolicies.length && hasMinCodeVersion(viewer.platformDetails, { native: 181 }) ) { const currentUserInfo = await fetchLoggedInUserInfo(viewer); return { notAcknowledgedPolicies, currentUserInfo: currentUserInfo, rawMessageInfos: [], truncationStatuses: {}, userInfos: [], rawEntryInfos: [], serverTime: 0, cookieChange: { threadInfos: {}, userInfos: [], }, }; } if (calendarQuery) { await setNewSession(viewer, calendarQuery, newServerTime); } const olmSessionPromise = (async () => { if ( userViewerData.cookieID && initialNotificationsEncryptedMessage && signedIdentityKeysBlob ) { await createOlmSession( initialNotificationsEncryptedMessage, 'notifications', userViewerData.cookieID, ); } })(); const threadCursors: { [string]: null } = {}; for (const watchedThreadID of request.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), olmSessionPromise, ]); const rawEntryInfos = entriesResult ? entriesResult.rawEntryInfos : null; const response: LogInResponse = { currentUserInfo, rawMessageInfos: messagesResult.rawMessageInfos, truncationStatuses: messagesResult.truncationStatuses, serverTime: newServerTime, userInfos: values(userInfos), cookieChange: { threadInfos: threadsResult.threadInfos, userInfos: [], }, }; if (rawEntryInfos) { return { ...response, rawEntryInfos, }; } return response; } 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(logInActionSources))), // 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), }); export const logInResponseValidator: TInterface = tShape({ currentUserInfo: loggedInUserInfoValidator, rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: t.list(userInfoValidator), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), serverTime: t.Number, cookieChange: tShape({ threadInfos: t.dict(tID, rawThreadInfoValidator), userInfos: t.list(userInfoValidator), }), notAcknowledgedPolicies: t.maybe(t.list(policyTypeValidator)), }); 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(); return await processSuccessfulLogin({ viewer, input: request, userID: id, calendarQuery, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, }); } 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, } = 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. 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'); } // 3. Validate SIWEMessage signature and handle possible errors. try { await siweMessage.validate(signature); } catch (error) { if (error === ErrorTypes.EXPIRED_MESSAGE) { // Thrown when the `expirationTime` is present and in the past. throw new ServerError('expired_message'); } else if (error === ErrorTypes.INVALID_SIGNATURE) { // Thrown when the `validate()` function can't verify the message. throw new ServerError('invalid_signature'); } else if (error === ErrorTypes.MALFORMED_SESSION) { // Thrown when some required field is missing. throw new ServerError('malformed_session'); } else { throw new ServerError('unknown_error'); } } // 4. 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'); } // 5. 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'); } } // 6. Ensure that `primaryIdentityPublicKeys.ed25519` matches SIWE // statement `primaryIdentityPublicKey` if `identityKeys` exists. if ( identityKeys && identityKeys.primaryIdentityPublicKeys.ed25519 !== primaryIdentityPublicKey ) { throw new ServerError('primary_public_key_mismatch'); } // 7. Construct `SIWESocialProof` object with the stringified // SIWEMessage and the corresponding signature. const socialProof: SIWESocialProof = { siweMessage: siweMessage.toMessage(), siweMessageSignature: signature, }; // 8. Create account with call to `processSIWEAccountCreation(...)` // if address does not correspond to an existing user. let userID = await fetchUserIDForEthereumAddress(siweMessage.address); if (!userID && doNotRegister) { throw new ServerError('account_does_not_exist'); } else if (!userID) { const siweAccountCreationRequest = { address: siweMessage.address, calendarQuery, deviceTokenUpdateRequest, platformDetails, socialProof, }; userID = await processSIWEAccountCreation( viewer, siweAccountCreationRequest, ); } // 9. Complete login with call to `processSuccessfulLogin(...)`. return await processSuccessfulLogin({ viewer, input: request, userID, calendarQuery, socialProof, signedIdentityKeysBlob, initialNotificationsEncryptedMessage, }); } 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); } export const updateUserAvatarResponseValidator: TInterface = tShape({ updates: createUpdatesResultValidator, }); export const updateUserAvatarResponderValidator: TUnion< ?ClientAvatar | UpdateUserAvatarResponse, > = t.union([ t.maybe(clientAvatarValidator), updateUserAvatarResponseValidator, ]); async function updateUserAvatarResponder( viewer: Viewer, request: UpdateUserAvatarRequest, ): Promise { return await updateUserAvatar(viewer, request); } export const claimUsernameResponseValidator: TInterface = tShape({ message: t.String, signature: t.String, }); async function claimUsernameResponder( viewer: Viewer, ): Promise { const [username, accountInfo] = await Promise.all([ fetchUsername(viewer.userID), fetchOlmAccount('content'), ]); if (!username) { throw new ServerError('invalid_credentials'); } 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, }, 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, }; diff --git a/lib/shared/updates/join-thread-spec.js b/lib/shared/updates/join-thread-spec.js index 8341acdcb..022bbcfc2 100644 --- a/lib/shared/updates/join-thread-spec.js +++ b/lib/shared/updates/join-thread-spec.js @@ -1,179 +1,177 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import t from 'tcomb'; import type { UpdateInfoFromRawInfoParams, UpdateSpec } from './update-spec.js'; +import { rawThreadInfoValidator } from '../../permissions/minimally-encoded-thread-permissions-validators.js'; import { type RawEntryInfo, rawEntryInfoValidator, } from '../../types/entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../../types/message-types.js'; import { messageTruncationStatusValidator, rawMessageInfoValidator, } from '../../types/message-types.js'; -import { - legacyRawThreadInfoValidator, - type RawThreadInfos, -} from '../../types/thread-types.js'; +import { type RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, ThreadJoinUpdateData, } from '../../types/update-types.js'; import { tNumber, tShape } from '../../utils/validation-utils.js'; import { combineTruncationStatuses } from '../message-utils.js'; import { threadInFilterList } from '../thread-utils.js'; export const joinThreadSpec: UpdateSpec< ThreadJoinUpdateInfo, ThreadJoinRawUpdateInfo, ThreadJoinUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadJoinUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, mergeEntryInfos( entryIDs: Set, mergedEntryInfos: Array, update: ThreadJoinUpdateInfo, ) { for (const entryInfo of update.rawEntryInfos) { const entryID = entryInfo.id; if (!entryID || entryIDs.has(entryID)) { continue; } mergedEntryInfos.push(entryInfo); entryIDs.add(entryID); } }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadJoinUpdateInfo, ) { if ( !threadInFilterList(update.threadInfo) || filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set([...filteredThreadIDs, update.threadInfo.id]); }, getRawMessageInfos(update: ThreadJoinUpdateInfo) { return update.rawMessageInfos; }, mergeMessageInfosAndTruncationStatuses( messageIDs: Set, messageInfos: Array, truncationStatuses: MessageTruncationStatuses, update: ThreadJoinUpdateInfo, ) { for (const messageInfo of update.rawMessageInfos) { const messageID = messageInfo.id; if (!messageID || messageIDs.has(messageID)) { continue; } messageInfos.push(messageInfo); messageIDs.add(messageID); } truncationStatuses[update.threadInfo.id] = combineTruncationStatuses( update.truncationStatus, truncationStatuses[update.threadInfo.id], ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.JOIN_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, updateContentForServerDB(data: ThreadJoinUpdateData) { const { threadID } = data; return JSON.stringify({ threadID }); }, entitiesToFetch(update: ThreadJoinRawUpdateInfo) { return { threadID: update.threadID, detailedThreadID: update.threadID, }; }, rawInfoFromData(data: ThreadJoinUpdateData, id: string) { return { type: updateTypes.JOIN_THREAD, id, time: data.time, threadID: data.threadID, }; }, updateInfoFromRawInfo( info: ThreadJoinRawUpdateInfo, params: UpdateInfoFromRawInfoParams, ) { const { data, rawEntryInfosByThreadID, rawMessageInfosByThreadID } = params; const { threadInfos, calendarResult, messageInfosResult } = data; const threadInfo = threadInfos[info.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.JOIN_THREAD because we couldn't " + `fetch RawThreadInfo for ${info.threadID}`, ); return null; } invariant(calendarResult, 'should be set'); const rawEntryInfos = rawEntryInfosByThreadID[info.threadID] ?? []; invariant(messageInfosResult, 'should be set'); const rawMessageInfos = rawMessageInfosByThreadID[info.threadID] ?? []; return { type: updateTypes.JOIN_THREAD, id: info.id, time: info.time, threadInfo, rawMessageInfos, truncationStatus: messageInfosResult.truncationStatuses[info.threadID], rawEntryInfos, }; }, deleteCondition: 'all_types', keyForUpdateData(data: ThreadJoinUpdateData) { return data.threadID; }, keyForUpdateInfo(info: ThreadJoinUpdateInfo) { return info.threadInfo.id; }, typesOfReplacedUpdatesForMatchingKey: 'all_types', infoValidator: tShape({ type: tNumber(updateTypes.JOIN_THREAD), id: t.String, time: t.Number, - threadInfo: legacyRawThreadInfoValidator, + threadInfo: rawThreadInfoValidator, rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatus: messageTruncationStatusValidator, rawEntryInfos: t.list(rawEntryInfoValidator), }), }); diff --git a/lib/shared/updates/update-thread-spec.js b/lib/shared/updates/update-thread-spec.js index a2909ebae..422e9cb85 100644 --- a/lib/shared/updates/update-thread-spec.js +++ b/lib/shared/updates/update-thread-spec.js @@ -1,119 +1,117 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import t from 'tcomb'; import type { UpdateInfoFromRawInfoParams, UpdateSpec } from './update-spec.js'; -import { - legacyRawThreadInfoValidator, - type RawThreadInfos, -} from '../../types/thread-types.js'; +import { rawThreadInfoValidator } from '../../permissions/minimally-encoded-thread-permissions-validators.js'; +import { type RawThreadInfos } from '../../types/thread-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ThreadUpdateInfo, ThreadRawUpdateInfo, ThreadUpdateData, } from '../../types/update-types.js'; import { tNumber, tShape } from '../../utils/validation-utils.js'; import { threadInFilterList } from '../thread-utils.js'; export const updateThreadSpec: UpdateSpec< ThreadUpdateInfo, ThreadRawUpdateInfo, ThreadUpdateData, > = Object.freeze({ generateOpsForThreadUpdates( storeThreadInfos: RawThreadInfos, update: ThreadUpdateInfo, ) { if (_isEqual(storeThreadInfos[update.threadInfo.id])(update.threadInfo)) { return null; } return [ { type: 'replace', payload: { id: update.threadInfo.id, threadInfo: update.threadInfo, }, }, ]; }, reduceCalendarThreadFilters( filteredThreadIDs: $ReadOnlySet, update: ThreadUpdateInfo, ) { if ( threadInFilterList(update.threadInfo) || !filteredThreadIDs.has(update.threadInfo.id) ) { return filteredThreadIDs; } return new Set( [...filteredThreadIDs].filter(id => id !== update.threadInfo.id), ); }, rawUpdateInfoFromRow(row: Object) { const { threadID } = JSON.parse(row.content); return { type: updateTypes.UPDATE_THREAD, id: row.id.toString(), time: row.time, threadID, }; }, updateContentForServerDB(data: ThreadUpdateData) { return JSON.stringify({ threadID: data.threadID }); }, entitiesToFetch(update: ThreadRawUpdateInfo) { return { threadID: update.threadID, }; }, rawInfoFromData(data: ThreadUpdateData, id: string) { return { type: updateTypes.UPDATE_THREAD, id, time: data.time, threadID: data.threadID, }; }, updateInfoFromRawInfo( info: ThreadRawUpdateInfo, params: UpdateInfoFromRawInfoParams, ) { const threadInfo = params.data.threadInfos[info.threadID]; if (!threadInfo) { console.warn( "failed to hydrate updateTypes.UPDATE_THREAD because we couldn't " + `fetch RawThreadInfo for ${info.threadID}`, ); return null; } return { type: updateTypes.UPDATE_THREAD, id: info.id, time: info.time, threadInfo, }; }, deleteCondition: new Set([ updateTypes.UPDATE_THREAD, updateTypes.UPDATE_THREAD_READ_STATUS, ]), keyForUpdateData(data: ThreadUpdateData) { return data.threadID; }, keyForUpdateInfo(info: ThreadUpdateInfo) { return info.threadInfo.id; }, typesOfReplacedUpdatesForMatchingKey: new Set([ updateTypes.UPDATE_THREAD_READ_STATUS, ]), infoValidator: tShape({ type: tNumber(updateTypes.UPDATE_THREAD), id: t.String, time: t.Number, - threadInfo: legacyRawThreadInfoValidator, + threadInfo: rawThreadInfoValidator, }), }); diff --git a/lib/types/request-types.js b/lib/types/request-types.js index 02a358f15..27aeff8a5 100644 --- a/lib/types/request-types.js +++ b/lib/types/request-types.js @@ -1,312 +1,310 @@ // @flow import invariant from 'invariant'; import t, { type TUnion, type TInterface } from 'tcomb'; import { type ActivityUpdate } from './activity-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { signedIdentityKeysBlobValidator } from './crypto-types.js'; import type { Platform, PlatformDetails } from './device-types.js'; import { type RawEntryInfo, type CalendarQuery, rawEntryInfoValidator, } from './entry-types.js'; import type { ThreadInconsistencyReportShape, EntryInconsistencyReportShape, ClientThreadInconsistencyReportShape, ClientEntryInconsistencyReportShape, } from './report-types.js'; -import { - legacyRawThreadInfoValidator, - type RawThreadInfo, -} from './thread-types.js'; +import { type RawThreadInfo } from './thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type AccountUserInfo, accountUserInfoValidator, } from './user-types.js'; +import { rawThreadInfoValidator } from '../permissions/minimally-encoded-thread-permissions-validators.js'; import { tNumber, tShape, tID } from '../utils/validation-utils.js'; // "Server requests" are requests for information that the server delivers to // clients. Clients then respond to those requests with a "client response". export const serverRequestTypes = Object.freeze({ PLATFORM: 0, //DEVICE_TOKEN: 1, (DEPRECATED) THREAD_INCONSISTENCY: 2, PLATFORM_DETAILS: 3, //INITIAL_ACTIVITY_UPDATE: 4, (DEPRECATED) ENTRY_INCONSISTENCY: 5, CHECK_STATE: 6, INITIAL_ACTIVITY_UPDATES: 7, MORE_ONE_TIME_KEYS: 8, SIGNED_IDENTITY_KEYS_BLOB: 9, INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE: 10, }); type ServerRequestType = $Values; export function assertServerRequestType( serverRequestType: number, ): ServerRequestType { invariant( serverRequestType === 0 || serverRequestType === 2 || serverRequestType === 3 || serverRequestType === 5 || serverRequestType === 6 || serverRequestType === 7 || serverRequestType === 8 || serverRequestType === 9 || serverRequestType === 10, 'number is not ServerRequestType enum', ); return serverRequestType; } type PlatformServerRequest = { +type: 0, }; const platformServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM), }); type PlatformClientResponse = { +type: 0, +platform: Platform, }; export type ThreadInconsistencyClientResponse = { ...ThreadInconsistencyReportShape, +type: 2, }; type PlatformDetailsServerRequest = { type: 3, }; const platformDetailsServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.PLATFORM_DETAILS), }); type PlatformDetailsClientResponse = { type: 3, platformDetails: PlatformDetails, }; export type EntryInconsistencyClientResponse = { type: 5, ...EntryInconsistencyReportShape, }; type FailUnmentioned = Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>; type StateChanges = Partial<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>; export type ServerCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: FailUnmentioned, +stateChanges?: StateChanges, }; const serverCheckStateServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.CHECK_STATE), hashesToCheck: t.dict(t.String, t.Number), failUnmentioned: t.maybe( tShape({ threadInfos: t.maybe(t.Boolean), entryInfos: t.maybe(t.Boolean), userInfos: t.maybe(t.Boolean), }), ), stateChanges: t.maybe( tShape({ - rawThreadInfos: t.maybe(t.list(legacyRawThreadInfoValidator)), + rawThreadInfos: t.maybe(t.list(rawThreadInfoValidator)), rawEntryInfos: t.maybe(t.list(rawEntryInfoValidator)), currentUserInfo: t.maybe(currentUserInfoValidator), userInfos: t.maybe(t.list(accountUserInfoValidator)), deleteThreadIDs: t.maybe(t.list(tID)), deleteEntryIDs: t.maybe(t.list(tID)), deleteUserInfoIDs: t.maybe(t.list(t.String)), }), ), }); type CheckStateClientResponse = { +type: 6, +hashResults: { +[key: string]: boolean }, }; type InitialActivityUpdatesClientResponse = { +type: 7, +activityUpdates: $ReadOnlyArray, }; type MoreOneTimeKeysServerRequest = { +type: 8, }; const moreOneTimeKeysServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.MORE_ONE_TIME_KEYS), }); type MoreOneTimeKeysClientResponse = { +type: 8, +keys: $ReadOnlyArray, }; type SignedIdentityKeysBlobServerRequest = { +type: 9, }; const signedIdentityKeysBlobServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB), }); type SignedIdentityKeysBlobClientResponse = { +type: 9, +signedIdentityKeysBlob: SignedIdentityKeysBlob, }; type InitialNotificationsEncryptedMessageServerRequest = { +type: 10, }; const initialNotificationsEncryptedMessageServerRequestValidator = tShape({ type: tNumber(serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE), }); type InitialNotificationsEncryptedMessageClientResponse = { +type: 10, +initialNotificationsEncryptedMessage: string, }; export type ServerServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ServerCheckStateServerRequest | MoreOneTimeKeysServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; export const serverServerRequestValidator: TUnion = t.union([ platformServerRequestValidator, platformDetailsServerRequestValidator, serverCheckStateServerRequestValidator, moreOneTimeKeysServerRequestValidator, signedIdentityKeysBlobServerRequestValidator, initialNotificationsEncryptedMessageServerRequestValidator, ]); export type ClientResponse = | PlatformClientResponse | ThreadInconsistencyClientResponse | PlatformDetailsClientResponse | EntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientCheckStateServerRequest = { +type: 6, +hashesToCheck: { +[key: string]: number }, +failUnmentioned?: Partial<{ +threadInfos: boolean, +entryInfos: boolean, +userInfos: boolean, }>, +stateChanges?: Partial<{ +rawThreadInfos: RawThreadInfo[], +rawEntryInfos: RawEntryInfo[], +currentUserInfo: CurrentUserInfo, +userInfos: AccountUserInfo[], +deleteThreadIDs: string[], +deleteEntryIDs: string[], +deleteUserInfoIDs: string[], }>, }; export type ClientServerRequest = | PlatformServerRequest | PlatformDetailsServerRequest | ClientCheckStateServerRequest | MoreOneTimeKeysServerRequest | SignedIdentityKeysBlobServerRequest | InitialNotificationsEncryptedMessageServerRequest; // This is just the client variant of ClientResponse. The server needs to handle // multiple client versions so the type supports old versions of certain client // responses, but the client variant only need to support the latest version. type ClientThreadInconsistencyClientResponse = { ...ClientThreadInconsistencyReportShape, +type: 2, }; type ClientEntryInconsistencyClientResponse = { +type: 5, ...ClientEntryInconsistencyReportShape, }; export type ClientClientResponse = | PlatformClientResponse | ClientThreadInconsistencyClientResponse | PlatformDetailsClientResponse | ClientEntryInconsistencyClientResponse | CheckStateClientResponse | InitialActivityUpdatesClientResponse | MoreOneTimeKeysClientResponse | SignedIdentityKeysBlobClientResponse | InitialNotificationsEncryptedMessageClientResponse; export type ClientInconsistencyResponse = | ClientThreadInconsistencyClientResponse | ClientEntryInconsistencyClientResponse; export const processServerRequestsActionType = 'PROCESS_SERVER_REQUESTS'; export type ProcessServerRequestsPayload = { +serverRequests: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type ProcessServerRequestAction = { +type: 'PROCESS_SERVER_REQUESTS', +payload: ProcessServerRequestsPayload, }; export type GetSessionPublicKeysArgs = { +session: string, }; export type OlmSessionInitializationInfo = { +prekey: string, +prekeySignature: string, +oneTimeKey: string, }; export const olmSessionInitializationInfoValidator: TInterface = tShape({ prekey: t.String, prekeySignature: t.String, oneTimeKey: t.String, }); export type GetOlmSessionInitializationDataResponse = { +signedIdentityKeysBlob: SignedIdentityKeysBlob, +contentInitializationInfo: OlmSessionInitializationInfo, +notifInitializationInfo: OlmSessionInitializationInfo, }; export const getOlmSessionInitializationDataResponseValidator: TInterface = tShape({ signedIdentityKeysBlob: signedIdentityKeysBlobValidator, contentInitializationInfo: olmSessionInitializationInfoValidator, notifInitializationInfo: olmSessionInitializationInfoValidator, }); diff --git a/lib/types/socket-types.js b/lib/types/socket-types.js index 4100dde8c..072ce6e23 100644 --- a/lib/types/socket-types.js +++ b/lib/types/socket-types.js @@ -1,547 +1,545 @@ // @flow import invariant from 'invariant'; import t, { type TInterface, type TUnion } from 'tcomb'; import { type ActivityUpdate, activityUpdateValidator, type UpdateActivityResult, updateActivityResultValidator, } from './activity-types.js'; import { type CompressedData, compressedDataValidator, } from './compression-types.js'; import type { APIRequest } from './endpoints.js'; import { type RawEntryInfo, rawEntryInfoValidator, type CalendarQuery, } from './entry-types.js'; import { type MessagesResponse, messagesResponseValidator, type NewMessagesPayload, newMessagesPayloadValidator, } from './message-types.js'; import { type ServerServerRequest, serverServerRequestValidator, type ClientServerRequest, type ClientResponse, type ClientClientResponse, } from './request-types.js'; import type { SessionState, SessionIdentification } from './session-types.js'; -import { - type RawThreadInfos, - legacyRawThreadInfoValidator, -} from './thread-types.js'; +import { type RawThreadInfos } from './thread-types.js'; import { type ClientUpdatesResult, type ClientUpdatesResultWithUserInfos, type ServerUpdatesResult, serverUpdatesResultValidator, type ServerUpdatesResultWithUserInfos, serverUpdatesResultWithUserInfosValidator, } from './update-types.js'; import { type UserInfo, userInfoValidator, type CurrentUserInfo, currentUserInfoValidator, type LoggedOutUserInfo, loggedOutUserInfoValidator, } from './user-types.js'; +import { rawThreadInfoValidator } from '../permissions/minimally-encoded-thread-permissions-validators.js'; import { tShape, tNumber, tID } from '../utils/validation-utils.js'; // The types of messages that the client sends across the socket export const clientSocketMessageTypes = Object.freeze({ INITIAL: 0, RESPONSES: 1, //ACTIVITY_UPDATES: 2, (DEPRECATED) PING: 3, ACK_UPDATES: 4, API_REQUEST: 5, }); export type ClientSocketMessageType = $Values; export function assertClientSocketMessageType( ourClientSocketMessageType: number, ): ClientSocketMessageType { invariant( ourClientSocketMessageType === 0 || ourClientSocketMessageType === 1 || ourClientSocketMessageType === 3 || ourClientSocketMessageType === 4 || ourClientSocketMessageType === 5, 'number is not ClientSocketMessageType enum', ); return ourClientSocketMessageType; } export type InitialClientSocketMessage = { +type: 0, +id: number, +payload: { +sessionIdentification: SessionIdentification, +sessionState: SessionState, +clientResponses: $ReadOnlyArray, }, }; export type ResponsesClientSocketMessage = { +type: 1, +id: number, +payload: { +clientResponses: $ReadOnlyArray, }, }; export type PingClientSocketMessage = { +type: 3, +id: number, }; export type AckUpdatesClientSocketMessage = { +type: 4, +id: number, +payload: { +currentAsOf: number, }, }; export type APIRequestClientSocketMessage = { +type: 5, +id: number, +payload: APIRequest, }; export type ClientSocketMessage = | InitialClientSocketMessage | ResponsesClientSocketMessage | PingClientSocketMessage | AckUpdatesClientSocketMessage | APIRequestClientSocketMessage; export type ClientInitialClientSocketMessage = { +type: 0, +id: number, +payload: { +sessionIdentification: SessionIdentification, +sessionState: SessionState, +clientResponses: $ReadOnlyArray, }, }; export type ClientResponsesClientSocketMessage = { +type: 1, +id: number, +payload: { +clientResponses: $ReadOnlyArray, }, }; export type ClientClientSocketMessage = | ClientInitialClientSocketMessage | ClientResponsesClientSocketMessage | PingClientSocketMessage | AckUpdatesClientSocketMessage | APIRequestClientSocketMessage; export type ClientSocketMessageWithoutID = $Diff< ClientClientSocketMessage, { id: number }, >; // The types of messages that the server sends across the socket export const serverSocketMessageTypes = Object.freeze({ STATE_SYNC: 0, REQUESTS: 1, ERROR: 2, AUTH_ERROR: 3, ACTIVITY_UPDATE_RESPONSE: 4, PONG: 5, UPDATES: 6, MESSAGES: 7, API_RESPONSE: 8, COMPRESSED_MESSAGE: 9, }); export type ServerSocketMessageType = $Values; export function assertServerSocketMessageType( ourServerSocketMessageType: number, ): ServerSocketMessageType { invariant( ourServerSocketMessageType === 0 || ourServerSocketMessageType === 1 || ourServerSocketMessageType === 2 || ourServerSocketMessageType === 3 || ourServerSocketMessageType === 4 || ourServerSocketMessageType === 5 || ourServerSocketMessageType === 6 || ourServerSocketMessageType === 7 || ourServerSocketMessageType === 8 || ourServerSocketMessageType === 9, 'number is not ServerSocketMessageType enum', ); return ourServerSocketMessageType; } export const stateSyncPayloadTypes = Object.freeze({ FULL: 0, INCREMENTAL: 1, }); export const fullStateSyncActionType = 'FULL_STATE_SYNC'; export type BaseFullStateSync = { +messagesResult: MessagesResponse, +threadInfos: RawThreadInfos, +rawEntryInfos: $ReadOnlyArray, +userInfos: $ReadOnlyArray, +updatesCurrentAsOf: number, }; const baseFullStateSyncValidator = tShape({ messagesResult: messagesResponseValidator, - threadInfos: t.dict(tID, legacyRawThreadInfoValidator), + threadInfos: t.dict(tID, rawThreadInfoValidator), rawEntryInfos: t.list(rawEntryInfoValidator), userInfos: t.list(userInfoValidator), updatesCurrentAsOf: t.Number, }); export type ClientFullStateSync = { ...BaseFullStateSync, +currentUserInfo: CurrentUserInfo, }; export type StateSyncFullActionPayload = { ...ClientFullStateSync, +calendarQuery: CalendarQuery, +keyserverID: string, }; export type ClientStateSyncFullSocketPayload = { ...ClientFullStateSync, +type: 0, // Included iff client is using sessionIdentifierTypes.BODY_SESSION_ID +sessionID?: string, }; export type ServerFullStateSync = { ...BaseFullStateSync, +currentUserInfo: CurrentUserInfo, }; const serverFullStateSyncValidator = tShape({ ...baseFullStateSyncValidator.meta.props, currentUserInfo: currentUserInfoValidator, }); export type ServerStateSyncFullSocketPayload = { ...ServerFullStateSync, +type: 0, // Included iff client is using sessionIdentifierTypes.BODY_SESSION_ID +sessionID?: string, }; const serverStateSyncFullSocketPayloadValidator = tShape({ ...serverFullStateSyncValidator.meta.props, type: tNumber(stateSyncPayloadTypes.FULL), sessionID: t.maybe(t.String), }); export const incrementalStateSyncActionType = 'INCREMENTAL_STATE_SYNC'; export type BaseIncrementalStateSync = { +messagesResult: MessagesResponse, +deltaEntryInfos: $ReadOnlyArray, +deletedEntryIDs: $ReadOnlyArray, +userInfos: $ReadOnlyArray, }; const baseIncrementalStateSyncValidator = tShape({ messagesResult: messagesResponseValidator, deltaEntryInfos: t.list(rawEntryInfoValidator), deletedEntryIDs: t.list(tID), userInfos: t.list(userInfoValidator), }); export type ClientIncrementalStateSync = { ...BaseIncrementalStateSync, +updatesResult: ClientUpdatesResult, }; export type StateSyncIncrementalActionPayload = { ...ClientIncrementalStateSync, +calendarQuery: CalendarQuery, +keyserverID: string, }; type ClientStateSyncIncrementalSocketPayload = { +type: 1, ...ClientIncrementalStateSync, }; export type ServerIncrementalStateSync = { ...BaseIncrementalStateSync, +updatesResult: ServerUpdatesResult, }; const serverIncrementalStateSyncValidator = tShape({ ...baseIncrementalStateSyncValidator.meta.props, updatesResult: serverUpdatesResultValidator, }); type ServerStateSyncIncrementalSocketPayload = { +type: 1, ...ServerIncrementalStateSync, }; const serverStateSyncIncrementalSocketPayloadValidator = tShape({ type: tNumber(stateSyncPayloadTypes.INCREMENTAL), ...serverIncrementalStateSyncValidator.meta.props, }); export type ClientStateSyncSocketPayload = | ClientStateSyncFullSocketPayload | ClientStateSyncIncrementalSocketPayload; export type ServerStateSyncSocketPayload = | ServerStateSyncFullSocketPayload | ServerStateSyncIncrementalSocketPayload; const serverStateSyncSocketPayloadValidator = t.union([ serverStateSyncFullSocketPayloadValidator, serverStateSyncIncrementalSocketPayloadValidator, ]); export type ServerStateSyncServerSocketMessage = { +type: 0, +responseTo: number, +payload: ServerStateSyncSocketPayload, }; export const serverStateSyncServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.STATE_SYNC), responseTo: t.Number, payload: serverStateSyncSocketPayloadValidator, }); type ServerRequestsServerSocketMessagePayload = { +serverRequests: $ReadOnlyArray, }; export type ServerRequestsServerSocketMessage = { +type: 1, +responseTo?: number, +payload: ServerRequestsServerSocketMessagePayload, }; export const serverRequestsServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.REQUESTS), responseTo: t.maybe(t.Number), payload: tShape({ serverRequests: t.list(serverServerRequestValidator), }), }); export type ErrorServerSocketMessage = { type: 2, responseTo?: number, message: string, payload?: Object, }; export const errorServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.ERROR), responseTo: t.maybe(t.Number), message: t.String, payload: t.maybe(t.Object), }); type SessionChange = { +cookie: string, +currentUserInfo: LoggedOutUserInfo, }; export type AuthErrorServerSocketMessage = { +type: 3, +responseTo: number, +message: string, +sessionChange: SessionChange, }; export const authErrorServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.AUTH_ERROR), responseTo: t.Number, message: t.String, sessionChange: t.maybe( tShape({ cookie: t.String, currentUserInfo: loggedOutUserInfoValidator, }), ), }); export type ActivityUpdateResponseServerSocketMessage = { +type: 4, +responseTo: number, +payload: UpdateActivityResult, }; export const activityUpdateResponseServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.ACTIVITY_UPDATE_RESPONSE), responseTo: t.Number, payload: updateActivityResultValidator, }); export type PongServerSocketMessage = { +type: 5, +responseTo: number, }; export const pongServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.PONG), responseTo: t.Number, }); export type ServerUpdatesServerSocketMessage = { +type: 6, +payload: ServerUpdatesResultWithUserInfos, }; export const serverUpdatesServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.UPDATES), payload: serverUpdatesResultWithUserInfosValidator, }); export type MessagesServerSocketMessage = { +type: 7, +payload: NewMessagesPayload, }; export const messagesServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.MESSAGES), payload: newMessagesPayloadValidator, }); export type APIResponseServerSocketMessage = { +type: 8, +responseTo: number, +payload?: Object, }; export const apiResponseServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.API_RESPONSE), responseTo: t.Number, payload: t.maybe(t.Object), }); export type CompressedMessageServerSocketMessage = { +type: 9, +payload: CompressedData, }; export const compressedMessageServerSocketMessageValidator: TInterface = tShape({ type: tNumber(serverSocketMessageTypes.COMPRESSED_MESSAGE), payload: compressedDataValidator, }); export type ServerServerSocketMessage = | ServerStateSyncServerSocketMessage | ServerRequestsServerSocketMessage | ErrorServerSocketMessage | AuthErrorServerSocketMessage | ActivityUpdateResponseServerSocketMessage | PongServerSocketMessage | ServerUpdatesServerSocketMessage | MessagesServerSocketMessage | APIResponseServerSocketMessage | CompressedMessageServerSocketMessage; export const serverServerSocketMessageValidator: TUnion = t.union([ serverStateSyncServerSocketMessageValidator, serverRequestsServerSocketMessageValidator, errorServerSocketMessageValidator, authErrorServerSocketMessageValidator, activityUpdateResponseServerSocketMessageValidator, pongServerSocketMessageValidator, serverUpdatesServerSocketMessageValidator, messagesServerSocketMessageValidator, apiResponseServerSocketMessageValidator, compressedMessageServerSocketMessageValidator, ]); export type ClientRequestsServerSocketMessage = { +type: 1, +responseTo?: number, +payload: { +serverRequests: $ReadOnlyArray, }, }; export type ClientStateSyncServerSocketMessage = { +type: 0, +responseTo: number, +payload: ClientStateSyncSocketPayload, }; export type ClientUpdatesServerSocketMessage = { +type: 6, +payload: ClientUpdatesResultWithUserInfos, }; export type ClientServerSocketMessage = | ClientStateSyncServerSocketMessage | ClientRequestsServerSocketMessage | ErrorServerSocketMessage | AuthErrorServerSocketMessage | ActivityUpdateResponseServerSocketMessage | PongServerSocketMessage | ClientUpdatesServerSocketMessage | MessagesServerSocketMessage | APIResponseServerSocketMessage | CompressedMessageServerSocketMessage; export type SocketListener = (message: ClientServerSocketMessage) => void; export type ConnectionStatus = | 'connecting' | 'connected' | 'reconnecting' | 'disconnecting' | 'forcedDisconnecting' | 'disconnected'; export type ConnectionInfo = { +status: ConnectionStatus, +queuedActivityUpdates: $ReadOnlyArray, +lateResponses: $ReadOnlyArray, +showDisconnectedBar: boolean, }; export const connectionInfoValidator: TInterface = tShape({ status: t.enums.of([ 'connecting', 'connected', 'reconnecting', 'disconnecting', 'forcedDisconnecting', 'disconnected', ]), queuedActivityUpdates: t.list(activityUpdateValidator), lateResponses: t.list(t.Number), showDisconnectedBar: t.Boolean, }); export const defaultConnectionInfo: ConnectionInfo = { status: 'connecting', queuedActivityUpdates: [], lateResponses: [], showDisconnectedBar: false, }; export const updateConnectionStatusActionType = 'UPDATE_CONNECTION_STATUS'; export type UpdateConnectionStatusPayload = { +status: ConnectionStatus, +keyserverID: string, }; export const setLateResponseActionType = 'SET_LATE_RESPONSE'; export type SetLateResponsePayload = { +messageID: number, +isLate: boolean, +keyserverID: string, }; export const updateDisconnectedBarActionType = 'UPDATE_DISCONNECTED_BAR'; export type UpdateDisconnectedBarPayload = { +visible: boolean, +keyserverID: string, }; export type OneTimeKeyGenerator = (inc: number) => string; export type GRPCStream = { readyState: number, onopen: (ev: any) => mixed, onmessage: (ev: MessageEvent) => mixed, onclose: (ev: CloseEvent) => mixed, close(code?: number, reason?: string): void, send(data: string | Blob | ArrayBuffer | $ArrayBufferView): void, }; export type CommTransportLayer = GRPCStream | WebSocket;