ManimCat / src /services /job-access-store.ts
Bin29's picture
Sync from main: e764154 feat(plot-skill): add math-exam-diagram SKILL.md for exam-style math figures
abcf568
import crypto from 'crypto'
import { redisClient, REDIS_KEYS, generateRedisKey } from '../config/redis'
import { ForbiddenError } from '../utils/errors'
const JOB_ACCESS_KEY_PREFIX = `${REDIS_KEYS.JOB_ACCESS}`
const DEFAULT_RETENTION_HOURS = 24
interface StoredJobAccessRecord {
apiKeyHash: string
clientId?: string
createdAt: number
}
interface StoreJobAccessInput {
jobId: string
apiKey: string
clientId?: string
}
interface AssertJobAccessInput {
jobId: string
apiKey: string
clientId?: string
}
function getRetentionSeconds(): number {
const raw = Number(process.env.JOB_RESULT_RETENTION_HOURS)
const hours = Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_RETENTION_HOURS
return hours * 60 * 60
}
function hashApiKey(apiKey: string): string {
return crypto.createHash('sha256').update(apiKey).digest('hex')
}
export async function storeJobAccess(input: StoreJobAccessInput): Promise<void> {
const key = generateRedisKey(JOB_ACCESS_KEY_PREFIX, input.jobId)
const payload: StoredJobAccessRecord = {
apiKeyHash: hashApiKey(input.apiKey),
clientId: input.clientId?.trim() || undefined,
createdAt: Date.now(),
}
await redisClient.set(key, JSON.stringify(payload))
await redisClient.expire(key, getRetentionSeconds())
}
export async function assertJobAccess(input: AssertJobAccessInput): Promise<void> {
const key = generateRedisKey(JOB_ACCESS_KEY_PREFIX, input.jobId)
const raw = await redisClient.get(key)
if (!raw) {
throw new ForbiddenError('当前任务不属于这个客户端或已不可访问')
}
let record: StoredJobAccessRecord
try {
record = JSON.parse(raw) as StoredJobAccessRecord
} catch {
throw new ForbiddenError('当前任务的访问元数据无效')
}
if (record.apiKeyHash !== hashApiKey(input.apiKey)) {
throw new ForbiddenError('当前任务不属于这个 API key')
}
const expectedClientId = record.clientId?.trim()
if (expectedClientId && expectedClientId !== (input.clientId?.trim() || '')) {
throw new ForbiddenError('当前任务不属于这个浏览器客户端')
}
}
export async function getJobAccessCreatedAt(jobId: string): Promise<number | null> {
const key = generateRedisKey(JOB_ACCESS_KEY_PREFIX, jobId)
const raw = await redisClient.get(key)
if (!raw) {
return null
}
try {
const record = JSON.parse(raw) as StoredJobAccessRecord
return typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
? record.createdAt
: null
} catch {
return null
}
}