UNPKG

@cosmstack/blackshield

Version:

A developer-first security toolkit for React/Next.js applications

420 lines (414 loc) 14.5 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/server/index.ts var index_exports = {}; __export(index_exports, { commonSchemas: () => commonSchemas, createServerValidator: () => createServerValidator, createSignedCookie: () => createSignedCookie, csrfMiddleware: () => csrfMiddleware, deleteSecureCookie: () => deleteSecureCookie, generateCsrfToken: () => generateCsrfToken, protect: () => protect, protectServerAction: () => protectServerAction, readSecureCookie: () => readSecureCookie, setCsrfTokenCookie: () => setCsrfTokenCookie, validateCsrfToken: () => validateCsrfToken, validateServerInput: () => validateServerInput, verifyCsrfToken: () => verifyCsrfToken }); module.exports = __toCommonJS(index_exports); // src/server/validation.ts var import_zod = require("zod"); function validateServerInput(schema, input) { try { const data = schema.parse(input); return { data, errors: {}, isValid: true }; } catch (error) { if (error instanceof import_zod.z.ZodError) { const errors = {}; for (const issue of error.issues) { const path = issue.path.join("."); if (!errors[path]) { errors[path] = []; } errors[path].push(issue.message); } return { data: input, errors, isValid: false }; } return { data: input, errors: { _root: ["Validation failed"] }, isValid: false }; } } function createServerValidator(schema) { return (input) => validateServerInput(schema, input); } var commonSchemas = { email: import_zod.z.string().email(), password: import_zod.z.string().min(8), id: import_zod.z.string().uuid(), slug: import_zod.z.string().regex(/^[a-z0-9-]+$/), url: import_zod.z.string().url(), safeString: import_zod.z.string().max(1e3).regex(/^[^<>]*$/) // No HTML tags }; // src/server/cookies.ts var import_jose = require("jose"); var import_headers = require("next/headers"); var secret = new TextEncoder().encode( process.env.BLACKSHIELD_SECRET || "fallback-secret-change-in-production" ); async function createSignedCookie(name, value, options = {}) { const { maxAge = 60 * 60 * 24 * 7, // 7 days httpOnly = true, secure = process.env.NODE_ENV === "production", sameSite = "lax", path = "/" } = options; try { const token = await new import_jose.SignJWT({ data: value }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(Math.floor(Date.now() / 1e3) + maxAge).sign(secret); const cookieStore = await (0, import_headers.cookies)(); cookieStore.set(name, token, { maxAge, httpOnly, secure, sameSite, path }); } catch (error) { if (process.env.NODE_ENV === "development") { console.error("[Blackshield] Failed to create signed cookie:", error); } throw new Error("Failed to create secure cookie"); } } async function readSecureCookie(name) { try { const cookieStore = await (0, import_headers.cookies)(); const token = cookieStore.get(name)?.value; if (!token) { return null; } const { payload } = await (0, import_jose.jwtVerify)(token, secret); return payload.data; } catch (error) { if (process.env.NODE_ENV === "development") { console.error("[Blackshield] Failed to read secure cookie:", error); } return null; } } async function deleteSecureCookie(name) { const cookieStore = await (0, import_headers.cookies)(); cookieStore.delete(name); } // src/server/csrf.ts var import_jose2 = require("jose"); var import_headers2 = require("next/headers"); var DEFAULT_CONFIG = { tokenHeader: "x-csrf-token", tokenCookie: "csrf-token", tokenExpiry: 3600 // 1 hour }; function getJwtSecret() { const secret2 = process.env.JWT_SECRET || process.env.NEXTAUTH_SECRET; if (!secret2) { throw new Error( "JWT_SECRET or NEXTAUTH_SECRET environment variable is required for CSRF protection" ); } return new TextEncoder().encode(secret2); } async function generateCsrfToken(config = {}) { const { tokenExpiry } = { ...DEFAULT_CONFIG, ...config }; const secret2 = getJwtSecret(); const expiresAt = Date.now() + tokenExpiry * 1e3; const token = await new import_jose2.SignJWT({ type: "csrf", iat: Math.floor(Date.now() / 1e3), exp: Math.floor(expiresAt / 1e3) }).setProtectedHeader({ alg: "HS256" }).sign(secret2); return { token, expiresAt }; } async function verifyCsrfToken(token) { try { const secret2 = getJwtSecret(); const { payload } = await (0, import_jose2.jwtVerify)(token, secret2); return payload.type === "csrf" && typeof payload.exp === "number" && payload.exp > Math.floor(Date.now() / 1e3); } catch { return false; } } async function setCsrfTokenCookie(config = {}) { const { tokenCookie } = { ...DEFAULT_CONFIG, ...config }; const { token } = await generateCsrfToken(config); const cookieStore = await (0, import_headers2.cookies)(); cookieStore.set(tokenCookie, token, { httpOnly: false, // Allow client-side access secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: config.tokenExpiry || DEFAULT_CONFIG.tokenExpiry }); return token; } function getCsrfTokenFromRequest(req, config = {}) { const { tokenHeader, tokenCookie } = { ...DEFAULT_CONFIG, ...config }; const headerToken = req.headers.get(tokenHeader); if (headerToken) return headerToken; const cookieToken = req.cookies.get(tokenCookie)?.value; if (cookieToken) return cookieToken; if (req.method === "POST" && req.headers.get("content-type")?.includes("application/x-www-form-urlencoded")) { return null; } return null; } async function validateCsrfToken(req, config = {}) { if (["GET", "HEAD", "OPTIONS"].includes(req.method)) { return true; } const token = getCsrfTokenFromRequest(req, config); if (!token) return false; return await verifyCsrfToken(token); } function csrfMiddleware(config = {}) { return async (req) => { const isValid = await validateCsrfToken(req, config); if (!isValid) { return Response.json({ error: "Invalid or missing CSRF token" }, { status: 403 }); } return null; }; } // src/server/middleware.ts var import_server = require("next/server"); var rateLimitStore = /* @__PURE__ */ new Map(); function checkRateLimit(key, max, windowSeconds) { const now = Date.now(); const windowMs = windowSeconds * 1e3; const entry = rateLimitStore.get(key); if (entry && now > entry.resetTime) { rateLimitStore.delete(key); } const currentEntry = rateLimitStore.get(key); if (!currentEntry) { rateLimitStore.set(key, { count: 1, resetTime: now + windowMs }); return true; } if (currentEntry.count >= max) { return false; } currentEntry.count++; return true; } function cleanupExpiredEntries() { const now = Date.now(); for (const [key, entry] of rateLimitStore.entries()) { if (now > entry.resetTime) { rateLimitStore.delete(key); } } } var lastCleanup = 0; var CLEANUP_INTERVAL = 5 * 60 * 1e3; function maybeCleanup() { const now = Date.now(); if (now - lastCleanup > CLEANUP_INTERVAL) { cleanupExpiredEntries(); lastCleanup = now; } } function protect(handler, options = {}) { return async (req, params) => { try { if (options.rateLimit) { maybeCleanup(); const { max, windowSeconds, keyGenerator } = options.rateLimit; const key = keyGenerator ? keyGenerator(req) : req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "anonymous"; if (!checkRateLimit(key, max, windowSeconds)) { return import_server.NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 }); } } if (options.csrf) { const csrfConfig = typeof options.csrf === "boolean" ? {} : options.csrf; const isValidCsrf = await validateCsrfToken(req, csrfConfig); if (!isValidCsrf) { return import_server.NextResponse.json({ error: "Invalid or missing CSRF token" }, { status: 403 }); } } let user = null; if (options.requireAuth || options.roles || options.permissions || options.customAuth) { user = await readSecureCookie("session"); if (options.requireAuth && !user) { return import_server.NextResponse.json({ error: "Authentication required" }, { status: 401 }); } } if (options.roles && options.roles.length > 0) { if (!user || !user.roles) { return import_server.NextResponse.json({ error: "Insufficient permissions" }, { status: 403 }); } const hasRequiredRole = options.roles.some((role) => user.roles?.includes(role)); if (!hasRequiredRole) { return import_server.NextResponse.json({ error: "Insufficient permissions" }, { status: 403 }); } } if (options.permissions && options.permissions.length > 0) { if (!user || !user.permissions) { return import_server.NextResponse.json({ error: "Insufficient permissions" }, { status: 403 }); } const hasRequiredPermission = options.permissions.some( (permission) => user.permissions?.includes(permission) ); if (!hasRequiredPermission) { return import_server.NextResponse.json({ error: "Insufficient permissions" }, { status: 403 }); } } if (options.customAuth) { const isAuthorized = await options.customAuth(user, req); if (!isAuthorized) { return import_server.NextResponse.json({ error: "Access denied" }, { status: 403 }); } } let validatedInput; if (options.schemaValidation) { let input; if (req.method === "GET") { const url = new URL(req.url); input = Object.fromEntries(url.searchParams.entries()); } else { try { const contentType = req.headers.get("content-type") || ""; if (contentType.includes("application/json")) { input = await req.json(); } else if (contentType.includes("application/x-www-form-urlencoded")) { const formData = await req.formData(); input = Object.fromEntries(formData.entries()); } else { input = await req.text(); } } catch (error) { return import_server.NextResponse.json({ error: "Invalid request body" }, { status: 400 }); } } const validation = validateServerInput(options.schemaValidation, input); if (!validation.isValid) { return import_server.NextResponse.json( { error: "Validation failed", details: validation.errors }, { status: 400 } ); } validatedInput = validation.data; } const context = { user, validatedInput }; return await handler(req, context, params); } catch (error) { console.error("[Blackshield] Protection middleware error:", error); return import_server.NextResponse.json({ error: "Internal server error" }, { status: 500 }); } }; } function protectServerAction(action, options = {}) { return async (...args) => { try { let user = null; if (options.requireAuth || options.roles || options.permissions || options.customAuth) { user = await readSecureCookie("session"); if (options.requireAuth && !user) { return { error: "Authentication required", status: 401 }; } } if (options.roles && options.roles.length > 0) { if (!user || !user.roles) { return { error: "Insufficient permissions", status: 403 }; } const hasRequiredRole = options.roles.some((role) => user.roles?.includes(role)); if (!hasRequiredRole) { return { error: "Insufficient permissions", status: 403 }; } } if (options.permissions && options.permissions.length > 0) { if (!user || !user.permissions) { return { error: "Insufficient permissions", status: 403 }; } const hasRequiredPermission = options.permissions.some( (permission) => user.permissions?.includes(permission) ); if (!hasRequiredPermission) { return { error: "Insufficient permissions", status: 403 }; } } if (options.customAuth) { const mockReq = {}; const isAuthorized = await options.customAuth(user, mockReq); if (!isAuthorized) { return { error: "Access denied", status: 403 }; } } if (options.schemaValidation && args.length > 0) { const validation = validateServerInput(options.schemaValidation, args[0]); if (!validation.isValid) { return { error: "Validation failed", status: 400, details: validation.errors // biome-ignore lint/suspicious/noExplicitAny: Complex return type requires any for compatibility }; } args[0] = validation.data; } return await action(...args); } catch (error) { console.error("[Blackshield] Server action protection error:", error); return { error: "Internal server error", status: 500 }; } }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { commonSchemas, createServerValidator, createSignedCookie, csrfMiddleware, deleteSecureCookie, generateCsrfToken, protect, protectServerAction, readSecureCookie, setCsrfTokenCookie, validateCsrfToken, validateServerInput, verifyCsrfToken });