()
| 52 | } |
| 53 | |
| 54 | async function main(): Promise<void> { |
| 55 | const dryRun = process.argv.includes("--dry-run"); |
| 56 | |
| 57 | if (!isC2PASigningEnabled()) { |
| 58 | console.error( |
| 59 | "C2PA_SIGNING_ENABLED + C2PA_CERT_PEM_B64 + C2PA_PRIVATE_KEY_PEM_B64 must all be set.", |
| 60 | ); |
| 61 | process.exit(1); |
| 62 | } |
| 63 | |
| 64 | const files: string[] = []; |
| 65 | for (const root of ROOTS) await walk(root, files); |
| 66 | files.sort(); |
| 67 | |
| 68 | console.log(`Found ${files.length} PNGs under ${ROOTS.join(", ")}.`); |
| 69 | if (dryRun) console.log("(dry run — no files will be modified)"); |
| 70 | |
| 71 | const stats: BackfillStats = { signed: 0, skippedAlreadySigned: 0, failed: 0 }; |
| 72 | |
| 73 | for (const file of files) { |
| 74 | const buffer = await fs.promises.readFile(file); |
| 75 | |
| 76 | // Detect actual MIME type from magic bytes — some committed .png files |
| 77 | // are really JPEGs with the wrong extension. c2pa-rs rejects a MIME/buffer |
| 78 | // mismatch, so we feed it the truth. |
| 79 | const detected = await fileTypeFromBuffer(buffer); |
| 80 | const mimeType = detected?.mime ?? "image/png"; |
| 81 | if (!SIGNABLE_MIME_TYPES.has(mimeType)) { |
| 82 | console.warn(` ⚠ ${file}: unsupported type ${mimeType}, skipping`); |
| 83 | continue; |
| 84 | } |
| 85 | |
| 86 | if (await hasEmbeddedManifest(buffer, mimeType)) { |
| 87 | stats.skippedAlreadySigned++; |
| 88 | continue; |
| 89 | } |
| 90 | try { |
| 91 | const signed = signC2PA(buffer, { |
| 92 | claimGenerator: "AAO Docs Storyboard Generator", |
| 93 | title: path.basename(file), |
| 94 | softwareAgent: { name: "gemini-3.1-flash-image-preview", version: "preview" }, |
| 95 | mimeType, |
| 96 | attributes: { |
| 97 | relative_path: path.relative(process.cwd(), file), |
| 98 | // Backfilled rows hash the file bytes since the original prompt is |
| 99 | // no longer available. The hash still serves as tamper evidence. |
| 100 | source_sha256: createHash("sha256").update(buffer).digest("hex"), |
| 101 | backfilled: true, |
| 102 | }, |
| 103 | }); |
| 104 | if (!dryRun) await fs.promises.writeFile(file, signed.signedBuffer); |
| 105 | stats.signed++; |
| 106 | if (stats.signed % 10 === 0 || stats.signed === files.length) { |
| 107 | console.log(` [${stats.signed}/${files.length}] ${file}`); |
| 108 | } |
| 109 | } catch (err) { |
| 110 | console.error(` ✗ ${file}: ${(err as Error).message}`); |
| 111 | stats.failed++; |
no test coverage detected