การดูแลระบบ › Webhooks
เอกสาร/การดูแลระบบ/Webhooks

Webhooks

หน้านี้สอนอะไร

แจ้งบริการอื่นเมื่อเนื้อหาเปลี่ยน พร้อมลายเซ็นและการส่งซ้ำที่ไม่หายแม้ server รีสตาร์ต

ควรอ่านก่อน: Hooks

แจ้งบริการอื่นเมื่อเนื้อหาเปลี่ยนแปลง เช่น build static site ใหม่ ล้าง cache ของ CDN หรือแจ้งเตือนไปยัง ช่องแชต

export default defineConfig({
  webhooks: [
    // Every event of every collection and global, signed.
    { url: 'https://example.com/cms-hook', secret: process.env.WEBHOOK_SECRET },
    // Only publishing of posts, e.g. a Vercel or Netlify build hook.
    { url: process.env.BUILD_HOOK_URL!, events: ['publish', 'unpublish'], collections: ['posts'], globals: [] },
  ],
})
ตัวเลือกค่าเริ่มต้น
urlจำเป็นปลายทางที่ event จะถูก POST ไป
eventsทั้งหมดcreate, update, delete, publish, unpublish, draft
collectionsทั้งหมดยกเว้น usersslug ของ collection ใช้ [] เพื่อไม่เลือกเลย บัญชีผู้ใช้จะถูกส่งเฉพาะเมื่อระบุ users ไว้
globalsทั้งหมดslug ของ global ใช้ [] เพื่อไม่เลือกเลย
secret—ลงลายเซ็นทุก request (ดูด้านล่าง)
headers—header เพิ่มเติม เช่น token ที่ฝั่งรับใช้ตรวจสอบ

Events

Eventเมื่อใด
createมีการสร้างเอกสาร
updateเอกสารที่จัดเก็บไว้หรือ global มีการเปลี่ยนแปลง
deleteมีการลบเอกสาร
publishstatus เปลี่ยนเป็น published (ส่งพร้อมกับ create หรือ update)
unpublishstatus ไม่เป็น published อีกต่อไป
draftบันทึกเฉพาะฉบับร่าง เนื้อหาที่แสดงบนเว็บไม่เปลี่ยน (เวอร์ชัน)

การเผยแพร่ตามเวลาที่ตั้งไว้จะส่ง event เดียวกัน

Event ของคุณเอง

plugin และแอปส่ง event ของตัวเองได้ เช่น order.paid ของร้านค้า ประกาศชื่อไว้ใน events (plugin ประกาศให้เอง) ส่งด้วย cms.emit() แล้วระบุใน events ของ webhook event เหล่านี้ส่งไปเฉพาะ webhook ที่ระบุไว้

export default defineConfig({
  events: ['invoice.sent'],
  webhooks: [{ url: 'https://erp.example.com/hook', events: ['invoice.sent'], secret }],
})
 
cms.emit('invoice.sent', { number: 'INV-1' }, { collection: 'invoices', id: 7 })

body คือ { event, collection?, id?, doc, timestamp } โดย doc คือสิ่งที่ส่งให้ emit การส่ง ลายเซ็น และการส่งซ้ำเหมือนเดิม

Requests

POST /cms-hook
content-type: application/json
x-easy-cms-event: publish
x-easy-cms-delivery: 2f1c…               (the same on retries)
x-easy-cms-timestamp: 1761700000          (with `secret`: this attempt's time, Unix seconds)
x-easy-cms-signature-v2: v1=5c3e…         (with `secret`)
x-easy-cms-signature: sha256=9a0b…        (with `secret`, deprecated)
 
{ "event": "publish", "collection": "posts", "id": 12, "doc": { … }, "timestamp": "…" }

doc คือเอกสารที่จัดเก็บไว้ ครบทุกภาษา โดยไม่มี field ที่ซ่อนอยู่

ตรวจลายเซ็น

เมื่อตั้ง secret การส่งแต่ละครั้งจะเซ็นด้วย HMAC-SHA256 ของ <delivery>.<timestamp>.<body> โดย body เป็นแบบดิบตามที่ส่ง timestamp เป็นเวลาของการส่งครั้งนั้น ใครที่ดักจับ request ได้จึงเอาไปส่งซ้ำทีหลังไม่ได้ ใน Node ใช้ verifyWebhook() จาก @easy-cms/core ตรวจและได้ payload กลับมา

import { verifyWebhook } from '@easy-cms/core'
 
export async function POST(request: Request) {
  const body = await request.text() // body ดิบ ก่อน parse
  let payload
  try {
    payload = verifyWebhook(body, request.headers, process.env.CMS_WEBHOOK_SECRET!)
  } catch {
    return new Response('Bad signature', { status: 401 })
  }
  // …
}
  • ห้านาที ปฏิเสธ timestamp ที่ห่างจากตอนนี้เกิน 5 นาที (เปลี่ยนได้ด้วย { toleranceSeconds }) นาฬิกาของปลายทางจึงต้องตรง
  • ข้ามรายการที่จัดการแล้ว ภายในห้านาทีนั้น request ที่ส่งซ้ำยังผ่านการตรวจ ให้เก็บ x-easy-cms-delivery ที่จัดการแล้วไว้สักพักและข้ามรายการซ้ำ การลองใหม่ใช้ id เดิม
  • หลาย secret ส่งเป็น array ระหว่างเปลี่ยน secret ตัวไหนตรงก็ผ่าน

ภาษาอื่น ให้เทียบแบบใช้เวลาคงที่

import { createHmac, timingSafeEqual } from 'node:crypto'
 
const signed = `${headers['x-easy-cms-delivery']}.${headers['x-easy-cms-timestamp']}.${rawBody}`
const expected = `v1=${createHmac('sha256', secret).update(signed).digest('hex')}`
const given = headers['x-easy-cms-signature-v2']
const valid =
  expected.length === given.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given))
