98 lines
6.7 KiB
JavaScript
98 lines
6.7 KiB
JavaScript
// Opt-in, reviewed recovery of authentic cached previews. No model calls, deletes,
|
|
// download-source changes or URL-prefix substitution. The caller durably saves
|
|
// the returned plan before apply, and provides exact storage/DB adapters.
|
|
import { createHash } from 'node:crypto'
|
|
|
|
export const OPERATION_ID = 'material-preview-cache-20261023'
|
|
export const digest = value => createHash('sha256').update(value).digest('hex')
|
|
export const json = value => JSON.stringify(value, (_, v) => typeof v === 'bigint' ? v.toString() : v)
|
|
const check = (condition, message) => { if (!condition) throw new Error(message) }
|
|
const idPattern = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/
|
|
|
|
export function validateRecovery(manifest) {
|
|
check(manifest?.version === 1 && manifest.reviewed === true && manifest.operationId === OPERATION_ID, 'reviewed recovery required')
|
|
check(manifest.previewQuality === 'temporary-160px-derived' && manifest.materials?.length === 11, 'exact eleven temporary previews required')
|
|
check(idPattern.test(manifest.actorId), 'actor required')
|
|
const ids = new Set()
|
|
for (const row of manifest.materials) {
|
|
check(idPattern.test(row.materialId) && !ids.has(row.materialId), 'unique material identity required')
|
|
ids.add(row.materialId)
|
|
const old = new URL(row.oldPreviewUrl)
|
|
check(old.protocol === 'https:' && !old.username && !old.password && !old.search && !old.hash, 'exact original URL required')
|
|
check(/^[a-f0-9]{64}$/.test(row.sourceSha256) && /^[a-f0-9]{64}$/.test(row.cacheSha256), 'source and cache hashes required')
|
|
const bytes = Buffer.from(row.bodyBase64, 'base64')
|
|
check(bytes.length === row.bytes && bytes.length >= 100 && bytes.length <= 16384 && digest(bytes) === row.cacheSha256, 'cache bytes mismatch')
|
|
check(bytes.subarray(0, 4).toString() === 'RIFF' && bytes.subarray(8, 12).toString() === 'WEBP', 'WebP required')
|
|
check(row.width === 160 && row.height === 160 && row.provenance === '20261019-sharp-rotate-inside160-webp80', 'verified derivation required')
|
|
check(row.destinationKey === `由壹点AI/public-display/${OPERATION_ID}/${row.materialId}-${row.cacheSha256}.webp`, 'scoped immutable destination required')
|
|
check(row.newPreviewUrl === `https://s3.jiansun.vip/gongxiangpan/${row.destinationKey}`, 'canonical destination required')
|
|
}
|
|
return manifest
|
|
}
|
|
|
|
function verifyActor(actor) {
|
|
check(actor && actor.isActive === true && actor.isSuperAdmin === true && actor.status === 'active' && actor.archivedAt === null
|
|
&& actor.employmentStatus === 'active', 'active authorized recovery actor required')
|
|
}
|
|
|
|
function verifyRecord(record, row) {
|
|
check(record?.id === row.materialId && record.productModel === row.name && record.materialType === '材质'
|
|
&& record.deletedAt === null && record.previewUrl === row.oldPreviewUrl, 'source record changed')
|
|
}
|
|
|
|
export async function planRecovery(manifest, db) {
|
|
validateRecovery(manifest)
|
|
verifyActor(await db.actor(manifest.actorId))
|
|
const records = await db.records(manifest.materials.map(r => r.materialId))
|
|
check(records.length === 11, 'eleven source records required')
|
|
const ordered = manifest.materials.map(row => {
|
|
const record = records.find(r => r.id === row.materialId)
|
|
verifyRecord(record, row)
|
|
return record
|
|
})
|
|
return { version: 1, operationId: OPERATION_ID, manifestSha256: digest(json(manifest)), capturedAt: new Date().toISOString(), records: ordered }
|
|
}
|
|
|
|
export async function applyRecovery({ manifest, plan, persistedPlanSha256, db, objects }) {
|
|
validateRecovery(manifest)
|
|
check(plan.operationId === OPERATION_ID && plan.manifestSha256 === digest(json(manifest))
|
|
&& persistedPlanSha256 === digest(json(plan)) && plan.records?.length === 11, 'durably persisted matching plan required')
|
|
const originalPlan = await planRecovery(manifest, db)
|
|
check(json(originalPlan.records) === json(plan.records), 'source snapshot changed before upload')
|
|
check(await db.operationCount(OPERATION_ID) === 0, 'operation already recorded; inspect before continuing')
|
|
const uploaded = []
|
|
// Every object is publicly readable and hash-verified before the DB transaction.
|
|
// A conflict leaves harmless, named unreferenced objects, never deletes anything.
|
|
for (const row of manifest.materials) {
|
|
const body = Buffer.from(row.bodyBase64, 'base64')
|
|
const existing = await objects.get(row.destinationKey)
|
|
if (existing === null) {
|
|
await objects.putIfAbsent(row.destinationKey, body, { operationId: OPERATION_ID, materialId: row.materialId, sha256: row.cacheSha256, quality: manifest.previewQuality })
|
|
uploaded.push(row.destinationKey)
|
|
} else check(digest(existing) === row.cacheSha256, 'existing destination differs; refusing overwrite')
|
|
check(digest(await objects.get(row.destinationKey)) === row.cacheSha256, 'internal object readback mismatch')
|
|
check(digest(await objects.publicGet(row.newPreviewUrl)) === row.cacheSha256, 'public object readback mismatch')
|
|
}
|
|
await db.transaction(async tx => {
|
|
verifyActor(await tx.actor(manifest.actorId))
|
|
check(await tx.operationCount(OPERATION_ID) === 0, 'operation concurrently recorded')
|
|
const current = await tx.records(manifest.materials.map(r => r.materialId))
|
|
const ordered = manifest.materials.map(row => current.find(r => r.id === row.materialId))
|
|
check(json(ordered) === json(plan.records), 'source snapshot changed; transaction aborted')
|
|
for (const row of manifest.materials) {
|
|
const before = plan.records.find(r => r.id === row.materialId)
|
|
check(await tx.compareAndSet(row, before.updatedAt) === 1, 'concurrent record change; transaction aborted')
|
|
await tx.log({ materialId: row.materialId, userId: manifest.actorId, action: 'PREVIEW_CACHE_RECOVERY', changes: json({ operationId: OPERATION_ID, previewQuality: manifest.previewQuality, oldPreviewUrl: row.oldPreviewUrl, newPreviewUrl: row.newPreviewUrl, originalSourceSha256: row.sourceSha256, cacheSha256: row.cacheSha256, width: 160, height: 160, downloadsAndAssetsUnchanged: true, snapshotSha256: persistedPlanSha256 }) })
|
|
}
|
|
})
|
|
const after = await db.records(manifest.materials.map(r => r.materialId))
|
|
for (const before of plan.records) {
|
|
const actual = after.find(r => r.id === before.id)
|
|
const row = manifest.materials.find(r => r.materialId === before.id)
|
|
check(actual?.previewUrl === row.newPreviewUrl, 'post-commit preview mismatch; inspect operation logs')
|
|
const immutable = ({ previewUrl, updatedAt, ...rest }) => rest
|
|
check(json(immutable(actual)) === json(immutable(before)), 'post-commit unrelated data mismatch; inspect operation logs')
|
|
}
|
|
return { operationId: OPERATION_ID, updated: 11, uploaded, after, temporaryPreviewSize: 160, originalSourceRestored: false, downloadsAndAssetsUnchanged: true, modelCalls: 0 }
|
|
}
|