diff --git a/frontend/controller/app/identity.js b/frontend/controller/app/identity.js index 1b8fdb48c..56408316d 100644 --- a/frontend/controller/app/identity.js +++ b/frontend/controller/app/identity.js @@ -425,16 +425,21 @@ export default (sbp('sbp/selectors/register', { const errMessage = e?.message || String(e) console.error('[gi.app/identity] Error during login contract sync', e) + const wipeOut = (e && (e.name === 'ChelErrorForkedChain' || e.cause?.name === 'ChelErrorForkedChain')) + const promptOptions = { heading: L('Login error'), - question: L('Do you want to log out? {br_}Error details: {err}.', { err: errMessage, ...LTags() }), + question: wipeOut + ? L('The server\'s history for your identity contract has diverged from ours. This can happen in extremely rare circumstances due to either malicious activity or a bug. {br_}To fix this, the contract needs to be resynced, and some recent events may be missing. {br_}Would you like to log out and resync data on your next login? {br_}Error details: {err}.', { err: errMessage, ...LTags() }) + : L('Do you want to log out? {br_}Error details: {err}.', { err: errMessage, ...LTags() }), primaryButton: L('No'), secondaryButton: L('Yes') } const result = await sbp('gi.ui/prompt', promptOptions) if (!result) { - return sbp('gi.app/identity/_private/logout', state) + sbp('gi.ui/clearBanner') + return sbp('gi.app/identity/_private/logout', state, wipeOut) } else { sbp('okTurtles.events/emit', LOGIN_ERROR, { username, identityContractID, error: e }) throw e diff --git a/frontend/main.js b/frontend/main.js index 0b09877b2..1712ac8b4 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -109,6 +109,15 @@ async function startApp () { sbp('gi.ui/seriousErrorBanner', error) if (error?.name === 'ChelErrorForkedChain') { const rootState = sbp('state/vuex/state') + if (!rootState.contracts[contractID]) { + // If `rootState.contracts[contractID]` doesn't exist, it means we're no + // longer subscribed to the contract. This could happen, e.g., if the + // contract has since been released. In any case, the absence of + // `rootState.contracts[contractID]` means that there's nothing to + // left to recover. + console.error('Forked chain detected. However, there is no contract entry.', { contractID }, error) + return + } const type = rootState.contracts[contractID].type || '(unknown)' console.error('Forked chain detected', { contractID, type }, error) @@ -116,10 +125,15 @@ async function startApp () { if (retry) { sbp('gi.ui/clearBanner') - sbp('chelonia/contract/sync', contractID, { resync: true }).catch((e) => { - console.error('Error during re-sync', contractID, e) - alert(L('There was a problem resyncing the contract: {errMsg}\n\nPlease see the Application Logs under User Settings for more details. The Troubleshooting page in User Settings may be another way to fix the problem.', { errMsg: e?.message || e })) - }) + // If it's our identity contract, we need to log in again to be able + // to propery decrypt all data, since that requires the user password + ;((rootState.loggedIn?.identityContractID === contractID) + ? sbp('gi.actions/identity/logout', null, true) + : sbp('chelonia/contract/sync', contractID, { resync: true })) + .catch((e) => { + console.error('Error during re-sync', contractID, e) + alert(L('There was a problem resyncing the contract: {errMsg}\n\nPlease see the Application Logs under User Settings for more details. The Troubleshooting page in User Settings may be another way to fix the problem.', { errMsg: e?.message || e })) + }) } } if (process.env.CI) { diff --git a/shared/domains/chelonia/errors.js b/shared/domains/chelonia/errors.js index bf5923fd6..860a43a90 100644 --- a/shared/domains/chelonia/errors.js +++ b/shared/domains/chelonia/errors.js @@ -14,7 +14,7 @@ export const ChelErrorGenerator = ( // $FlowFixMe[prop-missing] if (params[1]?.cause !== this.cause) { // $FlowFixMe[prop-missing] - Object.defineProperty(this, 'cause', { value: params[1].cause }) + Object.defineProperty(this, 'cause', { configurable: true, writable: true, value: params[1].cause }) } if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor) diff --git a/shared/serdes/index.js b/shared/serdes/index.js index b4c992016..2f1473908 100644 --- a/shared/serdes/index.js +++ b/shared/serdes/index.js @@ -68,6 +68,10 @@ export const serializer = (data: any): any => { if (value instanceof Error) { const pos = verbatim.length verbatim[verbatim.length] = value + // We need to also serialize `Error.cause` recursively + if (value.cause) { + value.cause = serializer(value.cause).data + } return rawResult(['_', '_err', rawResult(['_', '_ref', pos]), value.name]) } // Same for other types supported by structuredClone but not JSON @@ -177,6 +181,9 @@ export const deserializer = (data: any): any => { if (value[2].name !== value[3]) { value[2].name = value[3] } + if (value[2].cause) { + value[2].cause = deserializer(value[2].cause) + } return value[2] } // These were functions converted to a MessagePort. Convert them on this