74 lines
3.7 KiB
JavaScript
74 lines
3.7 KiB
JavaScript
// Recover only the three browser-extension images retained by REA Evidence.
|
|
// This script reads the archived Evidence JSON and never accesses the WorkBuddy install.
|
|
import { createHash } from 'node:crypto'
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
|
|
const evidenceId = 'ev_9542a063514b1a980665e504a89126c7a17f00b42f0c6dff9cead085a929f5c7'
|
|
const evidencePath = path.resolve(`.tmp/rea-css-evidence/${evidenceId}.json`)
|
|
const outputDirectory = path.resolve('src/renderer/src/components/agents-assets')
|
|
const expected = [
|
|
{
|
|
selector: '#settings-browser-extension-chrome img',
|
|
mimeType: 'image/svg+xml',
|
|
sha256: 'bde4af5a5d1150d3ec3108c29a5d746a00892196f3eaf1db4c4dc99aac7ba266',
|
|
output: 'workbuddy-browser-chrome.svg'
|
|
},
|
|
{
|
|
selector: '#settings-browser-extension-edge img',
|
|
mimeType: 'image/svg+xml',
|
|
sha256: '968cf9d827bfeaf47e4e53da55ec144bbc9565e5cb7a65cdb8dd0284fb3a5d11',
|
|
output: 'workbuddy-browser-edge.svg'
|
|
},
|
|
{
|
|
selector: '#settings-browser-extension-qq-browser img',
|
|
mimeType: 'image/png',
|
|
sha256: '186c82828c04b5bfe300c201ecd75cefb7984d97ea01bd491e0cfbdeb5df15a1',
|
|
output: 'workbuddy-browser-qq.png'
|
|
}
|
|
]
|
|
|
|
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex')
|
|
const evidence = JSON.parse(await fs.readFile(evidencePath, 'utf8'))
|
|
if (evidence.evidence_id !== evidenceId) throw new Error(`REA identity mismatch: ${evidence.evidence_id}`)
|
|
if (evidence.completeness?.truncated_sections?.length) throw new Error('REA Evidence is truncated')
|
|
if (!/renderer[\\/]index\.html$/i.test(evidence.normalized_result?.target?.file_path ?? '')) {
|
|
throw new Error('REA Evidence target is not the authorized WorkBuddy renderer')
|
|
}
|
|
|
|
const media = evidence.normalized_result?.visuals?.media_assets
|
|
if (!media?.items || media.items.length !== expected.length) throw new Error('Unexpected REA media item count')
|
|
const decoded = []
|
|
for (const item of expected) {
|
|
const matches = media.items.filter((candidate) => candidate.selector === item.selector)
|
|
if (matches.length !== 1) throw new Error(`Expected one REA item for ${item.selector}`)
|
|
const asset = matches[0]
|
|
if (asset.status !== 'exported' || asset.visibility?.status !== 'observed') {
|
|
throw new Error(`REA did not export a visible image for ${item.selector}: ${asset.reason}`)
|
|
}
|
|
if (asset.artifact?.mime_type !== item.mimeType || asset.artifact.sha256 !== item.sha256) {
|
|
throw new Error(`REA artifact identity changed for ${item.selector}`)
|
|
}
|
|
const bytes = Buffer.from(asset.artifact.data_base64, 'base64')
|
|
if (bytes.length !== asset.artifact.bytes || digest(bytes) !== item.sha256) {
|
|
throw new Error(`REA artifact bytes changed for ${item.selector}`)
|
|
}
|
|
if (item.mimeType === 'image/svg+xml') {
|
|
const source = bytes.toString('utf8')
|
|
if (!/^<svg\s/.test(source) || !/\bviewBox=['"]/i.test(source)) throw new Error(`Invalid SVG source for ${item.output}`)
|
|
if (/<script\b|<foreignObject\b|\b(?:href|xlink:href)=['"](?:https?:|file:|data:)/i.test(source)) {
|
|
throw new Error(`Unsafe external or active SVG content for ${item.output}`)
|
|
}
|
|
} else if (item.mimeType === 'image/png') {
|
|
if (bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a') throw new Error(`Invalid PNG source for ${item.output}`)
|
|
if (bytes.readUInt32BE(16) !== 128 || bytes.readUInt32BE(20) !== 128) throw new Error('QQ browser icon is not 128x128')
|
|
}
|
|
decoded.push({ bytes, output: item.output, sha256: item.sha256 })
|
|
}
|
|
|
|
await fs.mkdir(outputDirectory, { recursive: true })
|
|
for (const asset of decoded) await fs.writeFile(path.join(outputDirectory, asset.output), asset.bytes)
|
|
console.log(JSON.stringify({ evidenceId, outputDirectory, assets: decoded.map(({ output, sha256, bytes }) => ({
|
|
output, sha256, bytes: bytes.length
|
|
})) }, null, 2))
|