Skip to content

@hibiki-js/core ​

Hibiki の中核パッケージです。不変なアプリビルダー、プロバイダー契約、リクエストの振り分け、安定したエラーコードを担当します。@hibiki-js/stripe や @hibiki-js/github などのプロバイダーは、この API に接続します。

扱うのは Web Standards の Request / Response だけです。環境変数の読み取り、ペイロードのログ、キュー、再送は行いません。

インストール ​

bash
pnpm add @hibiki-js/core

Node.js 22+、または Web Crypto と Request / Response があるランタイム(Cloudflare Workers、Deno、Bun など)。

短い例 ​

ts
import { Hibiki } from "@hibiki-js/core"
import { stripe } from "@hibiki-js/stripe"

const app = new Hibiki()
  .use(stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }))

app.on("stripe.invoice.paid", async ({ event }) => {
  console.log(event.data.object.id)
})

export async function POST(request: Request) {
  return app.handle(request, { provider: "stripe" })
}

Hibiki ​

ts
import { Hibiki } from "@hibiki-js/core"

const app = new Hibiki(options?)

Hibiki は型状態付きの不変ビルダーです。use() は新しいインスタンスを返し、元のインスタンスは変わりません。プロバイダー登録後の on() は、登録済みプロバイダー上のイベント名だけを受け付けます。

オプション ​

オプション型デフォルト説明
strictEventsbooleanfalsetrue のとき、未対応イベントを 200 ではなく 400(HIBIKI_UNSUPPORTED_EVENT)で返す
maxBodyBytesnumber1048576署名検証前に読み込む本文の上限バイト数。正の安全な整数を指定
idempotencyStoreIdempotencyStore件数制限付きのメモリ保存先重複判定用IDを確保する保存先。複数インスタンスでは永続化した共有保存先を指定

保存先の仕様、再送防止の限界、再試行時の動作はWebhookを安全に処理するを参照してください。

use(provider) ​

プロバイダーを登録し、型レジストリを更新した新しい Hibiki を返します。

ts
const app = new Hibiki()
  .use(stripe({ secret }))
  .use(github({ secret: githubSecret }))
  • 同じ provider.name を二重登録すると HIBIKI_PROVIDER_NOT_REGISTERED(status 500)を投げる
  • 直前のビルダーは変更しない

on(eventName, handler) ​

stripe.invoice.paid や github.pull_request.opened のような型付きイベントに、ハンドラをちょうど 1 つ登録します。

ts
app.on("stripe.checkout.session.completed", async (context) => {
  // context.event はその Stripe ペイロードに絞り込まれる
  console.log(context.event.data.object.id)
})

ハンドラのシグネチャ:

ts
(context: HibikiContext) => Promise<Response | void> | Response | void
  • this を返すので登録をチェーンできる
  • 同じイベントに再度登録すると HIBIKI_DUPLICATE_HANDLER(status 500)
  • Response を返した場合はそのまま応答する
  • void / undefined の場合は 204

useMiddleware(middleware) ​

登録済みハンドラの実行だけを包むミドルウェアを追加します。署名失敗、未対応 Content-Type、パース失敗、未対応イベントでは動きません。

ts
app.useMiddleware(async (context, next) => {
  const start = performance.now()
  await next()
  console.log(context.provider, context.id, performance.now() - start)
})

シグネチャ:

ts
(context: HibikiContext, next: () => Promise<void>) => Promise<void> | void

登録順に実行されます。通常の応答にするなら、意図的に打ち切る場合を除き await next() してください。

handle(request, options) ​

Webhook の Request を検証・パース・振り分けし、Response を返します。

ts
await app.handle(request, {
  provider: "stripe",
  waitUntil?: (promise: Promise<unknown>) => void,
})
オプション必須説明
providerはい登録済みプロバイダー名("stripe"、"github" など)
waitUntilいいえ任意のバックグラウンドスケジューラ(Cloudflare Workers の ctx.waitUntil など)。渡すと HibikiContext に載る

毎回の処理順:

  1. 名前付きプロバイダーを解決する
  2. クローンした Request から生 body を読む
  3. provider.verify(...)
  4. プロバイダーが contentTypes を持つ場合は検査する
  5. provider.parse(...)
  6. 対応ハンドラへ渡す(ミドルウェア込み)。未対応・未登録なら早期に返す

