บทความทั้งหมด
ทดสอบ Linear webhook บน localhost อย่างปลอดภัย
Linearwebhookslocalhostdeveloper integrations

ทดสอบ Linear webhook บน localhost อย่างปลอดภัย

บริการภายนอกเรียก localhost โดยตรงไม่ได้ tunnel จะสร้าง URL HTTPS สาธารณะและส่งต่อ headers กับ body ต้นฉบับมายังเซิร์ฟเวอร์ในเครื่อง เก็บ raw body ก่อน JSON parser ตรวจสอบลายเซ็นและข้อมูล บันทึกการส่งลง storage แบบถาวร แล้วจึงเริ่ม business logic

เส้นทางของ Linear webhook มายังแอปในเครื่อง

บริการภายนอกเรียก localhost โดยตรงไม่ได้ tunnel จะสร้าง URL HTTPS สาธารณะและส่งต่อ headers กับ body ต้นฉบับมายังเซิร์ฟเวอร์ในเครื่อง >เอกสารอย่างเป็นทางการ

1. สร้าง endpoint ที่เก็บ raw body

เก็บ raw body ก่อน JSON parser ตรวจสอบลายเซ็นและข้อมูล บันทึกการส่งลง storage แบบถาวร แล้วจึงเริ่ม business logic

import express from "express";
import crypto from "node:crypto";

const app = express();
const secret = process.env.LINEAR_WEBHOOK_SECRET;

app.post(
  "/webhooks/linear",
  express.raw({ type: "application/json", limit: "1mb" }),
  async (req, res) => {
    const rawBody = req.body;
    const signature = req.get("linear-signature");

    if (!verifyLinearSignature(signature, rawBody, secret)) {
      return res.sendStatus(401);
    }

    let payload;
    try {
      payload = JSON.parse(rawBody.toString("utf8"));
    } catch {
      return res.sendStatus(400);
    }

    if (!Number.isFinite(payload.webhookTimestamp) ||
        Math.abs(Date.now() - payload.webhookTimestamp) > 60_000) {
      return res.sendStatus(401);
    }

    const deliveryId = req.get("linear-delivery") || payload.webhookId;
    if (!deliveryId) return res.sendStatus(400);

    try {
      await recordAndEnqueueOnce(deliveryId, payload);
      return res.sendStatus(200);
    } catch (error) {
      console.error("Linear webhook persistence failed", error);
      return res.sendStatus(500);
    }
  }
);

app.use(express.json());
app.listen(3000);

เก็บ raw body ก่อน JSON parser ตรวจสอบลายเซ็นและข้อมูล บันทึกการส่งลง storage แบบถาวร แล้วจึงเริ่ม business logic >เอกสารอย่างเป็นทางการ

2. เปิดพอร์ตในเครื่องผ่าน HTTPS

เปิดเซิร์ฟเวอร์และรัน tunnel ใน terminal อีกหน้าตลอดการทดสอบ การได้ 401 จาก request ที่ไม่มีลายเซ็นแสดงว่า routing ทำงาน

npx portpreview 3000
https://example.portpreview.dev/webhooks/linear

เปิดเซิร์ฟเวอร์และรัน tunnel ใน terminal อีกหน้าตลอดการทดสอบ การได้ 401 จาก request ที่ไม่มีลายเซ็นแสดงว่า routing ทำงาน

3. ตั้งค่า webhook ใน Linear

บันทึก URL HTTPS แบบเต็ม เลือกเฉพาะ event ที่ต้องใช้ และสร้าง event จริงใน workspace ทดสอบ เก็บ secret นอก repository

บันทึก URL HTTPS แบบเต็ม เลือกเฉพาะ event ที่ต้องใช้ และสร้าง event จริงใน workspace ทดสอบ เก็บ secret นอก repository

ตรวจสอบลายเซ็นจากไบต์ต้นฉบับ

คำนวณ HMAC ด้วย webhook secret และข้อมูลต้นฉบับตามสเปก เปรียบเทียบแบบ constant-time และห้ามเขียน secret ลง log

function verifyLinearSignature(signature, rawBody, secret) {
  if (!secret || typeof signature !== "string" ||
      !/^[0-9a-f]{64}$/i.test(signature)) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest();
  const actual = Buffer.from(signature, "hex");

  return actual.length === expected.length &&
    crypto.timingSafeEqual(actual, expected);
}

คำนวณ HMAC ด้วย webhook secret และข้อมูลต้นฉบับตามสเปก เปรียบเทียบแบบ constant-time และห้ามเขียน secret ลง log >คู่มือฉบับเต็ม

ป้องกัน replay ด้วย timestamp

ตรวจสอบความใหม่ของ timestamp หลังลายเซ็นผ่าน และตั้งเวลาเครื่องให้ตรงเพื่อไม่ให้ request ที่ถูกต้องถูกปฏิเสธ

