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/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,28 +996,489 @@ 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
+ }
1180
+
1181
+ // src/middleware/headers/builder.ts
1182
+ function buildCSP(policy) {
1183
+ const directives = [];
1184
+ const directiveMap = {
1185
+ defaultSrc: "default-src",
1186
+ scriptSrc: "script-src",
1187
+ styleSrc: "style-src",
1188
+ imgSrc: "img-src",
1189
+ fontSrc: "font-src",
1190
+ connectSrc: "connect-src",
1191
+ mediaSrc: "media-src",
1192
+ objectSrc: "object-src",
1193
+ frameSrc: "frame-src",
1194
+ childSrc: "child-src",
1195
+ workerSrc: "worker-src",
1196
+ frameAncestors: "frame-ancestors",
1197
+ formAction: "form-action",
1198
+ baseUri: "base-uri",
1199
+ manifestSrc: "manifest-src",
1200
+ reportUri: "report-uri",
1201
+ reportTo: "report-to"
1202
+ };
1203
+ for (const [key, directive] of Object.entries(directiveMap)) {
1204
+ const value = policy[key];
1205
+ if (value !== void 0 && value !== false) {
1206
+ if (Array.isArray(value)) {
1207
+ directives.push(`${directive} ${value.join(" ")}`);
1208
+ } else if (typeof value === "string") {
1209
+ directives.push(`${directive} ${value}`);
1210
+ }
1211
+ }
1212
+ }
1213
+ if (policy.upgradeInsecureRequests) {
1214
+ directives.push("upgrade-insecure-requests");
1215
+ }
1216
+ if (policy.blockAllMixedContent) {
1217
+ directives.push("block-all-mixed-content");
1218
+ }
1219
+ return directives.join("; ");
1220
+ }
1221
+ function buildHSTS(config) {
1222
+ let value = `max-age=${config.maxAge}`;
1223
+ if (config.includeSubDomains) {
1224
+ value += "; includeSubDomains";
1225
+ }
1226
+ if (config.preload) {
1227
+ value += "; preload";
1228
+ }
1229
+ return value;
1230
+ }
1231
+ function buildPermissionsPolicy(policy) {
1232
+ const directives = [];
1233
+ const featureMap = {
1234
+ accelerometer: "accelerometer",
1235
+ ambientLightSensor: "ambient-light-sensor",
1236
+ autoplay: "autoplay",
1237
+ battery: "battery",
1238
+ camera: "camera",
1239
+ displayCapture: "display-capture",
1240
+ documentDomain: "document-domain",
1241
+ encryptedMedia: "encrypted-media",
1242
+ fullscreen: "fullscreen",
1243
+ geolocation: "geolocation",
1244
+ gyroscope: "gyroscope",
1245
+ magnetometer: "magnetometer",
1246
+ microphone: "microphone",
1247
+ midi: "midi",
1248
+ payment: "payment",
1249
+ pictureInPicture: "picture-in-picture",
1250
+ publicKeyCredentialsGet: "publickey-credentials-get",
1251
+ screenWakeLock: "screen-wake-lock",
1252
+ syncXhr: "sync-xhr",
1253
+ usb: "usb",
1254
+ webShare: "web-share",
1255
+ xrSpatialTracking: "xr-spatial-tracking"
1256
+ };
1257
+ for (const [key, feature] of Object.entries(featureMap)) {
1258
+ const origins = policy[key];
1259
+ if (origins !== void 0) {
1260
+ if (origins.length === 0) {
1261
+ directives.push(`${feature}=()`);
1262
+ } else {
1263
+ const formatted = origins.map((o) => o === "self" ? "self" : `"${o}"`).join(" ");
1264
+ directives.push(`${feature}=(${formatted})`);
1265
+ }
1266
+ }
1267
+ }
1268
+ return directives.join(", ");
1269
+ }
1270
+ var PRESET_STRICT = {
1271
+ contentSecurityPolicy: {
1272
+ defaultSrc: ["'self'"],
1273
+ scriptSrc: ["'self'"],
1274
+ styleSrc: ["'self'"],
1275
+ imgSrc: ["'self'", "data:"],
1276
+ fontSrc: ["'self'"],
1277
+ objectSrc: ["'none'"],
1278
+ frameAncestors: ["'none'"],
1279
+ formAction: ["'self'"],
1280
+ baseUri: ["'self'"],
1281
+ upgradeInsecureRequests: true
1282
+ },
1283
+ strictTransportSecurity: {
1284
+ maxAge: 31536e3,
1285
+ // 1 year
1286
+ includeSubDomains: true,
1287
+ preload: true
1288
+ },
1289
+ xFrameOptions: "DENY",
1290
+ xContentTypeOptions: true,
1291
+ xDnsPrefetchControl: "off",
1292
+ xDownloadOptions: true,
1293
+ xPermittedCrossDomainPolicies: "none",
1294
+ referrerPolicy: "strict-origin-when-cross-origin",
1295
+ crossOriginOpenerPolicy: "same-origin",
1296
+ crossOriginEmbedderPolicy: "require-corp",
1297
+ crossOriginResourcePolicy: "same-origin",
1298
+ permissionsPolicy: {
1299
+ camera: [],
1300
+ microphone: [],
1301
+ geolocation: [],
1302
+ payment: []
1303
+ },
1304
+ originAgentCluster: true
1305
+ };
1306
+ var PRESET_RELAXED = {
1307
+ contentSecurityPolicy: {
1308
+ defaultSrc: ["'self'"],
1309
+ scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
1310
+ styleSrc: ["'self'", "'unsafe-inline'"],
1311
+ imgSrc: ["'self'", "data:", "blob:", "https:"],
1312
+ fontSrc: ["'self'", "https:", "data:"],
1313
+ connectSrc: ["'self'", "https:", "wss:"],
1314
+ frameSrc: ["'self'"]
1315
+ },
1316
+ strictTransportSecurity: {
1317
+ maxAge: 86400,
1318
+ // 1 day
1319
+ includeSubDomains: false
1320
+ },
1321
+ xFrameOptions: "SAMEORIGIN",
1322
+ xContentTypeOptions: true,
1323
+ referrerPolicy: "no-referrer-when-downgrade"
1324
+ };
1325
+ var PRESET_API = {
1326
+ contentSecurityPolicy: {
1327
+ defaultSrc: ["'none'"],
1328
+ frameAncestors: ["'none'"]
1329
+ },
1330
+ strictTransportSecurity: {
1331
+ maxAge: 31536e3,
1332
+ includeSubDomains: true
1333
+ },
1334
+ xFrameOptions: "DENY",
1335
+ xContentTypeOptions: true,
1336
+ referrerPolicy: "no-referrer",
1337
+ crossOriginResourcePolicy: "same-origin"
1338
+ };
1339
+ function getPreset(name) {
1340
+ switch (name) {
1341
+ case "strict":
1342
+ return PRESET_STRICT;
1343
+ case "relaxed":
1344
+ return PRESET_RELAXED;
1345
+ case "api":
1346
+ return PRESET_API;
1347
+ default:
1348
+ return PRESET_STRICT;
1349
+ }
1350
+ }
1351
+ function buildHeaders(config) {
1352
+ const headers = new Headers();
1353
+ if (config.contentSecurityPolicy) {
1354
+ const csp = buildCSP(config.contentSecurityPolicy);
1355
+ if (csp) headers.set("Content-Security-Policy", csp);
1356
+ }
1357
+ if (config.strictTransportSecurity) {
1358
+ headers.set("Strict-Transport-Security", buildHSTS(config.strictTransportSecurity));
1359
+ }
1360
+ if (config.xFrameOptions) {
1361
+ headers.set("X-Frame-Options", config.xFrameOptions);
1362
+ }
1363
+ if (config.xContentTypeOptions) {
1364
+ headers.set("X-Content-Type-Options", "nosniff");
1365
+ }
1366
+ if (config.xDnsPrefetchControl) {
1367
+ headers.set("X-DNS-Prefetch-Control", config.xDnsPrefetchControl);
1368
+ }
1369
+ if (config.xDownloadOptions) {
1370
+ headers.set("X-Download-Options", "noopen");
1371
+ }
1372
+ if (config.xPermittedCrossDomainPolicies) {
1373
+ headers.set("X-Permitted-Cross-Domain-Policies", config.xPermittedCrossDomainPolicies);
1374
+ }
1375
+ if (config.referrerPolicy) {
1376
+ const value = Array.isArray(config.referrerPolicy) ? config.referrerPolicy.join(", ") : config.referrerPolicy;
1377
+ headers.set("Referrer-Policy", value);
1378
+ }
1379
+ if (config.crossOriginOpenerPolicy) {
1380
+ headers.set("Cross-Origin-Opener-Policy", config.crossOriginOpenerPolicy);
1381
+ }
1382
+ if (config.crossOriginEmbedderPolicy) {
1383
+ headers.set("Cross-Origin-Embedder-Policy", config.crossOriginEmbedderPolicy);
1384
+ }
1385
+ if (config.crossOriginResourcePolicy) {
1386
+ headers.set("Cross-Origin-Resource-Policy", config.crossOriginResourcePolicy);
1387
+ }
1388
+ if (config.permissionsPolicy) {
1389
+ const pp = buildPermissionsPolicy(config.permissionsPolicy);
1390
+ if (pp) headers.set("Permissions-Policy", pp);
1391
+ }
1392
+ if (config.originAgentCluster) {
1393
+ headers.set("Origin-Agent-Cluster", "?1");
1394
+ }
1395
+ return headers;
1396
+ }
1397
+
1398
+ // src/middleware/headers/middleware.ts
1399
+ function mergeConfigs(base, custom) {
1400
+ return {
1401
+ ...base,
1402
+ ...custom,
1403
+ // Deep merge CSP if both exist
1404
+ contentSecurityPolicy: custom.contentSecurityPolicy === false ? false : custom.contentSecurityPolicy ? base.contentSecurityPolicy ? { ...base.contentSecurityPolicy, ...custom.contentSecurityPolicy } : custom.contentSecurityPolicy : base.contentSecurityPolicy,
1405
+ // Deep merge HSTS if both exist
1406
+ strictTransportSecurity: custom.strictTransportSecurity === false ? false : custom.strictTransportSecurity ? base.strictTransportSecurity ? { ...base.strictTransportSecurity, ...custom.strictTransportSecurity } : custom.strictTransportSecurity : base.strictTransportSecurity,
1407
+ // Deep merge Permissions-Policy if both exist
1408
+ permissionsPolicy: custom.permissionsPolicy === false ? false : custom.permissionsPolicy ? base.permissionsPolicy ? { ...base.permissionsPolicy, ...custom.permissionsPolicy } : custom.permissionsPolicy : base.permissionsPolicy
1409
+ };
1410
+ }
1411
+ function withSecurityHeaders(handler, options = {}) {
1412
+ const { preset, config, override = false } = options;
1413
+ let baseConfig = preset ? getPreset(preset) : PRESET_STRICT;
1414
+ if (config) {
1415
+ baseConfig = mergeConfigs(baseConfig, config);
1416
+ }
1417
+ const securityHeaders = buildHeaders(baseConfig);
1418
+ return async (req) => {
1419
+ const response = await handler(req);
1420
+ const newHeaders = new Headers(response.headers);
1421
+ securityHeaders.forEach((value, key) => {
1422
+ if (override || !newHeaders.has(key)) {
1423
+ newHeaders.set(key, value);
1424
+ }
1425
+ });
1426
+ return new Response(response.body, {
1427
+ status: response.status,
1428
+ statusText: response.statusText,
1429
+ headers: newHeaders
1430
+ });
1431
+ };
1432
+ }
1433
+ function createSecurityHeaders(options = {}) {
1434
+ const { preset, config } = options;
1435
+ let baseConfig = preset ? getPreset(preset) : PRESET_STRICT;
1436
+ if (config) {
1437
+ baseConfig = mergeConfigs(baseConfig, config);
1438
+ }
1439
+ return buildHeaders(baseConfig);
1440
+ }
1441
+ function createSecurityHeadersObject(options = {}) {
1442
+ const headers = createSecurityHeaders(options);
1443
+ const obj = {};
1444
+ headers.forEach((value, key) => {
1445
+ obj[key] = value;
1446
+ });
1447
+ return obj;
1448
+ }
997
1449
 
