|
ドキュメント
デモを予約プラットフォーム
プラットフォームMCPCLIAPI
ワークフロー
ガイド変更履歴

ようこそ

  • 概要
  • 認証
  • エラーとステータスコード
  • Webhookシグネチャ

ローカライゼーション

  • 概要
  • ジョブを作成
  • 翻訳対象外のキーをロックする
  • ジョブグループを追跡
  • 単一ジョブを取得
  • ジョブ一覧
  • Webhook配信
  • リアルタイム進捗(WebSocket)

パイプライン

  • 概要
  • ローカライズ前のAI編集
  • 人によるレビュー
  • AI評価(ポストエディット)
  • 自然なコピーに言い換える
  • 逆翻訳チェック
  • パイプラインを設定
  • パイプライン実行を確認する

プロビジョニング

  • 概要
  • プロビジョニングジョブを作成
  • ソースの種類
  • AIが抽出するもの
  • Webhook配信
  • ライブ進行状況(WebSocket)

同期

  • Localize
  • Recognize

エンジン管理

  • エンジン提案

Webhookシグネチャ

非同期ジョブが完了しても、Lingo.dev では延々とポーリングする必要はありません。代わりに、登録済みの callbackUrl として設定した HTTPS エンドポイントに POST で通知します。便利な仕組みですが、同時にリスクもあります。公開 URL はインターネットから届くあらゆるリクエストを受け付けるため、URL を知られれば、誰でも偽造した「ジョブ完了」イベントをあなたのハンドラーに POST できてしまいます。

だから、すべてのコールバックで守るべきルールは同じです。信頼する前に、必ず検証する。 それぞれの配信には、あなたと Lingo.dev だけが知るシークレットから計算されたシグネチャが付いています。受信側で再計算し、一定時間比較で照合すれば、偽造ペイロードがビジネスロジックに到達することはありません。このページでは、その検証の仕組みだけをまとめています。localization と provisioning のコールバックは、どちらも同じ仕組みをそのまま使います。各ページではペイロードの形式を扱い、検証についてはこのページを参照します。

このページでわかること

  • 3つのヘッダー
  • 署名シークレット
  • シグネチャを検証する方法
  • なぜ生のボディが重要なのか
  • リプレイ攻撃を防ぐ
  • まずはすばやく応答し、処理は後で行う
  • 再試行とバックオフ

3つのヘッダー#

Lingo.dev は Standard Webhooks 仕様に準拠しています。これは複数のプロバイダーが採用しているオープンな方式なので、ベンダー独自の特殊仕様ではなく、公開された仕様に基づいて検証できます。各配信には、次の 3 つのヘッダーが含まれます。

ヘッダー説明
webhook-idその配信を一意に識別する ID です。
webhook-timestamp配信が送信された時刻を表す Unix タイムスタンプ(秒)です。
webhook-signatureシグネチャ本体です: v1,{base64(HMAC-SHA256(secret, "{id}.{timestamp}.{body}"))}

署名対象は、この 3 つの要素をピリオドでつないだ文字列です。順番は、webhook-id、次に webhook-timestamp、最後に 生のリクエストボディ。この順序どおりである必要があります。この文字列を組み立て、シークレットで HMAC-SHA256 を計算し、その結果を base64 エンコードしたものが比較対象の値になります。

webhook-signature ヘッダーには、方式バージョン(v1,...)付きのシグネチャがスペース区切りで複数入る場合があります。検証側は、どれか 1 つでも 一致すればその配信を受け入れます。単一の値だけを見るのではなく、一覧として順に確認するのが、このヘッダーを安全にパースするやり方です。そのため、以下のサンプルでも含まれているすべてのシグネチャをループで確認しています。

署名シークレット#

シークレットは、callbackUrl を指定して初めてジョブを送信したときに、組織ごとに生成されます。形式は、先頭に whsec_ が付き、その後ろに base64 エンコードされた鍵バイト列が続きます。

text
whsec_Mf9aQ7n...base64...key...bytes

実際の鍵バイト列を取り出すには、whsec_ プレフィックスを削除し、残りを base64 デコードします。HMAC キーとして使うのは、そのデコード後の値であって、プレフィックス付き文字列そのものではありません。見た目は合っているのに実装が一致しない、最もよくある原因は、whsec_... の文字列をそのまま署名に使ってしまうことです。必ず先にデコードしてください。

このシークレットは API キーと同じように扱ってください

署名シークレットは、本物のコールバックと偽造されたコールバックを見分けるためのものです。サーバー側だけで管理し、ソース管理にもクライアントバンドルにも含めないでください。これを持っている人は、あなたのハンドラーが受け入れるペイロードに署名できてしまいます。Lingo.dev の組織スコープ認証情報の扱いについては、API Keys を参照してください。

シグネチャを検証する#

検証は、ハンドラーの前段に一度だけ組み込む関数で行えます。その関数がやることは 3 つだけです。生のボディから期待されるシグネチャを再計算すること、一定時間比較で受信した値と照合すること、そして一致しないものをあなたのコードが動く前に拒否することです。同じ関数で、Lingo.dev が送るすべての非同期イベントを保護できます。ローカライゼーション完了、provisioning 完了、あらゆるイベントタイプ、あらゆるプロダクト面で共通です。

javascript
import crypto from "node:crypto";

