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 (
@@ -83,6 +85,8 @@ export async function POST() { } ``` +For more Next.js examples see the [package documentation](packages/nextjs). + ## Quickstart (SvelteKit) First, install Edge-CSRF's SvelteKit integration library: @@ -231,7 +235,7 @@ app.listen(port, () => { }); ``` -With the middleware installed, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. +With the middleware installed, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. ## Quickstart (Node-HTTP) @@ -313,7 +317,7 @@ server.listen(3000, () => { }); ``` -With the CSRF protection method, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. +With the CSRF protection method, all HTTP submission requests (e.g. POST, PUT, DELETE, PATCH) will be rejected if they do not include a valid CSRF token. ## Development diff --git a/examples/next15-approuter-html-submission/.eslintrc.json b/examples/next15-approuter-html-submission/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/examples/next15-approuter-html-submission/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/next15-approuter-html-submission/README.md b/examples/next15-approuter-html-submission/README.md new file mode 100644 index 0000000..54b0aff --- /dev/null +++ b/examples/next15-approuter-html-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-html-submission/app/form-handler/route.ts b/examples/next15-approuter-html-submission/app/form-handler/route.ts new file mode 100644 index 0000000..0f58232 --- /dev/null +++ b/examples/next15-approuter-html-submission/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-html-submission/app/layout.tsx b/examples/next15-approuter-html-submission/app/layout.tsx new file mode 100644 index 0000000..fdd2cb9 --- /dev/null +++ b/examples/next15-approuter-html-submission/app/layout.tsx @@ -0,0 +1,19 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'edge-csrf html form submission example', +}; + +export default function Layout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/examples/next15-approuter-html-submission/app/page.tsx b/examples/next15-approuter-html-submission/app/page.tsx new file mode 100644 index 0000000..07d200b --- /dev/null +++ b/examples/next15-approuter-html-submission/app/page.tsx @@ -0,0 +1,57 @@ +import { headers } from 'next/headers'; + +import '../styles/globals.css'; + +export default async function Page() { + const headersList = await headers(); + const csrfToken = headersList.get('X-CSRF-Token') || 'missing'; + + return ( + <> +

+ CSRF token value: + {csrfToken} +

+

HTML Form Submission Example:

+ + Form without CSRF (should fail): + + +
+
+
+ Form with incorrect CSRF (should fail): + + + +
+
+
+ Form with CSRF (should succeed): + + + +
+

HTML File Upload Example:

+
+ Form without CSRF (should fail): + + +
+
+
+ Form with incorrect CSRF (should fail): + + + +
+
+
+ Form with CSRF (should succeed): + + + +
+ + ); +} 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} +

+
+ Form without CSRF (should fail): + + +
+
+
+ Form with incorrect CSRF (should fail): + + +
+
+
+ Form with CSRF (should succeed): + + +
+ + ); +} 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):

+
+ Form fetches CSRF token before submission (should succeed): + + +
+ + ); +} 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 +

+ +
+ +
+ Next.js Logo +
+ + +
+ ) +} 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:

+
+ Form without CSRF (should fail): + + +
+
+
+ Form with incorrect CSRF (should fail): + + + +
+
+
+ Form with CSRF (should succeed): + + + +
+

Example 2:

+
+ Form without CSRF (should fail): + + +
+
+
+ Form with incorrect CSRF (should fail): + + + +
+
+
+ Form with CSRF (should succeed): + + + +
+ + ); +} 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

