nextjs-secure 0.1.1 → 0.2.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.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/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var crypto$1 = require('crypto');
4
+
3
5
  // src/core/errors.ts
4
6
  var SecureError = class extends Error {
5
7
  /**
@@ -994,9 +996,190 @@ function clearAllRateLimits() {
994
996
  defaultStore.clear();
995
997
  }
996
998
  }
999
+ var encoder = new TextEncoder();
1000
+ function randomBytes(length) {
1001
+ const bytes = new Uint8Array(length);
1002
+ crypto$1.webcrypto.getRandomValues(bytes);
1003
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1004
+ }
1005
+ async function createSignature(data, secret) {
1006
+ const key = await crypto$1.webcrypto.subtle.importKey(
1007
+ "raw",
1008
+ encoder.encode(secret),
1009
+ { name: "HMAC", hash: "SHA-256" },
1010
+ false,
1011
+ ["sign"]
1012
+ );
1013
+ const sig = await crypto$1.webcrypto.subtle.sign("HMAC", key, encoder.encode(data));
1014
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1015
+ }
1016
+ function safeCompare(a, b) {
1017
+ if (a.length !== b.length) return false;
1018
+ let result = 0;
1019
+ for (let i = 0; i < a.length; i++) {
1020
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
1021
+ }
1022
+ return result === 0;
1023
+ }
1024
+ async function createToken(secret, length = 32) {
1025
+ const data = randomBytes(length);
1026
+ const sig = await createSignature(data, secret);
1027
+ return `${data}.${sig}`;
1028
+ }
1029
+ async function verifyToken(token, secret) {
1030
+ if (!token || typeof token !== "string") return false;
1031
+ const parts = token.split(".");
1032
+ if (parts.length !== 2) return false;
1033
+ const [data, sig] = parts;
1034
+ if (!data || !sig) return false;
1035
+ try {
1036
+ const expected = await createSignature(data, secret);
1037
+ return safeCompare(sig, expected);
1038
+ } catch {
1039
+ return false;
1040
+ }
1041
+ }
1042
+ function tokensMatch(a, b) {
1043
+ if (!a || !b) return false;
1044
+ return safeCompare(a, b);
1045
+ }
1046
+
1047
+ // src/middleware/csrf/middleware.ts
1048
+ var DEFAULT_COOKIE = {
1049
+ name: "__csrf",
1050
+ path: "/",
1051
+ httpOnly: true,
1052
+ secure: process.env.NODE_ENV === "production",
1053
+ sameSite: "strict",
1054
+ maxAge: 86400
1055
+ // 24h
1056
+ };
1057
+ var DEFAULT_CONFIG2 = {
1058
+ headerName: "x-csrf-token",
1059
+ fieldName: "_csrf",
1060
+ tokenLength: 32,
1061
+ protectedMethods: ["POST", "PUT", "PATCH", "DELETE"]
1062
+ };
1063
+ function getSecret(config) {
1064
+ const secret = config.secret || process.env.CSRF_SECRET;
1065
+ if (!secret) {
1066
+ throw new Error(
1067
+ "CSRF secret is required. Set config.secret or CSRF_SECRET env variable."
1068
+ );
1069
+ }
1070
+ return secret;
1071
+ }
1072
+ function buildCookieString(name, value, opts) {
1073
+ let cookie = `${name}=${value}`;
1074
+ if (opts.path) cookie += `; Path=${opts.path}`;
1075
+ if (opts.domain) cookie += `; Domain=${opts.domain}`;
1076
+ if (opts.maxAge) cookie += `; Max-Age=${opts.maxAge}`;
1077
+ if (opts.httpOnly) cookie += "; HttpOnly";
1078
+ if (opts.secure) cookie += "; Secure";
1079
+ if (opts.sameSite) cookie += `; SameSite=${opts.sameSite}`;
1080
+ return cookie;
1081
+ }
1082
+ async function extractToken(req, headerName, fieldName) {
1083
+ const headerToken = req.headers.get(headerName);
1084
+ if (headerToken) return headerToken;
1085
+ const contentType = req.headers.get("content-type") || "";
1086
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1087
+ try {
1088
+ const cloned = req.clone();
1089
+ const formData = await cloned.formData();
1090
+ const token = formData.get(fieldName);
1091
+ if (typeof token === "string") return token;
1092
+ } catch {
1093
+ }
1094
+ }
1095
+ if (contentType.includes("application/json")) {
1096
+ try {
1097
+ const cloned = req.clone();
1098
+ const body = await cloned.json();
1099
+ if (body && typeof body[fieldName] === "string") {
1100
+ return body[fieldName];
1101
+ }
1102
+ } catch {
1103
+ }
1104
+ }
1105
+ return null;
1106
+ }
1107
+ function defaultErrorResponse(_req, reason) {
1108
+ return new Response(JSON.stringify({ error: "CSRF validation failed", reason }), {
1109
+ status: 403,
1110
+ headers: { "Content-Type": "application/json" }
1111
+ });
1112
+ }
1113
+ function withCSRF(handler, config = {}) {
1114
+ const secret = getSecret(config);
1115
+ const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
1116
+ const headerName = config.headerName || DEFAULT_CONFIG2.headerName;
1117
+ const fieldName = config.fieldName || DEFAULT_CONFIG2.fieldName;
1118
+ const protectedMethods = config.protectedMethods || DEFAULT_CONFIG2.protectedMethods;
1119
+ const onError = config.onError || defaultErrorResponse;
1120
+ return async (req) => {
1121
+ const method = req.method.toUpperCase();
1122
+ if (!protectedMethods.includes(method)) {
1123
+ return handler(req);
1124
+ }
1125
+ if (config.skip) {
1126
+ const shouldSkip = await config.skip(req);
1127
+ if (shouldSkip) return handler(req);
1128
+ }
1129
+ const cookieName = cookieOpts.name || "__csrf";
1130
+ const cookieToken = req.cookies.get(cookieName)?.value;
1131
+ if (!cookieToken) {
1132
+ return onError(req, "missing_cookie");
1133
+ }
1134
+ const cookieValid = await verifyToken(cookieToken, secret);
1135
+ if (!cookieValid) {
1136
+ return onError(req, "invalid_cookie");
1137
+ }
1138
+ const requestToken = await extractToken(req, headerName, fieldName);
1139
+ if (!requestToken) {
1140
+ return onError(req, "missing_token");
1141
+ }
1142
+ if (!tokensMatch(cookieToken, requestToken)) {
1143
+ return onError(req, "token_mismatch");
1144
+ }
1145
+ return handler(req);
1146
+ };
1147
+ }
1148
+ async function generateCSRF(config = {}) {
1149
+ const secret = getSecret(config);
1150
+ const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
1151
+ const tokenLength = config.tokenLength || DEFAULT_CONFIG2.tokenLength;
1152
+ const cookieName = cookieOpts.name || "__csrf";
1153
+ const token = await createToken(secret, tokenLength);
1154
+ const cookieHeader = buildCookieString(cookieName, token, cookieOpts);
1155
+ return { token, cookieHeader };
1156
+ }
1157
+ async function validateCSRF(req, config = {}) {
1158
+ const secret = getSecret(config);
1159
+ const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
1160
+ const headerName = config.headerName || DEFAULT_CONFIG2.headerName;
1161
+ const fieldName = config.fieldName || DEFAULT_CONFIG2.fieldName;
1162
+ const cookieName = cookieOpts.name || "__csrf";
1163
+ const cookieToken = req.cookies.get(cookieName)?.value;
1164
+ if (!cookieToken) {
1165
+ return { valid: false, reason: "missing_cookie" };
1166
+ }
1167
+ const cookieValid = await verifyToken(cookieToken, secret);
1168
+ if (!cookieValid) {
1169
+ return { valid: false, reason: "invalid_cookie" };
1170
+ }
1171
+ const requestToken = await extractToken(req, headerName, fieldName);
1172
+ if (!requestToken) {
1173
+ return { valid: false, reason: "missing_token" };
1174
+ }
1175
+ if (!tokensMatch(cookieToken, requestToken)) {
1176
+ return { valid: false, reason: "token_mismatch" };
1177
+ }
1178
+ return { valid: true };
1179
+ }
997
1180
 
998
1181
  // src/index.ts
999
- var VERSION = "0.1.0";
1182
+ var VERSION = "0.2.0";
1000
1183
 
1001
1184
  exports.AuthenticationError = AuthenticationError;
1002
1185
  exports.AuthorizationError = AuthorizationError;
@@ -1010,9 +1193,11 @@ exports.ValidationError = ValidationError;
1010
1193
  exports.anonymizeIp = anonymizeIp;
1011
1194
  exports.checkRateLimit = checkRateLimit;
1012
1195
  exports.clearAllRateLimits = clearAllRateLimits;
1196
+ exports.createCSRFToken = createToken;
1013
1197
  exports.createMemoryStore = createMemoryStore;
1014
1198
  exports.createRateLimiter = createRateLimiter;
1015
1199
  exports.formatDuration = formatDuration;
1200
+ exports.generateCSRF = generateCSRF;
1016
1201
  exports.getClientIp = getClientIp;
1017
1202
  exports.getGeoInfo = getGeoInfo;
1018
1203
  exports.getGlobalMemoryStore = getGlobalMemoryStore;
@@ -1028,6 +1213,10 @@ exports.parseDuration = parseDuration;
1028
1213
  exports.resetRateLimit = resetRateLimit;
1029
1214
  exports.sleep = sleep;
1030
1215
  exports.toSecureError = toSecureError;
1216
+ exports.tokensMatch = tokensMatch;
1217
+ exports.validateCSRF = validateCSRF;
1218
+ exports.verifyCSRFToken = verifyToken;
1219
+ exports.withCSRF = withCSRF;
1031
1220
  exports.withRateLimit = withRateLimit;
1032
1221
  //# sourceMappingURL=index.cjs.map
1033
1222
  //# sourceMappingURL=index.cjs.map