|
| 1 | +# ZSend Webhooks |
| 2 | + |
| 3 | +ZSend Webhooks 是一個部署在 Cloudflare Workers 上的 webhook 接收端。它會接收 ZSend 傳來的 POST 請求,驗證 `x-zsend-signature` 與 `x-zsend-timestamp`,確認 payload 沒有被竄改後才回應成功。 |
| 4 | + |
| 5 | +## 功能 |
| 6 | + |
| 7 | +- 只接受 `POST` webhook 請求 |
| 8 | +- 使用 `SECRET_KEY` 驗證 HMAC SHA-256 簽章 |
| 9 | +- 支援 ZSend 測試事件:`X-ZSend-Event: test` |
| 10 | +- 使用 Cloudflare Workers 執行 |
| 11 | +- 使用 Vitest 與 `@cloudflare/vitest-pool-workers` 測試 Worker 行為 |
| 12 | + |
| 13 | +## Zeabur 測試事件例外 |
| 14 | + |
| 15 | +目前程式中對 `X-ZSend-Event: test` 有一個暫時性的例外處理:只要收到這個事件,就會直接回應 `200 OK`,不會進入簽章驗證流程。 |
| 16 | + |
| 17 | +這是為了相容 Zeabur 目前的設計缺陷。Zeabur 會在提供 webhook secret 的瞬間,甚至在 endpoint 端有機會完成 `SECRET_KEY` 設定之前,就先送出 `test` 事件。此時接收端還來不及填入與 Zeabur 相同的 secret,因此測試事件無法通過一般的 `x-zsend-signature` 驗證流程。為了讓 Zeabur 的測試功能可以正常判定 endpoint 存活,才暫時讓 `test` 事件無論簽章狀態如何都回應成功。 |
| 18 | + |
| 19 | +等 Zeabur 官方修正測試 webhook 的簽章行為後,預計會移除這段 `test` 事件的例外處理,讓所有 webhook 請求都走一致的簽章驗證流程。 |
| 20 | + |
| 21 | +## 需求 |
| 22 | + |
| 23 | +- Node.js 22+ |
| 24 | +- npm |
| 25 | +- Cloudflare 帳號與 Wrangler 登入狀態 |
| 26 | + |
| 27 | +## 安裝 |
| 28 | + |
| 29 | +```sh |
| 30 | +npm ci |
| 31 | +``` |
| 32 | + |
| 33 | +建立本機環境變數檔: |
| 34 | + |
| 35 | +```sh |
| 36 | +# macOS / Linux |
| 37 | +cp .env.example .env |
| 38 | + |
| 39 | +# Windows PowerShell |
| 40 | +Copy-Item .env.example .env |
| 41 | +``` |
| 42 | + |
| 43 | +接著設定 webhook 驗章用的 secret: |
| 44 | + |
| 45 | +```env |
| 46 | +SECRET_KEY=your_secret_key_here |
| 47 | +``` |
| 48 | + |
| 49 | +## 本機開發 |
| 50 | + |
| 51 | +啟動 Cloudflare Workers 開發伺服器: |
| 52 | + |
| 53 | +```sh |
| 54 | +npm run dev |
| 55 | +``` |
| 56 | + |
| 57 | +如果要使用專案內的 `start` script: |
| 58 | + |
| 59 | +```sh |
| 60 | +npm start |
| 61 | +``` |
| 62 | + |
| 63 | +## 部署 |
| 64 | + |
| 65 | +先把 `SECRET_KEY` 設成 Cloudflare Workers secret: |
| 66 | + |
| 67 | +```sh |
| 68 | +npx wrangler secret put SECRET_KEY |
| 69 | +``` |
| 70 | + |
| 71 | +部署 Worker: |
| 72 | + |
| 73 | +```sh |
| 74 | +npm run deploy |
| 75 | +``` |
| 76 | + |
| 77 | +## Webhook 請求格式 |
| 78 | + |
| 79 | +Worker 會檢查以下 headers: |
| 80 | + |
| 81 | +| Header | 說明 | |
| 82 | +| ------------------- | -------------------------------------------- | |
| 83 | +| `x-zsend-event` | ZSend 事件名稱;若為 `test` 會直接回應 `OK!` | |
| 84 | +| `x-zsend-signature` | HMAC 簽章,格式為 `sha256=<hex_digest>` | |
| 85 | +| `x-zsend-timestamp` | 簽章使用的 timestamp | |
| 86 | + |
| 87 | +Body 必須是合法 JSON。 |
| 88 | + |
| 89 | +簽章計算方式: |
| 90 | + |
| 91 | +```ts |
| 92 | +const message = `${timestamp}.${rawBody}`; |
| 93 | +const digest = crypto.createHmac("sha256", secret).update(message).digest("hex"); |
| 94 | +const signature = `sha256=${digest}`; |
| 95 | +``` |
| 96 | + |
| 97 | +注意:`rawBody` 必須是實際送出 request body 的原始字串;如果重新格式化 JSON,簽章會不同。 |
| 98 | + |
| 99 | +## 回應狀態 |
| 100 | + |
| 101 | +| 狀態碼 | 回應 | 情境 | |
| 102 | +| ------ | --------------------------------------------------- | -------------------------------------- | |
| 103 | +| `200` | `OK!` | 簽章正確,或收到 `X-ZSend-Event: test` | |
| 104 | +| `400` | `Missing signature or timestamp` | 缺少簽章或 timestamp | |
| 105 | +| `400` | `Invalid JSON` | body 不是合法 JSON | |
| 106 | +| `401` | `Invalid signature` | 簽章驗證失敗 | |
| 107 | +| `405` | `Method Not Allowed` | 不是 `POST` 請求 | |
| 108 | +| `500` | `Server configuration error: SECRET_KEY is not set` | Worker 未設定 `SECRET_KEY` | |
| 109 | + |
| 110 | +## 測試與檢查 |
| 111 | + |
| 112 | +```sh |
| 113 | +npm run test:run |
| 114 | +npm run typecheck |
| 115 | +npm run format:check |
| 116 | +npm run types:check |
| 117 | +``` |
| 118 | + |
| 119 | +開發時也可以使用 watch 模式: |
| 120 | + |
| 121 | +```sh |
| 122 | +npm test |
| 123 | +``` |
| 124 | + |
| 125 | +## 專案結構 |
| 126 | + |
| 127 | +```txt |
| 128 | +src/_index.ts Worker fetch 入口 |
| 129 | +src/router.ts Webhook request 驗證與回應邏輯 |
| 130 | +test/index.spec.ts Worker 單元與整合測試 |
| 131 | +wrangler.jsonc Cloudflare Workers 設定 |
| 132 | +``` |
0 commit comments