+
+ Form without CSRF (should fail): + + +
+
+
+ Form with incorrect CSRF (should fail): + + + +
+
+
+ Form with CSRF (should succeed): + + + +
+ +); + +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 (
@@ -103,7 +104,7 @@ type Data = { }; export default function handler(req: NextApiRequest, res: NextApiResponse) { - // this code won't execute unless CSRF token passes validation + // this code won't execute unless CSRF token passes validation res.status(200).json({ status: 'success' }); } ``` @@ -125,6 +126,13 @@ Here are some [examples](examples) in this repository: | Next.js 14 | app router | [Server action (form)](examples/next14-approuter-server-action-form-submission) | | Next.js 14 | app router | [Server action (non-form)](examples/next14-approuter-server-action-non-form-submission) | | Next.js 14 | pages router | [HTML form](examples/next14-pagesrouter-html-submission) | +| Next.js 15 | app router | [HTML form](examples/next15-approuter-html-submission) | +| Next.js 15 | app router | [JavaScript (dynamic)](examples/next15-approuter-js-submission-dynamic) | +| Next.js 15 | app router | [JavaScript (static)](examples/next15-approuter-js-submission-static) | +| Next.js 15 | app router | [Sentry](examples/next15-approuter-sentry) | +| Next.js 15 | app router | [Server action (form)](examples/next15-approuter-server-action-form-submission) | +| Next.js 15 | app router | [Server action (non-form)](examples/next15-approuter-server-action-non-form-submission) | +| Next.js 15 | pages router | [HTML form](examples/next15-pagesrouter-html-submission) | ## Lower-level implementations @@ -154,7 +162,7 @@ export const middleware = async (request: NextRequest) => { if (err instanceof CsrfError) return new NextResponse('invalid csrf token', { status: 403 }); throw err; } - + return response; }; ``` diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index b9d9691..a110a19 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -20,10 +20,10 @@ "test-all": "vitest run --environment node && vitest run --environment edge-runtime && vitest run --environment miniflare" }, "devDependencies": { - "next": "^14.2.0" + "next": "^15.0.0" }, "peerDependencies": { - "next": "^13.0.0 || ^14.0.0" + "next": "^13.0.0 || ^14.0.0 || ^15.0.0" }, "keywords": [ "csrf", diff --git a/packages/nextjs/src/index.test.ts b/packages/nextjs/src/index.test.ts index af6add0..801d981 100644 --- a/packages/nextjs/src/index.test.ts +++ b/packages/nextjs/src/index.test.ts @@ -46,13 +46,17 @@ describe('csrfProtect unit tests', () => { const response = NextResponse.next(); request.cookies.get = vi.fn(); - response.cookies.set = vi.fn(); + + const setSpy = vi.fn(); + Object.defineProperty(response, 'cookies', { + value: { set: setSpy }, + }); const csrfProtect = createCsrfProtect(); await csrfProtect(request, response); expect(request.cookies.get).toHaveBeenCalledOnce(); - expect(response.cookies.set).toHaveBeenCalledOnce(); + expect(setSpy).toHaveBeenCalledOnce(); }); it('adds token to response header', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 648b29a..71bb849 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: packages/nextjs: devDependencies: next: - specifier: ^14.2.0 - version: 14.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^15.0.0 + version: 15.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) packages/node-http: dependencies: @@ -135,6 +135,9 @@ packages: resolution: {integrity: sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==} engines: {node: '>=16'} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} @@ -447,6 +450,111 @@ packages: '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -545,59 +653,53 @@ packages: resolution: {integrity: sha512-kpbVlznPuxNQahssQvZiNPQo/iPme7qV3WMQeg6TYNCkYD7n6vEqeFZ5E/eQgB59xCanpvw4Ci8y/+SdMK6BUg==} engines: {node: '>=16.13'} - '@next/env@14.2.2': - resolution: {integrity: sha512-sk72qRfM1Q90XZWYRoJKu/UWlTgihrASiYw/scb15u+tyzcze3bOuJ/UV6TBOQEeUaxOkRqGeuGUdiiuxc5oqw==} + '@next/env@15.0.0': + resolution: {integrity: sha512-Mcv8ZVmEgTO3bePiH/eJ7zHqQEs2gCqZ0UId2RxHmDDc7Pw6ngfSrOFlxG8XDpaex+n2G+TKPsQAf28MO+88Gw==} - '@next/swc-darwin-arm64@14.2.2': - resolution: {integrity: sha512-3iPgMhzbalizGwHNFUcGnDhFPSgVBHQ8aqSTAMxB5BvJG0oYrDf1WOJZlbXBgunOEj/8KMVbejEur/FpvFsgFQ==} + '@next/swc-darwin-arm64@15.0.0': + resolution: {integrity: sha512-Gjgs3N7cFa40a9QT9AEHnuGKq69/bvIOn0SLGDV+ordq07QOP4k1GDOVedMHEjVeqy1HBLkL8rXnNTuMZIv79A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.2': - resolution: {integrity: sha512-x7Afi/jt0ZBRUZHTi49yyej4o8znfIMHO4RvThuoc0P+uli8Jd99y5GKjxoYunPKsXL09xBXEM1+OQy2xEL0Ag==} + '@next/swc-darwin-x64@15.0.0': + resolution: {integrity: sha512-BUtTvY5u9s5berAuOEydAUlVMjnl6ZjXS+xVrMt317mglYZ2XXjY8YRDCaz9vYMjBNPXH8Gh75Cew5CMdVbWTw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.2': - resolution: {integrity: sha512-zbfPtkk7L41ODMJwSp5VbmPozPmMMQrzAc0HAUomVeVIIwlDGs/UCqLJvLNDt4jpWgc21SjjyIn762lNGrMaUA==} + '@next/swc-linux-arm64-gnu@15.0.0': + resolution: {integrity: sha512-sbCoEpuWUBpYoLSgYrk0CkBv8RFv4ZlPxbwqRHr/BWDBJppTBtF53EvsntlfzQJ9fosYX12xnS6ltxYYwsMBjg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.2': - resolution: {integrity: sha512-wPbS3pI/JU16rm3XdLvvTmlsmm1nd+sBa2ohXgBZcShX4TgOjD4R+RqHKlI1cjo/jDZKXt6OxmcU0Iys0OC/yg==} + '@next/swc-linux-arm64-musl@15.0.0': + resolution: {integrity: sha512-JAw84qfL81aQCirXKP4VkgmhiDpXJupGjt8ITUkHrOVlBd+3h5kjfPva5M0tH2F9KKSgJQHEo3F5S5tDH9h2ww==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.2': - resolution: {integrity: sha512-NqWOHqqq8iC9tuHvZxjQ2tX+jWy2X9y8NX2mcB4sj2bIccuCxbIZrU/ThFPZZPauygajZuVQ6zediejQHwZHwQ==} + '@next/swc-linux-x64-gnu@15.0.0': + resolution: {integrity: sha512-r5Smd03PfxrGKMewdRf2RVNA1CU5l2rRlvZLQYZSv7FUsXD5bKEcOZ/6/98aqRwL7diXOwD8TCWJk1NbhATQHg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.2': - resolution: {integrity: sha512-lGepHhwb9sGhCcU7999+iK1ZZT+6rrIoVg40MP7DZski9GIZP80wORSbt5kJzh9v2x2ev2lxC6VgwMQT0PcgTA==} + '@next/swc-linux-x64-musl@15.0.0': + resolution: {integrity: sha512-fM6qocafz4Xjhh79CuoQNeGPhDHGBBUbdVtgNFJOUM8Ih5ZpaDZlTvqvqsh5IoO06CGomxurEGqGz/4eR/FaMQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.2': - resolution: {integrity: sha512-TZSh/48SfcLEQ4rD25VVn2kdIgUWmMflRX3OiyPwGNXn3NiyPqhqei/BaqCYXViIQ+6QsG9R0C8LftMqy8JPMA==} + '@next/swc-win32-arm64-msvc@15.0.0': + resolution: {integrity: sha512-ZOd7c/Lz1lv7qP/KzR513XEa7QzW5/P0AH3A5eR1+Z/KmDOvMucht0AozccPc0TqhdV1xaXmC0Fdx0hoNzk6ng==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.2': - resolution: {integrity: sha512-M0tBVNMEBJN2ZNQWlcekMn6pvLria7Sa2Fai5znm7CCJz4pP3lrvlSxhKdkCerk0D9E0bqx5yAo3o2Q7RrD4gA==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-x64-msvc@14.2.2': - resolution: {integrity: sha512-a/20E/wtTJZ3Ykv3f/8F0l7TtgQa2LWHU2oNB9bsu0VjqGuGGHmm/q6waoUNQYTVPYrrlxxaHjJcDV6aiSTt/w==} + '@next/swc-win32-x64-msvc@15.0.0': + resolution: {integrity: sha512-2RVWcLtsqg4LtaoJ3j7RoKpnWHgcrz5XvuUGE7vBYU2i6M2XeD9Y8RlLaF770LEIScrrl8MdWsp6odtC6sZccg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -731,8 +833,8 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.5': - resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@swc/helpers@0.5.13': + resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} '@types/better-sqlite3@7.6.9': resolution: {integrity: sha512-FvktcujPDj9XKMJQWFcl2vVl7OdRIqsSRX9b0acWwTmwLK9CF2eqo/FRcmMLNpugKoX/avA6pb7TorDLmpgTnQ==} @@ -1097,6 +1199,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1215,6 +1324,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + devalue@4.3.3: resolution: {integrity: sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==} @@ -1657,6 +1770,9 @@ packages: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -1959,21 +2075,24 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - next@14.2.2: - resolution: {integrity: sha512-oGwUaa2bCs47FbuxWMpOoXtBMPYpvTPgdZr3UAo+pu7Ns00z9otmYpoeV1HEiYL06AlRQQIA/ypK526KjJfaxg==} - engines: {node: '>=18.17.0'} + next@15.0.0: + resolution: {integrity: sha512-/ivqF6gCShXpKwY9hfrIQYh8YMge8L3W+w1oRLv/POmK4MOQnh+FscZ8a0fRFTSQWE+2z9ctNYvELD9vP2FV+A==} + engines: {node: '>=18.18.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 + react-dom: ^18.2.0 || 19.0.0-rc-65a56d0e-20241020 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': optional: true '@playwright/test': optional: true + babel-plugin-react-compiler: + optional: true sass: optional: true @@ -2251,6 +2370,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -2273,6 +2397,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2295,6 +2423,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -2375,13 +2506,13 @@ packages: strip-literal@2.1.0: resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} - styled-jsx@5.1.1: - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' peerDependenciesMeta: '@babel/core': optional: true @@ -2737,6 +2868,11 @@ snapshots: dependencies: '@edge-runtime/primitives': 4.1.0 + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.6.2 + optional: true + '@esbuild/aix-ppc64@0.19.12': optional: true @@ -2914,6 +3050,81 @@ snapshots: '@iarna/toml@2.2.5': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3069,33 +3280,30 @@ snapshots: - bufferutil - utf-8-validate - '@next/env@14.2.2': {} - - '@next/swc-darwin-arm64@14.2.2': - optional: true + '@next/env@15.0.0': {} - '@next/swc-darwin-x64@14.2.2': + '@next/swc-darwin-arm64@15.0.0': optional: true - '@next/swc-linux-arm64-gnu@14.2.2': + '@next/swc-darwin-x64@15.0.0': optional: true - '@next/swc-linux-arm64-musl@14.2.2': + '@next/swc-linux-arm64-gnu@15.0.0': optional: true - '@next/swc-linux-x64-gnu@14.2.2': + '@next/swc-linux-arm64-musl@15.0.0': optional: true - '@next/swc-linux-x64-musl@14.2.2': + '@next/swc-linux-x64-gnu@15.0.0': optional: true - '@next/swc-win32-arm64-msvc@14.2.2': + '@next/swc-linux-x64-musl@15.0.0': optional: true - '@next/swc-win32-ia32-msvc@14.2.2': + '@next/swc-win32-arm64-msvc@15.0.0': optional: true - '@next/swc-win32-x64-msvc@14.2.2': + '@next/swc-win32-x64-msvc@15.0.0': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3208,9 +3416,8 @@ snapshots: '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.5': + '@swc/helpers@0.5.13': dependencies: - '@swc/counter': 0.1.3 tslib: 2.6.2 '@types/better-sqlite3@7.6.9': @@ -3671,6 +3878,18 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -3766,6 +3985,9 @@ snapshots: destroy@1.2.0: {} + detect-libc@2.0.3: + optional: true + devalue@4.3.3: {} dezalgo@1.0.4: @@ -4398,6 +4620,9 @@ snapshots: call-bind: 1.0.7 get-intrinsic: 1.2.4 + is-arrayish@0.3.2: + optional: true + is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 @@ -4665,27 +4890,27 @@ snapshots: negotiator@0.6.3: {} - next@14.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + next@15.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@next/env': 14.2.2 - '@swc/helpers': 0.5.5 + '@next/env': 15.0.0 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.13 busboy: 1.6.0 caniuse-lite: 1.0.30001611 - graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(react@18.2.0) + styled-jsx: 5.1.6(react@18.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.2 - '@next/swc-darwin-x64': 14.2.2 - '@next/swc-linux-arm64-gnu': 14.2.2 - '@next/swc-linux-arm64-musl': 14.2.2 - '@next/swc-linux-x64-gnu': 14.2.2 - '@next/swc-linux-x64-musl': 14.2.2 - '@next/swc-win32-arm64-msvc': 14.2.2 - '@next/swc-win32-ia32-msvc': 14.2.2 - '@next/swc-win32-x64-msvc': 14.2.2 + '@next/swc-darwin-arm64': 15.0.0 + '@next/swc-darwin-x64': 15.0.0 + '@next/swc-linux-arm64-gnu': 15.0.0 + '@next/swc-linux-arm64-musl': 15.0.0 + '@next/swc-linux-x64-gnu': 15.0.0 + '@next/swc-linux-x64-musl': 15.0.0 + '@next/swc-win32-arm64-msvc': 15.0.0 + '@next/swc-win32-x64-msvc': 15.0.0 + sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -4978,6 +5203,9 @@ snapshots: dependencies: lru-cache: 6.0.0 + semver@7.6.3: + optional: true + send@0.18.0: dependencies: debug: 2.6.9 @@ -5025,6 +5253,33 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5044,6 +5299,11 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.25 @@ -5121,7 +5381,7 @@ snapshots: dependencies: js-tokens: 9.0.0 - styled-jsx@5.1.1(react@18.2.0): + styled-jsx@5.1.6(react@18.2.0): dependencies: client-only: 0.0.1 react: 18.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ba96576..2d9e2d3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,4 @@ packages: - "packages/**" - "shared" - - "!examples/next14-approuter-html-submission/*" + - "!examples/**"