Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion webhooks/nextjs-signature-verification/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
CL_SHARED_SECRET=<secret>
# ----- Commerce Layer ------
# The shared secret of the webhook that calls /api/verify
CL_SHARED_SECRET=
3 changes: 0 additions & 3 deletions webhooks/nextjs-signature-verification/.eslintrc.json

This file was deleted.

17 changes: 17 additions & 0 deletions webhooks/nextjs-signature-verification/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Dependencies
/node_modules

# Next.js
/.next/
/out/

# Misc
.DS_Store
*.pem

# Local env files
.env
.env.local

# TypeScript incremental build cache
tsconfig.tsbuildinfo
1 change: 1 addition & 0 deletions webhooks/nextjs-signature-verification/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
auto-install-peers = true
7 changes: 7 additions & 0 deletions webhooks/nextjs-signature-verification/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
.next
.cache
out
public
pnpm-lock.yaml
tsconfig.tsbuildinfo
4 changes: 4 additions & 0 deletions webhooks/nextjs-signature-verification/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"trailingComma": "none",
"printWidth": 100
}
9 changes: 9 additions & 0 deletions webhooks/nextjs-signature-verification/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions webhooks/nextjs-signature-verification/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
63 changes: 52 additions & 11 deletions webhooks/nextjs-signature-verification/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
# nextjs-signature-verification

