{
  "name": "Real Image Manifest Validator v0.1.0",
  "nodes": [
    {
      "id": "wf10-manual",
      "name": "Manual test",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        0
      ]
    },
    {
      "id": "wf10-input",
      "name": "Supply reviewed image bytes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "// Read the README. Intentionally empty bytes: replace data with canonical base64 and review all metadata.\n// No embedded images, pinned data, credentials, or network fetch. The first unconfigured run must fail.\nreturn [{json:{\n  \"content_id\": \"demo-image-001\",\n  \"assets\": [\n    {\n      \"path\": \"assets/imagem-1.jpg\",\n      \"mime_type\": \"image/jpeg\",\n      \"bytes\": 77002,\n      \"sha256\": \"2f24b4b340a6fda421fc59bccf112737118f9b0f8f93427858322d1d4cbc36df\",\n      \"alt\": \"n8n canvas showing six steps from a fictional topic to a validated brief\",\n      \"caption\": \"AI Content Machine: first AI brief demonstration, 29 September 2026\",\n      \"permission_note\": \"Demonstration metadata only. Confirm permission for the exact image you supply before reuse.\"\n    }\n  ],\n  \"asset_files\": {\n    \"assets/imagem-1.jpg\": {\n      \"data\": \"\",\n      \"sha256\": \"2f24b4b340a6fda421fc59bccf112737118f9b0f8f93427858322d1d4cbc36df\"\n    }\n  }\n}}];"
      },
      "position": [
        280,
        0
      ]
    },
    {
      "id": "wf10-validator",
      "name": "Validate image manifest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "const crypto = require('crypto');\nfunction validateImageManifest(input, crypto, ByteBuffer) {\n  const fail = message => { throw new Error(message); };\n  const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);\n  const text = (value, label) => {\n    if (typeof value !== 'string' || !value.trim() || value.length > 2000) fail('Invalid ' + label);\n    return value.trim();\n  };\n  if (!object(input)) fail('Invalid manifest');\n  const contentId = text(input.content_id, 'content_id');\n  if (!Array.isArray(input.assets) || input.assets.length < 1 || input.assets.length > 2) fail('One or two real images are required');\n  if (!object(input.asset_files)) fail('Missing asset_files');\n  const limit = 2 * 1024 * 1024;\n  const maximumBase64Length = 4 * Math.ceil(limit / 3);\n  // Exact membership avoids JavaScript's $ anchor accepting a trailing newline.\n  const allowedPaths = new Set(['assets/imagem-1.jpg', 'assets/imagem-1.png', 'assets/imagem-2.jpg', 'assets/imagem-2.png']);\n  const seen = new Set();\n  const assets = Array.from(input.assets).map(image => {\n    if (!object(image) || typeof image.path !== 'string' || !allowedPaths.has(image.path)) fail('Invalid image path');\n    if (seen.has(image.path)) fail('Duplicate image path');\n    seen.add(image.path);\n    const jpegExpected = image.path.endsWith('.jpg');\n    if (image.mime_type !== (jpegExpected ? 'image/jpeg' : 'image/png')) fail('Image MIME/extension mismatch');\n    if (!Number.isSafeInteger(image.bytes) || image.bytes < 1 || image.bytes > limit) fail('Invalid declared image size');\n    const hashPattern = /^[a-f0-9]{64}$/;\n    if (typeof image.sha256 !== 'string' || !hashPattern.test(image.sha256)) fail('Invalid manifest SHA-256');\n    const alt = text(image.alt, 'image alt');\n    const caption = text(image.caption, 'image credit/caption');\n    const permissionNote = text(image.permission_note, 'image permission');\n    if (!Object.prototype.hasOwnProperty.call(input.asset_files, image.path)) fail('Missing image data');\n    const file = input.asset_files[image.path];\n    if (!object(file) || typeof file.data !== 'string' || !file.data.length) fail('Missing image data');\n    // Bound the encoded value before allocating a decoded buffer; no URL or network adapter.\n    if (file.data.length > maximumBase64Length) fail('Image exceeds 2 MiB');\n    if (file.data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(file.data)) fail('Invalid canonical base64');\n    const bytes = ByteBuffer.from(file.data, 'base64');\n    if (!bytes.length || bytes.length > limit || bytes.toString('base64') !== file.data) fail('Invalid image bytes');\n    if (bytes.length !== image.bytes) fail('Image size mismatch');\n    const jpeg = bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;\n    const png = bytes.length >= 8 && bytes.subarray(0, 8).toString('hex') === '89504e470d0a1a0a';\n    if (!(jpegExpected ? jpeg : png)) fail('Image signature mismatch');\n    if (typeof file.sha256 !== 'string' || !hashPattern.test(file.sha256)) fail('Invalid file SHA-256');\n    const actualHash = crypto.createHash('sha256').update(bytes).digest('hex');\n    if (actualHash !== image.sha256 || actualHash !== file.sha256) fail('Image integrity mismatch');\n    return { path: image.path, mime_type: image.mime_type, bytes: bytes.length, sha256: actualHash, alt, caption, permission_note: permissionNote };\n  });\n  if (Object.keys(input.asset_files).length !== seen.size || Object.keys(input.asset_files).some(path => !seen.has(path))) fail('Unexpected asset file');\n  // Deliberately omit supplied bytes, unrelated fields, and private metadata from the output.\n  return { content_id: contentId, status: 'assets_valid', image_count: assets.length, verified_paths: assets.map(image => image.path), assets,\n    byte_integrity_verified: true, image_decoding_verified: false, visual_review_required: true, rights_verified_by_code: false, publication_approved: false };\n}\nreturn $input.all().map((item, index) => ({ json: validateImageManifest(item.json, crypto, Buffer), pairedItem: { item: index } }));"
      },
      "position": [
        560,
        0
      ]
    }
  ],
  "connections": {
    "Manual test": {
      "main": [
        [
          {
            "node": "Supply reviewed image bytes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supply reviewed image bytes": {
      "main": [
        [
          {
            "node": "Validate image manifest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1",
    "timezone": "UTC",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "none",
    "saveManualExecutions": false
  },
  "tags": []
}
