fix: restore security hardening compatibility and validation

Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
This commit is contained in:
elky
2026-09-07 21:14:27 +08:00
parent a5c3699ae9
commit a90d564931
191 changed files with 6785 additions and 1643 deletions
@@ -0,0 +1,103 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, type App } from 'vue'
import type { ActionResultResponse } from '@/api/providerOps'
import { useProviderBalance } from '../useProviderBalance'
const api = vi.hoisted(() => ({
batchQueryBalance: vi.fn<() => Promise<Record<string, ActionResultResponse>>>(),
getArchitectures: vi.fn().mockResolvedValue([]),
}))
vi.mock('@/api/providerOps', () => api)
let app: App | undefined
let root: HTMLDivElement
function mountBalance() {
let balance!: ReturnType<typeof useProviderBalance>
root = document.createElement('div')
app = createApp({
setup() {
balance = useProviderBalance()
return () => null
},
})
app.mount(root)
return balance
}
function result(status: ActionResultResponse['status'], available: number): ActionResultResponse {
return {
status,
action_type: 'query_balance',
data: { total_available: available, currency: 'USD', extra: {} },
message: null,
executed_at: '2026-09-07T00:00:00Z',
response_time_ms: 0,
cache_ttl_seconds: 0,
}
}
beforeEach(() => {
vi.useFakeTimers()
api.batchQueryBalance.mockReset()
})
afterEach(() => {
app?.unmount()
app = undefined
root?.remove()
vi.useRealTimers()
})
describe('provider balance refresh', () => {
it('does not overwrite a newer refresh with an older pending retry', async () => {
const balance = mountBalance()
const providers = [{ id: 'provider-1', ops_configured: true }]
let resolveRetry!: (value: Record<string, ActionResultResponse>) => void
api.batchQueryBalance
.mockResolvedValueOnce({ 'provider-1': result('pending', 0) })
.mockImplementationOnce(() => new Promise(resolve => { resolveRetry = resolve }))
.mockResolvedValueOnce({ 'provider-1': result('success', 20) })
await balance.loadBalances(providers)
await vi.advanceTimersByTimeAsync(12_000)
await balance.loadBalances(providers)
resolveRetry({ 'provider-1': result('success', 10) })
await Promise.resolve()
expect(balance.getProviderBalance('provider-1')).toEqual({ available: 20, currency: 'USD' })
})
it('ignores a pending response after unmount', async () => {
const balance = mountBalance()
let resolveLoad!: (value: Record<string, ActionResultResponse>) => void
api.batchQueryBalance.mockImplementationOnce(() => new Promise(resolve => { resolveLoad = resolve }))
const loading = balance.loadBalances([{ id: 'provider-1', ops_configured: true }])
app?.unmount()
app = undefined
resolveLoad({ 'provider-1': result('pending', 10) })
await loading
expect(balance.balanceCache.value).toEqual({})
expect(vi.getTimerCount()).toBe(0)
})
it('preserves zero balances and false check-in results', async () => {
const balance = mountBalance()
api.batchQueryBalance.mockResolvedValueOnce({
'provider-1': {
...result('success', 0),
data: {
total_available: 0,
currency: 'USD',
extra: { balance: 0, points: 0, checkin_success: false, checkin_message: 'try again' },
},
},
})
await balance.loadBalances([{ id: 'provider-1', ops_configured: true }])
expect(balance.getProviderBalanceBreakdown('provider-1')).toEqual({ balance: 0, points: 0, currency: 'USD' })
expect(balance.getProviderCheckin('provider-1')).toEqual({ success: false, message: 'try again' })
})
})
@@ -48,7 +48,7 @@ export function useProviderBalance() {
const schemas: Record<string, CredentialsSchema> = {}
for (const arch of archs) {
if (arch.credentials_schema) {
schemas[arch.architecture_id] = arch.credentials_schema as CredentialsSchema
schemas[arch.architecture_id] = arch.credentials_schema
}
}
architectureSchemas.value = schemas
@@ -59,7 +59,7 @@ export function useProviderBalance() {
}
// 异步加载余额数据(使用批量接口)
async function loadBalances(providers: ProviderWithEndpointsSummary[], fullReload = true) {
async function loadBalances(providers: Pick<ProviderWithEndpointsSummary, 'id' | 'ops_configured'>[], fullReload = true) {
if (fullReload) {
balanceCache.value = {}
}
@@ -108,6 +108,7 @@ export function useProviderBalance() {
async function retryPendingBalances(providerIds: string[], loadVersion: number, retryCount: number) {
try {
const results = await batchQueryBalance(providerIds)
if (loadVersion !== balanceLoadVersion) return
const stillPending: string[] = []
for (const [providerId, result] of Object.entries(results)) {
@@ -173,14 +174,16 @@ export function useProviderBalance() {
return null
}
const data = result.data as Record<string, unknown>
const extra = data.extra
if (!extra || extra.balance === undefined || extra.points === undefined) {
const extra = typeof data.extra === 'object' && data.extra !== null
? data.extra as Record<string, unknown>
: null
if (!extra || typeof extra.balance !== 'number' || typeof extra.points !== 'number') {
return null
}
return {
balance: extra.balance,
points: extra.points,
currency: data.currency || 'USD',
currency: typeof data.currency === 'string' && data.currency ? data.currency : 'USD',
}
}
@@ -224,13 +227,15 @@ export function useProviderBalance() {
return null
}
const data = result.data as Record<string, unknown>
const extra = data.extra
if (!extra || extra.checkin_success === undefined) {
const extra = typeof data.extra === 'object' && data.extra !== null
? data.extra as Record<string, unknown>
: null
if (!extra || (extra.checkin_success !== null && typeof extra.checkin_success !== 'boolean')) {
return null
}
return {
success: extra.checkin_success,
message: extra.checkin_message || '',
message: typeof extra.checkin_message === 'string' ? extra.checkin_message : '',
}
}
@@ -244,13 +249,15 @@ export function useProviderBalance() {
return null
}
const data = result.data as Record<string, unknown>
const extra = data.extra
const extra = typeof data.extra === 'object' && data.extra !== null
? data.extra as Record<string, unknown>
: null
if (!extra || !extra.cookie_expired) {
return null
}
return {
expired: true,
message: extra.cookie_expired_message || 'Cookie 已失效',
message: typeof extra.cookie_expired_message === 'string' ? extra.cookie_expired_message : 'Cookie 已失效',
}
}
@@ -296,7 +303,9 @@ export function useProviderBalance() {
}
const data = result.data as Record<string, unknown>
const extra = data.extra
const extra = typeof data.extra === 'object' && data.extra !== null
? data.extra as Record<string, unknown>
: null
if (!extra) return []
// 从 schema 缓存中获取格式化配置
@@ -319,6 +328,7 @@ export function useProviderBalance() {
// 组件卸载时清理
function cleanup() {
balanceLoadVersion++
stopTick()
pendingTimers.forEach(clearTimeout)
pendingTimers.clear()
@@ -36,7 +36,10 @@ function providerModelsFetchResult(response: ProviderModelsQueryResponse): Fetch
if (response.success && response.data?.models) {
const partialWarning = response.data.warning ?? response.data.error
return {
models: response.data.models,
models: response.data.models.map((model) => ({
...model,
api_formats: model.api_formats ?? (model.api_format ? [model.api_format] : []),
})),
warning: partialWarning ? parseUpstreamModelError(partialWarning) : undefined,
fromCache: response.data.from_cache,
}