feat(core): improve auth handling (#15271)

fix #15270
fix #15260
fix #15257


#### PR Dependency Tree


* **PR #15271** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
DarkSky
2026-07-18 06:27:01 +08:00
committed by GitHub
parent 427db39862
commit d24c17f300
17 changed files with 273 additions and 99 deletions
@@ -8,9 +8,10 @@ import {
type AuthTokenResponse,
classifyAuthError,
} from '@affine/auth';
import { app, net, safeStorage } from 'electron';
import { app, safeStorage } from 'electron';
import { logger } from '../logger';
import { authFetch } from './transport';
const FILEPATH = path.join(app.getPath('userData'), 'auth-sessions.json');
const TEMP_FILEPATH = `${FILEPATH}.tmp`;
@@ -105,7 +106,7 @@ function storage(endpoint: string) {
}
async function refresh(endpoint: string, refreshToken: string) {
const response = await net.fetch(
const response = await authFetch(
new URL('/api/auth/session/refresh', endpoint).toString(),
{
method: 'POST',
@@ -192,7 +193,7 @@ export async function revokeAuthSession(endpoint: string) {
await getAuthSessionBroker(normalized).revoke(
'sign-out',
async (refreshToken: string) => {
const response = await net.fetch(
const response = await authFetch(
new URL('/api/auth/session/revoke', normalized).toString(),
{
method: 'POST',
@@ -1,7 +1,7 @@
import os from 'node:os';
import type { AuthTokenResponse } from '@affine/auth';
import { net, session } from 'electron';
import { session } from 'electron';
import { logger } from '../logger';
import type { NamespaceHandlers } from '../type';
@@ -12,6 +12,7 @@ import {
revokeAuthSession,
setAuthSession,
} from './auth-session';
import { authFetch, getAuthTransportSession } from './transport';
export interface SignInResponse {
id?: string;
@@ -47,14 +48,21 @@ function authUrl(endpoint: string, path: string) {
async function readJson<T>(response: Response): Promise<T> {
const text = await response.text();
if (!response.ok) {
throw new Error(text || response.statusText);
let message = text || response.statusText;
try {
const error = JSON.parse(text);
if (typeof error.message === 'string') {
message = error.message;
}
} catch {}
throw new Error(message);
}
return text ? JSON.parse(text) : ({} as T);
}
async function fetchAuth(endpoint: string, path: string, body?: unknown) {
return await net.fetch(authUrl(endpoint, path), {
return await authFetch(authUrl(endpoint, path), {
method: 'POST',
headers: {
'content-type': 'application/json',
@@ -66,18 +74,21 @@ async function fetchAuth(endpoint: string, path: string, body?: unknown) {
}
async function clearAuthCookies(endpoint: string) {
const sessions = [session.defaultSession, getAuthTransportSession()];
await Promise.all(
authCookieNames.map(name =>
session.defaultSession.cookies
.remove(endpoint, name)
.catch(error =>
logger.debug(
'failed to clear native auth cookie',
endpoint,
name,
error
sessions.flatMap(authSession =>
authCookieNames.map(name =>
authSession.cookies
.remove(endpoint, name)
.catch(error =>
logger.debug(
'failed to clear native auth cookie',
endpoint,
name,
error
)
)
)
)
)
);
}
@@ -148,7 +159,7 @@ export const authHandlers = {
challenge?: string;
}
) => {
const response = await net.fetch(authUrl(endpoint, '/api/auth/sign-in'), {
const response = await authFetch(authUrl(endpoint, '/api/auth/sign-in'), {
method: 'POST',
headers: {
'content-type': 'application/json',
@@ -0,0 +1,11 @@
import { session } from 'electron';
const AUTH_TRANSPORT_PARTITION = 'affine-auth-transport';
export function getAuthTransportSession() {
return session.fromPartition(AUTH_TRANSPORT_PARTITION, { cache: false });
}
export function authFetch(input: string | Request, init?: RequestInit) {
return getAuthTransportSession().fetch(input, init);
}
@@ -18,6 +18,36 @@ if (isDev) {
protocol = 'affine-dev';
}
const authMethods = new Set(['magic-link', 'oauth', 'open-app-signin']);
function summarizeDeepLink(rawUrl: string) {
try {
const url = new URL(rawUrl);
const method = url.searchParams.get('method');
const server = url.searchParams.get('server');
let serverOrigin: string | undefined;
try {
serverOrigin = server ? new URL(server).origin : undefined;
} catch {
serverOrigin = undefined;
}
return {
protocol: url.protocol,
action: url.hostname,
method: method && authMethods.has(method) ? method : undefined,
serverOrigin,
};
} catch {
return { valid: false };
}
}
function logDeepLinkFailure(rawUrl: string, error: unknown) {
logger.error('failed to handle affine url', summarizeDeepLink(rawUrl), {
error: error instanceof Error ? error.name : typeof error,
});
}
export function setupDeepLink(app: App) {
if (process.defaultApp) {
if (process.argv.length >= 2) {
@@ -30,14 +60,14 @@ export function setupDeepLink(app: App) {
}
app.on('open-url', (event, url) => {
logger.log('open-url', url);
logger.log('open-url', summarizeDeepLink(url));
if (url.startsWith(`${protocol}://`)) {
event.preventDefault();
app
.whenReady()
.then(() => handleAffineUrl(url))
.catch(e => {
logger.error('failed to handle affine url', e);
logDeepLinkFailure(url, e);
});
}
});
@@ -55,7 +85,7 @@ export function setupDeepLink(app: App) {
if (url?.startsWith(`${protocol}://`)) {
event.preventDefault();
handleAffineUrl(url).catch(e => {
logger.error('failed to handle affine url', e);
logDeepLinkFailure(url, e);
});
}
})
@@ -66,10 +96,15 @@ export function setupDeepLink(app: App) {
// app may be brought up without having a running instance
// need to read the url from the command line
const url = process.argv.at(-1);
logger.log('url from argv', process.argv, url);
logger.log(
'url from argv',
url?.startsWith(`${protocol}://`)
? summarizeDeepLink(url)
: { deepLink: false, argumentCount: process.argv.length }
);
if (url?.startsWith(`${protocol}://`)) {
handleAffineUrl(url).catch(e => {
logger.error('failed to handle affine url', e);
logDeepLinkFailure(url, e);
});
}
});
@@ -78,7 +113,7 @@ export function setupDeepLink(app: App) {
async function handleAffineUrl(url: string) {
await showMainWindow();
logger.info('open affine url', url);
logger.info('open affine url', summarizeDeepLink(url));
const urlObj = new URL(url);
if (urlObj.hostname === 'authentication') {
@@ -93,7 +128,7 @@ async function handleAffineUrl(url: string) {
method !== 'open-app-signin') ||
!payload
) {
logger.error('Invalid authentication url', url);
logger.error('Invalid authentication url', summarizeDeepLink(url));
return;
}
@@ -99,7 +99,7 @@ export const registerHandlers = () => {
return await handleIpcMessage(e, ...args);
} catch (error) {
logger.error(`error in ipc handler when calling ${args[0]}`, error);
return null;
throw error;
}
});