feat: implement desktop authorization recovery handling and session management improvements
This commit is contained in:
@@ -83,6 +83,7 @@ import {
|
|||||||
resolveDesktopClientReleaseDownloadURL,
|
resolveDesktopClientReleaseDownloadURL,
|
||||||
retryDesktopPublishTask,
|
retryDesktopPublishTask,
|
||||||
rotateDesktopClient,
|
rotateDesktopClient,
|
||||||
|
setDesktopUnauthorizedRecoveryHandler,
|
||||||
} from './transport/api-client'
|
} from './transport/api-client'
|
||||||
import { initTray, showTrayBalloon } from './tray'
|
import { initTray, showTrayBalloon } from './tray'
|
||||||
import { STANDARD_USER_AGENT } from './user-agent'
|
import { STANDARD_USER_AGENT } from './user-agent'
|
||||||
@@ -174,6 +175,7 @@ let loginWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
|
|||||||
let settingsWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
|
let settingsWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
|
||||||
let windowModeSwitchQueue: Promise<void> = Promise.resolve()
|
let windowModeSwitchQueue: Promise<void> = Promise.resolve()
|
||||||
let clientTokenRotatePromise: Promise<DesktopClientRotateResponse> | null = null
|
let clientTokenRotatePromise: Promise<DesktopClientRotateResponse> | null = null
|
||||||
|
let desktopAuthRecoveryPromise: Promise<boolean> | null = null
|
||||||
let canHandleDesktopDeepLinks = false
|
let canHandleDesktopDeepLinks = false
|
||||||
const pendingDesktopDeepLinks: string[] = []
|
const pendingDesktopDeepLinks: string[] = []
|
||||||
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>()
|
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>()
|
||||||
@@ -200,7 +202,19 @@ interface WindowModeRequest {
|
|||||||
animate?: boolean
|
animate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesktopAuthRecoveryRequest {
|
||||||
|
reason: 'desktop-client-token-unauthorized'
|
||||||
|
method: string
|
||||||
|
path: string
|
||||||
|
status: number
|
||||||
|
code?: number
|
||||||
|
message: string
|
||||||
|
requestId: string
|
||||||
|
at: number
|
||||||
|
}
|
||||||
|
|
||||||
const WINDOW_READY_TIMEOUT_MS = 1200
|
const WINDOW_READY_TIMEOUT_MS = 1200
|
||||||
|
const DESKTOP_AUTH_RECOVERY_TIMEOUT_MS = 12_000
|
||||||
|
|
||||||
const WINDOW_SIZES: Record<WindowMode, WindowSizePreset> = {
|
const WINDOW_SIZES: Record<WindowMode, WindowSizePreset> = {
|
||||||
login: { width: 340, height: 540, minWidth: 340, minHeight: 540, resizable: false },
|
login: { width: 340, height: 540, minWidth: 340, minHeight: 540, resizable: false },
|
||||||
@@ -442,6 +456,92 @@ async function clearRendererDesktopSessions(): Promise<void> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function desktopAuthRecoveryRequestId(): string {
|
||||||
|
return `desktop-auth-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendDesktopAuthRecoveryRequest(payload: DesktopAuthRecoveryRequest): boolean {
|
||||||
|
const targetContents =
|
||||||
|
mainRendererContents && !mainRendererContents.isDestroyed()
|
||||||
|
? mainRendererContents
|
||||||
|
: (currentMainWindow() ?? currentActiveWindow())?.webContents
|
||||||
|
|
||||||
|
if (targetContents && !targetContents.isDestroyed()) {
|
||||||
|
targetContents.send('desktop:auth-recovery-requested', payload)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expireDesktopSessionToLogin(reason: string): Promise<void> {
|
||||||
|
console.warn('[desktop-main] desktop auth expired; returning to login', { reason })
|
||||||
|
currentWindowMode = 'login'
|
||||||
|
persistWindowMode('login')
|
||||||
|
await Promise.race([clearRendererDesktopSessions(), timeout(1000)])
|
||||||
|
await Promise.race([releaseRuntimeSession({ skipRemote: true }), timeout(1500)])
|
||||||
|
await queueWindowModeSwitch('login', { source: 'auth-state', animate: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestDesktopAuthRecovery(context: {
|
||||||
|
method: string
|
||||||
|
path: string
|
||||||
|
status: number
|
||||||
|
code?: number
|
||||||
|
message: string
|
||||||
|
}): Promise<boolean> {
|
||||||
|
if (desktopAuthRecoveryPromise) {
|
||||||
|
return desktopAuthRecoveryPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
desktopAuthRecoveryPromise = (async () => {
|
||||||
|
const requestId = desktopAuthRecoveryRequestId()
|
||||||
|
const beforeToken = getRuntimeControllerSnapshot().session?.client_token ?? null
|
||||||
|
|
||||||
|
const requested = sendDesktopAuthRecoveryRequest({
|
||||||
|
reason: 'desktop-client-token-unauthorized',
|
||||||
|
method: context.method,
|
||||||
|
path: context.path,
|
||||||
|
status: context.status,
|
||||||
|
code: context.code,
|
||||||
|
message: context.message,
|
||||||
|
requestId,
|
||||||
|
at: Date.now(),
|
||||||
|
})
|
||||||
|
if (!requested) {
|
||||||
|
throw new Error('desktop_auth_recovery_target_unavailable')
|
||||||
|
}
|
||||||
|
|
||||||
|
const deadline = Date.now() + DESKTOP_AUTH_RECOVERY_TIMEOUT_MS
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await timeout(250)
|
||||||
|
const snapshot = getRuntimeControllerSnapshot()
|
||||||
|
const nextToken = snapshot.session?.client_token ?? null
|
||||||
|
if (nextToken && nextToken !== beforeToken) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!snapshot.session) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await expireDesktopSessionToLogin(
|
||||||
|
context.message || `desktop_auth_recovery_failed_${requestId}`,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
})()
|
||||||
|
.catch(async (error) => {
|
||||||
|
await expireDesktopSessionToLogin(
|
||||||
|
error instanceof Error ? error.message : 'desktop_auth_recovery_failed',
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
desktopAuthRecoveryPromise = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return desktopAuthRecoveryPromise
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureMainWindow(): Promise<ElectronBrowserWindow> {
|
async function ensureMainWindow(): Promise<ElectronBrowserWindow> {
|
||||||
const existing = currentMainWindow()
|
const existing = currentMainWindow()
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -1214,15 +1314,21 @@ function registerBridgeHandlers(): void {
|
|||||||
}
|
}
|
||||||
return clientTokenRotatePromise
|
return clientTokenRotatePromise
|
||||||
})
|
})
|
||||||
safeHandle('desktop:runtime-session-release', async (_event, revoke?: boolean) => {
|
safeHandle(
|
||||||
|
'desktop:runtime-session-release',
|
||||||
|
async (_event, revoke?: boolean, options?: { skipRemote?: unknown }) => {
|
||||||
if (revoke) {
|
if (revoke) {
|
||||||
currentWindowMode = 'login'
|
currentWindowMode = 'login'
|
||||||
persistWindowMode('login')
|
persistWindowMode('login')
|
||||||
await clearRendererDesktopSessions()
|
await clearRendererDesktopSessions()
|
||||||
}
|
}
|
||||||
await releaseRuntimeSession({ revoke: Boolean(revoke) })
|
await releaseRuntimeSession({
|
||||||
return null
|
revoke: Boolean(revoke),
|
||||||
|
skipRemote: options?.skipRemote === true,
|
||||||
})
|
})
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
)
|
||||||
safeHandle(
|
safeHandle(
|
||||||
'desktop:set-window-mode',
|
'desktop:set-window-mode',
|
||||||
async (event: IpcMainInvokeEvent, mode: WindowMode, request?: unknown) => {
|
async (event: IpcMainInvokeEvent, mode: WindowMode, request?: unknown) => {
|
||||||
@@ -1264,6 +1370,7 @@ if (!hasSingleInstanceLock) {
|
|||||||
await initSessionRegistry()
|
await initSessionRegistry()
|
||||||
initAccountHealth()
|
initAccountHealth()
|
||||||
initTransport()
|
initTransport()
|
||||||
|
setDesktopUnauthorizedRecoveryHandler(requestDesktopAuthRecovery)
|
||||||
initScheduler()
|
initScheduler()
|
||||||
startStorageCleanupScheduler()
|
startStorageCleanupScheduler()
|
||||||
initProcessMetricsSampler()
|
initProcessMetricsSampler()
|
||||||
|
|||||||
@@ -716,9 +716,11 @@ export function syncRuntimeSession(next: DesktopRuntimeSessionSyncRequest | null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function releaseRuntimeSession(options: { revoke?: boolean } = {}): Promise<void> {
|
export async function releaseRuntimeSession(
|
||||||
|
options: { revoke?: boolean; skipRemote?: boolean } = {},
|
||||||
|
): Promise<void> {
|
||||||
const currentSession = state.session
|
const currentSession = state.session
|
||||||
const shouldReleaseRemote = canRunLive(currentSession)
|
const shouldReleaseRemote = !options.skipRemote && canRunLive(currentSession)
|
||||||
|
|
||||||
if (shouldReleaseRemote) {
|
if (shouldReleaseRemote) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ interface DesktopTransportSession {
|
|||||||
clientToken: string | null
|
clientToken: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DesktopRequestMethod = 'GET' | 'POST' | 'DELETE'
|
||||||
|
|
||||||
|
interface DesktopUnauthorizedRecoveryContext {
|
||||||
|
method: DesktopRequestMethod
|
||||||
|
path: string
|
||||||
|
status: number
|
||||||
|
code?: number
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DesktopUnauthorizedRecoveryHandler = (
|
||||||
|
context: DesktopUnauthorizedRecoveryContext,
|
||||||
|
) => Promise<boolean>
|
||||||
|
|
||||||
interface MonitoringCancelTaskPayload {
|
interface MonitoringCancelTaskPayload {
|
||||||
lease_token: string
|
lease_token: string
|
||||||
reason?: string | null
|
reason?: string | null
|
||||||
@@ -75,6 +89,7 @@ let transportSession: DesktopTransportSession = {
|
|||||||
baseURL: normalizeDesktopApiBaseURL(process.env.DESKTOP_API_BASE_URL),
|
baseURL: normalizeDesktopApiBaseURL(process.env.DESKTOP_API_BASE_URL),
|
||||||
clientToken: null,
|
clientToken: null,
|
||||||
}
|
}
|
||||||
|
let unauthorizedRecoveryHandler: DesktopUnauthorizedRecoveryHandler | null = null
|
||||||
const ACCOUNT_UPSERT_CACHE_TTL_MS = 5 * 60_000
|
const ACCOUNT_UPSERT_CACHE_TTL_MS = 5 * 60_000
|
||||||
const ACCOUNT_UPSERT_DIAGNOSTIC_FILE = 'desktop-account-upsert-errors.jsonl'
|
const ACCOUNT_UPSERT_DIAGNOSTIC_FILE = 'desktop-account-upsert-errors.jsonl'
|
||||||
|
|
||||||
@@ -319,8 +334,40 @@ function shouldFallbackToMainProcess(error: unknown): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDesktopUnauthorizedError(error: unknown): error is ApiClientError {
|
||||||
|
return error instanceof ApiClientError && error.status === 401
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoverDesktopAuthorization(
|
||||||
|
method: DesktopRequestMethod,
|
||||||
|
path: string,
|
||||||
|
error: unknown,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!unauthorizedRecoveryHandler || !isDesktopUnauthorizedError(error)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransportAuthState('expired')
|
||||||
|
try {
|
||||||
|
return await unauthorizedRecoveryHandler({
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
status: error.status ?? 401,
|
||||||
|
code: error.code,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
} catch (recoveryError) {
|
||||||
|
console.warn('[desktop-transport] unauthorized recovery failed', {
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
message: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function proxyDesktopRequest<T, B = unknown>(
|
async function proxyDesktopRequest<T, B = unknown>(
|
||||||
method: 'GET' | 'POST' | 'DELETE',
|
method: DesktopRequestMethod,
|
||||||
path: string,
|
path: string,
|
||||||
body?: B,
|
body?: B,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@@ -384,13 +431,23 @@ async function desktopGet<T>(path: string): Promise<T> {
|
|||||||
try {
|
try {
|
||||||
return await proxyDesktopRequest<T>('GET', path)
|
return await proxyDesktopRequest<T>('GET', path)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (await recoverDesktopAuthorization('GET', path, error)) {
|
||||||
|
return proxyDesktopRequest<T>('GET', path)
|
||||||
|
}
|
||||||
if (!shouldFallbackToMainProcess(error)) {
|
if (!shouldFallbackToMainProcess(error)) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
transportState.requestDebugMode = 'main-process'
|
transportState.requestDebugMode = 'main-process'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
return await getDesktopApiClient().get<T>(path)
|
||||||
|
} catch (error) {
|
||||||
|
if (await recoverDesktopAuthorization('GET', path, error)) {
|
||||||
return getDesktopApiClient().get<T>(path)
|
return getDesktopApiClient().get<T>(path)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function desktopPost<T, B = unknown>(path: string, body?: B): Promise<T> {
|
async function desktopPost<T, B = unknown>(path: string, body?: B): Promise<T> {
|
||||||
@@ -399,13 +456,23 @@ async function desktopPost<T, B = unknown>(path: string, body?: B): Promise<T> {
|
|||||||
try {
|
try {
|
||||||
return await proxyDesktopRequest<T, B>('POST', path, body)
|
return await proxyDesktopRequest<T, B>('POST', path, body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (await recoverDesktopAuthorization('POST', path, error)) {
|
||||||
|
return proxyDesktopRequest<T, B>('POST', path, body)
|
||||||
|
}
|
||||||
if (!shouldFallbackToMainProcess(error)) {
|
if (!shouldFallbackToMainProcess(error)) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
transportState.requestDebugMode = 'main-process'
|
transportState.requestDebugMode = 'main-process'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
return await getDesktopApiClient().post<T, B>(path, body)
|
||||||
|
} catch (error) {
|
||||||
|
if (await recoverDesktopAuthorization('POST', path, error)) {
|
||||||
return getDesktopApiClient().post<T, B>(path, body)
|
return getDesktopApiClient().post<T, B>(path, body)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function desktopDelete<T>(path: string): Promise<T> {
|
async function desktopDelete<T>(path: string): Promise<T> {
|
||||||
@@ -414,13 +481,23 @@ async function desktopDelete<T>(path: string): Promise<T> {
|
|||||||
try {
|
try {
|
||||||
return await proxyDesktopRequest<T>('DELETE', path)
|
return await proxyDesktopRequest<T>('DELETE', path)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (await recoverDesktopAuthorization('DELETE', path, error)) {
|
||||||
|
return proxyDesktopRequest<T>('DELETE', path)
|
||||||
|
}
|
||||||
if (!shouldFallbackToMainProcess(error)) {
|
if (!shouldFallbackToMainProcess(error)) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
transportState.requestDebugMode = 'main-process'
|
transportState.requestDebugMode = 'main-process'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
return await getDesktopApiClient().remove<T>(path)
|
||||||
|
} catch (error) {
|
||||||
|
if (await recoverDesktopAuthorization('DELETE', path, error)) {
|
||||||
return getDesktopApiClient().remove<T>(path)
|
return getDesktopApiClient().remove<T>(path)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initTransport(): ApiClient {
|
export function initTransport(): ApiClient {
|
||||||
@@ -474,6 +551,12 @@ export function setTransportDispatchState(next: TransportDispatchState): void {
|
|||||||
transportState.dispatchState = next
|
transportState.dispatchState = next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setDesktopUnauthorizedRecoveryHandler(
|
||||||
|
handler: DesktopUnauthorizedRecoveryHandler | null,
|
||||||
|
): void {
|
||||||
|
unauthorizedRecoveryHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
export function noteTransportHeartbeat(success: boolean): void {
|
export function noteTransportHeartbeat(success: boolean): void {
|
||||||
transportState.lastHeartbeatAt = Date.now()
|
transportState.lastHeartbeatAt = Date.now()
|
||||||
transportState.lastHeartbeatStatus = success ? 'success' : 'failed'
|
transportState.lastHeartbeatStatus = success ? 'success' : 'failed'
|
||||||
|
|||||||
@@ -39,6 +39,17 @@ interface WindowModeRequest {
|
|||||||
animate?: boolean
|
animate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesktopAuthRecoveryRequest {
|
||||||
|
reason: 'desktop-client-token-unauthorized'
|
||||||
|
method: string
|
||||||
|
path: string
|
||||||
|
status: number
|
||||||
|
code?: number
|
||||||
|
message: string
|
||||||
|
requestId: string
|
||||||
|
at: number
|
||||||
|
}
|
||||||
|
|
||||||
interface DesktopAppSettings {
|
interface DesktopAppSettings {
|
||||||
openAtLogin: boolean
|
openAtLogin: boolean
|
||||||
keepRunningInBackground: boolean
|
keepRunningInBackground: boolean
|
||||||
@@ -243,10 +254,19 @@ const desktopBridge = {
|
|||||||
ipcRenderer.invoke(
|
ipcRenderer.invoke(
|
||||||
'desktop:runtime-session-rotate-client-token',
|
'desktop:runtime-session-rotate-client-token',
|
||||||
) as Promise<DesktopClientRotateResponse>,
|
) as Promise<DesktopClientRotateResponse>,
|
||||||
releaseRuntimeSession: (revoke?: boolean) =>
|
releaseRuntimeSession: (revoke?: boolean, options?: { skipRemote?: boolean }) =>
|
||||||
ipcRenderer.invoke('desktop:runtime-session-release', revoke) as Promise<null>,
|
ipcRenderer.invoke('desktop:runtime-session-release', revoke, options) as Promise<null>,
|
||||||
setWindowMode: (mode: 'login' | 'main', request?: WindowModeRequest) =>
|
setWindowMode: (mode: 'login' | 'main', request?: WindowModeRequest) =>
|
||||||
ipcRenderer.invoke('desktop:set-window-mode', mode, request) as Promise<null>,
|
ipcRenderer.invoke('desktop:set-window-mode', mode, request) as Promise<null>,
|
||||||
|
onAuthRecoveryRequested: (listener: (event: DesktopAuthRecoveryRequest) => void) => {
|
||||||
|
const wrapped = (_event: IpcRendererEvent, payload: DesktopAuthRecoveryRequest) => {
|
||||||
|
listener(payload)
|
||||||
|
}
|
||||||
|
ipcRenderer.on('desktop:auth-recovery-requested', wrapped)
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.off('desktop:auth-recovery-requested', wrapped)
|
||||||
|
}
|
||||||
|
},
|
||||||
onRuntimeInvalidated: (
|
onRuntimeInvalidated: (
|
||||||
listener: (event: {
|
listener: (event: {
|
||||||
reason:
|
reason:
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const error = shallowRef<string | null>(null)
|
|||||||
let loginPromise: Promise<void> | null = null
|
let loginPromise: Promise<void> | null = null
|
||||||
let logoutPromise: Promise<void> | null = null
|
let logoutPromise: Promise<void> | null = null
|
||||||
let renewPromise: Promise<void> | null = null
|
let renewPromise: Promise<void> | null = null
|
||||||
|
let recoverDesktopClientPromise: Promise<void> | null = null
|
||||||
let tokenRenewTimer: ReturnType<typeof setInterval> | null = null
|
let tokenRenewTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
function logRuntimeSessionSyncFailure(reason: string, err: unknown): void {
|
function logRuntimeSessionSyncFailure(reason: string, err: unknown): void {
|
||||||
@@ -300,6 +301,20 @@ async function rotateRuntimeClientToken(current: DesktopSession): Promise<Deskto
|
|||||||
return applyRotatedDesktopClient(current, rotated)
|
return applyRotatedDesktopClient(current, rotated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshDesktopClientRegistration(current: DesktopSession): Promise<DesktopSession> {
|
||||||
|
if (!current.user) {
|
||||||
|
throw new Error('missing_desktop_user')
|
||||||
|
}
|
||||||
|
|
||||||
|
const registered = await registerDesktopClient(current.user)
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
clientToken: registered.client_token,
|
||||||
|
clientTokenExpiresAt: registered.expires_at,
|
||||||
|
desktopClient: registered.client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyRotatedDesktopClient(
|
function applyRotatedDesktopClient(
|
||||||
current: DesktopSession,
|
current: DesktopSession,
|
||||||
rotated: DesktopClientRotateResponse,
|
rotated: DesktopClientRotateResponse,
|
||||||
@@ -317,7 +332,7 @@ async function forceLogoutAfterRenewFailure(err: unknown): Promise<void> {
|
|||||||
error.value = '登录状态已过期,请重新登录'
|
error.value = '登录状态已过期,请重新登录'
|
||||||
persistSession(null)
|
persistSession(null)
|
||||||
try {
|
try {
|
||||||
await window.desktopBridge?.app?.releaseRuntimeSession?.(true)
|
await window.desktopBridge?.app?.releaseRuntimeSession?.(false, { skipRemote: true })
|
||||||
} catch (releaseError) {
|
} catch (releaseError) {
|
||||||
console.warn('[desktop-session] release after renewal failure failed', releaseError)
|
console.warn('[desktop-session] release after renewal failure failed', releaseError)
|
||||||
}
|
}
|
||||||
@@ -400,6 +415,54 @@ async function renewAuthenticatedSession(options: { force?: boolean } = {}): Pro
|
|||||||
return renewPromise
|
return renewPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function recoverDesktopClientSession(): Promise<void> {
|
||||||
|
const current = session.value
|
||||||
|
if (current?.mode !== 'authenticated') {
|
||||||
|
await forceLogoutAfterRenewFailure(new Error('desktop_auth_recovery_requires_login'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recoverDesktopClientPromise) {
|
||||||
|
return recoverDesktopClientPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
recoverDesktopClientPromise = (async () => {
|
||||||
|
let next = current
|
||||||
|
let expectedSession = current
|
||||||
|
|
||||||
|
try {
|
||||||
|
next = await refreshLoginTokens(next)
|
||||||
|
} catch (err) {
|
||||||
|
if (isApiAuthExpiredError(err)) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
keepSessionAfterRenewFailure(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.value !== expectedSession) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
persistSession(next)
|
||||||
|
expectedSession = next
|
||||||
|
|
||||||
|
next = await refreshDesktopClientRegistration(next)
|
||||||
|
if (session.value !== expectedSession) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
persistSession(next)
|
||||||
|
})()
|
||||||
|
.catch(async (err) => {
|
||||||
|
await forceLogoutAfterRenewFailure(err)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
recoverDesktopClientPromise = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return recoverDesktopClientPromise
|
||||||
|
}
|
||||||
|
|
||||||
function ensureTokenRenewTimer(): void {
|
function ensureTokenRenewTimer(): void {
|
||||||
if (typeof window === 'undefined' || tokenRenewTimer) {
|
if (typeof window === 'undefined' || tokenRenewTimer) {
|
||||||
return
|
return
|
||||||
@@ -820,6 +883,15 @@ if (typeof window !== 'undefined') {
|
|||||||
void syncStoredDesktopClientDeviceInfo()
|
void syncStoredDesktopClientDeviceInfo()
|
||||||
ensureTokenRenewTimer()
|
ensureTokenRenewTimer()
|
||||||
void renewAuthenticatedSession()
|
void renewAuthenticatedSession()
|
||||||
|
|
||||||
|
window.desktopBridge?.app?.onAuthRecoveryRequested?.((event) => {
|
||||||
|
void recoverDesktopClientSession().catch((err) => {
|
||||||
|
console.warn('[desktop-session] desktop auth recovery failed', {
|
||||||
|
requestId: event.requestId,
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDesktopSession() {
|
export function useDesktopSession() {
|
||||||
@@ -833,6 +905,7 @@ export function useDesktopSession() {
|
|||||||
setApiBaseURL: persistApiBaseURL,
|
setApiBaseURL: persistApiBaseURL,
|
||||||
syncRuntimeSession: syncRuntimeSessionToMain,
|
syncRuntimeSession: syncRuntimeSessionToMain,
|
||||||
renewSession: renewAuthenticatedSession,
|
renewSession: renewAuthenticatedSession,
|
||||||
|
recoverDesktopClientSession,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
enterPreview,
|
enterPreview,
|
||||||
|
|||||||
+14
-1
@@ -9,6 +9,7 @@ import type {
|
|||||||
DesktopClientUpdateResult,
|
DesktopClientUpdateResult,
|
||||||
DesktopPublishTaskListResponse,
|
DesktopPublishTaskListResponse,
|
||||||
DesktopRuntimeSessionSyncRequest,
|
DesktopRuntimeSessionSyncRequest,
|
||||||
|
DesktopTaskInfo,
|
||||||
ListDesktopPublishTasksParams,
|
ListDesktopPublishTasksParams,
|
||||||
} from '@geo/shared-types'
|
} from '@geo/shared-types'
|
||||||
import type { DesktopObservedRequest } from '../shared/network-debug'
|
import type { DesktopObservedRequest } from '../shared/network-debug'
|
||||||
@@ -75,6 +76,17 @@ declare global {
|
|||||||
secure: boolean
|
secure: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesktopAuthRecoveryRequest {
|
||||||
|
reason: 'desktop-client-token-unauthorized'
|
||||||
|
method: string
|
||||||
|
path: string
|
||||||
|
status: number
|
||||||
|
code?: number
|
||||||
|
message: string
|
||||||
|
requestId: string
|
||||||
|
at: number
|
||||||
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
desktopBridge: {
|
desktopBridge: {
|
||||||
app: {
|
app: {
|
||||||
@@ -132,7 +144,7 @@ declare global {
|
|||||||
cleanStorage(): Promise<DesktopStorageCleanupResult>
|
cleanStorage(): Promise<DesktopStorageCleanupResult>
|
||||||
syncRuntimeSession(session: DesktopRuntimeSessionSyncRequest | null): Promise<null>
|
syncRuntimeSession(session: DesktopRuntimeSessionSyncRequest | null): Promise<null>
|
||||||
rotateRuntimeClientToken(): Promise<DesktopClientRotateResponse>
|
rotateRuntimeClientToken(): Promise<DesktopClientRotateResponse>
|
||||||
releaseRuntimeSession(revoke?: boolean): Promise<null>
|
releaseRuntimeSession(revoke?: boolean, options?: { skipRemote?: boolean }): Promise<null>
|
||||||
setWindowMode(
|
setWindowMode(
|
||||||
mode: 'login' | 'main',
|
mode: 'login' | 'main',
|
||||||
request?: {
|
request?: {
|
||||||
@@ -140,6 +152,7 @@ declare global {
|
|||||||
animate?: boolean
|
animate?: boolean
|
||||||
},
|
},
|
||||||
): Promise<null>
|
): Promise<null>
|
||||||
|
onAuthRecoveryRequested(listener: (event: DesktopAuthRecoveryRequest) => void): () => void
|
||||||
onRuntimeInvalidated(
|
onRuntimeInvalidated(
|
||||||
listener: (event: {
|
listener: (event: {
|
||||||
reason:
|
reason:
|
||||||
|
|||||||
Reference in New Issue
Block a user