すべての記事
Linear webhook を localhost で安全にテストする方法
Linearwebhookslocalhostdeveloper integrations

Linear webhook を localhost で安全にテストする方法

外部サービスから localhost へ直接接続はできません。トンネルは公開 HTTPS URL を用意し、実際の headers と変更されていない body をローカルサーバーへ転送します。 JSON parser より先に raw body を保存します。署名と入力を確認し、配信を永続化してから業務処理を開始します。

Linear webhook がローカル環境へ届く仕組み

外部サービスから localhost へ直接接続はできません。トンネルは公開 HTTPS URL を用意し、実際の headers と変更されていない body をローカルサーバーへ転送します。 >公式ドキュメント

1. raw body を保持するエンドポイントを作る

JSON parser より先に raw body を保存します。署名と入力を確認し、配信を永続化してから業務処理を開始します。

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);

JSON parser より先に raw body を保存します。署名と入力を確認し、配信を永続化してから業務処理を開始します。 >公式ドキュメント

2. ローカルポートを HTTPS で公開する

サーバーを起動し、別のターミナルでトンネルを開いたままにします。未署名リクエストへの 401 は routing と認証拒否が正常な証拠です。

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

サーバーを起動し、別のターミナルでトンネルを開いたままにします。未署名リクエストへの 401 は routing と認証拒否が正常な証拠です。

3. Linear で webhook を設定する

完全な HTTPS URL と必要なイベントだけを登録し、テスト環境で実際にイベントを発生させます。secret はリポジトリ外に保存します。

完全な HTTPS URL と必要なイベントだけを登録し、テスト環境で実際にイベントを発生させます。secret はリポジトリ外に保存します。

元のバイト列で署名を検証する

webhook secret と仕様で定められた正確なデータから HMAC を計算します。定数時間で比較し、secret をログへ出力しません。

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);
}

webhook secret と仕様で定められた正確なデータから HMAC を計算します。定数時間で比較し、secret をログへ出力しません。 >実践ガイド

タイムスタンプでリプレイを防ぐ

署名検証後に timestamp の鮮度を確認し、古いメッセージを拒否します。ホスト時計も同期してください。

署名検証後に timestamp の鮮度を確認し、古いメッセージを拒否します。ホスト時計も同期してください。

payload と配信 ID を理解する

既知の type だけを処理し、追加フィールドは許容します。安定した配信 ID を永続ストレージで重複排除に使います。

既知の type だけを処理し、追加フィールドは許容します。安定した配信 ID を永続ストレージで重複排除に使います。

すばやく応答し冪等に処理する

配信 ID の予約と job 作成を同じ transaction で行ってから 200 を返します。永続化できない場合はエラーを返します。

配信 ID の予約と job 作成を同じ transaction で行ってから 200 を返します。永続化できない場合はエラーを返します。 >実践ガイド

ローカルで全フローをテストする

  1. サーバーを起動し、別のターミナルでトンネルを開いたままにします。未署名リクエストへの 401 は routing と認証拒否が正常な証拠です。
  2. 完全な HTTPS URL と必要なイベントだけを登録し、テスト環境で実際にイベントを発生させます。secret はリポジトリ外に保存します。
  3. webhook secret と仕様で定められた正確なデータから HMAC を計算します。定数時間で比較し、secret をログへ出力しません。
  4. 配信 ID の予約と job 作成を同じ transaction で行ってから 200 を返します。永続化できない場合はエラーを返します。

routing、headers、署名、応答時間、冪等性を順に確認します。合成リクエストは拒否経路のテストに向いています。

Linear webhook のトラブルシューティング

  • サーバーを起動し、別のターミナルでトンネルを開いたままにします。未署名リクエストへの 401 は routing と認証拒否が正常な証拠です。
  • webhook secret と仕様で定められた正確なデータから HMAC を計算します。定数時間で比較し、secret をログへ出力しません。
  • 署名検証後に timestamp の鮮度を確認し、古いメッセージを拒否します。ホスト時計も同期してください。
  • POST path、トンネルの port、raw body、secret、時計、DB latency を確認してください。

>実践ガイド

ローカル環境と本番環境のセキュリティ確認

  • HTTPS を必須にし、サイズと method を制限し、secret とログを保護して、古いテスト URL を削除します。
  • JSON parser より先に raw body を保存します。署名と入力を確認し、配信を永続化してから業務処理を開始します。
  • 配信 ID の予約と job 作成を同じ transaction で行ってから 200 を返します。永続化できない場合はエラーを返します。

HTTPS を必須にし、サイズと method を制限し、secret とログを保護して、古いテスト URL を削除します。 >実践ガイド

よくある質問

localhost で Linear webhook をテストするには?
サーバーを起動し、別のターミナルでトンネルを開いたままにします。未署名リクエストへの 401 は routing と認証拒否が正常な証拠です。
Linear webhook の署名を検証するには?
webhook secret と仕様で定められた正確なデータから HMAC を計算します。定数時間で比較し、secret をログへ出力しません。
Linear が webhook を再送するのはなぜですか?
配信 ID の予約と job 作成を同じ transaction で行ってから 200 を返します。永続化できない場合はエラーを返します。
冪等性にはどのキーを使いますか?
既知の type だけを処理し、追加フィールドは許容します。安定した配信 ID を永続ストレージで重複排除に使います。