nextjs-secure 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/csrf.d.ts CHANGED
@@ -1,26 +1,75 @@
1
+ import { NextRequest } from 'next/server';
2
+
3
+ interface CSRFCookieOptions {
4
+ name?: string;
5
+ path?: string;
6
+ domain?: string;
7
+ secure?: boolean;
8
+ httpOnly?: boolean;
9
+ sameSite?: 'strict' | 'lax' | 'none';
10
+ maxAge?: number;
11
+ }
12
+ interface CSRFConfig {
13
+ /** Cookie settings */
14
+ cookie?: CSRFCookieOptions;
15
+ /** Header name to check for token (default: x-csrf-token) */
16
+ headerName?: string;
17
+ /** Form field name (default: _csrf) */
18
+ fieldName?: string;
19
+ /** Secret for signing tokens */
20
+ secret?: string;
21
+ /** Token length in bytes (default: 32) */
22
+ tokenLength?: number;
23
+ /** Methods to protect (default: POST, PUT, PATCH, DELETE) */
24
+ protectedMethods?: string[];
25
+ /** Skip CSRF check for specific requests */
26
+ skip?: (req: NextRequest) => boolean | Promise<boolean>;
27
+ /** Called when CSRF validation fails */
28
+ onError?: (req: NextRequest, reason: string) => Response | Promise<Response>;
29
+ }
30
+ interface CSRFToken {
31
+ value: string;
32
+ cookie: string;
33
+ }
34
+
35
+ type RouteHandler = (req: NextRequest) => Response | Promise<Response>;
1
36
  /**
2
- * CSRF Protection Middleware (Coming Soon)
3
- *
4
- * @example
5
- * ```typescript
6
- * import { withCsrf, generateCsrfToken } from 'next-secure/csrf'
7
- *
8
- * // Generate token
9
- * export async function GET() {
10
- * const token = await generateCsrfToken()
11
- * return Response.json({ csrfToken: token })
12
- * }
37
+ * CSRF protection middleware
13
38
  *
14
- * // Validate token
15
- * export const POST = withCsrf(async (req) => {
16
- * return Response.json({ ok: true })
17
- * })
18
- * ```
19
- *
20
- * @packageDocumentation
39
+ * Uses double submit cookie pattern:
40
+ * 1. Server sets a signed token in a cookie
41
+ * 2. Client sends the same token in header/body
42
+ * 3. Server compares both values
43
+ */
44
+ declare function withCSRF(handler: RouteHandler, config?: CSRFConfig): RouteHandler;
45
+ /**
46
+ * Generate a new CSRF token and cookie header
47
+ * Use this in GET routes to set the initial token
48
+ */
49
+ declare function generateCSRF(config?: CSRFConfig): Promise<{
50
+ token: string;
51
+ cookieHeader: string;
52
+ }>;
53
+ /**
54
+ * Validate a CSRF token without middleware
55
+ * Useful for custom validation flows
56
+ */
57
+ declare function validateCSRF(req: NextRequest, config?: CSRFConfig): Promise<{
58
+ valid: boolean;
59
+ reason?: string;
60
+ }>;
61
+
62
+ /**
63
+ * Create a signed CSRF token
64
+ */
65
+ declare function createToken(secret: string, length?: number): Promise<string>;
66
+ /**
67
+ * Verify a signed CSRF token
68
+ */
69
+ declare function verifyToken(token: string, secret: string): Promise<boolean>;
70
+ /**
71
+ * Compare two tokens (constant-time)
21
72
  */
22
- declare function withCsrf(): void;
23
- declare function generateCsrfToken(): void;
24
- declare function validateCsrfToken(): void;
73
+ declare function tokensMatch(a: string, b: string): boolean;
25
74
 