998
1450
  // src/index.ts
999
- var VERSION = "0.1.0";
1451
+ var VERSION = "0.3.0";
1000
1452
 
1001
1453
  exports.AuthenticationError = AuthenticationError;
1002
1454
  exports.AuthorizationError = AuthorizationError;
1003
1455
  exports.ConfigurationError = ConfigurationError;
1004
1456
  exports.CsrfError = CsrfError;
1005
1457
  exports.MemoryStore = MemoryStore;
1458
+ exports.PRESET_API = PRESET_API;
1459
+ exports.PRESET_RELAXED = PRESET_RELAXED;
1460
+ exports.PRESET_STRICT = PRESET_STRICT;
1006
1461
  exports.RateLimitError = RateLimitError;
1007
1462
  exports.SecureError = SecureError;
1008
1463
  exports.VERSION = VERSION;
1009
1464
  exports.ValidationError = ValidationError;
1010
1465
  exports.anonymizeIp = anonymizeIp;
1466
+ exports.buildCSP = buildCSP;
1467
+ exports.buildHSTS = buildHSTS;
1468
+ exports.buildPermissionsPolicy = buildPermissionsPolicy;
1011
1469
  exports.checkRateLimit = checkRateLimit;
1012
1470
  exports.clearAllRateLimits = clearAllRateLimits;
