Cloudflare Workersで、Stripe Webhookから送信されるイベントを処理するAPIを作成する方法

Cloudflare WorkersはV8ベースのランタイムで動作しています。

そのため、npmに公開されているライブラリの中には、Cloudflare Workersで動作しないものも存在します。

## stripe-nodeはCloudflare Workersでも利用可能

StripeのNode.js向けラリブラリである`stripe-node`は、2023年4月にCloudflare Workersへの対応を完了しました。

https://blog.cloudflare.com/ja-jp/announcing-stripe-support-in-workers-ja-jp/

Stripe Webhookを処理するAPIも、Cloudflare Workersを利用して構築できます。

この記事では、Cloudflare WorkersでStripeのWebhookイベントを検証する方法を紹介します。

## HonoでAPIを作成する

Cloudflare WorkersでAPIを作成するには、Honoを利用すると便利です。

https://hono.dev/

`npm create`でアプリをセットアップします。

```bash
$ npm create hono hono-cfw
```

どのサービスにデプロイするかを尋ねられます。

`cloudflare-workers`を選びましょう。

```bash
? Which template do you want to use? › - Use arrow-keys. Return to submit.
    aws-lambda
    bun
    cloudflare-pages
❯   cloudflare-workers
    deno
    fastly
    lagon
    lambda-edge
    netlify
  ↓ nextjs
```

テンプレートからファイルが生成されます。

ディレクトリに移動して、ライブラリをインストールしましょう。

```bash
% cd hono-cfw
% npm install
```

最後にWranglerでローカル環境にてAPIを立ち上げましょう。

```bash
% npm run dev
warning package.json: No license field
$ wrangler dev src/index.ts
 ⛅️ wrangler 3.5.1
------------------
wrangler dev now uses local mode by default, powered by 🔥 Miniflare and 👷 workerd.
To run an edge preview session for your Worker, use wrangler dev --remote
⎔ Starting local server...
[mf:inf] Ready on http://127.0.0.1:65373/
```

cURLでAPIが動作することを確認しましょう。

```bash
% curl http://127.0.0.1:65373/
Hello Hono
```

### Honoで、Webhook用のAPIを追加する

Webhook用のパスを追加しましょう。

```diff_typescript
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))
+app.post('/webhook', async (c) => {
+    console.log(await c.req.json())
+    return c.json({
+        message: 'ok'
+    })
+})

export default app
```

これで`/webhook`への`POST`リクエストを受け付けることができます。

## Stripe CLIでStripe WebhookをローカルのAPIに中継する

Stripeで発生したイベントをローカルのAPIに中継するには、Stripe CLIを使います。

```bash
% stripe listen --forward-to http://127.0.0.1:65373/webhook
```

`--forward-to`のURLは **{`npm run dev`で表示されたURL}/webhook** を指定しましょう。

Webhookのシークレットが発行されますので、コピーしましょう。

```
Your webhook signing secret is whsec_xxxxx (^C to quit)
````

### 環境変数は、`.dev.vars`ファイルへ

Wranglerを使って開発する場合、ローカルで利用する環境変数は`.dev.vars`に保存します。

先ほどコピーしたシークレットも、`.dev.vars`に保存しましょう。

**.dev.vars**
```
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
```

あわせて、Stripeのシークレットキーも保存しておきましょう。
ダッシュボード直リンク: https://dashboard.stripe.com/test/apikeys


**.dev.vars**
```
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
STRIPE_SECRET_KEY=sk_test_xxxx
```

### アプリを再起動して、環境変数を読み込み直す

環境変数を設定しましたので、`npm run dev`を実行し直しましょう。


```bash
% npm run dev
warning package.json: No license field
$ wrangler dev src/index.ts
 ⛅️ wrangler 3.5.1
