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
+28 -19
View File
@@ -57,11 +57,7 @@ import { Auth } from './plugins/auth';
import { HashCash } from './plugins/hashcash';
import { NbStoreNativeDBApis } from './plugins/nbstore';
import { Preview } from './plugins/preview';
import {
deleteEndpointToken,
readEndpointToken,
writeEndpointToken,
} from './proxy';
import { clearEndpointSession, getValidAccessToken } from './proxy';
const storeManagerClient = createStoreManagerClient();
setTelemetryTransport(storeManagerClient.telemetry);
@@ -185,46 +181,44 @@ framework.scope(ServerScope).override(AuthProvider, resolver => {
const endpoint = serverService.server.baseUrl;
return {
async signInMagicLink(email, linkToken, clientNonce) {
const { token } = await Auth.signInMagicLink({
await Auth.signInMagicLink({
endpoint,
email,
token: linkToken,
clientNonce,
});
await writeEndpointToken(endpoint, token);
},
async signInOauth(code, state, _provider, clientNonce) {
const { token } = await Auth.signInOauth({
await Auth.signInOauth({
endpoint,
code,
state,
clientNonce,
});
await writeEndpointToken(endpoint, token);
return {};
},
async signInPassword(credential) {
const { token } = await Auth.signInPassword({
await Auth.signInPassword({
endpoint,
...credential,
});
await writeEndpointToken(endpoint, token);
},
async signInOpenAppSignInCode(code) {
const { token } = await Auth.signInOpenApp({
await Auth.signInOpenApp({
endpoint,
code,
});
await writeEndpointToken(endpoint, token);
},
async signOut() {
const token = await readEndpointToken(endpoint);
try {
await Auth.signOut({ endpoint, token });
await Auth.signOut({ endpoint });
} finally {
await deleteEndpointToken(endpoint);
await clearEndpointSession(endpoint);
}
},
async clearSession() {
await clearEndpointSession(endpoint);
},
};
});
@@ -310,6 +304,13 @@ window.addEventListener('focus', () => {
frameworkProvider.get(LifecycleService).applicationFocus();
});
frameworkProvider.get(LifecycleService).applicationStart();
CapacitorApp.addListener('appStateChange', ({ isActive }) => {
if (!isActive) return;
const servers = frameworkProvider.get(ServersService).servers$.value;
Promise.allSettled(
servers.map(server => getValidAccessToken(server.baseUrl))
).catch(console.error);
}).catch(console.error);
const getErrorMessage = (error: unknown, fallback: string) => {
if (typeof error === 'string' && error) {
@@ -462,13 +463,21 @@ function createStoreManagerClient() {
authTokenChannelServer.addEventListener('message', event => {
const { id, endpoint } = event.data as { id?: string; endpoint?: string };
if (!id || !endpoint) return;
readEndpointToken(endpoint)
getValidAccessToken(endpoint)
.then(token => authTokenChannelServer.postMessage({ id, token }))
.catch(() => authTokenChannelServer.postMessage({ id, token: null }));
.catch(error =>
authTokenChannelServer.postMessage({
id,
error:
typeof error === 'object' && error && 'code' in error
? error.code
: 'AUTH_SESSION_TEMPORARILY_UNAVAILABLE',
})
);
});
authTokenChannelServer.start();
worker.postMessage(
{ type: 'native-auth-token-channel', port: authTokenChannelClient },
{ type: 'auth-access-token-channel', port: authTokenChannelClient },
[authTokenChannelClient]
);
return new StoreManagerClient(new OpClient(worker));