{
  "name": "Source Manifest Validator v0.1.1 - Manual test",
  "nodes": [
    {
      "name": "Manual test",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "id": "7050d598-3c79-410e-9f72-e3382f8b70db",
      "position": [
        0,
        0
      ]
    },
    {
      "name": "Demonstration manifest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "// Fictional reviewer; sample data only. Replace with a genuinely reviewed manifest.\nreturn [{json:{\"content_id\":\"demo-001\",\"required_claims\":[\"Schedule uses timezone\"],\"sources\":[{\"url\":\"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger\",\"title\":\"Schedule Trigger\",\"claim\":\"Schedule uses timezone\",\"source_type\":\"primary\",\"verification_status\":\"verified\",\"verified_by\":\"demo-editor\",\"checked_at\":\"2026-09-29T18:00:00+01:00\"}]}}];"
      },
      "id": "063a8b36-0736-4d40-8c33-ba6bbe7c0776",
      "position": [
        280,
        0
      ]
    },
    {
      "name": "Validate source manifest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "function validateSourceManifest(input, now) {\n const fail=m=>{throw new Error(m)};\n const str=(v,label)=>{if(typeof v!=='string'||!v.trim()||v.length>2000)fail('Invalid '+label);return v.trim()};\n // Accept explicit-timezone ISO timestamps at minute, second or millisecond precision.\n // Validate calendar fields first: Date.parse otherwise normalizes impossible dates.\n const timestamp=value=>{\n  if(typeof value!=='string'||value.length>2000)return NaN;\n  const m=/^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{1,3}))?)?(Z|[+-]\\d{2}:\\d{2})$/.exec(value);\n  if(!m)return NaN;\n  const year=Number(m[1]),month=Number(m[2]),day=Number(m[3]);\n  const leap=year%4===0&&(year%100!==0||year%400===0);\n  const days=[31,leap?29:28,31,30,31,30,31,31,30,31,30,31];\n  if(month<1||month>12||day<1||day>days[month-1]||Number(m[4])>23||Number(m[5])>59||Number(m[6]||0)>59)return NaN;\n  if(m[8]!=='Z'&&(Number(m[8].slice(1,3))>23||Number(m[8].slice(4,6))>59))return NaN;\n  return Date.parse(value);\n };\n if(!input||typeof input!=='object'||Array.isArray(input))fail('Invalid manifest');\n const id=str(input.content_id,'content_id');\n const time=timestamp(now);if(!Number.isFinite(time))fail('Invalid current time');\n if(!Array.isArray(input.required_claims)||!input.required_claims.length||input.required_claims.length>50)fail('Invalid required_claims');\n // Array.from visits sparse slots too, so missing entries cannot bypass validation.\n const claims=Array.from(input.required_claims,c=>str(c,'required claim'));\n if(new Set(claims).size!==claims.length)fail('Duplicate required claim');\n if(!Array.isArray(input.sources)||!input.sources.length||input.sources.length>50)fail('Invalid sources');\n const seen=new Set();\n const sources=Array.from(input.sources,s=>{\n  if(!s||typeof s!=='object'||Array.isArray(s))fail('Invalid source');\n  const raw=str(s.url,'source URL');\n  // Deliberately narrow portable contract: DNS host + ASCII path, no URL global or modules.\n  const match=/^https:\\/\\/([A-Za-z0-9.-]+)(\\/[A-Za-z0-9._~!$&'()*+,;=:@/-]*)?$/.exec(raw);\n  const labels=match?match[1].split('.'):[];\n  // Numeric-looking final labels can make browsers interpret the host as IPv4.\n  const tld=labels[labels.length-1]||'';\n  if(!match||labels.length<2||match[1].length>253||!labels.every(l=>/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(l))||!/[A-Za-z]/.test(tld)||/^0x[0-9a-f]*$/i.test(tld)||(match[2]||'').split('/').some(s=>s==='.'||s==='..'))fail('Use a clean HTTPS source URL with a DNS host and plain ASCII path; no credentials, query, fragment, port, percent encoding or dot segments');\n  const href='https://'+match[1].toLowerCase()+(match[2]||'/');\n  const key=href.replace(/\\/$/,'');if(seen.has(key))fail('Duplicate source URL');seen.add(key);\n  const title=str(s.title,'source title'),claim=str(s.claim,'source claim');\n  if(s.verification_status!=='verified')fail('Source has not been reviewed');\n  const reviewer=str(s.verified_by,'reviewer');\n  const date=str(s.checked_at,'checked_at');if(!/(?:Z|[+-]\\d{2}:\\d{2})$/.test(date))fail('Timestamp needs timezone');\n  const checked=timestamp(date);if(!Number.isFinite(checked)||checked>time)fail('Invalid or future verification timestamp');\n  if(s.source_type!=='primary')fail('Primary source classification required');\n  return {url:href,title,claim,verification_status:'verified',verified_by:reviewer,checked_at:date,source_type:'primary'};\n });\n const missing=claims.filter(c=>!sources.some(s=>s.claim===c));if(missing.length)fail('Uncovered required claims: '+missing.join(', '));\n return {content_id:id,status:'source_manifest_valid',source_count:sources.length,required_claims:claims,sources,uncovered_claims:[],factual_accuracy_verified_by_code:false};\n}\nreturn $input.all().map(item=>({json:validateSourceManifest(item.json,$now.toISO())}));"
      },
      "id": "24ee52ea-9b51-42ab-8dc1-31adf9087f36",
      "position": [
        560,
        0
      ]
    }
  ],
  "connections": {
    "Manual test": {
      "main": [
        [
          {
            "node": "Demonstration manifest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Demonstration manifest": {
      "main": [
        [
          {
            "node": "Validate source manifest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1",
    "timezone": "Africa/Luanda"
  },
  "tags": []
}
