Website: products, library, catalogs and their console page

/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]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 19:06:08 -07:00
1 parent b4f6fce9df
commit 51d5bb5ff3
24 files changed
+1064 -5

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
// 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" },
],
},
],
};