26
- export { generateCsrfToken, validateCsrfToken, withCsrf };
75
+ export { type CSRFConfig, type CSRFCookieOptions, type CSRFToken, createToken, generateCSRF, tokensMatch, validateCSRF, verifyToken, withCSRF };
package/dist/csrf.js CHANGED
@@ -1,14 +1,188 @@
1
- // src/middleware/csrf/index.ts
2
- function withCsrf() {
3
- throw new Error("CSRF middleware coming soon in v0.2.0");
1
+ import { webcrypto } from 'crypto';
2
+
3
+ // src/middleware/csrf/token.ts
4
+ var encoder = new TextEncoder();
5
+ function randomBytes(length) {
6
+ const bytes = new Uint8Array(length);
7
+ webcrypto.getRandomValues(bytes);
8
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
9
+ }
10
+ async function createSignature(data, secret) {
11
+ const key = await webcrypto.subtle.importKey(
12
+ "raw",
13
+ encoder.encode(secret),
14
+ { name: "HMAC", hash: "SHA-256" },
15
+ false,
16
+ ["sign"]
17
+ );
18
+ const sig = await webcrypto.subtle.sign("HMAC", key, encoder.encode(data));
19
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
20
+ }
21
+ function safeCompare(a, b) {
22
+ if (a.length !== b.length) return false;
23
+ let result = 0;
24
+ for (let i = 0; i < a.length; i++) {
25
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
26
+ }
27
+ return result === 0;
28
+ }
29
+ async function createToken(secret, length = 32) {
30
+ const data = randomBytes(length);
31
+ const sig = await createSignature(data, secret);
32
+ return `${data}.${sig}`;
33
+ }
34
+ async function verifyToken(token, secret) {
35
+ if (!token || typeof token !== "string") return false;
36
+ const parts = token.split(".");
37
+ if (parts.length !== 2) return false;
38
+ const [data, sig] = parts;
39
+ if (!data || !sig) return false;
40
+ try {
41
+ const expected = await createSignature(data, secret);
42
+ return safeCompare(sig, expected);
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ function tokensMatch(a, b) {
48
+ if (!a || !b) return false;
49
+ return safeCompare(a, b);
50
+ }
51
+
52
+ // src/middleware/csrf/middleware.ts
53
+ var DEFAULT_COOKIE = {
54
+ name: "__csrf",
55
+ path: "/",
56
+ httpOnly: true,
57
+ secure: process.env.NODE_ENV === "production",
58
+ sameSite: "strict",
59
+ maxAge: 86400
60
+ // 24h
61
+ };
62
+ var DEFAULT_CONFIG = {
63
+ headerName: "x-csrf-token",
64
+ fieldName: "_csrf",
65
+ tokenLength: 32,
66
+ protectedMethods: ["POST", "PUT", "PATCH", "DELETE"]
67
+ };
68
+ function getSecret(config) {
69
+ const secret = config.secret || process.env.CSRF_SECRET;
70
+ if (!secret) {
71
+ throw new Error(
72
+ "CSRF secret is required. Set config.secret or CSRF_SECRET env variable."
73
+ );
74
+ }
75
+ return secret;
76
+ }
77
+ function buildCookieString(name, value, opts) {
78
+ let cookie = `${name}=${value}`;
79
+ if (opts.path) cookie += `; Path=${opts.path}`;
80
+ if (opts.domain) cookie += `; Domain=${opts.domain}`;
81
+ if (opts.maxAge) cookie += `; Max-Age=${opts.maxAge}`;
82
+ if (opts.httpOnly) cookie += "; HttpOnly";
83
+ if (opts.secure) cookie += "; Secure";
84
+ if (opts.sameSite) cookie += `; SameSite=${opts.sameSite}`;
85
+ return cookie;
86
+ }
87
+ async function extractToken(req, headerName, fieldName) {
88
+ const headerToken = req.headers.get(headerName);
89
+ if (headerToken) return headerToken;
90
+ const contentType = req.headers.get("content-type") || "";
91
+ if (contentType.includes("application/x-www-form-urlencoded")) {
92
+ try {
93
+ const cloned = req.clone();
94
+ const formData = await cloned.formData();
95
+ const token = formData.get(fieldName);
96
+ if (typeof token === "string") return token;
97
+ } catch {
98
+ }
99
+ }
100
+ if (contentType.includes("application/json")) {
101
+ try {
102
+ const cloned = req.clone();
103
+ const body = await cloned.json();
104
+ if (body && typeof body[fieldName] === "string") {
105
+ return body[fieldName];
106
+ }
107
+ } catch {
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ function defaultErrorResponse(_req, reason) {
113
+ return new Response(JSON.stringify({ error: "CSRF validation failed", reason }), {
114
+ status: 403,
115
+ headers: { "Content-Type": "application/json" }
116
+ });
117
+ }
118
+ function withCSRF(handler, config = {}) {
119
+ const secret = getSecret(config);
120
+ const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
121
+ const headerName = config.headerName || DEFAULT_CONFIG.headerName;
122
+ const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName;
123
+ const protectedMethods = config.protectedMethods || DEFAULT_CONFIG.protectedMethods;
124
+ const onError = config.onError || defaultErrorResponse;
125
+ return async (req) => {
126
+ const method = req.method.toUpperCase();
127
+ if (!protectedMethods.includes(method)) {
128
+ return handler(req);
129
+ }
130
+ if (config.skip) {
131
+ const shouldSkip = await config.skip(req);
132
+ if (shouldSkip) return handler(req);
133
+ }
134
+ const cookieName = cookieOpts.name || "__csrf";
135
+ const cookieToken = req.cookies.get(cookieName)?.value;
136
+ if (!cookieToken) {
137
+ return onError(req, "missing_cookie");
138
+ }
139
+ const cookieValid = await verifyToken(cookieToken, secret);
140
+ if (!cookieValid) {
141
+ return onError(req, "invalid_cookie");
142
+ }
143
+ const requestToken = await extractToken(req, headerName, fieldName);
144
+ if (!requestToken) {
145
+ return onError(req, "missing_token");
146
+ }
147
+ if (!tokensMatch(cookieToken, requestToken)) {
148
+ return onError(req, "token_mismatch");
149
+ }
150
+ return handler(req);
151
+ };
4
152
  }
5
- function generateCsrfToken() {
6
- throw new Error("CSRF middleware coming soon in v0.2.0");
153
+ async function generateCSRF(config = {}) {
154
+ const secret = getSecret(config);
155
+ const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
156
+ const tokenLength = config.tokenLength || DEFAULT_CONFIG.tokenLength;
157
+ const cookieName = cookieOpts.name || "__csrf";
158
+ const token = await createToken(secret, tokenLength);
159
+ const cookieHeader = buildCookieString(cookieName, token, cookieOpts);
160
+ return { token, cookieHeader };
7
161
  }
8
- function validateCsrfToken() {
9
- throw new Error("CSRF middleware coming soon in v0.2.0");
162
+ async function validateCSRF(req, config = {}) {
163
+ const secret = getSecret(config);
164
+ const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
165
+ const headerName = config.headerName || DEFAULT_CONFIG.headerName;
166
+ const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName;
167
+ const cookieName = cookieOpts.name || "__csrf";
168
+ const cookieToken = req.cookies.get(cookieName)?.value;
169
+ if (!cookieToken) {
170
+ return { valid: false, reason: "missing_cookie" };
171
+ }
172
+ const cookieValid = await verifyToken(cookieToken, secret);
173
+ if (!cookieValid) {
174
+ return { valid: false, reason: "invalid_cookie" };
175
+ }
176
+ const requestToken = await extractToken(req, headerName, fieldName);
177
+ if (!requestToken) {
178
+ return { valid: false, reason: "missing_token" };
179
+ }
180
+ if (!tokensMatch(cookieToken, requestToken)) {
181
+ return { valid: false, reason: "token_mismatch" };
182
+ }
183
+ return { valid: true };
10
184
  }
11
185
 
12
- export { generateCsrfToken, validateCsrfToken, withCsrf };
186
+ export { createToken, generateCSRF, tokensMatch, validateCSRF, verifyToken, withCSRF };
13
187
  //# sourceMappingURL=csrf.js.map
14
188
  //# sourceMappingURL=csrf.js.map
package/dist/csrf.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware/csrf/index.ts"],"names":[],"mappings":";AAuBO,SAAS,QAAA,GAAW;AACzB,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD;AAEO,SAAS,iBAAA,GAAoB;AAClC,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD;AAEO,SAAS,iBAAA,GAAoB;AAClC,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD","file":"csrf.js","sourcesContent":["/**\n * CSRF Protection Middleware (Coming Soon)\n *\n * @example\n * ```typescript\n * import { withCsrf, generateCsrfToken } from 'next-secure/csrf'\n *\n * // Generate token\n * export async function GET() {\n * const token = await generateCsrfToken()\n * return Response.json({ csrfToken: token })\n * }\n *\n * // Validate token\n * export const POST = withCsrf(async (req) => {\n * return Response.json({ ok: true })\n * })\n * ```\n *\n * @packageDocumentation\n */\n\n// Placeholder for CSRF middleware\nexport function withCsrf() {\n throw new Error('CSRF middleware coming soon in v0.2.0')\n}\n\nexport function generateCsrfToken() {\n throw new Error('CSRF middleware coming soon in v0.2.0')\n}\n\nexport function validateCsrfToken() {\n throw new Error('CSRF middleware coming soon in v0.2.0')\n}\n"]}
1
+ {"version":3,"sources":["../src/middleware/csrf/token.ts","../src/middleware/csrf/middleware.ts"],"names":[],"mappings":"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","file":"csrf.js","sourcesContent":["import { webcrypto } from 'node:crypto'\n\nconst encoder = new TextEncoder()\n\n/**\n * Generate random bytes as hex string\n */\nexport function randomBytes(length: number): string {\n const bytes = new Uint8Array(length)\n webcrypto.getRandomValues(bytes)\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Create HMAC signature\n */\nasync function createSignature(data: string, secret: string): Promise<string> {\n const key = await webcrypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n\n const sig = await webcrypto.subtle.sign('HMAC', key, encoder.encode(data))\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks\n */\nfunction safeCompare(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n\n let result = 0\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i)\n }\n return result === 0\n}\n\n/**\n * Create a signed CSRF token\n */\nexport async function createToken(\n secret: string,\n length: number = 32\n): Promise<string> {\n const data = randomBytes(length)\n const sig = await createSignature(data, secret)\n return `${data}.${sig}`\n}\n\n/**\n * Verify a signed CSRF token\n */\nexport async function verifyToken(\n token: string,\n secret: string\n): Promise<boolean> {\n if (!token || typeof token !== 'string') return false\n\n const parts = token.split('.')\n if (parts.length !== 2) return false\n\n const [data, sig] = parts\n if (!data || !sig) return false\n\n try {\n const expected = await createSignature(data, secret)\n return safeCompare(sig, expected)\n } catch {\n return false\n }\n}\n\n/**\n * Compare two tokens (constant-time)\n */\nexport function tokensMatch(a: string, b: string): boolean {\n if (!a || !b) return false\n return safeCompare(a, b)\n}\n","import type { NextRequest } from 'next/server'\nimport type { CSRFConfig, CSRFCookieOptions } from './types'\nimport { createToken, verifyToken, tokensMatch } from './token'\n\ntype RouteHandler = (req: NextRequest) => Response | Promise<Response>\n\nconst DEFAULT_COOKIE: CSRFCookieOptions = {\n name: '__csrf',\n path: '/',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: 86400, // 24h\n}\n\nconst DEFAULT_CONFIG: Required<Omit<CSRFConfig, 'skip' | 'onError'>> = {\n cookie: DEFAULT_COOKIE,\n headerName: 'x-csrf-token',\n fieldName: '_csrf',\n secret: '',\n tokenLength: 32,\n protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],\n}\n\nfunction getSecret(config: CSRFConfig): string {\n const secret = config.secret || process.env.CSRF_SECRET\n if (!secret) {\n throw new Error(\n 'CSRF secret is required. Set config.secret or CSRF_SECRET env variable.'\n )\n }\n return secret\n}\n\nfunction buildCookieString(name: string, value: string, opts: CSRFCookieOptions): string {\n let cookie = `${name}=${value}`\n\n if (opts.path) cookie += `; Path=${opts.path}`\n if (opts.domain) cookie += `; Domain=${opts.domain}`\n if (opts.maxAge) cookie += `; Max-Age=${opts.maxAge}`\n if (opts.httpOnly) cookie += '; HttpOnly'\n if (opts.secure) cookie += '; Secure'\n if (opts.sameSite) cookie += `; SameSite=${opts.sameSite}`\n\n return cookie\n}\n\n/**\n * Extract token from request (header or body)\n */\nasync function extractToken(\n req: NextRequest,\n headerName: string,\n fieldName: string\n): Promise<string | null> {\n // check header first\n const headerToken = req.headers.get(headerName)\n if (headerToken) return headerToken\n\n // try to get from form data\n const contentType = req.headers.get('content-type') || ''\n\n if (contentType.includes('application/x-www-form-urlencoded')) {\n try {\n const cloned = req.clone()\n const formData = await cloned.formData()\n const token = formData.get(fieldName)\n if (typeof token === 'string') return token\n } catch {\n // ignore parse errors\n }\n }\n\n if (contentType.includes('application/json')) {\n try {\n const cloned = req.clone()\n const body = await cloned.json()\n if (body && typeof body[fieldName] === 'string') {\n return body[fieldName]\n }\n } catch {\n // ignore parse errors\n }\n }\n\n return null\n}\n\nfunction defaultErrorResponse(_req: NextRequest, reason: string): Response {\n return new Response(JSON.stringify({ error: 'CSRF validation failed', reason }), {\n status: 403,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\n/**\n * CSRF protection middleware\n *\n * Uses double submit cookie pattern:\n * 1. Server sets a signed token in a cookie\n * 2. Client sends the same token in header/body\n * 3. Server compares both values\n */\nexport function withCSRF(handler: RouteHandler, config: CSRFConfig = {}): RouteHandler {\n const secret = getSecret(config)\n const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie }\n const headerName = config.headerName || DEFAULT_CONFIG.headerName\n const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName\n const protectedMethods = config.protectedMethods || DEFAULT_CONFIG.protectedMethods\n const onError = config.onError || defaultErrorResponse\n\n return async (req: NextRequest): Promise<Response> => {\n const method = req.method.toUpperCase()\n\n // skip unprotected methods\n if (!protectedMethods.includes(method)) {\n return handler(req)\n }\n\n // custom skip logic\n if (config.skip) {\n const shouldSkip = await config.skip(req)\n if (shouldSkip) return handler(req)\n }\n\n const cookieName = cookieOpts.name || '__csrf'\n const cookieToken = req.cookies.get(cookieName)?.value\n\n // no cookie = first request, reject\n if (!cookieToken) {\n return onError(req, 'missing_cookie')\n }\n\n // verify cookie token is valid (signed by us)\n const cookieValid = await verifyToken(cookieToken, secret)\n if (!cookieValid) {\n return onError(req, 'invalid_cookie')\n }\n\n // get token from request\n const requestToken = await extractToken(req, headerName, fieldName)\n if (!requestToken) {\n return onError(req, 'missing_token')\n }\n\n // compare tokens\n if (!tokensMatch(cookieToken, requestToken)) {\n return onError(req, 'token_mismatch')\n }\n\n return handler(req)\n }\n}\n\n/**\n * Generate a new CSRF token and cookie header\n * Use this in GET routes to set the initial token\n */\nexport async function generateCSRF(config: CSRFConfig = {}): Promise<{\n token: string\n cookieHeader: string\n}> {\n const secret = getSecret(config)\n const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie }\n const tokenLength = config.tokenLength || DEFAULT_CONFIG.tokenLength\n const cookieName = cookieOpts.name || '__csrf'\n\n const token = await createToken(secret, tokenLength)\n const cookieHeader = buildCookieString(cookieName, token, cookieOpts)\n\n return { token, cookieHeader }\n}\n\n/**\n * Validate a CSRF token without middleware\n * Useful for custom validation flows\n */\nexport async function validateCSRF(\n req: NextRequest,\n config: CSRFConfig = {}\n): Promise<{ valid: boolean; reason?: string }> {\n const secret = getSecret(config)\n const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie }\n const headerName = config.headerName || DEFAULT_CONFIG.headerName\n const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName\n const cookieName = cookieOpts.name || '__csrf'\n\n const cookieToken = req.cookies.get(cookieName)?.value\n if (!cookieToken) {\n return { valid: false, reason: 'missing_cookie' }\n }\n\n const cookieValid = await verifyToken(cookieToken, secret)\n if (!cookieValid) {\n return { valid: false, reason: 'invalid_cookie' }\n }\n\n const requestToken = await extractToken(req, headerName, fieldName)\n if (!requestToken) {\n return { valid: false, reason: 'missing_token' }\n }\n\n if (!tokensMatch(cookieToken, requestToken)) {\n return { valid: false, reason: 'token_mismatch' }\n }\n\n return { valid: true }\n}\n"]}
package/dist/headers.cjs CHANGED
@@ -1,14 +1,284 @@
1
1
  'use strict';
2
2
 
3
- // src/middleware/headers/index.ts
4
- function securityHeaders() {
5
- throw new Error("Security headers coming soon in v0.2.0");
3
+ // src/middleware/headers/builder.ts
4
+ function buildCSP(policy) {
5
+ const directives = [];
6
+ const directiveMap = {
7
+ defaultSrc: "default-src",
8
+ scriptSrc: "script-src",
9
+ styleSrc: "style-src",
10
+ imgSrc: "img-src",
11
+ fontSrc: "font-src",
12
+ connectSrc: "connect-src",
13
+ mediaSrc: "media-src",
14
+ objectSrc: "object-src",
15
+ frameSrc: "frame-src",
16
+ childSrc: "child-src",
17
+ workerSrc: "worker-src",
18
+ frameAncestors: "frame-ancestors",
19
+ formAction: "form-action",
20
+ baseUri: "base-uri",
21
+ manifestSrc: "manifest-src",
22
+ reportUri: "report-uri",
23
+ reportTo: "report-to"
24
+ };
25
+ for (const [key, directive] of Object.entries(directiveMap)) {
26
+ const value = policy[key];
27
+ if (value !== void 0 && value !== false) {
28
+ if (Array.isArray(value)) {
29
+ directives.push(`${directive} ${value.join(" ")}`);
30
+ } else if (typeof value === "string") {
31
+ directives.push(`${directive} ${value}`);
32
+ }
33
+ }
34
+ }
35
+ if (policy.upgradeInsecureRequests) {
36
+ directives.push("upgrade-insecure-requests");
37
+ }
38
+ if (policy.blockAllMixedContent) {
39
+ directives.push("block-all-mixed-content");
40
+ }
41
+ return directives.join("; ");
6
42
  }
7
- function createCsp() {
8
- throw new Error("Security headers coming soon in v0.2.0");
43
+ function buildHSTS(config) {
44
+ let value = `max-age=${config.maxAge}`;
45
+ if (config.includeSubDomains) {
46
+ value += "; includeSubDomains";
47
+ }
48
+ if (config.preload) {
49
+ value += "; preload";
50
+ }
51
+ return value;
52
+ }
53
+ function buildPermissionsPolicy(policy) {
54
+ const directives = [];
55
+ const featureMap = {
56
+ accelerometer: "accelerometer",
57
+ ambientLightSensor: "ambient-light-sensor",
58
+ autoplay: "autoplay",
59
+ battery: "battery",
60
+ camera: "camera",
61
+ displayCapture: "display-capture",
62
+ documentDomain: "document-domain",
63
+ encryptedMedia: "encrypted-media",
64
+ fullscreen: "fullscreen",
65
+ geolocation: "geolocation",
66
+ gyroscope: "gyroscope",
67
+ magnetometer: "magnetometer",
68
+ microphone: "microphone",
69
+ midi: "midi",
70
+ payment: "payment",
71
+ pictureInPicture: "picture-in-picture",
72
+ publicKeyCredentialsGet: "publickey-credentials-get",
73
+ screenWakeLock: "screen-wake-lock",
74
+ syncXhr: "sync-xhr",
75
+ usb: "usb",
76
+ webShare: "web-share",
77
+ xrSpatialTracking: "xr-spatial-tracking"
78
+ };
79
+ for (const [key, feature] of Object.entries(featureMap)) {
80
+ const origins = policy[key];
81
+ if (origins !== void 0) {
82
+ if (origins.length === 0) {
83
+ directives.push(`${feature}=()`);
84
+ } else {
85
+ const formatted = origins.map((o) => o === "self" ? "self" : `"${o}"`).join(" ");
86
+ directives.push(`${feature}=(${formatted})`);
87
+ }
88
+ }
89
+ }
90
+ return directives.join(", ");
91
+ }
92
+ var PRESET_STRICT = {
93
+ contentSecurityPolicy: {
94
+ defaultSrc: ["'self'"],
95
+ scriptSrc: ["'self'"],
96
+ styleSrc: ["'self'"],
97
+ imgSrc: ["'self'", "data:"],
98
+ fontSrc: ["'self'"],
99
+ objectSrc: ["'none'"],
100
+ frameAncestors: ["'none'"],
101
+ formAction: ["'self'"],
102
+ baseUri: ["'self'"],
103
+ upgradeInsecureRequests: true
104
+ },
105
+ strictTransportSecurity: {
106
+ maxAge: 31536e3,
107
+ // 1 year
108
+ includeSubDomains: true,
109
+ preload: true
110
+ },
111
+ xFrameOptions: "DENY",
112
+ xContentTypeOptions: true,
113
+ xDnsPrefetchControl: "off",
114
+ xDownloadOptions: true,
115
+ xPermittedCrossDomainPolicies: "none",
116
+ referrerPolicy: "strict-origin-when-cross-origin",
117
+ crossOriginOpenerPolicy: "same-origin",
118
+ crossOriginEmbedderPolicy: "require-corp",
119
+ crossOriginResourcePolicy: "same-origin",
120
+ permissionsPolicy: {
121
+ camera: [],
122
+ microphone: [],
123
+ geolocation: [],
124
+ payment: []
125
+ },
126
+ originAgentCluster: true
127
+ };
128
+ var PRESET_RELAXED = {
129
+ contentSecurityPolicy: {
130
+ defaultSrc: ["'self'"],
131
+ scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
132
+ styleSrc: ["'self'", "'unsafe-inline'"],
133
+ imgSrc: ["'self'", "data:", "blob:", "https:"],
134
+ fontSrc: ["'self'", "https:", "data:"],
135
+ connectSrc: ["'self'", "https:", "wss:"],
136
+ frameSrc: ["'self'"]
137
+ },
138
+ strictTransportSecurity: {
139
+ maxAge: 86400,
140
+ // 1 day
141
+ includeSubDomains: false
142
+ },
143
+ xFrameOptions: "SAMEORIGIN",
144
+ xContentTypeOptions: true,
145
+ referrerPolicy: "no-referrer-when-downgrade"
146
+ };
147
+ var PRESET_API = {
148
+ contentSecurityPolicy: {
149
+ defaultSrc: ["'none'"],
150
+ frameAncestors: ["'none'"]
151
+ },
152
+ strictTransportSecurity: {
153
+ maxAge: 31536e3,
154
+ includeSubDomains: true
155
+ },
156
+ xFrameOptions: "DENY",
157
+ xContentTypeOptions: true,
158
+ referrerPolicy: "no-referrer",
159
+ crossOriginResourcePolicy: "same-origin"
160
+ };
161
+ function getPreset(name) {
162
+ switch (name) {
163
+ case "strict":
164
+ return PRESET_STRICT;
165
+ case "relaxed":
166
+ return PRESET_RELAXED;
167
+ case "api":
168
+ return PRESET_API;
169
+ default:
170
+ return PRESET_STRICT;
171
+ }
172
+ }
173
+ function buildHeaders(config) {
174
+ const headers = new Headers();
175
+ if (config.contentSecurityPolicy) {
176
+ const csp = buildCSP(config.contentSecurityPolicy);
177
+ if (csp) headers.set("Content-Security-Policy", csp);
178
+ }
179
+ if (config.strictTransportSecurity) {
180
+ headers.set("Strict-Transport-Security", buildHSTS(config.strictTransportSecurity));
181
+ }
182
+ if (config.xFrameOptions) {
183
+ headers.set("X-Frame-Options", config.xFrameOptions);
184
+ }
185
+ if (config.xContentTypeOptions) {
186
+ headers.set("X-Content-Type-Options", "nosniff");
187
+ }
188
+ if (config.xDnsPrefetchControl) {
189
+ headers.set("X-DNS-Prefetch-Control", config.xDnsPrefetchControl);
190
+ }
191
+ if (config.xDownloadOptions) {
192
+ headers.set("X-Download-Options", "noopen");
193
+ }
194
+ if (config.xPermittedCrossDomainPolicies) {
195
+ headers.set("X-Permitted-Cross-Domain-Policies", config.xPermittedCrossDomainPolicies);
196
+ }
197
+ if (config.referrerPolicy) {
198
+ const value = Array.isArray(config.referrerPolicy) ? config.referrerPolicy.join(", ") : config.referrerPolicy;
199
+ headers.set("Referrer-Policy", value);
200
+ }
201
+ if (config.crossOriginOpenerPolicy) {
202
+ headers.set("Cross-Origin-Opener-Policy", config.crossOriginOpenerPolicy);
203
+ }
204
+ if (config.crossOriginEmbedderPolicy) {
205
+ headers.set("Cross-Origin-Embedder-Policy", config.crossOriginEmbedderPolicy);
206
+ }
207
+ if (config.crossOriginResourcePolicy) {
208
+ headers.set("Cross-Origin-Resource-Policy", config.crossOriginResourcePolicy);
209
+ }
210
+ if (config.permissionsPolicy) {
211
+ const pp = buildPermissionsPolicy(config.permissionsPolicy);
212
+ if (pp) headers.set("Permissions-Policy", pp);
213
+ }
214
+ if (config.originAgentCluster) {
215
+ headers.set("Origin-Agent-Cluster", "?1");
216
+ }
217
+ return headers;
218
+ }
219
+
220
+ // src/middleware/headers/middleware.ts
221
+ function mergeConfigs(base, custom) {
222
+ return {
223
+ ...base,
224
+ ...custom,
225
+ // Deep merge CSP if both exist
226
+ contentSecurityPolicy: custom.contentSecurityPolicy === false ? false : custom.contentSecurityPolicy ? base.contentSecurityPolicy ? { ...base.contentSecurityPolicy, ...custom.contentSecurityPolicy } : custom.contentSecurityPolicy : base.contentSecurityPolicy,
227
+ // Deep merge HSTS if both exist
228
+ strictTransportSecurity: custom.strictTransportSecurity === false ? false : custom.strictTransportSecurity ? base.strictTransportSecurity ? { ...base.strictTransportSecurity, ...custom.strictTransportSecurity } : custom.strictTransportSecurity : base.strictTransportSecurity,
229
+ // Deep merge Permissions-Policy if both exist
230
+ permissionsPolicy: custom.permissionsPolicy === false ? false : custom.permissionsPolicy ? base.permissionsPolicy ? { ...base.permissionsPolicy, ...custom.permissionsPolicy } : custom.permissionsPolicy : base.permissionsPolicy
231
+ };
232
+ }
233
+ function withSecurityHeaders(handler, options = {}) {
234
+ const { preset, config, override = false } = options;
235
+ let baseConfig = preset ? getPreset(preset) : PRESET_STRICT;
236
+ if (config) {
237
+ baseConfig = mergeConfigs(baseConfig, config);
238
+ }
239
+ const securityHeaders = buildHeaders(baseConfig);
240
+ return async (req) => {
241
+ const response = await handler(req);
242
+ const newHeaders = new Headers(response.headers);
243
+ securityHeaders.forEach((value, key) => {
244
+ if (override || !newHeaders.has(key)) {
245
+ newHeaders.set(key, value);
246
+ }
247
+ });
248
+ return new Response(response.body, {
249
+ status: response.status,
250
+ statusText: response.statusText,
251
+ headers: newHeaders
252
+ });
253
+ };
254
+ }
255
+ function createSecurityHeaders(options = {}) {
256
+ const { preset, config } = options;
257
+ let baseConfig = preset ? getPreset(preset) : PRESET_STRICT;
258
+ if (config) {
259
+ baseConfig = mergeConfigs(baseConfig, config);
260
+ }
261
+ return buildHeaders(baseConfig);
262
+ }
263
+ function createSecurityHeadersObject(options = {}) {
264
+ const headers = createSecurityHeaders(options);
265
+ const obj = {};
266
+ headers.forEach((value, key) => {
267
+ obj[key] = value;
268
+ });
269
+ return obj;
9
270
  }
10
271
 
11
- exports.createCsp = createCsp;
12
- exports.securityHeaders = securityHeaders;
272
+ exports.PRESET_API = PRESET_API;
273
+ exports.PRESET_RELAXED = PRESET_RELAXED;
274
+ exports.PRESET_STRICT = PRESET_STRICT;
275
+ exports.buildCSP = buildCSP;
276
+ exports.buildHSTS = buildHSTS;
277
+ exports.buildHeaders = buildHeaders;
278
+ exports.buildPermissionsPolicy = buildPermissionsPolicy;
279
+ exports.createSecurityHeaders = createSecurityHeaders;
280
+ exports.createSecurityHeadersObject = createSecurityHeadersObject;
281
+ exports.getPreset = getPreset;
282
+ exports.withSecurityHeaders = withSecurityHeaders;
13
283
  //# sourceMappingURL=headers.cjs.map
14
284
  //# sourceMappingURL=headers.cjs.map