nextjs-secure 0.3.0 → 0.5.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
@@ -1444,10 +1444,2373 @@ function createSecurityHeadersObject(options = {}) {
1444
1444
  });
1445
1445
  return obj;
1446
1446
  }
1447
+ var encoder2 = new TextEncoder();
1448
+ var decoder = new TextDecoder();
1449
+ function base64UrlDecode(str) {
1450
+ const pad = str.length % 4;
1451
+ if (pad) {
1452
+ str += "=".repeat(4 - pad);
1453
+ }
1454
+ const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
1455
+ const binary = atob(base64);
1456
+ const bytes = new Uint8Array(binary.length);
1457
+ for (let i = 0; i < binary.length; i++) {
1458
+ bytes[i] = binary.charCodeAt(i);
1459
+ }
1460
+ return bytes;
1461
+ }
1462
+ function decodeJWT(token) {
1463
+ try {
1464
+ const parts = token.split(".");
1465
+ if (parts.length !== 3) return null;
1466
+ const header = JSON.parse(decoder.decode(base64UrlDecode(parts[0])));
1467
+ const payload = JSON.parse(decoder.decode(base64UrlDecode(parts[1])));
1468
+ const signature = base64UrlDecode(parts[2]);
1469
+ return { header, payload, signature };
1470
+ } catch {
1471
+ return null;
1472
+ }
1473
+ }
1474
+ function getAlgorithmParams(alg) {
1475
+ switch (alg) {
1476
+ case "HS256":
1477
+ return { name: "HMAC", hash: "SHA-256" };
1478
+ case "HS384":
1479
+ return { name: "HMAC", hash: "SHA-384" };
1480
+ case "HS512":
1481
+ return { name: "HMAC", hash: "SHA-512" };
1482
+ case "RS256":
1483
+ return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
1484
+ case "RS384":
1485
+ return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" };
1486
+ case "RS512":
1487
+ return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" };
1488
+ case "ES256":
1489
+ return { name: "ECDSA", hash: "SHA-256", namedCurve: "P-256" };
1490
+ case "ES384":
1491
+ return { name: "ECDSA", hash: "SHA-384", namedCurve: "P-384" };
1492
+ case "ES512":
1493
+ return { name: "ECDSA", hash: "SHA-512", namedCurve: "P-521" };
1494
+ default:
1495
+ return null;
1496
+ }
1497
+ }
1498
+ async function verifyHMAC(data, signature, secret, hash) {
1499
+ const key = await webcrypto.subtle.importKey(
1500
+ "raw",
1501
+ encoder2.encode(secret),
1502
+ { name: "HMAC", hash },
1503
+ false,
1504
+ ["verify"]
1505
+ );
1506
+ return webcrypto.subtle.verify("HMAC", key, signature, encoder2.encode(data));
1507
+ }
1508
+ async function importPublicKey(pem, algorithm) {
1509
+ const pemContents = pem.replace(/-----BEGIN.*-----/, "").replace(/-----END.*-----/, "").replace(/\s/g, "");
1510
+ const binaryDer = base64UrlDecode(pemContents.replace(/\+/g, "-").replace(/\//g, "_"));
1511
+ const keyUsages = ["verify"];
1512
+ if (algorithm.name === "RSASSA-PKCS1-v1_5") {
1513
+ return webcrypto.subtle.importKey(
1514
+ "spki",
1515
+ binaryDer,
1516
+ { name: algorithm.name, hash: algorithm.hash },
1517
+ false,
1518
+ keyUsages
1519
+ );
1520
+ }
1521
+ if (algorithm.name === "ECDSA") {
1522
+ return webcrypto.subtle.importKey(
1523
+ "spki",
1524
+ binaryDer,
1525
+ { name: algorithm.name, namedCurve: algorithm.namedCurve },
1526
+ false,
1527
+ keyUsages
1528
+ );
1529
+ }
1530
+ throw new Error(`Unsupported algorithm: ${algorithm.name}`);
1531
+ }
1532
+ async function verifyAsymmetric(data, signature, publicKey, algorithm) {
1533
+ const key = await importPublicKey(publicKey, algorithm);
1534
+ const params = algorithm.name === "ECDSA" ? { name: "ECDSA", hash: algorithm.hash } : algorithm.name;
1535
+ return webcrypto.subtle.verify(params, key, signature, encoder2.encode(data));
1536
+ }
1537
+ function validateClaims(payload, config) {
1538
+ const now = Math.floor(Date.now() / 1e3);
1539
+ const tolerance = config.clockTolerance || 0;
1540
+ if (payload.exp !== void 0 && payload.exp < now - tolerance) {
1541
+ return {
1542
+ code: "expired_token",
1543
+ message: "Token has expired",
1544
+ status: 401
1545
+ };
1546
+ }
1547
+ if (payload.nbf !== void 0 && payload.nbf > now + tolerance) {
1548
+ return {
1549
+ code: "invalid_token",
1550
+ message: "Token not yet valid",
1551
+ status: 401
1552
+ };
1553
+ }
1554
+ if (config.issuer) {
1555
+ const issuers = Array.isArray(config.issuer) ? config.issuer : [config.issuer];
1556
+ if (!payload.iss || !issuers.includes(payload.iss)) {
1557
+ return {
1558
+ code: "invalid_token",
1559
+ message: "Invalid token issuer",
1560
+ status: 401
1561
+ };
1562
+ }
1563
+ }
1564
+ if (config.audience) {
1565
+ const audiences = Array.isArray(config.audience) ? config.audience : [config.audience];
1566
+ const tokenAudiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
1567
+ const hasValidAudience = audiences.some((aud) => tokenAudiences.includes(aud));
1568
+ if (!hasValidAudience) {
1569
+ return {
1570
+ code: "invalid_token",
1571
+ message: "Invalid token audience",
1572
+ status: 401
1573
+ };
1574
+ }
1575
+ }
1576
+ return null;
1577
+ }
1578
+ async function verifyJWT(token, config) {
1579
+ const decoded = decodeJWT(token);
1580
+ if (!decoded) {
1581
+ return {
1582
+ payload: null,
1583
+ error: {
1584
+ code: "invalid_token",
1585
+ message: "Malformed token",
1586
+ status: 401
1587
+ }
1588
+ };
1589
+ }
1590
+ const { header, payload, signature } = decoded;
1591
+ const alg = header.alg;
1592
+ const allowedAlgorithms = config.algorithms || ["HS256"];
1593
+ if (!allowedAlgorithms.includes(alg)) {
1594
+ return {
1595
+ payload: null,
1596
+ error: {
1597
+ code: "invalid_token",
1598
+ message: `Algorithm ${alg} not allowed`,
1599
+ status: 401
1600
+ }
1601
+ };
1602
+ }
1603
+ const algorithmParams = getAlgorithmParams(alg);
1604
+ if (!algorithmParams) {
1605
+ return {
1606
+ payload: null,
1607
+ error: {
1608
+ code: "invalid_token",
1609
+ message: `Unsupported algorithm: ${alg}`,
1610
+ status: 401
1611
+ }
1612
+ };
1613
+ }
1614
+ const parts = token.split(".");
1615
+ const signedData = `${parts[0]}.${parts[1]}`;
1616
+ let isValid = false;
1617
+ try {
1618
+ if (algorithmParams.name === "HMAC") {
1619
+ if (!config.secret) {
1620
+ return {
1621
+ payload: null,
1622
+ error: {
1623
+ code: "invalid_token",
1624
+ message: "Secret required for HMAC algorithms",
1625
+ status: 500
1626
+ }
1627
+ };
1628
+ }
1629
+ isValid = await verifyHMAC(signedData, signature, config.secret, algorithmParams.hash);
1630
+ } else {
1631
+ if (!config.publicKey) {
1632
+ return {
1633
+ payload: null,
1634
+ error: {
1635
+ code: "invalid_token",
1636
+ message: "Public key required for asymmetric algorithms",
1637
+ status: 500
1638
+ }
1639
+ };
1640
+ }
1641
+ isValid = await verifyAsymmetric(signedData, signature, config.publicKey, algorithmParams);
1642
+ }
1643
+ } catch {
1644
+ isValid = false;
1645
+ }
1646
+ if (!isValid) {
1647
+ return {
1648
+ payload: null,
1649
+ error: {
1650
+ code: "invalid_signature",
1651
+ message: "Invalid token signature",
1652
+ status: 401
1653
+ }
1654
+ };
1655
+ }
1656
+ const claimsError = validateClaims(payload, config);
1657
+ if (claimsError) {
1658
+ return { payload: null, error: claimsError };
1659
+ }
1660
+ return { payload, error: null };
1661
+ }
1662
+ function extractBearerToken(authHeader) {
1663
+ if (!authHeader) return null;
1664
+ if (!authHeader.startsWith("Bearer ")) return null;
1665
+ return authHeader.slice(7);
1666
+ }
1667
+
1668
+ // src/middleware/auth/middleware.ts
1669
+ function defaultErrorResponse2(_req, error) {
1670
+ return new Response(
1671
+ JSON.stringify({
1672
+ error: error.code,
1673
+ message: error.message
1674
+ }),
1675
+ {
1676
+ status: error.status,
1677
+ headers: { "Content-Type": "application/json" }
1678
+ }
1679
+ );
1680
+ }
1681
+ async function getTokenFromRequest(req, config) {
1682
+ if (config?.getToken) {
1683
+ return config.getToken(req);
1684
+ }
1685
+ return extractBearerToken(req.headers.get("authorization"));
1686
+ }
1687
+ function withJWT(handler, config) {
1688
+ const secret = config.secret || process.env.JWT_SECRET;
1689
+ const effectiveConfig = { ...config, secret };
1690
+ return async (req) => {
1691
+ const token = await getTokenFromRequest(req, effectiveConfig);
1692
+ if (!token) {
1693
+ return defaultErrorResponse2(req, {
1694
+ code: "missing_token",
1695
+ message: "Authentication required",
1696
+ status: 401
1697
+ });
1698
+ }
1699
+ const { payload, error } = await verifyJWT(token, effectiveConfig);
1700
+ if (error) {
1701
+ return defaultErrorResponse2(req, error);
1702
+ }
1703
+ const user = effectiveConfig.mapUser ? await effectiveConfig.mapUser(payload) : {
1704
+ id: payload.sub || "",
1705
+ email: payload.email,
1706
+ name: payload.name,
1707
+ roles: payload.roles,
1708
+ permissions: payload.permissions
1709
+ };
1710
+ return handler(req, { user, token });
1711
+ };
1712
+ }
1713
+ function withAPIKey(handler, config) {
1714
+ const headerName = config.headerName || "x-api-key";
1715
+ const queryParam = config.queryParam || "api_key";
1716
+ return async (req) => {
1717
+ let apiKey = req.headers.get(headerName);
1718
+ if (!apiKey) {
1719
+ const url = new URL(req.url);
1720
+ apiKey = url.searchParams.get(queryParam);
1721
+ }
1722
+ if (!apiKey) {
1723
+ return defaultErrorResponse2(req, {
1724
+ code: "missing_api_key",
1725
+ message: "API key required",
1726
+ status: 401
1727
+ });
1728
+ }
1729
+ const user = await config.validate(apiKey, req);
1730
+ if (!user) {
1731
+ return defaultErrorResponse2(req, {
1732
+ code: "invalid_api_key",
1733
+ message: "Invalid API key",
1734
+ status: 401
1735
+ });
1736
+ }
1737
+ return handler(req, { user });
1738
+ };
1739
+ }
1740
+ function withSession(handler, config) {
1741
+ const cookieName = config.cookieName || "session";
1742
+ return async (req) => {
1743
+ const sessionId = req.cookies.get(cookieName)?.value;
1744
+ if (!sessionId) {
1745
+ return defaultErrorResponse2(req, {
1746
+ code: "missing_session",
1747
+ message: "Session required",
1748
+ status: 401
1749
+ });
1750
+ }
1751
+ const user = await config.validate(sessionId, req);
1752
+ if (!user) {
1753
+ return defaultErrorResponse2(req, {
1754
+ code: "invalid_session",
1755
+ message: "Invalid or expired session",
1756
+ status: 401
1757
+ });
1758
+ }
1759
+ return handler(req, { user });
1760
+ };
1761
+ }
1762
+ function withRoles(handler, config) {
1763
+ return async (req, ctx) => {
1764
+ const { user } = ctx;
1765
+ const userRoles = config.getUserRoles ? config.getUserRoles(user) : user.roles || [];
1766
+ if (config.roles && config.roles.length > 0) {
1767
+ const hasRole = config.roles.some((role) => userRoles.includes(role));
1768
+ if (!hasRole) {
1769
+ return defaultErrorResponse2(req, {
1770
+ code: "insufficient_roles",
1771
+ message: "Insufficient permissions",
1772
+ status: 403
1773
+ });
1774
+ }
1775
+ }
1776
+ const userPermissions = config.getUserPermissions ? config.getUserPermissions(user) : user.permissions || [];
1777
+ if (config.permissions && config.permissions.length > 0) {
1778
+ const hasAllPermissions = config.permissions.every(
1779
+ (perm) => userPermissions.includes(perm)
1780
+ );
1781
+ if (!hasAllPermissions) {
1782
+ return defaultErrorResponse2(req, {
1783
+ code: "insufficient_permissions",
1784
+ message: "Insufficient permissions",
1785
+ status: 403
1786
+ });
1787
+ }
1788
+ }
1789
+ if (config.authorize) {
1790
+ const authorized = await config.authorize(user, req);
1791
+ if (!authorized) {
1792
+ return defaultErrorResponse2(req, {
1793
+ code: "unauthorized",
1794
+ message: "Unauthorized",
1795
+ status: 403
1796
+ });
1797
+ }
1798
+ }
1799
+ return handler(req, ctx);
1800
+ };
1801
+ }
1802
+ function withAuth(handler, config) {
1803
+ const onError = config.onError || defaultErrorResponse2;
1804
+ return async (req) => {
1805
+ let user = null;
1806
+ let token;
1807
+ if (config.jwt) {
1808
+ const secret = config.jwt.secret || process.env.JWT_SECRET;
1809
+ const jwtConfig = { ...config.jwt, secret };
1810
+ const jwtToken = await getTokenFromRequest(req, jwtConfig);
1811
+ if (jwtToken) {
1812
+ const { payload, error } = await verifyJWT(jwtToken, jwtConfig);
1813
+ if (!error && payload) {
1814
+ user = jwtConfig.mapUser ? await jwtConfig.mapUser(payload) : {
1815
+ id: payload.sub || "",
1816
+ email: payload.email,
1817
+ name: payload.name,
1818
+ roles: payload.roles
1819
+ };
1820
+ token = jwtToken;
1821
+ }
1822
+ }
1823
+ }
1824
+ if (!user && config.apiKey) {
1825
+ const headerName = config.apiKey.headerName || "x-api-key";
1826
+ const queryParam = config.apiKey.queryParam || "api_key";
1827
+ let apiKey = req.headers.get(headerName);
1828
+ if (!apiKey) {
1829
+ const url = new URL(req.url);
1830
+ apiKey = url.searchParams.get(queryParam);
1831
+ }
1832
+ if (apiKey) {
1833
+ const apiUser = await config.apiKey.validate(apiKey, req);
1834
+ if (apiUser) {
1835
+ user = apiUser;
1836
+ }
1837
+ }
1838
+ }
1839
+ if (!user && config.session) {
1840
+ const cookieName = config.session.cookieName || "session";
1841
+ const sessionId = req.cookies.get(cookieName)?.value;
1842
+ if (sessionId) {
1843
+ const sessionUser = await config.session.validate(sessionId, req);
1844
+ if (sessionUser) {
1845
+ user = sessionUser;
1846
+ }
1847
+ }
1848
+ }
1849
+ if (!user) {
1850
+ return onError(req, {
1851
+ code: "unauthorized",
1852
+ message: "Authentication required",
1853
+ status: 401
1854
+ });
1855
+ }
1856
+ if (config.rbac) {
1857
+ const userRoles = config.rbac.getUserRoles ? config.rbac.getUserRoles(user) : user.roles || [];
1858
+ if (config.rbac.roles && config.rbac.roles.length > 0) {
1859
+ const hasRole = config.rbac.roles.some((role) => userRoles.includes(role));
1860
+ if (!hasRole) {
1861
+ return onError(req, {
1862
+ code: "insufficient_roles",
1863
+ message: "Insufficient permissions",
1864
+ status: 403
1865
+ });
1866
+ }
1867
+ }
1868
+ const userPermissions = config.rbac.getUserPermissions ? config.rbac.getUserPermissions(user) : user.permissions || [];
1869
+ if (config.rbac.permissions && config.rbac.permissions.length > 0) {
1870
+ const hasAllPermissions = config.rbac.permissions.every(
1871
+ (perm) => userPermissions.includes(perm)
1872
+ );
1873
+ if (!hasAllPermissions) {
1874
+ return onError(req, {
1875
+ code: "insufficient_permissions",
1876
+ message: "Insufficient permissions",
1877
+ status: 403
1878
+ });
1879
+ }
1880
+ }
1881
+ if (config.rbac.authorize) {
1882
+ const authorized = await config.rbac.authorize(user, req);
1883
+ if (!authorized) {
1884
+ return onError(req, {
1885
+ code: "unauthorized",
1886
+ message: "Unauthorized",
1887
+ status: 403
1888
+ });
1889
+ }
1890
+ }
1891
+ }
1892
+ if (config.onSuccess) {
1893
+ await config.onSuccess(req, user);
1894
+ }
1895
+ return handler(req, { user, token });
1896
+ };
1897
+ }
1898
+ function withOptionalAuth(handler, config) {
1899
+ return async (req) => {
1900
+ let user = null;
1901
+ let token;
1902
+ if (config.jwt) {
1903
+ const secret = config.jwt.secret || process.env.JWT_SECRET;
1904
+ const jwtConfig = { ...config.jwt, secret };
1905
+ const jwtToken = await getTokenFromRequest(req, jwtConfig);
1906
+ if (jwtToken) {
1907
+ const { payload, error } = await verifyJWT(jwtToken, jwtConfig);
1908
+ if (!error && payload) {
1909
+ user = jwtConfig.mapUser ? await jwtConfig.mapUser(payload) : {
1910
+ id: payload.sub || "",
1911
+ email: payload.email,
1912
+ name: payload.name,
1913
+ roles: payload.roles
1914
+ };
1915
+ token = jwtToken;
1916
+ }
1917
+ }
1918
+ }
1919
+ if (!user && config.apiKey) {
1920
+ const headerName = config.apiKey.headerName || "x-api-key";
1921
+ let apiKey = req.headers.get(headerName);
1922
+ if (apiKey) {
1923
+ const apiUser = await config.apiKey.validate(apiKey, req);
1924
+ if (apiUser) user = apiUser;
1925
+ }
1926
+ }
1927
+ if (!user && config.session) {
1928
+ const cookieName = config.session.cookieName || "session";
1929
+ const sessionId = req.cookies.get(cookieName)?.value;
1930
+ if (sessionId) {
1931
+ const sessionUser = await config.session.validate(sessionId, req);
1932
+ if (sessionUser) user = sessionUser;
1933
+ }
1934
+ }
1935
+ return handler(req, { user, token });
1936
+ };
1937
+ }
1938
+
1939
+ // src/middleware/validation/utils.ts
1940
+ function isZodSchema(schema) {
1941
+ return typeof schema === "object" && schema !== null && "safeParse" in schema && typeof schema.safeParse === "function";
1942
+ }
1943
+ function isCustomSchema(schema) {
1944
+ if (typeof schema !== "object" || schema === null) return false;
1945
+ if ("safeParse" in schema) return false;
1946
+ const entries = Object.entries(schema);
1947
+ if (entries.length === 0) return false;
1948
+ return entries.every(([_, rule]) => {
1949
+ return typeof rule === "object" && rule !== null && "type" in rule;
1950
+ });
1951
+ }
1952
+ var EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
1953
+ var URL_PATTERN = /^https?:\/\/(?:(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}|localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
1954
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1955
+ var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
1956
+ function validateField(value, rule, fieldName) {
1957
+ if (value === void 0 || value === null || value === "") {
1958
+ if (rule.required) {
1959
+ return {
1960
+ field: fieldName,
1961
+ code: "required",
1962
+ message: rule.message || `${fieldName} is required`,
1963
+ received: value
1964
+ };
1965
+ }
1966
+ return null;
1967
+ }
1968
+ switch (rule.type) {
1969
+ case "string":
1970
+ if (typeof value !== "string") {
1971
+ return {
1972
+ field: fieldName,
1973
+ code: "invalid_type",
1974
+ message: rule.message || `${fieldName} must be a string`,
1975
+ expected: "string",
1976
+ received: typeof value
1977
+ };
1978
+ }
1979
+ if (rule.minLength !== void 0 && value.length < rule.minLength) {
1980
+ return {
1981
+ field: fieldName,
1982
+ code: "too_short",
1983
+ message: rule.message || `${fieldName} must be at least ${rule.minLength} characters`,
1984
+ received: value.length
1985
+ };
1986
+ }
1987
+ if (rule.maxLength !== void 0 && value.length > rule.maxLength) {
1988
+ return {
1989
+ field: fieldName,
1990
+ code: "too_long",
1991
+ message: rule.message || `${fieldName} must be at most ${rule.maxLength} characters`,
1992
+ received: value.length
1993
+ };
1994
+ }
1995
+ if (rule.pattern && !rule.pattern.test(value)) {
1996
+ return {
1997
+ field: fieldName,
1998
+ code: "invalid_pattern",
1999
+ message: rule.message || `${fieldName} has invalid format`,
2000
+ received: value
2001
+ };
2002
+ }
2003
+ break;
2004
+ case "number":
2005
+ const num = typeof value === "number" ? value : Number(value);
2006
+ if (isNaN(num)) {
2007
+ return {
2008
+ field: fieldName,
2009
+ code: "invalid_type",
2010
+ message: rule.message || `${fieldName} must be a number`,
2011
+ expected: "number",
2012
+ received: typeof value
2013
+ };
2014
+ }
2015
+ if (rule.integer && !Number.isInteger(num)) {
2016
+ return {
2017
+ field: fieldName,
2018
+ code: "invalid_integer",
2019
+ message: rule.message || `${fieldName} must be an integer`,
2020
+ received: num
2021
+ };
2022
+ }
2023
+ if (rule.min !== void 0 && num < rule.min) {
2024
+ return {
2025
+ field: fieldName,
2026
+ code: "too_small",
2027
+ message: rule.message || `${fieldName} must be at least ${rule.min}`,
2028
+ received: num
2029
+ };
2030
+ }
2031
+ if (rule.max !== void 0 && num > rule.max) {
2032
+ return {
2033
+ field: fieldName,
2034
+ code: "too_large",
2035
+ message: rule.message || `${fieldName} must be at most ${rule.max}`,
2036
+ received: num
2037
+ };
2038
+ }
2039
+ break;
2040
+ case "boolean":
2041
+ if (typeof value !== "boolean" && value !== "true" && value !== "false") {
2042
+ return {
2043
+ field: fieldName,
2044
+ code: "invalid_type",
2045
+ message: rule.message || `${fieldName} must be a boolean`,
2046
+ expected: "boolean",
2047
+ received: typeof value
2048
+ };
2049
+ }
2050
+ break;
2051
+ case "email":
2052
+ if (typeof value !== "string" || !EMAIL_PATTERN.test(value)) {
2053
+ return {
2054
+ field: fieldName,
2055
+ code: "invalid_email",
2056
+ message: rule.message || `${fieldName} must be a valid email address`,
2057
+ received: value
2058
+ };
2059
+ }
2060
+ break;
2061
+ case "url":
2062
+ if (typeof value !== "string" || !URL_PATTERN.test(value)) {
2063
+ return {
2064
+ field: fieldName,
2065
+ code: "invalid_url",
2066
+ message: rule.message || `${fieldName} must be a valid URL`,
2067
+ received: value
2068
+ };
2069
+ }
2070
+ break;
2071
+ case "uuid":
2072
+ if (typeof value !== "string" || !UUID_PATTERN.test(value)) {
2073
+ return {
2074
+ field: fieldName,
2075
+ code: "invalid_uuid",
2076
+ message: rule.message || `${fieldName} must be a valid UUID`,
2077
+ received: value
2078
+ };
2079
+ }
2080
+ break;
2081
+ case "date":
2082
+ if (typeof value !== "string" || !DATE_PATTERN.test(value)) {
2083
+ const parsed = new Date(value);
2084
+ if (isNaN(parsed.getTime())) {
2085
+ return {
2086
+ field: fieldName,
2087
+ code: "invalid_date",
2088
+ message: rule.message || `${fieldName} must be a valid date`,
2089
+ received: value
2090
+ };
2091
+ }
2092
+ }
2093
+ break;
2094
+ case "array":
2095
+ if (!Array.isArray(value)) {
2096
+ return {
2097
+ field: fieldName,
2098
+ code: "invalid_type",
2099
+ message: rule.message || `${fieldName} must be an array`,
2100
+ expected: "array",
2101
+ received: typeof value
2102
+ };
2103
+ }
2104
+ if (rule.minItems !== void 0 && value.length < rule.minItems) {
2105
+ return {
2106
+ field: fieldName,
2107
+ code: "too_few_items",
2108
+ message: rule.message || `${fieldName} must have at least ${rule.minItems} items`,
2109
+ received: value.length
2110
+ };
2111
+ }
2112
+ if (rule.maxItems !== void 0 && value.length > rule.maxItems) {
2113
+ return {
2114
+ field: fieldName,
2115
+ code: "too_many_items",
2116
+ message: rule.message || `${fieldName} must have at most ${rule.maxItems} items`,
2117
+ received: value.length
2118
+ };
2119
+ }
2120
+ if (rule.items) {
2121
+ for (let i = 0; i < value.length; i++) {
2122
+ const itemError = validateField(value[i], rule.items, `${fieldName}[${i}]`);
2123
+ if (itemError) return itemError;
2124
+ }
2125
+ }
2126
+ break;
2127
+ case "object":
2128
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2129
+ return {
2130
+ field: fieldName,
2131
+ code: "invalid_type",
2132
+ message: rule.message || `${fieldName} must be an object`,
2133
+ expected: "object",
2134
+ received: Array.isArray(value) ? "array" : typeof value
2135
+ };
2136
+ }
2137
+ break;
2138
+ }
2139
+ if (rule.custom) {
2140
+ const result = rule.custom(value);
2141
+ if (result !== true) {
2142
+ return {
2143
+ field: fieldName,
2144
+ code: "custom_validation",
2145
+ message: typeof result === "string" ? result : rule.message || `${fieldName} failed validation`,
2146
+ received: value
2147
+ };
2148
+ }
2149
+ }
2150
+ return null;
2151
+ }
2152
+ function validateCustomSchema(data, schema) {
2153
+ if (typeof data !== "object" || data === null) {
2154
+ return {
2155
+ success: false,
2156
+ errors: [{
2157
+ field: "_root",
2158
+ code: "invalid_type",
2159
+ message: "Expected an object",
2160
+ received: data
2161
+ }]
2162
+ };
2163
+ }
2164
+ const errors = [];
2165
+ const record = data;
2166
+ for (const [fieldName, rule] of Object.entries(schema)) {
2167
+ const error = validateField(record[fieldName], rule, fieldName);
2168
+ if (error) {
2169
+ errors.push(error);
2170
+ }
2171
+ }
2172
+ if (errors.length > 0) {
2173
+ return { success: false, errors };
2174
+ }
2175
+ return { success: true, data };
2176
+ }
2177
+ function validateZodSchema(data, schema) {
2178
+ const result = schema.safeParse(data);
2179
+ if (result.success) {
2180
+ return { success: true, data: result.data };
2181
+ }
2182
+ const errors = result.error.issues.map((issue) => ({
2183
+ field: issue.path.join(".") || "_root",
2184
+ code: issue.code,
2185
+ message: issue.message,
2186
+ path: issue.path.map(String)
2187
+ }));
2188
+ return { success: false, errors };
2189
+ }
2190
+ function walkObject(obj, fn, path = "") {
2191
+ if (typeof obj === "string") {
2192
+ return fn(obj, path);
2193
+ }
2194
+ if (Array.isArray(obj)) {
2195
+ return obj.map((item, i) => walkObject(item, fn, `${path}[${i}]`));
2196
+ }
2197
+ if (typeof obj === "object" && obj !== null) {
2198
+ const result = {};
2199
+ for (const [key, value] of Object.entries(obj)) {
2200
+ const newPath = path ? `${path}.${key}` : key;
2201
+ result[key] = walkObject(value, fn, newPath);
2202
+ }
2203
+ return result;
2204
+ }
2205
+ return obj;
2206
+ }
2207
+ function parseQueryString(url) {
2208
+ const result = {};
2209
+ try {
2210
+ const urlObj = new URL(url);
2211
+ for (const [key, value] of urlObj.searchParams.entries()) {
2212
+ if (key in result) {
2213
+ const existing = result[key];
2214
+ if (Array.isArray(existing)) {
2215
+ existing.push(value);
2216
+ } else {
2217
+ result[key] = [existing, value];
2218
+ }
2219
+ } else {
2220
+ result[key] = value;
2221
+ }
2222
+ }
2223
+ } catch {
2224
+ }
2225
+ return result;
2226
+ }
2227
+
2228
+ // src/middleware/validation/validators/schema.ts
2229
+ function validate(data, schema) {
2230
+ if (isZodSchema(schema)) {
2231
+ return validateZodSchema(data, schema);
2232
+ }
2233
+ if (isCustomSchema(schema)) {
2234
+ return validateCustomSchema(data, schema);
2235
+ }
2236
+ return {
2237
+ success: false,
2238
+ errors: [{
2239
+ field: "_schema",
2240
+ code: "invalid_schema",
2241
+ message: "Invalid schema provided"
2242
+ }]
2243
+ };
2244
+ }
2245
+ async function validateBody(request, schema) {
2246
+ let body;
2247
+ try {
2248
+ const contentType = request.headers.get("content-type") || "";
2249
+ if (contentType.includes("application/json")) {
2250
+ body = await request.json();
2251
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
2252
+ const text = await request.text();
2253
+ body = Object.fromEntries(new URLSearchParams(text));
2254
+ } else if (contentType.includes("multipart/form-data")) {
2255
+ const formData = await request.formData();
2256
+ const obj = {};
2257
+ formData.forEach((value, key) => {
2258
+ if (typeof value === "string") {
2259
+ obj[key] = value;
2260
+ }
2261
+ });
2262
+ body = obj;
2263
+ } else {
2264
+ try {
2265
+ body = await request.json();
2266
+ } catch {
2267
+ body = {};
2268
+ }
2269
+ }
2270
+ } catch (error) {
2271
+ return {
2272
+ success: false,
2273
+ errors: [{
2274
+ field: "_body",
2275
+ code: "parse_error",
2276
+ message: "Failed to parse request body"
2277
+ }]
2278
+ };
2279
+ }
2280
+ return validate(body, schema);
2281
+ }
2282
+ function validateQuery(request, schema) {
2283
+ const query = parseQueryString(request.url);
2284
+ return validate(query, schema);
2285
+ }
2286
+ function validateParams(params, schema) {
2287
+ return validate(params, schema);
2288
+ }
2289
+ async function validateRequest(request, config) {
2290
+ const allErrors = [];
2291
+ const data = {};
2292
+ if (config.body) {
2293
+ const bodyResult = await validateBody(request, config.body);
2294
+ if (!bodyResult.success) {
2295
+ allErrors.push(...(bodyResult.errors || []).map((e) => ({
2296
+ ...e,
2297
+ field: `body.${e.field}`.replace("body._root", "body")
2298
+ })));
2299
+ } else {
2300
+ data.body = bodyResult.data;
2301
+ }
2302
+ } else {
2303
+ data.body = {};
2304
+ }
2305
+ if (config.query) {
2306
+ const queryResult = validateQuery(request, config.query);
2307
+ if (!queryResult.success) {
2308
+ allErrors.push(...(queryResult.errors || []).map((e) => ({
2309
+ ...e,
2310
+ field: `query.${e.field}`.replace("query._root", "query")
2311
+ })));
2312
+ } else {
2313
+ data.query = queryResult.data;
2314
+ }
2315
+ } else {
2316
+ data.query = {};
2317
+ }
2318
+ if (config.params && config.routeParams) {
2319
+ const paramsResult = validateParams(config.routeParams, config.params);
2320
+ if (!paramsResult.success) {
2321
+ allErrors.push(...(paramsResult.errors || []).map((e) => ({
2322
+ ...e,
2323
+ field: `params.${e.field}`.replace("params._root", "params")
2324
+ })));
2325
+ } else {
2326
+ data.params = paramsResult.data;
2327
+ }
2328
+ } else {
2329
+ data.params = {};
2330
+ }
2331
+ if (allErrors.length > 0) {
2332
+ return { success: false, errors: allErrors };
2333
+ }
2334
+ return {
2335
+ success: true,
2336
+ data
2337
+ };
2338
+ }
2339
+ function defaultValidationErrorResponse(errors) {
2340
+ return new Response(
2341
+ JSON.stringify({
2342
+ error: "validation_error",
2343
+ message: "Request validation failed",
2344
+ details: errors.map((e) => ({
2345
+ field: e.field,
2346
+ code: e.code,
2347
+ message: e.message
2348
+ }))
2349
+ }),
2350
+ {
2351
+ status: 400,
2352
+ headers: { "Content-Type": "application/json" }
2353
+ }
2354
+ );
2355
+ }
2356
+ function createValidator(schema) {
2357
+ return (data) => validate(data, schema);
2358
+ }
2359
+
2360
+ // src/middleware/validation/validators/content-type.ts
2361
+ var MIME_TYPES = {
2362
+ // Text
2363
+ TEXT_PLAIN: "text/plain",
2364
+ TEXT_HTML: "text/html",
2365
+ TEXT_CSS: "text/css",
2366
+ TEXT_JAVASCRIPT: "text/javascript",
2367
+ // Application
2368
+ JSON: "application/json",
2369
+ FORM_URLENCODED: "application/x-www-form-urlencoded",
2370
+ MULTIPART_FORM: "multipart/form-data",
2371
+ XML: "application/xml",
2372
+ PDF: "application/pdf",
2373
+ ZIP: "application/zip",
2374
+ GZIP: "application/gzip",
2375
+ OCTET_STREAM: "application/octet-stream",
2376
+ // Image
2377
+ IMAGE_PNG: "image/png",
2378
+ IMAGE_JPEG: "image/jpeg",
2379
+ IMAGE_GIF: "image/gif",
2380
+ IMAGE_WEBP: "image/webp",
2381
+ IMAGE_SVG: "image/svg+xml",
2382
+ // Audio
2383
+ AUDIO_MP3: "audio/mpeg",
2384
+ AUDIO_WAV: "audio/wav",
2385
+ AUDIO_OGG: "audio/ogg",
2386
+ // Video
2387
+ VIDEO_MP4: "video/mp4",
2388
+ VIDEO_WEBM: "video/webm"
2389
+ };
2390
+ function parseContentType(header) {
2391
+ if (!header) {
2392
+ return {
2393
+ type: "",
2394
+ subtype: "",
2395
+ mediaType: "",
2396
+ parameters: {}
2397
+ };
2398
+ }
2399
+ const parts = header.split(";").map((p) => p.trim());
2400
+ const mediaType = parts[0].toLowerCase();
2401
+ const [type = "", subtype = ""] = mediaType.split("/");
2402
+ const parameters = {};
2403
+ for (let i = 1; i < parts.length; i++) {
2404
+ const [key, value] = parts[i].split("=").map((p) => p.trim());
2405
+ if (key && value) {
2406
+ parameters[key.toLowerCase()] = value.replace(/^["']|["']$/g, "");
2407
+ }
2408
+ }
2409
+ return {
2410
+ type,
2411
+ subtype,
2412
+ mediaType,
2413
+ charset: parameters["charset"],
2414
+ boundary: parameters["boundary"],
2415
+ parameters
2416
+ };
2417
+ }
2418
+ function isAllowedContentType(contentType, allowedTypes, strict = false) {
2419
+ if (!contentType) {
2420
+ return !strict;
2421
+ }
2422
+ const { mediaType } = parseContentType(contentType);
2423
+ return allowedTypes.some((allowed) => {
2424
+ const normalizedAllowed = allowed.toLowerCase().trim();
2425
+ if (mediaType === normalizedAllowed) {
2426
+ return true;
2427
+ }
2428
+ if (normalizedAllowed.endsWith("/*")) {
2429
+ const prefix = normalizedAllowed.slice(0, -2);
2430
+ return mediaType.startsWith(prefix + "/");
2431
+ }
2432
+ if (!normalizedAllowed.includes("/")) {
2433
+ const { type } = parseContentType(contentType);
2434
+ return type === normalizedAllowed;
2435
+ }
2436
+ return false;
2437
+ });
2438
+ }
2439
+ function validateContentType(request, config) {
2440
+ const contentType = request.headers.get("content-type");
2441
+ const { allowed, strict = false, charset } = config;
2442
+ if (strict && !contentType) {
2443
+ return {
2444
+ valid: false,
2445
+ contentType: null,
2446
+ reason: "Content-Type header is required"
2447
+ };
2448
+ }
2449
+ if (contentType && !isAllowedContentType(contentType, allowed, strict)) {
2450
+ return {
2451
+ valid: false,
2452
+ contentType,
2453
+ reason: `Content-Type '${contentType}' is not allowed`
2454
+ };
2455
+ }
2456
+ if (charset && contentType) {
2457
+ const parsed = parseContentType(contentType);
2458
+ if (parsed.charset && parsed.charset.toLowerCase() !== charset.toLowerCase()) {
2459
+ return {
2460
+ valid: false,
2461
+ contentType,
2462
+ reason: `Charset '${parsed.charset}' is not allowed, expected '${charset}'`
2463
+ };
2464
+ }
2465
+ }
2466
+ return { valid: true, contentType };
2467
+ }
2468
+ function defaultContentTypeErrorResponse(contentType, reason) {
2469
+ return new Response(
2470
+ JSON.stringify({
2471
+ error: "invalid_content_type",
2472
+ message: reason,
2473
+ received: contentType
2474
+ }),
2475
+ {
2476
+ status: 415,
2477
+ // Unsupported Media Type
2478
+ headers: { "Content-Type": "application/json" }
2479
+ }
2480
+ );
2481
+ }
2482
+ function isJsonRequest(request) {
2483
+ return isAllowedContentType(
2484
+ request.headers.get("content-type"),
2485
+ [MIME_TYPES.JSON]
2486
+ );
2487
+ }
2488
+ function isFormRequest(request) {
2489
+ return isAllowedContentType(
2490
+ request.headers.get("content-type"),
2491
+ [MIME_TYPES.FORM_URLENCODED, MIME_TYPES.MULTIPART_FORM]
2492
+ );
2493
+ }
2494
+
2495
+ // src/middleware/validation/sanitizers/path.ts
2496
+ var DANGEROUS_PATTERNS = [
2497
+ // Unix path traversal
2498
+ /\.\.\//g,
2499
+ /\.\./g,
2500
+ // Windows path traversal
2501
+ /\.\.\\/g,
2502
+ // Null byte (can truncate paths in some systems)
2503
+ /%00/g,
2504
+ /\0/g,
2505
+ // URL encoded traversal
2506
+ /%2e%2e%2f/gi,
2507
+ // ../
2508
+ /%2e%2e\//gi,
2509
+ // ../
2510
+ /%2e%2e%5c/gi,
2511
+ // ..\
2512
+ /%2e%2e\\/gi,
2513
+ // ..\
2514
+ // Double URL encoding
2515
+ /%252e%252e%252f/gi,
2516
+ /%252e%252e%255c/gi,
2517
+ // Unicode encoding
2518
+ /\.%u002e\//gi,
2519
+ /%u002e%u002e%u002f/gi,
2520
+ // Overlong UTF-8 encoding
2521
+ /%c0%ae%c0%ae%c0%af/gi,
2522
+ /%c1%9c/gi
2523
+ // Backslash variant
2524
+ ];
2525
+ var DEFAULT_BLOCKED_EXTENSIONS = [
2526
+ ".exe",
2527
+ ".dll",
2528
+ ".so",
2529
+ ".dylib",
2530
+ // Executables
2531
+ ".sh",
2532
+ ".bash",
2533
+ ".bat",
2534
+ ".cmd",
2535
+ ".ps1",
2536
+ // Scripts
2537
+ ".php",
2538
+ ".asp",
2539
+ ".aspx",
2540
+ ".jsp",
2541
+ ".cgi",
2542
+ // Server scripts
2543
+ ".htaccess",
2544
+ ".htpasswd",
2545
+ // Apache config
2546
+ ".env",
2547
+ ".git",
2548
+ ".svn"
2549
+ // Config/VCS
2550
+ ];
2551
+ function normalizePathSeparators(path) {
2552
+ return path.replace(/\\/g, "/");
2553
+ }
2554
+ function decodePathComponent(path) {
2555
+ let result = path;
2556
+ let previous = "";
2557
+ while (result !== previous) {
2558
+ previous = result;
2559
+ try {
2560
+ result = decodeURIComponent(result);
2561
+ } catch {
2562
+ break;
2563
+ }
2564
+ }
2565
+ return result;
2566
+ }
2567
+ function hasPathTraversal(path) {
2568
+ if (!path || typeof path !== "string") return false;
2569
+ const normalized = normalizePathSeparators(decodePathComponent(path));
2570
+ for (const pattern of DANGEROUS_PATTERNS) {
2571
+ pattern.lastIndex = 0;
2572
+ if (pattern.test(normalized)) {
2573
+ return true;
2574
+ }
2575
+ }
2576
+ if (normalized.includes("..")) {
2577
+ return true;
2578
+ }
2579
+ return false;
2580
+ }
2581
+ function validatePath(path, config = {}) {
2582
+ if (!path || typeof path !== "string") {
2583
+ return { valid: false, reason: "Path is empty or not a string" };
2584
+ }
2585
+ const {
2586
+ allowAbsolute = false,
2587
+ allowedPrefixes = [],
2588
+ allowedExtensions,
2589
+ blockedExtensions = DEFAULT_BLOCKED_EXTENSIONS,
2590
+ maxDepth = 10,
2591
+ maxLength = 255,
2592
+ normalize = true
2593
+ } = config;
2594
+ if (path.length > maxLength) {
2595
+ return { valid: false, reason: `Path exceeds maximum length of ${maxLength}` };
2596
+ }
2597
+ let normalized = decodePathComponent(path);
2598
+ if (normalize) {
2599
+ normalized = normalizePathSeparators(normalized);
2600
+ }
2601
+ if (normalized.includes("\0") || path.includes("%00")) {
2602
+ return { valid: false, reason: "Path contains null bytes" };
2603
+ }
2604
+ if (hasPathTraversal(path)) {
2605
+ return { valid: false, reason: "Path contains traversal sequences" };
2606
+ }
2607
+ const isAbsolute = normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || // Windows drive letter
2608
+ normalized.startsWith("\\\\");
2609
+ if (isAbsolute && !allowAbsolute) {
2610
+ return { valid: false, reason: "Absolute paths are not allowed" };
2611
+ }
2612
+ if (allowedPrefixes.length > 0) {
2613
+ const hasValidPrefix = allowedPrefixes.some((prefix) => {
2614
+ const normalizedPrefix = normalizePathSeparators(prefix);
2615
+ return normalized.startsWith(normalizedPrefix);
2616
+ });
2617
+ if (!hasValidPrefix) {
2618
+ return { valid: false, reason: "Path does not start with an allowed prefix" };
2619
+ }
2620
+ }
2621
+ const segments = normalized.split("/").filter((s) => s && s !== ".");
2622
+ if (segments.length > maxDepth) {
2623
+ return { valid: false, reason: `Path depth exceeds maximum of ${maxDepth}` };
2624
+ }
2625
+ const lastSegment = segments[segments.length - 1] || "";
2626
+ const dotIndex = lastSegment.lastIndexOf(".");
2627
+ const extension = dotIndex > 0 ? lastSegment.slice(dotIndex).toLowerCase() : "";
2628
+ if (extension && blockedExtensions.length > 0) {
2629
+ if (blockedExtensions.map((e) => e.toLowerCase()).includes(extension)) {
2630
+ return { valid: false, reason: `Extension ${extension} is not allowed` };
2631
+ }
2632
+ }
2633
+ if (extension && allowedExtensions && allowedExtensions.length > 0) {
2634
+ if (!allowedExtensions.map((e) => e.toLowerCase()).includes(extension)) {
2635
+ return { valid: false, reason: `Extension ${extension} is not in allowed list` };
2636
+ }
2637
+ }
2638
+ const sanitized = normalized.replace(/\/+/g, "/");
2639
+ return { valid: true, sanitized };
2640
+ }
2641
+ function sanitizePath(path, config = {}) {
2642
+ if (!path || typeof path !== "string") return "";
2643
+ const { normalize = true, maxLength = 255 } = config;
2644
+ let result = decodePathComponent(path);
2645
+ if (normalize) {
2646
+ result = normalizePathSeparators(result);
2647
+ }
2648
+ result = result.replace(/\0/g, "").replace(/%00/g, "");
2649
+ result = result.replace(/\.\.\//g, "").replace(/\.\.\\/g, "");
2650
+ if (!config.allowAbsolute) {
2651
+ result = result.replace(/^\/+/, "");
2652
+ result = result.replace(/^[a-zA-Z]:/, "");
2653
+ result = result.replace(/^\\\\/, "");
2654
+ }
2655
+ result = result.replace(/\/+/g, "/");
2656
+ result = result.replace(/\/+$/, "");
2657
+ if (result.length > maxLength) {
2658
+ result = result.slice(0, maxLength);
2659
+ }
2660
+ return result;
2661
+ }
2662
+ function getExtension(path) {
2663
+ if (!path || typeof path !== "string") return "";
2664
+ const normalized = normalizePathSeparators(path);
2665
+ const segments = normalized.split("/");
2666
+ const filename = segments[segments.length - 1] || "";
2667
+ const dotIndex = filename.lastIndexOf(".");
2668
+ if (dotIndex <= 0) return "";
2669
+ return filename.slice(dotIndex).toLowerCase();
2670
+ }
2671
+ function sanitizeFilename(filename) {
2672
+ if (typeof filename !== "string") return "file";
2673
+ if (!filename) return "file";
2674
+ let result = filename;
2675
+ result = result.replace(/[/\\]/g, "");
2676
+ result = result.replace(/\0/g, "");
2677
+ result = result.replace(/[\x00-\x1f\x7f]/g, "");
2678
+ result = result.replace(/[<>:"|?*]/g, "");
2679
+ result = result.replace(/^[.\s]+|[.\s]+$/g, "");
2680
+ if (result.length > 255) {
2681
+ const ext = getExtension(result);
2682
+ const name = result.slice(0, 255 - ext.length);
2683
+ result = name + ext;
2684
+ }
2685
+ return result || "file";
2686
+ }
2687
+
2688
+ // src/middleware/validation/validators/file.ts
2689
+ var MAGIC_NUMBERS = [
2690
+ // Images
2691
+ { type: "image/jpeg", extension: ".jpg", signature: [255, 216, 255] },
2692
+ { type: "image/png", extension: ".png", signature: [137, 80, 78, 71, 13, 10, 26, 10] },
2693
+ { type: "image/gif", extension: ".gif", signature: [71, 73, 70, 56] },
2694
+ // GIF87a or GIF89a
2695
+ { type: "image/webp", extension: ".webp", signature: [82, 73, 70, 70], offset: 0 },
2696
+ // RIFF
2697
+ { type: "image/bmp", extension: ".bmp", signature: [66, 77] },
2698
+ { type: "image/tiff", extension: ".tiff", signature: [73, 73, 42, 0] },
2699
+ // Little endian
2700
+ { type: "image/tiff", extension: ".tiff", signature: [77, 77, 0, 42] },
2701
+ // Big endian
2702
+ { type: "image/x-icon", extension: ".ico", signature: [0, 0, 1, 0] },
2703
+ { type: "image/svg+xml", extension: ".svg", signature: [60, 63, 120, 109, 108] },
2704
+ // <?xml
2705
+ // Documents
2706
+ { type: "application/pdf", extension: ".pdf", signature: [37, 80, 68, 70] },
2707
+ // %PDF
2708
+ { type: "application/zip", extension: ".zip", signature: [80, 75, 3, 4] },
2709
+ // PK
2710
+ { type: "application/gzip", extension: ".gz", signature: [31, 139] },
2711
+ { type: "application/x-rar-compressed", extension: ".rar", signature: [82, 97, 114, 33] },
2712
+ { type: "application/x-7z-compressed", extension: ".7z", signature: [55, 122, 188, 175, 39, 28] },
2713
+ // Microsoft Office (new format - zip based)
2714
+ { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", extension: ".xlsx", signature: [80, 75, 3, 4] },
2715
+ { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", extension: ".docx", signature: [80, 75, 3, 4] },
2716
+ { type: "application/vnd.openxmlformats-officedocument.presentationml.presentation", extension: ".pptx", signature: [80, 75, 3, 4] },
2717
+ // Microsoft Office (old format)
2718
+ { type: "application/msword", extension: ".doc", signature: [208, 207, 17, 224, 161, 177, 26, 225] },
2719
+ { type: "application/vnd.ms-excel", extension: ".xls", signature: [208, 207, 17, 224, 161, 177, 26, 225] },
2720
+ // Audio
2721
+ { type: "audio/mpeg", extension: ".mp3", signature: [255, 251] },
2722
+ // MP3 frame sync
2723
+ { type: "audio/mpeg", extension: ".mp3", signature: [73, 68, 51] },
2724
+ // ID3
2725
+ { type: "audio/wav", extension: ".wav", signature: [82, 73, 70, 70] },
2726
+ // RIFF
2727
+ { type: "audio/ogg", extension: ".ogg", signature: [79, 103, 103, 83] },
2728
+ { type: "audio/flac", extension: ".flac", signature: [102, 76, 97, 67] },
2729
+ // Video
2730
+ { type: "video/mp4", extension: ".mp4", signature: [0, 0, 0], offset: 0 },
2731
+ // Partial match
2732
+ { type: "video/webm", extension: ".webm", signature: [26, 69, 223, 163] },
2733
+ { type: "video/avi", extension: ".avi", signature: [82, 73, 70, 70] },
2734
+ // RIFF
2735
+ { type: "video/quicktime", extension: ".mov", signature: [0, 0, 0, 20, 102, 116, 121, 112] },
2736
+ // Web
2737
+ { type: "application/wasm", extension: ".wasm", signature: [0, 97, 115, 109] },
2738
+ // \0asm
2739
+ // Fonts
2740
+ { type: "font/woff", extension: ".woff", signature: [119, 79, 70, 70] },
2741
+ { type: "font/woff2", extension: ".woff2", signature: [119, 79, 70, 50] }
2742
+ ];
2743
+ var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
2744
+ var DEFAULT_MAX_FILES = 10;
2745
+ var DANGEROUS_EXTENSIONS = [
2746
+ ".exe",
2747
+ ".dll",
2748
+ ".so",
2749
+ ".dylib",
2750
+ ".bin",
2751
+ ".sh",
2752
+ ".bash",
2753
+ ".bat",
2754
+ ".cmd",
2755
+ ".ps1",
2756
+ ".vbs",
2757
+ ".php",
2758
+ ".asp",
2759
+ ".aspx",
2760
+ ".jsp",
2761
+ ".cgi",
2762
+ ".pl",
2763
+ ".py",
2764
+ ".rb",
2765
+ ".jar",
2766
+ ".class",
2767
+ ".msi",
2768
+ ".dmg",
2769
+ ".pkg",
2770
+ ".deb",
2771
+ ".rpm",
2772
+ ".scr",
2773
+ ".pif",
2774
+ ".com",
2775
+ ".hta"
2776
+ ];
2777
+ function checkMagicNumber(bytes, magicNumber) {
2778
+ const offset = magicNumber.offset || 0;
2779
+ const signature = magicNumber.signature;
2780
+ if (bytes.length < offset + signature.length) {
2781
+ return false;
2782
+ }
2783
+ for (let i = 0; i < signature.length; i++) {
2784
+ if (bytes[offset + i] !== signature[i]) {
2785
+ return false;
2786
+ }
2787
+ }
2788
+ return true;
2789
+ }
2790
+ function detectFileType(bytes) {
2791
+ for (const magic of MAGIC_NUMBERS) {
2792
+ if (checkMagicNumber(bytes, magic)) {
2793
+ return { type: magic.type, extension: magic.extension };
2794
+ }
2795
+ }
2796
+ return null;
2797
+ }
2798
+ async function validateFile(file, config = {}) {
2799
+ const {
2800
+ maxSize = DEFAULT_MAX_FILE_SIZE,
2801
+ minSize = 0,
2802
+ allowedTypes = [],
2803
+ blockedTypes = [],
2804
+ allowedExtensions = [],
2805
+ blockedExtensions = DANGEROUS_EXTENSIONS,
2806
+ validateMagicNumbers = true,
2807
+ sanitizeFilename: doSanitize = true
2808
+ } = config;
2809
+ const errors = [];
2810
+ const extension = getExtension(file.name);
2811
+ const info = {
2812
+ filename: doSanitize ? sanitizeFilename(file.name) : file.name,
2813
+ size: file.size,
2814
+ type: file.type,
2815
+ extension
2816
+ };
2817
+ if (file.size > maxSize) {
2818
+ errors.push({
2819
+ filename: file.name,
2820
+ code: "size_exceeded",
2821
+ message: `File size (${formatBytes(file.size)}) exceeds maximum allowed (${formatBytes(maxSize)})`,
2822
+ details: { size: file.size, maxSize }
2823
+ });
2824
+ }
2825
+ if (file.size < minSize) {
2826
+ errors.push({
2827
+ filename: file.name,
2828
+ code: "size_too_small",
2829
+ message: `File size (${formatBytes(file.size)}) is below minimum required (${formatBytes(minSize)})`,
2830
+ details: { size: file.size, minSize }
2831
+ });
2832
+ }
2833
+ if (blockedExtensions.length > 0 && extension) {
2834
+ if (blockedExtensions.map((e) => e.toLowerCase()).includes(extension.toLowerCase())) {
2835
+ errors.push({
2836
+ filename: file.name,
2837
+ code: "extension_not_allowed",
2838
+ message: `File extension '${extension}' is not allowed`,
2839
+ details: { extension, blockedExtensions }
2840
+ });
2841
+ }
2842
+ }
2843
+ if (allowedExtensions.length > 0 && extension) {
2844
+ if (!allowedExtensions.map((e) => e.toLowerCase()).includes(extension.toLowerCase())) {
2845
+ errors.push({
2846
+ filename: file.name,
2847
+ code: "extension_not_allowed",
2848
+ message: `File extension '${extension}' is not in allowed list`,
2849
+ details: { extension, allowedExtensions }
2850
+ });
2851
+ }
2852
+ }
2853
+ if (blockedTypes.length > 0 && file.type) {
2854
+ if (blockedTypes.includes(file.type)) {
2855
+ errors.push({
2856
+ filename: file.name,
2857
+ code: "type_not_allowed",
2858
+ message: `File type '${file.type}' is not allowed`,
2859
+ details: { type: file.type, blockedTypes }
2860
+ });
2861
+ }
2862
+ }
2863
+ if (allowedTypes.length > 0) {
2864
+ if (!allowedTypes.includes(file.type)) {
2865
+ errors.push({
2866
+ filename: file.name,
2867
+ code: "type_not_allowed",
2868
+ message: `File type '${file.type}' is not in allowed list`,
2869
+ details: { type: file.type, allowedTypes }
2870
+ });
2871
+ }
2872
+ }
2873
+ if (validateMagicNumbers && errors.length === 0) {
2874
+ try {
2875
+ const buffer = await file.arrayBuffer();
2876
+ const bytes = new Uint8Array(buffer.slice(0, 32));
2877
+ const detected = detectFileType(bytes);
2878
+ if (detected) {
2879
+ if (file.type && detected.type !== file.type) {
2880
+ const isSimilar = detected.type.startsWith("image/") && file.type.startsWith("image/") || detected.type.startsWith("audio/") && file.type.startsWith("audio/") || detected.type.startsWith("video/") && file.type.startsWith("video/");
2881
+ if (!isSimilar) {
2882
+ errors.push({
2883
+ filename: file.name,
2884
+ code: "invalid_content",
2885
+ message: `File content doesn't match declared type (claimed: ${file.type}, detected: ${detected.type})`,
2886
+ details: { claimed: file.type, detected: detected.type }
2887
+ });
2888
+ }
2889
+ }
2890
+ }
2891
+ } catch {
2892
+ }
2893
+ }
2894
+ return {
2895
+ valid: errors.length === 0,
2896
+ info,
2897
+ errors
2898
+ };
2899
+ }
2900
+ async function validateFiles(files, config = {}) {
2901
+ const { maxFiles = DEFAULT_MAX_FILES } = config;
2902
+ const allErrors = [];
2903
+ const infos = [];
2904
+ if (files.length > maxFiles) {
2905
+ allErrors.push({
2906
+ filename: "",
2907
+ code: "too_many_files",
2908
+ message: `Too many files (${files.length}), maximum allowed is ${maxFiles}`,
2909
+ details: { count: files.length, maxFiles }
2910
+ });
2911
+ }
2912
+ for (const file of files) {
2913
+ const result = await validateFile(file, config);
2914
+ infos.push(result.info);
2915
+ allErrors.push(...result.errors);
2916
+ }
2917
+ return {
2918
+ valid: allErrors.length === 0,
2919
+ infos,
2920
+ errors: allErrors
2921
+ };
2922
+ }
2923
+ function extractFilesFromFormData(formData) {
2924
+ const files = /* @__PURE__ */ new Map();
2925
+ formData.forEach((value, key) => {
2926
+ if (value instanceof File) {
2927
+ const existing = files.get(key) || [];
2928
+ existing.push(value);
2929
+ files.set(key, existing);
2930
+ }
2931
+ });
2932
+ return files;
2933
+ }
2934
+ async function validateFilesFromRequest(request, config = {}) {
2935
+ const contentType = request.headers.get("content-type") || "";
2936
+ if (!contentType.includes("multipart/form-data")) {
2937
+ return { valid: true, files: /* @__PURE__ */ new Map(), errors: [] };
2938
+ }
2939
+ try {
2940
+ const formData = await request.formData();
2941
+ const fileMap = extractFilesFromFormData(formData);
2942
+ const allInfos = /* @__PURE__ */ new Map();
2943
+ const allErrors = [];
2944
+ let totalFileCount = 0;
2945
+ for (const [field, files] of fileMap.entries()) {
2946
+ totalFileCount += files.length;
2947
+ const result = await validateFiles(files, { ...config, maxFiles: Infinity });
2948
+ allInfos.set(field, result.infos);
2949
+ allErrors.push(...result.errors.map((e) => ({ ...e, field })));
2950
+ }
2951
+ const maxFiles = config.maxFiles ?? DEFAULT_MAX_FILES;
2952
+ if (totalFileCount > maxFiles) {
2953
+ allErrors.push({
2954
+ filename: "",
2955
+ code: "too_many_files",
2956
+ message: `Total file count (${totalFileCount}) exceeds maximum (${maxFiles})`,
2957
+ details: { count: totalFileCount, maxFiles }
2958
+ });
2959
+ }
2960
+ return {
2961
+ valid: allErrors.length === 0,
2962
+ files: allInfos,
2963
+ errors: allErrors
2964
+ };
2965
+ } catch {
2966
+ return {
2967
+ valid: false,
2968
+ files: /* @__PURE__ */ new Map(),
2969
+ errors: [{
2970
+ filename: "",
2971
+ code: "invalid_content",
2972
+ message: "Failed to parse multipart form data"
2973
+ }]
2974
+ };
2975
+ }
2976
+ }
2977
+ function defaultFileErrorResponse(errors) {
2978
+ return new Response(
2979
+ JSON.stringify({
2980
+ error: "file_validation_error",
2981
+ message: "File validation failed",
2982
+ details: errors.map((e) => ({
2983
+ filename: e.filename,
2984
+ field: e.field,
2985
+ code: e.code,
2986
+ message: e.message
2987
+ }))
2988
+ }),
2989
+ {
2990
+ status: 400,
2991
+ headers: { "Content-Type": "application/json" }
2992
+ }
2993
+ );
2994
+ }
2995
+ function formatBytes(bytes) {
2996
+ if (bytes === 0) return "0 B";
2997
+ const units = ["B", "KB", "MB", "GB"];
2998
+ const k = 1024;
2999
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3000
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${units[i]}`;
3001
+ }
3002
+
3003
+ // src/middleware/validation/sanitizers/xss.ts
3004
+ var DEFAULT_ALLOWED_TAGS = [
3005
+ "a",
3006
+ "abbr",
3007
+ "b",
3008
+ "blockquote",
3009
+ "br",
3010
+ "code",
3011
+ "del",
3012
+ "em",
3013
+ "h1",
3014
+ "h2",
3015
+ "h3",
3016
+ "h4",
3017
+ "h5",
3018
+ "h6",
3019
+ "hr",
3020
+ "i",
3021
+ "ins",
3022
+ "li",
3023
+ "mark",
3024
+ "ol",
3025
+ "p",
3026
+ "pre",
3027
+ "q",
3028
+ "s",
3029
+ "small",
3030
+ "span",
3031
+ "strong",
3032
+ "sub",
3033
+ "sup",
3034
+ "u",
3035
+ "ul"
3036
+ ];
3037
+ var DEFAULT_ALLOWED_ATTRIBUTES = {
3038
+ a: ["href", "title", "target", "rel"],
3039
+ img: ["src", "alt", "title", "width", "height"],
3040
+ abbr: ["title"],
3041
+ q: ["cite"],
3042
+ blockquote: ["cite"]
3043
+ };
3044
+ var DEFAULT_SAFE_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
3045
+ var DANGEROUS_PATTERNS2 = [
3046
+ // Event handlers
3047
+ /\bon\w+\s*=/gi,
3048
+ // JavaScript protocol
3049
+ /javascript\s*:/gi,
3050
+ // VBScript protocol
3051
+ /vbscript\s*:/gi,
3052
+ // Data URI with scripts
3053
+ /data\s*:[^,]*(?:text\/html|application\/javascript|text\/javascript)/gi,
3054
+ // Expression in CSS
3055
+ /expression\s*\(/gi,
3056
+ // Binding in CSS (Firefox)
3057
+ /-moz-binding\s*:/gi,
3058
+ // Behavior in CSS (IE)
3059
+ /behavior\s*:/gi,
3060
+ // Import in CSS
3061
+ /@import/gi,
3062
+ // Script tags
3063
+ /<\s*script/gi,
3064
+ // Style tags with expressions
3065
+ /<\s*style[^>]*>[^<]*expression/gi,
3066
+ // SVG with scripts
3067
+ /<\s*svg[^>]*onload/gi,
3068
+ // Object/embed/applet tags
3069
+ /<\s*(object|embed|applet)/gi,
3070
+ // Base tag (can redirect resources)
3071
+ /<\s*base/gi,
3072
+ // Meta refresh
3073
+ /<\s*meta[^>]*http-equiv\s*=\s*["']?refresh/gi,
3074
+ // Form action hijacking
3075
+ /<\s*form[^>]*action\s*=\s*["']?javascript/gi,
3076
+ // Link tag with import
3077
+ /<\s*link[^>]*rel\s*=\s*["']?import/gi
3078
+ ];
3079
+ var HTML_ENTITIES = {
3080
+ "&": "&amp;",
3081
+ "<": "&lt;",
3082
+ ">": "&gt;",
3083
+ '"': "&quot;",
3084
+ "'": "&#x27;",
3085
+ "/": "&#x2F;",
3086
+ "`": "&#x60;",
3087
+ "=": "&#x3D;"
3088
+ };
3089
+ function escapeHtml(str) {
3090
+ return str.replace(/[&<>"'`=/]/g, (char) => HTML_ENTITIES[char] || char);
3091
+ }
3092
+ function unescapeHtml(str) {
3093
+ const entityMap = {
3094
+ "&amp;": "&",
3095
+ "&lt;": "<",
3096
+ "&gt;": ">",
3097
+ "&quot;": '"',
3098
+ "&#x27;": "'",
3099
+ "&#x2F;": "/",
3100
+ "&#x60;": "`",
3101
+ "&#x3D;": "=",
3102
+ "&#39;": "'",
3103
+ "&#47;": "/"
3104
+ };
3105
+ return str.replace(/&(?:amp|lt|gt|quot|#x27|#x2F|#x60|#x3D|#39|#47);/gi, (entity) => {
3106
+ return entityMap[entity.toLowerCase()] || entity;
3107
+ });
3108
+ }
3109
+ function stripHtml(str) {
3110
+ let result = str.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
3111
+ result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
3112
+ result = result.replace(/<[^>]*>/g, "");
3113
+ result = unescapeHtml(result);
3114
+ result = result.replace(/\0/g, "");
3115
+ return result.trim();
3116
+ }
3117
+ function isSafeUrl(url, allowedProtocols = DEFAULT_SAFE_PROTOCOLS) {
3118
+ if (!url) return true;
3119
+ const trimmed = url.trim().toLowerCase();
3120
+ if (trimmed.startsWith("javascript:")) return false;
3121
+ if (trimmed.startsWith("vbscript:")) return false;
3122
+ if (trimmed.startsWith("data:image/")) return true;
3123
+ if (trimmed.startsWith("data:")) return false;
3124
+ try {
3125
+ const parsed = new URL(url, "https://example.com");
3126
+ if (parsed.protocol && !allowedProtocols.includes(parsed.protocol)) {
3127
+ if (!url.includes(":")) return true;
3128
+ return false;
3129
+ }
3130
+ } catch {
3131
+ return true;
3132
+ }
3133
+ return true;
3134
+ }
3135
+ function sanitizeHtml(str, allowedTags = DEFAULT_ALLOWED_TAGS, allowedAttributes = DEFAULT_ALLOWED_ATTRIBUTES, allowedProtocols = DEFAULT_SAFE_PROTOCOLS) {
3136
+ let result = str.replace(/\0/g, "");
3137
+ result = result.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
3138
+ result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
3139
+ result = result.replace(/<!--[\s\S]*?-->/g, "");
3140
+ result = result.replace(/<\/?([a-z][a-z0-9]*)\b([^>]*)>/gi, (match, tagName, attributes) => {
3141
+ const lowerTag = tagName.toLowerCase();
3142
+ const isClosing = match.startsWith("</");
3143
+ if (!allowedTags.includes(lowerTag)) {
3144
+ return "";
3145
+ }
3146
+ if (isClosing) {
3147
+ return `</${lowerTag}>`;
3148
+ }
3149
+ const allowedAttrs = allowedAttributes[lowerTag] || [];
3150
+ const safeAttrs = [];
3151
+ const attrRegex = /([a-z][a-z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*))/gi;
3152
+ let attrMatch;
3153
+ while ((attrMatch = attrRegex.exec(attributes)) !== null) {
3154
+ const attrName = attrMatch[1].toLowerCase();
3155
+ const attrValue = attrMatch[2] || attrMatch[3] || attrMatch[4] || "";
3156
+ if (!allowedAttrs.includes(attrName)) continue;
3157
+ if (DANGEROUS_PATTERNS2.some((pattern) => pattern.test(attrValue))) continue;
3158
+ if (["href", "src", "action", "formaction"].includes(attrName)) {
3159
+ if (!isSafeUrl(attrValue, allowedProtocols)) continue;
3160
+ }
3161
+ const safeValue = escapeHtml(attrValue);
3162
+ safeAttrs.push(`${attrName}="${safeValue}"`);
3163
+ }
3164
+ const attrStr = safeAttrs.length > 0 ? " " + safeAttrs.join(" ") : "";
3165
+ return `<${lowerTag}${attrStr}>`;
3166
+ });
3167
+ for (const pattern of DANGEROUS_PATTERNS2) {
3168
+ result = result.replace(pattern, "");
3169
+ }
3170
+ return result;
3171
+ }
3172
+ function detectXSS(str) {
3173
+ if (!str || typeof str !== "string") return false;
3174
+ const normalized = str.replace(/\\x([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\u([0-9a-f]{4})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);?/gi, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
3175
+ for (const pattern of DANGEROUS_PATTERNS2) {
3176
+ pattern.lastIndex = 0;
3177
+ if (pattern.test(normalized)) {
3178
+ return true;
3179
+ }
3180
+ }
3181
+ return false;
3182
+ }
3183
+ function sanitize(input, config = {}) {
3184
+ if (!input || typeof input !== "string") return "";
3185
+ const {
3186
+ mode = "escape",
3187
+ allowedTags = DEFAULT_ALLOWED_TAGS,
3188
+ allowedAttributes = DEFAULT_ALLOWED_ATTRIBUTES,
3189
+ allowedProtocols = DEFAULT_SAFE_PROTOCOLS,
3190
+ maxLength,
3191
+ stripNull = true
3192
+ } = config;
3193
+ let result = input;
3194
+ if (stripNull) {
3195
+ result = result.replace(/\0/g, "");
3196
+ }
3197
+ switch (mode) {
3198
+ case "escape":
3199
+ result = escapeHtml(result);
3200
+ break;
3201
+ case "strip":
3202
+ result = stripHtml(result);
3203
+ break;
3204
+ case "allow-safe":
3205
+ result = sanitizeHtml(result, allowedTags, allowedAttributes, allowedProtocols);
3206
+ break;
3207
+ }
3208
+ if (maxLength !== void 0 && result.length > maxLength) {
3209
+ result = result.slice(0, maxLength);
3210
+ }
3211
+ return result;
3212
+ }
3213
+ function sanitizeObject(obj, config = {}) {
3214
+ if (typeof obj === "string") {
3215
+ return sanitize(obj, config);
3216
+ }
3217
+ if (Array.isArray(obj)) {
3218
+ return obj.map((item) => sanitizeObject(item, config));
3219
+ }
3220
+ if (typeof obj === "object" && obj !== null) {
3221
+ const result = {};
3222
+ for (const [key, value] of Object.entries(obj)) {
3223
+ result[key] = sanitizeObject(value, config);
3224
+ }
3225
+ return result;
3226
+ }
3227
+ return obj;
3228
+ }
3229
+ function sanitizeFields(obj, fields, config = {}) {
3230
+ const result = { ...obj };
3231
+ for (const field of fields) {
3232
+ if (field in result && typeof result[field] === "string") {
3233
+ result[field] = sanitize(result[field], config);
3234
+ }
3235
+ }
3236
+ return result;
3237
+ }
3238
+
3239
+ // src/middleware/validation/sanitizers/sql.ts
3240
+ var SQL_PATTERNS = [
3241
+ // High severity - Definite attacks
3242
+ {
3243
+ pattern: /'\s*OR\s+'?\d+'?\s*=\s*'?\d+'?/gi,
3244
+ name: "OR '1'='1' attack",
3245
+ severity: "high"
3246
+ },
3247
+ {
3248
+ pattern: /'\s*OR\s+'[^']*'\s*=\s*'[^']*'/gi,
3249
+ name: "OR 'x'='x' attack",
3250
+ severity: "high"
3251
+ },
3252
+ {
3253
+ pattern: /;\s*DROP\s+(TABLE|DATABASE|INDEX|VIEW)/gi,
3254
+ name: "DROP statement",
3255
+ severity: "high"
3256
+ },
3257
+ {
3258
+ pattern: /;\s*DELETE\s+FROM/gi,
3259
+ name: "DELETE statement",
3260
+ severity: "high"
3261
+ },
3262
+ {
3263
+ pattern: /;\s*TRUNCATE\s+/gi,
3264
+ name: "TRUNCATE statement",
3265
+ severity: "high"
3266
+ },
3267
+ {
3268
+ pattern: /;\s*INSERT\s+INTO/gi,
3269
+ name: "INSERT statement",
3270
+ severity: "high"
3271
+ },
3272
+ {
3273
+ pattern: /;\s*UPDATE\s+\w+\s+SET/gi,
3274
+ name: "UPDATE statement",
3275
+ severity: "high"
3276
+ },
3277
+ {
3278
+ pattern: /UNION\s+(ALL\s+)?SELECT/gi,
3279
+ name: "UNION SELECT attack",
3280
+ severity: "high"
3281
+ },
3282
+ {
3283
+ pattern: /EXEC(\s+|\()+(sp_|xp_)/gi,
3284
+ name: "SQL Server stored procedure",
3285
+ severity: "high"
3286
+ },
3287
+ {
3288
+ pattern: /EXECUTE\s+IMMEDIATE/gi,
3289
+ name: "Oracle EXECUTE IMMEDIATE",
3290
+ severity: "high"
3291
+ },
3292
+ {
3293
+ pattern: /INTO\s+(OUT|DUMP)FILE/gi,
3294
+ name: "MySQL file write",
3295
+ severity: "high"
3296
+ },
3297
+ {
3298
+ pattern: /LOAD_FILE\s*\(/gi,
3299
+ name: "MySQL file read",
3300
+ severity: "high"
3301
+ },
3302
+ {
3303
+ pattern: /BENCHMARK\s*\(\s*\d+\s*,/gi,
3304
+ name: "MySQL BENCHMARK DoS",
3305
+ severity: "high"
3306
+ },
3307
+ {
3308
+ pattern: /SLEEP\s*\(\s*\d+\s*\)/gi,
3309
+ name: "SQL SLEEP time-based attack",
3310
+ severity: "high"
3311
+ },
3312
+ {
3313
+ pattern: /WAITFOR\s+DELAY/gi,
3314
+ name: "SQL Server WAITFOR DELAY",
3315
+ severity: "high"
3316
+ },
3317
+ {
3318
+ pattern: /PG_SLEEP\s*\(/gi,
3319
+ name: "PostgreSQL pg_sleep",
3320
+ severity: "high"
3321
+ },
3322
+ // Medium severity - Likely attacks
3323
+ {
3324
+ pattern: /'\s*--/g,
3325
+ name: "SQL comment injection",
3326
+ severity: "medium"
3327
+ },
3328
+ {
3329
+ pattern: /'\s*#/g,
3330
+ name: "MySQL comment injection",
3331
+ severity: "medium"
3332
+ },
3333
+ {
3334
+ pattern: /\/\*[\s\S]*?\*\//g,
3335
+ name: "Block comment",
3336
+ severity: "medium"
3337
+ },
3338
+ {
3339
+ pattern: /'\s*;\s*$/g,
3340
+ name: "Statement terminator",
3341
+ severity: "medium"
3342
+ },
3343
+ {
3344
+ pattern: /HAVING\s+\d+\s*=\s*\d+/gi,
3345
+ name: "HAVING clause injection",
3346
+ severity: "medium"
3347
+ },
3348
+ {
3349
+ pattern: /GROUP\s+BY\s+\d+/gi,
3350
+ name: "GROUP BY injection",
3351
+ severity: "medium"
3352
+ },
3353
+ {
3354
+ pattern: /ORDER\s+BY\s+\d+/gi,
3355
+ name: "ORDER BY injection",
3356
+ severity: "medium"
3357
+ },
3358
+ {
3359
+ pattern: /CONCAT\s*\(/gi,
3360
+ name: "CONCAT function",
3361
+ severity: "medium"
3362
+ },
3363
+ {
3364
+ pattern: /CHAR\s*\(\s*\d+\s*\)/gi,
3365
+ name: "CHAR function bypass",
3366
+ severity: "medium"
3367
+ },
3368
+ {
3369
+ pattern: /0x[0-9a-f]{2,}/gi,
3370
+ name: "Hex encoded value",
3371
+ severity: "medium"
3372
+ },
3373
+ {
3374
+ pattern: /CONVERT\s*\(/gi,
3375
+ name: "CONVERT function",
3376
+ severity: "medium"
3377
+ },
3378
+ {
3379
+ pattern: /CAST\s*\(/gi,
3380
+ name: "CAST function",
3381
+ severity: "medium"
3382
+ },
3383
+ // Low severity - Suspicious but may be false positives
3384
+ {
3385
+ pattern: /'\s*AND\s+'?\d+'?\s*=\s*'?\d+'?/gi,
3386
+ name: "AND '1'='1' pattern",
3387
+ severity: "low"
3388
+ },
3389
+ {
3390
+ pattern: /'\s*AND\s+'[^']*'\s*=\s*'[^']*'/gi,
3391
+ name: "AND 'x'='x' pattern",
3392
+ severity: "low"
3393
+ },
3394
+ {
3395
+ pattern: /SELECT\s+[\w\s,*]+\s+FROM/gi,
3396
+ name: "SELECT statement",
3397
+ severity: "low"
3398
+ },
3399
+ {
3400
+ pattern: /'\s*\+\s*'/g,
3401
+ name: "String concatenation",
3402
+ severity: "low"
3403
+ },
3404
+ {
3405
+ pattern: /'\s*\|\|\s*'/g,
3406
+ name: "Oracle string concatenation",
3407
+ severity: "low"
3408
+ }
3409
+ ];
3410
+ var ENCODED_PATTERNS = [
3411
+ {
3412
+ pattern: /%27\s*%4f%52\s*%27/gi,
3413
+ // URL encoded ' OR '
3414
+ name: "URL encoded OR injection",
3415
+ severity: "high"
3416
+ },
3417
+ {
3418
+ pattern: /%27\s*%2d%2d/gi,
3419
+ // URL encoded ' --
3420
+ name: "URL encoded comment injection",
3421
+ severity: "medium"
3422
+ },
3423
+ {
3424
+ pattern: /\0|%00/g,
3425
+ // Null byte (decoded or encoded)
3426
+ name: "Null byte injection",
3427
+ severity: "high"
3428
+ },
3429
+ {
3430
+ pattern: /\\x27/gi,
3431
+ // Hex escape
3432
+ name: "Hex escaped quote",
3433
+ severity: "medium"
3434
+ },
3435
+ {
3436
+ pattern: /\\u0027/gi,
3437
+ // Unicode escape
3438
+ name: "Unicode escaped quote",
3439
+ severity: "medium"
3440
+ }
3441
+ ];
3442
+ function normalizeInput(input) {
3443
+ let result = input;
3444
+ try {
3445
+ result = decodeURIComponent(result);
3446
+ } catch {
3447
+ }
3448
+ result = result.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);?/gi, (_, dec) => String.fromCharCode(parseInt(dec, 10))).replace(/&quot;/gi, '"').replace(/&apos;/gi, "'").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&amp;/gi, "&");
3449
+ result = result.replace(
3450
+ /\\x([0-9a-f]{2})/gi,
3451
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
3452
+ );
3453
+ result = result.replace(
3454
+ /\\u([0-9a-f]{4})/gi,
3455
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
3456
+ );
3457
+ return result;
3458
+ }
3459
+ function detectSQLInjection(input, options = {}) {
3460
+ if (!input || typeof input !== "string") return [];
3461
+ const {
3462
+ customPatterns = [],
3463
+ checkEncoded = true,
3464
+ minSeverity = "low"
3465
+ } = options;
3466
+ const severityOrder = { low: 0, medium: 1, high: 2 };
3467
+ const minSeverityLevel = severityOrder[minSeverity];
3468
+ const detections = [];
3469
+ const seenPatterns = /* @__PURE__ */ new Set();
3470
+ const normalizedInput = checkEncoded ? normalizeInput(input) : input;
3471
+ const allPatterns = [
3472
+ ...SQL_PATTERNS,
3473
+ ...checkEncoded ? ENCODED_PATTERNS : [],
3474
+ ...customPatterns.map((p) => ({ pattern: p, name: "Custom pattern", severity: "high" }))
3475
+ ];
3476
+ for (const { pattern, name, severity } of allPatterns) {
3477
+ if (severityOrder[severity] < minSeverityLevel) continue;
3478
+ pattern.lastIndex = 0;
3479
+ const testInput = checkEncoded ? normalizedInput : input;
3480
+ if (pattern.test(testInput)) {
3481
+ const key = `${name}:${severity}`;
3482
+ if (!seenPatterns.has(key)) {
3483
+ seenPatterns.add(key);
3484
+ detections.push({
3485
+ field: "",
3486
+ // Will be set by caller
3487
+ value: input,
3488
+ pattern: name,
3489
+ severity
3490
+ });
3491
+ }
3492
+ }
3493
+ }
3494
+ return detections;
3495
+ }
3496
+ function hasSQLInjection(input, minSeverity = "medium") {
3497
+ return detectSQLInjection(input, { minSeverity }).length > 0;
3498
+ }
3499
+ function sanitizeSQLInput(input) {
3500
+ if (!input || typeof input !== "string") return "";
3501
+ let result = input;
3502
+ result = result.replace(/\0/g, "");
3503
+ result = result.replace(/'/g, "''");
3504
+ result = result.replace(/;/g, "");
3505
+ result = result.replace(/--/g, "");
3506
+ result = result.replace(/\/\*/g, "");
3507
+ result = result.replace(/\*\//g, "");
3508
+ result = result.replace(/0x[0-9a-f]+/gi, "");
3509
+ return result;
3510
+ }
3511
+ function detectSQLInjectionInObject(obj, options = {}) {
3512
+ const { fields, deep = true, customPatterns, minSeverity } = options;
3513
+ const detections = [];
3514
+ function walk(value, path) {
3515
+ if (typeof value === "string") {
3516
+ if (fields && fields.length > 0) {
3517
+ const fieldName = path.split(".").pop() || path;
3518
+ if (!fields.includes(fieldName)) return;
3519
+ }
3520
+ const detected = detectSQLInjection(value, { customPatterns, minSeverity });
3521
+ for (const d of detected) {
3522
+ detections.push({ ...d, field: path });
3523
+ }
3524
+ } else if (deep && Array.isArray(value)) {
3525
+ value.forEach((item, i) => walk(item, `${path}[${i}]`));
3526
+ } else if (deep && typeof value === "object" && value !== null) {
3527
+ for (const [key, val] of Object.entries(value)) {
3528
+ walk(val, path ? `${path}.${key}` : key);
3529
+ }
3530
+ }
3531
+ }
3532
+ walk(obj, "");
3533
+ return detections;
3534
+ }
3535
+
3536
+ // src/middleware/validation/middleware.ts
3537
+ function withValidation(handler, config) {
3538
+ const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
3539
+ return async (req) => {
3540
+ const result = await validateRequest(req, {
3541
+ body: config.body,
3542
+ query: config.query,
3543
+ params: config.params,
3544
+ routeParams: config.routeParams
3545
+ });
3546
+ if (!result.success) {
3547
+ return onError(req, result.errors || []);
3548
+ }
3549
+ return handler(req, { validated: result.data });
3550
+ };
3551
+ }
3552
+ function withSanitization(handler, config = {}) {
3553
+ const {
3554
+ fields,
3555
+ mode = "escape",
3556
+ allowedTags,
3557
+ skip,
3558
+ onSanitized
3559
+ } = config;
3560
+ return async (req) => {
3561
+ if (skip && await skip(req)) {
3562
+ return handler(req, { sanitized: null, changes: [] });
3563
+ }
3564
+ let body;
3565
+ try {
3566
+ body = await req.json();
3567
+ } catch {
3568
+ return handler(req, { sanitized: null, changes: [] });
3569
+ }
3570
+ const changes = [];
3571
+ const sanitized = walkObject(body, (value, path) => {
3572
+ if (fields && fields.length > 0) {
3573
+ const fieldName = path.split(".").pop() || path;
3574
+ if (!fields.includes(fieldName)) {
3575
+ return value;
3576
+ }
3577
+ }
3578
+ const cleaned = sanitize(value, { mode, allowedTags });
3579
+ if (cleaned !== value) {
3580
+ changes.push({
3581
+ field: path,
3582
+ original: value,
3583
+ sanitized: cleaned
3584
+ });
3585
+ }
3586
+ return cleaned;
3587
+ }, "");
3588
+ if (onSanitized && changes.length > 0) {
3589
+ onSanitized(req, changes);
3590
+ }
3591
+ return handler(req, { sanitized, changes });
3592
+ };
3593
+ }
3594
+ function withXSSProtection(handler, config = {}) {
3595
+ const { fields, onDetection, checkQuery = true } = config;
3596
+ return async (req) => {
3597
+ const detections = [];
3598
+ if (checkQuery) {
3599
+ const url = new URL(req.url);
3600
+ for (const [key, value] of url.searchParams.entries()) {
3601
+ if (detectXSS(value)) {
3602
+ detections.push({ field: `query.${key}`, value });
3603
+ }
3604
+ }
3605
+ }
3606
+ let body;
3607
+ try {
3608
+ body = await req.json();
3609
+ } catch {
3610
+ body = null;
3611
+ }
3612
+ if (body) {
3613
+ walkObject(body, (value, path) => {
3614
+ if (fields && fields.length > 0) {
3615
+ const fieldName = path.split(".").pop() || path;
3616
+ if (!fields.includes(fieldName)) {
3617
+ return value;
3618
+ }
3619
+ }
3620
+ if (detectXSS(value)) {
3621
+ detections.push({ field: path, value });
3622
+ }
3623
+ return value;
3624
+ }, "");
3625
+ }
3626
+ if (detections.length > 0) {
3627
+ if (onDetection) {
3628
+ for (const { field, value } of detections) {
3629
+ const result = await onDetection(req, field, value);
3630
+ if (result instanceof Response) {
3631
+ return result;
3632
+ }
3633
+ }
3634
+ }
3635
+ return new Response(
3636
+ JSON.stringify({
3637
+ error: "xss_detected",
3638
+ message: "Potentially malicious content detected",
3639
+ fields: detections.map((d) => d.field)
3640
+ }),
3641
+ {
3642
+ status: 400,
3643
+ headers: { "Content-Type": "application/json" }
3644
+ }
3645
+ );
3646
+ }
3647
+ return handler(req);
3648
+ };
3649
+ }
3650
+ function withSQLProtection(handler, config = {}) {
3651
+ const {
3652
+ fields,
3653
+ deep = true,
3654
+ mode = "block",
3655
+ customPatterns,
3656
+ allowList = [],
3657
+ onDetection
3658
+ } = config;
3659
+ return async (req) => {
3660
+ let body;
3661
+ try {
3662
+ body = await req.json();
3663
+ } catch {
3664
+ return handler(req);
3665
+ }
3666
+ const detections = detectSQLInjectionInObject(body, {
3667
+ fields,
3668
+ deep,
3669
+ customPatterns,
3670
+ minSeverity: mode === "detect" ? "low" : "medium"
3671
+ });
3672
+ const filtered = detections.filter((d) => !allowList.includes(d.value));
3673
+ if (filtered.length > 0) {
3674
+ if (onDetection) {
3675
+ const result = await onDetection(req, filtered);
3676
+ if (result instanceof Response) {
3677
+ return result;
3678
+ }
3679
+ }
3680
+ if (mode === "block") {
3681
+ return new Response(
3682
+ JSON.stringify({
3683
+ error: "sql_injection_detected",
3684
+ message: "Potentially malicious SQL detected",
3685
+ detections: filtered.map((d) => ({
3686
+ field: d.field,
3687
+ pattern: d.pattern,
3688
+ severity: d.severity
3689
+ }))
3690
+ }),
3691
+ {
3692
+ status: 400,
3693
+ headers: { "Content-Type": "application/json" }
3694
+ }
3695
+ );
3696
+ }
3697
+ }
3698
+ return handler(req);
3699
+ };
3700
+ }
3701
+ function withContentType(handler, config) {
3702
+ const onInvalid = config.onInvalid || ((_, contentType) => defaultContentTypeErrorResponse(contentType, `Content-Type '${contentType}' is not allowed`));
3703
+ return async (req) => {
3704
+ const result = validateContentType(req, config);
3705
+ if (!result.valid) {
3706
+ return onInvalid(req, result.contentType);
3707
+ }
3708
+ return handler(req);
3709
+ };
3710
+ }
3711
+ function withFileValidation(handler, config = {}) {
3712
+ const onInvalid = config.onInvalid || ((_, errors) => defaultFileErrorResponse(errors));
3713
+ return async (req) => {
3714
+ const result = await validateFilesFromRequest(req, config);
3715
+ if (!result.valid) {
3716
+ return onInvalid(req, result.errors);
3717
+ }
3718
+ return handler(req, { files: result.files });
3719
+ };
3720
+ }
3721
+ function withSecureValidation(handler, config) {
3722
+ return async (req) => {
3723
+ const allErrors = [];
3724
+ if (config.contentType) {
3725
+ const ctResult = validateContentType(req, config.contentType);
3726
+ if (!ctResult.valid) {
3727
+ allErrors.push({
3728
+ field: "Content-Type",
3729
+ code: "invalid_content_type",
3730
+ message: ctResult.reason || "Invalid Content-Type"
3731
+ });
3732
+ }
3733
+ }
3734
+ let files;
3735
+ if (config.files) {
3736
+ const fileResult = await validateFilesFromRequest(req, config.files);
3737
+ if (!fileResult.valid) {
3738
+ allErrors.push(...fileResult.errors.map((e) => ({
3739
+ field: e.field || e.filename,
3740
+ code: e.code,
3741
+ message: e.message
3742
+ })));
3743
+ } else {
3744
+ files = fileResult.files;
3745
+ }
3746
+ }
3747
+ if (allErrors.length > 0) {
3748
+ const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
3749
+ return onError(req, allErrors);
3750
+ }
3751
+ let validated;
3752
+ if (config.schema) {
3753
+ const schemaResult = await validateRequest(req, {
3754
+ body: config.schema.body,
3755
+ query: config.schema.query,
3756
+ params: config.schema.params,
3757
+ routeParams: config.routeParams
3758
+ });
3759
+ if (!schemaResult.success) {
3760
+ allErrors.push(...schemaResult.errors || []);
3761
+ } else {
3762
+ validated = schemaResult.data;
3763
+ }
3764
+ } else {
3765
+ validated = {
3766
+ body: {},
3767
+ query: {},
3768
+ params: {}
3769
+ };
3770
+ }
3771
+ if (config.sql && validated?.body) {
3772
+ const sqlDetections = detectSQLInjectionInObject(validated.body, {
3773
+ fields: config.sql.fields,
3774
+ deep: config.sql.deep,
3775
+ customPatterns: config.sql.customPatterns
3776
+ });
3777
+ if (sqlDetections.length > 0 && config.sql.mode !== "detect") {
3778
+ allErrors.push(...sqlDetections.map((d) => ({
3779
+ field: d.field,
3780
+ code: "sql_injection",
3781
+ message: `Potential SQL injection detected: ${d.pattern}`
3782
+ })));
3783
+ }
3784
+ }
3785
+ if (config.xss?.enabled && validated?.body) {
3786
+ walkObject(validated.body, (value, path) => {
3787
+ if (config.xss?.fields && config.xss.fields.length > 0) {
3788
+ const fieldName = path.split(".").pop() || path;
3789
+ if (!config.xss.fields.includes(fieldName)) {
3790
+ return value;
3791
+ }
3792
+ }
3793
+ if (detectXSS(value)) {
3794
+ allErrors.push({
3795
+ field: path,
3796
+ code: "xss_detected",
3797
+ message: "Potentially malicious content detected"
3798
+ });
3799
+ }
3800
+ return value;
3801
+ }, "");
3802
+ }
3803
+ if (allErrors.length > 0) {
3804
+ const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
3805
+ return onError(req, allErrors);
3806
+ }
3807
+ return handler(req, { validated, files });
3808
+ };
3809
+ }
1447
3810
 
1448
3811
  // src/index.ts
1449
- var VERSION = "0.3.0";
3812
+ var VERSION = "0.5.0";
1450
3813
 
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 };
3814
+ export { AuthenticationError, AuthorizationError, ConfigurationError, CsrfError, DANGEROUS_EXTENSIONS, MIME_TYPES, MemoryStore, PRESET_API, PRESET_RELAXED, PRESET_STRICT, RateLimitError, SecureError, VERSION, ValidationError, anonymizeIp, buildCSP, buildHSTS, buildPermissionsPolicy, checkRateLimit, clearAllRateLimits, createToken as createCSRFToken, createMemoryStore, createRateLimiter, createSecurityHeaders, createSecurityHeadersObject, createValidator, decodeJWT, detectFileType, detectSQLInjection, detectXSS, escapeHtml, extractBearerToken, formatDuration, generateCSRF, getClientIp, getGeoInfo, getGlobalMemoryStore, getPreset, getRateLimitStatus, hasPathTraversal, hasSQLInjection, isFormRequest, isJsonRequest, isLocalhost, isPrivateIp, isSecureError, isValidIp, normalizeIp, nowInMs, nowInSeconds, parseDuration, resetRateLimit, sanitize, sanitizeFields, sanitizeFilename, sanitizeObject, sanitizePath, sanitizeSQLInput, sleep, stripHtml, toSecureError, tokensMatch, validate, validateBody, validateCSRF, validateContentType, validateFile, validateFiles, validatePath, validateQuery, validateRequest, verifyToken as verifyCSRFToken, verifyJWT, withAPIKey, withAuth, withCSRF, withContentType, withFileValidation, withJWT, withOptionalAuth, withRateLimit, withRoles, withSQLProtection, withSanitization, withSecureValidation, withSecurityHeaders, withSession, withValidation, withXSSProtection };
1452
3815
  //# sourceMappingURL=index.js.map
1453
3816
  //# sourceMappingURL=index.js.map