← 所有文章
個案分析

X OAuth 故障演練:小團隊點樣將私訊放入簽名入站隊列

7 月 1 日,X 嘅開發者狀態頁俾支援團隊一個好實用嘅提醒:OAuth2.0 login 同 /2/users/me 喺 6 月 30 日 23:00 UTC 至 7 月 1 日 01:00 UTC 期間出現 401 錯誤。事故已經恢復,狀態頁之後顯示所有系統正常。對每日只睇一次 X 嘅團隊嚟講,呢件事只係背景噪音;但對支援流程由刷新 OAuth、呼叫 /2/users/me、再輪詢活動開始嘅小團隊嚟講,呢個就係一次故障演練。

呢個案例來自一個 3 人新加坡出海團隊。產品發佈期間,佢哋同時從 X、WhatsApp 同 LINE 接收客戶問題。7 月呢次事故冇令佢哋遺失訊息,但復盤指出一個問題:佢哋自己嘅支援流水線將 X 身分刷新放喺每次入站任務第一步。只要第一步回傳 401,後面入隊同分派就唔會開始。

解法唔係「永遠唔用 X 官方 API」。真正嘅改動係:將即時客戶訊息放入簽名入站隊列,先保存每次投遞,再將平台 API 放喺回覆或者補充資料階段,而唔係俾佢決定團隊可唔可以見到訊息。

脆弱嘅第一跳

團隊最早嘅 X 整合喺 2026 年好常見:用 X API v2 同 OAuth 2.0 PKCE,先確認目前授權使用者,再查詢私訊同提及。X 自己嘅 Direct Messages 文件將 Manage Direct Messages 描述為用於建立對話、傳送 DM、刪除 DM event 嘅 endpoint;前置條件亦好清楚:已核准嘅 developer account、Developer Console 入面嘅 project 同 app,以及 OAuth 2.0 PKCE 產生嘅 user access token。

當任務係「透過 X 送出呢則 DM」或者「喺 X 開發者平台管理對話」時,呢個官方介面合理。但呢個發佈團隊嘅營運任務唔同:

客戶從 X、WhatsApp 或 LINE 傳來訊息
  -> 支援系統收到訊息
  -> 喺任何 AI 或人工流程之前先保存事件
  -> 坐席由正確帳號回覆

舊流程將次序倒轉。佢先要求 X 證明目前帳號身分,然後先將訊息放入支援隊列。平時冇人留意;一旦 OAuth 或 /2/users/me 出問題,隊列就冇新 X 活動,因為入站 script 喺分派之前已經退出。

團隊真正需要兩件事:訊息要以事件形式到達;隊列唔應該由某一個平台嘅身分 endpoint 定義。

新嘅入站約定

佢哋先註冊 UnifyPort webhook endpoint,再調整其他流程:

curl -X POST https://api.unifyport.ai/v1/webhook-endpoints \
  -H "X-Api-Key: <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://support.example.com/webhook",
  "status": "active",
  "subscribed_events": ["message.received"],
  "signing_secret": "<WEBHOOK_SIGNING_SECRET>"
}'

UnifyPort 嘅非官方接口連接一般訊息帳號,並將入站活動投遞成一條標準事件流。對 X 嚟講,provider 值係 twitter;對 WhatsApp 同 LINE,同一個 envelope 仍然適用。一則 X 私訊會咁樣到達:

{
  "id": "evt_9b71a4c20d",
  "type": "message.received",
  "provider": "twitter",
  "account_id": "acc_launch_x",
  "occurred_at": "2026-07-09T02:30:00Z",
  "data": {
    "conversation": { "id": "x_dm_48192", "type": "user", "title": "Aria Chen" },
    "sender": { "id": "x_user_48291", "type": "user", "name": "Aria Chen" },
    "message": {
      "id": "x_msg_20260709_001",
      "type": "text",
      "text": "The preorder link returns 401 for me. Can you check?",
      "direction": "inbound",
      "sent_at": "2026-07-09T02:29:58Z"
    },
    "event": { "kind": "message_received" }
  }
}

