0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hono + TypeScriptでStripe Webhookを型安全に実装する

0
Posted at

HonoでStripeのWebhookを受け取る場合、実装自体はそれほど難しくありません。

ただ、実際に書いてみると、

  • Webhookの署名を検証する
  • 生のRequest Bodyを扱う
  • event.typeでイベントを分岐する
  • イベントごとの型を扱う
  • テスト用のWebhook Requestを作る

といった処理が必要になります。

今回は、TypeScript向けWebhookフレームワーク Hibiki を使って、HonoでStripe Webhookを型安全に処理してみます。

ちなみにHibikiは僕が開発しているフレームワークのためステマです。
だけど実際使いやすいフレームワークだとは思うのでお付き合いください。

完成形

先に完成形を見ると、次のようになります。

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

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

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  console.log(event.data.object.id)
})

const app = new Hono()

app.post(
  "/webhooks/stripe",
  hibiki(webhooks, "stripe"),
)

export default app

Webhookの署名検証やイベントのパースはHibiki側で行われます。

stripe.checkout.session.completedを指定すると、そのイベントに対応した型としてeventを扱えます。

インストール

今回は以下の3つを使います。

npm install @hibiki-js/core @hibiki-js/stripe @hibiki-js/hono

Hono本体は@hibiki-js/honoのpeer dependencyなので、まだ入っていない場合は別途インストールします。

npm install hono

Hibikiを作成する

まず、HibikiにStripe Providerを登録します。

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

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

secretにはStripe Dashboardなどで取得したWebhook Signing Secretを指定します。

whsec_...

みたいな形のやつです。

イベントハンドラを書く

次に、受け取りたいStripeイベントを登録します。

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  console.log(event.data.object.id)
})

Hibikiでは、Provider名.イベント名でイベントを指定します。

stripe.checkout.session.completed

ここで重要なのが型推論です。

stripe.checkout.session.completedを指定すると、ハンドラ内のeventもそのイベントに対応した型として扱われます。

例えば、こんな感じにかけます。

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  const session = event.data.object

  console.log(session.id)
  console.log(session.customer)
})

イベントごとに自分で型をキャストする必要はありません。

Honoに組み込む

Honoとの接続には@hibiki-js/honoを使います。

import { Hono } from "hono"
import { hibiki } from "@hibiki-js/hono"

const app = new Hono()

app.post(
  "/webhooks/stripe",
  hibiki(webhooks, "stripe"),
)

これで、POST /webhooks/stripeに届いたRequestがHibikiへ渡されます。

全体では次のようになります。

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

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

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  console.log(event.data.object.id)
})

const app = new Hono()

app.post(
  "/webhooks/stripe",
  hibiki(webhooks, "stripe"),
)

export default app

複数イベントを処理する

複数のStripeイベントを処理したい場合は、on()を追加するだけです。

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  console.log("Checkout completed:", event.data.object.id)
})

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

webhooks.on("stripe.customer.subscription.deleted", async ({ event }) => {
  console.log("Subscription deleted:", event.data.object.id)
})

イベントごとにそれぞれ対応する型が推論されます。

そのため、大きなswitch文にすべてのイベント処理をまとめる必要がありません。

Stripe SDKは不要

HibikiのStripe Providerでは、Webhook署名検証のためにStripe SDKを利用していません。

署名検証にはWeb Crypto APIのHMAC-SHA256を利用しています。

そのため、Webhookを受信するだけであれば、stripeパッケージをruntime dependencyとして追加する必要はありません。

Hibikiの各パッケージもruntime dependency 0で作られています。

もちろん、Webhook処理の中でStripe APIを呼び出したい場合にはStripe SDKを併用できます。

例えば、こんな感じの使い方も可能です。

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  const session = event.data.object

  // ここからStripe SDKを使って別のAPIを呼ぶ
})

署名検証は自動で行われる

Webhookでは、受け取ったJSONをそのまま信用してはいけません。

Stripeから送られてきたRequestであることを確認するために、Webhook Signatureを検証する必要があります。

Hibikiでは、Webhook Secretを設定しておけば、イベントをパースする前に署名検証が行われます。

