import crypto from 'node:crypto';
function timingSafeEqual(a, b) {
const ba = Buffer.from(a);
const bb = Buffer.from(b);
if (ba.length !== bb.length) return false;
return crypto.timingSafeEqual(ba, bb);
}
export function verifyWorkflowSignature({ secret, rawBody, signatureHeader }) {
if (!signatureHeader?.startsWith('sha256=')) return false;
const provided = signatureHeader;
const digest = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const expected = `sha256=${digest}`;
return timingSafeEqual(provided, expected);
}
Modular design for your two use cases
Use case 1 — Linear MCP auto-update (“may not require a return”)
Goal: Claude updates the Linear issue using MCP tools, so your automation doesn’t need to parse the model output to update the task.
Recommended approach:
Still use Option B, but set mode = "mcp".
Submitter stores jobId and optionally comments “Running…”.
Poller confirms completion and then:
If the task already looks updated (e.g., status/label changed), mark done and stop.
Otherwise, post a fallback comment with the raw result.output and/or an error.
Why still poll?
MCP tool execution can fail silently or partially; polling gives you a reliable completion signal and a place to implement fallback behavior.
What is the “return” for MCP mode?
Workflow always stores/returns whatever Claude printed as job.result.output.
For MCP-style “update Linear and exit”, the best practice is to require a single-line JSON receipt at the end of the response (see docs/PROMPT_TEMPLATES.md).
If you truly don’t need response content, you can ignore job.result.output and use either:
the completion webhook (callback.url), or
polling (GET /api/jobs/:jobId) as a fallback.
Use case 2 — Manual update required (structured response contract)
Goal: you want a predictable payload that n8n can parse and write back to the task.
Recommended approach:
Use Option B with mode = "manual".
In your prompt, require Claude to output a single JSON object (no markdown fencing) so n8n can parse it.
Poller extracts job.result.output and parses JSON. If parsing fails, fall back to “raw text” update.