------------------
wrangler dev now uses local mode by default, powered by 🔥 Miniflare and 👷 workerd.
To run an edge preview session for your Worker, use wrangler dev --remote
⎔ Starting local server...
[mf:inf] Ready on http://127.0.0.1:123456/
```

APIのURL（ポート番号）が変わりますので、`stripe listen --forward-to`も再度実行します。

:::note
Webhookのシークレットは、`stripe listen`を再起動しても変更されません。
:::


```bash
% stripe listen --forward-to http://127.0.0.1:123456/webhook
```

## 「Stripeからのリクエストか」を検証する処理を追加する

作成したWebhook APIを、Stripe Webhook以外から呼び出されないようにしましょう。

Stripeの場合、Stripe SDKを利用して検証処理を追加します。

```bash
% npm i stripe
```

続いて作成したAPIパスに、検証処理を追加しましょう。


```typescript
import { Hono } from 'hono'
import { env } from 'hono/adapter'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))
app.post('/webhook', async (c) => {
    const { STRIPE_SECRET_API_KEY, STRIPE_WEBHOOK_SECRET } = env(c)
    const stripe = new Stripe(STRIPE_SECRET_KEY, {
        apiVersion: "2023-08-16",
    });
    const signature = c.req.headers.get("stripe-signature");
    try {
        if (!signature) {
            return c.json({
                message: 'Bad Request'
            }, 400)
        }
        const body = await c.req.text();
        const event = await stripe.webhooks.constructEventAsync(
            body,
            signature,
            STRIPE_WEBHOOK_SECRET
        );
        switch(event.type) {
            case 'payment_intent.created': {
                console.log(event.data.object)
                break
            }
            default:
                break
        }
        return c.json({
            message: 'ok'
        }, 200)
      } catch (err) {
        const errorMessage = `⚠️  Webhook signature verification failed. ${err instanceof Error ? err.message : 'Internal server error'}`
        console.log(errorMessage);
        return c.json({
            message: errorMessage
        }, 400)
      }
})

export default app
```

:::note
### Cloudflare WorkersでStripe Webhookを処理する際のポイント

Webhook APIを開発されたことがある方は、Next.jsやAWS Lambda / firebaseなどで処理する際と実装が少し異なることに気づかれるかもしれません。

ドキュメントで掲載している実装と、Cloudflare Workersで動かすための実装の差分を以下にDiffとして紹介します。

https://stripe.com/docs/webhooks/quickstart

```diff_typescript
    const stripe = new Stripe(STRIPE_SECRET_KEY, {
        apiVersion: "2023-08-16",
    });
    const signature = c.req.headers.get("stripe-signature");
    try {
        if (!signature) {
            return c.json({
                message: 'Bad Request'
            }, 400)
        }
        const body = await c.req.text();
-        const event = stripe.webhooks.constructEvent(
-            request.body,
-            signature,
-            endpointSecret
-        );
+        const event = await stripe.webhooks.constructEventAsync(
+            body,
+            signature,
+            STRIPE_WEBHOOK_SECRET
+        );
        switch(event.type) {
            case 'payment_intent.created': {
                console.log(event.data.object)
                break
            }
            default:
                break
        }
        return c.json({
            message: 'ok'
        }, 200)
      } catch (err) {
        const errorMessage = `⚠️  Webhook signature verification failed. ${err instanceof Error ? err.message : 'Internal server error'}`
        console.log(errorMessage);
        return c.json({
            message: errorMessage
        }, 400)
      }
})

export default app
```

もし「Cloudflare Workers / Pagesでは、Stripe Webhookがうまく動かない・・・」というケースに遭遇された場合は、この実装方法の違いに対応できているかをご確認ください。
:::



## Stripe CLIでWebhookイベントをテストする

実際に検証やWebhookのリクエストが処理できているかを確認しましょう。

`curl`で直接APIを呼び出すと、エラーが出ます。

```bash
% curl -XPOST http://127.0.0.1:51351/webhook
{"message":"Bad Request"}
```

Stripe CLIでイベントをシミュレートしましょう。

```bash
% stripe trigger payment_intent.created
Setting up fixture for: payment_intent
Running fixture for: payment_intent
Trigger succeeded! Check dashboard for event details.
```

`npm run dev`を実行している画面にPayment Intentの情報が表示されます。

```bash
[mf:inf] Updated and ready on http://127.0.0.1:51351/
Object {
  id: pi_3NhnLTL6R1kGwUF41dAgicMq,
  object: payment_intent,
  amount: 2000,
  amount_capturable: 0,
  amount_details: Object
  ...
}
```

## StripeとCloudflareを利用したアプリを開発する際のリソース

StripeとCloudflareを組み合わせた例については、Zennにワークショップ記事をいくつか公開しています。

https://zenn.dev/stripe/articles/cloudflare-workers-and-stripe-payment-links

https://zenn.dev/stripe/books/workshop-astro-cloudflare-pages

こちらも併せてお試しください。

## More resource

https://hono.dev/snippets/stripe-webhook