Files
WechatExplorer/src/main/api-token-store.ts
T
Wxw-Gu cf3f115124 feat: 完成 TraceMemo v2.2.0 品牌身份与数据迁移升级
- 将 WechatExplorer 产品身份升级为 TraceMemo
- 更新 appId、Runtime Identity、Reader Skill 和 API 环境变量
- 增加旧用户数据、Knowledge、Token 与 AI Provider 安全迁移
- 保留 Windows WeFlow 和旧版配置兼容
- 完善首次启动迁移测试及 v2.2.0 发布文档
- 移除 macOS Intel x64 构建与发布支持
2026-08-11 17:52:57 +08:00

149 lines
4.5 KiB
TypeScript

import crypto from 'crypto'
import { app, safeStorage } from 'electron'
import fs from 'fs-extra'
import path from 'path'
import type {
ApiTokenActionResult,
ApiTokenRevealResult,
ApiTokenStatus
} from '../shared/local-api-auth'
const MASKED_TOKEN = '••••••••••••••••'
const TOKEN_BYTES = 32
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
interface TokenReadResult {
success: boolean
token?: string
error?: string
}
export class ApiTokenStore {
private cachedToken: string | null = null
private automaticGenerationBlockedReason: string | null = null
constructor(private readonly filePathOverride?: string) {}
private get filePath(): string {
return this.filePathOverride || path.join(app.getPath('userData'), 'local-api-token.bin')
}
getStatus(): ApiTokenStatus {
const available = safeStorage.isEncryptionAvailable()
if (!available) {
return {
available: false,
hasToken: false,
maskedToken: MASKED_TOKEN,
error: '系统安全存储不可用,本地 API 已安全停用。请检查系统钥匙串或凭据服务后重试。'
}
}
const result = this.read()
return {
available: true,
hasToken: result.success && Boolean(result.token),
maskedToken: MASKED_TOKEN,
...(result.success
? result.token || !this.automaticGenerationBlockedReason
? {}
: { error: this.automaticGenerationBlockedReason }
: { error: result.error })
}
}
setAutomaticGenerationBlocked(reason?: string): void {
this.automaticGenerationBlockedReason = reason?.trim() || null
}
ensureToken(): ApiTokenActionResult {
if (!safeStorage.isEncryptionAvailable()) {
return {
success: false,
available: false,
hasToken: false,
maskedToken: MASKED_TOKEN,
error: '系统安全存储不可用,本地 API 已安全停用。请检查系统钥匙串或凭据服务后重试。'
}
}
const current = this.read()
if (!current.success) return this.actionError(current.error)
if (current.token) return this.actionSuccess()
if (this.automaticGenerationBlockedReason) {
return this.actionError(this.automaticGenerationBlockedReason)
}
return this.persist(this.generateToken())
}
revealToken(): ApiTokenRevealResult {
const ensured = this.ensureToken()
if (!ensured.success) return ensured
return { ...ensured, token: this.cachedToken || undefined }
}
rotateToken(): ApiTokenActionResult {
if (!safeStorage.isEncryptionAvailable()) return this.ensureToken()
const result = this.persist(this.generateToken())
if (result.success) this.automaticGenerationBlockedReason = null
return result
}
getTokenForAuthentication(): string | null {
if (this.cachedToken) return this.cachedToken
const result = this.read()
return result.success ? result.token || null : null
}
private generateToken(): string {
return crypto.randomBytes(TOKEN_BYTES).toString('base64url')
}
private read(): TokenReadResult {
if (this.cachedToken) return { success: true, token: this.cachedToken }
if (!safeStorage.isEncryptionAvailable()) {
return { success: false, error: '系统安全存储不可用' }
}
if (!fs.existsSync(this.filePath)) return { success: true }
try {
const token = safeStorage.decryptString(fs.readFileSync(this.filePath))
if (!TOKEN_PATTERN.test(token)) throw new Error('invalid token data')
this.cachedToken = token
return { success: true, token }
} catch {
return { success: false, error: '已保存的 API Token 无法从系统安全存储读取' }
}
}
private persist(token: string): ApiTokenActionResult {
try {
fs.ensureDirSync(path.dirname(this.filePath))
fs.writeFileSync(this.filePath, safeStorage.encryptString(token), { mode: 0o600 })
fs.chmodSync(this.filePath, 0o600)
this.cachedToken = token
return this.actionSuccess()
} catch {
return this.actionError('API Token 无法保存到系统安全存储')
}
}
private actionSuccess(): ApiTokenActionResult {
return {
success: true,
available: true,
hasToken: true,
maskedToken: MASKED_TOKEN
}
}
private actionError(error?: string): ApiTokenActionResult {
return {
success: false,
available: safeStorage.isEncryptionAvailable(),
hasToken: false,
maskedToken: MASKED_TOKEN,
error: error || 'API Token 安全存储不可用'
}
}
}
export const apiTokenStore = new ApiTokenStore()