diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0f85b4f..58433c0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,11 +29,40 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 9
- - run: pnpm install
- - run: pnpm -r test run -- --environment node
- - run: pnpm -r test run -- --environment edge-runtime
- - run: pnpm -r build # only necessary for miniflare
- - run: pnpm -r test run -- --environment miniflare
+ - name: Install dependencies
+ run: pnpm install
+ - name: Run tests
+ run: |
+ pnpm -r build # only necessary for miniflare
+ pnpm -r test run -- --environment node
+ pnpm -r test run -- --environment edge-runtime
+ pnpm -r test run -- --environment miniflare
+
+ test-nextjs:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ version: [13, 14, 15]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ - uses: pnpm/action-setup@v3
+ with:
+ version: 9
+ - name: Install dependencies
+ run: pnpm install
+ - name: Install Next.js version
+ working-directory: packages/nextjs
+ run: pnpm install next@${{ matrix.version }}
+ - name: Run tests
+ working-directory: packages/nextjs
+ run: |
+ pnpm build # only necessary for miniflare
+ pnpm test run -- --environment node
+ pnpm test run -- --environment edge-runtime
+ pnpm test run -- --environment miniflare
build:
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 90d0353..6d69eb4 100644
--- a/README.md
+++ b/README.md
@@ -60,8 +60,10 @@ Now, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be reject
import { headers } from 'next/headers';
-export default function Page() {
- const csrfToken = headers().get('X-CSRF-Token') || 'missing';
+export default async function Page() {
+ // NOTE: headers() don't need to be awaited in Next14
+ const h = await headers();
+ const csrfToken = h.get('X-CSRF-Token') || 'missing';
return (
+
+
+
+
+ HTML File Upload Example:
+
+
+
+
+
+ >
+ );
+}
diff --git a/examples/next15-approuter-html-submission/middleware.ts b/examples/next15-approuter-html-submission/middleware.ts
new file mode 100644
index 0000000..cda3d96
--- /dev/null
+++ b/examples/next15-approuter-html-submission/middleware.ts
@@ -0,0 +1,10 @@
+import { createCsrfMiddleware } from '@edge-csrf/nextjs';
+
+// initalize csrf protection middleware
+const csrfMiddleware = createCsrfMiddleware({
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+export const middleware = csrfMiddleware;
diff --git a/examples/next15-approuter-html-submission/next-env.d.ts b/examples/next15-approuter-html-submission/next-env.d.ts
new file mode 100644
index 0000000..40c3d68
--- /dev/null
+++ b/examples/next15-approuter-html-submission/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/examples/next15-approuter-html-submission/next.config.js b/examples/next15-approuter-html-submission/next.config.js
new file mode 100644
index 0000000..1b7b80e
--- /dev/null
+++ b/examples/next15-approuter-html-submission/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+}
+
+module.exports = nextConfig;
diff --git a/examples/next15-approuter-html-submission/package.json b/examples/next15-approuter-html-submission/package.json
new file mode 100644
index 0000000..c24252f
--- /dev/null
+++ b/examples/next15-approuter-html-submission/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "edge-csrf-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@edge-csrf/nextjs": "^2.0.0",
+ "@types/node": "^20.8.9",
+ "@types/react": "^18.2.33",
+ "@types/react-dom": "^18.2.14",
+ "eslint": "^8.52.0",
+ "eslint-config-next": "^15.0.0",
+ "next": "^15.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/examples/next15-approuter-html-submission/public/favicon.ico b/examples/next15-approuter-html-submission/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next15-approuter-html-submission/public/favicon.ico differ
diff --git a/examples/next15-approuter-html-submission/public/vercel.svg b/examples/next15-approuter-html-submission/public/vercel.svg
new file mode 100644
index 0000000..fbf0e25
--- /dev/null
+++ b/examples/next15-approuter-html-submission/public/vercel.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/examples/next15-approuter-html-submission/styles/globals.css b/examples/next15-approuter-html-submission/styles/globals.css
new file mode 100644
index 0000000..35b366a
--- /dev/null
+++ b/examples/next15-approuter-html-submission/styles/globals.css
@@ -0,0 +1,26 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+ color: blue;
+ text-decoration: underline;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+ body {
+ color: white;
+ background: black;
+ }
+}
diff --git a/examples/next15-approuter-html-submission/tsconfig.json b/examples/next15-approuter-html-submission/tsconfig.json
new file mode 100644
index 0000000..046d7b1
--- /dev/null
+++ b/examples/next15-approuter-html-submission/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "target": "ES2017",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/examples/next15-approuter-js-submission-dynamic/.eslintrc.json b/examples/next15-approuter-js-submission-dynamic/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/examples/next15-approuter-js-submission-dynamic/README.md b/examples/next15-approuter-js-submission-dynamic/README.md
new file mode 100644
index 0000000..54b0aff
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/README.md
@@ -0,0 +1,26 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, install dependencies:
+
+```bash
+npm install
+# or
+pnpm install
+# or
+yarn install
+```
+
+Next, run the development server:
+
+```bash
+npm run dev
+# or
+pnpm dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
diff --git a/examples/next15-approuter-js-submission-dynamic/app/form-handler/route.ts b/examples/next15-approuter-js-submission-dynamic/app/form-handler/route.ts
new file mode 100644
index 0000000..0f58232
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/app/form-handler/route.ts
@@ -0,0 +1,5 @@
+import { NextResponse } from 'next/server';
+
+export async function POST() {
+ return NextResponse.json({ status: 'success' });
+}
diff --git a/examples/next15-approuter-js-submission-dynamic/app/layout.tsx b/examples/next15-approuter-js-submission-dynamic/app/layout.tsx
new file mode 100644
index 0000000..79ebc73
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/app/layout.tsx
@@ -0,0 +1,28 @@
+import { Metadata } from 'next';
+import { headers } from 'next/headers';
+
+export async function generateMetadata(): Promise {
+ const headersList = await headers();
+ const csrfToken = headersList.get('X-CSRF-Token') || 'missing';
+
+ return {
+ title: 'edge-csrf example',
+ other: {
+ 'x-csrf-token': csrfToken,
+ },
+ };
+}
+
+export default function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/examples/next15-approuter-js-submission-dynamic/app/page.tsx b/examples/next15-approuter-js-submission-dynamic/app/page.tsx
new file mode 100644
index 0000000..fdfd706
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/app/page.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+import '../styles/globals.css';
+
+export default function Page() {
+ const [csrfToken, setCsrfToken] = useState('loading...');
+
+ useEffect(() => {
+ const el = document.querySelector('meta[name="x-csrf-token"]') as HTMLMetaElement | null;
+ if (el) setCsrfToken(el.content);
+ else setCsrfToken('missing');
+ }, []);
+
+ // method to generate form handlers
+ const onSubmit = (tokenVal: string | null): React.FormEventHandler => (
+ async (event: React.FormEvent) => {
+ event.preventDefault();
+
+ // get form values
+ const data = new FormData(event.currentTarget);
+
+ // build fetch args
+ const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) };
+ if (tokenVal != null) fetchArgs.headers = { 'X-CSRF-Token': tokenVal };
+
+ // send to backend
+ const response = await fetch('/form-handler', fetchArgs);
+
+ // show response
+ // eslint-disable-next-line no-alert
+ alert(response.statusText);
+ }
+ );
+
+ return (
+ <>
+ JavaScript Form Submission Example:
+
+ CSRF token value:
+ {csrfToken}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/examples/next15-approuter-js-submission-dynamic/middleware.ts b/examples/next15-approuter-js-submission-dynamic/middleware.ts
new file mode 100644
index 0000000..cda3d96
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/middleware.ts
@@ -0,0 +1,10 @@
+import { createCsrfMiddleware } from '@edge-csrf/nextjs';
+
+// initalize csrf protection middleware
+const csrfMiddleware = createCsrfMiddleware({
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+export const middleware = csrfMiddleware;
diff --git a/examples/next15-approuter-js-submission-dynamic/next-env.d.ts b/examples/next15-approuter-js-submission-dynamic/next-env.d.ts
new file mode 100644
index 0000000..40c3d68
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/examples/next15-approuter-js-submission-dynamic/next.config.js b/examples/next15-approuter-js-submission-dynamic/next.config.js
new file mode 100644
index 0000000..1b7b80e
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+}
+
+module.exports = nextConfig;
diff --git a/examples/next15-approuter-js-submission-dynamic/package.json b/examples/next15-approuter-js-submission-dynamic/package.json
new file mode 100644
index 0000000..c24252f
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "edge-csrf-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@edge-csrf/nextjs": "^2.0.0",
+ "@types/node": "^20.8.9",
+ "@types/react": "^18.2.33",
+ "@types/react-dom": "^18.2.14",
+ "eslint": "^8.52.0",
+ "eslint-config-next": "^15.0.0",
+ "next": "^15.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/examples/next15-approuter-js-submission-dynamic/public/favicon.ico b/examples/next15-approuter-js-submission-dynamic/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next15-approuter-js-submission-dynamic/public/favicon.ico differ
diff --git a/examples/next15-approuter-js-submission-dynamic/public/vercel.svg b/examples/next15-approuter-js-submission-dynamic/public/vercel.svg
new file mode 100644
index 0000000..fbf0e25
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/public/vercel.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/examples/next15-approuter-js-submission-dynamic/styles/globals.css b/examples/next15-approuter-js-submission-dynamic/styles/globals.css
new file mode 100644
index 0000000..35b366a
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/styles/globals.css
@@ -0,0 +1,26 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+ color: blue;
+ text-decoration: underline;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+ body {
+ color: white;
+ background: black;
+ }
+}
diff --git a/examples/next15-approuter-js-submission-dynamic/tsconfig.json b/examples/next15-approuter-js-submission-dynamic/tsconfig.json
new file mode 100644
index 0000000..3d97fa6
--- /dev/null
+++ b/examples/next15-approuter-js-submission-dynamic/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/examples/next15-approuter-js-submission-static/.eslintrc.json b/examples/next15-approuter-js-submission-static/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/examples/next15-approuter-js-submission-static/README.md b/examples/next15-approuter-js-submission-static/README.md
new file mode 100644
index 0000000..54b0aff
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/README.md
@@ -0,0 +1,26 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, install dependencies:
+
+```bash
+npm install
+# or
+pnpm install
+# or
+yarn install
+```
+
+Next, run the development server:
+
+```bash
+npm run dev
+# or
+pnpm dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
diff --git a/examples/next15-approuter-js-submission-static/app/form-handler/route.ts b/examples/next15-approuter-js-submission-static/app/form-handler/route.ts
new file mode 100644
index 0000000..0f58232
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/app/form-handler/route.ts
@@ -0,0 +1,5 @@
+import { NextResponse } from 'next/server';
+
+export async function POST() {
+ return NextResponse.json({ status: 'success' });
+}
diff --git a/examples/next15-approuter-js-submission-static/app/layout.tsx b/examples/next15-approuter-js-submission-static/app/layout.tsx
new file mode 100644
index 0000000..abe896b
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/app/layout.tsx
@@ -0,0 +1,19 @@
+import { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'edge-csrf examples',
+};
+
+export default function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/examples/next15-approuter-js-submission-static/app/page.tsx b/examples/next15-approuter-js-submission-static/app/page.tsx
new file mode 100644
index 0000000..9d4bb77
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/app/page.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import '../styles/globals.css';
+
+export default function Page() {
+ const handleSubmit = async (ev: React.FormEvent) => {
+ // prevent default form submission
+ ev.preventDefault();
+
+ // get form values
+ const data = new FormData(ev.currentTarget);
+
+ // get token (see middleware.ts)
+ const csrfResp = await fetch('/csrf-token');
+ const { csrfToken } = await csrfResp.json();
+
+ // build fetch args
+ const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) };
+ if (csrfToken) fetchArgs.headers = { 'X-CSRF-Token': csrfToken };
+
+ // send to backend
+ const response = await fetch('/form-handler', fetchArgs);
+
+ // show response
+ // eslint-disable-next-line no-alert
+ alert(response.statusText);
+ };
+
+ return (
+ <>
+ JavaScript Form Submission Example (Static Optimized):
+
+ >
+ );
+}
diff --git a/examples/next15-approuter-js-submission-static/middleware.ts b/examples/next15-approuter-js-submission-static/middleware.ts
new file mode 100644
index 0000000..82ec7a2
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/middleware.ts
@@ -0,0 +1,28 @@
+import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+const csrfProtect = createCsrfProtect({
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+export async function middleware(request: NextRequest) {
+ const response = NextResponse.next();
+
+ // csrf protection
+ try {
+ await csrfProtect(request, response);
+ } catch (err) {
+ if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 });
+ throw err;
+ }
+
+ // return token (for use in static-optimized-example)
+ if (request.nextUrl.pathname === '/csrf-token') {
+ return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' });
+ }
+
+ return response;
+}
diff --git a/examples/next15-approuter-js-submission-static/next-env.d.ts b/examples/next15-approuter-js-submission-static/next-env.d.ts
new file mode 100644
index 0000000..40c3d68
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/examples/next15-approuter-js-submission-static/next.config.js b/examples/next15-approuter-js-submission-static/next.config.js
new file mode 100644
index 0000000..1b7b80e
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+}
+
+module.exports = nextConfig;
diff --git a/examples/next15-approuter-js-submission-static/package.json b/examples/next15-approuter-js-submission-static/package.json
new file mode 100644
index 0000000..c24252f
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "edge-csrf-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@edge-csrf/nextjs": "^2.0.0",
+ "@types/node": "^20.8.9",
+ "@types/react": "^18.2.33",
+ "@types/react-dom": "^18.2.14",
+ "eslint": "^8.52.0",
+ "eslint-config-next": "^15.0.0",
+ "next": "^15.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/examples/next15-approuter-js-submission-static/public/favicon.ico b/examples/next15-approuter-js-submission-static/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next15-approuter-js-submission-static/public/favicon.ico differ
diff --git a/examples/next15-approuter-js-submission-static/public/vercel.svg b/examples/next15-approuter-js-submission-static/public/vercel.svg
new file mode 100644
index 0000000..fbf0e25
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/public/vercel.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/examples/next15-approuter-js-submission-static/styles/globals.css b/examples/next15-approuter-js-submission-static/styles/globals.css
new file mode 100644
index 0000000..35b366a
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/styles/globals.css
@@ -0,0 +1,26 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+ color: blue;
+ text-decoration: underline;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+ body {
+ color: white;
+ background: black;
+ }
+}
diff --git a/examples/next15-approuter-js-submission-static/tsconfig.json b/examples/next15-approuter-js-submission-static/tsconfig.json
new file mode 100644
index 0000000..3d97fa6
--- /dev/null
+++ b/examples/next15-approuter-js-submission-static/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/examples/next15-approuter-sentry/.eslintrc.json b/examples/next15-approuter-sentry/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/examples/next15-approuter-sentry/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/examples/next15-approuter-sentry/.sentryclirc b/examples/next15-approuter-sentry/.sentryclirc
new file mode 100644
index 0000000..65cbcb3
--- /dev/null
+++ b/examples/next15-approuter-sentry/.sentryclirc
@@ -0,0 +1,3 @@
+
+[auth]
+token=REPLACEME
diff --git a/examples/next15-approuter-sentry/README.md b/examples/next15-approuter-sentry/README.md
new file mode 100644
index 0000000..c403366
--- /dev/null
+++ b/examples/next15-approuter-sentry/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/examples/next15-approuter-sentry/app/api/sentry-example-api/route.js b/examples/next15-approuter-sentry/app/api/sentry-example-api/route.js
new file mode 100644
index 0000000..f486f3d
--- /dev/null
+++ b/examples/next15-approuter-sentry/app/api/sentry-example-api/route.js
@@ -0,0 +1,9 @@
+import { NextResponse } from "next/server";
+
+export const dynamic = "force-dynamic";
+
+// A faulty API route to test Sentry's error monitoring
+export function GET() {
+ throw new Error("Sentry Example API Route Error");
+ return NextResponse.json({ data: "Testing Sentry Error..." });
+}
diff --git a/examples/next15-approuter-sentry/app/favicon.ico b/examples/next15-approuter-sentry/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next15-approuter-sentry/app/favicon.ico differ
diff --git a/examples/next15-approuter-sentry/app/global-error.jsx b/examples/next15-approuter-sentry/app/global-error.jsx
new file mode 100644
index 0000000..2e6130a
--- /dev/null
+++ b/examples/next15-approuter-sentry/app/global-error.jsx
@@ -0,0 +1,19 @@
+"use client";
+
+import * as Sentry from "@sentry/nextjs";
+import Error from "next/error";
+import { useEffect } from "react";
+
+export default function GlobalError({ error }) {
+ useEffect(() => {
+ Sentry.captureException(error);
+ }, [error]);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/examples/next15-approuter-sentry/app/globals.css b/examples/next15-approuter-sentry/app/globals.css
new file mode 100644
index 0000000..fd81e88
--- /dev/null
+++ b/examples/next15-approuter-sentry/app/globals.css
@@ -0,0 +1,27 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+ }
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ rgb(var(--background-end-rgb))
+ )
+ rgb(var(--background-start-rgb));
+}
diff --git a/examples/next15-approuter-sentry/app/layout.tsx b/examples/next15-approuter-sentry/app/layout.tsx
new file mode 100644
index 0000000..40e027f
--- /dev/null
+++ b/examples/next15-approuter-sentry/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from 'next'
+import { Inter } from 'next/font/google'
+import './globals.css'
+
+const inter = Inter({ subsets: ['latin'] })
+
+export const metadata: Metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/examples/next15-approuter-sentry/app/page.tsx b/examples/next15-approuter-sentry/app/page.tsx
new file mode 100644
index 0000000..e396bc7
--- /dev/null
+++ b/examples/next15-approuter-sentry/app/page.tsx
@@ -0,0 +1,113 @@
+import Image from 'next/image'
+
+export default function Home() {
+ return (
+
+
+
+ Get started by editing
+ app/page.tsx
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/examples/next15-approuter-sentry/app/sentry-example-page/page.jsx b/examples/next15-approuter-sentry/app/sentry-example-page/page.jsx
new file mode 100644
index 0000000..f8011b2
--- /dev/null
+++ b/examples/next15-approuter-sentry/app/sentry-example-page/page.jsx
@@ -0,0 +1,79 @@
+"use client";
+
+import Head from "next/head";
+import * as Sentry from "@sentry/nextjs";
+
+export default function Page() {
+ return (
+
+
+
Sentry Onboarding
+
+
+
+
+
+
+
+
+ Get started by sending us a sample error:
+
+
+
+ Next, look for the error on the{" "}
+ Issues Page.
+
+
+ For more information, see{" "}
+
+ https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+
+
+
+ );
+}
diff --git a/examples/next15-approuter-sentry/middleware.ts b/examples/next15-approuter-sentry/middleware.ts
new file mode 100644
index 0000000..82ec7a2
--- /dev/null
+++ b/examples/next15-approuter-sentry/middleware.ts
@@ -0,0 +1,28 @@
+import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+const csrfProtect = createCsrfProtect({
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+export async function middleware(request: NextRequest) {
+ const response = NextResponse.next();
+
+ // csrf protection
+ try {
+ await csrfProtect(request, response);
+ } catch (err) {
+ if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 });
+ throw err;
+ }
+
+ // return token (for use in static-optimized-example)
+ if (request.nextUrl.pathname === '/csrf-token') {
+ return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' });
+ }
+
+ return response;
+}
diff --git a/examples/next15-approuter-sentry/next-env.d.ts b/examples/next15-approuter-sentry/next-env.d.ts
new file mode 100644
index 0000000..40c3d68
--- /dev/null
+++ b/examples/next15-approuter-sentry/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/examples/next15-approuter-sentry/next.config.js b/examples/next15-approuter-sentry/next.config.js
new file mode 100644
index 0000000..30d0488
--- /dev/null
+++ b/examples/next15-approuter-sentry/next.config.js
@@ -0,0 +1,47 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {}
+
+module.exports = nextConfig
+
+
+// Injected content via Sentry wizard below
+
+const { withSentryConfig } = require("@sentry/nextjs");
+
+module.exports = withSentryConfig(
+ module.exports,
+ {
+ // For all available options, see:
+ // https://github.com/getsentry/sentry-webpack-plugin#options
+
+ // Suppresses source map uploading logs during build
+ silent: true,
+ org: "REPLACEME",
+ project: "REPLACEME",
+ },
+ {
+ // For all available options, see:
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
+
+ // Upload a larger set of source maps for prettier stack traces (increases build time)
+ widenClientFileUpload: true,
+
+ // Transpiles SDK to be compatible with IE11 (increases bundle size)
+ transpileClientSDK: true,
+
+ // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
+ tunnelRoute: "/monitoring",
+
+ // Hides source maps from generated client bundles
+ hideSourceMaps: true,
+
+ // Automatically tree-shake Sentry logger statements to reduce bundle size
+ disableLogger: true,
+
+ // Enables automatic instrumentation of Vercel Cron Monitors.
+ // See the following for more information:
+ // https://docs.sentry.io/product/crons/
+ // https://vercel.com/docs/cron-jobs
+ automaticVercelMonitors: true,
+ }
+);
diff --git a/examples/next15-approuter-sentry/package.json b/examples/next15-approuter-sentry/package.json
new file mode 100644
index 0000000..2484df9
--- /dev/null
+++ b/examples/next15-approuter-sentry/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "edge-csrf-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@edge-csrf/nextjs": "^2.0.0",
+ "@sentry/nextjs": "^7.93.0",
+ "next": "^15.0.0",
+ "react": "^18",
+ "react-dom": "^18"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10.0.1",
+ "eslint": "^8",
+ "eslint-config-next": "^15.0.0",
+ "postcss": "^8",
+ "tailwindcss": "^3.3.0",
+ "typescript": "^5"
+ }
+}
diff --git a/examples/next15-approuter-sentry/postcss.config.js b/examples/next15-approuter-sentry/postcss.config.js
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/examples/next15-approuter-sentry/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/next15-approuter-sentry/public/next.svg b/examples/next15-approuter-sentry/public/next.svg
new file mode 100644
index 0000000..5174b28
--- /dev/null
+++ b/examples/next15-approuter-sentry/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/next15-approuter-sentry/public/vercel.svg b/examples/next15-approuter-sentry/public/vercel.svg
new file mode 100644
index 0000000..d2f8422
--- /dev/null
+++ b/examples/next15-approuter-sentry/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/next15-approuter-sentry/sentry.client.config.ts b/examples/next15-approuter-sentry/sentry.client.config.ts
new file mode 100644
index 0000000..1c8a013
--- /dev/null
+++ b/examples/next15-approuter-sentry/sentry.client.config.ts
@@ -0,0 +1,51 @@
+// This file configures the initialization of Sentry on the client.
+// The config you add here will be used whenever a users loads a page in their browser.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+import type { BaseTransportOptions } from '@sentry/types';
+
+async function fetchWithCSRFHeader(input: RequestInfo | URL, init: RequestInit = {}): Promise {
+ // get csrf token (see middleware.ts)
+ const csrfResp = await fetch('/csrf-token');
+ const { csrfToken } = await csrfResp.json();
+
+ // add token to headers
+ const headers = new Headers(init.headers);
+ headers.append('X-CSRF-Token', csrfToken);
+
+ // construct init object with the updated headers
+ const modifiedInit = { ...init, headers };
+
+ // call native fetch function with the original input and the modified init object
+ return fetch(input, modifiedInit);
+}
+
+Sentry.init({
+ dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME",
+
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+
+ replaysOnErrorSampleRate: 1.0,
+
+ // This sets the sample rate to be 10%. You may want this to be 100% while
+ // in development and sample at a lower rate in production
+ replaysSessionSampleRate: 0.1,
+
+ // You can remove this option if you're not planning to use the Sentry Session Replay feature:
+ integrations: [
+ new Sentry.Replay({
+ // Additional Replay configuration goes in here, for example:
+ maskAllText: true,
+ blockAllMedia: true,
+ }),
+ ],
+
+ transport: (options: BaseTransportOptions) => {
+ return Sentry.makeFetchTransport(options, fetchWithCSRFHeader);
+ }
+});
diff --git a/examples/next15-approuter-sentry/sentry.edge.config.ts b/examples/next15-approuter-sentry/sentry.edge.config.ts
new file mode 100644
index 0000000..766031c
--- /dev/null
+++ b/examples/next15-approuter-sentry/sentry.edge.config.ts
@@ -0,0 +1,16 @@
+// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
+// The config you add here will be used whenever one of the edge features is loaded.
+// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME",
+
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+});
diff --git a/examples/next15-approuter-sentry/sentry.server.config.ts b/examples/next15-approuter-sentry/sentry.server.config.ts
new file mode 100644
index 0000000..990ba65
--- /dev/null
+++ b/examples/next15-approuter-sentry/sentry.server.config.ts
@@ -0,0 +1,15 @@
+// This file configures the initialization of Sentry on the server.
+// The config you add here will be used whenever the server handles a request.
+// https://docs.sentry.io/platforms/javascript/guides/nextjs/
+
+import * as Sentry from "@sentry/nextjs";
+
+Sentry.init({
+ dsn: "https://REPLACEME.ingest.sentry.io/REPLACEME",
+
+ // Adjust this value in production, or use tracesSampler for greater control
+ tracesSampleRate: 1,
+
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
+ debug: false,
+});
diff --git a/examples/next15-approuter-sentry/tailwind.config.ts b/examples/next15-approuter-sentry/tailwind.config.ts
new file mode 100644
index 0000000..c7ead80
--- /dev/null
+++ b/examples/next15-approuter-sentry/tailwind.config.ts
@@ -0,0 +1,20 @@
+import type { Config } from 'tailwindcss'
+
+const config: Config = {
+ content: [
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {
+ backgroundImage: {
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+ 'gradient-conic':
+ 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
+ },
+ },
+ },
+ plugins: [],
+}
+export default config
diff --git a/examples/next15-approuter-sentry/tsconfig.json b/examples/next15-approuter-sentry/tsconfig.json
new file mode 100644
index 0000000..c714696
--- /dev/null
+++ b/examples/next15-approuter-sentry/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/examples/next15-approuter-server-action-form-submission/.eslintrc.json b/examples/next15-approuter-server-action-form-submission/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/examples/next15-approuter-server-action-form-submission/README.md b/examples/next15-approuter-server-action-form-submission/README.md
new file mode 100644
index 0000000..54b0aff
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/README.md
@@ -0,0 +1,26 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, install dependencies:
+
+```bash
+npm install
+# or
+pnpm install
+# or
+yarn install
+```
+
+Next, run the development server:
+
+```bash
+npm run dev
+# or
+pnpm dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
diff --git a/examples/next15-approuter-server-action-form-submission/app/layout.tsx b/examples/next15-approuter-server-action-form-submission/app/layout.tsx
new file mode 100644
index 0000000..abe896b
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/app/layout.tsx
@@ -0,0 +1,19 @@
+import { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'edge-csrf examples',
+};
+
+export default function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/examples/next15-approuter-server-action-form-submission/app/page.tsx b/examples/next15-approuter-server-action-form-submission/app/page.tsx
new file mode 100644
index 0000000..f7358ab
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/app/page.tsx
@@ -0,0 +1,70 @@
+import { revalidatePath } from 'next/cache';
+import { headers } from 'next/headers';
+import { redirect } from 'next/navigation';
+
+import '../styles/globals.css';
+
+export default async function Page() {
+ const headersList = await headers();
+ const csrfToken = headersList.get('X-CSRF-Token') || 'missing';
+
+ async function myAction() {
+ 'use server';
+
+ // eslint-disable-next-line no-console
+ console.log('passed csrf validation');
+ revalidatePath('/');
+ redirect('/');
+ }
+
+ return (
+ <>
+
+ CSRF token value:
+ {csrfToken}
+
+ Server Action Form Submission Example:
+ NOTE: Look at browser network logs and server console for submission feedback
+ Example 1:
+
+
+
+
+
+ Example 2:
+
+
+
+
+
+ >
+ );
+}
diff --git a/examples/next15-approuter-server-action-form-submission/middleware.ts b/examples/next15-approuter-server-action-form-submission/middleware.ts
new file mode 100644
index 0000000..82ec7a2
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/middleware.ts
@@ -0,0 +1,28 @@
+import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+const csrfProtect = createCsrfProtect({
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+export async function middleware(request: NextRequest) {
+ const response = NextResponse.next();
+
+ // csrf protection
+ try {
+ await csrfProtect(request, response);
+ } catch (err) {
+ if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 });
+ throw err;
+ }
+
+ // return token (for use in static-optimized-example)
+ if (request.nextUrl.pathname === '/csrf-token') {
+ return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' });
+ }
+
+ return response;
+}
diff --git a/examples/next15-approuter-server-action-form-submission/next-env.d.ts b/examples/next15-approuter-server-action-form-submission/next-env.d.ts
new file mode 100644
index 0000000..40c3d68
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/examples/next15-approuter-server-action-form-submission/next.config.js b/examples/next15-approuter-server-action-form-submission/next.config.js
new file mode 100644
index 0000000..1b7b80e
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+}
+
+module.exports = nextConfig;
diff --git a/examples/next15-approuter-server-action-form-submission/package.json b/examples/next15-approuter-server-action-form-submission/package.json
new file mode 100644
index 0000000..c24252f
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "edge-csrf-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@edge-csrf/nextjs": "^2.0.0",
+ "@types/node": "^20.8.9",
+ "@types/react": "^18.2.33",
+ "@types/react-dom": "^18.2.14",
+ "eslint": "^8.52.0",
+ "eslint-config-next": "^15.0.0",
+ "next": "^15.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/examples/next15-approuter-server-action-form-submission/public/favicon.ico b/examples/next15-approuter-server-action-form-submission/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next15-approuter-server-action-form-submission/public/favicon.ico differ
diff --git a/examples/next15-approuter-server-action-form-submission/public/vercel.svg b/examples/next15-approuter-server-action-form-submission/public/vercel.svg
new file mode 100644
index 0000000..fbf0e25
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/public/vercel.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/examples/next15-approuter-server-action-form-submission/styles/globals.css b/examples/next15-approuter-server-action-form-submission/styles/globals.css
new file mode 100644
index 0000000..35b366a
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/styles/globals.css
@@ -0,0 +1,26 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+ color: blue;
+ text-decoration: underline;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+ body {
+ color: white;
+ background: black;
+ }
+}
diff --git a/examples/next15-approuter-server-action-form-submission/tsconfig.json b/examples/next15-approuter-server-action-form-submission/tsconfig.json
new file mode 100644
index 0000000..3d97fa6
--- /dev/null
+++ b/examples/next15-approuter-server-action-form-submission/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/.eslintrc.json b/examples/next15-approuter-server-action-non-form-submission/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/README.md b/examples/next15-approuter-server-action-non-form-submission/README.md
new file mode 100644
index 0000000..54b0aff
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/README.md
@@ -0,0 +1,26 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, install dependencies:
+
+```bash
+npm install
+# or
+pnpm install
+# or
+yarn install
+```
+
+Next, run the development server:
+
+```bash
+npm run dev
+# or
+pnpm dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
diff --git a/examples/next15-approuter-server-action-non-form-submission/app/layout.tsx b/examples/next15-approuter-server-action-non-form-submission/app/layout.tsx
new file mode 100644
index 0000000..abe896b
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/app/layout.tsx
@@ -0,0 +1,19 @@
+import { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'edge-csrf examples',
+};
+
+export default function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/app/page.tsx b/examples/next15-approuter-server-action-non-form-submission/app/page.tsx
new file mode 100644
index 0000000..9cfd2c4
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/app/page.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import { example1, example2 } from '../lib/actions';
+
+export default function Page() {
+ const handleClick1 = async () => {
+ const csrfResp = await fetch('/csrf-token');
+ const { csrfToken } = await csrfResp.json();
+
+ const data = {
+ key1: 'val1',
+ key2: 'val2',
+ };
+
+ // use token as first argument to server action
+ await example1(csrfToken, data);
+ };
+
+ const handleClick2 = async () => {
+ const csrfResp = await fetch('/csrf-token');
+ const { csrfToken } = await csrfResp.json();
+
+ // add token to FormData instance
+ const data = new FormData();
+ data.set('csrf_token', csrfToken);
+ data.set('key1', 'val1');
+ data.set('key2', 'val2');
+
+ await example2(data);
+ };
+
+ return (
+ <>
+ Server Action Non-Form Submission Examples:
+ NOTE: Look at browser network logs and server console for submission feedback
+ Example with object argument:
+
+ Example with FormData argument:
+
+ >
+ );
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/lib/actions.ts b/examples/next15-approuter-server-action-non-form-submission/lib/actions.ts
new file mode 100644
index 0000000..38bae4e
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/lib/actions.ts
@@ -0,0 +1,11 @@
+'use server';
+
+export async function example1(csrfToken: string, data: { key1: string; key2: string; }) {
+ // eslint-disable-next-line no-console
+ console.log(data);
+}
+
+export async function example2(data: FormData) {
+ // eslint-disable-next-line no-console
+ console.log(data);
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/middleware.ts b/examples/next15-approuter-server-action-non-form-submission/middleware.ts
new file mode 100644
index 0000000..82ec7a2
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/middleware.ts
@@ -0,0 +1,28 @@
+import { CsrfError, createCsrfProtect } from '@edge-csrf/nextjs';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+const csrfProtect = createCsrfProtect({
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+export async function middleware(request: NextRequest) {
+ const response = NextResponse.next();
+
+ // csrf protection
+ try {
+ await csrfProtect(request, response);
+ } catch (err) {
+ if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 });
+ throw err;
+ }
+
+ // return token (for use in static-optimized-example)
+ if (request.nextUrl.pathname === '/csrf-token') {
+ return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' });
+ }
+
+ return response;
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/next-env.d.ts b/examples/next15-approuter-server-action-non-form-submission/next-env.d.ts
new file mode 100644
index 0000000..40c3d68
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/examples/next15-approuter-server-action-non-form-submission/next.config.js b/examples/next15-approuter-server-action-non-form-submission/next.config.js
new file mode 100644
index 0000000..1b7b80e
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+}
+
+module.exports = nextConfig;
diff --git a/examples/next15-approuter-server-action-non-form-submission/package.json b/examples/next15-approuter-server-action-non-form-submission/package.json
new file mode 100644
index 0000000..c24252f
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "edge-csrf-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@edge-csrf/nextjs": "^2.0.0",
+ "@types/node": "^20.8.9",
+ "@types/react": "^18.2.33",
+ "@types/react-dom": "^18.2.14",
+ "eslint": "^8.52.0",
+ "eslint-config-next": "^15.0.0",
+ "next": "^15.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/public/favicon.ico b/examples/next15-approuter-server-action-non-form-submission/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next15-approuter-server-action-non-form-submission/public/favicon.ico differ
diff --git a/examples/next15-approuter-server-action-non-form-submission/public/vercel.svg b/examples/next15-approuter-server-action-non-form-submission/public/vercel.svg
new file mode 100644
index 0000000..fbf0e25
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/public/vercel.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/examples/next15-approuter-server-action-non-form-submission/styles/globals.css b/examples/next15-approuter-server-action-non-form-submission/styles/globals.css
new file mode 100644
index 0000000..35b366a
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/styles/globals.css
@@ -0,0 +1,26 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+ color: blue;
+ text-decoration: underline;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+ body {
+ color: white;
+ background: black;
+ }
+}
diff --git a/examples/next15-approuter-server-action-non-form-submission/tsconfig.json b/examples/next15-approuter-server-action-non-form-submission/tsconfig.json
new file mode 100644
index 0000000..3d97fa6
--- /dev/null
+++ b/examples/next15-approuter-server-action-non-form-submission/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/examples/next15-pagesrouter-html-submission/.eslintrc.json b/examples/next15-pagesrouter-html-submission/.eslintrc.json
new file mode 100644
index 0000000..5dd12e1
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/.eslintrc.json
@@ -0,0 +1,7 @@
+{
+ "extends": "next/core-web-vitals",
+ "rules": {
+ "react/function-component-definition": "off",
+ "react/jsx-props-no-spreading": "off"
+ }
+}
diff --git a/examples/next15-pagesrouter-html-submission/README.md b/examples/next15-pagesrouter-html-submission/README.md
new file mode 100644
index 0000000..3eb6496
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/README.md
@@ -0,0 +1,25 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, install dependencies:
+
+```bash
+npm install
+# or
+pnpm install
+# or
+yarn install
+```
+
+Next, run the development server:
+
+```bash
+npm run dev
+# or
+pnpm dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
diff --git a/examples/next15-pagesrouter-html-submission/middleware.ts b/examples/next15-pagesrouter-html-submission/middleware.ts
new file mode 100644
index 0000000..cda3d96
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/middleware.ts
@@ -0,0 +1,10 @@
+import { createCsrfMiddleware } from '@edge-csrf/nextjs';
+
+// initalize csrf protection middleware
+const csrfMiddleware = createCsrfMiddleware({
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ },
+});
+
+export const middleware = csrfMiddleware;
diff --git a/examples/next15-pagesrouter-html-submission/next-env.d.ts b/examples/next15-pagesrouter-html-submission/next-env.d.ts
new file mode 100644
index 0000000..a4a7b3f
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
diff --git a/examples/next15-pagesrouter-html-submission/next.config.js b/examples/next15-pagesrouter-html-submission/next.config.js
new file mode 100644
index 0000000..1b7b80e
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+}
+
+module.exports = nextConfig;
diff --git a/examples/next15-pagesrouter-html-submission/package.json b/examples/next15-pagesrouter-html-submission/package.json
new file mode 100644
index 0000000..c24252f
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "edge-csrf-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@edge-csrf/nextjs": "^2.0.0",
+ "@types/node": "^20.8.9",
+ "@types/react": "^18.2.33",
+ "@types/react-dom": "^18.2.14",
+ "eslint": "^8.52.0",
+ "eslint-config-next": "^15.0.0",
+ "next": "^15.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/examples/next15-pagesrouter-html-submission/pages/_app.tsx b/examples/next15-pagesrouter-html-submission/pages/_app.tsx
new file mode 100644
index 0000000..766bf32
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/pages/_app.tsx
@@ -0,0 +1,6 @@
+import '../styles/globals.css';
+import type { AppProps } from 'next/app';
+
+export default function App({ Component, pageProps }: AppProps) {
+ return ;
+}
diff --git a/examples/next15-pagesrouter-html-submission/pages/api/form-handler.ts b/examples/next15-pagesrouter-html-submission/pages/api/form-handler.ts
new file mode 100644
index 0000000..3fbc7b9
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/pages/api/form-handler.ts
@@ -0,0 +1,9 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+type Data = {
+ status: string;
+};
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ res.status(200).json({ status: 'success' });
+}
diff --git a/examples/next15-pagesrouter-html-submission/pages/index.tsx b/examples/next15-pagesrouter-html-submission/pages/index.tsx
new file mode 100644
index 0000000..dada7c8
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/pages/index.tsx
@@ -0,0 +1,41 @@
+import type { NextPage, GetServerSideProps } from 'next';
+
+type Props = {
+ csrfToken: string;
+};
+
+export const getServerSideProps: GetServerSideProps = async ({ res }) => {
+ const csrfToken = res.getHeader('X-CSRF-Token') || 'missing';
+ return { props: { csrfToken } };
+};
+
+const Home: NextPage = ({ csrfToken }) => (
+ <>
+
+ CSRF token value:
+ {csrfToken}
+
+ HTML Form Submission Example
+
+
+
+
+
+ >
+);
+
+export default Home;
diff --git a/examples/next15-pagesrouter-html-submission/public/favicon.ico b/examples/next15-pagesrouter-html-submission/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/examples/next15-pagesrouter-html-submission/public/favicon.ico differ
diff --git a/examples/next15-pagesrouter-html-submission/public/vercel.svg b/examples/next15-pagesrouter-html-submission/public/vercel.svg
new file mode 100644
index 0000000..fbf0e25
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/public/vercel.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/examples/next15-pagesrouter-html-submission/styles/Home.module.css b/examples/next15-pagesrouter-html-submission/styles/Home.module.css
new file mode 100644
index 0000000..bd50f42
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/styles/Home.module.css
@@ -0,0 +1,129 @@
+.container {
+ padding: 0 2rem;
+}
+
+.main {
+ min-height: 100vh;
+ padding: 4rem 0;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.footer {
+ display: flex;
+ flex: 1;
+ padding: 2rem 0;
+ border-top: 1px solid #eaeaea;
+ justify-content: center;
+ align-items: center;
+}
+
+.footer a {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-grow: 1;
+}
+
+.title a {
+ color: #0070f3;
+ text-decoration: none;
+}
+
+.title a:hover,
+.title a:focus,
+.title a:active {
+ text-decoration: underline;
+}
+
+.title {
+ margin: 0;
+ line-height: 1.15;
+ font-size: 4rem;
+}
+
+.title,
+.description {
+ text-align: center;
+}
+
+.description {
+ margin: 4rem 0;
+ line-height: 1.5;
+ font-size: 1.5rem;
+}
+
+.code {
+ background: #fafafa;
+ border-radius: 5px;
+ padding: 0.75rem;
+ font-size: 1.1rem;
+ font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
+ Bitstream Vera Sans Mono, Courier New, monospace;
+}
+
+.grid {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ max-width: 800px;
+}
+
+.card {
+ margin: 1rem;
+ padding: 1.5rem;
+ text-align: left;
+ color: inherit;
+ text-decoration: none;
+ border: 1px solid #eaeaea;
+ border-radius: 10px;
+ transition: color 0.15s ease, border-color 0.15s ease;
+ max-width: 300px;
+}
+
+.card:hover,
+.card:focus,
+.card:active {
+ color: #0070f3;
+ border-color: #0070f3;
+}
+
+.card h2 {
+ margin: 0 0 1rem 0;
+ font-size: 1.5rem;
+}
+
+.card p {
+ margin: 0;
+ font-size: 1.25rem;
+ line-height: 1.5;
+}
+
+.logo {
+ height: 1em;
+ margin-left: 0.5rem;
+}
+
+@media (max-width: 600px) {
+ .grid {
+ width: 100%;
+ flex-direction: column;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .card,
+ .footer {
+ border-color: #222;
+ }
+ .code {
+ background: #111;
+ }
+ .logo img {
+ filter: invert(1);
+ }
+}
diff --git a/examples/next15-pagesrouter-html-submission/styles/globals.css b/examples/next15-pagesrouter-html-submission/styles/globals.css
new file mode 100644
index 0000000..4f18421
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/styles/globals.css
@@ -0,0 +1,26 @@
+html,
+body {
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+ body {
+ color: white;
+ background: black;
+ }
+}
diff --git a/examples/next15-pagesrouter-html-submission/tsconfig.json b/examples/next15-pagesrouter-html-submission/tsconfig.json
new file mode 100644
index 0000000..a798452
--- /dev/null
+++ b/examples/next15-pagesrouter-html-submission/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"]
+}
diff --git a/package.json b/package.json
index c960655..b7b373a 100644
--- a/package.json
+++ b/package.json
@@ -22,5 +22,5 @@
"vitest": "^1.4.0",
"vitest-environment-miniflare": "^2.14.2"
},
- "packageManager": "pnpm@9.2.0+sha512.98a80fd11c2e7096747762304106432b3ddc67dcf54b5a8c01c93f68a2cd5e05e6821849522a06fb76284d41a2660d5e334f2ee3bbf29183bf2e739b1dafa771"
+ "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
}
diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md
index bea6867..3a3e610 100644
--- a/packages/nextjs/README.md
+++ b/packages/nextjs/README.md
@@ -1,6 +1,6 @@
# Next.js
-This is the documentation for Edge-CSRF's Next.js integration. The integration works with Next.js 13 and Next.js 14.
+This is the documentation for Edge-CSRF's Next.js integration. The integration works with Next.js 13, 14 and 15.
## Quickstart
@@ -40,8 +40,9 @@ Now, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be reject
import { headers } from 'next/headers';
-export default function Page() {
- const csrfToken = headers().get('X-CSRF-Token') || 'missing';
+export default async function Page() {
+ const h = await headers();
+ const csrfToken = h.get('X-CSRF-Token') || 'missing';
return (