mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
feat: 为本地 HTTP API 增加 Token 鉴权与安全加固
- 使用 Electron safeStorage 加密存储并自动初始化 API Token - 为 health 以外的接口增加 Bearer Token 鉴权 - 限制 CORS 仅允许可信本地 Origin - 增加鉴权、Token rotation、safeStorage 和手动验收测试
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
API_TOKEN_ROTATION_CONFIRMATION,
|
||||
confirmApiTokenRotation
|
||||
} from '../../src/renderer/src/features/api-center/utils/confirmApiTokenRotation'
|
||||
|
||||
describe('API token rotation confirmation', () => {
|
||||
it('requires explicit confirmation and explains immediate invalidation', () => {
|
||||
const reject = vi.fn(() => false)
|
||||
expect(confirmApiTokenRotation(reject)).toBe(false)
|
||||
expect(reject).toHaveBeenCalledWith(API_TOKEN_ROTATION_CONFIRMATION)
|
||||
expect(API_TOKEN_ROTATION_CONFIRMATION).toContain('旧 Token 将立即失效')
|
||||
expect(API_TOKEN_ROTATION_CONFIRMATION).toContain('Agent / Reader Skill 需要更新 Token')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import fs from 'fs-extra'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wxe-api-token-store-'))
|
||||
const storage = vi.hoisted(() => ({ available: true }))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => root },
|
||||
safeStorage: {
|
||||
isEncryptionAvailable: () => storage.available,
|
||||
encryptString: (value: string) => Buffer.from(value, 'utf8').reverse(),
|
||||
decryptString: (value: Buffer) => Buffer.from(value).reverse().toString('utf8')
|
||||
}
|
||||
}))
|
||||
|
||||
import { ApiTokenStore } from '../../src/main/api-token-store'
|
||||
|
||||
describe('ApiTokenStore', () => {
|
||||
const filePath = path.join(root, 'fixture-token.bin')
|
||||
|
||||
beforeEach(() => {
|
||||
storage.available = true
|
||||
fs.removeSync(filePath)
|
||||
})
|
||||
|
||||
afterAll(() => fs.removeSync(root))
|
||||
|
||||
it('generates a 256-bit base64url token once and persists it', () => {
|
||||
const firstStore = new ApiTokenStore(filePath)
|
||||
expect(firstStore.ensureToken()).toMatchObject({ success: true, hasToken: true })
|
||||
const first = firstStore.revealToken().token
|
||||
expect(first).toMatch(/^[A-Za-z0-9_-]{43}$/)
|
||||
expect(fs.readFileSync(filePath, 'utf8')).not.toContain(String(first))
|
||||
expect(fs.statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
|
||||
const secondStore = new ApiTokenStore(filePath)
|
||||
expect(secondStore.ensureToken()).toMatchObject({ success: true, hasToken: true })
|
||||
expect(secondStore.revealToken().token).toBe(first)
|
||||
})
|
||||
|
||||
it('rotates the token while keeping status responses masked', () => {
|
||||
const store = new ApiTokenStore(filePath)
|
||||
store.ensureToken()
|
||||
const oldToken = store.revealToken().token
|
||||
const result = store.rotateToken()
|
||||
const newToken = store.revealToken().token
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
available: true,
|
||||
hasToken: true,
|
||||
maskedToken: '••••••••••••••••'
|
||||
})
|
||||
expect(newToken).not.toBe(oldToken)
|
||||
})
|
||||
|
||||
it('fails closed without writing plaintext when safeStorage is unavailable', () => {
|
||||
storage.available = false
|
||||
const store = new ApiTokenStore(filePath)
|
||||
expect(store.ensureToken()).toMatchObject({ success: false, available: false, hasToken: false })
|
||||
expect(fs.existsSync(filePath)).toBe(false)
|
||||
expect(store.revealToken().token).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user