{
  "name": "Camelus → Linear",
  "nodes": [
    {
      "parameters": { "httpMethod": "POST", "path": "camelus", "responseMode": "onReceived", "options": { "rawBody": true } },
      "id": "a1b2c3d4-0001-4000-8000-000000000001",
      "name": "Camelus webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [0, 0],
      "webhookId": "camelus-linear"
    },
    {
      "parameters": {
        "jsCode": "// Paste the destination's signing secret (whsec_…) here.\nconst SECRET = 'whsec_PASTE_YOUR_SECRET';\nconst crypto = require('crypto');\nconst out = [];\nfor (const item of $input.all()) {\n  const headers = item.json.headers;\n  const raw = Buffer.from(item.binary.data.data, 'base64').toString('utf8');\n  const ts = headers['camelus-timestamp'];\n  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('Stale or missing Camelus-Timestamp');\n  const expected = crypto.createHmac('sha256', SECRET).update(`${ts}.${raw}`).digest('hex');\n  const ok = (headers['camelus-signature'] || '').split(',').some((p) => p.trim().replace(/^v1=/, '') === expected);\n  if (!ok) throw new Error('Bad Camelus-Signature');\n  const body = JSON.parse(raw);\n  if (!body.data || !body.data.theme) continue; // test.ping and non-theme events\n  out.push({ json: body });\n}\nreturn out;"
      },
      "id": "a1b2c3d4-0001-4000-8000-000000000002",
      "name": "Verify",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [220, 0]
    },
    {
      "parameters": {
        "teamId": "PICK_YOUR_TEAM",
        "title": "={{ $json.data.theme.title }}",
        "additionalFields": {
          "description": "={{ $json.data.theme.summary || '' }}\n\nAsked by {{ $json.data.theme.requesters }} people ({{ $json.data.theme.paying_requesters }} paying). Score {{ $json.data.theme.score }}.\n\n{{ $json.data.theme.representative_items.map(i => '- ' + i.summary_en).join('\\n') }}\n\n[Open in Camelus]({{ $json.data.theme.url }})\n\n---\ncamelus-callback: {{ $json.callback.url }} {{ $json.callback.token }}"
        }
      },
      "id": "a1b2c3d4-0001-4000-8000-000000000003",
      "name": "Create Linear issue",
      "type": "n8n-nodes-base.linear",
      "typeVersion": 1,
      "position": [440, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Verify').item.json.callback.url }}",
        "sendHeaders": true,
        "headerParameters": { "parameters": [{ "name": "Authorization", "value": "=Bearer {{ $('Verify').item.json.callback.token }}" }] },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ status: 'planned', external_id: $json.identifier, external_url: $json.url }) }}",
        "options": {}
      },
      "id": "a1b2c3d4-0001-4000-8000-000000000004",
      "name": "Report planned",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [660, 0]
    },
    {
      "parameters": { "teamId": "PICK_YOUR_TEAM", "resources": ["issue"] },
      "id": "a1b2c3d4-0001-4000-8000-000000000005",
      "name": "Linear issue updated",
      "type": "n8n-nodes-base.linearTrigger",
      "typeVersion": 1,
      "position": [0, 260],
      "webhookId": "camelus-linear-updates"
    },
    {
      "parameters": {
        "jsCode": "// Completed issues that came from Camelus: read the callback from the description footer.\nconst out = [];\nfor (const item of $input.all()) {\n  const issue = item.json.data || {};\n  const done = issue.state && issue.state.type === 'completed';\n  const m = (issue.description || '').match(/camelus-callback: (\\S+) (cbt_\\S+)/);\n  if (done && m) out.push({ json: { url: m[1], token: m[2], external_id: issue.identifier, external_url: issue.url } });\n}\nreturn out;"
      },
      "id": "a1b2c3d4-0001-4000-8000-000000000006",
      "name": "Completed Camelus issues",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [220, 260]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": { "parameters": [{ "name": "Authorization", "value": "=Bearer {{ $json.token }}" }] },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ status: 'shipped', external_id: $json.external_id, external_url: $json.external_url }) }}",
        "options": {}
      },
      "id": "a1b2c3d4-0001-4000-8000-000000000007",
      "name": "Report shipped",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [440, 260]
    }
  ],
  "connections": {
    "Camelus webhook": { "main": [[{ "node": "Verify", "type": "main", "index": 0 }]] },
    "Verify": { "main": [[{ "node": "Create Linear issue", "type": "main", "index": 0 }]] },
    "Create Linear issue": { "main": [[{ "node": "Report planned", "type": "main", "index": 0 }]] },
    "Linear issue updated": { "main": [[{ "node": "Completed Camelus issues", "type": "main", "index": 0 }]] },
    "Completed Camelus issues": { "main": [[{ "node": "Report shipped", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" },
  "meta": { "templateCredsSetupCompleted": false, "description": "Camelus themes → Linear issues, with status callbacks. https://camelus.si/docs/destinations#n8n" }
}
