From 3e4e4326925d89b53dbe1eaa8abb8b61bcf9eea0 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 12:10:10 +0200 Subject: [PATCH 01/21] build(dependencies): update package-lock.json via npm audit fix --- package-lock.json | 832 +++++++++++++++++++++++++--------------------- 1 file changed, 450 insertions(+), 382 deletions(-) diff --git a/package-lock.json b/package-lock.json index 791d36b..79d28df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { - "name": "aem-mcp-server", - "version": "2.0.0", + "name": "@netcentric/aem-mcp-server", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "aem-mcp-server", - "version": "2.0.0", + "name": "@netcentric/aem-mcp-server", + "version": "1.0.0", + "license": "AGPL-3.0-only", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.3", "cors": "^2.8.5", @@ -468,45 +469,56 @@ "node": ">=18" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", - "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", + }, "peerDependencies": { - "zod": "^3.24.1" + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/@types/body-parser": { @@ -645,40 +657,37 @@ "node": ">= 0.6" } }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/accepts/node_modules/mime-types": { + "node_modules/ajv-formats": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "ajv": "^8.0.0" }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "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" + "peerDependencies": { + "ajv": "^8.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/ansi-regex": { @@ -706,23 +715,27 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { @@ -737,6 +750,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -844,9 +858,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -872,6 +886,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -904,6 +919,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -912,14 +928,16 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -1011,18 +1029,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -1053,9 +1072,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, "engines": { "node": ">= 16" }, @@ -1066,36 +1089,27 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/finalhandler": { "version": "2.1.0", @@ -1135,6 +1149,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1164,6 +1179,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -1187,6 +1203,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -1199,6 +1216,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1210,6 +1228,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1218,9 +1237,10 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1228,19 +1248,33 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/https": { @@ -1250,15 +1284,19 @@ "dev": true }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/image-size": { @@ -1287,6 +1325,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1311,10 +1358,26 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" }, "node_modules/jszip": { "version": "3.10.1", @@ -1341,6 +1404,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1366,6 +1430,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1442,6 +1531,16 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -1495,18 +1594,10 @@ "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -1536,18 +1627,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/readable-stream": { @@ -1571,6 +1662,15 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1586,14 +1686,6 @@ "node": ">= 18" } }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "engines": { - "node": ">=16" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1641,27 +1733,6 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -1708,14 +1779,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -1727,13 +1798,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -1780,9 +1851,10 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1843,38 +1915,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" + "node": ">=18" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { @@ -1906,14 +1974,6 @@ "node": ">= 0.8" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -1998,6 +2058,24 @@ "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } }, "dependencies": { @@ -2183,36 +2261,34 @@ "dev": true, "optional": true }, + "@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "requires": {} + }, "@modelcontextprotocol/sdk": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", - "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "requires": { - "ajv": "^6.12.6", + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "dependencies": { - "zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" - }, - "zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "requires": {} - } + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" } }, "@types/body-parser": { @@ -2342,32 +2418,25 @@ "requires": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" - }, - "dependencies": { - "mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" - }, - "mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "requires": { - "mime-db": "^1.54.0" - } - } } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "ajv": "^8.0.0" } }, "ansi-regex": { @@ -2381,19 +2450,19 @@ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" }, "body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "requires": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" } }, "bytes": { @@ -2478,9 +2547,9 @@ } }, "debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { "ms": "^2.1.3" } @@ -2526,9 +2595,9 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "requires": { "es-errors": "^1.3.0" } @@ -2596,17 +2665,18 @@ "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==" }, "express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "requires": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -2627,38 +2697,25 @@ "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" - }, - "dependencies": { - "mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" - }, - "mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "requires": { - "mime-db": "^1.54.0" - } - } } }, "express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "requires": {} + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "requires": { + "ip-address": "^10.2.0" + } }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==" }, "finalhandler": { "version": "2.1.0", @@ -2735,23 +2792,28 @@ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "requires": { "function-bind": "^1.1.2" } }, + "hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==" + }, "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" } }, "https": { @@ -2761,9 +2823,9 @@ "dev": true }, "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -2788,6 +2850,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2809,10 +2876,20 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==" + }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==" }, "jszip": { "version": "3.10.1", @@ -2850,6 +2927,19 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" }, + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "requires": { + "mime-db": "^1.54.0" + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2902,6 +2992,11 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, + "path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==" + }, "pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -2951,15 +3046,10 @@ "ipaddr.js": "1.9.1" } }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" - }, "qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "requires": { "side-channel": "^1.1.0" } @@ -2979,14 +3069,14 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" } }, "readable-stream": { @@ -3012,6 +3102,11 @@ } } }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -3022,13 +3117,6 @@ "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" - }, - "dependencies": { - "path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" - } } }, "safe-buffer": { @@ -3057,21 +3145,6 @@ "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" - }, - "dependencies": { - "mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" - }, - "mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "requires": { - "mime-db": "^1.54.0" - } - } } }, "serve-static": { @@ -3110,24 +3183,24 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "requires": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "requires": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" } }, "side-channel-map": { @@ -3154,9 +3227,9 @@ } }, "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" }, "string_decoder": { "version": "1.1.1", @@ -3199,27 +3272,19 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "requires": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "dependencies": { - "mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" - }, - "mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "requires": { - "mime-db": "^1.54.0" - } + "content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" } } }, @@ -3240,14 +3305,6 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3304,6 +3361,17 @@ "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==" + }, + "zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==" + }, + "zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "requires": {} } } } From 9038ccff2030fbc615107e8c6e452db57599575b Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 12:11:02 +0200 Subject: [PATCH 02/21] docs: add CLAUDE.md for project guidance and command reference --- CLAUDE.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cd9b1cd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,70 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```sh +npm run build # build:types (tsc, .d.ts only) + build:ts (esbuild bundle to dist/) +npm run build:ts # esbuild only — fastest iteration when types are unchanged +npm run build:types # tsc --emitDeclarationOnly (also doubles as the typecheck) +npm start # node dist/cli.js with MCP_LOGGER=true +npm run test:dev # node ./dist/cli.js (no logger) +npm run test:npm # pack + global install + smoke-run on port 5502 +``` + +No test runner, linter, or formatter is configured. `npm run build:types` is the only available typecheck. + +Single-process run with custom params (mirrors the `aem-mcp` bin): +```sh +node dist/cli.js -H=https://author.example.com -u=user -p=pass # Basic auth +node dist/cli.js -H=https://author.example.com -i= -s= # AEMaaCS OAuth S2S +MCP_USERNAME=foo MCP_PASSWORD=bar node dist/cli.js # gate /mcp with HTTP Basic +``` + +`MCP_LOGGER=true` enables logging. Without it, `LOGGER` is a no-op — this is intentional (stdout must stay clean for MCP stdio clients; see `src/utils/logger.ts`). + +## Architecture + +The server is a Model Context Protocol (MCP) gateway that translates JSON-RPC tool calls into AEM HTTP operations (Sling/QueryBuilder/JCR). Request flow: + +``` +MCP client ──HTTP POST /mcp──▶ Express (server/app.server.ts) + │ + ▼ + server-handler.ts ──per-session──▶ StreamableHTTPServerTransport + │ │ + ▼ ▼ + mcp.server.ts (MCP SDK Server) transports map (mcp.transports.ts) + │ CallToolRequest + ▼ + mcp.aem-handler.ts (switch on method name) + │ + ▼ + aem.connector.ts ──▶ aem.fetch.ts ──HTTP──▶ AEM +``` + +Key seams: + +- **`src/cli.ts`** — yargs entry. Parses `--host/--user/--pass/--id/--secret/--mcpPort` into `CliParams` (see `src/types.ts`) and calls `startServer`. +- **`src/server/app.server.ts`** — Express app. Exposes `GET /` (info), `GET /health`, `POST /mcp`. `GET/DELETE /mcp` return 405. CORS is wide open (`origin: '*'`). Optional Basic auth middleware exists in `app.auth.ts` but is **currently commented out** at the `useBasicAuth(app)` line — re-enable by uncommenting both the import and call. The middleware only activates if both `MCP_USERNAME` and `MCP_PASSWORD` env vars are set. +- **`src/mcp/mcp.server-handler.ts`** — Per-request session routing. New `StreamableHTTPServerTransport` is created on `initialize`; subsequent requests reuse the transport by `mcp-session-id` header. A new `MCPRequestHandler` (and therefore a new `AEMConnector`) is constructed per session — there's no global AEM client. +- **`src/mcp/mcp.server.ts`** — Wires MCP SDK handlers: `ListTools` returns the static `tools` array; `CallTool` dispatches via `MCPRequestHandler.handleRequest`. Has special-case handling for `OAUTH_REQUIRED` errors that surface an `authUrl` back to the client. +- **`src/mcp/mcp.tools.ts`** — The single source of truth for the MCP tool surface (~46 tools, JSON-Schema input definitions). When adding a tool you must update **both** this file (schema) and `mcp.aem-handler.ts` (the switch statement that routes `method` → `AEMConnector.xxx`). They are not auto-derived from each other. +- **`src/aem/aem.connector.ts`** — The big file (~3700 lines). All AEM domain logic — pages, components, assets, workflows, replication, search. Every public method wraps its body in `safeExecute` (from `aem.errors.ts`) and returns either `createSuccessResponse(...)` or throws an `AEMOperationError` with a code from `AEM_ERROR_CODES`. Path inputs are validated against `isValidContentPath` (must start with `/content`, `/content/dam`, `/conf`, or `/content/experience-fragments` — see `aem.config.ts`). +- **`src/aem/aem.fetch.ts`** — Thin fetch wrapper. Two auth modes selected by `loadConfig` in the connector: Basic (`user`/`pass`) sends `Authorization: Basic `; OAuth S2S (`id`/`secret`) calls Adobe IMS (`aem.auth.ts`) to mint a Bearer token, caches it with `expires_in - 60s` headroom, and retries once on 401 by refreshing the token. Form posts use `URLSearchParams` and set `Content-Type: application/x-www-form-urlencoded` (required for SlingPostServlet). + +## Component update conventions + +Two behaviors are non-obvious and worth knowing before touching component code paths: + +1. **Dialog-driven property validation.** `updateComponent` and `addComponent` fetch the component's `cq:dialog` (and walk `sling:resourceSuperType` recursively) to build a `fieldDefinitions` map, then validate provided properties against it — select fields check against the dialog's option values, checkboxes against boolean coercion, numberfields against `Number(...)`. See `getComponentDefinition` and `validateComponentProperties` in `aem.connector.ts`. +2. **`cq:template` materialization.** When `addComponent` finds a `cq:template` node under the component definition (e.g., column controls), it merges template properties into the new node and creates the template's child nodes via separate POSTs. See `getComponentTemplate` and `applyTemplateChildNodes`. + +## Important constraints + +- **ESM-only** (`"type": "module"` in `package.json`). Internal imports use `.js` suffixes even in `.ts` files — this is required, not a mistake. +- **AGPL-3.0** license — avoid copying code from sources incompatible with this license. +- Releases are driven by **semantic-release** parsing Angular-style commits (`feat:`, `fix:`, `BREAKING CHANGE:` footer); see `docs/CONTRIBUTING.md`. Do not bump versions manually. +- `executeJCRQuery` is **not** a JCR SQL2 executor despite the name — it's a thin wrapper around QueryBuilder fulltext (see the docstring on the method). Don't change its behavior to actually run SQL2 without coordinating with callers. +- The `src/test/` directory contains certificate-generation and mTLS test-server scripts for in-progress cert-auth work, not unit tests. From a9a1a591ae275e2cad0a2070fc19a15104c3a371 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:01:03 +0200 Subject: [PATCH 03/21] fix: leak #1 - sanitize URLs and reject embedded credentials in host --- src/aem/aem.fetch.ts | 8 ++++++-- src/cli.ts | 7 +++++++ src/utils/sanitize.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/utils/sanitize.ts diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index dff3fff..913598d 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -1,5 +1,6 @@ import { getAccessToken } from './aem.auth.js'; import { LOGGER } from '../utils/logger.js'; +import { sanitizeUrl, hasUrlCredentials } from '../utils/sanitize.js'; export type AEMBasicAuth = { username: string; @@ -39,6 +40,9 @@ export class AEMFetch { private tokenExpiry: number; constructor(config: AEMFetchConfig) { + if (hasUrlCredentials(config.host)) { + throw new Error('AEM host URL must not contain embedded credentials. Pass them via username/password (Basic) or clientId/clientSecret (OAuth).'); + } this.config = config; this.fetch = null; this.token = ''; @@ -176,7 +180,7 @@ export class AEMFetch { try { response = await this.fetch(url, options); if (response.status === 401) { - LOGGER.warn(`AEM request to ${url} returned 401 Unauthorized. Attempting to refresh token...`); + LOGGER.warn(`AEM request to ${sanitizeUrl(url)} returned 401 Unauthorized. Attempting to refresh token...`); await this.refreshAuthToken(); response = await this.fetch(url, options); } @@ -184,7 +188,7 @@ export class AEMFetch { if (response.status >= 300 && response.status < 400 && !response.ok) { const location = response.headers.get('Location'); if (location) { - LOGGER.warn(`Redirect detected (${response.status}) from ${url} to ${location}`); + LOGGER.warn(`Redirect detected (${response.status}) from ${sanitizeUrl(url)} to ${sanitizeUrl(location)}`); // Follow the redirect manually if fetch didn't const redirectUrl = location.startsWith('http') ? location : `${this.config.host}${location}`; response = await this.fetch(redirectUrl, { ...options, redirect: 'follow' }); diff --git a/src/cli.ts b/src/cli.ts index f6c9ccf..872767f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,6 +4,7 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { startServer } from './index.js'; import { CliParams } from './types'; +import { hasUrlCredentials } from './utils/sanitize.js'; type CliArgs = CliParams & { help?: boolean; @@ -26,4 +27,10 @@ if (argv.help) { } const { host, user, pass, mcpPort, id, secret } = argv; + +if (host && hasUrlCredentials(host)) { + console.error('Error: --host (-H) must not contain embedded credentials. Pass them via -u/-p (Basic) or -i/-s (OAuth) instead.'); + process.exit(1); +} + startServer({ host, user, pass, mcpPort, id, secret }); diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts new file mode 100644 index 0000000..2ab709d --- /dev/null +++ b/src/utils/sanitize.ts @@ -0,0 +1,30 @@ +/** + * Strip userinfo (`user:pass@`) from a URL before it is logged, interpolated + * into an error message, or otherwise rendered to a place the credential + * should not reach. Returns `''` if the input is not a valid + * URL — never echo the raw input back, since the whole point is to not leak. + */ +export function sanitizeUrl(url: string): string { + try { + const u = new URL(url); + u.username = ''; + u.password = ''; + return u.toString(); + } catch { + return ''; + } +} + +/** + * True when the given URL string carries embedded credentials (`user:pass@`). + * Used at config-load time to reject misconfigured hosts before any fetch is + * attempted. + */ +export function hasUrlCredentials(url: string): boolean { + try { + const u = new URL(url); + return !!u.username || !!u.password; + } catch { + return false; + } +} From f586da6dda5a32cb003d2b0cee6ff82a13adbdfa Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:07:54 +0200 Subject: [PATCH 04/21] fix: leak #2 - redact CLI params before logging session init --- src/mcp/mcp.server-handler.ts | 3 ++- src/utils/sanitize.ts | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/mcp/mcp.server-handler.ts b/src/mcp/mcp.server-handler.ts index 6a8d4bb..a60de35 100644 --- a/src/mcp/mcp.server-handler.ts +++ b/src/mcp/mcp.server-handler.ts @@ -6,6 +6,7 @@ import { transports } from './mcp.transports.js'; import { createMCPServer } from './mcp.server.js'; import { CliParams } from '../types.js'; import { LOGGER } from '../utils/logger.js'; +import { redactCliParams } from '../utils/sanitize.js'; export const handleRequest = async (req: Request, res: Response, cliParams: CliParams) => { LOGGER.log('1.Received MCP request:', req.body); @@ -54,7 +55,7 @@ export const handleRequest = async (req: Request, res: Response, cliParams: CliP }); // Connect the transport to the MCP server BEFORE handling the request - LOGGER.log('Connecting to MCP server with CLI params:', cliParams); + LOGGER.log('Connecting to MCP server with CLI params:', redactCliParams(cliParams)); const server = createMCPServer(cliParams); await server.connect(transport); await transport.handleRequest(req, res, req.body); diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts index 2ab709d..1a85d70 100644 --- a/src/utils/sanitize.ts +++ b/src/utils/sanitize.ts @@ -28,3 +28,43 @@ export function hasUrlCredentials(url: string): boolean { return false; } } + +export type RedactedCliParams = { + host: string; + authMode: 'basic' | 'oauth' | 'none'; + mcpPort?: number; + hasUser: boolean; + hasPass: boolean; + hasId: boolean; + hasSecret: boolean; +}; + +/** + * Render `CliParams` in a form safe to log. Strips userinfo from `host`, + * collapses credential presence to booleans, and surfaces the auth mode + * without echoing any secret value. + */ +export function redactCliParams(p: { + host?: string; + user?: string; + pass?: string; + id?: string; + secret?: string; + mcpPort?: number; +}): RedactedCliParams { + const hasUser = !!p.user; + const hasPass = !!p.pass; + const hasId = !!p.id; + const hasSecret = !!p.secret; + const authMode: RedactedCliParams['authMode'] = + hasId && hasSecret ? 'oauth' : hasUser && hasPass ? 'basic' : 'none'; + return { + host: p.host ? sanitizeUrl(p.host) : '', + authMode, + mcpPort: p.mcpPort, + hasUser, + hasPass, + hasId, + hasSecret, + }; +} From f6f12dbf9e2a8047f1566701e75e5b29f90f288c Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:11:07 +0200 Subject: [PATCH 05/21] fix: leak #1b - sanitize URLs in error messages --- src/aem/aem.errors.ts | 8 +++++--- src/utils/sanitize.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/aem/aem.errors.ts b/src/aem/aem.errors.ts index f75db1e..0131858 100644 --- a/src/aem/aem.errors.ts +++ b/src/aem/aem.errors.ts @@ -1,4 +1,5 @@ import { LOGGER } from '../utils/logger.js'; +import { sanitizeErrorMessage } from '../utils/sanitize.js'; export interface AEMErrorDetails { [key: string]: any; @@ -93,11 +94,12 @@ export function handleAEMHttpError(error: any, operation: string): AEMOperationE return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, `HTTP ${status}: ${errorMsg}`, { status, data, operation }); } } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { - return createAEMError(AEM_ERROR_CODES.CONNECTION_FAILED, 'Cannot connect to AEM instance. Check host and network.', { originalError: error.message }, true, 5000); + return createAEMError(AEM_ERROR_CODES.CONNECTION_FAILED, 'Cannot connect to AEM instance. Check host and network.', { originalError: sanitizeErrorMessage(error.message) }, true, 5000); } else if (error.code === 'ETIMEDOUT') { - return createAEMError(AEM_ERROR_CODES.TIMEOUT, 'Request to AEM timed out.', { originalError: error.message }, true, 10000); + return createAEMError(AEM_ERROR_CODES.TIMEOUT, 'Request to AEM timed out.', { originalError: sanitizeErrorMessage(error.message) }, true, 10000); } else { - return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, `Unexpected error during ${operation}: ${error.message}`, { originalError: error.message }); + const safeMsg = sanitizeErrorMessage(error.message); + return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, `Unexpected error during ${operation}: ${safeMsg}`, { originalError: safeMsg }); } } diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts index 1a85d70..a70b245 100644 --- a/src/utils/sanitize.ts +++ b/src/utils/sanitize.ts @@ -15,6 +15,18 @@ export function sanitizeUrl(url: string): string { } } +/** + * Strip userinfo from every URL-like substring embedded in an error message. + * Node fetch's TypeError ("Request cannot be constructed from a URL that + * includes credentials: http://user:pass@host/...") is the motivating case, + * but any error whose `.message` interpolates a URL benefits from this. + */ +export function sanitizeErrorMessage(msg: string): string { + if (!msg) return msg; + // Match `scheme://userinfo@` and drop the userinfo segment. + return msg.replace(/(\bhttps?:\/\/)[^\s@/]+@/gi, '$1'); +} + /** * True when the given URL string carries embedded credentials (`user:pass@`). * Used at config-load time to reject misconfigured hosts before any fetch is From 18d4780970c1367dc5b05b12d279335d2a4fa524 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:14:37 +0200 Subject: [PATCH 06/21] fix: leak #3 - strip Authorization on cross-origin redirect --- src/aem/aem.fetch.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index 913598d..0278c07 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -33,6 +33,19 @@ export type AEMFetchConfig = { type FetchInstance = (input: RequestInfo, init?: RequestInit) => Promise; +/** + * Same-origin check for redirect handling: a redirect is treated as same-origin + * only when scheme + host + port all match. Unparseable inputs are treated as + * cross-origin (fail safer — strip credentials rather than risk leaking them). + */ +function isSameOrigin(from: string, to: string): boolean { + try { + return new URL(from).origin === new URL(to).origin; + } catch { + return false; + } +} + export class AEMFetch { private fetch: FetchInstance | null; private readonly config: AEMFetchConfig; @@ -188,10 +201,21 @@ export class AEMFetch { if (response.status >= 300 && response.status < 400 && !response.ok) { const location = response.headers.get('Location'); if (location) { - LOGGER.warn(`Redirect detected (${response.status}) from ${sanitizeUrl(url)} to ${sanitizeUrl(location)}`); - // Follow the redirect manually if fetch didn't const redirectUrl = location.startsWith('http') ? location : `${this.config.host}${location}`; - response = await this.fetch(redirectUrl, { ...options, redirect: 'follow' }); + const sameOrigin = isSameOrigin(url, redirectUrl); + LOGGER.warn( + `Redirect detected (${response.status}) from ${sanitizeUrl(url)} to ${sanitizeUrl(redirectUrl)}` + + (sameOrigin ? '' : ' (cross-origin: Authorization stripped)') + ); + if (sameOrigin) { + response = await this.fetch(redirectUrl, { ...options, redirect: 'follow' }); + } else { + // Cross-origin: do NOT use this.fetch (it would re-inject Authorization via + // getFetchInstance). Use bare fetch and explicitly drop any Authorization header. + const safeHeaders = new Headers(options.headers || {}); + safeHeaders.delete('Authorization'); + response = await fetch(redirectUrl, { ...options, headers: safeHeaders, redirect: 'follow' }); + } } } if (!response.ok) { From 11e4964fd0c44b8308360e0015db51b745753083 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:25:24 +0200 Subject: [PATCH 07/21] fix: leak #4 - summarize AEM response bodies in error details --- src/aem/aem.errors.ts | 30 +++++++------------------ src/utils/sanitize.ts | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/aem/aem.errors.ts b/src/aem/aem.errors.ts index 0131858..a5fede1 100644 --- a/src/aem/aem.errors.ts +++ b/src/aem/aem.errors.ts @@ -1,5 +1,5 @@ import { LOGGER } from '../utils/logger.js'; -import { sanitizeErrorMessage } from '../utils/sanitize.js'; +import { sanitizeErrorMessage, summarizeAemBody } from '../utils/sanitize.js'; export interface AEMErrorDetails { [key: string]: any; @@ -61,37 +61,23 @@ export function createAEMError( export function handleAEMHttpError(error: any, operation: string): AEMOperationError { if (error.response) { const status = error.response.status; - const data = error.response.data; + const summary = summarizeAemBody(error.response.data); switch (status) { case 401: - return createAEMError(AEM_ERROR_CODES.AUTHENTICATION_FAILED, 'Authentication failed. Check AEM credentials.', { status, data }); + return createAEMError(AEM_ERROR_CODES.AUTHENTICATION_FAILED, 'Authentication failed. Check AEM credentials.', { status, summary }); case 403: - return createAEMError(AEM_ERROR_CODES.INSUFFICIENT_PERMISSIONS, 'Insufficient permissions for this operation.', { status, data, operation }); + return createAEMError(AEM_ERROR_CODES.INSUFFICIENT_PERMISSIONS, 'Insufficient permissions for this operation.', { status, summary, operation }); case 404: - return createAEMError(AEM_ERROR_CODES.RESOURCE_NOT_FOUND, 'Resource not found in AEM.', { status, data, operation }); + return createAEMError(AEM_ERROR_CODES.RESOURCE_NOT_FOUND, 'Resource not found in AEM.', { status, summary, operation }); case 429: const retryAfter = error.response.headers['retry-after']; - return createAEMError(AEM_ERROR_CODES.RATE_LIMITED, 'Rate limit exceeded. Please try again later.', { status, data }, true, retryAfter ? parseInt(retryAfter) * 1000 : 60000); + return createAEMError(AEM_ERROR_CODES.RATE_LIMITED, 'Rate limit exceeded. Please try again later.', { status, summary }, true, retryAfter ? parseInt(retryAfter) * 1000 : 60000); case 500: case 502: case 503: - return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, 'AEM system error. Please try again later.', { status, data }, true, 30000); + return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, 'AEM system error. Please try again later.', { status, summary }, true, 30000); default: - // Handle both string and object error data - let errorMsg = 'Unknown error'; - if (typeof data === 'string' && data.trim().length > 0) { - try { - const parsed = JSON.parse(data); - errorMsg = parsed.message || JSON.stringify(parsed); - } catch { - errorMsg = data; - } - } else if (data && typeof data === 'object' && data.message) { - errorMsg = data.message; - } else if (data && typeof data === 'object') { - errorMsg = JSON.stringify(data); - } - return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, `HTTP ${status}: ${errorMsg}`, { status, data, operation }); + return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, `HTTP ${status}: ${summary ?? 'Unknown error'}`, { status, summary, operation }); } } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { return createAEMError(AEM_ERROR_CODES.CONNECTION_FAILED, 'Cannot connect to AEM instance. Check host and network.', { originalError: sanitizeErrorMessage(error.message) }, true, 5000); diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts index a70b245..4911600 100644 --- a/src/utils/sanitize.ts +++ b/src/utils/sanitize.ts @@ -41,6 +41,58 @@ export function hasUrlCredentials(url: string): boolean { } } +/** + * Reduce an AEM error response body to a short, safe summary suitable for + * inclusion in an `AEMOperationError.details` field that may be serialized + * back to an MCP client. HTML pages (Sling error pages) collapse to a + * placeholder; JSON bodies surface only the `message`/`error` field; plain + * text is truncated and stripped of control characters. + * + * Returns `null` when there is nothing usable. + */ +export function summarizeAemBody(data: unknown, maxLen = 200): string | null { + if (data == null) return null; + + if (typeof data === 'object') { + const obj = data as Record; + if (typeof obj.message === 'string') return truncate(obj.message, maxLen); + if (typeof obj.error === 'string') return truncate(obj.error, maxLen); + try { + return truncate(JSON.stringify(obj), maxLen); + } catch { + return null; + } + } + + if (typeof data !== 'string') return null; + const str = data.trim(); + if (!str) return null; + + if (/^<(?:!doctype|html|\?xml)/i.test(str)) { + return ''; + } + + try { + const parsed = JSON.parse(str); + if (parsed && typeof parsed === 'object') { + const msg = (parsed as Record).message; + const err = (parsed as Record).error; + if (typeof msg === 'string') return truncate(msg, maxLen); + if (typeof err === 'string') return truncate(err, maxLen); + } + } catch { + /* not JSON — fall through to plain-text handling */ + } + + // eslint-disable-next-line no-control-regex + const cleaned = str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); + return truncate(cleaned, maxLen); +} + +function truncate(s: string, maxLen: number): string { + return s.length > maxLen ? s.slice(0, maxLen) + '…' : s; +} + export type RedactedCliParams = { host: string; authMode: 'basic' | 'oauth' | 'none'; From fe6ad4e6b16242c555b343f0d803f9c242f65ccd Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:39:08 +0200 Subject: [PATCH 08/21] fix: leak #5 - deduplicate concurrent OAuth token mints --- src/aem/aem.fetch.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index 0278c07..e6b2ddf 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -51,6 +51,7 @@ export class AEMFetch { private readonly config: AEMFetchConfig; private token: string; private tokenExpiry: number; + private inflightToken: Promise | null; constructor(config: AEMFetchConfig) { if (hasUrlCredentials(config.host)) { @@ -60,6 +61,7 @@ export class AEMFetch { this.fetch = null; this.token = ''; this.tokenExpiry = 0; + this.inflightToken = null; } /** @@ -113,10 +115,23 @@ export class AEMFetch { if (this.token && now < this.tokenExpiry) { return this.token; } - const token = await getAccessToken(config.clientId, config.clientSecret, config.scope); - this.token = token.access_token; - this.tokenExpiry = now + (token.expires_in - 60) * 1000; - return this.token; + // Dedup concurrent mints: if another caller has already kicked off the + // IMS request, ride on its promise instead of issuing a parallel mint. + // Without this, N concurrent post-expiry callers trigger N IMS calls. + if (this.inflightToken) { + return this.inflightToken; + } + this.inflightToken = (async () => { + try { + const token = await getAccessToken(config.clientId, config.clientSecret, config.scope); + this.token = token.access_token; + this.tokenExpiry = now + (token.expires_in - 60) * 1000; + return this.token; + } finally { + this.inflightToken = null; + } + })(); + return this.inflightToken; } // Basic Authentication (username/password) From 77427a68952cf56bd9f37bf0f886550b2f50ddf0 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:51:07 +0200 Subject: [PATCH 09/21] fix: leak #6 - use fresh AbortSignal on 401 retry --- src/aem/aem.fetch.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index e6b2ddf..95d6e29 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -205,12 +205,18 @@ export class AEMFetch { // Explicitly set redirect to follow (default behavior, but making it explicit) options.redirect = options.redirect || 'follow'; let response: Response; + let retryTimeoutId: NodeJS.Timeout | undefined; try { response = await this.fetch(url, options); if (response.status === 401) { LOGGER.warn(`AEM request to ${sanitizeUrl(url)} returned 401 Unauthorized. Attempting to refresh token...`); await this.refreshAuthToken(); - response = await this.fetch(url, options); + // Fresh timeout window for the retry: the original signal may already be aborted + // if the refresh took longer than the original `timeout`. + const retry = this.getTimeoutOptions(timeout); + retryTimeoutId = retry.timeoutId; + const retryOptions = timeout ? { ...options, signal: retry.signal } : options; + response = await this.fetch(url, retryOptions); } // Handle redirect status codes (300-399) - fetch should follow automatically, but log if it doesn't if (response.status >= 300 && response.status < 400 && !response.ok) { @@ -302,6 +308,7 @@ export class AEMFetch { } } finally { if (timeoutId) clearTimeout(timeoutId); + if (retryTimeoutId) clearTimeout(retryTimeoutId); } } @@ -398,7 +405,8 @@ export class AEMFetch { if (timeout) { options.signal = signal; } - + + let retryTimeoutId: NodeJS.Timeout | undefined; try { const response = await this.fetch(fullUrl, { ...options, @@ -406,20 +414,27 @@ export class AEMFetch { body, headers }); - + if (response.status === 401) { await this.refreshAuthToken(); - return await this.fetch(fullUrl, { + // Fresh timeout window for the retry: the original signal may already be aborted + // if the refresh took longer than the original `timeout`. + const retry = this.getTimeoutOptions(timeout); + retryTimeoutId = retry.timeoutId; + const retryInit: RequestInit = { ...options, method: 'POST', body, - headers: new Headers(headers) - }); + headers: new Headers(headers), + }; + if (timeout) retryInit.signal = retry.signal; + return await this.fetch(fullUrl, retryInit); } - + return response; } finally { if (timeoutId) clearTimeout(timeoutId); + if (retryTimeoutId) clearTimeout(retryTimeoutId); } } } From 016776bd23202aff451eaf805111fe879f42f4b9 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:55:14 +0200 Subject: [PATCH 10/21] fix: leak #7 - skip 401 retry on permission errors --- src/aem/aem.fetch.ts | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index 95d6e29..a5dfc37 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -46,6 +46,31 @@ function isSameOrigin(from: string, to: string): boolean { } } +/** + * Decide whether a 401 response should trigger a token refresh + retry. + * Inspects the `WWW-Authenticate` header for an OAuth Bearer `error=` directive + * (RFC 6750). Retries are only useful for `invalid_token`/`expired_token`; + * `insufficient_scope`, `invalid_request`, `insufficient_user_authentication` + * cannot be solved by a fresh token, so we fail fast instead of burning a + * round-trip. Absence of the header or unknown error codes fall back to retry. + */ +function shouldRetryOn401(response: Response): boolean { + const wwwAuth = response.headers.get('WWW-Authenticate'); + if (!wwwAuth) return true; + const match = wwwAuth.match(/error\s*=\s*"([^"]+)"|error\s*=\s*([^\s,]+)/i); + if (!match) return true; + const err = (match[1] || match[2]).toLowerCase(); + if (err === 'invalid_token' || err === 'expired_token') return true; + if ( + err === 'insufficient_scope' || + err === 'invalid_request' || + err === 'insufficient_user_authentication' + ) { + return false; + } + return true; +} + export class AEMFetch { private fetch: FetchInstance | null; private readonly config: AEMFetchConfig; @@ -208,7 +233,7 @@ export class AEMFetch { let retryTimeoutId: NodeJS.Timeout | undefined; try { response = await this.fetch(url, options); - if (response.status === 401) { + if (response.status === 401 && shouldRetryOn401(response)) { LOGGER.warn(`AEM request to ${sanitizeUrl(url)} returned 401 Unauthorized. Attempting to refresh token...`); await this.refreshAuthToken(); // Fresh timeout window for the retry: the original signal may already be aborted @@ -415,7 +440,7 @@ export class AEMFetch { headers }); - if (response.status === 401) { + if (response.status === 401 && shouldRetryOn401(response)) { await this.refreshAuthToken(); // Fresh timeout window for the retry: the original signal may already be aborted // if the refresh took longer than the original `timeout`. From ee30674a6d3a14df4d8b49d746b97c75727b5f8f Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 14:58:03 +0200 Subject: [PATCH 11/21] fix: leak #8 - encode Basic auth credentials as Latin-1 --- src/aem/aem.fetch.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index a5dfc37..31906e1 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -161,7 +161,11 @@ export class AEMFetch { // Basic Authentication (username/password) if (config.username && config.password) { - return Buffer.from(`${config.username}:${config.password}`).toString('base64'); + // AEM Sling decodes Basic credentials as ISO-8859-1 (not UTF-8). Encoding the + // source as 'latin1' keeps ASCII identical while making 0x80-0xFF code points + // (é/ü/ñ/etc.) round-trip correctly. Passwords with code points > 0xFF still + // can't be expressed in Basic auth and are out of scope. + return Buffer.from(`${config.username}:${config.password}`, 'latin1').toString('base64'); } throw new Error('No authentication credentials provided'); From 6c2c5714cdf817076222e12a6475a1e1595c42a4 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 15:00:39 +0200 Subject: [PATCH 12/21] fix: leak #9 - skip 401 retry for Basic auth --- src/aem/aem.fetch.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index 31906e1..8fc7b8a 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -98,6 +98,16 @@ export class AEMFetch { this.fetch = this.getFetchInstance(); } + /** + * True when the configured auth is OAuth Server-to-Server (clientId+secret), + * false when Basic (username+password). Refreshing the token is only useful + * for OAuth — for Basic, re-encoding the same credentials produces the same + * Base64, so a 401-retry is a wasted round-trip. + */ + private get isOAuth(): boolean { + return !!this.config.auth.clientId && !this.config.auth.username; + } + /** * Returns a fetch instance with proper headers for AEM authentication. */ @@ -110,8 +120,7 @@ export class AEMFetch { // Always set Authorization (required for all requests) // Use Bearer for OAuth server-to-server, Basic for username/password - const isOAuth = this.config.auth.clientId && !this.config.auth.username; - if (isOAuth) { + if (this.isOAuth) { headers.set('Authorization', `Bearer ${this.token}`); } else { headers.set('Authorization', `Basic ${this.token}`); @@ -237,7 +246,7 @@ export class AEMFetch { let retryTimeoutId: NodeJS.Timeout | undefined; try { response = await this.fetch(url, options); - if (response.status === 401 && shouldRetryOn401(response)) { + if (response.status === 401 && this.isOAuth && shouldRetryOn401(response)) { LOGGER.warn(`AEM request to ${sanitizeUrl(url)} returned 401 Unauthorized. Attempting to refresh token...`); await this.refreshAuthToken(); // Fresh timeout window for the retry: the original signal may already be aborted @@ -444,7 +453,7 @@ export class AEMFetch { headers }); - if (response.status === 401 && shouldRetryOn401(response)) { + if (response.status === 401 && this.isOAuth && shouldRetryOn401(response)) { await this.refreshAuthToken(); // Fresh timeout window for the retry: the original signal may already be aborted // if the refresh took longer than the original `timeout`. From a647b0c08e253e09de5000fcc83b6329ef5636b4 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Wed, 10 Jun 2026 15:03:59 +0200 Subject: [PATCH 13/21] fix: leak #10 - allow IMS endpoint override via AEM_IMS_URL --- README.md | 8 ++++++++ src/aem/aem.auth.ts | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c292dd..5e08343 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ Options: For AEMaaCS, use the `clientId` and `clientSecret` for authentication. [More info](https://developer.adobe.com/developer-console/docs/guides/authentication/ServerToServerAuthentication/implementation). For self-hosted AEM use user/pass. The default credentials are `admin:admin`. +#### Environment variables + +| Variable | Purpose | +|---|---| +| `MCP_LOGGER` | Set to `true` to enable diagnostic logging on stdout (off by default — required off for MCP stdio clients). | +| `MCP_USERNAME` / `MCP_PASSWORD` | Optional HTTP Basic auth gate on `POST /mcp` (only active when both are set). | +| `AEM_IMS_URL` | Override the Adobe IMS token endpoint. Defaults to `https://ims-na1.adobelogin.com/ims/token`. Set to `https://ims-eu1.adobelogin.com/ims/token` (EMEA) or `https://ims-jp1.adobelogin.com/ims/token` (APAC) for non-NA AEMaaCS tenants. | + ### Example Command ```sh aem-mcp -u=user@domain.com -p=mypass -H=https://author-qa.domain.com diff --git a/src/aem/aem.auth.ts b/src/aem/aem.auth.ts index 8747aa1..6043071 100644 --- a/src/aem/aem.auth.ts +++ b/src/aem/aem.auth.ts @@ -1,4 +1,6 @@ -const IMS_URL = "https://ims-na1.adobelogin.com/ims/token"; // Change region if needed +// IMS endpoint defaults to NA. Override with AEM_IMS_URL for EMEA +// (https://ims-eu1.adobelogin.com/ims/token) or APAC (https://ims-jp1.adobelogin.com/ims/token). +const IMS_URL = process.env.AEM_IMS_URL || "https://ims-na1.adobelogin.com/ims/token"; const SCOPES = "openid,AdobeID,read_organizations,additional_info.projectedProductContext,aem_author_read,aem_author_write"; type AccessTokenResponse = { From 074095b6418e94cc99a915062f7f692f34a854dd Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 12:10:33 +0200 Subject: [PATCH 14/21] build(gitignore): exclude .DS_Store, src/test/, and plan/ --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 731fb5d..b5787bf 100644 --- a/.gitignore +++ b/.gitignore @@ -159,6 +159,13 @@ dist *.swo *~ +# macOS +.DS_Store +**/.DS_Store + +# Local mTLS test certificates (throwaway, generated by src/test/gen-test-certs.sh) +src/test/ + # AEM connection and auth files .aem-connection.json .aem-oauth-tokens.json @@ -170,3 +177,5 @@ dist TEST_RESULTS.md INTERACTIVE_TESTING.md comprehensive-test.js + +plan/ \ No newline at end of file From c1dcc36e4b826ce24955a9f5d83cb3a48e7e406a Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 12:13:16 +0200 Subject: [PATCH 15/21] fix: leak #14 - reject expires_in <= 60 to prevent IMS storm Guard the OAuth token mint path in AEMFetch.getAuthToken: when IMS returns expires_in <= 60 (or NaN/undefined), tokenExpiry would be placed at-or-before 'now', forcing an IMS round-trip on every request. Single-flight dedups concurrent callers within a tick but cannot prevent the per-call burn. Throw a readable error instead of accepting the unusable token. --- src/aem/aem.fetch.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/aem/aem.fetch.ts b/src/aem/aem.fetch.ts index 8fc7b8a..6411413 100644 --- a/src/aem/aem.fetch.ts +++ b/src/aem/aem.fetch.ts @@ -158,6 +158,15 @@ export class AEMFetch { this.inflightToken = (async () => { try { const token = await getAccessToken(config.clientId, config.clientSecret, config.scope); + // Reject expires_in <= 60: a value at-or-below the 60s headroom would place + // tokenExpiry in the past, forcing an IMS mint on every request. Single-flight + // dedups within a tick but still burns a round-trip per call. NaN/undefined + // fail this check too (NaN > 60 is false). + if (!(token.expires_in > 60)) { + throw new Error( + `IMS returned invalid expires_in (${token.expires_in}); must be > 60 seconds to leave refresh headroom.` + ); + } this.token = token.access_token; this.tokenExpiry = now + (token.expires_in - 60) * 1000; return this.token; From 3216fc5053c9f6b2665c93f02b7f5dce9ef51189 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 14:37:55 +0200 Subject: [PATCH 16/21] fix: leak #15 - validate Origin header to prevent DNS rebinding Replace cors({ origin: '*' }) with a narrow allowlist enforced ahead of the cors middleware. Defaults cover the MCP Inspector (UI :6274, proxy :6277) on both localhost and 127.0.0.1; extra origins via repeatable --allow-origin CLI flag or comma-separated MCP_ALLOWED_ORIGINS env var. Disallowed Origin returns HTTP 403 + JSON-RPC -32600 body. Requests with no Origin (CLI curl) pass through unchanged. Access-Control-Expose-Headers: Mcp-Session-Id preserved. --- src/cli.ts | 9 +++++++- src/server/app.server.ts | 45 +++++++++++++++++++++++++++++++++++++--- src/types.ts | 1 + 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 872767f..9c86c21 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,12 @@ const argv: CliArgs = yargs(hideBin(process.argv)).options({ id: { type: 'string', default: '', alias: 'i', describe: 'clientId' }, secret: { type: 'string', default: '', alias: 's', describe: 'clientSecret' }, mcpPort: { type: 'number', default: 8502, alias: 'm' }, + 'allow-origin': { + type: 'string', + array: true, + default: [], + describe: 'extra Origin header value to allow on /mcp (repeatable). Inspector ports 6274/6277 on localhost+127.0.0.1 are always allowed. Comma-separated env: MCP_ALLOWED_ORIGINS.', + }, }) .help() .alias('h', 'help') @@ -27,10 +33,11 @@ if (argv.help) { } const { host, user, pass, mcpPort, id, secret } = argv; +const allowOrigin = argv.allowOrigin ?? []; if (host && hasUrlCredentials(host)) { console.error('Error: --host (-H) must not contain embedded credentials. Pass them via -u/-p (Basic) or -i/-s (OAuth) instead.'); process.exit(1); } -startServer({ host, user, pass, mcpPort, id, secret }); +startServer({ host, user, pass, mcpPort, id, secret, allowOrigin }); diff --git a/src/server/app.server.ts b/src/server/app.server.ts index c6a86be..91e0d7c 100644 --- a/src/server/app.server.ts +++ b/src/server/app.server.ts @@ -1,4 +1,4 @@ -import express, { Request, Response } from 'express'; +import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import { handleRequest } from '../mcp/mcp.server-handler.js'; // import { useBasicAuth } from './app.auth.js'; @@ -7,12 +7,51 @@ import { config } from '../config.js'; import { CliParams } from '../types.js'; import { LOGGER } from '../utils/logger.js'; +// MCP spec MUST: validate Origin header to prevent DNS-rebinding attacks. +// Defaults cover the official MCP Inspector (UI :6274, proxy :6277) on both +// loopback hostnames. Extra origins via --allow-origin CLI flag or comma- +// separated MCP_ALLOWED_ORIGINS env var. A loose regex like ^http://localhost(:\d+)?$ +// is NOT used: it would permit any other process bound to a local port to spoof +// an Origin header and widen the rebinding attack surface. +const DEFAULT_ALLOWED_ORIGINS = [ + 'http://localhost:6274', + 'http://127.0.0.1:6274', + 'http://localhost:6277', + 'http://127.0.0.1:6277', +]; + +function buildOriginAllowlist(extra: string[] = []): Set { + const fromEnv = (process.env.MCP_ALLOWED_ORIGINS || '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + return new Set([...DEFAULT_ALLOWED_ORIGINS, ...fromEnv, ...extra]); +} + const createServer = (params: CliParams = {}) => { const app = express(); + const allowedOrigins = buildOriginAllowlist(params.allowOrigin); + + // Gate before cors(): a disallowed Origin returns HTTP 403 with a JSON-RPC + // -32600 (Invalid Request) body. Requests without an Origin header (CLI curl, + // server-to-server tooling) pass through — only browsers attach Origin. + app.use((req: Request, res: Response, next: NextFunction) => { + const origin = req.headers.origin; + if (typeof origin === 'string' && !allowedOrigins.has(origin)) { + res.status(403).json({ + jsonrpc: '2.0', + error: { code: -32600, message: 'Origin not allowed' }, + id: null, + }); + return; + } + next(); + }); + app.use(cors({ - origin: '*', // Allow all origins - adjust as needed for production - exposedHeaders: ['Mcp-Session-Id'] + origin: (origin, cb) => cb(null, !origin || allowedOrigins.has(origin)), + exposedHeaders: ['Mcp-Session-Id'], })); app.use(express.json()); app.use(express.json({ limit: '10mb' })); diff --git a/src/types.ts b/src/types.ts index 76d658f..c815b1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,4 +5,5 @@ export type CliParams = { id?: string; secret?: string; mcpPort?: number; + allowOrigin?: string[]; }; From 0f362328e37abc02c92980988e044090bca70778 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 14:39:10 +0200 Subject: [PATCH 17/21] docs: codify testing workflow for plan-driven work in CLAUDE.md --- CLAUDE.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index cd9b1cd..9361a3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,23 @@ MCP_USERNAME=foo MCP_PASSWORD=bar node dist/cli.js # gate /m `MCP_LOGGER=true` enables logging. Without it, `LOGGER` is a no-op — this is intentional (stdout must stay clean for MCP stdio clients; see `src/utils/logger.ts`). +## Testing workflow + +When working on any item that has a `How to Test` block (e.g. each entry in +`plan/mcp-cert-auth-implementation.md`), **run every test you can run from the +terminal yourself**. That includes ad-hoc Node harnesses, `curl` against a +locally-started server, port checks, etc. The repo has no test runner — write a +one-off `.mjs` under `src/test/` when needed and run it. + +Show the results back to the user. Only ask the user to run a test manually +when you genuinely cannot execute it from this session (e.g. browser-driven +flows like the MCP Inspector UI, real-IMS / real-AEM tenants, OS-level signals +across machines). When you do hand off, give **exact step-by-step commands** +the user can paste — never "please verify X" without the recipe. + +Typecheck (`npm run build:types`) is necessary but not sufficient. Plan- +specified runtime behaviour must actually be observed. + ## Architecture The server is a Model Context Protocol (MCP) gateway that translates JSON-RPC tool calls into AEM HTTP operations (Sling/QueryBuilder/JCR). Request flow: From be4cb2ec7c90f565e35dfb86529c21139a4da9f0 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 14:49:12 +0200 Subject: [PATCH 18/21] fix: leak #16 - bind to loopback by default to prevent LAN exposure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without an explicit bind argument, Express's `app.listen(port, …)` binds 0.0.0.0, exposing /mcp to anyone on the same LAN (cafe WiFi, office, hotel) who can then drive every tool with whatever AEM credentials the server was launched with. Default to 127.0.0.1; add a `--bind` CLI flag and `MCP_BIND` env var for explicit all-interfaces opt-in. CLI flag overrides env. No auth flow or MCP protocol surface changed. --- src/cli.ts | 9 +++++++-- src/server/app.server.ts | 11 ++++++++--- src/types.ts | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 9c86c21..ac958a0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,11 @@ const argv: CliArgs = yargs(hideBin(process.argv)).options({ id: { type: 'string', default: '', alias: 'i', describe: 'clientId' }, secret: { type: 'string', default: '', alias: 's', describe: 'clientSecret' }, mcpPort: { type: 'number', default: 8502, alias: 'm' }, + bind: { + type: 'string', + default: process.env.MCP_BIND || '127.0.0.1', + describe: 'host interface to bind (default 127.0.0.1, loopback-only). Use 0.0.0.0 to expose on the LAN. Env: MCP_BIND.', + }, 'allow-origin': { type: 'string', array: true, @@ -32,7 +37,7 @@ if (argv.help) { process.exit(0); // prevent startServer from running } -const { host, user, pass, mcpPort, id, secret } = argv; +const { host, user, pass, mcpPort, id, secret, bind } = argv; const allowOrigin = argv.allowOrigin ?? []; if (host && hasUrlCredentials(host)) { @@ -40,4 +45,4 @@ if (host && hasUrlCredentials(host)) { process.exit(1); } -startServer({ host, user, pass, mcpPort, id, secret, allowOrigin }); +startServer({ host, user, pass, mcpPort, id, secret, allowOrigin, bind }); diff --git a/src/server/app.server.ts b/src/server/app.server.ts index 91e0d7c..4ad5dc5 100644 --- a/src/server/app.server.ts +++ b/src/server/app.server.ts @@ -117,14 +117,19 @@ const createServer = (params: CliParams = {}) => { } export const startServer = (params: CliParams = {}) => { - const { mcpPort = 8502 } = params || {}; + // Default to loopback (127.0.0.1): without an explicit bind argument Express + // listens on 0.0.0.0, exposing /mcp to anyone on the same LAN (cafe WiFi, + // office, hotel) who can then drive every tool with whatever AEM credentials + // the server was launched with. Pass --bind 0.0.0.0 (or MCP_BIND=0.0.0.0) + // to opt back into all-interfaces explicitly. + const { mcpPort = 8502, bind = '127.0.0.1' } = params || {}; const app = createServer(params); - app.listen(mcpPort, (error) => { + app.listen(mcpPort, bind, (error?: Error) => { if (error) { LOGGER.error('Failed to start server:', error); process.exit(1); } - LOGGER.log(`0. AEM MCP Server listening on port ${mcpPort}`); + LOGGER.log(`0. AEM MCP Server listening on ${bind}:${mcpPort}`); }); }; diff --git a/src/types.ts b/src/types.ts index c815b1e..a6f6578 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,4 +6,5 @@ export type CliParams = { secret?: string; mcpPort?: number; allowOrigin?: string[]; + bind?: string; }; From d5eab210d84dbdbde799ac0cc04bdc44ac3e7f0c Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 15:17:27 +0200 Subject: [PATCH 19/21] fix: leak #17 - graceful drain on SIGINT/SIGTERM + fatal-error fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the unconditional process.exit(0) on SIGINT with a drain sequence: server.close() to stop accepting new connections, closeIdleConnections() to drop keep-alive sockets, transports[].close() for MCP session teardown, then wait up to --shutdown-drain-seconds (env: MCP_SHUTDOWN_DRAIN_SECONDS, default 60s) for in-flight tool calls to finish. Worst-case bulk operations (bulkUpdateComponents) can hold for minutes — a shorter window would still abandon work. Wire the same handler to SIGTERM. Exit code semantics: clean drain -> 0; forced timeout -> 1 so orchestrators (Kubernetes, systemd) can distinguish "everything finished" from "had to abandon pending work" and surface alerts on the latter. A "0" on timeout would hide hostile keep-opens or stuck handlers from operators. Add uncaughtException / unhandledRejection fallbacks: log to stderr with the "[fatal]" prefix, sync-close transports (process state is undefined per Node docs — no async drain), exit 1. --- src/cli.ts | 8 ++- src/server/app.server.ts | 106 ++++++++++++++++++++++++++++++++++++--- src/types.ts | 1 + 3 files changed, 107 insertions(+), 8 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index ac958a0..b757320 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -22,6 +22,11 @@ const argv: CliArgs = yargs(hideBin(process.argv)).options({ default: process.env.MCP_BIND || '127.0.0.1', describe: 'host interface to bind (default 127.0.0.1, loopback-only). Use 0.0.0.0 to expose on the LAN. Env: MCP_BIND.', }, + 'shutdown-drain-seconds': { + type: 'number', + default: Number(process.env.MCP_SHUTDOWN_DRAIN_SECONDS) || 60, + describe: 'max seconds to wait for in-flight requests to finish on SIGINT/SIGTERM before forcing exit. Default 60s — must outlast worst-case bulk tool calls (see docs/BULK_OPERATIONS.md). Env: MCP_SHUTDOWN_DRAIN_SECONDS.', + }, 'allow-origin': { type: 'string', array: true, @@ -39,10 +44,11 @@ if (argv.help) { const { host, user, pass, mcpPort, id, secret, bind } = argv; const allowOrigin = argv.allowOrigin ?? []; +const shutdownDrainSeconds = argv.shutdownDrainSeconds ?? 60; if (host && hasUrlCredentials(host)) { console.error('Error: --host (-H) must not contain embedded credentials. Pass them via -u/-p (Basic) or -i/-s (OAuth) instead.'); process.exit(1); } -startServer({ host, user, pass, mcpPort, id, secret, allowOrigin, bind }); +startServer({ host, user, pass, mcpPort, id, secret, allowOrigin, bind, shutdownDrainSeconds }); diff --git a/src/server/app.server.ts b/src/server/app.server.ts index 4ad5dc5..c4f1d7b 100644 --- a/src/server/app.server.ts +++ b/src/server/app.server.ts @@ -6,6 +6,7 @@ import { AEMConnector } from '../aem/aem.connector.js'; import { config } from '../config.js'; import { CliParams } from '../types.js'; import { LOGGER } from '../utils/logger.js'; +import { transports } from '../mcp/mcp.transports.js'; // MCP spec MUST: validate Origin header to prevent DNS-rebinding attacks. // Defaults cover the official MCP Inspector (UI :6274, proxy :6277) on both @@ -122,18 +123,109 @@ export const startServer = (params: CliParams = {}) => { // office, hotel) who can then drive every tool with whatever AEM credentials // the server was launched with. Pass --bind 0.0.0.0 (or MCP_BIND=0.0.0.0) // to opt back into all-interfaces explicitly. - const { mcpPort = 8502, bind = '127.0.0.1' } = params || {}; + const { mcpPort = 8502, bind = '127.0.0.1', shutdownDrainSeconds = 60 } = params || {}; const app = createServer(params); - app.listen(mcpPort, bind, (error?: Error) => { + const server = app.listen(mcpPort, bind, (error?: Error) => { if (error) { LOGGER.error('Failed to start server:', error); process.exit(1); } LOGGER.log(`0. AEM MCP Server listening on ${bind}:${mcpPort}`); }); -}; -process.on('SIGINT', async () => { - LOGGER.log('Shutting down server...'); - process.exit(0); -}); + // Graceful drain on SIGINT/SIGTERM. The prior handler called process.exit(0) + // unconditionally, which dropped in-flight tool calls mid-flight — including + // multi-minute bulk operations (bulkUpdateComponents, bulkConvertComponents, + // see docs/BULK_OPERATIONS.md) where half-applied AEM mutations are worse + // than either a clean success or a clean rollback. + // + // Drain sequence (logs to stderr at each step — LOGGER is a no-op without + // MCP_LOGGER, but shutdown diagnostics must always be visible): + // 1. server.close() -> stop accepting new connections + // 2. closeIdleConnections() -> drop keep-alive sockets between requests + // so server.close()'s callback can fire without waiting for them + // 3. transports[].close() -> tear down MCP sessions + // 4. wait up to shutdownDrainSeconds for in-flight requests + // 5. process.exit(0) + let shuttingDown = false; + const drain = (signal: NodeJS.Signals) => { + if (shuttingDown) return; + shuttingDown = true; + const startedAt = Date.now(); + process.stderr.write( + `[shutdown] ${signal} received — starting drain (max ${shutdownDrainSeconds}s)\n` + ); + + const forceExit = setTimeout(() => { + const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1); + process.stderr.write( + `[shutdown] drain deadline reached after ${elapsed}s — abandoning pending requests, exiting 1\n` + ); + // Exit non-zero so orchestrators (Kubernetes, systemd) can distinguish a + // forced timeout from a clean drain. A "0" here would tell the + // orchestrator everything was fine and suppress alerts even when a + // slow/stuck handler (or a hostile keep-open) prevented the drain from + // completing. + process.exit(1); + }, shutdownDrainSeconds * 1000); + // Don't keep the event loop alive solely for this timer — once server.close + // resolves and the process is otherwise idle, exit cleanly. + forceExit.unref(); + + server.close((err) => { + clearTimeout(forceExit); + const elapsed = ((Date.now() - startedAt) / 1000).toFixed(2); + if (err) { + process.stderr.write(`[shutdown] server.close error: ${err.message}\n`); + } + process.stderr.write(`[shutdown] drain complete in ${elapsed}s — exit 0\n`); + process.exit(0); + }); + + // Drop keep-alive sockets between requests so the close() callback can + // resolve once active requests finish. Without this, idle keep-alives + // hold the server open until the OS times them out. + if (typeof server.closeIdleConnections === 'function') { + server.closeIdleConnections(); + } + + // Tear down MCP transports. Each transport may have open SSE streams or + // session state that needs explicit cleanup. + for (const [sessionId, transport] of Object.entries(transports)) { + try { + process.stderr.write(`[shutdown] closing transport ${sessionId}\n`); + transport.close(); + } catch (e: any) { + process.stderr.write( + `[shutdown] error closing transport ${sessionId}: ${e?.message ?? e}\n` + ); + } + } + }; + + process.on('SIGINT', () => drain('SIGINT')); + process.on('SIGTERM', () => drain('SIGTERM')); + + // Fatal-error fallbacks. Node docs are explicit that the process is in an + // undefined state after `uncaughtException` — we MUST NOT try to resume + // normal work or run the full async drain. Do sync-only cleanup (close + // transports, write a stderr breadcrumb) and exit non-zero so the + // orchestrator restarts the container instead of leaving a half-dead pod. + const fatal = (kind: string, err: unknown) => { + if (shuttingDown) return; + shuttingDown = true; + const message = + err instanceof Error ? err.stack || err.message : String(err); + process.stderr.write(`[fatal] ${kind}: ${message}\n`); + for (const transport of Object.values(transports)) { + try { + transport.close(); + } catch { + // best-effort sync cleanup; process state is already suspect + } + } + process.exit(1); + }; + process.on('uncaughtException', (err) => fatal('uncaughtException', err)); + process.on('unhandledRejection', (reason) => fatal('unhandledRejection', reason)); +}; diff --git a/src/types.ts b/src/types.ts index a6f6578..c4041a6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,4 +7,5 @@ export type CliParams = { mcpPort?: number; allowOrigin?: string[]; bind?: string; + shutdownDrainSeconds?: number; }; From c4b710b9d73f14ce02a47e4c2709e33654256b09 Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 15:24:37 +0200 Subject: [PATCH 20/21] docs: codify "clarify before deciding" and testing workflow in CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two workflow rules added to make Claude's behaviour in this repo predictable without needing to repeat the instructions every session: - `Clarify before deciding` — before any non-trivial decision (credentials, destructive vs read-only, scope, approach against real environments), ask the user first with concrete options. Do not infer or default-to- destructive just because a plan lists a destructive path. - `Testing workflow` — for items with a `How to Test` block, run every terminal-driven test yourself (ad-hoc Node harnesses under src/test/, curl against a locally-started server, port checks). Show results and only delegate to the user for tests that genuinely cannot run from a session (browser UIs, real cloud tenants, multi-machine signals) — with paste- ready commands when handing off. Typecheck is necessary but not sufficient. --- CLAUDE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9361a3f..b7a2648 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,27 @@ MCP_USERNAME=foo MCP_PASSWORD=bar node dist/cli.js # gate /m `MCP_LOGGER=true` enables logging. Without it, `LOGGER` is a no-op — this is intentional (stdout must stay clean for MCP stdio clients; see `src/utils/logger.ts`). +## Clarify before deciding + +Before taking any action that involves a real choice — credentials, destructive +vs read-only, which approach to take, which environment to touch, what scope +to cover — **ask the user first** with concrete options (use the +AskUserQuestion tool when it's a short choice). Do not infer credentials, +assume "the obvious" tool from a list, or default to a destructive path +because it's listed in a plan. + +What counts as "a real choice": +- Anything that mutates external state (a real AEM, a real database, a real + remote — even via a read-only-looking tool that might be misconfigured). +- Anything where multiple plausible paths exist and the safe / fast / cheap + trade-off is not obvious from context. +- Anything where the user hasn't given an explicit instruction in *this* + conversation and there's no durable preference in memory. + +What does NOT count: typechecking, running local terminal-only tests, reading +files, writing to gitignored harnesses, recompiling. Take those without +asking. + ## Testing workflow When working on any item that has a `How to Test` block (e.g. each entry in From aedd490fe0e7b7239e0eb35256749ee0ae82451d Mon Sep 17 00:00:00 2001 From: Zoran Markovic Date: Thu, 11 Jun 2026 16:52:45 +0200 Subject: [PATCH 21/21] fix: leak #18 - stale session returns 404 to enable Inspector auto-reinit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the 400 Bad Request on unknown Mcp-Session-Id with 404 Not Found per the MCP StreamableHTTP specification §6.3. Clients that send a stale session ID (e.g. after a server restart) must receive 404 so that inspection tooling like the MCP Inspector can detect the lost session and trigger its automatic reinitialization loop. A 400 leaves the Inspector stuck on a generic error with no recovery path, requiring a manual reconnect. Also tighten the JSON-RPC error code from -32000 (generic server error) to -32001 (session not found) so programmatic clients can distinguish a bad-session rejection from an internal server fault. --- docs/phase1-smoke-audit.md | 65 +++++++++++++++++++++++++++++++++++ src/mcp/mcp.server-handler.ts | 11 +++--- 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 docs/phase1-smoke-audit.md diff --git a/docs/phase1-smoke-audit.md b/docs/phase1-smoke-audit.md new file mode 100644 index 0000000..a0ebfc6 --- /dev/null +++ b/docs/phase1-smoke-audit.md @@ -0,0 +1,65 @@ +# Phase 1 Smoke Test Audit — Transport Compliance & Bug Fixes + +**Date:** 2026-06-11 +**Branch:** fix/DDE-365-security-hardening +**Node:** v20.19.4 +**Build:** `npm run build` (esbuild, ESM) + +--- + +## Test Results + +| ID | Area | Command / Harness | Expected | Result | +|----|------|-------------------|----------|--------| +| T1 | Loopback binding (BF2) | `lsof -nP -iTCP:8520 -sTCP:LISTEN` | `127.0.0.1:8520 LISTEN` | **PASS** | +| T2 | Credential in `--host` rejected | `node dist/cli.js -H http://admin:admin@localhost:4502` | exit 1 + error message | **PASS** | +| T3 | Health check — live AEM | `GET /health` (AEM at localhost:4502) | `auth: authorized` | **PASS** | +| T4 | MCP initialize — session ID | `POST /mcp` initialize body | HTTP 200 + `Mcp-Session-Id` header | **PASS** | +| T5 | OAuth single-flight storm | `node src/test/smoke-single-flight.mjs` (4 assertions) | ALL PASS | **PASS** | +| T6 | Blocked origin (BF3) | `Origin: http://evil.test` | HTTP 403 | **PASS** | +| T6b | Port-fuzzing variant | `Origin: http://localhost:9999` | HTTP 403 | **PASS** | +| T7 | Inspector origins allowed | Origins `localhost/127.0.0.1` on ports 6274 + 6277 | HTTP 200 (all 4) | **PASS** | +| T8 | No `Origin` passthrough | `curl` without Origin header | HTTP 200 | **PASS** | +| T9 | Invalid JSON-RPC → 400 (BF4) | Missing `method` field; wrong `jsonrpc` version | HTTP 400 (both) | **PASS** | +| T10 | Stale session (BF5/leak #18) | `Mcp-Session-Id: 00000000-...` on non-initialize | HTTP 404 | **PASS** | +| T11 | SIGINT clean drain (BF8) | `test-leak-17.mjs` Test 1 | exit 0, `drain complete` in stderr | **PASS** | +| T12 | SIGTERM clean drain (BF8) | `test-leak-17.mjs` Test 2 | exit 0, `drain complete` in stderr | **PASS** | +| T13 | Drain deadline → exit 1 | `test-leak-17.mjs` Test 3 (`--shutdown-drain-seconds 1` + stuck request) | exit 1, `drain deadline reached` within ~1s | **PASS** | +| T14 | `uncaughtException` / `unhandledRejection` | `test-leak-17.mjs` Tests 7–8 | exit 1, `[fatal]` log | **PASS** | +| T15 | Raw password never in stderr | `grep SUPERSECRET_DO_NOT_LOG /tmp/t15-stderr.txt` | no match | **PASS** | + +**Total: 16/16 PASS** (T13 drain sub-tests + T14 fatal-error sub-tests covered by 19 assertions in `test-leak-17.mjs`) + +--- + +## Automated Test Harnesses + +| File | What it covers | Run | +|------|---------------|-----| +| `src/test/smoke-single-flight.mjs` | OAuth single-flight token mint (3 scenarios, 4 assertions) | `npm run build && node src/test/smoke-single-flight.mjs` | +| `src/test/test-leak-17.mjs` | SIGINT/SIGTERM drain, drain deadline, uncaughtException, unhandledRejection (8 scenarios, 19 assertions) | `npm run build && node src/test/test-leak-17.mjs` | +| `src/test/test-leak-18.mjs` | Stale session → 404, error code -32001, re-initialize path (4 scenarios, 8 assertions) | `npm run build && node src/test/test-leak-18.mjs` | + +--- + +## Findings + +### ✅ T10 — Stale session now returns 404 (fixed as leak #18) + +- **Location:** `src/mcp/mcp.server-handler.ts:66` +- **Was:** `res.status(400)` with JSON-RPC code `-32000` +- **Fixed:** `res.status(404)` with JSON-RPC code `-32001` and message "Session not found." +- **Impact:** MCP Inspector auto-reinitialization loop now triggers correctly on server restart. +- **Test:** `src/test/test-leak-18.mjs` — 8/8 assertions PASS + +### ℹ Accept header requirement (SDK enforcement, not a bug) + +The MCP SDK's `StreamableHTTPServerTransport` returns **406 Not Acceptable** when the `POST /mcp` request is missing `Accept: application/json, text/event-stream`. This is correct per the MCP StreamableHTTP spec. All runbook `curl` commands must include this header — commands without it are invalid clients, not a server bug. + +Correct curl baseline: +```sh +curl -X POST http://127.0.0.1:8502/mcp \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{...}' +``` \ No newline at end of file diff --git a/src/mcp/mcp.server-handler.ts b/src/mcp/mcp.server-handler.ts index a60de35..c73d349 100644 --- a/src/mcp/mcp.server-handler.ts +++ b/src/mcp/mcp.server-handler.ts @@ -61,13 +61,16 @@ export const handleRequest = async (req: Request, res: Response, cliParams: CliP await transport.handleRequest(req, res, req.body); return; // Already handled } else { - // Invalid request - no session ID or not initialization request + // MCP StreamableHTTP spec §6.3: unknown session-id MUST return 404 so + // that clients (e.g. MCP Inspector) detect the stale reference and + // trigger their auto-reinitialization loop. A 400 here would cause + // Inspector to display a generic error with no automatic recovery path. LOGGER.log('Invalid request - no session ID or not initialization request'); - res.status(400).json({ + res.status(404).json({ jsonrpc: '2.0', error: { - code: -32000, - message: 'Bad Request: No valid session ID provided. Please re-initialize the MCP server.', + code: -32001, + message: 'Session not found. Please re-initialize the MCP server.', }, id: null, });