1471
+ exports.createCSRFToken = createToken;
1013
1472
  exports.createMemoryStore = createMemoryStore;
1014
1473
  exports.createRateLimiter = createRateLimiter;
1474
+ exports.createSecurityHeaders = createSecurityHeaders;
1475
+ exports.createSecurityHeadersObject = createSecurityHeadersObject;
1015
1476
  exports.formatDuration = formatDuration;
1477
+ exports.generateCSRF = generateCSRF;
1016
1478
  exports.getClientIp = getClientIp;
1017
1479
  exports.getGeoInfo = getGeoInfo;
1018
1480
  exports.getGlobalMemoryStore = getGlobalMemoryStore;
1481
+ exports.getPreset = getPreset;
1019
1482
  exports.getRateLimitStatus = getRateLimitStatus;
1020
1483
  exports.isLocalhost = isLocalhost;
1021
1484
  exports.isPrivateIp = isPrivateIp;
@@ -1028,6 +1491,11 @@ exports.parseDuration = parseDuration;
1028
1491
  exports.resetRateLimit = resetRateLimit;
1029
1492
  exports.sleep = sleep;
1030
1493
  exports.toSecureError = toSecureError;
1494
+ exports.tokensMatch = tokensMatch;
1495
+ exports.validateCSRF = validateCSRF;
1496
+ exports.verifyCSRFToken = verifyToken;
1497
+ exports.withCSRF = withCSRF;
1031
1498
  exports.withRateLimit = withRateLimit;
1499
+ exports.withSecurityHeaders = withSecurityHeaders;
1032
1500
  //# sourceMappingURL=index.cjs.map
1033
1501
  //# sourceMappingURL=index.cjs.map