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