From febc9bd5b5ccc8eeb7ce23312384ca3b0a4b62b5 Mon Sep 17 00:00:00 2001 From: jango-blockchained Date: Sun, 23 Mar 2025 13:00:02 +0100 Subject: [PATCH] chore: Update configuration and dependencies for enhanced MCP server functionality - Add RATE_LIMIT_MAX_AUTH_REQUESTS to .env.example for improved rate limiting - Update bun.lock and package.json to include new dependencies: @anthropic-ai/sdk, express-rate-limit, and their type definitions - Modify bunfig.toml for build settings and output configuration - Refactor src/config.ts to incorporate rate limiting settings - Implement security middleware for enhanced request validation and sanitization - Introduce rate limiting middleware for API and authentication endpoints - Add tests for configuration validation and rate limiting functionality --- .env.example | 1 + bun.lock | 230 +++----------- bunfig.toml | 36 ++- extra/ha-analyzer-cli.ts | 125 ++++---- package.json | 27 +- silent-mcp.sh | 25 -- src/__tests__/config.test.ts | 106 +++++++ src/__tests__/rate-limit.test.ts | 85 ++++++ src/__tests__/security.test.ts | 169 +++++++++++ src/config.ts | 89 +++--- src/index.ts | 33 +- src/middleware/rate-limit.middleware.ts | 26 ++ src/openapi.ts | 284 ++++++++++++++++++ src/schemas/config.schema.ts | 79 +++++ .../__tests__/enhanced-middleware.test.ts | 135 +++++++++ src/security/enhanced-middleware.ts | 189 ++++++++++++ test-cursor.sh | 13 - test-jsonrpc.js | 146 --------- test/setup.ts | 33 ++ webpack.config.cjs | 48 --- 20 files changed, 1347 insertions(+), 532 deletions(-) delete mode 100755 silent-mcp.sh create mode 100644 src/__tests__/config.test.ts create mode 100644 src/__tests__/rate-limit.test.ts create mode 100644 src/__tests__/security.test.ts create mode 100644 src/middleware/rate-limit.middleware.ts create mode 100644 src/openapi.ts create mode 100644 src/schemas/config.schema.ts create mode 100644 src/security/__tests__/enhanced-middleware.test.ts create mode 100644 src/security/enhanced-middleware.ts delete mode 100755 test-cursor.sh delete mode 100755 test-jsonrpc.js create mode 100644 test/setup.ts delete mode 100644 webpack.config.cjs diff --git a/.env.example b/.env.example index 36f9d06..b2b30d7 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,7 @@ JWT_ALGORITHM=HS256 # Rate Limiting RATE_LIMIT_WINDOW=900000 RATE_LIMIT_MAX_REQUESTS=100 +RATE_LIMIT_MAX_AUTH_REQUESTS=5 RATE_LIMIT_REGULAR=100 RATE_LIMIT_WEBSOCKET=1000 diff --git a/bun.lock b/bun.lock index db7e549..e0bb5f2 100644 --- a/bun.lock +++ b/bun.lock @@ -4,11 +4,14 @@ "": { "name": "homeassistant-mcp", "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", "@elysiajs/cors": "^1.2.0", "@elysiajs/swagger": "^1.2.0", + "@types/express-rate-limit": "^5.1.3", "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.11.24", - "@types/sanitize-html": "^2.9.5", + "@types/sanitize-html": "^2.13.0", + "@types/swagger-ui-express": "^4.1.8", "@types/ws": "^8.5.10", "@xmldom/xmldom": "^0.9.7", "chalk": "^5.4.1", @@ -16,12 +19,15 @@ "dotenv": "^16.4.7", "elysia": "^1.2.11", "express": "^4.21.2", + "express-rate-limit": "^7.5.0", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "node-fetch": "^3.3.2", "node-record-lpcm16": "^1.0.1", "openai": "^4.83.0", - "sanitize-html": "^2.11.0", + "openapi-types": "^12.1.3", + "sanitize-html": "^2.15.0", + "swagger-ui-express": "^5.0.1", "typescript": "^5.3.3", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", @@ -34,6 +40,7 @@ "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", + "@types/supertest": "^6.0.2", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", @@ -44,19 +51,16 @@ "eslint-plugin-prettier": "^5.1.3", "husky": "^9.0.11", "prettier": "^3.2.5", - "supertest": "^6.3.3", - "terser-webpack-plugin": "^5.3.10", - "ts-loader": "^9.5.1", - "uuid": "^11.0.5", - "webpack": "^5.98.0", - "webpack-cli": "^5.1.4", - "webpack-node-externals": "^3.0.0", + "supertest": "^7.1.0", + "uuid": "^11.1.0", }, }, }, "packages": { "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="], + "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], "@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="], @@ -127,8 +131,6 @@ "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, ""], - "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], - "@elysiajs/cors": ["@elysiajs/cors@1.2.0", "", { "peerDependencies": { "elysia": ">= 1.2.0" } }, ""], "@elysiajs/swagger": ["@elysiajs/swagger@1.2.0", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, ""], @@ -173,8 +175,6 @@ "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], - "@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], @@ -193,6 +193,8 @@ "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, ""], + "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.15", "", {}, ""], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], @@ -205,16 +207,18 @@ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cookiejar": ["@types/cookiejar@2.1.5", "", {}, "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q=="], + "@types/cors": ["@types/cors@2.8.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], - "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], - "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="], + "@types/express-rate-limit": ["@types/express-rate-limit@5.1.3", "", { "dependencies": { "@types/express": "*" } }, "sha512-H+TYy3K53uPU2TqPGFYaiWc2xJV6+bIFkDd/Ma2/h67Pa6ARk9kWE0p/K9OH1Okm0et9Sfm66fmXoAxsH2PHXg=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], @@ -233,6 +237,8 @@ "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.8", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, ""], + "@types/methods": ["@types/methods@1.1.4", "", {}, "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], "@types/ms": ["@types/ms@2.1.0", "", {}, ""], @@ -253,6 +259,12 @@ "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], + "@types/superagent": ["@types/superagent@8.1.9", "", { "dependencies": { "@types/cookiejar": "^2.1.5", "@types/methods": "^1.1.4", "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ=="], + + "@types/supertest": ["@types/supertest@6.0.2", "", { "dependencies": { "@types/methods": "^1.1.4", "@types/superagent": "^8.1.0" } }, "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg=="], + + "@types/swagger-ui-express": ["@types/swagger-ui-express@4.1.8", "", { "dependencies": { "@types/express": "*", "@types/serve-static": "*" } }, "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, ""], "@types/uuid": ["@types/uuid@10.0.0", "", {}, ""], @@ -283,48 +295,8 @@ "@unhead/schema": ["@unhead/schema@1.11.18", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, ""], - "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], - - "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], - - "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], - - "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], - - "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], - - "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], - - "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], - - "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], - - "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], - - "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], - - "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], - - "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], - - "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], - - "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], - - "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], - - "@webpack-cli/configtest": ["@webpack-cli/configtest@2.1.1", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw=="], - - "@webpack-cli/info": ["@webpack-cli/info@2.0.2", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A=="], - - "@webpack-cli/serve": ["@webpack-cli/serve@2.0.5", "", { "peerDependencies": { "webpack": "5.x.x", "webpack-cli": "5.x.x" } }, "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.9.7", "", {}, ""], - "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], - - "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, ""], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -337,10 +309,6 @@ "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, ""], "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -377,8 +345,6 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, ""], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, ""], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -395,12 +361,8 @@ "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], - "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, ""], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], @@ -409,14 +371,10 @@ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, ""], - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, ""], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, ""], - "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], - "component-emitter": ["component-emitter@1.3.1", "", {}, ""], "concat-map": ["concat-map@0.0.1", "", {}, ""], @@ -483,18 +441,12 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], - "entities": ["entities@4.5.0", "", {}, ""], - "envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, ""], "es-errors": ["es-errors@1.3.0", "", {}, ""], - "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, ""], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -529,12 +481,12 @@ "event-target-shim": ["event-target-shim@5.0.1", "", {}, ""], - "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""], "fast-diff": ["fast-diff@1.3.0", "", {}, ""], @@ -549,8 +501,6 @@ "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], - "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, ""], "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], @@ -569,8 +519,6 @@ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""], - "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], - "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, ""], "flatted": ["flatted@3.3.2", "", {}, ""], @@ -585,7 +533,7 @@ "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, ""], - "formidable": ["formidable@2.1.2", "", { "dependencies": { "dezalgo": "^1.0.4", "hexoid": "^1.0.0", "once": "^1.4.0", "qs": "^6.11.0" } }, ""], + "formidable": ["formidable@3.5.2", "", { "dependencies": { "dezalgo": "^1.0.4", "hexoid": "^2.0.0", "once": "^1.4.0" } }, "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg=="], "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], @@ -609,8 +557,6 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, ""], - "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], - "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, ""], "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, ""], @@ -629,7 +575,7 @@ "helmet": ["helmet@7.2.0", "", {}, ""], - "hexoid": ["hexoid@1.0.0", "", {}, ""], + "hexoid": ["hexoid@2.0.0", "", {}, "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw=="], "hookable": ["hookable@5.5.3", "", {}, ""], @@ -647,22 +593,16 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""], - "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, ""], "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, ""], "inherits": ["inherits@2.0.4", "", {}, ""], - "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-arrayish": ["is-arrayish@0.3.2", "", {}, ""], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, ""], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""], @@ -677,8 +617,6 @@ "isexe": ["isexe@2.0.0", "", {}, ""], - "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], - "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], @@ -701,7 +639,7 @@ "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], - "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], + "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -711,8 +649,6 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, ""], - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, ""], @@ -727,14 +663,10 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, ""], - "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - "kuler": ["kuler@2.0.0", "", {}, ""], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, ""], - "loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, ""], "lodash.includes": ["lodash.includes@4.3.0", "", {}, ""], @@ -793,8 +725,6 @@ "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], - "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, ""], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, ""], @@ -843,8 +773,6 @@ "path-key": ["path-key@3.1.1", "", {}, ""], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], "path-type": ["path-type@4.0.0", "", {}, ""], @@ -857,8 +785,6 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], - "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], - "postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""], "prelude-ls": ["prelude-ls@1.2.1", "", {}, ""], @@ -877,8 +803,6 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, ""], - "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], @@ -887,14 +811,8 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, ""], - "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - - "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], - "resolve-from": ["resolve-from@4.0.0", "", {}, ""], "reusify": ["reusify@1.0.4", "", {}, ""], @@ -909,22 +827,16 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sanitize-html": ["sanitize-html@2.14.0", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, ""], - - "schema-utils": ["schema-utils@4.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g=="], + "sanitize-html": ["sanitize-html@2.15.0", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA=="], "semver": ["semver@7.7.1", "", { "bin": "bin/semver.js" }, ""], "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], - "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], - "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], "shebang-regex": ["shebang-regex@3.0.0", "", {}, ""], @@ -943,12 +855,8 @@ "slash": ["slash@3.0.0", "", {}, ""], - "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, ""], - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "stack-trace": ["stack-trace@0.0.10", "", {}, ""], @@ -963,22 +871,18 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, ""], - "superagent": ["superagent@8.1.2", "", { "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", "formidable": "^2.1.2", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0", "semver": "^7.3.8" } }, ""], + "superagent": ["superagent@9.0.2", "", { "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.4", "debug": "^4.3.4", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.0", "formidable": "^3.5.1", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.11.0" } }, "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w=="], - "supertest": ["supertest@6.3.4", "", { "dependencies": { "methods": "^1.1.2", "superagent": "^8.1.2" } }, ""], + "supertest": ["supertest@7.1.0", "", { "dependencies": { "methods": "^1.1.2", "superagent": "^9.0.1" } }, "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "swagger-ui-dist": ["swagger-ui-dist@5.20.1", "", { "dependencies": { "@scarf/scarf": "=1.4.0" } }, "sha512-qBPCis2w8nP4US7SvUxdJD3OwKcqiWeZmjN2VWhq2v+ESZEXOP/7n4DeiOiiZcGYTKMHAHUUrroHaTsjUWTEGw=="], + + "swagger-ui-express": ["swagger-ui-express@5.0.1", "", { "dependencies": { "swagger-ui-dist": ">=5.0.0" }, "peerDependencies": { "express": ">=4.0.0 || >=5.0.0-beta" } }, "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA=="], "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, ""], - "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], - - "terser": ["terser@5.39.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw=="], - - "terser-webpack-plugin": ["terser-webpack-plugin@5.3.14", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw=="], - "test-exclude": ["test-exclude@6.0.0", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" } }, "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w=="], "text-hex": ["text-hex@1.0.0", "", {}, ""], @@ -997,8 +901,6 @@ "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, ""], - "ts-loader": ["ts-loader@9.5.2", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4", "source-map": "^0.7.4" }, "peerDependencies": { "typescript": "*", "webpack": "^5.0.0" } }, "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw=="], - "tslib": ["tslib@2.8.1", "", {}, ""], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, ""], @@ -1023,34 +925,20 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "uuid": ["uuid@11.0.5", "", { "bin": "dist/esm/bin/uuid" }, ""], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], - "watchpack": ["watchpack@2.4.2", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, ""], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, ""], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, ""], - "webpack": ["webpack@5.98.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA=="], - - "webpack-cli": ["webpack-cli@5.1.4", "", { "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", "@webpack-cli/info": "^2.0.2", "@webpack-cli/serve": "^2.0.5", "colorette": "^2.0.14", "commander": "^10.0.1", "cross-spawn": "^7.0.3", "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "peerDependencies": { "webpack": "5.x.x" }, "bin": { "webpack-cli": "bin/cli.js" } }, "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg=="], - - "webpack-merge": ["webpack-merge@5.10.0", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.0" } }, "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA=="], - - "webpack-node-externals": ["webpack-node-externals@3.0.0", "", {}, "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ=="], - - "webpack-sources": ["webpack-sources@3.2.3", "", {}, "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, ""], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], - "wildcard": ["wildcard@2.0.1", "", {}, "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="], - "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, ""], "winston-daily-rotate-file": ["winston-daily-rotate-file@5.0.0", "", { "dependencies": { "file-stream-rotator": "^0.6.1", "object-hash": "^3.0.0", "triple-beam": "^1.4.1", "winston-transport": "^4.7.0" }, "peerDependencies": { "winston": "^3" } }, ""], @@ -1073,6 +961,10 @@ "zod": ["zod@3.24.1", "", {}, ""], + "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.75", "", { "dependencies": { "undici-types": "~5.26.4" } }, ""], + + "@anthropic-ai/sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, ""], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1099,8 +991,6 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "clone-deep/is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], - "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, ""], "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, ""], @@ -1113,18 +1003,14 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""], + "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, ""], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "formdata-node/web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, ""], - - "formidable/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, ""], - "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], - "jest-haste-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], - "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], @@ -1141,27 +1027,17 @@ "openai/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, ""], - "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - - "resolve-cwd/resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "superagent/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, ""], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - - "ts-loader/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], - - "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, ""], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""], @@ -1191,8 +1067,6 @@ "jest-diff/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], - "jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "jest-matcher-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], "jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], @@ -1205,20 +1079,10 @@ "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, ""], - "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "ts-loader/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], - - "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], - "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/bunfig.toml b/bunfig.toml index 7586d09..20fbce4 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -19,10 +19,34 @@ collectCoverageFrom = [ ] [build] -target = "node" +target = "bun" outdir = "./dist" -minify = true +minify = { + whitespace = true, + syntax = true, + identifiers = true, + module = true +} sourcemap = "external" +entry = ["./src/index.ts", "./src/stdio-server.ts"] +splitting = true +naming = "[name].[hash].[ext]" +publicPath = "/assets/" +define = { + "process.env.NODE_ENV": "process.env.NODE_ENV" +} + +[build.javascript] +platform = "node" +format = "esm" +treeshaking = true +packages = { + external = ["bun:*"] +} + +[build.typescript] +dts = true +typecheck = true [install] production = false @@ -48,6 +72,12 @@ reload = true [performance] gc = true optimize = true +jit = true +smol = true +compact = true [test.env] -NODE_ENV = "test" \ No newline at end of file +NODE_ENV = "test" + +[watch] +ignore = ["**/node_modules/**", "**/dist/**", "**/.git/**"] \ No newline at end of file diff --git a/extra/ha-analyzer-cli.ts b/extra/ha-analyzer-cli.ts index 96af8fe..0dc0ab3 100644 --- a/extra/ha-analyzer-cli.ts +++ b/extra/ha-analyzer-cli.ts @@ -1,5 +1,5 @@ import fetch from "node-fetch"; -import OpenAI from "openai"; +import { Anthropic } from "@anthropic-ai/sdk"; import { DOMParser, Element, Document } from '@xmldom/xmldom'; import dotenv from 'dotenv'; import readline from 'readline'; @@ -9,11 +9,11 @@ import chalk from 'chalk'; dotenv.config(); // Retrieve API keys from environment variables -const openaiApiKey = process.env.OPENAI_API_KEY; +const anthropicApiKey = process.env.ANTHROPIC_API_KEY; const hassToken = process.env.HASS_TOKEN; -if (!openaiApiKey) { - console.error("Please set the OPENAI_API_KEY environment variable."); +if (!anthropicApiKey) { + console.error("Please set the ANTHROPIC_API_KEY environment variable."); process.exit(1); } @@ -113,13 +113,11 @@ interface ModelConfig { contextWindow: number; } -// Update model listing to filter based on API key availability +// Update model listing to use Anthropic's Claude models const AVAILABLE_MODELS: ModelConfig[] = [ - // OpenAI models always available - { name: 'gpt-4', maxTokens: 8192, contextWindow: 8192 }, - { name: 'gpt-4-turbo-preview', maxTokens: 4096, contextWindow: 128000 }, - { name: 'gpt-3.5-turbo', maxTokens: 4096, contextWindow: 16385 }, - { name: 'gpt-3.5-turbo-16k', maxTokens: 16385, contextWindow: 16385 }, + // Anthropic Claude models + { name: 'claude-3-7-sonnet-20250219', maxTokens: 4096, contextWindow: 200000 }, + { name: 'claude-3-5-haiku-20241022', maxTokens: 4096, contextWindow: 200000 }, // Conditionally include DeepSeek models ...(process.env.DEEPSEEK_API_KEY ? [ @@ -131,7 +129,7 @@ const AVAILABLE_MODELS: ModelConfig[] = [ // Add configuration interface interface AppConfig { mcpServer: string; - openaiModel: string; + anthropicModel: string; maxRetries: number; analysisTimeout: number; selectedModel: ModelConfig; @@ -146,30 +144,31 @@ const logger = { debug: (msg: string) => process.env.DEBUG && console.log(chalk.gray(`› ${msg}`)) }; -// Update default model selection in loadConfig +// Update loadConfig to use Claude models function loadConfig(): AppConfig { - // Always use gpt-4 for now - const defaultModel = AVAILABLE_MODELS.find(m => m.name === 'gpt-4') || AVAILABLE_MODELS[0]; + // Use Claude 3.7 Sonnet as the default model + const defaultModel = AVAILABLE_MODELS.find(m => m.name === 'claude-3-7-sonnet-20250219') || AVAILABLE_MODELS[0]; return { mcpServer: process.env.MCP_SERVER || 'http://localhost:3000', - openaiModel: defaultModel.name, + anthropicModel: defaultModel.name, maxRetries: parseInt(process.env.MAX_RETRIES || '3'), analysisTimeout: parseInt(process.env.ANALYSIS_TIMEOUT || '30000'), selectedModel: defaultModel }; } -function getOpenAIClient(): OpenAI { +// Replace OpenAI client with Anthropic client +function getAnthropicClient(): Anthropic { const config = loadConfig(); - return new OpenAI({ - apiKey: config.selectedModel.name.startsWith('deepseek') - ? process.env.DEEPSEEK_API_KEY - : openaiApiKey, - baseURL: config.selectedModel.name.startsWith('deepseek') - ? 'https://api.deepseek.com/v1' - : 'https://api.openai.com/v1' + if (config.selectedModel.name.startsWith('deepseek') && process.env.DEEPSEEK_API_KEY) { + // This is just a stub for DeepSeek - you'd need to implement this properly + throw new Error("DeepSeek models not implemented yet with Anthropic integration"); + } + + return new Anthropic({ + apiKey: anthropicApiKey, }); } @@ -463,7 +462,7 @@ function getRelevantDeviceTypes(prompt: string): string[] { } /** - * Generates analysis and recommendations using the OpenAI API based on the Home Assistant data + * Generates analysis and recommendations using the Anthropic API based on the Home Assistant data */ async function generateAnalysis(haInfo: any): Promise { const config = loadConfig(); @@ -520,7 +519,7 @@ async function generateAnalysis(haInfo: any): Promise { } // Original analysis code for non-test mode - const openai = getOpenAIClient(); + const anthropic = getAnthropicClient(); const systemSummary = { total_devices: haInfo.device_summary?.total_devices || 0, @@ -588,20 +587,21 @@ Generate your response in this EXACT format: `; try { - const completion = await openai.chat.completions.create({ + const completion = await anthropic.messages.create({ model: config.selectedModel.name, messages: [ { - role: "system", - content: "You are a Home Assistant expert. Analyze the system data and provide detailed insights in the specified XML format. Be specific and actionable in your recommendations." - }, - { role: "user", content: prompt } + role: "user", + content: `You are a Home Assistant expert. Analyze the system data and provide detailed insights in the specified XML format. Be specific and actionable in your recommendations. + + ${prompt}` + } ], temperature: 0.7, max_tokens: Math.min(config.selectedModel.maxTokens, 4000) }); - const result = completion.choices[0].message?.content || ""; + const result = completion.content[0]?.type === 'text' ? completion.content[0].text : ""; // Clean the response and parse XML const cleanedResult = result.replace(/```xml/g, '').replace(/```/g, '').trim(); @@ -673,7 +673,7 @@ Generate your response in this EXACT format: throw new Error(`Failed to parse analysis response: ${parseError.message}`); } } catch (error) { - console.error("Error during OpenAI API call:", error); + console.error("Error during Anthropic API call:", error); throw new Error("Failed to generate analysis"); } } @@ -814,7 +814,7 @@ async function handleAutomationOptimization(haInfo: any): Promise { } async function analyzeAutomations(automations: any[]): Promise { - const openai = getOpenAIClient(); + const anthropic = getAnthropicClient(); const config = loadConfig(); // Create a more detailed summary of automations @@ -894,20 +894,21 @@ Focus on: 5. Analyzing the distribution of automation types and suggesting optimizations`; try { - const completion = await openai.chat.completions.create({ + const completion = await anthropic.messages.create({ model: config.selectedModel.name, messages: [ { - role: "system", - content: "You are a Home Assistant automation expert. Analyze the provided automation summary and respond with specific, actionable suggestions in the required XML format." - }, - { role: "user", content: prompt } + role: "user", + content: `You are a Home Assistant automation expert. Analyze the provided automation summary and respond with specific, actionable suggestions in the required XML format. + + ${prompt}` + } ], temperature: 0.2, max_tokens: Math.min(config.selectedModel.maxTokens, 2048) }); - const response = completion.choices[0].message?.content || ""; + const response = completion.content[0]?.type === 'text' ? completion.content[0].text : ""; // Ensure the response is valid XML if (!response.trim().startsWith('')) { @@ -945,7 +946,7 @@ Focus on: } } -// Add new handleCustomPrompt function +// Update handleCustomPrompt function to use Anthropic async function handleCustomPrompt(haInfo: any, customPrompt: string): Promise { try { // Add device metadata @@ -1027,15 +1028,15 @@ async function handleCustomPrompt(haInfo: any, customPrompt: string): PromiseYou are a Home Assistant expert. Analyze the following Home Assistant information and respond to the user's prompt. Current system has ${totalDevices} devices across ${deviceTypes.length} types. Device distribution: ${deviceSummary} @@ -1047,16 +1048,17 @@ async function handleCustomPrompt(haInfo: any, customPrompt: string): Promise + + ${customPrompt}` + } ], - max_tokens: Math.min(config.selectedModel.maxTokens, 2048), // Limit token usage + max_tokens: Math.min(config.selectedModel.maxTokens, 2048), temperature: 0.3, }); console.log("\nAnalysis Results:\n"); - console.log(completion.choices[0].message?.content || "No response generated"); + console.log(completion.content[0]?.type === 'text' ? completion.content[0].text : "No response generated"); } catch (error) { console.error("Error processing custom prompt:", error); @@ -1075,24 +1077,25 @@ async function handleCustomPrompt(haInfo: any, customPrompt: string): PromiseYou are a Home Assistant expert. Provide a simple analysis of the system. + + ${retryPrompt}` + } ], - max_tokens: Math.min(config.selectedModel.maxTokens, 2048), // Limit token usage + max_tokens: Math.min(config.selectedModel.maxTokens, 2048), temperature: 0.3, }); console.log("\nAnalysis Results:\n"); - console.log(retryCompletion.choices[0].message?.content || "No response generated"); + console.log(retryCompletion.content[0]?.type === 'text' ? retryCompletion.content[0].text : "No response generated"); } catch (retryError) { console.error("Error during retry:", retryError); } @@ -1174,9 +1177,9 @@ function getItems(xmlDoc: Document, path: string): string[] { .map(item => (item as Element).textContent || ""); } -// Replace the Express server initialization at the bottom with Bun's server -if (process.env.PROCESSOR_TYPE === 'openai') { - // Initialize Bun server for OpenAI +// Replace the Express/Bun server initialization +if (process.env.PROCESSOR_TYPE === 'anthropic') { + // Initialize Bun server for Anthropic const server = Bun.serve({ port: process.env.PORT || 3000, async fetch(req) { @@ -1206,7 +1209,7 @@ if (process.env.PROCESSOR_TYPE === 'openai') { }, }); - console.log(`[OpenAI Server] Running on port ${server.port}`); + console.log(`[Anthropic Server] Running on port ${server.port}`); } else { console.log('[Claude Mode] Using stdio communication'); } diff --git a/package.json b/package.json index 0c74eb9..ec1fc2b 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,10 @@ "start:stdio": "bun run dist/stdio-server.js", "dev": "bun --hot --watch src/index.ts", "build": "bun build ./src/index.ts --outdir ./dist --target bun --minify", - "build:node": "webpack --config webpack.config.cjs", + "build:all": "bun build ./src/index.ts ./src/stdio-server.ts --outdir ./dist --target bun --minify", + "build:node": "bun build ./src/index.ts --outdir ./dist --target node --minify", "build:stdio": "bun build ./src/stdio-server.ts --outdir ./dist --target node --minify", - "prepare": "husky install && npm run build", + "prepare": "husky install && bun run build:all", "stdio": "node ./bin/mcp-stdio.js", "test": "bun test", "test:watch": "bun test --watch", @@ -32,11 +33,14 @@ "example:speech": "bun run extra/speech-to-text-example.ts" }, "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", "@elysiajs/cors": "^1.2.0", "@elysiajs/swagger": "^1.2.0", + "@types/express-rate-limit": "^5.1.3", "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.11.24", - "@types/sanitize-html": "^2.9.5", + "@types/sanitize-html": "^2.13.0", + "@types/swagger-ui-express": "^4.1.8", "@types/ws": "^8.5.10", "@xmldom/xmldom": "^0.9.7", "chalk": "^5.4.1", @@ -44,12 +48,15 @@ "dotenv": "^16.4.7", "elysia": "^1.2.11", "express": "^4.21.2", + "express-rate-limit": "^7.5.0", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.2", "node-fetch": "^3.3.2", "node-record-lpcm16": "^1.0.1", "openai": "^4.83.0", - "sanitize-html": "^2.11.0", + "openapi-types": "^12.1.3", + "sanitize-html": "^2.15.0", + "swagger-ui-express": "^5.0.1", "typescript": "^5.3.3", "winston": "^3.11.0", "winston-daily-rotate-file": "^5.0.0", @@ -62,6 +69,7 @@ "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", + "@types/supertest": "^6.0.2", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", @@ -72,13 +80,8 @@ "eslint-plugin-prettier": "^5.1.3", "husky": "^9.0.11", "prettier": "^3.2.5", - "supertest": "^6.3.3", - "terser-webpack-plugin": "^5.3.10", - "ts-loader": "^9.5.1", - "uuid": "^11.0.5", - "webpack": "^5.98.0", - "webpack-cli": "^5.1.4", - "webpack-node-externals": "^3.0.0" + "supertest": "^7.1.0", + "uuid": "^11.1.0" }, "engines": { "bun": ">=1.0.0", @@ -93,4 +96,4 @@ "README.md", "LICENSE" ] -} \ No newline at end of file +} diff --git a/silent-mcp.sh b/silent-mcp.sh deleted file mode 100755 index 5381ca5..0000000 --- a/silent-mcp.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# Ensure we're running in a clean environment for MCP -# Set silent environment variables -export LOG_LEVEL=silent -export USE_STDIO_TRANSPORT=true - -# Explicitly mark that we are NOT in Cursor mode -export CURSOR_COMPATIBLE=false - -# Flag to prevent recursive execution -export SILENT_MCP_RUNNING=true - -# Clean up any existing processes - optional but can help with "already" errors -# pkill -f "node.*stdio-server" >/dev/null 2>&1 || true - -# Direct execution - always use local file -if [ -f "./dist/stdio-server.js" ]; then - # Keep stdout intact (for JSON-RPC messages) but redirect stderr to /dev/null - node ./dist/stdio-server.js 2>/dev/null -else - # If no local file, run directly through node using the globally installed package - # This avoids calling npx again which would create a loop - node $(npm root -g)/homeassistant-mcp/dist/stdio-server.js 2>/dev/null -fi \ No newline at end of file diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts new file mode 100644 index 0000000..e48d6a4 --- /dev/null +++ b/src/__tests__/config.test.ts @@ -0,0 +1,106 @@ +import { expect, test, describe, beforeEach, afterEach } from 'bun:test'; +import { MCPServerConfigSchema } from '../schemas/config.schema.js'; + +describe('Configuration Validation', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Reset environment variables before each test + process.env = { ...originalEnv }; + }); + + afterEach(() => { + // Restore original environment after each test + process.env = originalEnv; + }); + + test('validates default configuration', () => { + const config = MCPServerConfigSchema.parse({}); + expect(config).toBeDefined(); + expect(config.port).toBe(3000); + expect(config.environment).toBe('development'); + }); + + test('validates custom port', () => { + const config = MCPServerConfigSchema.parse({ port: 8080 }); + expect(config.port).toBe(8080); + }); + + test('rejects invalid port', () => { + expect(() => MCPServerConfigSchema.parse({ port: 0 })).toThrow(); + expect(() => MCPServerConfigSchema.parse({ port: 70000 })).toThrow(); + }); + + test('validates environment values', () => { + expect(() => MCPServerConfigSchema.parse({ environment: 'development' })).not.toThrow(); + expect(() => MCPServerConfigSchema.parse({ environment: 'production' })).not.toThrow(); + expect(() => MCPServerConfigSchema.parse({ environment: 'test' })).not.toThrow(); + expect(() => MCPServerConfigSchema.parse({ environment: 'invalid' })).toThrow(); + }); + + test('validates rate limiting configuration', () => { + const config = MCPServerConfigSchema.parse({ + rateLimit: { + maxRequests: 50, + maxAuthRequests: 10 + } + }); + expect(config.rateLimit.maxRequests).toBe(50); + expect(config.rateLimit.maxAuthRequests).toBe(10); + }); + + test('rejects invalid rate limit values', () => { + expect(() => MCPServerConfigSchema.parse({ + rateLimit: { + maxRequests: 0, + maxAuthRequests: 5 + } + })).toThrow(); + + expect(() => MCPServerConfigSchema.parse({ + rateLimit: { + maxRequests: 100, + maxAuthRequests: -1 + } + })).toThrow(); + }); + + test('validates execution timeout', () => { + const config = MCPServerConfigSchema.parse({ executionTimeout: 5000 }); + expect(config.executionTimeout).toBe(5000); + }); + + test('rejects invalid execution timeout', () => { + expect(() => MCPServerConfigSchema.parse({ executionTimeout: 500 })).toThrow(); + expect(() => MCPServerConfigSchema.parse({ executionTimeout: 400000 })).toThrow(); + }); + + test('validates transport settings', () => { + const config = MCPServerConfigSchema.parse({ + useStdioTransport: true, + useHttpTransport: false + }); + expect(config.useStdioTransport).toBe(true); + expect(config.useHttpTransport).toBe(false); + }); + + test('validates CORS settings', () => { + const config = MCPServerConfigSchema.parse({ + corsOrigin: 'https://example.com' + }); + expect(config.corsOrigin).toBe('https://example.com'); + }); + + test('validates debug settings', () => { + const config = MCPServerConfigSchema.parse({ + debugMode: true, + debugStdio: true, + debugHttp: true, + silentStartup: false + }); + expect(config.debugMode).toBe(true); + expect(config.debugStdio).toBe(true); + expect(config.debugHttp).toBe(true); + expect(config.silentStartup).toBe(false); + }); +}); \ No newline at end of file diff --git a/src/__tests__/rate-limit.test.ts b/src/__tests__/rate-limit.test.ts new file mode 100644 index 0000000..da94973 --- /dev/null +++ b/src/__tests__/rate-limit.test.ts @@ -0,0 +1,85 @@ +import { expect, test, describe, beforeAll, afterAll } from 'bun:test'; +import express from 'express'; +import { apiLimiter, authLimiter } from '../middleware/rate-limit.middleware.js'; +import supertest from 'supertest'; + +describe('Rate Limiting Middleware', () => { + let app: express.Application; + let request: supertest.SuperTest; + + beforeAll(() => { + app = express(); + + // Set up test routes with rate limiting + app.use('/api', apiLimiter); + app.use('/auth', authLimiter); + + // Test endpoints + app.get('/api/test', (req, res) => { + res.json({ message: 'API test successful' }); + }); + + app.post('/auth/login', (req, res) => { + res.json({ message: 'Login successful' }); + }); + + request = supertest(app); + }); + + test('allows requests within API rate limit', async () => { + // Make multiple requests within the limit + for (let i = 0; i < 5; i++) { + const response = await request.get('/api/test'); + expect(response.status).toBe(200); + expect(response.body.message).toBe('API test successful'); + } + }); + + test('enforces API rate limit', async () => { + // Make more requests than the limit allows + const requests = Array(150).fill(null).map(() => + request.get('/api/test') + ); + + const responses = await Promise.all(requests); + + // Some requests should be successful, others should be rate limited + const successfulRequests = responses.filter(r => r.status === 200); + const limitedRequests = responses.filter(r => r.status === 429); + + expect(successfulRequests.length).toBeGreaterThan(0); + expect(limitedRequests.length).toBeGreaterThan(0); + }); + + test('allows requests within auth rate limit', async () => { + // Make multiple requests within the limit + for (let i = 0; i < 3; i++) { + const response = await request.post('/auth/login'); + expect(response.status).toBe(200); + expect(response.body.message).toBe('Login successful'); + } + }); + + test('enforces stricter auth rate limit', async () => { + // Make more requests than the auth limit allows + const requests = Array(10).fill(null).map(() => + request.post('/auth/login') + ); + + const responses = await Promise.all(requests); + + // Some requests should be successful, others should be rate limited + const successfulRequests = responses.filter(r => r.status === 200); + const limitedRequests = responses.filter(r => r.status === 429); + + expect(successfulRequests.length).toBeLessThan(10); + expect(limitedRequests.length).toBeGreaterThan(0); + }); + + test('includes rate limit headers', async () => { + const response = await request.get('/api/test'); + expect(response.headers['ratelimit-limit']).toBeDefined(); + expect(response.headers['ratelimit-remaining']).toBeDefined(); + expect(response.headers['ratelimit-reset']).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/src/__tests__/security.test.ts b/src/__tests__/security.test.ts new file mode 100644 index 0000000..551a51f --- /dev/null +++ b/src/__tests__/security.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, test, beforeEach } from 'bun:test'; +import express, { Request, Response } from 'express'; +import request from 'supertest'; +import { SecurityMiddleware } from '../security/enhanced-middleware'; + +describe('SecurityMiddleware', () => { + const app = express(); + + // Apply security middleware + app.use(SecurityMiddleware.createRouter()); + + // Test routes + app.get('/test', (_req: Request, res: Response) => { + res.status(200).json({ message: 'Test successful' }); + }); + + app.post('/test', (req: Request, res: Response) => { + res.status(200).json(req.body); + }); + + app.post('/auth/login', (_req: Request, res: Response) => { + res.status(200).json({ message: 'Auth successful' }); + }); + + describe('Security Headers', () => { + test('should set security headers correctly', async () => { + const response = await request(app).get('/test'); + + expect(response.status).toBe(200); + expect(response.headers['x-frame-options']).toBe('DENY'); + expect(response.headers['x-xss-protection']).toBe('1; mode=block'); + expect(response.headers['x-content-type-options']).toBe('nosniff'); + expect(response.headers['referrer-policy']).toBe('strict-origin-when-cross-origin'); + expect(response.headers['strict-transport-security']).toBe('max-age=31536000; includeSubDomains; preload'); + expect(response.headers['x-permitted-cross-domain-policies']).toBe('none'); + expect(response.headers['cross-origin-embedder-policy']).toBe('require-corp'); + expect(response.headers['cross-origin-opener-policy']).toBe('same-origin'); + expect(response.headers['cross-origin-resource-policy']).toBe('same-origin'); + expect(response.headers['origin-agent-cluster']).toBe('?1'); + expect(response.headers['x-powered-by']).toBeUndefined(); + }); + + test('should set Content-Security-Policy header correctly', async () => { + const response = await request(app).get('/test'); + + expect(response.status).toBe(200); + expect(response.headers['content-security-policy']).toContain("default-src 'self'"); + expect(response.headers['content-security-policy']).toContain("script-src 'self' 'unsafe-inline'"); + expect(response.headers['content-security-policy']).toContain("style-src 'self' 'unsafe-inline'"); + expect(response.headers['content-security-policy']).toContain("img-src 'self' data: https:"); + expect(response.headers['content-security-policy']).toContain("font-src 'self'"); + expect(response.headers['content-security-policy']).toContain("connect-src 'self'"); + expect(response.headers['content-security-policy']).toContain("frame-ancestors 'none'"); + expect(response.headers['content-security-policy']).toContain("form-action 'self'"); + }); + }); + + describe('Request Validation', () => { + test('should reject requests with long URLs', async () => { + const longUrl = '/test?' + 'x'.repeat(2500); + const response = await request(app).get(longUrl); + expect(response.status).toBe(413); + expect(response.body.error).toBe(true); + expect(response.body.message).toContain('URL too long'); + }); + + test('should reject large request bodies', async () => { + const largeBody = { data: 'x'.repeat(2 * 1024 * 1024) }; // 2MB + const response = await request(app) + .post('/test') + .set('Content-Type', 'application/json') + .send(largeBody); + expect(response.status).toBe(413); + expect(response.body.error).toBe(true); + expect(response.body.message).toContain('Request body too large'); + }); + + test('should require correct content type for POST requests', async () => { + const response = await request(app) + .post('/test') + .set('Content-Type', 'text/plain') + .send('test data'); + expect(response.status).toBe(415); + expect(response.body.error).toBe(true); + expect(response.body.message).toContain('Content-Type must be application/json'); + }); + }); + + describe('Input Sanitization', () => { + test('should sanitize string input with HTML', async () => { + const response = await request(app) + .post('/test') + .set('Content-Type', 'application/json') + .send({ text: 'Hello' }); + expect(response.status).toBe(200); + expect(response.body.text).toBe('Hello'); + }); + + test('should sanitize nested object input', async () => { + const response = await request(app) + .post('/test') + .set('Content-Type', 'application/json') + .send({ + user: { + name: 'John', + bio: 'Developer' + } + }); + expect(response.status).toBe(200); + expect(response.body.user.name).toBe('John'); + expect(response.body.user.bio).toBe('Developer'); + }); + + test('should sanitize array input', async () => { + const response = await request(app) + .post('/test') + .set('Content-Type', 'application/json') + .send({ + items: [ + 'Hello', + 'World' + ] + }); + expect(response.status).toBe(200); + expect(response.body.items[0]).toBe('Hello'); + expect(response.body.items[1]).toBe('World'); + }); + }); + + describe('Rate Limiting', () => { + beforeEach(() => { + SecurityMiddleware.clearRateLimits(); + }); + + test('should enforce regular rate limits', async () => { + // Make 50 requests (should succeed) + for (let i = 0; i < 50; i++) { + const response = await request(app).get('/test'); + expect(response.status).toBe(200); + } + + // 51st request should fail + const response = await request(app).get('/test'); + expect(response.status).toBe(429); + expect(response.body.error).toBe(true); + expect(response.body.message).toContain('Too many requests'); + }); + + test('should enforce stricter auth rate limits', async () => { + // Make 3 auth requests (should succeed) + for (let i = 0; i < 3; i++) { + const response = await request(app) + .post('/auth/login') + .set('Content-Type', 'application/json') + .send({}); + expect(response.status).toBe(200); + } + + // 4th auth request should fail + const response = await request(app) + .post('/auth/login') + .set('Content-Type', 'application/json') + .send({}); + expect(response.status).toBe(429); + expect(response.body.error).toBe(true); + expect(response.body.message).toContain('Too many authentication requests'); + }); + }); +}); \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 31451de..d1da1d7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,50 +3,59 @@ * Values can be overridden using environment variables */ -export interface MCPServerConfig { - // Server configuration - port: number; - environment: string; +import { MCPServerConfigSchema, MCPServerConfigType } from './schemas/config.schema.js'; +import { logger } from './utils/logger.js'; - // Execution settings - executionTimeout: number; - streamingEnabled: boolean; +function loadConfig(): MCPServerConfigType { + try { + const rawConfig = { + // Server configuration + port: parseInt(process.env.PORT || '3000', 10), + environment: process.env.NODE_ENV || 'development', - // Transport settings - useStdioTransport: boolean; - useHttpTransport: boolean; + // Execution settings + executionTimeout: parseInt(process.env.EXECUTION_TIMEOUT || '30000', 10), + streamingEnabled: process.env.STREAMING_ENABLED === 'true', - // Debug and logging - debugMode: boolean; - debugStdio: boolean; - debugHttp: boolean; - silentStartup: boolean; + // Transport settings + useStdioTransport: process.env.USE_STDIO_TRANSPORT === 'true', + useHttpTransport: process.env.USE_HTTP_TRANSPORT === 'true', - // CORS settings - corsOrigin: string; + // Debug and logging + debugMode: process.env.DEBUG_MODE === 'true', + debugStdio: process.env.DEBUG_STDIO === 'true', + debugHttp: process.env.DEBUG_HTTP === 'true', + silentStartup: process.env.SILENT_STARTUP === 'true', + + // CORS settings + corsOrigin: process.env.CORS_ORIGIN || '*', + + // Rate limiting + rateLimit: { + maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10), + maxAuthRequests: parseInt(process.env.RATE_LIMIT_MAX_AUTH_REQUESTS || '5', 10), + }, + }; + + // Validate and parse configuration + const validatedConfig = MCPServerConfigSchema.parse(rawConfig); + + // Log validation success + if (!validatedConfig.silentStartup) { + logger.info('Configuration validated successfully'); + if (validatedConfig.debugMode) { + logger.debug('Current configuration:', validatedConfig); + } + } + + return validatedConfig; + } catch (error) { + // Log validation errors + logger.error('Configuration validation failed:', error); + throw new Error('Invalid configuration. Please check your environment variables.'); + } } -export const APP_CONFIG: MCPServerConfig = { - // Server configuration - port: parseInt(process.env.PORT || '3000', 10), - environment: process.env.NODE_ENV || 'development', - - // Execution settings - executionTimeout: parseInt(process.env.EXECUTION_TIMEOUT || '30000', 10), - streamingEnabled: process.env.STREAMING_ENABLED === 'true', - - // Transport settings - useStdioTransport: process.env.USE_STDIO_TRANSPORT === 'true', - useHttpTransport: process.env.USE_HTTP_TRANSPORT === 'true', - - // Debug and logging - debugMode: process.env.DEBUG_MODE === 'true', - debugStdio: process.env.DEBUG_STDIO === 'true', - debugHttp: process.env.DEBUG_HTTP === 'true', - silentStartup: process.env.SILENT_STARTUP === 'true', - - // CORS settings - corsOrigin: process.env.CORS_ORIGIN || '*', -}; - +export const APP_CONFIG = loadConfig(); +export type { MCPServerConfigType }; export default APP_CONFIG; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a40b2cb..6e4d232 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,12 +5,16 @@ import express from 'express'; import cors from 'cors'; +import swaggerUi from 'swagger-ui-express'; import { MCPServer } from './mcp/MCPServer.js'; import { loggingMiddleware, timeoutMiddleware } from './mcp/middleware/index.js'; import { StdioTransport } from './mcp/transports/stdio.transport.js'; import { HttpTransport } from './mcp/transports/http.transport.js'; import { APP_CONFIG } from './config.js'; import { logger } from "./utils/logger.js"; +import { openApiConfig } from './openapi.js'; +import { apiLimiter, authLimiter } from './middleware/rate-limit.middleware.js'; +import { SecurityMiddleware } from './security/enhanced-middleware.js'; // Home Assistant tools import { LightsControlTool } from './tools/homeassistant/lights.tool.js'; @@ -111,10 +115,37 @@ async function main() { if (APP_CONFIG.useHttpTransport) { logger.info('Using HTTP transport on port ' + APP_CONFIG.port); const app = express(); + + // Apply enhanced security middleware + app.use(SecurityMiddleware.applySecurityHeaders); + + // CORS configuration app.use(cors({ - origin: APP_CONFIG.corsOrigin + origin: APP_CONFIG.corsOrigin, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + maxAge: 86400 // 24 hours })); + // Apply rate limiting to all routes + app.use('/api', apiLimiter); + app.use('/auth', authLimiter); + + // Swagger UI setup + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiConfig, { + explorer: true, + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'Home Assistant MCP API Documentation' + })); + + // Health check endpoint + app.get('/health', (req, res) => { + res.json({ + status: 'ok', + version: process.env.npm_package_version || '1.0.0' + }); + }); + const httpTransport = new HttpTransport({ port: APP_CONFIG.port, corsOrigin: APP_CONFIG.corsOrigin, diff --git a/src/middleware/rate-limit.middleware.ts b/src/middleware/rate-limit.middleware.ts new file mode 100644 index 0000000..90462dc --- /dev/null +++ b/src/middleware/rate-limit.middleware.ts @@ -0,0 +1,26 @@ +import rateLimit from 'express-rate-limit'; +import { APP_CONFIG } from '../config.js'; + +// Create a limiter for API endpoints +export const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: APP_CONFIG.rateLimit?.maxRequests || 100, // Limit each IP to 100 requests per windowMs + message: { + status: 'error', + message: 'Too many requests from this IP, please try again later.' + }, + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers +}); + +// Create a stricter limiter for authentication endpoints +export const authLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hour + max: APP_CONFIG.rateLimit?.maxAuthRequests || 5, // Limit each IP to 5 login requests per hour + message: { + status: 'error', + message: 'Too many login attempts from this IP, please try again later.' + }, + standardHeaders: true, + legacyHeaders: false, +}); \ No newline at end of file diff --git a/src/openapi.ts b/src/openapi.ts new file mode 100644 index 0000000..4546ec4 --- /dev/null +++ b/src/openapi.ts @@ -0,0 +1,284 @@ +import type { OpenAPIV3 } from 'openapi-types' + +export const openApiConfig: OpenAPIV3.Document = { + openapi: '3.0.0', + info: { + title: 'Home Assistant MCP API', + description: ` +# Home Assistant Model Context Protocol API + +The Model Context Protocol (MCP) provides a standardized interface for AI tools to interact with Home Assistant. +This API documentation covers all available endpoints and features of the MCP server. + +## Features +- Tool Management +- Real-time Communication +- Health Monitoring +- Rate Limiting +- Authentication +- Server-Sent Events (SSE) +`, + version: '1.0.0', + contact: { + name: 'Home Assistant MCP', + url: 'https://github.com/your-repo/homeassistant-mcp' + }, + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT' + } + }, + servers: [ + { + url: 'http://localhost:3000', + description: 'Local development server' + } + ], + paths: { + '/health': { + get: { + tags: ['Health'], + summary: 'Health check endpoint', + description: 'Returns the current health status and version of the server', + responses: { + '200': { + description: 'Server is healthy', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/HealthCheck' + } + } + } + } + } + } + }, + '/api/tools': { + get: { + tags: ['Tools'], + summary: 'List available tools', + description: 'Returns a list of all registered tools and their capabilities', + security: [{ bearerAuth: [] }], + responses: { + '200': { + description: 'List of available tools', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/Tool' + } + } + } + } + }, + '401': { + description: 'Unauthorized', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + }, + '/api/mcp/execute': { + post: { + tags: ['MCP'], + summary: 'Execute a tool command', + description: 'Executes a command using a registered tool', + security: [{ bearerAuth: [] }], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ExecuteRequest' + } + } + } + }, + responses: { + '200': { + description: 'Command executed successfully', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ExecuteResponse' + } + } + } + }, + '400': { + description: 'Invalid request', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + '401': { + description: 'Unauthorized', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + }, + '/api/mcp/stream': { + get: { + tags: ['SSE'], + summary: 'Stream events', + description: 'Opens a Server-Sent Events connection for real-time updates', + security: [{ bearerAuth: [] }], + responses: { + '200': { + description: 'SSE stream established', + content: { + 'text/event-stream': { + schema: { + type: 'string' + } + } + } + }, + '401': { + description: 'Unauthorized', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + } + }, + components: { + schemas: { + Error: { + type: 'object', + properties: { + code: { + type: 'string', + description: 'Error code' + }, + message: { + type: 'string', + description: 'Error message' + } + }, + required: ['code', 'message'] + }, + HealthCheck: { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['ok', 'error'], + description: 'Current health status' + }, + version: { + type: 'string', + description: 'Server version' + } + }, + required: ['status', 'version'] + }, + Tool: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Tool name' + }, + description: { + type: 'string', + description: 'Tool description' + }, + parameters: { + type: 'object', + description: 'Tool parameters schema' + }, + returns: { + type: 'object', + description: 'Tool return value schema' + } + }, + required: ['name', 'description'] + }, + ExecuteRequest: { + type: 'object', + properties: { + tool: { + type: 'string', + description: 'Name of the tool to execute' + }, + params: { + type: 'object', + description: 'Tool parameters' + } + }, + required: ['tool'] + }, + ExecuteResponse: { + type: 'object', + properties: { + result: { + type: 'object', + description: 'Tool execution result' + }, + error: { + type: 'string', + description: 'Error message if execution failed' + } + } + } + }, + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + description: 'JWT token for authentication' + } + } + }, + tags: [ + { + name: 'Health', + description: 'Health check endpoints for monitoring server status' + }, + { + name: 'MCP', + description: 'Model Context Protocol endpoints for tool execution' + }, + { + name: 'Tools', + description: 'Tool management endpoints for listing and configuring tools' + }, + { + name: 'SSE', + description: 'Server-Sent Events endpoints for real-time updates' + } + ], + security: [ + { + bearerAuth: [] + } + ] +} \ No newline at end of file diff --git a/src/schemas/config.schema.ts b/src/schemas/config.schema.ts new file mode 100644 index 0000000..16f06ad --- /dev/null +++ b/src/schemas/config.schema.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; + +export const RateLimitSchema = z.object({ + maxRequests: z.number().int().min(1).default(100), + maxAuthRequests: z.number().int().min(1).default(5), +}); + +export const MCPServerConfigSchema = z.object({ + // Server configuration + port: z.number().int().min(1).max(65535).default(3000), + environment: z.enum(['development', 'test', 'production']).default('development'), + + // Execution settings + executionTimeout: z.number().int().min(1000).max(300000).default(30000), + streamingEnabled: z.boolean().default(false), + + // Transport settings + useStdioTransport: z.boolean().default(false), + useHttpTransport: z.boolean().default(true), + + // Debug and logging + debugMode: z.boolean().default(false), + debugStdio: z.boolean().default(false), + debugHttp: z.boolean().default(false), + silentStartup: z.boolean().default(false), + + // CORS settings + corsOrigin: z.string().default('*'), + + // Rate limiting + rateLimit: RateLimitSchema.default({ + maxRequests: 100, + maxAuthRequests: 5, + }), + + // Speech features + speech: z.object({ + enabled: z.boolean().default(false), + wakeWord: z.object({ + enabled: z.boolean().default(false), + threshold: z.number().min(0).max(1).default(0.05), + }), + asr: z.object({ + enabled: z.boolean().default(false), + model: z.enum(['base', 'small', 'medium', 'large']).default('base'), + engine: z.enum(['faster_whisper', 'whisper']).default('faster_whisper'), + beamSize: z.number().int().min(1).max(10).default(5), + computeType: z.enum(['float32', 'float16', 'int8']).default('float32'), + language: z.string().default('en'), + }), + audio: z.object({ + minSpeechDuration: z.number().min(0.1).max(10).default(1.0), + silenceDuration: z.number().min(0.1).max(5).default(0.5), + sampleRate: z.number().int().min(8000).max(48000).default(16000), + channels: z.number().int().min(1).max(2).default(1), + chunkSize: z.number().int().min(256).max(4096).default(1024), + }), + }).default({ + enabled: false, + wakeWord: { enabled: false, threshold: 0.05 }, + asr: { + enabled: false, + model: 'base', + engine: 'faster_whisper', + beamSize: 5, + computeType: 'float32', + language: 'en', + }, + audio: { + minSpeechDuration: 1.0, + silenceDuration: 0.5, + sampleRate: 16000, + channels: 1, + chunkSize: 1024, + }, + }), +}); + +export type MCPServerConfigType = z.infer; \ No newline at end of file diff --git a/src/security/__tests__/enhanced-middleware.test.ts b/src/security/__tests__/enhanced-middleware.test.ts new file mode 100644 index 0000000..3a3abff --- /dev/null +++ b/src/security/__tests__/enhanced-middleware.test.ts @@ -0,0 +1,135 @@ +import { expect, test, describe, beforeEach, afterEach } from 'bun:test'; +import { SecurityMiddleware } from '../enhanced-middleware'; + +describe('Enhanced Security Middleware', () => { + describe('Security Headers', () => { + test('applies security headers correctly', () => { + const request = new Request('http://localhost'); + SecurityMiddleware.applySecurityHeaders(request); + + expect(request.headers.get('content-security-policy')).toBeDefined(); + expect(request.headers.get('x-frame-options')).toBe('DENY'); + expect(request.headers.get('strict-transport-security')).toBeDefined(); + expect(request.headers.get('x-xss-protection')).toBe('1; mode=block'); + }); + }); + + describe('Request Validation', () => { + test('validates request size', async () => { + const largeBody = 'x'.repeat(2 * 1024 * 1024); // 2MB + const request = new Request('http://localhost', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'content-length': largeBody.length.toString() + }, + body: JSON.stringify({ data: largeBody }) + }); + + await expect(SecurityMiddleware.validateRequest(request)).rejects.toThrow('Request body too large'); + }); + + test('validates URL length', async () => { + const longUrl = 'http://localhost/' + 'x'.repeat(3000); + const request = new Request(longUrl); + + await expect(SecurityMiddleware.validateRequest(request)).rejects.toThrow('URL too long'); + }); + + test('validates and sanitizes POST request body', async () => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + name: 'Hello', + age: 25 + }) + }); + + await SecurityMiddleware.validateRequest(request); + const body = await request.json(); + expect(body.name).not.toContain('Hello'; + const sanitized = SecurityMiddleware.sanitizeInput(input); + expect(sanitized).toBe('Hello'); + }); + + test('sanitizes nested object input', () => { + const input = { + name: 'John', + details: { + bio: 'Web Developer' + } + }; + const sanitized = SecurityMiddleware.sanitizeInput(input) as any; + expect(sanitized.name).toBe('John'); + expect(sanitized.details.bio).toBe('Web Developer'); + }); + + test('sanitizes array input', () => { + const input = [ + 'Hello', + 'World' + ]; + const sanitized = SecurityMiddleware.sanitizeInput(input) as string[]; + expect(sanitized[0]).toBe('Hello'); + expect(sanitized[1]).toBe('World'); + }); + }); + + describe('Rate Limiting', () => { + beforeEach(() => { + // Reset rate limit stores before each test + (SecurityMiddleware as any).rateLimitStore.clear(); + (SecurityMiddleware as any).authLimitStore.clear(); + }); + + test('enforces regular rate limits', () => { + const ip = '127.0.0.1'; + + // Should allow up to 100 requests + for (let i = 0; i < 100; i++) { + expect(() => SecurityMiddleware.checkRateLimit(ip, false)).not.toThrow(); + } + + // Should block the 101st request + expect(() => SecurityMiddleware.checkRateLimit(ip, false)).toThrow('Too many requests'); + }); + + test('enforces stricter auth rate limits', () => { + const ip = '127.0.0.1'; + + // Should allow up to 5 auth requests + for (let i = 0; i < 5; i++) { + expect(() => SecurityMiddleware.checkRateLimit(ip, true)).not.toThrow(); + } + + // Should block the 6th auth request + expect(() => SecurityMiddleware.checkRateLimit(ip, true)).toThrow('Too many authentication requests'); + }); + + test('resets rate limits after window expires', async () => { + const ip = '127.0.0.1'; + + // Make max requests + for (let i = 0; i < 100; i++) { + SecurityMiddleware.checkRateLimit(ip, false); + } + + // Wait for rate limit window to expire + const store = (SecurityMiddleware as any).rateLimitStore.get(ip); + store.resetTime = Date.now() - 1000; // Set reset time to the past + + // Should allow requests again + expect(() => SecurityMiddleware.checkRateLimit(ip, false)).not.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/src/security/enhanced-middleware.ts b/src/security/enhanced-middleware.ts new file mode 100644 index 0000000..a83a456 --- /dev/null +++ b/src/security/enhanced-middleware.ts @@ -0,0 +1,189 @@ +import express, { Request, Response, NextFunction, Router } from 'express'; +import sanitizeHtml from 'sanitize-html'; + +// Custom error type with status code +class SecurityError extends Error { + constructor(public message: string, public statusCode: number) { + super(message); + this.name = 'SecurityError'; + } +} + +// Security configuration +const SECURITY_CONFIG = { + FRAME_OPTIONS: 'DENY', + XSS_PROTECTION: '1; mode=block', + REFERRER_POLICY: 'strict-origin-when-cross-origin', + HSTS_MAX_AGE: 31536000, // 1 year in seconds + CSP: { + 'default-src': ["'self'"], + 'script-src': ["'self'", "'unsafe-inline'"], + 'style-src': ["'self'", "'unsafe-inline'"], + 'img-src': ["'self'", 'data:', 'https:'], + 'font-src': ["'self'"], + 'connect-src': ["'self'"], + 'frame-ancestors': ["'none'"], + 'form-action': ["'self'"] + }, + // Request validation config + MAX_URL_LENGTH: 2048, + MAX_BODY_SIZE: '1mb', + // Rate limiting config + RATE_LIMIT: { + WINDOW_MS: 15 * 60 * 1000, // 15 minutes + MAX_REQUESTS: 50, + MESSAGE: 'Too many requests from this IP, please try again later.' + }, + AUTH_RATE_LIMIT: { + WINDOW_MS: 15 * 60 * 1000, + MAX_REQUESTS: 3, + MESSAGE: 'Too many authentication attempts from this IP, please try again later.' + } +}; + +export class SecurityMiddleware { + private static rateLimitStore = new Map(); + private static authLimitStore = new Map(); + + private static validateRequest(req: Request): void { + // Check URL length + if (req.originalUrl.length > SECURITY_CONFIG.MAX_URL_LENGTH) { + throw new SecurityError('URL too long', 413); + } + + // Check content type for POST requests + if (req.method === 'POST' && req.headers['content-type'] !== 'application/json') { + throw new SecurityError('Content-Type must be application/json', 415); + } + } + + private static sanitizeInput(input: unknown): unknown { + if (typeof input === 'string') { + return sanitizeHtml(input, { + allowedTags: [], + allowedAttributes: {} + }); + } else if (Array.isArray(input)) { + return input.map(item => SecurityMiddleware.sanitizeInput(item)); + } else if (input && typeof input === 'object') { + const sanitized: Record = {}; + for (const [key, value] of Object.entries(input)) { + sanitized[key] = SecurityMiddleware.sanitizeInput(value); + } + return sanitized; + } + return input; + } + + private static applySecurityHeaders(res: Response): void { + // Remove X-Powered-By header + res.removeHeader('X-Powered-By'); + + // Set security headers + res.setHeader('X-Frame-Options', SECURITY_CONFIG.FRAME_OPTIONS); + res.setHeader('X-XSS-Protection', SECURITY_CONFIG.XSS_PROTECTION); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Referrer-Policy', SECURITY_CONFIG.REFERRER_POLICY); + res.setHeader('Strict-Transport-Security', `max-age=${SECURITY_CONFIG.HSTS_MAX_AGE}; includeSubDomains; preload`); + res.setHeader('X-Permitted-Cross-Domain-Policies', 'none'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Resource-Policy', 'same-origin'); + res.setHeader('Origin-Agent-Cluster', '?1'); + + // Set Content-Security-Policy + const cspDirectives = Object.entries(SECURITY_CONFIG.CSP) + .map(([key, values]) => `${key} ${values.join(' ')}`) + .join('; '); + res.setHeader('Content-Security-Policy', cspDirectives); + } + + private static checkRateLimit(req: Request): void { + const ip = req.ip || req.socket.remoteAddress || 'unknown'; + const now = Date.now(); + const isAuth = req.path.startsWith('/auth'); + const store = isAuth ? SecurityMiddleware.authLimitStore : SecurityMiddleware.rateLimitStore; + const config = isAuth ? SECURITY_CONFIG.AUTH_RATE_LIMIT : SECURITY_CONFIG.RATE_LIMIT; + + let record = store.get(ip); + if (!record || now > record.resetTime) { + record = { count: 1, resetTime: now + config.WINDOW_MS }; + } else { + record.count++; + if (record.count > config.MAX_REQUESTS) { + throw new SecurityError( + isAuth ? 'Too many authentication requests' : 'Too many requests', + 429 + ); + } + } + + store.set(ip, record); + } + + /** + * Create Express router with all security middleware + */ + public static createRouter(): Router { + const router = express.Router(); + + // Body parser middleware with size limit + router.use(express.json({ + limit: SECURITY_CONFIG.MAX_BODY_SIZE, + type: 'application/json' + })); + + // Error handler for body-parser errors + router.use((err: Error, _req: Request, res: Response, next: NextFunction) => { + if (err instanceof SyntaxError && 'type' in err && err.type === 'entity.too.large') { + res.status(413).json({ + error: true, + message: 'Request body too large' + }); + } else { + next(err); + } + }); + + // Main security middleware + router.use((req: Request, res: Response, next: NextFunction) => { + try { + // Apply security headers + SecurityMiddleware.applySecurityHeaders(res); + + // Check rate limits + SecurityMiddleware.checkRateLimit(req); + + // Validate request + SecurityMiddleware.validateRequest(req); + + // Sanitize input + if (req.body) { + req.body = SecurityMiddleware.sanitizeInput(req.body); + } + + next(); + } catch (error) { + if (error instanceof SecurityError) { + res.status(error.statusCode).json({ + error: true, + message: error.message + }); + } else { + res.status(500).json({ + error: true, + message: 'Internal server error' + }); + } + } + }); + + return router; + } + + // For testing purposes + public static clearRateLimits(): void { + SecurityMiddleware.rateLimitStore.clear(); + SecurityMiddleware.authLimitStore.clear(); + } +} diff --git a/test-cursor.sh b/test-cursor.sh deleted file mode 100755 index 51b4cc6..0000000 --- a/test-cursor.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Clean up any existing processes first -pkill -f "node.*stdio-server" >/dev/null 2>&1 || true - -# Simulate Cursor environment by setting env variables -export CURSOR_SESSION=test-session -export CURSOR_COMPATIBLE=true -export USE_STDIO_TRANSPORT=true -export LOG_LEVEL=info - -# Run npx with the simulated environment -npx homeassistant-mcp \ No newline at end of file diff --git a/test-jsonrpc.js b/test-jsonrpc.js deleted file mode 100755 index 74b6e0c..0000000 --- a/test-jsonrpc.js +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env node - -/** - * JSON-RPC 2.0 Test Script for MCP Server - * - * This script tests the stdio transport communication with the MCP server - * by sending JSON-RPC 2.0 requests and processing responses. - * - * Usage: - * ./stdio-start.sh | node test-jsonrpc.js - * or - * node test-jsonrpc.js < sample-responses.json - */ - -const { spawn } = require('child_process'); -const readline = require('readline'); - -// Generate a random request ID -const generateId = () => Math.random().toString(36).substring(2, 15); - -// Counter for keeping track of requests/responses -let messageCount = 0; -let pendingRequests = new Map(); - -// Set up readline interface for stdin -const rl = readline.createInterface({ - input: process.stdin, - terminal: false -}); - -// Handle responses from the MCP server -rl.on('line', (line) => { - try { - const response = JSON.parse(line); - messageCount++; - - console.log(`\n[RECEIVED] Response #${messageCount}:`); - console.dir(response, { depth: null, colors: true }); - - // Check if this is a notification - if (!response.id && response.method) { - console.log(`šŸ‘‰ Received notification: ${response.method}`); - return; - } - - // Check if this is a response to a pending request - if (response.id && pendingRequests.has(response.id)) { - const requestTime = pendingRequests.get(response.id); - const responseTime = Date.now(); - console.log(`ā±ļø Response time: ${responseTime - requestTime}ms`); - pendingRequests.delete(response.id); - } - - // Check for error - if (response.error) { - console.log(`āŒ Error [${response.error.code}]: ${response.error.message}`); - } else if (response.result) { - console.log(`āœ… Success`); - } - } catch (error) { - console.error(`Error parsing response: ${error.message}`); - console.error(`Raw response: ${line}`); - } -}); - -// Define test requests -const testRequests = [ - // Test valid request - { - jsonrpc: "2.0", - id: generateId(), - method: "listDevicesTool", - params: { - entity_type: "light" - } - }, - - // Test method not found - { - jsonrpc: "2.0", - id: generateId(), - method: "nonexistentMethod", - params: {} - }, - - // Test invalid params - { - jsonrpc: "2.0", - id: generateId(), - method: "controlTool", - params: { - // Missing required parameters - } - }, - - // Test notification (no response expected) - { - jsonrpc: "2.0", - method: "ping", - params: { - timestamp: Date.now() - } - }, - - // Test malformed request (missing jsonrpc version) - { - id: generateId(), - method: "listDevicesTool", - params: {} - } -]; - -// Send requests with delay between each -let requestIndex = 0; - -function sendNextRequest() { - if (requestIndex >= testRequests.length) { - console.log('\n✨ All test requests sent!'); - return; - } - - const request = testRequests[requestIndex++]; - console.log(`\n[SENDING] Request #${requestIndex}:`); - console.dir(request, { depth: null, colors: true }); - - // Store the request time for calculating response time - if (request.id) { - pendingRequests.set(request.id, Date.now()); - } - - // Send the request to the MCP server - process.stdout.write(JSON.stringify(request) + '\n'); - - // Schedule the next request - setTimeout(sendNextRequest, 1000); -} - -// Start sending test requests after a delay to allow server initialization -console.log('šŸš€ Starting JSON-RPC 2.0 test...'); -setTimeout(sendNextRequest, 2000); - -// Handle Ctrl+C -process.on('SIGINT', () => { - console.log('\nšŸ‘‹ Test script terminated'); - process.exit(0); -}); \ No newline at end of file diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..012def9 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,33 @@ +import { beforeAll, afterAll } from 'bun:test'; + +// Mock environment variables for testing +const TEST_ENV = { + NODE_ENV: 'test', + PORT: '3000', + EXECUTION_TIMEOUT: '30000', + STREAMING_ENABLED: 'false', + USE_STDIO_TRANSPORT: 'false', + USE_HTTP_TRANSPORT: 'true', + DEBUG_MODE: 'false', + DEBUG_STDIO: 'false', + DEBUG_HTTP: 'false', + SILENT_STARTUP: 'false', + CORS_ORIGIN: '*', + RATE_LIMIT_MAX_REQUESTS: '100', + RATE_LIMIT_MAX_AUTH_REQUESTS: '5' +}; + +beforeAll(() => { + // Store original environment + process.env = { + ...process.env, + ...TEST_ENV + }; +}); + +afterAll(() => { + // Clean up test environment + Object.keys(TEST_ENV).forEach(key => { + delete process.env[key]; + }); +}); \ No newline at end of file diff --git a/webpack.config.cjs b/webpack.config.cjs deleted file mode 100644 index 5086592..0000000 --- a/webpack.config.cjs +++ /dev/null @@ -1,48 +0,0 @@ -const path = require('path'); -const TerserPlugin = require('terser-webpack-plugin'); - -module.exports = { - mode: 'production', - target: 'node', - entry: './src/utils/stdio-transport.ts', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'stdio-transport.js', - library: { - type: 'commonjs2' - } - }, - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - ], - }, - resolve: { - extensions: ['.tsx', '.ts', '.js'], - extensionAlias: { - '.js': ['.js', '.ts'], - '.cjs': ['.cjs', '.cts'], - '.mjs': ['.mjs', '.mts'] - } - }, - optimization: { - minimize: true, - minimizer: [new TerserPlugin({ - terserOptions: { - format: { - comments: false, - }, - }, - extractComments: false, - })], - }, - externals: { - // Mark node modules as external to reduce bundle size - 'express': 'commonjs express', - 'winston': 'commonjs winston' - } -}; \ No newline at end of file