/products (search), product pages with revisions, getting and staying on a revision, retracting; /products/:id/edit; /create and new revisions upload from the browser straight to the catalog with a token from the hub; /library; /catalogs (directory, registering with a once-shown key); /console/catalogs to approve or reject. Every page now sends a nonce-based CSP (proxy.ts), so product descriptions can't run scripts. buttonVariants now merges classes, so outline links get their border. Co-Authored-By: Claude Opus 5.5 <[email protected]>
42 lines
1.7 KiB
TypeScript
42 lines
1.7 KiB
TypeScript
// A Content Security Policy for every page: scripts only from this site with the request's nonce (Next.js adds it to
|
|
// its own scripts), so HTML that slips into a page, such as a product's description, can't run anything. Uploads go
|
|
// from the browser straight to catalogs, so connect-src allows any https address (and local ones, for development).
|
|
import { NextResponse, type NextRequest } from "next/server";
|
|
|
|
export function proxy(request: NextRequest) {
|
|
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
|
const dev = process.env.NODE_ENV === "development";
|
|
const policy = [
|
|
"default-src 'self'",
|
|
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${dev ? " 'unsafe-eval'" : ""}`,
|
|
// Descriptions keep their style attributes (the API's allowlist limits them to colors, sizes and alignment).
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' https: data: blob:",
|
|
"font-src 'self'",
|
|
`connect-src 'self' https: http://localhost:* http://127.0.0.1:*${dev ? " ws:" : ""}`,
|
|
"object-src 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
"frame-ancestors 'none'",
|
|
].join("; ");
|
|
|
|
const headers = new Headers(request.headers);
|
|
headers.set("x-nonce", nonce);
|
|
headers.set("Content-Security-Policy", policy);
|
|
const response = NextResponse.next({ request: { headers } });
|
|
response.headers.set("Content-Security-Policy", policy);
|
|
return response;
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
{
|
|
source: "/((?!api|_next/static|_next/image|icon.svg|favicon.ico).*)",
|
|
missing: [
|
|
{ type: "header", key: "next-router-prefetch" },
|
|
{ type: "header", key: "purpose", value: "prefetch" },
|
|
],
|
|
},
|
|
],
|
|
};
|