322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
import type {
|
|
CreatePublishJobResponse,
|
|
DesktopAccountInfo,
|
|
DesktopBugReportSubmitRequest,
|
|
DesktopBugReportSubmitResponse,
|
|
DesktopClientReleaseCheckResponse,
|
|
DesktopClientRotateResponse,
|
|
DesktopClientUpdateProgressEvent,
|
|
DesktopClientUpdateResult,
|
|
DesktopPublishTaskListResponse,
|
|
DesktopRuntimeSessionSyncRequest,
|
|
DesktopTaskInfo,
|
|
ListDesktopPublishTasksParams,
|
|
} from '@geo/shared-types'
|
|
import type { IpcRendererEvent } from 'electron'
|
|
import { contextBridge, ipcRenderer } from 'electron/renderer'
|
|
import type { DesktopRuntimeSnapshot } from '../renderer/types'
|
|
import type { DesktopObservedRequest } from '../shared/network-debug'
|
|
|
|
interface DesktopDeviceInfo {
|
|
device_name: string
|
|
os: string
|
|
cpu_arch: string
|
|
}
|
|
|
|
interface DesktopAppVersionInfo {
|
|
version: string
|
|
channel: string
|
|
}
|
|
|
|
interface DesktopClientIDScope {
|
|
tenant_id: number
|
|
workspace_id: number
|
|
user_id: number
|
|
}
|
|
|
|
interface WindowModeRequest {
|
|
source?: 'auth-state' | 'login' | 'logout'
|
|
animate?: boolean
|
|
}
|
|
|
|
interface DesktopAppSettings {
|
|
openAtLogin: boolean
|
|
keepRunningInBackground: boolean
|
|
autoCleanStorageWeekly: boolean
|
|
}
|
|
|
|
interface DesktopLoginCredentials {
|
|
identifier: string
|
|
password: string
|
|
}
|
|
|
|
interface DesktopStorageEntry {
|
|
id: string
|
|
label: string
|
|
kind: 'app-cache' | 'bound-account' | 'unbound-session'
|
|
partition: string | null
|
|
accountId: string | null
|
|
sizeBytes: number
|
|
cleanableBytes: number
|
|
lastModifiedAt: number | null
|
|
cleanable: boolean
|
|
active: boolean
|
|
}
|
|
|
|
interface DesktopStorageSnapshot {
|
|
generatedAt: number
|
|
totalBytes: number
|
|
cleanableBytes: number
|
|
appCacheBytes: number
|
|
boundAccountBytes: number
|
|
boundAccountCleanableBytes: number
|
|
unboundSessionBytes: number
|
|
unboundSessionCount: number
|
|
boundAccountCount: number
|
|
entries: DesktopStorageEntry[]
|
|
autoCleanStorageWeekly: boolean
|
|
lastCleanupAt: number | null
|
|
nextCleanupAt: number | null
|
|
}
|
|
|
|
interface DesktopStorageCleanupResult {
|
|
cleanedAt: number
|
|
cleanedBytes: number
|
|
removedEntries: number
|
|
before: DesktopStorageSnapshot
|
|
after: DesktopStorageSnapshot
|
|
}
|
|
|
|
interface WorkbenchNavigationState {
|
|
canGoBack: boolean
|
|
canGoForward: boolean
|
|
loading: boolean
|
|
url: string
|
|
title: string
|
|
host: string
|
|
secure: boolean
|
|
}
|
|
|
|
const rendererProxyRequestChannel = 'desktop:renderer-devtools-proxy:request'
|
|
const rendererProxyResponseChannel = 'desktop:renderer-devtools-proxy:response'
|
|
|
|
if (import.meta.env.DEV) {
|
|
ipcRenderer.on(
|
|
rendererProxyRequestChannel,
|
|
async (
|
|
_event,
|
|
payload: {
|
|
requestId?: unknown
|
|
request?: {
|
|
url?: unknown
|
|
method?: unknown
|
|
headers?: unknown
|
|
body?: unknown
|
|
}
|
|
},
|
|
) => {
|
|
const requestId = typeof payload?.requestId === 'string' ? payload.requestId : ''
|
|
const request = payload?.request
|
|
if (!requestId || !request || typeof request !== 'object') {
|
|
return
|
|
}
|
|
|
|
const url = typeof request.url === 'string' ? request.url : ''
|
|
const method = typeof request.method === 'string' ? request.method : 'GET'
|
|
const headers = isStringRecord(request.headers) ? request.headers : {}
|
|
const body = typeof request.body === 'string' ? request.body : undefined
|
|
|
|
const response = await (async () => {
|
|
try {
|
|
const fetchResponse = await fetch(url, {
|
|
method,
|
|
headers,
|
|
body,
|
|
})
|
|
const bodyText = await fetchResponse.text()
|
|
return {
|
|
ok: fetchResponse.ok,
|
|
status: fetchResponse.status,
|
|
statusText: fetchResponse.statusText,
|
|
headers: Object.fromEntries(fetchResponse.headers.entries()),
|
|
bodyText,
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
status: 0,
|
|
statusText: '',
|
|
headers: {},
|
|
bodyText: '',
|
|
error: String(error && ((error as Error).message || error)),
|
|
}
|
|
}
|
|
})()
|
|
|
|
ipcRenderer.send(rendererProxyResponseChannel, {
|
|
requestId,
|
|
response,
|
|
})
|
|
},
|
|
)
|
|
}
|
|
|
|
const desktopBridge = {
|
|
app: {
|
|
ping: () => ipcRenderer.invoke('desktop:ping') as Promise<string>,
|
|
versionInfo: () => ipcRenderer.invoke('desktop:version-info') as Promise<DesktopAppVersionInfo>,
|
|
checkClientRelease: () =>
|
|
ipcRenderer.invoke(
|
|
'desktop:check-client-release',
|
|
) as Promise<DesktopClientReleaseCheckResponse>,
|
|
clientReleaseDownloadURL: () =>
|
|
ipcRenderer.invoke('desktop:client-release-download-url') as Promise<{
|
|
download_url: string
|
|
}>,
|
|
quitApp: () => ipcRenderer.invoke('desktop:quit-app') as Promise<null>,
|
|
startClientUpdate: () =>
|
|
ipcRenderer.invoke('desktop:start-client-update') as Promise<DesktopClientUpdateResult>,
|
|
deviceInfo: () => ipcRenderer.invoke('desktop:device-info') as Promise<DesktopDeviceInfo>,
|
|
clientId: (scope: DesktopClientIDScope) =>
|
|
ipcRenderer.invoke('desktop:client-id', scope) as Promise<string>,
|
|
runtimeSnapshot: () =>
|
|
ipcRenderer.invoke('desktop:runtime-snapshot') as Promise<DesktopRuntimeSnapshot>,
|
|
runtimeAccountSnapshot: (accountId: string) =>
|
|
ipcRenderer.invoke('desktop:runtime-account-snapshot', accountId) as Promise<
|
|
DesktopRuntimeSnapshot['accounts'][number] | null
|
|
>,
|
|
submitBugReport: (payload: DesktopBugReportSubmitRequest) =>
|
|
ipcRenderer.invoke(
|
|
'desktop:submit-bug-report',
|
|
payload,
|
|
) as Promise<DesktopBugReportSubmitResponse>,
|
|
bindPublishAccount: (platformId: string) =>
|
|
ipcRenderer.invoke('desktop:bind-publish-account', platformId) as Promise<DesktopAccountInfo>,
|
|
refreshRuntimeAccounts: () =>
|
|
ipcRenderer.invoke('desktop:refresh-runtime-accounts') as Promise<null>,
|
|
probeRuntimeAccount: (accountId: string) =>
|
|
ipcRenderer.invoke('desktop:probe-runtime-account', accountId) as Promise<
|
|
DesktopRuntimeSnapshot['accounts'][number] | null
|
|
>,
|
|
openPublishAccountConsole: (account: {
|
|
id: string
|
|
platform: string
|
|
platformUid: string
|
|
displayName: string
|
|
}) => ipcRenderer.invoke('desktop:open-publish-account-console', account) as Promise<null>,
|
|
unbindPublishAccount: (accountId: string, syncVersion: number) =>
|
|
ipcRenderer.invoke('desktop:unbind-publish-account', accountId, syncVersion) as Promise<null>,
|
|
listPublishTasks: (params?: ListDesktopPublishTasksParams) =>
|
|
ipcRenderer.invoke(
|
|
'desktop:list-publish-tasks',
|
|
params,
|
|
) as Promise<DesktopPublishTaskListResponse>,
|
|
retryPublishTask: (taskId: string) =>
|
|
ipcRenderer.invoke('desktop:retry-publish-task', taskId) as Promise<CreatePublishJobResponse>,
|
|
cancelPublishTask: (taskId: string) =>
|
|
ipcRenderer.invoke('desktop:cancel-publish-task', taskId) as Promise<DesktopTaskInfo>,
|
|
openExternalUrl: (url: string) =>
|
|
ipcRenderer.invoke('desktop:open-external-url', url) as Promise<null>,
|
|
openSettingsWindow: () => ipcRenderer.invoke('desktop:open-settings-window') as Promise<null>,
|
|
getAppSettings: () =>
|
|
ipcRenderer.invoke('desktop:get-app-settings') as Promise<DesktopAppSettings>,
|
|
getLoginCredentials: () =>
|
|
ipcRenderer.invoke(
|
|
'desktop:get-login-credentials',
|
|
) as Promise<DesktopLoginCredentials | null>,
|
|
saveLoginCredentials: (credentials: DesktopLoginCredentials) =>
|
|
ipcRenderer.invoke(
|
|
'desktop:save-login-credentials',
|
|
credentials,
|
|
) as Promise<DesktopLoginCredentials>,
|
|
clearLoginCredentials: () =>
|
|
ipcRenderer.invoke('desktop:clear-login-credentials') as Promise<null>,
|
|
setAppSetting: (key: keyof DesktopAppSettings, value: boolean) =>
|
|
ipcRenderer.invoke('desktop:set-app-setting', key, value) as Promise<DesktopAppSettings>,
|
|
storageSnapshot: () =>
|
|
ipcRenderer.invoke('desktop:storage-snapshot') as Promise<DesktopStorageSnapshot>,
|
|
cleanStorage: () =>
|
|
ipcRenderer.invoke('desktop:clean-storage') as Promise<DesktopStorageCleanupResult>,
|
|
syncRuntimeSession: (session: DesktopRuntimeSessionSyncRequest | null) =>
|
|
ipcRenderer.invoke('desktop:runtime-session-sync', session) as Promise<null>,
|
|
rotateRuntimeClientToken: () =>
|
|
ipcRenderer.invoke(
|
|
'desktop:runtime-session-rotate-client-token',
|
|
) as Promise<DesktopClientRotateResponse>,
|
|
releaseRuntimeSession: (revoke?: boolean) =>
|
|
ipcRenderer.invoke('desktop:runtime-session-release', revoke) as Promise<null>,
|
|
setWindowMode: (mode: 'login' | 'main', request?: WindowModeRequest) =>
|
|
ipcRenderer.invoke('desktop:set-window-mode', mode, request) as Promise<null>,
|
|
onRuntimeInvalidated: (
|
|
listener: (event: {
|
|
reason: 'account-health' | 'publish-task-lease' | 'publish-task-progress'
|
|
at: number
|
|
accountId?: string
|
|
taskId?: string
|
|
}) => void,
|
|
) => {
|
|
const wrapped = (
|
|
_event: IpcRendererEvent,
|
|
payload: {
|
|
reason: 'account-health' | 'publish-task-lease' | 'publish-task-progress'
|
|
at: number
|
|
accountId?: string
|
|
taskId?: string
|
|
},
|
|
) => {
|
|
listener(payload)
|
|
}
|
|
ipcRenderer.on('desktop:runtime-invalidated', wrapped)
|
|
return () => {
|
|
ipcRenderer.off('desktop:runtime-invalidated', wrapped)
|
|
}
|
|
},
|
|
onNetworkObserved: (listener: (event: DesktopObservedRequest) => void) => {
|
|
const wrapped = (_event: IpcRendererEvent, payload: DesktopObservedRequest) => {
|
|
listener(payload)
|
|
}
|
|
ipcRenderer.on('desktop:network-observed', wrapped)
|
|
return () => {
|
|
ipcRenderer.off('desktop:network-observed', wrapped)
|
|
}
|
|
},
|
|
onClientUpdateProgress: (listener: (event: DesktopClientUpdateProgressEvent) => void) => {
|
|
const wrapped = (_event: IpcRendererEvent, payload: DesktopClientUpdateProgressEvent) => {
|
|
listener(payload)
|
|
}
|
|
ipcRenderer.on('desktop:client-update-progress', wrapped)
|
|
return () => {
|
|
ipcRenderer.off('desktop:client-update-progress', wrapped)
|
|
}
|
|
},
|
|
},
|
|
workbenchNavigation: {
|
|
getState: () =>
|
|
ipcRenderer.invoke(
|
|
'desktop:workbench-navigation:get-state',
|
|
) as Promise<WorkbenchNavigationState>,
|
|
goBack: () => ipcRenderer.invoke('desktop:workbench-navigation:go-back') as Promise<null>,
|
|
goForward: () => ipcRenderer.invoke('desktop:workbench-navigation:go-forward') as Promise<null>,
|
|
reload: () => ipcRenderer.invoke('desktop:workbench-navigation:reload') as Promise<null>,
|
|
onStateChanged: (listener: (state: WorkbenchNavigationState) => void) => {
|
|
const wrapped = (_event: IpcRendererEvent, payload: WorkbenchNavigationState) => {
|
|
listener(payload)
|
|
}
|
|
ipcRenderer.on('desktop:workbench-navigation-state', wrapped)
|
|
return () => {
|
|
ipcRenderer.off('desktop:workbench-navigation-state', wrapped)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
contextBridge.exposeInMainWorld('desktopBridge', desktopBridge)
|
|
|
|
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
if (!value || typeof value !== 'object') {
|
|
return false
|
|
}
|
|
|
|
return Object.values(value).every((item) => typeof item === 'string')
|
|
}
|