diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js index d4ff0dbc7..1824b7c0f 100644 --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -1,819 +1,834 @@ // @flow import html from 'common-tags/lib/html/index.js'; import { detect as detectBrowser } from 'detect-browser'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; import t from 'tcomb'; import { promisify } from 'util'; import { inviteLinkUrl } from 'lib/facts/links.js'; import { baseLegalPolicies } from 'lib/facts/policies.js'; import stores from 'lib/facts/stores.js'; import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer.js'; import { freshMessageStore } from 'lib/reducers/message-reducer.js'; import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js'; import { threadHasPermission, threadIsPending, parsePendingThreadID, createPendingThread, } from 'lib/shared/thread-utils.js'; import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js'; import { entryStoreValidator } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { keyserverStoreValidator } from 'lib/types/keyserver-types.js'; import { inviteLinksStoreValidator } from 'lib/types/link-types.js'; import { defaultNumberPerThread, messageStoreValidator, } from 'lib/types/message-types.js'; import { defaultEnabledReports } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { threadStoreValidator } from 'lib/types/thread-types.js'; import { currentUserInfoValidator, userInfosValidator, } from 'lib/types/user-types.js'; import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { promiseAll } from 'lib/utils/promises.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; -import { tBool, tNumber, tShape, tString } from 'lib/utils/validation-utils.js'; +import { + tBool, + tNumber, + tShape, + tString, + ashoatKeyserverID, +} from 'lib/utils/validation-utils.js'; import getTitle from 'web/title/getTitle.js'; import { navInfoValidator } from 'web/types/nav-types.js'; import { navInfoFromURL } from 'web/url-utils.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { hasAnyNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchCurrentUserInfo, fetchKnownUserInfos, fetchUserInfos, } from '../fetchers/user-fetchers.js'; import { getWebPushConfig } from '../push/providers.js'; import { setNewSession } from '../session/cookies.js'; import { Viewer } from '../session/viewer.js'; import { streamJSON, waitForStream } from '../utils/json-stream.js'; import { getAppURLFactsFromRequestURL, getCommAppURLFacts, } from '../utils/urls.js'; import { validateOutput } from '../utils/validation-utils.js'; const { renderToNodeStream } = ReactDOMServer; const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600&display=swap'; const localFontsURL = 'fonts/local-fonts.css'; async function getFontsURL() { try { await access(localFontsURL); return localFontsURL; } catch { return googleFontsURL; } } type AssetInfo = { +jsURL: string, +fontsURL: string, +cssInclude: string, +olmFilename: string, +commQueryExecutorFilename: string, +opaqueURL: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontsURL = await getFontsURL(); assetInfo = { jsURL: 'http://localhost:8080/dev.build.js', fontsURL, cssInclude: '', olmFilename: '', commQueryExecutorFilename: '', opaqueURL: 'http://localhost:8080/opaque-ke.wasm', }; return assetInfo; } try { const manifestString = await readFile('../web/dist/manifest.json', 'utf8'); const manifest = JSON.parse(manifestString); const webworkersManifestString = await readFile( '../web/dist/webworkers/manifest.json', 'utf8', ); const webworkersManifest = JSON.parse(webworkersManifestString); assetInfo = { jsURL: `compiled/${manifest['browser.js']}`, fontsURL: googleFontsURL, cssInclude: html` `, olmFilename: manifest['olm.wasm'], commQueryExecutorFilename: webworkersManifest['comm_query_executor.wasm'], opaqueURL: `compiled/${manifest['comm_opaque2_wasm_bg.wasm']}`, }; return assetInfo; } catch { throw new Error( 'Could not load manifest.json for web build. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } let webpackCompiledRootComponent: ?React.ComponentType<{}> = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe web/dist doesn't always exist const webpackBuild = await import('web/dist/app.build.cjs'); webpackCompiledRootComponent = webpackBuild.app.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load app.build.cjs. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } const initialReduxStateValidator = tShape({ navInfo: navInfoValidator, deviceID: t.Nil, currentUserInfo: currentUserInfoValidator, draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })), sessionID: t.maybe(t.String), entryStore: entryStoreValidator, threadStore: threadStoreValidator, userStore: tShape({ userInfos: userInfosValidator, inconsistencyReports: t.irreducible( 'default inconsistencyReports', _isEqual([]), ), }), messageStore: messageStoreValidator, updatesCurrentAsOf: t.Number, loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})), calendarFilters: t.irreducible( 'defaultCalendarFilters', _isEqual(defaultCalendarFilters), ), communityPickerStore: t.irreducible( 'default communityPickerStore', _isEqual({ chat: null, calendar: null }), ), urlPrefix: tString(''), windowDimensions: t.irreducible( 'default windowDimensions', _isEqual({ width: 0, height: 0 }), ), baseHref: t.String, notifPermissionAlertInfo: t.irreducible( 'default notifPermissionAlertInfo', _isEqual(defaultNotifPermissionAlertInfo), ), connection: tShape({ status: tString('connecting'), queuedActivityUpdates: t.irreducible( 'default queuedActivityUpdates', _isEqual([]), ), actualizedCalendarQuery: tShape({ startDate: t.String, endDate: t.String, filters: t.irreducible( 'default filters', _isEqual(defaultCalendarFilters), ), }), lateResponses: t.irreducible('default lateResponses', _isEqual([])), showDisconnectedBar: tBool(false), }), watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])), lifecycleState: tString('active'), enabledApps: t.irreducible( 'defaultWebEnabledApps', _isEqual(defaultWebEnabledApps), ), reportStore: t.irreducible( 'default reportStore', _isEqual({ enabledReports: defaultEnabledReports, queuedReports: [], }), ), nextLocalID: tNumber(0), deviceToken: t.Nil, dataLoaded: t.Boolean, windowActive: tBool(true), userPolicies: t.irreducible('default userPolicies', _isEqual({})), cryptoStore: t.irreducible( 'default cryptoStore', _isEqual({ primaryIdentityKeys: null, notificationIdentityKeys: null, }), ), pushApiPublicKey: t.maybe(t.String), _persist: t.Nil, commServicesAccessToken: t.Nil, inviteLinksStore: inviteLinksStoreValidator, lastCommunicatedPlatformDetails: t.irreducible( 'default lastCommunicatedPlatformDetails', _isEqual({}), ), keyserverStore: keyserverStoreValidator, }); async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl); const { basePath, baseDomain } = appURLFacts; const baseURL = basePath.replace(/\/$/, ''); const baseHref = baseDomain + baseURL; const loadingPromise = getWebpackCompiledRootComponentForSSR(); const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( viewer.id, baseLegalPolicies, ); const initialNavInfoPromise = (async () => { try { const urlInfo = infoFromURL(req.url); let backupInfo = { now: currentDateInTimeZone(viewer.timeZone), }; // Some user ids in selectedUserList might not exist in the userStore // (e.g. they were included in the results of the user search endpoint) // Because of that we keep their userInfos inside the navInfo. if (urlInfo.selectedUserList) { const fetchedUserInfos = await fetchUserInfos(urlInfo.selectedUserList); const userInfos = {}; for (const userID in fetchedUserInfos) { const userInfo = fetchedUserInfos[userID]; if (userInfo.username) { userInfos[userID] = userInfo; } } backupInfo = { userInfos, ...backupInfo }; } return navInfoFromURL(urlInfo, backupInfo); } catch (e) { throw new ServerError(e.message); } })(); const calendarQueryPromise = (async () => { const initialNavInfo = await initialNavInfoPromise; return { startDate: initialNavInfo.startDate, endDate: initialNavInfo.endDate, filters: defaultCalendarFilters, }; })(); const messageSelectionCriteria = { joinedThreads: true }; const initialTime = Date.now(); const assetInfoPromise = getAssetInfo(); const threadInfoPromise = fetchThreadInfos(viewer); const messageInfoPromise = fetchMessageInfos( viewer, messageSelectionCriteria, defaultNumberPerThread, ); const entryInfoPromise = (async () => { const calendarQuery = await calendarQueryPromise; return await fetchEntryInfos(viewer, [calendarQuery]); })(); const currentUserInfoPromise = fetchCurrentUserInfo(viewer); const userInfoPromise = fetchKnownUserInfos(viewer); const sessionIDPromise = (async () => { const calendarQuery = await calendarQueryPromise; if (viewer.loggedIn) { await setNewSession(viewer, calendarQuery, initialTime); } return viewer.sessionID; })(); const threadStorePromise = (async () => { const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ threadInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; })(); const messageStorePromise = (async () => { const [ { threadInfos }, { rawMessageInfos, truncationStatuses }, hasNotAcknowledgedPolicies, ] = await Promise.all([ threadInfoPromise, messageInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); if (hasNotAcknowledgedPolicies) { return { messages: {}, threads: {}, local: {}, currentAsOf: 0, }; } const { messageStore: freshStore } = freshMessageStore( rawMessageInfos, truncationStatuses, mostRecentMessageTimestamp(rawMessageInfos, initialTime), threadInfos, ); return freshStore; })(); const entryStorePromise = (async () => { const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ entryInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); if (hasNotAcknowledgedPolicies) { return { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }; } return { entryInfos: _keyBy('id')(rawEntryInfos), daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), lastUserInteractionCalendar: initialTime, }; })(); const userStorePromise = (async () => { const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ userInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return { userInfos: hasNotAcknowledgedPolicies ? {} : userInfos, inconsistencyReports: [], }; })(); const navInfoPromise = (async () => { const [ { threadInfos }, messageStore, currentUserInfo, userStore, finalNavInfo, ] = await Promise.all([ threadInfoPromise, messageStorePromise, currentUserInfoPromise, userStorePromise, initialNavInfoPromise, ]); const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; if ( requestedActiveChatThreadID && !threadIsPending(requestedActiveChatThreadID) && !threadHasPermission( threadInfos[requestedActiveChatThreadID], threadPermissions.VISIBLE, ) ) { finalNavInfo.activeChatThreadID = null; } if (!finalNavInfo.activeChatThreadID) { const mostRecentThread = mostRecentlyReadThread( messageStore, threadInfos, ); if (mostRecentThread) { finalNavInfo.activeChatThreadID = mostRecentThread; } } if ( finalNavInfo.activeChatThreadID && threadIsPending(finalNavInfo.activeChatThreadID) && finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID ) { const pendingThreadData = parsePendingThreadID( finalNavInfo.activeChatThreadID, ); if ( pendingThreadData && pendingThreadData.threadType !== threadTypes.SIDEBAR && currentUserInfo.id ) { const { userInfos } = userStore; const members = [...pendingThreadData.memberIDs, currentUserInfo.id] .map(id => { const userInfo = userInfos[id]; if (!userInfo || !userInfo.username) { return undefined; } const { username } = userInfo; return { id, username }; }) .filter(Boolean); const newPendingThread = createPendingThread({ viewerID: currentUserInfo.id, threadType: pendingThreadData.threadType, members, }); finalNavInfo.activeChatThreadID = newPendingThread.id; finalNavInfo.pendingThread = newPendingThread; } } return finalNavInfo; })(); const currentAsOfPromise = (async () => { const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise; return hasNotAcknowledgedPolicies ? 0 : initialTime; })(); const pushApiPublicKeyPromise = (async () => { const pushConfig = await getWebPushConfig(); if (!pushConfig) { if (process.env.NODE_ENV !== 'development') { console.warn('keyserver/secrets/web_push_config.json should exist'); } return null; } return pushConfig.publicKey; })(); const inviteLinksStorePromise = (async () => { const primaryInviteLinks = await fetchPrimaryInviteLinks(viewer); const links = {}; for (const link of primaryInviteLinks) { if (link.primary) { links[link.communityID] = { primaryLink: link, }; } } return { links, }; })(); + const keyserverStorePromise = (async () => { + const sessionID = await sessionIDPromise; + return { + keyserverInfos: { + [ashoatKeyserverID]: { cookie: undefined, sessionID }, + }, + }; + })(); + const { jsURL, fontsURL, cssInclude, olmFilename, opaqueURL, commQueryExecutorFilename, } = await assetInfoPromise; // prettier-ignore res.write(html` ${getTitle(0)} ${cssInclude}
`); const Loading = await loadingPromise; const reactStream = renderToNodeStream(); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); res.write(html`
`); } const inviteSecretRegex = /^[a-z0-9]+$/i; // On native, if this responder is called, it means that the app isn't // installed. async function inviteResponder(req: $Request, res: $Response): Promise { const { secret } = req.params; const userAgent = req.get('User-Agent'); const detectionResult = detectBrowser(userAgent); if (detectionResult.os === 'Android OS') { const isSecretValid = inviteSecretRegex.test(secret); const referrer = isSecretValid ? `&referrer=${encodeURIComponent(`utm_source=invite/${secret}`)}` : ''; const redirectUrl = `${stores.googlePlayUrl}${referrer}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } else if (detectionResult.os !== 'iOS') { const urlFacts = getCommAppURLFacts(); const baseDomain = urlFacts?.baseDomain ?? ''; const basePath = urlFacts?.basePath ?? '/'; const redirectUrl = `${baseDomain}${basePath}handle/invite/${secret}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } const fontsURL = await getFontsURL(); res.end(html` Comm

Comm

To join this community, download the Comm app and reopen this invite link

Download Comm Invite Link
Visit Comm’s website arrow up right `); } export { websiteResponder, inviteResponder }; diff --git a/lib/types/keyserver-types.js b/lib/types/keyserver-types.js index 8401eb067..7b30d09f9 100644 --- a/lib/types/keyserver-types.js +++ b/lib/types/keyserver-types.js @@ -1,26 +1,28 @@ // @flow import t, { type TInterface } from 'tcomb'; import { tShape } from '../utils/validation-utils.js'; // Once we start using the cookie field on web, // the cookie field type should be changed to string | null. // See https://linear.app/comm/issue/ENG-4347/stop-using-browser-cookies export type KeyserverInfo = { +cookie?: ?string, + +sessionID?: ?string, }; export type KeyserverStore = { +keyserverInfos: { +[key: string]: KeyserverInfo }, }; export const keyserverInfoValidator: TInterface = tShape({ cookie: t.maybe(t.String), + sessionID: t.maybe(t.String), }); export const keyserverStoreValidator: TInterface = tShape({ keyserverInfos: t.dict(t.String, keyserverInfoValidator), }); diff --git a/lib/utils/sanitization.js b/lib/utils/sanitization.js index cdc178029..0b9c82dc7 100644 --- a/lib/utils/sanitization.js +++ b/lib/utils/sanitization.js @@ -1,321 +1,330 @@ // @flow import clone from 'just-clone'; import stringHash from 'string-hash'; import { setNewSessionActionType } from './action-utils.js'; import { setDeviceTokenActionTypes } from '../actions/device-actions.js'; import type { BaseAction, AppState } from '../types/redux-types.js'; export type ReduxCrashReport = { +preloadedState: AppState, +currentState: AppState, +actions: $ReadOnlyArray, }; export type RedactionHelpers = { +redactString: string => string, +redactColor: string => string, }; // eg {"email":"squad@bot.com"} => {"email":"[redacted]"} const keysWithStringsToBeRedacted = new Set([ 'source', 'value', 'targetID', 'sourceMessageAuthorID', 'content', 'cookie', 'creatorID', 'currentMediaID', 'currentUser', 'childThreadID', 'dbText', 'description', 'draft', 'email', 'entryID', 'extras', 'filename', 'first255Chars', 'id', 'inputFilename', 'latestMessage', 'local_id', 'localID', 'mediaLocalID', 'mediaNativeID', 'messageID', 'messageLocalID', 'messageServerID', 'name', 'newThreadID', 'parentThreadID', 'path', 'serverID', 'sessionID', 'sourceMessageID', 'thread', 'threadID', 'thumbnailID', 'uploadLocalID', 'uploadServerID', 'uiName', 'uri', 'user', 'username', 'deletedUserID', 'deviceToken', 'updatedUserID', 'role', 'ed25519', 'curve25519', 'picklingKey', 'pickledAccount', ]); // eg {"memberIDs":["123", "456"]} => {"memberIDs":["redacted", "redacted"]} const keysWithArraysToBeRedacted = new Set([ 'memberIDs', 'messageIDs', 'already_friends', 'invalid_user', 'user_blocked', 'deletedEntryIDs', 'addedUserIDs', ]); // eg "userInfos":{"1":[Object]} => "userInfos":{"redacted":[Object]} const keysWithObjectsWithKeysToBeRedacted = new Set([ 'userInfos', 'threadInfos', 'threads', 'messages', 'entryInfos', 'roles', ]); // eg {"text":"hello world"} => {"text":"z6lgz waac5"} const keysWithStringsToBeScrambled = new Set(['text', 'robotext']); // eg {"uri":"https://comm.app/1234/5678"} // => {"uri":"https://comm.app/images/placeholder.png"} const keysWithImageURIsToBeReplaced = new Set([ 'uri', 'localURI', 'inputURI', 'outputURI', 'newURI', 'thumbnailURI', ]); // (special case that redacts triply-linked [] to handle `daysToEntries` ) // eg "daysToEntries":{"2020-12-29":["123"]} // => "daysToEntries":{"2020-12-29":["redacted"]} const keysWithObjectsWithArraysToBeRedacted = new Set(['daysToEntries']); function generateSaltedRedactionFn(): string => string { const salt = Math.random().toString(36); return (str: string) => { return `[redacted-${stringHash(str.concat(salt))}]`; }; } function generateColorRedactionFn(): string => string { const salt = Math.random().toString(16); return (oldColor: string) => { return `${stringHash(oldColor.concat(salt)).toString(16).slice(0, 6)}`; }; } function placeholderImageURI(): string { return 'https://comm.app/images/placeholder.png'; } function scrambleText(str: string): string { const arr = []; for (const char of str) { if (char === ' ') { arr.push(' '); continue; } const randomChar = Math.random().toString(36)[2]; arr.push(randomChar); } return arr.join(''); } function sanitizeReduxReport(reduxReport: ReduxCrashReport): ReduxCrashReport { const redactionHelpers: RedactionHelpers = { redactString: generateSaltedRedactionFn(), redactColor: generateColorRedactionFn(), }; return { preloadedState: sanitizeState(reduxReport.preloadedState, redactionHelpers), currentState: sanitizeState(reduxReport.currentState, redactionHelpers), actions: reduxReport.actions.map(x => sanitizeAction(sanitizeActionSecrets(x), redactionHelpers), ), }; } const MessageListRouteName = 'MessageList'; const ThreadSettingsRouteName = 'ThreadSettings'; function potentiallyRedactReactNavigationKey( key: string, redactionFn: string => string, ): string { if (key.startsWith(MessageListRouteName)) { return `${MessageListRouteName}${redactionFn( key.substring(MessageListRouteName.length), )}`; } else if (key.startsWith(ThreadSettingsRouteName)) { return `${ThreadSettingsRouteName}${redactionFn( key.substring(ThreadSettingsRouteName.length), )}`; } return key; } function sanitizeNavState( obj: Object, redactionHelpers: RedactionHelpers, ): void { for (const k in obj) { if (k === 'params') { sanitizePII(obj[k], redactionHelpers); } else if (k === 'key') { obj[k] = potentiallyRedactReactNavigationKey( obj[k], redactionHelpers.redactString, ); } else if (typeof obj[k] === 'object') { sanitizeNavState(obj[k], redactionHelpers); } } } function sanitizePII(obj: Object, redactionHelpers: RedactionHelpers): void { for (const k in obj) { if (k === 'navState') { sanitizeNavState(obj[k], redactionHelpers); continue; } if (keysWithObjectsWithKeysToBeRedacted.has(k)) { for (const keyToBeRedacted in obj[k]) { obj[k][redactionHelpers.redactString(keyToBeRedacted)] = obj[k][keyToBeRedacted]; delete obj[k][keyToBeRedacted]; } } if (keysWithObjectsWithArraysToBeRedacted.has(k)) { for (const arrayToBeRedacted in obj[k]) { obj[k][arrayToBeRedacted] = obj[k][arrayToBeRedacted].map( redactionHelpers.redactString, ); } } if (keysWithStringsToBeRedacted.has(k) && typeof obj[k] === 'string') { obj[k] = redactionHelpers.redactString(obj[k]); } else if (k === 'key') { obj[k] = potentiallyRedactReactNavigationKey( obj[k], redactionHelpers.redactString, ); } else if (k === 'color') { obj[k] = redactionHelpers.redactColor(obj[k]); } else if (keysWithStringsToBeScrambled.has(k)) { obj[k] = scrambleText(obj[k]); } else if (keysWithImageURIsToBeReplaced.has(k)) { obj[k] = placeholderImageURI(); } else if (keysWithArraysToBeRedacted.has(k)) { obj[k] = obj[k].map(redactionHelpers.redactString); } else if (typeof obj[k] === 'object') { sanitizePII(obj[k], redactionHelpers); } } } function sanitizeActionSecrets(action: BaseAction): BaseAction { if (action.type === setNewSessionActionType) { const { sessionChange } = action.payload; if (sessionChange.cookieInvalidated) { const { cookie, ...rest } = sessionChange; return { type: 'SET_NEW_SESSION', payload: { ...action.payload, sessionChange: { cookieInvalidated: true, ...rest }, }, }; } else { const { cookie, ...rest } = sessionChange; return { type: 'SET_NEW_SESSION', payload: { ...action.payload, sessionChange: { cookieInvalidated: false, ...rest }, }, }; } } else if ( action.type === setDeviceTokenActionTypes.started && action.payload ) { return ({ type: 'SET_DEVICE_TOKEN_STARTED', payload: 'FAKE', loadingInfo: action.loadingInfo, }: any); } else if (action.type === setDeviceTokenActionTypes.success) { return { type: 'SET_DEVICE_TOKEN_SUCCESS', payload: 'FAKE', loadingInfo: action.loadingInfo, }; } return action; } function sanitizeAction( action: BaseAction, redactionHelpers: RedactionHelpers, ): BaseAction { const actionCopy = clone(action); sanitizePII(actionCopy, redactionHelpers); return actionCopy; } function sanitizeState( state: AppState, redactionHelpers: RedactionHelpers, ): AppState { - const keyserverInfos = { ...state.keyserverStore.keyserverInfos }; - for (const key in keyserverInfos) { - if (keyserverInfos[key].cookie === undefined) { - continue; - } - keyserverInfos[key] = { ...keyserverInfos[key], cookie: null }; - } - const keyserverStore = { ...state.keyserverStore, keyserverInfos }; + const { keyserverInfos } = state.keyserverStore; + const keyserverInfosCopy = Object.fromEntries( + Object.keys(keyserverInfos).map(key => [ + key, + keyserverInfos[key].cookie === undefined + ? keyserverInfos[key] + : { + ...keyserverInfos[key], + cookie: null, + }, + ]), + ); + const keyserverStore = { + ...state.keyserverStore, + keyserverInfos: keyserverInfosCopy, + }; + state = { ...state, keyserverStore }; if (state.deviceToken !== undefined && state.deviceToken !== null) { state = { ...state, deviceToken: null }; } const stateCopy = clone(state); sanitizePII(stateCopy, redactionHelpers); return stateCopy; } export { sanitizeActionSecrets, sanitizeAction, sanitizeState, sanitizeReduxReport, };