Migrate CSP middleware to Next.js proxy convention.

Renames src/middleware.ts to src/proxy.ts per Next.js 16 deprecation guidance. CSP nonce behavior is unchanged; build no longer emits the middleware warning.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-06-29 22:40:21 -06:00
parent 39ca498d88
commit 47faa1e488
7 changed files with 24 additions and 24 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ The release build now does the full packaging pipeline:
1. Generates branded icons in `src-tauri/icons/`
2. Stages a desktop-only frontend export tree that omits Next server-only
routes/middleware (`src/app/api`, `src/middleware.ts`)
routes/proxy (`src/app/api`, `src/proxy.ts`)
3. Stages a managed backend runtime bundle from `backend/` into
`src-tauri/backend-runtime/`
4. Builds the frontend export with `NEXT_OUTPUT=export`
+1 -1
View File
@@ -52,7 +52,7 @@ See [RELEASE_INPUTS.md](./RELEASE_INPUTS.md) for the plain-language answer to
1. Generates the desktop icon set in `src-tauri/icons/`
2. Stages a desktop-only frontend export tree that omits Next server-only
routes/middleware (`src/app/api`, `src/middleware.ts`)
routes/proxy (`src/app/api`, `src/proxy.ts`)
3. Stages a managed backend runtime bundle into `src-tauri/backend-runtime/`
4. Builds the frontend export with `NEXT_OUTPUT=export`
5. Copies `frontend/out` into `src-tauri/companion-www/`
@@ -17,7 +17,7 @@ const excludedPaths = [
'.next',
'out',
'src/app/api',
'src/middleware.ts',
'src/proxy.ts',
];
function normalizeRelativePath(target) {
+2 -2
View File
@@ -9,12 +9,12 @@ const skipTypecheck = process.env.NEXT_SKIP_TYPECHECK === '1';
// Desktop packaging: set NEXT_OUTPUT=export to produce a static export
// (frontend/out/) suitable for Tauri bundling and companion server hosting.
// This disables API routes, middleware, and server-side image optimization —
// This disables API routes, proxy, and server-side image optimization —
// all handled by the Tauri shell and companion server in packaged mode.
// Default remains 'standalone' for the web deployment (Docker/Vercel).
const isDesktopExport = process.env.NEXT_OUTPUT === 'export';
// CSP is now emitted dynamically by src/middleware.ts (Phase 5F-A) so that
// CSP is now emitted dynamically by src/proxy.ts (Phase 5F-A) so that
// each document response carries a unique per-request nonce. Non-CSP
// security headers remain here because they are static and benefit from
// next.config's catch-all source matcher.
@@ -5,7 +5,7 @@
* 1. Document CSP remains hydration-safe for the Next.js runtime
* 2. CSP is deterministic across repeated requests
* 3. next.config.ts no longer owns a static CSP header
* 4. Middleware does not break API/static routes (matcher exclusion)
* 4. Proxy does not break API/static routes (matcher exclusion)
* 5. Google Fonts domains are preserved in CSP
* 6. Production CSP preserves required directives
*/
@@ -13,26 +13,26 @@
import { describe, expect, it } from 'vitest';
import { NextRequest } from 'next/server';
import { middleware, config as middlewareConfig } from '@/middleware';
import { proxy, config as proxyConfig } from '@/proxy';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Call middleware with a fake document request and return the response. */
function callMiddleware(path = '/') {
/** Call proxy with a fake document request and return the response. */
function callProxy(path = '/') {
const req = new NextRequest(`http://localhost${path}`, { method: 'GET' });
return middleware(req);
return proxy(req);
}
/** Extract the CSP header string from a middleware response. */
/** Extract the CSP header string from a proxy response. */
function getCsp(path = '/'): string {
return callMiddleware(path).headers.get('Content-Security-Policy') ?? '';
return callProxy(path).headers.get('Content-Security-Policy') ?? '';
}
/** Check whether the middleware matcher regex excludes a given path. */
/** Check whether the proxy matcher regex excludes a given path. */
function matcherExcludes(path: string): boolean {
const pattern = middlewareConfig.matcher[0];
const pattern = proxyConfig.matcher[0];
// Next.js wraps the matcher in ^/<pattern>$ for path matching.
// We replicate the essential check: the negative-lookahead prefix groups.
const re = new RegExp(`^${pattern}$`);
@@ -55,7 +55,7 @@ describe('hydration-safe CSP header', () => {
expect(csp).toMatch(/script-src [^;]*'unsafe-inline'/);
});
it('middleware still returns a CSP header for document requests', () => {
it('proxy still returns a CSP header for document requests', () => {
const csp = getCsp();
expect(csp).toContain("default-src 'self'");
expect(csp).toContain("script-src 'self'");
@@ -117,10 +117,10 @@ describe('next.config.ts CSP removal', () => {
});
// ---------------------------------------------------------------------------
// 4. Middleware does not break API/static routes
// 4. Proxy does not break API/static routes
// ---------------------------------------------------------------------------
describe('middleware matcher exclusions', () => {
describe('proxy matcher exclusions', () => {
it('excludes /api paths', () => {
expect(matcherExcludes('/api/mesh/events')).toBe(true);
});
@@ -13,19 +13,19 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { NextRequest } from 'next/server';
import { middleware, config as middlewareConfig } from '@/middleware';
import { proxy, config as proxyConfig } from '@/proxy';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function callMiddleware(path = '/') {
function callProxy(path = '/') {
const req = new NextRequest(`http://localhost${path}`, { method: 'GET' });
return middleware(req);
return proxy(req);
}
function getCsp(path = '/'): string {
return callMiddleware(path).headers.get('Content-Security-Policy') ?? '';
return callProxy(path).headers.get('Content-Security-Policy') ?? '';
}
/** Extract a single CSP directive by name. */
@@ -36,7 +36,7 @@ function getDirective(name: string, csp?: string): string {
}
function matcherExcludes(path: string): boolean {
const pattern = middlewareConfig.matcher[0];
const pattern = proxyConfig.matcher[0];
const re = new RegExp(`^${pattern}$`);
return !re.test(path);
}
@@ -1,5 +1,5 @@
/**
* Phase 5F-A: CSP nonce plumbing middleware.
* Phase 5F-A: CSP nonce plumbing proxy.
*
* Generates a per-request cryptographic nonce and emits a dynamic
* Content-Security-Policy header for document (page) responses.
@@ -36,7 +36,7 @@ function buildCsp(nonce: string, strictScripts = false): string {
return directives.join('; ');
}
export function middleware(request: NextRequest) {
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
// Forward a nonce for staged CSP support. Strict script-src is opt-in until