關鍵欄位刻意保持簡單:idtypeprovideraccount_idoccurred_atdata。支援隊列可以按值分流,而唔需要為每個渠道學一套 event model。

先驗證,再保存,再分派

每次投遞都會帶 X-Device-Timestamp;如果啟用簽名,亦會帶 X-Device-Signature。簽名係用 endpoint 嘅 signing_secret 對 timestamp、英文句點同原始 request body 做 HMAC-SHA256 後得到嘅 hex 值。團隊將下面呢層驗證放喺隊列前面:

import crypto from "crypto";
import express from "express";

const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;

app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const timestamp = req.get("X-Device-Timestamp") || "";
  const signature = req.get("X-Device-Signature") || "";

  const hmac = crypto.createHmac("sha256", signingSecret);
  hmac.update(timestamp + ".");
  hmac.update(req.body);
  const expected = hmac.digest("hex");

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!valid) {
    res.status(401).end();
    return;
  }

  const event = JSON.parse(req.body.toString("utf8"));
  await storeEvent(event.id, req.body);

  if (event.type === "message.received") {
    await routeInboundMessage({
      provider: event.provider,
      accountId: event.account_id,
      conversationId: event.data.conversation.id,
      senderId: event.data.sender.id,
      text: event.data.message.text,
      occurredAt: event.occurred_at
    });
  }

  res.status(200).end();
});

次序比程式碼本身更重要。先用原始 bytes 驗證簽名;再按 id 保存事件;然後先送去 Slack、helpdesk、CRM 或 AI 分流工作。UnifyPort 文件明確講,webhook event 係入站流量嘅唯一記錄;錯過嘅事件唔可以之後再由 message-history API 補返。所以保存係入站邊界一部分,唔係下游優化項。

下一次演練發生咗咩

兩星期後,團隊做咗一次內部演練。佢哋阻斷以前會呼叫 /2/users/me 嘅任務,保留 webhook receiver 在線,然後向三個渠道送測試訊息。

X、WhatsApp 同 LINE 訊息都進入同一張 message.received 表。因為團隊故意暫停抓 profile metadata 嘅補充 worker,X 嘅 Slack 提醒慢少少,但原始事件已經保存好。坐席仍然睇到邊個發訊息、幾時到達、邊個帳號收到,以及客戶講咗咩。平台側補充資料可以稍後再追上。

回覆仍然係明確動作。坐席決定回覆時,後端用已連接帳號同收件人呼叫 POST /v1/messages

curl -X POST https://api.unifyport.ai/v1/messages \
  -H "X-Api-Key: <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "account_id": "acc_launch_x",
  "to": { "id": "x_user_48291", "type": "user" },
  "message": {
    "type": "text",
    "text": "Thanks for flagging it. The checkout link is fixed now."
  }
}'

呢個設計唔會令平台故障消失。如果 X 本身不可用,任何整合都會受影響。差別更窄、亦更實際:支援台唔再依賴 profile lookup 或輪詢任務先成功,先可以保存已經投遞到 webhook 嘅客戶活動。

團隊留下嘅檢查清單

佢哋將發佈前 runbook 收斂成五條:

  1. 先註冊 webhook,使用 subscribed_events: ["message.received"]
  2. 保持簽名開啟,並用原始 body 驗證 X-Device-Signature
  3. 每個事件先按 id 保存,再做分派、補充資料、AI 或人工指派。
  4. 將平台 API 呼叫視為補充資料或回覆步驟,唔好當成入站閘門。
  5. provideraccount_iddata.conversation.id 分流,咁接入 WhatsApp、LINE、Telegram、Zalo 或 TikTok 時唔需要再建另一條隊列。

7 月呢次 X 事故好短,所以佢更似測試信號,而唔係災難。小團隊好難完全移除對平台 API 嘅依賴,但可以選擇依賴放喺邊度。將佢放喺簽名入站隊列後面,而唔係放喺客戶訊息到達團隊之前。