fix(core): desktop e2e (#15062)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Sign-in flows now reliably propagate richer authentication results
(user data and session type), improving persistence and reducing
intermittent sign-in issues.
* Native token handling gains a fallback for environments without
encrypted storage, improving session reliability.

* **New Features**
* User-visible warning when sign-in is session-only because encrypted
storage is unavailable.

* **Chores**
  * Tooling ignore patterns updated to exclude .codex.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-06-01 23:54:41 +08:00
committed by GitHub
parent 7123595831
commit 38110de134
7 changed files with 79 additions and 19 deletions
@@ -1,3 +1,4 @@
import { notify } from '@affine/component';
import { configureElectronStateStorageImpls } from '@affine/core/desktop/storage';
import { configureCommonModules } from '@affine/core/modules';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header';
@@ -25,6 +26,16 @@ import { configureDesktopWorkbenchModule } from '@affine/core/modules/workbench'
import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import { Framework } from '@toeverything/infra';
function notifySessionOnlySignIn(sessionOnly?: boolean) {
if (!sessionOnly) return;
notify.warning({
title: 'Sign-in is only valid for this session',
message:
'Encrypted storage is unavailable, so you will need to sign in again after restarting AFFiNE.',
});
}
export function setupModules() {
const framework = new Framework();
configureCommonModules(framework);
@@ -75,26 +86,38 @@ export function setupModules() {
return {
async signInMagicLink(email, token, clientNonce) {
await apis.handler.auth.signInMagicLink(
const result = await apis.handler.auth.signInMagicLink(
endpoint,
email,
token,
clientNonce
);
notifySessionOnlySignIn(result.sessionOnly);
},
async signInOauth(code, state, _provider, clientNonce) {
return await apis.handler.auth.signInOauth(
const result = await apis.handler.auth.signInOauth(
endpoint,
code,
state,
clientNonce
);
notifySessionOnlySignIn(result.sessionOnly);
return result;
},
async signInPassword(credential) {
await apis.handler.auth.signInPassword(endpoint, credential);
const result = await apis.handler.auth.signInPassword(
endpoint,
credential
);
notifySessionOnlySignIn(result.sessionOnly);
return result;
},
async signInOpenAppSignInCode(code) {
await apis.handler.auth.signInOpenAppSignInCode(endpoint, code);
const result = await apis.handler.auth.signInOpenAppSignInCode(
endpoint,
code
);
notifySessionOnlySignIn(result.sessionOnly);
},
async signOut() {
await apis.handler.auth.signOut(endpoint);
@@ -19,6 +19,16 @@ export interface SignInResponse {
redirectUri?: string;
}
export interface PasswordSignInResponse extends SignInResponse {
id: string;
email: string;
name: string;
hasPassword: boolean | null;
avatarUrl: string | null;
emailVerified: boolean;
sessionOnly?: boolean;
}
interface ExchangeResponse {
token?: string;
}
@@ -86,8 +96,9 @@ async function exchangeSession(endpoint: string, response: SignInResponse) {
throw new Error('Missing native auth token.');
}
setNativeAuthToken(endpoint, body.token);
const persistent = setNativeAuthToken(endpoint, body.token);
await clearAuthCookies(endpoint);
return { persistent };
}
export const authHandlers = {
@@ -104,7 +115,8 @@ export const authHandlers = {
client_nonce: clientNonce,
});
const body = await readJson<SignInResponse>(response);
await exchangeSession(endpoint, body);
const { persistent } = await exchangeSession(endpoint, body);
return { sessionOnly: !persistent };
},
signInOauth: async (
@@ -120,8 +132,8 @@ export const authHandlers = {
client_nonce: clientNonce,
});
const body = await readJson<SignInResponse>(response);
await exchangeSession(endpoint, body);
return { redirectUri: body.redirectUri };
const { persistent } = await exchangeSession(endpoint, body);
return { redirectUri: body.redirectUri, sessionOnly: !persistent };
},
signInPassword: async (
@@ -152,16 +164,20 @@ export const authHandlers = {
password: credential.password,
}),
});
const body = await readJson<SignInResponse>(response);
await exchangeSession(endpoint, body);
return body;
const body = await readJson<PasswordSignInResponse>(response);
const { persistent } = await exchangeSession(endpoint, body);
return { ...body, sessionOnly: !persistent };
},
signInOpenAppSignInCode: async (_e, endpoint: string, code: string) => {
const response = await fetchAuth(endpoint, '/api/auth/open-app/sign-in', {
code,
});
await exchangeSession(endpoint, await readJson(response));
const { persistent } = await exchangeSession(
endpoint,
await readJson(response)
);
return { sessionOnly: !persistent };
},
signOut: async (_e, endpoint: string) => {
@@ -11,6 +11,9 @@ type TokenRecord = {
token: string;
};
// safeStorage may not be available in some environments (e.g. Linux without a keyring), so we fall back to an in-memory store in that case
const memoryTokenStore: Record<string, string> = {};
function normalizeEndpoint(endpoint: string) {
return new URL(endpoint).origin;
}
@@ -51,19 +54,33 @@ function decryptToken(value: string): TokenRecord | null {
}
export function setNativeAuthToken(endpoint: string, token: string) {
const normalizedEndpoint = normalizeEndpoint(endpoint);
if (!safeStorage.isEncryptionAvailable()) {
memoryTokenStore[normalizedEndpoint] = token;
return false;
}
const store = readStore();
store[normalizeEndpoint(endpoint)] = encryptToken({ token });
store[normalizedEndpoint] = encryptToken({ token });
writeStore(store);
return true;
}
export function deleteNativeAuthToken(endpoint: string) {
const normalizedEndpoint = normalizeEndpoint(endpoint);
delete memoryTokenStore[normalizedEndpoint];
const store = readStore();
delete store[normalizeEndpoint(endpoint)];
delete store[normalizedEndpoint];
writeStore(store);
}
export function getNativeAuthToken(endpoint: string) {
const encrypted = readStore()[normalizeEndpoint(endpoint)];
const normalizedEndpoint = normalizeEndpoint(endpoint);
const memoryToken = memoryTokenStore[normalizedEndpoint];
if (memoryToken) return memoryToken;
const encrypted = readStore()[normalizedEndpoint];
if (!encrypted) return null;
return decryptToken(encrypted)?.token ?? null;
}