This example shows a code implementation based on Next.js to verify [callback authenticity](https://docs.commercelayer.io/core/callbacks-security) when receiving a webhook event. The example is going to sign the payload with the shared secret (SHA256 HMAC) and compare the result with the X-CommerceLayer-Signature callback header.
This example shows a code implementation based on Next.js to verify [callback authenticity](https://docs.commercelayer.io/core/callbacks-security) when receiving a webhook event. The example signs the raw payload with the shared secret (SHA256 HMAC) and compares the result with the `X-CommerceLayer-Signature` callback header, using a constant-time comparison.

The whole verification logic lives in [`pages/api/verify.ts`](./pages/api/verify.ts).

---

## Requirements

- Node.js 22 or later
- [pnpm](https://pnpm.io)
- [ngrok](https://ngrok.com) (to expose your local server to Commerce Layer)

## Quick start guide

1. Install dependencies
1. Install dependencies:

```bash
pnpm install
```

2. Start the local server:
2. Rename the `.env.example` file to `.env.local` and add your valid secret from the webhook, like so:

```text
CL_SHARED_SECRET=your-webhook-secret
```

3. Start the local server in development mode:

```bash
pnpm dev
```

3. Start a ngrok HTTP tunnel listening for HTTP/HTTPS traffic on port 3000:
4. Start a ngrok HTTP tunnel listening for HTTP/HTTPS traffic on port 3000:

```bash
ngrok http 3000
```

4. Create a new `orders.place` webhook using the CLI or the Webhooks app inside the Hub:
5. Create a new `orders.place` webhook using the CLI or the Webhooks app inside the Hub:

```bash
cl webhooks:create \
Expand All @@ -34,12 +48,6 @@ cl webhooks:create \
-i "customer,line_items,shipping_address,billing_address,shipments.shipping_method,payment_method,payment_source,market"
```

5. Rename the .env.example file to .env.local and add your valid secret from the webhook, like so:

```text
CL_SHARED_SECRET="your-webhook-secret"
```

6. Place a new order using Commerce Layer [Demo Stores](https://github.com/commercelayer/demo-store), [Hosted Microstore](https://github.com/commercelayer/commercelayer-microstore), or the [CLI Checkout Plugin](https://github.com/commercelayer/commercelayer-cli-plugin-checkout).

```bash
Expand All @@ -56,3 +64,36 @@ or
cl checkout -S <sku-code-1> -S <sku-code-2> -m <market-id> -e <email-address>
```

## Testing it locally

You don't need a real webhook to try the endpoint out. With the server running, sign a payload yourself and call `/api/verify`:

```bash
BODY='{"data":{"id":"abc123","type":"orders"}}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "your-webhook-secret" -binary | base64)

curl -i -X POST http://localhost:3000/api/verify \
-H "Content-Type: application/json" \
-H "X-CommerceLayer-Signature: $SIGNATURE" \
-d "$BODY"
```

The endpoint answers with:

| Status | Body | When |
| ------ | -------------------------------------------------------- | ----------------------------------- |
| `200` | `{ "success": true }` | the signature matches the payload |
| `401` | `{ "success": false, "error": "Unauthorized" }` | the signature is missing or invalid |
| `405` | `{ "success": false, "error": "Method Not Allowed" }` | the request is not a `POST` |
| `500` | `{ "success": false, "error": "Internal Server Error" }` | `CL_SHARED_SECRET` is not set |

## Available scripts

| Script | Description |
| ---------------- | --------------------------------------- |
| `pnpm dev` | Starts the development server |
| `pnpm build` | Builds the app for production |
| `pnpm start` | Serves the production build |
| `pnpm typecheck` | Runs TypeScript without emitting output |
| `pnpm lint` | Lints the codebase with ESLint |
| `pnpm format` | Formats the codebase with Prettier |
27 changes: 27 additions & 0 deletions webhooks/nextjs-signature-verification/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypeScript from "eslint-config-next/typescript";
import prettier from "eslint-config-prettier/flat";

/** @type {import('eslint').Linter.Config[]} */
const config = [
{
ignores: [".next/**", "node_modules/**", "out/**", "public/**"]
},
...nextCoreWebVitals,
...nextTypeScript,

{
rules: {
// The webhook handler logs verification outcomes on purpose.
"no-console": ["warn", { allow: ["warn", "error"] }],
// `noUnusedLocals`/`noUnusedParameters` in tsconfig.json already cover this,
// and the base rule misfires on type-only declarations.
"no-unused-vars": "off"
}
},

// Must stay last: turns off every rule that conflicts with Prettier.
prettier
];

export default config;
4 changes: 3 additions & 1 deletion webhooks/nextjs-signature-verification/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/dev/types/root-params.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/** @type {import('next').NextConfig} */

const nextConfig = {};

module.exports = nextConfig;
export default nextConfig;
64 changes: 48 additions & 16 deletions webhooks/nextjs-signature-verification/package.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
{
"name": "webhooks-nextjs-signature",
"version": "0.1.0",
"author": {
"name": "Matteo Alessani",
"email": "matteo@commercelayer.io"
},
"license": "MIT",
"name": "nextjs-signature-verification",
"version": "1.0.0",
"description": "A Next.js project that verifies the authenticity of Commerce Layer webhook callbacks.",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"typecheck": "tsc --noEmit",
"format": "prettier --write .",
"lint": "eslint .",
"lint:fix": "pnpm run format && eslint . --fix"
},
"repository": {
"type": "git",
"url": "git+https://github.com/commercelayer/examples.git"
},
"author": {
"name": "Commerce Layer",
"email": "development@commercelayer.io"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/commercelayer/examples/issues"
},
"homepage": "https://github.com/commercelayer/examples/tree/main/webhooks/nextjs-signature-verification#readme",
"keywords": [
"commercelayer",
"webhooks",
"signature",
"hmac",
"nextjs",
"reactjs",
"headless",
"ecommerce",
"composable",
"api"
],
"dependencies": {
"next": "14.2.5",
"react": "18.3.1",
"react-dom": "18.3.1"
"next": "^16.3.4",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/node": "22.0.2",
"@types/react": "18.3.3",
"eslint": "9.8.0",
"typescript": "5.5.4"
"@types/node": "^24.10.1",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
"eslint": "^9.39.1",
"eslint-config-next": "^16.3.4",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.9.6",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22"
}
}
}
8 changes: 0 additions & 8 deletions webhooks/nextjs-signature-verification/pages/_app.page.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions webhooks/nextjs-signature-verification/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { AppProps } from "next/app";
import "../styles/globals.css";

function CustomApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

export default CustomApp;
Loading