@workspace/security
Security utilities for API responses and headers
Server-side security functions for JSON responses and HTML documents. Provides consistent header application, cache control, and CSP management.
Heads up
This package is server-only. Importing in client code throws an error. Use security functions exclusively in API routes, Server Components, and middleware.
Core Response Functions
secureJson()
Base JSON response function with security headers.
import { secureJson } from "@workspace/security";
export async function GET() {
return secureJson({ message: "Hello" });
}
Default Headers
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
Options
secureJson(body, {
status?: number; // HTTP status (default: 200)
headers?: HeadersInit; // Additional headers
cache?: CachePolicy; // Cache control (default: "none")
vary?: string[]; // Vary header values
includePoweredByRemoval?: boolean; // Remove Server/X-Powered-By (default: true)
})
secureUserJson()
JSON response for authenticated user data. Applies private cache headers and authentication-specific Vary headers.
import { secureUserJson } from "@workspace/security";
import { headers } from "next/headers";
import { auth } from "@workspace/auth/server";
export async function GET() {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session?.user?.id) {
return secureErrorJson(
{ message: "Unauthorized", code: "UNAUTHORIZED" },
{ status: 401 }
);
}
const profile = await getUserProfile(session.user.id);
return secureUserJson(profile);
}
Applied Headers
Cache-Control: private, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
Vary: Cookie, Authorization
securePublicJson()
JSON response for public data with TTL-based caching.
import { securePublicJson } from "@workspace/security";
export async function GET() {
const articles = await getPublicArticles();
return securePublicJson(articles, {
maxAge: 300 // Cache for 5 minutes
});
}
maxAge Options
60
- 1 minute cache300
- 5 minute cache (default)3600
- 1 hour cache
Applied Headers
Cache-Control: public, max-age={n}, s-maxage={n}, stale-while-revalidate={n/2}
Vary: Accept, Accept-Encoding, Origin
Public Data Only
Only use securePublicJson()
for data available to all users. User-specific data requires secureUserJson()
to prevent cache poisoning.
secureErrorJson()
Error responses with appropriate cache prevention.
import { secureErrorJson } from "@workspace/security";
export async function POST(request: Request) {
try {
const data = await request.json();
// Process...
} catch (error) {
return secureErrorJson(
{
message: "Invalid request data",
code: "VALIDATION_ERROR",
issues: ["Field 'email' is required"]
},
{ status: 400, userScoped: true }
);
}
}
Error Object Structure
{
message: string;
code: string;
issues?: string[]; // Optional validation errors
}
Options
status
- HTTP status codeuserScoped
- IncludeVary: Cookie, Authorization
headers
HTML Security Functions
withHtmlSecurityHeaders()
Applies security headers to HTML responses.
import { withHtmlSecurityHeaders } from "@workspace/security";
import { NextResponse } from "next/server";
export async function GET() {
const html = await renderPage();
return withHtmlSecurityHeaders(
new NextResponse(html, {
headers: { "Content-Type": "text/html; charset=utf-8" }
})
);
}
Applied Headers
X-Content-Type-Options: nosniff
CSP Override
import { withHtmlSecurityHeaders, CSPBuilder } from "@workspace/security";
const csp = new CSPBuilder()
.add("script-src", ["'self'", "https://js.stripe.com"])
.add("frame-src", ["'self'", "https://js.stripe.com"])
.toString();
return withHtmlSecurityHeaders(response, { cspOverride: csp });
CSPBuilder
Fluent API for building Content Security Policy directives.
import { CSPBuilder } from "@workspace/security";
const policy = new CSPBuilder()
.add("script-src", ["'self'", "https://trusted-cdn.com"])
.add("style-src", ["'self'", "https://fonts.googleapis.com"])
.add("img-src", ["'self'", "data:", "https:"])
.toString();
// Result:
// "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; script-src 'self' https://trusted-cdn.com; ..."
Default Directives
default-src 'self'
frame-ancestors 'none'
base-uri 'self'
CSP Extension Pattern
Add only required sources to CSP. The builder includes secure defaults. Avoid 'unsafe-inline'
and 'unsafe-eval'
unless absolutely necessary.
Utility Functions
isStaticAssetPath()
Identifies static asset requests for middleware optimization.
import { isStaticAssetPath } from "@workspace/security";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Skip cache headers for static assets
if (!isStaticAssetPath(request.nextUrl.pathname)) {
response.headers.set("Cache-Control", "no-store");
}
return response;
}
Detected Paths
/_next/static/*
/_next/image/*
/_next/data/*
/assets/*
/favicon.ico
- Files with static extensions (
.js
,.css
,.png
,.woff2
, etc.)
Cache Policy Types
type CachePolicy =
| "none" // No Cache-Control header
| "private-no-store" // User data - no caching
| "public-60" // Public data - 1 min TTL
| "public-300" // Public data - 5 min TTL
| "public-3600"; // Public data - 1 hour TTL
Cache headers generated per policy:
Usage Patterns
API Route Protection
import { headers } from "next/headers";
import { auth } from "@workspace/auth/server";
import { secureUserJson, secureErrorJson } from "@workspace/security";
export async function GET() {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session?.user?.id) {
return secureErrorJson(
{ message: "Unauthorized", code: "UNAUTHORIZED" },
{ status: 401, userScoped: true }
);
}
const data = await fetchUserData(session.user.id);
return secureUserJson(data);
}
Public Data Endpoint
import { securePublicJson } from "@workspace/security";
export async function GET() {
const posts = await getPublicBlogPosts();
return securePublicJson(posts, {
maxAge: 300, // 5 minutes
additionalVary: ["Accept-Language"] // Content negotiation
});
}
Error Responses
import { secureErrorJson } from "@workspace/security";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
message: z.string().min(10)
});
export async function POST(request: Request) {
const body = await request.json();
const result = schema.safeParse(body);
if (!result.success) {
return secureErrorJson(
{
message: "Validation failed",
code: "VALIDATION_ERROR",
issues: result.error.issues.map(i => i.message)
},
{ status: 400 }
);
}
// Process valid data...
}
Security Considerations
Server-Only Enforcement
The package throws errors when imported in client code:
if (
typeof globalThis.window !== "undefined" &&
process.env.NODE_ENV !== "test" &&
process.env.STORYBOOK !== "true"
) {
throw new Error(
"@workspace/security must only be imported on the server."
);
}
This prevents accidental exposure of security utilities in browser bundles.
Cache Poisoning Prevention
User data requires secureUserJson()
with Vary: Cookie, Authorization
:
// ❌ Wrong - user data with public cache
export async function GET() {
const userData = await getCurrentUser();
return securePublicJson(userData); // Cache poisoning risk!
}
// ✅ Correct - user data with private cache
export async function GET() {
const userData = await getCurrentUser();
return secureUserJson(userData);
}
Without proper Vary
headers, CDNs may cache user-specific data and serve it to other users.
CSP Maintenance
Extend CSP incrementally for third-party integrations:
import { CSPBuilder } from "@workspace/security";
// Analytics integration
const csp = new CSPBuilder()
.add("script-src", ["https://www.googletagmanager.com"])
.add("img-src", ["https://www.google-analytics.com"])
.add("connect-src", ["https://www.google-analytics.com"])
.toString();
Document required CSP extensions in code comments or configuration files.
Integration with Middleware
Middleware applies headers globally:
import { isStaticAssetPath } from "@workspace/security";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Security headers for all non-static responses
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set("X-DNS-Prefetch-Control", "off");
// Cache control for dynamic content
if (!isStaticAssetPath(request.nextUrl.pathname)) {
response.headers.set("Cache-Control", "no-store");
}
return response;
}
API routes supplement middleware headers with response-specific controls via secureJson()
functions.
Testing
Mock security functions in tests:
import { vi } from "vitest";
vi.mock("@workspace/security", () => ({
secureUserJson: vi.fn((body, opts) =>
Response.json(body, { status: opts?.status ?? 200 })
),
secureErrorJson: vi.fn((error, opts) =>
Response.json({ error }, { status: opts?.status ?? 500 })
),
}));
Integration tests verify actual header application:
import { expect, test } from "vitest";
test("applies security headers", async () => {
const response = await fetch("/api/protected");
expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
expect(response.headers.get("Cache-Control")).toContain("no-store");
expect(response.headers.get("Vary")).toContain("Cookie");
});
AI-Assisted Security
AI assistants apply security functions automatically:
Secure by Default
When requesting API route implementations, AI assistants select appropriate security functions based on data sensitivity. Authenticated endpoints use secureUserJson()
, public endpoints use securePublicJson()
, and errors use secureErrorJson()
.
// Prompt: "Create API endpoint for user preferences"
import { headers } from "next/headers";
import { auth } from "@workspace/auth/server";
import { secureUserJson, secureErrorJson } from "@workspace/security";
export async function GET() {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session?.user?.id) {
return secureErrorJson(
{ message: "Unauthorized", code: "UNAUTHORIZED" },
{ status: 401, userScoped: true }
);
}
const preferences = await getUserPreferences(session.user.id);
return secureUserJson(preferences);
}
The AI applies authentication checks, appropriate response functions, and error handling patterns consistently.