-
Notifications
You must be signed in to change notification settings - Fork 5
Add framework examples and tighten Express raw-body handling #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Prateek32177
wants to merge
1
commit into
main
Choose a base branch
from
codex/add-framework-examples-and-setup-guide
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Tern Framework Examples | ||
|
|
||
| All examples are standalone packages and depend on the local repo build via `"@hookflo/tern": "../../"`. | ||
|
|
||
| ## Setup handler with Tern CLI (recommended) | ||
|
|
||
| ```bash | ||
| npx @hookflo/tern-cli | ||
| ``` | ||
|
|
||
| Then choose framework + Stripe platform, set `STRIPE_WEBHOOK_SECRET`, and use generated file path for your framework. | ||
|
|
||
| ## Quick starts | ||
|
|
||
| - **Hono**: `cd examples/hono && npm install && STRIPE_WEBHOOK_SECRET=whsec_xxx npm run dev` | ||
| - **Next.js**: `cd examples/nextjs && npm install && STRIPE_WEBHOOK_SECRET=whsec_xxx npm run dev` | ||
| - **Cloudflare Workers**: `cd examples/cloudflare-workers && npm install && STRIPE_WEBHOOK_SECRET=whsec_xxx npm run dev` | ||
| - **Express**: `cd examples/express && npm install && STRIPE_WEBHOOK_SECRET=whsec_xxx npm run dev` | ||
|
|
||
| ## Stripe CLI testing | ||
|
|
||
| ```bash | ||
| stripe listen --forward-to localhost:3000/webhooks/stripe | ||
| stripe trigger payment_intent.succeeded | ||
| ``` | ||
|
|
||
| Use the printed `whsec_...` secret as `STRIPE_WEBHOOK_SECRET`. | ||
|
|
||
| See per-example READMEs for deployment notes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Tern + Cloudflare Workers Example | ||
|
|
||
| ## Setup handler with Tern CLI | ||
|
|
||
| ```bash | ||
| npx @hookflo/tern-cli | ||
| ``` | ||
|
|
||
| Choose: | ||
| - Framework: `Cloudflare Workers` | ||
| - Platform: `Stripe` | ||
| - Env var: `STRIPE_WEBHOOK_SECRET` | ||
| - Handler path: `examples/cloudflare-workers/src/index.ts` | ||
|
|
||
| ## Manual setup | ||
|
|
||
| ```bash | ||
| cd examples/cloudflare-workers | ||
| npm install | ||
| STRIPE_WEBHOOK_SECRET=whsec_xxx npm run dev | ||
| ``` | ||
|
|
||
| Endpoint: `POST http://localhost:8787/webhooks/stripe` | ||
|
|
||
| ## Local test with Stripe CLI | ||
|
|
||
| ```bash | ||
| stripe listen --forward-to localhost:8787/webhooks/stripe | ||
| stripe trigger payment_intent.succeeded | ||
| ``` | ||
|
|
||
| ## Deploy | ||
|
|
||
| ```bash | ||
| cd examples/cloudflare-workers | ||
| wrangler secret put STRIPE_WEBHOOK_SECRET | ||
| wrangler deploy | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "name": "tern-example-cloudflare-workers", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "scripts": { | ||
| "dev": "wrangler dev", | ||
| "deploy": "wrangler deploy" | ||
| }, | ||
| "dependencies": { | ||
| "@hookflo/tern": "../../" | ||
| }, | ||
| "devDependencies": { | ||
| "@cloudflare/workers-types": "^4.20250430.0", | ||
| "typescript": "^5.8.3", | ||
| "wrangler": "^4.13.2" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { WebhookVerificationService } from '@hookflo/tern'; | ||
|
|
||
| interface Env { | ||
| STRIPE_WEBHOOK_SECRET: string; | ||
| } | ||
|
|
||
| export default { | ||
| async fetch(request: Request, env: Env): Promise<Response> { | ||
| const url = new URL(request.url); | ||
|
|
||
| if (url.pathname === '/' && request.method === 'GET') { | ||
| return Response.json({ status: 'ok', framework: 'cloudflare-workers' }); | ||
| } | ||
|
|
||
| if (url.pathname === '/webhooks/stripe' && request.method === 'POST') { | ||
| // Workers use the standard Web API Request — tern works natively. | ||
| const result = await WebhookVerificationService.verifyWithPlatformConfig( | ||
| request, | ||
| 'stripe', | ||
| env.STRIPE_WEBHOOK_SECRET, | ||
| ); | ||
|
|
||
| if (!result.isValid) { | ||
| console.error('❌ Verification failed:', result.error); | ||
| return Response.json({ error: result.error }, { status: 400 }); | ||
| } | ||
|
|
||
| console.log('✅ Verified. Event type:', result.payload?.type); | ||
| return Response.json({ received: true }); | ||
| } | ||
|
|
||
| return Response.json({ error: 'Not found' }, { status: 404 }); | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2021", | ||
| "module": "ESNext", | ||
| "moduleResolution": "Bundler", | ||
| "lib": ["ES2021"], | ||
| "types": ["@cloudflare/workers-types"], | ||
| "strict": true, | ||
| "noEmit": true | ||
| }, | ||
| "include": ["src"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| name = "tern-example-workers" | ||
| main = "src/index.ts" | ||
| compatibility_date = "2024-01-01" | ||
|
|
||
| # Set your real secret with: wrangler secret put STRIPE_WEBHOOK_SECRET | ||
| [vars] | ||
| STRIPE_WEBHOOK_SECRET = "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Tern + Express Example | ||
|
|
||
| ## Setup handler with Tern CLI | ||
|
|
||
| ```bash | ||
| npx @hookflo/tern-cli | ||
| ``` | ||
|
|
||
| Choose: | ||
| - Framework: `Express` | ||
| - Platform: `Stripe` | ||
| - Env var: `STRIPE_WEBHOOK_SECRET` | ||
| - Handler path: `examples/express/src/index.ts` | ||
|
|
||
| ## Manual setup | ||
|
|
||
| ```bash | ||
| cd examples/express | ||
| npm install | ||
| STRIPE_WEBHOOK_SECRET=whsec_xxx npm run dev | ||
| ``` | ||
|
|
||
| Endpoint: `POST http://localhost:3002/webhooks/stripe` | ||
|
|
||
| ## Local test with Stripe CLI | ||
|
|
||
| ```bash | ||
| stripe listen --forward-to localhost:3002/webhooks/stripe | ||
| stripe trigger payment_intent.succeeded | ||
| ``` | ||
|
|
||
| ## Deploy notes | ||
|
|
||
| - Set `STRIPE_WEBHOOK_SECRET` in your host env vars. | ||
| - Keep webhook route registration before `app.use(express.json())`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "tern-example-express", | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "tsx watch src/index.ts", | ||
| "build": "tsc", | ||
| "start": "node dist/index.js" | ||
| }, | ||
| "dependencies": { | ||
| "@hookflo/tern": "../../", | ||
| "express": "^4.21.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/express": "^5.0.1", | ||
| "tsx": "^4.19.3", | ||
| "typescript": "^5.8.3" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import express from 'express'; | ||
| import { createWebhookMiddleware } from '@hookflo/tern/express'; | ||
|
|
||
| const app = express(); | ||
|
|
||
| // ─── CRITICAL ORDER ─────────────────────────────────────────────────────────── | ||
| // Register the webhook route BEFORE app.use(express.json()). | ||
| // If express.json() runs first it consumes the raw body and signature | ||
| // verification will always fail — tern cannot recover the original bytes. | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| app.get('/', (_req, res) => { | ||
| res.json({ status: 'ok', framework: 'express' }); | ||
| }); | ||
|
|
||
| // Recommended: use the tern Express adapter. | ||
| // It applies express.raw() semantics internally by reading the raw stream/body | ||
| // and attaches the verified payload to req.webhook before calling next(). | ||
| app.post( | ||
| '/webhooks/stripe', | ||
| createWebhookMiddleware({ | ||
| platform: 'stripe', | ||
| secret: process.env.STRIPE_WEBHOOK_SECRET ?? '', | ||
| }), | ||
| (req, res) => { | ||
| const event = (req as any).webhook?.payload; | ||
| console.log('✅ Verified. Event type:', event?.type); | ||
| res.json({ received: true }); | ||
| }, | ||
| ); | ||
|
|
||
| // ── Alternative (no adapter) ────────────────────────────────────────────────── | ||
| // If you want to use WebhookVerificationService directly, reconstruct a | ||
| // Web API Request from the raw buffer that express.raw() gives you: | ||
| // | ||
| // import { WebhookVerificationService } from '@hookflo/tern' | ||
| // | ||
| // app.post( | ||
| // '/webhooks/stripe/raw', | ||
| // express.raw({ type: '*/*' }), // <-- must come before express.json() | ||
| // async (req, res) => { | ||
| // const webRequest = new Request('https://example.com/webhooks/stripe', { | ||
| // method: 'POST', | ||
| // headers: req.headers as Record<string, string>, | ||
| // body: req.body, // Buffer from express.raw() | ||
| // }) | ||
| // const result = await WebhookVerificationService.verifyWithPlatformConfig( | ||
| // webRequest, | ||
| // 'stripe', | ||
| // process.env.STRIPE_WEBHOOK_SECRET ?? '' | ||
| // ) | ||
| // if (!result.isValid) return res.status(400).json({ error: result.error }) | ||
| // res.json({ received: true }) | ||
| // } | ||
| // ) | ||
| // ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| // Global JSON parser — AFTER the webhook route | ||
| app.use(express.json()); | ||
|
|
||
| app.get('/health', (_req, res) => res.json({ ok: true })); | ||
|
|
||
| const port = Number(process.env.PORT ?? 3002); | ||
| app.listen(port, () => | ||
| console.log(`🚀 Express running at http://localhost:${port}`), | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "module": "NodeNext", | ||
| "moduleResolution": "NodeNext", | ||
| "strict": true, | ||
| "outDir": "dist", | ||
| "rootDir": "src" | ||
| }, | ||
| "include": ["src"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Tern + Hono Example | ||
|
|
||
| ## Setup handler with Tern CLI | ||
|
|
||
| ```bash | ||
| npx @hookflo/tern-cli | ||
| ``` | ||
|
|
||
| Choose: | ||
| - Framework: `Hono` | ||
| - Platform: `Stripe` | ||
| - Env var: `STRIPE_WEBHOOK_SECRET` | ||
| - Handler path: `examples/hono/src/index.ts` | ||
|
|
||
| ## Manual setup | ||
|
|
||
| ```bash | ||
| cd examples/hono | ||
| npm install | ||
| STRIPE_WEBHOOK_SECRET=whsec_xxx npm run dev | ||
| ``` | ||
|
|
||
| Endpoint: `POST http://localhost:3000/webhooks/stripe` | ||
|
|
||
| ## Local test with Stripe CLI | ||
|
|
||
| ```bash | ||
| stripe listen --forward-to localhost:3000/webhooks/stripe | ||
| stripe trigger payment_intent.succeeded | ||
| ``` | ||
|
|
||
| ## Deploy notes | ||
|
|
||
| - This example runs on Node via `@hono/node-server`. | ||
| - Deploy to any Node host (Railway, Render, Fly.io, etc.) and set `STRIPE_WEBHOOK_SECRET`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "tern-example-hono", | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "tsx watch src/index.ts", | ||
| "build": "tsc", | ||
| "start": "node dist/index.js" | ||
| }, | ||
| "dependencies": { | ||
| "@hookflo/tern": "../../", | ||
| "@hono/node-server": "^1.13.7", | ||
| "hono": "^4.7.10" | ||
| }, | ||
| "devDependencies": { | ||
| "tsx": "^4.19.3", | ||
| "typescript": "^5.8.3" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { serve } from '@hono/node-server'; | ||
| import { Hono } from 'hono'; | ||
| import { WebhookVerificationService } from '@hookflo/tern'; | ||
|
|
||
| const app = new Hono(); | ||
|
|
||
| app.get('/', (c) => c.json({ status: 'ok', framework: 'hono' })); | ||
|
|
||
| app.post('/webhooks/stripe', async (c) => { | ||
| // IMPORTANT: pass c.req.raw — the native Web API Request object. | ||
| // Never call c.req.json() or c.req.text() before this; it consumes the stream. | ||
| const result = await WebhookVerificationService.verifyWithPlatformConfig( | ||
| c.req.raw, | ||
| 'stripe', | ||
| process.env.STRIPE_WEBHOOK_SECRET ?? '', | ||
| ); | ||
|
|
||
| if (!result.isValid) { | ||
| console.error('❌ Verification failed:', result.error); | ||
| return c.json({ error: result.error }, 400); | ||
| } | ||
|
|
||
| console.log('✅ Verified. Event type:', result.payload?.type); | ||
| return c.json({ received: true }); | ||
| }); | ||
|
|
||
| const port = Number(process.env.PORT ?? 3000); | ||
| serve({ fetch: app.fetch, port }, () => | ||
| console.log(`🚀 Hono running at http://localhost:${port}`), | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "module": "NodeNext", | ||
| "moduleResolution": "NodeNext", | ||
| "strict": true, | ||
| "outDir": "dist", | ||
| "rootDir": "src" | ||
| }, | ||
| "include": ["src"] | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Missing rate limiting High
Copilot Autofix
AI 7 days ago
To fix the problem, introduce an Express rate‑limiting middleware and apply it to the webhook route (and optionally to other routes) so that expensive verification work in
createWebhookMiddlewarecannot be abused by unbounded request rates. The fix should not alter the existing behavior of the route for legitimate traffic, just bound the number of requests per time window.The best way to do this in this context is to use the widely‑used
express-rate-limitpackage. Inexamples/express/src/index.ts, we will:rateLimitfromexpress-rate-limit.stripeWebhookLimiterconfigured with a reasonablewindowMsandmaxrequests.stripeWebhookLimiteras middleware in theapp.post('/webhooks/stripe', ...)call, beforecreateWebhookMiddleware(...), ensuring that abusive traffic is rejected before the verification work runs.express.json()).Concretely:
expressimport.const app = express();.app.post('/webhooks/stripe', ...)definition to include the limiter as an additional argument between the route path andcreateWebhookMiddleware({ ... }).