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