// 远程操作授权闸门: 决定一个对端能不能开 terminal/forward/serial // 规则: // 1. settings.remoteEnabled === false -> 全部拒绝 // 2. settings.remoteAllowPeers[peerId][kind] === true -> 自动放行 // 3. 否则弹窗 (主进程打开一个 BrowserWindow 询问); 用户可勾选 "记住此对端" // 4. 同一对端 + 同一 kind 24h 内只弹一次 (除非显式 reset) import { BrowserWindow } from 'electron' import { getSettings } from '../settings' export type RemoteKind = 'terminal' | 'forward' | 'serial' | 'relation' /** 需要走"远程操作白名单"的种类 (relation 走关系层自己的授权, 不在这张表里) */ export type RemoteGateKind = Exclude interface Pending { resolve: (reply: ApprovalReply) => void timer: NodeJS.Timeout } /** * 用户对授权请求的回复。 * terminal/forward/serial 只用到 ok + remember; * relation (建立同步关系) 还会带上用户勾选的授权范围和他自己的同步目录。 */ export interface ApprovalReply { ok: boolean remember?: boolean /** relation: 用户开放给对方的能力 */ grants?: string[] /** relation: 用户这边的同步目录 */ syncLocalDir?: string /** relation: 允许对方用我网盘里的哪个子目录 */ syncRemotePath?: string } const pending = new Map() const approvedAt = new Map() const APPROVE_TTL_MS = 24 * 3600_000 function cacheKey(peerId: string, kind: RemoteGateKind, nonce?: string) { // nonce 用于 attach 这种需要每次重新确认的场景 return nonce ? `${peerId}::${kind}::${nonce}` : `${peerId}::${kind}` } export function isAutoAllowed(peerId: string, kind: RemoteGateKind): boolean { const s = getSettings() if (!s.remoteEnabled) return false const allow = s.remoteAllowPeers?.[peerId]?.[kind] return !!allow } export function isRemembered(peerId: string, kind: RemoteGateKind): boolean { const k = cacheKey(peerId, kind) const ts = approvedAt.get(k) if (!ts) return false if (Date.now() - ts > APPROVE_TTL_MS) { approvedAt.delete(k) return false } return true } export function markApproved(peerId: string, kind: RemoteGateKind) { approvedAt.set(cacheKey(peerId, kind), Date.now()) } export function resetApproval(peerId: string, kind?: RemoteGateKind) { for (const k of approvedAt.keys()) { if (k.startsWith(`${peerId}::`)) { if (!kind || k.startsWith(`${peerId}::${kind}`)) approvedAt.delete(k) } } } // 同步决策: 自动允许 / 缓存允许 -> 直接 true; 否则 false (UI 需要主动 ask) export function shouldAsk(peerId: string, kind: RemoteGateKind): boolean { if (isAutoAllowed(peerId, kind)) return false if (isRemembered(peerId, kind)) { markApproved(peerId, kind) return false } return true } const ASK_TIMEOUT_MS = 30_000 // 让 UI 弹一个授权对话框; resolve(true) 同意, resolve(false) 拒绝/超时 // 这里用 BrowserWindow 直接构造, 渲染层可以通过 IPC 拿到队列并显示 export interface ApprovalRequest { requestId: string peerId: string peerName: string kind: RemoteKind detail?: string ts: number /** relation: 对方想要的能力 */ grants?: string[] /** relation: 对方的同步目录 */ syncLocalDir?: string /** relation: 对方想用我网盘里的哪个子目录 */ syncRemotePath?: string /** relation: 关系 id */ relationId?: string } const waitingQueue: ApprovalRequest[] = [] const waitersByReq = new Map() export function enqueueApproval(req: ApprovalRequest): Promise { return new Promise((resolve) => { const timer = setTimeout(() => { pending.delete(req.requestId) waitersByReq.delete(req.requestId) // 从队列移除 const i = waitingQueue.findIndex(x => x.requestId === req.requestId) if (i >= 0) waitingQueue.splice(i, 1) resolve({ ok: false }) }, ASK_TIMEOUT_MS) pending.set(req.requestId, { resolve, timer }) waitersByReq.set(req.requestId, pending.get(req.requestId)!) waitingQueue.push(req) }) } export function listPendingApprovals(): ApprovalRequest[] { return [...waitingQueue] } export function replyApproval( requestId: string, ok: boolean, extra?: { remember?: boolean; grants?: string[]; syncLocalDir?: string; syncRemotePath?: string }, ): boolean { const p = pending.get(requestId) if (!p) return false clearTimeout(p.timer) pending.delete(requestId) waitersByReq.delete(requestId) const i = waitingQueue.findIndex(x => x.requestId === requestId) if (i >= 0) waitingQueue.splice(i, 1) p.resolve({ ok, ...(extra || {}) }) return true } export function focusMain() { const w = BrowserWindow.getAllWindows()[0] if (!w) return if (w.isMinimized()) w.restore() w.show() w.focus() }