stripe({
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
})

アプリケーション側では、以下のような形で処理できます。

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  // 署名検証後のイベント
})

また、署名検証で必要になる元のRequest BodyもHibiki側で扱います。

Timestamp tolerance

Stripe Providerでは署名のTimestamp toleranceも指定できます。

例えば300秒にする場合は、以下のような形にします。

const webhooks = new Hibiki().use(
  stripe({
    secret: process.env.STRIPE_WEBHOOK_SECRET!,
    tolerance: 300,
  }),
)

未対応イベント

Webhook Endpointには、アプリケーションで使用していないイベントが送られてくることもあります。

Hibikiでは、Hibiki側で未対応のProvider Eventを受信した場合、デフォルトでは200を返します。

これによって、不要なWebhook再送が発生しにくくなっています。

未対応イベントをエラーとして扱いたい場合は、strictEvents: true を利用できます。

この場合は400が返されます。

また、Hibikiが対応しているイベントでもハンドラが登録されていなければ204を返します。

テストする

Webhookは実装だけでなく、テスト用の署名付きRequestを作るのも少し面倒です。

Hibikiには@hibiki-js/testingがあります。

npm install -D @hibiki-js/testing

例えばStripeイベントを発生させるテストは次のように書けます。

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

const app = new Hibiki().use(
  stripe({
    secret: "whsec_test",
  }),
)

app.on("stripe.checkout.session.completed", ({ event }) => {
  console.log(event.data.object.id)
})

const webhook = createWebhookTest(app, {
  secrets: {
    stripe: "whsec_test",
  },
})

await webhook.emitEvent("stripe.checkout.session.completed", {
  id: "cs_test",
  object: "checkout.session",
})

より細かくRequest HeaderやTimestampを制御したい場合は、低レベルAPIのstripeRequestも利用できます。

Cloudflare Workersとも相性が良い

Hibiki CoreはWeb StandardsのRequest / Responseをベースにしています。

Honoも複数Runtimeで動作するため、Hono + Hibikiという組み合わせをCloudflare Workersなどで利用できます。

Hibiki側がNode.js固有のRequestオブジェクトへ依存していないため、

Hono + Hibiki + Cloudflare Workers

のような構成でも同じAPIを利用できます。

Hibikiのリポジトリには、Hono + StripeのExampleも置いています。

Stripe以外のWebhookも同じAPIで扱える

Hibikiでは現在、StripeのほかにGitHub Providerも提供しています。
そのうちSlackとかDiscordにも対応しよっかなって思ってます。

例えば、以下のようにすると同じHibikiインスタンスに複数Providerを登録できます。

import { github } from "@hibiki-js/github"

const webhooks = new Hibiki()
  .use(
    stripe({
      secret: process.env.STRIPE_WEBHOOK_SECRET!,
    }),
  )
  .use(
    github({
      secret: process.env.GITHUB_WEBHOOK_SECRET!,
    }),
  )

イベント処理も同じ形になります。

webhooks.on("stripe.checkout.session.completed", async ({ event }) => {
  // Stripe
})

webhooks.on("github.issues.opened", async ({ event }) => {
  // GitHub
})

Providerごとに署名方法やPayload形式は異なりますが、その違いをProvider Package側へ閉じ込めるのがHibikiの設計です。

まとめ

HonoでStripe Webhookを扱う場合、Hibikiを使うと、

  • Stripe Webhookの署名検証
  • Event Payloadのパース
  • イベントの振り分け
  • イベントごとの型推論
  • Honoとの接続
  • Webhookのテスト

をまとめて扱えます。

最小構成なら、

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

webhooks.on("stripe.checkout.session.completed", ({ event }) => {
  console.log(event.data.object.id)
})

app.post(
  "/webhooks/stripe",
  hibiki(webhooks, "stripe"),
)

まで短くできます。

Hibiki自体はまだ公開したばかりなので、Hono + Stripeで実際に使った際のフィードバックがあればIssueなどで教えていただけると助かります。

Getting Started:

Hono:

Stripe:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?