{
  "name": "Certificate & domain expiry watcher (read-only)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 7
            }
          ]
        }
      },
      "id": "n1",
      "name": "Every morning 07:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "domains",
              "value": "=[\"example.com\", \"your-client.pl\", \"mail.your-client.pl\"]",
              "type": "array"
            },
            {
              "id": "a2",
              "name": "warn_days",
              "value": 30,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "n2",
      "name": "Domains to watch",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Reads TLS expiry from the live connection and domain expiry from RDAP. No external deps.\nconst domains = $input.first().json.domains;\nconst warnDays = $input.first().json.warn_days || 30;\nconst out = [];\nfor (const host of domains) {\n  const row = { host, tls_days: null, domain_days: null, status: 'unknown', notes: [] };\n  // 1) TLS: certificate from the HTTPS connection\n  try {\n    const res = await this.helpers.httpRequest({\n      method: 'GET', url: 'https://' + host, returnFullResponse: true,\n      ignoreHttpStatusErrors: true, timeout: 10000,\n    });\n    const cert = res.request?.socket?.getPeerCertificate?.();\n    if (cert && cert.valid_to) {\n      row.tls_days = Math.floor((new Date(cert.valid_to) - Date.now()) / 86400000);\n    } else {\n      row.notes.push('certificate not readable');\n    }\n  } catch (e) {\n    row.notes.push('TLS: ' + String(e.message || e).slice(0, 80));\n  }\n  // 2) Domain registration: RDAP (public, no key)\n  try {\n    const rd = await this.helpers.httpRequest({\n      method: 'GET', url: 'https://rdap.org/domain/' + host.split('.').slice(-2).join('.'),\n      json: true, timeout: 10000,\n    });\n    const ev = (rd.events || []).find(e => e.eventAction === 'expiration');\n    if (ev) row.domain_days = Math.floor((new Date(ev.eventDate) - Date.now()) / 86400000);\n  } catch (e) {\n    row.notes.push('RDAP: no expiry data');\n  }\n  // 3) The worse of the two decides the status\n  const vals = [row.tls_days, row.domain_days].filter(v => v !== null);\n  const min = vals.length ? Math.min(...vals) : null;\n  row.status = min === null ? 'unknown' : min < 0 ? 'EXPIRED' : min <= 7 ? 'CRITICAL' : min <= warnDays ? 'WARNING' : 'OK';\n  out.push({ json: row });\n}\nreturn out;"
      },
      "id": "n3",
      "name": "Check TLS + RDAP",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Builds the digest. Sends NOTHING - it only prepares text for review.\nconst rows = $input.all().map(i => i.json);\nconst rank = { EXPIRED: 0, CRITICAL: 1, WARNING: 2, unknown: 3, OK: 4 };\nrows.sort((a, b) => rank[a.status] - rank[b.status]);\nconst needs = rows.filter(r => ['EXPIRED', 'CRITICAL', 'WARNING'].includes(r.status));\nconst line = r => {\n  const t = r.tls_days === null ? 'TLS ?' : 'TLS ' + r.tls_days + 'd';\n  const d = r.domain_days === null ? 'domain ?' : 'domain ' + r.domain_days + 'd';\n  return '  [' + r.status.padEnd(8) + '] ' + r.host.padEnd(28) + t + ' | ' + d;\n};\nconst text = [\n  needs.length ? '[ACTION] ' + needs.length + ' item(s) need attention' : '[OK] nothing expires within the warning window',\n  '', ...rows.map(line), '',\n  'Checked: ' + rows.length + ' | ' + new Date().toISOString().slice(0, 16).replace('T', ' '),\n].join('\n');\nreturn [{ json: { subject: (needs.length ? '[ACTION] ' : '[OK] ') + 'Certificate & domain expiry - ' + new Date().toISOString().slice(0,10), text, needs_action: needs.length, rows } }];",
        "mode": "runOnceForAllItems"
      },
      "id": "n4",
      "name": "Build digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ]
    }
  ],
  "connections": {
    "Every morning 07:00": {
      "main": [
        [
          {
            "node": "Domains to watch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Domains to watch": {
      "main": [
        [
          {
            "node": "Check TLS + RDAP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check TLS + RDAP": {
      "main": [
        [
          {
            "node": "Build digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}