ตรวจสอบความใหม่ของ timestamp หลังลายเซ็นผ่าน และตั้งเวลาเครื่องให้ตรงเพื่อไม่ให้ request ที่ถูกต้องถูกปฏิเสธ

ทำความเข้าใจ payload และรหัสการส่ง

รองรับเฉพาะ type ที่รู้จัก ยอมรับ field ใหม่ และใช้รหัสการส่งที่คงที่เพื่อ deduplication ใน storage

รองรับเฉพาะ type ที่รู้จัก ยอมรับ field ใหม่ และใช้รหัสการส่งที่คงที่เพื่อ deduplication ใน storage

ตอบกลับเร็วและประมวลผลแบบ idempotent

จองรหัสการส่งและสร้าง job ใน transaction เดียวก่อนตอบ 200 หากบันทึกถาวรไม่ได้ให้ตอบ error เพื่อรอ retry

จองรหัสการส่งและสร้าง job ใน transaction เดียวก่อนตอบ 200 หากบันทึกถาวรไม่ได้ให้ตอบ error เพื่อรอ retry >คู่มือฉบับเต็ม

ทดสอบขั้นตอนทั้งหมดในเครื่อง

  1. เปิดเซิร์ฟเวอร์และรัน tunnel ใน terminal อีกหน้าตลอดการทดสอบ การได้ 401 จาก request ที่ไม่มีลายเซ็นแสดงว่า routing ทำงาน
  2. บันทึก URL HTTPS แบบเต็ม เลือกเฉพาะ event ที่ต้องใช้ และสร้าง event จริงใน workspace ทดสอบ เก็บ secret นอก repository
  3. คำนวณ HMAC ด้วย webhook secret และข้อมูลต้นฉบับตามสเปก เปรียบเทียบแบบ constant-time และห้ามเขียน secret ลง log
  4. จองรหัสการส่งและสร้าง job ใน transaction เดียวก่อนตอบ 200 หากบันทึกถาวรไม่ได้ให้ตอบ error เพื่อรอ retry

ตรวจ routing, headers, ลายเซ็น เวลาตอบกลับ และ idempotency โดยใช้ request จำลองสำหรับทดสอบเส้นทางปฏิเสธ

แก้ปัญหา Linear webhook

  • เปิดเซิร์ฟเวอร์และรัน tunnel ใน terminal อีกหน้าตลอดการทดสอบ การได้ 401 จาก request ที่ไม่มีลายเซ็นแสดงว่า routing ทำงาน
  • คำนวณ HMAC ด้วย webhook secret และข้อมูลต้นฉบับตามสเปก เปรียบเทียบแบบ constant-time และห้ามเขียน secret ลง log
  • ตรวจสอบความใหม่ของ timestamp หลังลายเซ็นผ่าน และตั้งเวลาเครื่องให้ตรงเพื่อไม่ให้ request ที่ถูกต้องถูกปฏิเสธ
  • ตรวจ POST path, port ของ tunnel, raw body, secret, เวลาเครื่อง และ latency ของฐานข้อมูล

>คู่มือฉบับเต็ม

รายการตรวจสอบความปลอดภัยสำหรับเครื่องพัฒนาและ production

  • บังคับ HTTPS จำกัดขนาดและ method ป้องกัน secret ปกปิดข้อมูลใน log และลบ URL ทดสอบที่หมดอายุ
  • เก็บ raw body ก่อน JSON parser ตรวจสอบลายเซ็นและข้อมูล บันทึกการส่งลง storage แบบถาวร แล้วจึงเริ่ม business logic
  • จองรหัสการส่งและสร้าง job ใน transaction เดียวก่อนตอบ 200 หากบันทึกถาวรไม่ได้ให้ตอบ error เพื่อรอ retry

บังคับ HTTPS จำกัดขนาดและ method ป้องกัน secret ปกปิดข้อมูลใน log และลบ URL ทดสอบที่หมดอายุ >คู่มือฉบับเต็ม

คำถามที่พบบ่อย

จะทดสอบ Linear webhook บน localhost ได้อย่างไร
เปิดเซิร์ฟเวอร์และรัน tunnel ใน terminal อีกหน้าตลอดการทดสอบ การได้ 401 จาก request ที่ไม่มีลายเซ็นแสดงว่า routing ทำงาน
จะตรวจสอบลายเซ็น Linear webhook ได้อย่างไร
คำนวณ HMAC ด้วย webhook secret และข้อมูลต้นฉบับตามสเปก เปรียบเทียบแบบ constant-time และห้ามเขียน secret ลง log
ทำไม Linear จึงส่ง webhook ซ้ำ
จองรหัสการส่งและสร้าง job ใน transaction เดียวก่อนตอบ 200 หากบันทึกถาวรไม่ได้ให้ตอบ error เพื่อรอ retry
ควรใช้ค่าใดเป็น idempotency key
รองรับเฉพาะ type ที่รู้จัก ยอมรับ field ใหม่ และใช้รหัสการส่งที่คงที่เพื่อ deduplication ใน storage