function verifyWebhook(payload, headers, secret) {
  const msgId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];

  // Reject timestamps outside a tolerance window (replay prevention)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    throw new Error("Webhook timestamp too old");
  }

  // Recompute the expected signature over id.timestamp.body
  const content = `${msgId}.${timestamp}.${payload}`;
  const secretBytes = Buffer.from(secret.replace("whsec_", ""), "base64");
  const expected = crypto
    .createHmac("sha256", secretBytes)
    .update(content)
    .digest("base64");

  // A delivery may carry several signatures; accept if any matches
  for (const sig of signatures.split(" ")) {
    const [version, value] = sig.split(",", 2);
    if (version === "v1" && crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(value)
    )) {
      return JSON.parse(payload);
    }
  }

  throw new Error("Invalid webhook signature");
}

比較には、crypto.timingSafeEqual や hmac.compare_digest のような一定時間比較関数を使ってください。== は使わないでください。通常の文字列比較は、2 バイトの差分が見つかった時点で処理を打ち切るため、そのタイミング差からシグネチャが 1 バイトずつ漏れる可能性があります。一定時間比較はこのサイドチャネルを防ぐため、上の 2 つのサンプルでも使われています。

なぜ生のボディが重要なのか#

どちらの関数も、payload に対して署名している点に注目してください。つまり、JSON としてパースされる前の、受信したそのままのボディです。これは、他が正しくても統合でつまずきやすいポイントであり、問題になりやすい箇所だからこそ、ここではっきり書いておく価値があります。

シグネチャは、Lingo.dev が送信したまったく同じバイト列に対して計算されます。ボディを一度オブジェクトにパースしてから再シリアライズすると、空白、キー順、数値の表現が変わることがあります。そうなると、再計算した HMAC は元のバイト列に対するシグネチャと一致しません。ペイロードの意味が同じでも、バイト列が同じとは限らないのです。

パース済みオブジェクトではなく、生のボディで検証する

フレームワークがパースする前に、生のリクエストボディを取得し、そのバイト列を検証関数に渡してください。Express では webhook ルートで express.raw({ type: "application/json" }) を使います。FastAPI では await request.body() を読み取ります。パースするのは、シグネチャ検証を通過してからです。まず検証、次にパースです。

リプレイ攻撃を防ぐ#

攻撃者が取得した有効な署名付きペイロードは、そのまま再送できます。最初の配信時でも、1 時間後に送り直したコピーでも、シグネチャ自体は変わらないからです。その有効期間に上限を設けるのが webhook-timestamp ヘッダーです。このヘッダーには配信時刻が入っているため、検証側は自分で決めた許容時間より古いものを拒否できます。上のサンプルでは 5 分を使っています。

タイムスタンプを確認すれば、古いリプレイを防げます。取得されたコピーが許容時間を過ぎてから再送されても、新鮮性チェックで弾かれ、ハンドラーには届きません。

まずはすばやく応答し、処理は後で行う#

配信を検証できたら、すぐに 200 を返し、その後で本来の処理を行ってください。たとえば、データベースへの書き込み、下流サービスの呼び出し、キャッシュ無効化などです。

javascript
app.post(
  "/webhooks/lingo",
  express.raw({ type: "application/json" }),
  (req, res) => {
    let event;
    try {
      event = verifyWebhook(req.body.toString(), req.headers, process.env.LINGO_WEBHOOK_SECRET);
    } catch {
      return res.status(401).send("invalid signature");
    }

    // Acknowledge first, process after - never block the response on slow work
    res.status(200).send("ok");
    void handleEvent(event);
  }
);

理由は作法ではなく、仕組みの問題です。ハンドラーが遅いと HTTP 接続を開いたまま保持し続けます。タイムアウトするほど長引けば、その配信は失敗扱いとなって再試行されます。つまり、レスポンス経路の中で重い処理をすると、1 つのイベントが複数回の処理に増えてしまいます。まずはすばやく受領を返し、処理はキューやバックグラウンドタスクに渡してください。そうすれば、1 つのイベントは 1 つのイベントのままです。handleEvent の中で分岐するペイロード形式は各プロダクトのページにあります。localization callbacks と provisioning callbacks を参照してください。

再試行とバックオフ#

エンドポイントが一時的に落ちることはあります。デプロイ中、タイムアウト、Bad Gateway。そういうときでも、Lingo.dev はイベントを捨てません。

エンドポイントが non-2xx ステータスを返した場合、または到達不能な場合、配信は 30 秒から始まる指数バックオフで再試行され、最大 5 回 まで行われます。5 回目の試行後、その配信は失敗としてマークされ、Lingo.dev は再試行を停止します。ただし、結果そのものが失われるわけではありません。結果はジョブレコードから引き続き取得できるため、一定時間のダウンタイムで失うのはコールバックだけで、結果自体ではありません。このジョブレコードが最後の保険です。通常系は webhook で処理しつつ、保存済みジョブはいつでも頼れる正本として扱ってください。翻訳ジョブなら、直接ポーリングできます。

次のステップ#

認証
API キーが API へのすべてのリクエストを認証する仕組み
ローカライゼーション webhook
translation.completed と translation.failed のペイロード形式
Provisioning webhook
AI エンジンの provisioning ジョブ向けコールバックペイロード

このページは役に立ちましたか?

Max PrilutskiyMax Prilutskiy·更新済み 約2か月前·2分で読めます