// และ |ตอนนี้ − x-easy-cms-timestamp| ≤ 300 วินาที

x-easy-cms-signature (HMAC ของ body อย่างเดียว) ยังส่งอยู่จนถึง 2.0 แต่ request ที่ถูกดักจับเอาไปส่งซ้ำได้ ให้เปลี่ยนไปใช้ x-easy-cms-signature-v2

Delivery

webhook จะถูกส่งหลังจากบันทึกการเปลี่ยนแปลงแล้ว และจะไม่ทำให้การบันทึกช้าลงหรือล้มเหลว กรณี timeout (10 วินาที), ข้อผิดพลาดของเครือข่าย, คำตอบ 429 และ 5xx จะลองส่งใหม่ ส่วนคำตอบ 4xx อื่นๆ จะถูกบันทึก log และไม่ลองใหม่

  • ลองใหม่เร็วๆ สองครั้งใน process ที่ทำการเปลี่ยนแปลง (หลัง 1 วินาทีและ 5 วินาที)
  • การส่งแต่ละครั้งจะถูกบันทึกลงฐานข้อมูล (ตารางภายใน webhook-deliveries) ก่อนส่งครั้งแรก และลบออกเมื่อสำเร็จ จึงไม่หายแม้ restart หรือฟังก์ชัน serverless หยุดทำงานกลางคันระหว่างส่ง ถ้า process หยุดระหว่างส่ง การรันรอบถัดไปจะส่งต่อให้หลังผ่านไป 5 นาที
  • หลังจากนั้นจะลองใหม่พร้อมกับงานที่ตั้งเวลาไว้: หลัง 1 นาที, 5 นาที, 30 นาที, 2, 6 และ 12 ชั่วโมง server จะรันทุกนาทีเอง ส่วน serverless ให้ cron เรียก <api>/jobs/run
  • หลังครั้งสุดท้าย (ประมาณหนึ่งวัน) การส่งจะถูกเก็บไว้เป็น state: 'failed' พร้อม error ล่าสุด และไม่ลองใหม่อีก admin ดูได้ที่ ตั้งค่า → การส่ง และกดส่งซ้ำหรือลบได้ (ตรวจสุขภาพระบบ) รายการที่ล้มจะถูกลบเมื่อเกิน 30 วัน

ทุกครั้งที่ลองใหม่จะส่ง body และ x-easy-cms-delivery เหมือนเดิม (timestamp และลายเซ็นใหม่) ปลายทางจึงข้ามการส่งที่เคยจัดการแล้วได้ การเพิ่ม webhooks ใน config จะเพิ่มตาราง webhook-deliveries ให้สร้าง migration เหมือนการเปลี่ยน config ทั่วไป

บนแพลตฟอร์ม serverless ฟังก์ชันอาจหยุดทำงานก่อนที่ webhook จะถูกส่ง: ให้เรียก await cms.flushWebhooks() ก่อน return เมื่อเขียนข้อมูลผ่าน Local API บนแพลตฟอร์มเหล่านั้น ส่วน await cms.retryWebhooks() จะลองส่งรายการที่บันทึกไว้ซึ่งถึงเวลาแล้ว (runJobs() ทำทั้งส่วนนี้และการเผยแพร่ตามเวลา)

ขั้นต่อไป