既定の HTTP 応答 ​

状況ステータスBody
対応イベントでハンドラが void204空
対応イベントだがハンドラ未登録204空
未対応イベント(strictEvents: false)200空
未対応イベント(strictEvents: true)400HIBIKI_UNSUPPORTED_EVENT
ハンドラが Response を返すそのステータスその body
HibikiErrorerror.statuserror.code(プレーンテキスト)
HibikiError 以外の予期しない throw500Internal webhook error

未対応イベントへの 200 は意図的です。まだ扱っていないイベント種別で、Stripe / GitHub などが無限に再送しないようにするためです。

HibikiContext ​

ハンドラとミドルウェアに渡されます。

フィールド型説明
event型付きペイロードこのハンドラ向けにパース済みのイベント
providerstringプロバイダー名("stripe" など)
requestRequest元のリクエスト
headersHeadersrequest.headers と同じ
rawBodystringパースに使った UTF-8 の生 body
idstring?プロバイダーが渡す任意のイベント ID(Stripe は event.id)
waitUntil((promise: Promise<unknown>) => void)?handle に waitUntil を渡したときだけ存在

defineProvider / HibikiProvider ​

独自プロバイダーを出すときは defineProvider を使います。型付きの identity ヘルパーで、実行時の特別な処理はありません。

ts
import { defineProvider, HibikiError, type ParseResult } from "@hibiki-js/core"

interface Events {
  ping: { ok: true }
}

export const example = defineProvider<"example", Events>({
  name: "example",
  contentTypes: ["application/json"],
  async verify({ headers }) {
    if (!headers.get("x-example-signature")) {
      throw new HibikiError("HIBIKI_VERIFICATION_FAILED", "Missing signature", 400)
    }
  },
  async parse(): Promise<ParseResult<Events>> {
    return { kind: "supported", eventName: "ping", event: { ok: true } }
  },
})

HibikiProvider のメンバー ​

メンバー説明
name一意なプロバイダー ID。イベント名の接頭辞(name.event)になる
contentTypes?検証後に許可するメディアタイプ。; より前を小文字比較する
verify(input)署名・正当性チェック。失敗時は HibikiError を throw
parse(input){ kind: "supported", eventName, event, id? } または { kind: "unsupported", nativeEventName, id? } を返す

VerifyInput / ParseInput ​

フィールド対象説明
rawBody両方生のリクエストバイト(Uint8Array)
headers両方リクエストヘッダー
request両方元の Request
textparse のみUTF-8 デコード済み body

HibikiError ​

ts
class HibikiError extends Error {
  readonly code: HibikiErrorCode
  readonly status: number
}

HTTP 応答の body は error.code だけです。呼び出し側に見える想定のメッセージに秘密情報やペイロード詳細を載せないでください。返るのはコード文字列です。

コード典型的な statusタイミング
HIBIKI_VERIFICATION_FAILED400署名・正当性チェック失敗
HIBIKI_PARSE_FAILED400プロバイダーの parse が HibikiError 以外を throw
HIBIKI_UNSUPPORTED_EVENT400strictEvents: true で未対応イベント
HIBIKI_UNSUPPORTED_CONTENT_TYPE415Content-Type が provider.contentTypes にない
HIBIKI_PROVIDER_NOT_REGISTERED500不明なプロバイダー名、または use() の二重登録
HIBIKI_DUPLICATE_HANDLER500同じイベントへの 2 回目の on()
HIBIKI_HANDLER_FAILED500ハンドラまたはミドルウェアが throw

よく使う型 ​

型役割
EventMapプロバイダーのイベントマップ制約
ParseResult<TEvents> / SupportedParseResult<TEvents>parse の戻り値型
ProviderRegistry登録済みプロバイダーのマップ
RegisteredEventName<R>レジストリ R 向けの "provider.event" ユニオン
HibikiOptions / HandleOptions<R>コンストラクタ / handle のオプション型

関連 ​

セキュリティ関連のエラーコード ​

コードステータス
HIBIKI_PAYLOAD_TOO_LARGE413
HIBIKI_EVENT_IN_PROGRESS503
HIBIKI_IDEMPOTENCY_UNAVAILABLE503