feat(core): improve login flow (#15219)

#### PR Dependency Tree


* **PR #15219** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Added secure, automatic auth session token refresh and request replay
for expired-token responses across Android, iOS, and Electron.
* Updated sign-in flows to manage sessions without returning tokens to
the app layer.
* Added “Devices” management UI with sign out per device and sign out
all.
  * Enabled support for both Hashcash and Turnstile captcha providers.
* **Bug Fixes**
* Improved refresh de-duplication, inflight cancellation/clear behavior,
and recovery from corrupted/invalid sessions.
* **Tests**
* Expanded auth-session, refresh/revoke, and replay coverage (Electron
unit tests, Android instrumentation tests, iOS auth date parser tests).
* **Chores**
* Removed CAPTCHA site key from build-time configuration and adjusted CI
test execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-12 18:11:02 +08:00
committed by GitHub
parent 02b25e05d8
commit abf37d3dfa
57 changed files with 2919 additions and 789 deletions
@@ -11,7 +11,11 @@ import {
resolvePathInBase,
resourcesPath,
} from '../shared/utils';
import { getAuthTokenForUrl } from './auth/native-token';
import {
executeAuthSessionRequest,
getAccessTokenForUrl,
isManagedAuthEndpoint,
} from './auth/auth-session';
import { buildType, isDev } from './config';
import { logger } from './logger';
@@ -64,26 +68,6 @@ function buildTargetUrl(base: string, urlObject: URL) {
return new URL(`${urlObject.pathname}${urlObject.search}`, base).toString();
}
async function buildAuthorizedRequest(request: Request, targetUrl: string) {
const clonedRequest = request.clone();
const headers = new Headers(clonedRequest.headers);
const token = getAuthTokenForUrl(targetUrl);
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return new Request(targetUrl, {
body:
clonedRequest.method === 'GET' || clonedRequest.method === 'HEAD'
? undefined
: clonedRequest.body,
headers,
method: clonedRequest.method,
redirect: clonedRequest.redirect,
signal: clonedRequest.signal,
});
}
async function proxyRequest(
request: Request,
urlObject: URL,
@@ -92,13 +76,13 @@ async function proxyRequest(
) {
const { bypassCustomProtocolHandlers = true } = options;
const targetUrl = buildTargetUrl(base, urlObject);
const authorizedRequest = await buildAuthorizedRequest(request, targetUrl);
const proxiedRequest = bypassCustomProtocolHandlers
? Object.assign(authorizedRequest, {
bypassCustomProtocolHandlers: true,
})
: authorizedRequest;
return net.fetch(proxiedRequest);
return await executeAuthSessionRequest(request, targetUrl, request =>
net.fetch(
bypassCustomProtocolHandlers
? Object.assign(request, { bypassCustomProtocolHandlers: true })
: request
)
);
}
async function handleFileRequest(request: Request) {
@@ -268,17 +252,20 @@ export function registerProtocol() {
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
const url = new URL(details.url);
(async () => {
if (
url.protocol === 'http:' ||
const managedAuthRequest =
(url.protocol === 'http:' ||
url.protocol === 'https:' ||
url.protocol === 'ws:' ||
url.protocol === 'wss:'
) {
const token = getAuthTokenForUrl(details.url);
url.protocol === 'wss:') &&
isManagedAuthEndpoint(details.url);
let cancel = false;
(async () => {
if (managedAuthRequest) {
delete details.requestHeaders.authorization;
delete details.requestHeaders.Authorization;
const token = await getAccessTokenForUrl(details.url, 120_000);
if (token) {
delete details.requestHeaders.authorization;
details.requestHeaders.Authorization = `Bearer ${token}`;
}
}
@@ -292,11 +279,12 @@ export function registerProtocol() {
}
})()
.catch(err => {
cancel = managedAuthRequest;
logger.error('error handling before send headers', err);
})
.finally(() => {
callback({
cancel: false,
cancel,
requestHeaders: details.requestHeaders,
});
});