diff --git a/.github/workflows/run-tck.yaml b/.github/workflows/run-tck.yaml index 5e0154a6..83cdf9c9 100644 --- a/.github/workflows/run-tck.yaml +++ b/.github/workflows/run-tck.yaml @@ -10,11 +10,14 @@ on: env: # TODO once we have the TCK for 0.4.0 we will need to look at the branch to decide which TCK version to run. # Tag of the TCK - TCK_VERSION: 0.3.0.beta3 + # Temporary set to this commit sha because of this issue https://github.com/a2aproject/a2a-js/pull/142#issuecomment-3558892334 + TCK_VERSION: 601bf0d6f63baafb079c2ab23f53d73ce03e5461 # Tells uv to not need a venv, and instead use system UV_SYSTEM_PYTHON: 1 - # SUT_JSONRPC_URL to use for the TCK and the server agent - SUT_JSONRPC_URL: http://localhost:41241 + # Base URL for the SUT agent + SUT_BASE_URL: http://localhost:41241 + # Base URL for the SUT agent JSON-RPC transport + SUT_JSONRPC_URL: http://localhost:41241/a2a/jsonrpc # Slow system on CI TCK_STREAMING_TIMEOUT: 5.0 @@ -58,7 +61,7 @@ jobs: npm run tck:sut-agent & - name: Wait for SUT to start run: | - URL="${{ env.SUT_JSONRPC_URL }}/.well-known/agent-card.json" + URL="${{ env.SUT_BASE_URL }}/.well-known/agent-card.json" EXPECTED_STATUS=200 TIMEOUT=120 RETRY_INTERVAL=2 @@ -94,8 +97,9 @@ jobs: id: run-tck-mandatory timeout-minutes: 5 run: | - ./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory + ./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category mandatory --transports jsonrpc,rest working-directory: tck/a2a-tck + # TODO: Add back the transports jsonrpc,rest once the TCK supports REST, linked to https://github.com/a2aproject/a2a-tck/issues/99 - name: Run TCK (capabilities) id: run-tck-capabilities timeout-minutes: 5 diff --git a/.gitignore b/.gitignore index c10627bf..fd1e8df1 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,4 @@ testem.log # System files .DS_Store Thumbs.db +.nyc_output/ diff --git a/package-lock.json b/package-lock.json index 7270d8eb..cf89f128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -977,9 +977,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "cpu": [ "arm" ], @@ -991,9 +991,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "cpu": [ "arm64" ], @@ -1005,9 +1005,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "cpu": [ "arm64" ], @@ -1019,9 +1019,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "cpu": [ "x64" ], @@ -1033,9 +1033,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "cpu": [ "arm64" ], @@ -1047,9 +1047,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "cpu": [ "x64" ], @@ -1061,9 +1061,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "cpu": [ "arm" ], @@ -1075,9 +1075,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "cpu": [ "arm" ], @@ -1089,9 +1089,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "cpu": [ "arm64" ], @@ -1103,9 +1103,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "cpu": [ "arm64" ], @@ -1117,9 +1117,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "cpu": [ "loong64" ], @@ -1131,9 +1131,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "cpu": [ "ppc64" ], @@ -1145,9 +1145,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "cpu": [ "riscv64" ], @@ -1159,9 +1159,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "cpu": [ "riscv64" ], @@ -1173,9 +1173,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "cpu": [ "s390x" ], @@ -1187,9 +1187,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "cpu": [ "x64" ], @@ -1201,9 +1201,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "cpu": [ "x64" ], @@ -1215,9 +1215,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", "cpu": [ "arm64" ], @@ -1229,9 +1229,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "cpu": [ "arm64" ], @@ -1243,9 +1243,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "cpu": [ "ia32" ], @@ -1257,9 +1257,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", "cpu": [ "x64" ], @@ -1271,9 +1271,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "cpu": [ "x64" ], @@ -1425,9 +1425,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", "dev": true, "license": "MIT" }, @@ -1572,17 +1572,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", - "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", + "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/type-utils": "8.48.0", - "@typescript-eslint/utils": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/type-utils": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1596,7 +1596,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.0", + "@typescript-eslint/parser": "^8.47.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1612,17 +1612,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", - "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", + "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", "debug": "^4.3.4" }, "engines": { @@ -1638,14 +1637,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", - "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", + "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.0", - "@typescript-eslint/types": "^8.48.0", + "@typescript-eslint/tsconfig-utils": "^8.47.0", + "@typescript-eslint/types": "^8.47.0", "debug": "^4.3.4" }, "engines": { @@ -1660,14 +1659,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", - "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", + "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0" + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1678,9 +1677,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", - "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", + "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", "dev": true, "license": "MIT", "engines": { @@ -1695,15 +1694,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", - "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", + "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1720,9 +1719,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", - "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", + "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", "dev": true, "license": "MIT", "engines": { @@ -1734,20 +1733,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", - "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", + "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.0", - "@typescript-eslint/tsconfig-utils": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/project-service": "8.47.0", + "@typescript-eslint/tsconfig-utils": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -1788,16 +1788,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", - "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", + "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0" + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1812,13 +1812,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", - "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", + "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/types": "8.47.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1856,7 +1856,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1957,16 +1956,6 @@ "node": ">=8" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -2727,7 +2716,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2799,7 +2787,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2860,7 +2847,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3870,7 +3856,6 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4033,7 +4018,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4090,7 +4074,6 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4251,7 +4234,6 @@ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -4279,13 +4261,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/gts/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/gts/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -4392,24 +4367,30 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/human-signals": { @@ -5123,19 +5104,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -5147,20 +5115,16 @@ } }, "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==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, "node_modules/mimic-fn": { @@ -5221,6 +5185,16 @@ "node": ">= 6" } }, + "node_modules/minimist-options/node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -5705,7 +5679,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5789,12 +5762,11 @@ } }, "node_modules/prettier": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz", - "integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5910,16 +5882,16 @@ } }, "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" }, "engines": { "node": ">= 0.10" @@ -6229,9 +6201,9 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { @@ -6245,28 +6217,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -6334,6 +6306,13 @@ "npm": ">=2.0.0" } }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6571,16 +6550,6 @@ "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -6722,18 +6691,18 @@ } }, "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", + "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { @@ -6765,6 +6734,19 @@ "node": ">=14.18.0" } }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/supertest": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", @@ -7000,9 +6982,9 @@ "license": "Apache-2.0" }, "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD" }, @@ -7069,6 +7051,16 @@ "node": ">=8" } }, + "node_modules/tsup/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", @@ -7085,13 +7077,19 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.20.6", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -7647,7 +7645,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7657,16 +7654,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", - "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.47.0.tgz", + "integrity": "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.0", - "@typescript-eslint/parser": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0" + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7945,6 +7942,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/src/server/express/http_rest_handler.ts b/src/server/express/http_rest_handler.ts new file mode 100644 index 00000000..8e588c95 --- /dev/null +++ b/src/server/express/http_rest_handler.ts @@ -0,0 +1,471 @@ +import express, { + Request, + Response, + RequestHandler, + ErrorRequestHandler, + NextFunction, +} from 'express'; +import { A2ARequestHandler } from '../request_handler/a2a_request_handler.js'; +import { A2AError } from '../error.js'; +import { SSE_HEADERS, formatSSEEvent, formatSSEErrorEvent } from '../../sse_utils.js'; +import { + RestTransportHandler, + HTTP_STATUS, + mapErrorToStatus, + toHTTPError, +} from '../transports/rest/http_rest_transport_handler.js'; +import { ServerCallContext } from '../context.js'; +import { getRequestedExtensions } from '../utils.js'; +import { HTTP_EXTENSION_HEADER } from '../../constants.js'; +import { UserBuilder } from './common.js'; + +/** + * Options for configuring the HTTP REST handler. + */ +export interface HttpRestHandlerOptions { + requestHandler: A2ARequestHandler; + userBuilder: UserBuilder; +} + +/** + * Express error handler middleware for REST API JSON parse errors. + * Catches SyntaxError from express.json() and converts to A2A parse error format. + * + * @param err - Error thrown by express.json() middleware + * @param _req - Express request (unused) + * @param res - Express response + * @param next - Next middleware function + */ +const restErrorHandler: ErrorRequestHandler = ( + err: Error, + _req: Request, + res: Response, + next: NextFunction +) => { + if (err instanceof SyntaxError && 'body' in err) { + const a2aError = A2AError.parseError('Invalid JSON payload.'); + return res.status(400).json(toHTTPError(a2aError)); + } + next(err); +}; + +// Route patterns removed - using explicit route definitions instead + +/** + * Type alias for async Express route handlers used in this module. + */ +type AsyncRouteHandler = (req: Request, res: Response) => Promise; + +// ============================================================================ +// HTTP REST Handler - Main Export +// ============================================================================ + +/** + * Creates Express.js middleware to handle A2A HTTP+REST requests. + * + * This handler implements the A2A REST API specification with snake_case + * field names, providing endpoints for: + * - Agent card retrieval (GET /v1/card) + * - Message sending with optional streaming (POST /v1/message:send|stream) + * - Task management (GET/POST /v1/tasks/:taskId:cancel|subscribe) + * - Push notification configuration + * + * The handler acts as an adapter layer, converting between REST format + * (snake_case) at the API boundary and internal TypeScript format (camelCase) + * for business logic. + * + * @param options - Configuration options including the request handler + * @returns Express router configured with all A2A REST endpoints + * + * @example + * ```typescript + * const app = express(); + * const requestHandler = new DefaultRequestHandler(...); + * app.use('/api/rest', httpRestHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication })); + * ``` + */ +export function httpRestHandler(options: HttpRestHandlerOptions): RequestHandler { + const router = express.Router(); + const restTransportHandler = new RestTransportHandler(options.requestHandler); + + router.use(express.json(), restErrorHandler); + + // ============================================================================ + // Helper Functions + // ============================================================================ + + /** + * Builds a ServerCallContext from the Express request. + * Extracts protocol extensions from headers and builds user from request. + * + * @param req - Express request object + * @returns ServerCallContext with requested extensions and authenticated user + */ + const buildContext = async (req: Request): Promise => { + const user = await options.userBuilder(req); + return new ServerCallContext(getRequestedExtensions(req.header(HTTP_EXTENSION_HEADER)), user); + }; + + /** + * Sets activated extensions header in the response if any extensions were activated. + * + * @param res - Express response object + * @param context - ServerCallContext containing activated extensions + */ + const setExtensionsHeader = (res: Response, context: ServerCallContext): void => { + if (context.activatedExtensions) { + res.setHeader(HTTP_EXTENSION_HEADER, Array.from(context.activatedExtensions)); + } + }; + + /** + * Sends a JSON response with the specified status code. + * Handles 204 No Content responses specially (no body). + * Sets activated extensions header if present in context. + * + * @param res - Express response object + * @param statusCode - HTTP status code + * @param context - ServerCallContext for setting extension headers + * @param body - Response body (omitted for 204 responses) + */ + const sendResponse = ( + res: Response, + statusCode: number, + context: ServerCallContext, + body?: unknown + ): void => { + setExtensionsHeader(res, context); + res.status(statusCode); + if (statusCode === HTTP_STATUS.NO_CONTENT) { + res.end(); + } else { + res.json(body); + } + }; + + /** + * Sends a Server-Sent Events (SSE) stream response. + * Sets appropriate SSE headers, streams events, and handles errors gracefully. + * Events are already converted to REST format by the transport handler. + * Sets activated extensions header if present in context. + * + * @param res - Express response object + * @param stream - Async generator yielding REST-formatted events + * @param context - ServerCallContext for setting extension headers + */ + const sendStreamResponse = async ( + res: Response, + stream: AsyncGenerator, + context: ServerCallContext + ): Promise => { + // Get first event before flushing headers to catch early errors + // This allows returning proper HTTP error codes instead of 200 + SSE error + const iterator = stream[Symbol.asyncIterator](); + let firstResult: IteratorResult; + try { + firstResult = await iterator.next(); + } catch (error) { + // Early error - return proper HTTP error + const a2aError = + error instanceof A2AError + ? error + : A2AError.internalError(error instanceof Error ? error.message : 'Streaming error'); + const statusCode = mapErrorToStatus(a2aError.code); + sendResponse(res, statusCode, context, toHTTPError(a2aError)); + return; + } + + // First event succeeded - now set SSE headers and stream + Object.entries(SSE_HEADERS).forEach(([key, value]) => { + res.setHeader(key, value); + }); + setExtensionsHeader(res, context); + res.flushHeaders(); + + try { + // Write first event + if (!firstResult.done) { + res.write(formatSSEEvent(firstResult.value)); + } + // Continue with remaining events + for await (const event of { [Symbol.asyncIterator]: () => iterator }) { + res.write(formatSSEEvent(event)); + } + } catch (streamError: unknown) { + console.error('SSE streaming error:', streamError); + const a2aError = + streamError instanceof A2AError + ? streamError + : A2AError.internalError( + streamError instanceof Error ? streamError.message : 'Streaming error' + ); + if (!res.writableEnded) { + res.write(formatSSEErrorEvent(toHTTPError(a2aError))); + } + } finally { + if (!res.writableEnded) { + res.end(); + } + } + }; + + /** + * Handles errors in route handlers by converting them to A2A error format + * and sending appropriate HTTP response. + * Gracefully handles cases where headers have already been sent. + * + * @param res - Express response object + * @param error - Error to handle (can be A2AError or generic Error) + */ + const handleError = (res: Response, error: unknown): void => { + if (res.headersSent) { + if (!res.writableEnded) { + res.end(); + } + return; + } + const a2aError = + error instanceof A2AError + ? error + : A2AError.internalError(error instanceof Error ? error.message : 'Internal server error'); + const statusCode = mapErrorToStatus(a2aError.code); + res.status(statusCode).json(toHTTPError(a2aError)); + }; + + /** + * Wraps an async route handler to centralize error handling. + * Catches any errors thrown by the handler and passes them to handleError. + * + * @param handler - Async route handler function + * @returns Wrapped handler with built-in error handling + */ + const asyncHandler = (handler: AsyncRouteHandler): AsyncRouteHandler => { + return async (req: Request, res: Response): Promise => { + try { + await handler(req, res); + } catch (error) { + handleError(res, error); + } + }; + }; + + // ============================================================================ + // Route Handlers + // ============================================================================ + + /** + * GET /v1/card + * + * Retrieves the authenticated extended agent card. + * + * @returns 200 OK with agent card + * @returns 500 Internal Server Error on failure + */ + router.get( + '/v1/card', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const result = await restTransportHandler.getAuthenticatedExtendedAgentCard(); + sendResponse(res, HTTP_STATUS.OK, context, result); + }) + ); + + /** + * POST /v1/message:send + * + * Sends a message to the agent synchronously. + * Returns either a Message (for immediate responses) or a Task (for async processing). + * Note: Colon is escaped in route definition for Express compatibility. + * + * @param req.body - MessageSendParams (accepts both snake_case and camelCase) + * @returns 201 Created with RestMessage or RestTask + * @returns 400 Bad Request if message is invalid + */ + router.post( + '/v1/message\\:send', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const result = await restTransportHandler.sendMessage(req.body, context); + sendResponse(res, HTTP_STATUS.CREATED, context, result); + }) + ); + + /** + * POST /v1/message:stream + * + * Sends a message to the agent with streaming response. + * Returns a Server-Sent Events (SSE) stream of updates. + * Note: Colon is escaped in route definition for Express compatibility. + * + * @param req.body - MessageSendParams (accepts both snake_case and camelCase) + * @returns 200 OK with SSE stream of messages, tasks, and status updates + * @returns 400 Bad Request if message is invalid + * @returns 501 Not Implemented if streaming not supported + */ + router.post( + '/v1/message\\:stream', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const stream = await restTransportHandler.sendMessageStream(req.body, context); + await sendStreamResponse(res, stream, context); + }) + ); + + /** + * GET /v1/tasks/:taskId + * + * Retrieves the current status and details of a task. + * + * @param req.params.taskId - Task identifier + * @param req.query.historyLength - Optional number of history messages to include + * @returns 200 OK with RestTask + * @returns 400 Bad Request if historyLength is invalid + * @returns 404 Not Found if task doesn't exist + */ + router.get( + '/v1/tasks/:taskId', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const result = await restTransportHandler.getTask( + req.params.taskId, + context, + req.query.historyLength + ); + sendResponse(res, HTTP_STATUS.OK, context, result); + }) + ); + + /** + * POST /v1/tasks/:taskId:cancel + * + * Attempts to cancel an ongoing task. + * The task may not be immediately canceled depending on its current state. + * + * @param req.params.taskId - Task identifier + * @returns 202 Accepted with RestTask (task is being canceled) + * @returns 404 Not Found if task doesn't exist + * @returns 409 Conflict if task cannot be canceled + */ + router.post( + '/v1/tasks/:taskId\\:cancel', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const result = await restTransportHandler.cancelTask(req.params.taskId, context); + sendResponse(res, HTTP_STATUS.ACCEPTED, context, result); + }) + ); + + /** + * POST /v1/tasks/:taskId:subscribe + * + * Resubscribes to an existing task's updates via Server-Sent Events (SSE). + * Useful for reconnecting to long-running tasks or receiving missed updates. + * + * @param req.params.taskId - Task identifier + * @returns 200 OK with SSE stream of task status and artifact updates + * @returns 404 Not Found if task doesn't exist + * @returns 501 Not Implemented if streaming not supported + */ + router.post( + '/v1/tasks/:taskId\\:subscribe', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const stream = await restTransportHandler.resubscribe(req.params.taskId, context); + await sendStreamResponse(res, stream, context); + }) + ); + + /** + * POST /v1/tasks/:taskId/pushNotificationConfigs + * + * Creates a push notification configuration for a task. + * The agent will send task updates to the configured webhook URL. + * + * @param req.params.taskId - Task identifier + * @param req.body - Push notification configuration (snake_case format) + * @returns 201 Created with TaskPushNotificationConfig + * @returns 501 Not Implemented if push notifications not supported + */ + router.post( + '/v1/tasks/:taskId/pushNotificationConfigs', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const config = { + ...req.body, + taskId: req.params.taskId, + task_id: req.params.taskId, + }; + const result = await restTransportHandler.setTaskPushNotificationConfig(config, context); + sendResponse(res, HTTP_STATUS.CREATED, context, result); + }) + ); + + /** + * GET /v1/tasks/:taskId/pushNotificationConfigs + * + * Lists all push notification configurations for a task. + * + * @param req.params.taskId - Task identifier + * @returns 200 OK with array of TaskPushNotificationConfig + * @returns 404 Not Found if task doesn't exist + */ + router.get( + '/v1/tasks/:taskId/pushNotificationConfigs', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const result = await restTransportHandler.listTaskPushNotificationConfigs( + req.params.taskId, + context + ); + sendResponse(res, HTTP_STATUS.OK, context, result); + }) + ); + + /** + * GET /v1/tasks/:taskId/pushNotificationConfigs/:configId + * + * Retrieves a specific push notification configuration. + * + * @param req.params.taskId - Task identifier + * @param req.params.configId - Push notification configuration identifier + * @returns 200 OK with TaskPushNotificationConfig + * @returns 404 Not Found if task or config doesn't exist + */ + router.get( + '/v1/tasks/:taskId/pushNotificationConfigs/:configId', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + const result = await restTransportHandler.getTaskPushNotificationConfig( + req.params.taskId, + req.params.configId, + context + ); + sendResponse(res, HTTP_STATUS.OK, context, result); + }) + ); + + /** + * DELETE /v1/tasks/:taskId/pushNotificationConfigs/:configId + * + * Deletes a push notification configuration. + * + * @param req.params.taskId - Task identifier + * @param req.params.configId - Push notification configuration identifier + * @returns 204 No Content on success + * @returns 404 Not Found if task or config doesn't exist + */ + router.delete( + '/v1/tasks/:taskId/pushNotificationConfigs/:configId', + asyncHandler(async (req, res) => { + const context = await buildContext(req); + await restTransportHandler.deleteTaskPushNotificationConfig( + req.params.taskId, + req.params.configId, + context + ); + sendResponse(res, HTTP_STATUS.NO_CONTENT, context); + }) + ); + + return router; +} diff --git a/src/server/express/index.ts b/src/server/express/index.ts index 5a643cc8..62b42102 100644 --- a/src/server/express/index.ts +++ b/src/server/express/index.ts @@ -5,3 +5,9 @@ export { A2AExpressApp } from './a2a_express_app.js'; export { UserBuilder } from './common.js'; +export { jsonRpcHandler } from './json_rpc_handler.js'; +export type { JsonRpcHandlerOptions } from './json_rpc_handler.js'; +export { agentCardHandler } from './agent_card_handler.js'; +export type { AgentCardHandlerOptions, AgentCardProvider } from './agent_card_handler.js'; +export { httpRestHandler } from './http_rest_handler.js'; +export type { HttpRestHandlerOptions } from './http_rest_handler.js'; diff --git a/src/server/express/json_rpc_handler.ts b/src/server/express/json_rpc_handler.ts index 2482e29c..beec15d8 100644 --- a/src/server/express/json_rpc_handler.ts +++ b/src/server/express/json_rpc_handler.ts @@ -8,12 +8,13 @@ import express, { import { JSONRPCErrorResponse, JSONRPCSuccessResponse, JSONRPCResponse } from '../../types.js'; import { A2AError } from '../error.js'; import { A2ARequestHandler } from '../request_handler/a2a_request_handler.js'; -import { JsonRpcTransportHandler } from '../transports/jsonrpc_transport_handler.js'; +import { JsonRpcTransportHandler } from '../transports/jsonrpc/jsonrpc_transport_handler.js'; import { ServerCallContext } from '../context.js'; import { getRequestedExtensions } from '../utils.js'; import { HTTP_EXTENSION_HEADER } from '../../constants.js'; import { UnauthenticatedUser } from '../authentication/user.js'; import { UserBuilder } from './common.js'; +import { SSE_HEADERS, formatSSEEvent, formatSSEErrorEvent } from '../../sse_utils.js'; export interface JsonRpcHandlerOptions { requestHandler: A2ARequestHandler; @@ -55,17 +56,18 @@ export function jsonRpcHandler(options: JsonRpcHandlerOptions): RequestHandler { undefined >; - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); + // Set SSE headers using shared utility + Object.entries(SSE_HEADERS).forEach(([key, value]) => { + res.setHeader(key, value); + }); res.flushHeaders(); try { for await (const event of stream) { // Each event from the stream is already a JSONRPCResult - res.write(`id: ${new Date().getTime()}\n`); - res.write(`data: ${JSON.stringify(event)}\n\n`); + // Use shared formatSSEEvent utility + res.write(formatSSEEvent(event)); } } catch (streamError) { console.error(`Error during SSE streaming (request ${req.body?.id}):`, streamError); @@ -88,9 +90,8 @@ export function jsonRpcHandler(options: JsonRpcHandlerOptions): RequestHandler { res.status(500).json(errorResponse); // Should be JSON, not SSE here } else { // Try to send as last SSE event if possible, though client might have disconnected - res.write(`id: ${new Date().getTime()}\n`); - res.write(`event: error\n`); // Custom event type for client-side handling - res.write(`data: ${JSON.stringify(errorResponse)}\n\n`); + // Use shared formatSSEErrorEvent utility + res.write(formatSSEErrorEvent(errorResponse)); } } finally { if (!res.writableEnded) { diff --git a/src/server/index.ts b/src/server/index.ts index b08053b1..04bd93e0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,7 +19,7 @@ export { ResultManager } from './result_manager.js'; export type { TaskStore } from './store.js'; export { InMemoryTaskStore } from './store.js'; -export { JsonRpcTransportHandler } from './transports/jsonrpc_transport_handler.js'; +export { JsonRpcTransportHandler } from './transports/jsonrpc/jsonrpc_transport_handler.js'; export { ServerCallContext } from './context.js'; export { A2AError } from './error.js'; diff --git a/src/server/transports/jsonrpc_transport_handler.ts b/src/server/transports/jsonrpc/jsonrpc_transport_handler.ts similarity index 97% rename from src/server/transports/jsonrpc_transport_handler.ts rename to src/server/transports/jsonrpc/jsonrpc_transport_handler.ts index 072e1c55..fb6066c0 100644 --- a/src/server/transports/jsonrpc_transport_handler.ts +++ b/src/server/transports/jsonrpc/jsonrpc_transport_handler.ts @@ -4,10 +4,10 @@ import { TaskIdParams, A2ARequest, JSONRPCResponse, -} from '../../types.js'; -import { ServerCallContext } from '../context.js'; -import { A2AError } from '../error.js'; -import { A2ARequestHandler } from '../request_handler/a2a_request_handler.js'; +} from '../../../types.js'; +import { ServerCallContext } from '../../context.js'; +import { A2AError } from '../../error.js'; +import { A2ARequestHandler } from '../../request_handler/a2a_request_handler.js'; /** * Handles JSON-RPC transport layer, routing requests to A2ARequestHandler. diff --git a/src/server/transports/rest/http_rest_transport_handler.ts b/src/server/transports/rest/http_rest_transport_handler.ts new file mode 100644 index 00000000..52b68c64 --- /dev/null +++ b/src/server/transports/rest/http_rest_transport_handler.ts @@ -0,0 +1,454 @@ +/** + * HTTP+JSON (REST) Transport Handler + * + * Accepts both snake_case (REST) and camelCase (internal) input. + * Returns camelCase (internal types). + */ + +import { A2AError } from '../../error.js'; +import { A2ARequestHandler } from '../../request_handler/a2a_request_handler.js'; +import { ServerCallContext } from '../../context.js'; +import { + Message, + Task, + TaskStatusUpdateEvent, + TaskArtifactUpdateEvent, + MessageSendParams, + TaskPushNotificationConfig, + TaskQueryParams, + TaskIdParams, + Part, + AgentCard, + FileWithBytes, + FileWithUri, +} from '../../../types.js'; +import { + RestMessage, + RestMessageSendParams, + RestTaskPushNotificationConfig, + PartInput, + MessageInput, + MessageSendParamsInput, + TaskPushNotificationConfigInput, + FileInput, +} from './rest_types.js'; + +// ============================================================================ +// HTTP Status Codes and Error Mapping +// ============================================================================ + +/** + * HTTP status codes used in REST responses. + */ +export const HTTP_STATUS = { + OK: 200, + CREATED: 201, + ACCEPTED: 202, + NO_CONTENT: 204, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + NOT_FOUND: 404, + CONFLICT: 409, + INTERNAL_SERVER_ERROR: 500, + NOT_IMPLEMENTED: 501, +} as const; + +/** + * A2A error codes mapped to JSON-RPC and protocol-specific errors. + */ +const A2A_ERROR_CODE = { + PARSE_ERROR: -32700, + INVALID_REQUEST: -32600, + METHOD_NOT_FOUND: -32601, + INVALID_PARAMS: -32602, + TASK_NOT_FOUND: -32001, + TASK_NOT_CANCELABLE: -32002, + PUSH_NOTIFICATION_NOT_SUPPORTED: -32003, + UNSUPPORTED_OPERATION: -32004, + UNAUTHORIZED: -32005, +} as const; + +/** + * Maps A2A error codes to appropriate HTTP status codes. + * + * @param errorCode - A2A error code (e.g., -32700, -32600, -32602, etc.) + * @returns Corresponding HTTP status code + * + * @example + * mapErrorToStatus(-32602) // returns 400 (Bad Request) + * mapErrorToStatus(-32001) // returns 404 (Not Found) + */ +export function mapErrorToStatus(errorCode: number): number { + switch (errorCode) { + case A2A_ERROR_CODE.PARSE_ERROR: + case A2A_ERROR_CODE.INVALID_REQUEST: + case A2A_ERROR_CODE.INVALID_PARAMS: + return HTTP_STATUS.BAD_REQUEST; + case A2A_ERROR_CODE.METHOD_NOT_FOUND: + case A2A_ERROR_CODE.TASK_NOT_FOUND: + return HTTP_STATUS.NOT_FOUND; + case A2A_ERROR_CODE.TASK_NOT_CANCELABLE: + return HTTP_STATUS.CONFLICT; + case A2A_ERROR_CODE.PUSH_NOTIFICATION_NOT_SUPPORTED: + case A2A_ERROR_CODE.UNSUPPORTED_OPERATION: + return HTTP_STATUS.BAD_REQUEST; + case A2A_ERROR_CODE.UNAUTHORIZED: + return HTTP_STATUS.UNAUTHORIZED; + default: + return HTTP_STATUS.INTERNAL_SERVER_ERROR; + } +} + +// ============================================================================ +// HTTP Error Conversion +// ============================================================================ + +/** + * Converts an A2AError to HTTP+JSON transport format. + * This conversion is private to the HTTP transport layer - errors are currently + * tied to JSON-RPC format in A2AError, but for HTTP transport we need a simpler + * format without the JSON-RPC wrapper. + * + * @param error - The A2AError to convert + * @returns Error object with code, message, and optional data + */ +export function toHTTPError(error: A2AError): { + code: number; + message: string; + data?: Record; +} { + const errorObject: { code: number; message: string; data?: Record } = { + code: error.code, + message: error.message, + }; + + if (error.data !== undefined) { + errorObject.data = error.data; + } + + return errorObject; +} + +// ============================================================================ +// REST Transport Handler Class +// ============================================================================ + +/** + * Handles REST transport layer, routing requests to A2ARequestHandler. + * Performs type conversion, validation, and capability checks. + * Similar to JsonRpcTransportHandler but for HTTP+JSON (REST) protocol. + * + * Accepts both snake_case and camelCase inputs. + * Outputs camelCase for spec compliance. + */ +export class RestTransportHandler { + private requestHandler: A2ARequestHandler; + + constructor(requestHandler: A2ARequestHandler) { + this.requestHandler = requestHandler; + } + + // ========================================================================== + // Public API Methods + // ========================================================================== + + /** + * Gets the agent card (for capability checks). + */ + async getAgentCard(): Promise { + return this.requestHandler.getAgentCard(); + } + + /** + * Gets the authenticated extended agent card. + */ + async getAuthenticatedExtendedAgentCard(): Promise { + return this.requestHandler.getAuthenticatedExtendedAgentCard(); + } + + /** + * Sends a message to the agent. + * Accepts both snake_case and camelCase input, returns camelCase. + */ + async sendMessage( + params: MessageSendParamsInput, + context: ServerCallContext + ): Promise { + const normalized = this.normalizeMessageParams(params); + return this.requestHandler.sendMessage(normalized, context); + } + + /** + * Sends a message with streaming response. + * Accepts both snake_case and camelCase input, returns camelCase stream. + * @throws {A2AError} UnsupportedOperation if streaming not supported + */ + async sendMessageStream( + params: MessageSendParamsInput, + context: ServerCallContext + ): Promise< + AsyncGenerator< + Message | Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent, + void, + undefined + > + > { + await this.requireCapability('streaming'); + const normalized = this.normalizeMessageParams(params); + return this.requestHandler.sendMessageStream(normalized, context); + } + + /** + * Gets a task by ID. + * Validates historyLength parameter if provided. + */ + async getTask( + taskId: string, + context: ServerCallContext, + historyLength?: unknown + ): Promise { + const params: TaskQueryParams = { id: taskId }; + if (historyLength !== undefined) { + params.historyLength = this.parseHistoryLength(historyLength); + } + return this.requestHandler.getTask(params, context); + } + + /** + * Cancels a task. + */ + async cancelTask(taskId: string, context: ServerCallContext): Promise { + const params: TaskIdParams = { id: taskId }; + return this.requestHandler.cancelTask(params, context); + } + + /** + * Resubscribes to task updates. + * Returns camelCase stream of task updates. + * @throws {A2AError} UnsupportedOperation if streaming not supported + */ + async resubscribe( + taskId: string, + context: ServerCallContext + ): Promise< + AsyncGenerator + > { + await this.requireCapability('streaming'); + const params: TaskIdParams = { id: taskId }; + return this.requestHandler.resubscribe(params, context); + } + + /** + * Sets a push notification configuration. + * Accepts both snake_case and camelCase input, returns camelCase. + * @throws {A2AError} PushNotificationNotSupported if push notifications not supported + */ + async setTaskPushNotificationConfig( + config: TaskPushNotificationConfigInput, + context: ServerCallContext + ): Promise { + await this.requireCapability('pushNotifications'); + const normalized = this.normalizeTaskPushNotificationConfig(config); + return this.requestHandler.setTaskPushNotificationConfig(normalized, context); + } + + /** + * Lists all push notification configurations for a task. + */ + async listTaskPushNotificationConfigs( + taskId: string, + context: ServerCallContext + ): Promise { + return this.requestHandler.listTaskPushNotificationConfigs({ id: taskId }, context); + } + + /** + * Gets a specific push notification configuration. + */ + async getTaskPushNotificationConfig( + taskId: string, + configId: string, + context: ServerCallContext + ): Promise { + return this.requestHandler.getTaskPushNotificationConfig( + { id: taskId, pushNotificationConfigId: configId }, + context + ); + } + + /** + * Deletes a push notification configuration. + */ + async deleteTaskPushNotificationConfig( + taskId: string, + configId: string, + context: ServerCallContext + ): Promise { + await this.requestHandler.deleteTaskPushNotificationConfig( + { id: taskId, pushNotificationConfigId: configId }, + context + ); + } + + // ========================================================================== + // Private Transformation Methods + // ========================================================================== + // All type conversion between REST (snake_case) and internal (camelCase) formats + + /** + * Validates and normalizes message parameters. + * Accepts both snake_case and camelCase input. + * @throws {A2AError} InvalidParams if message is missing or conversion fails + */ + private normalizeMessageParams(input: MessageSendParamsInput): MessageSendParams { + if (!input.message) { + throw A2AError.invalidParams('message is required'); + } + + try { + return this.normalizeMessageSendParams(input); + } catch (error) { + if (error instanceof A2AError) throw error; + throw A2AError.invalidParams( + error instanceof Error ? error.message : 'Invalid message parameters' + ); + } + } + + /** + * Static map of capability to error for missing capabilities. + */ + private static readonly CAPABILITY_ERRORS: Record< + 'streaming' | 'pushNotifications', + () => A2AError + > = { + streaming: () => A2AError.unsupportedOperation('Agent does not support streaming'), + pushNotifications: () => A2AError.pushNotificationNotSupported(), + }; + + /** + * Validates that the agent supports a required capability. + * @throws {A2AError} UnsupportedOperation for streaming, PushNotificationNotSupported for push notifications + */ + private async requireCapability(capability: 'streaming' | 'pushNotifications'): Promise { + const agentCard = await this.getAgentCard(); + if (!agentCard.capabilities?.[capability]) { + throw RestTransportHandler.CAPABILITY_ERRORS[capability](); + } + } + + /** + * Parses and validates historyLength query parameter. + */ + private parseHistoryLength(value: unknown): number { + if (value === undefined || value === null) { + throw A2AError.invalidParams('historyLength is required'); + } + const parsed = parseInt(String(value), 10); + if (isNaN(parsed)) { + throw A2AError.invalidParams('historyLength must be a valid integer'); + } + if (parsed < 0) { + throw A2AError.invalidParams('historyLength must be non-negative'); + } + return parsed; + } + + /** + * Normalizes Part input - accepts both snake_case and camelCase for file mimeType. + */ + private normalizePart(part: PartInput): Part { + if (part.kind === 'text') return { kind: 'text', text: part.text }; + if (part.kind === 'file') { + const file = this.normalizeFile(part.file); + return { kind: 'file', file, metadata: part.metadata }; + } + return { kind: 'data', data: part.data, metadata: part.metadata }; + } + + /** + * Normalizes File input - accepts both snake_case (mime_type) and camelCase (mimeType). + */ + private normalizeFile(f: FileInput): FileWithBytes | FileWithUri { + // Access both formats via intersection cast + const file = f as FileInput & { mimeType?: string; mime_type?: string }; + const mimeType = file.mimeType ?? file.mime_type; + if ('bytes' in file) { + return { bytes: file.bytes, mimeType, name: file.name }; + } + return { uri: file.uri, mimeType, name: file.name }; + } + + /** + * Normalizes Message input - accepts both snake_case and camelCase. + */ + private normalizeMessage(input: MessageInput): Message { + // Cast to access both formats + const m = input as Message & RestMessage; + const messageId = m.messageId ?? m.message_id; + if (!messageId) { + throw A2AError.invalidParams('message.messageId is required'); + } + if (!m.parts || !Array.isArray(m.parts)) { + throw A2AError.invalidParams('message.parts must be an array'); + } + + return { + contextId: m.contextId ?? m.context_id, + extensions: m.extensions, + kind: 'message', + messageId, + metadata: m.metadata, + parts: m.parts.map((p) => this.normalizePart(p)), + referenceTaskIds: m.referenceTaskIds ?? m.reference_task_ids, + role: m.role, + taskId: m.taskId ?? m.task_id, + }; + } + + /** + * Normalizes MessageSendParams - accepts both snake_case and camelCase. + */ + private normalizeMessageSendParams(input: MessageSendParamsInput): MessageSendParams { + // Cast to access both formats + const p = input as MessageSendParams & RestMessageSendParams; + const config = p.configuration as + | (MessageSendParams['configuration'] & RestMessageSendParams['configuration']) + | undefined; + + return { + configuration: config + ? { + acceptedOutputModes: config.acceptedOutputModes ?? config.accepted_output_modes, + blocking: config.blocking, + historyLength: config.historyLength ?? config.history_length, + } + : undefined, + message: this.normalizeMessage(p.message), + metadata: p.metadata, + }; + } + + /** + * Normalizes TaskPushNotificationConfig - accepts both snake_case and camelCase. + */ + private normalizeTaskPushNotificationConfig( + input: TaskPushNotificationConfigInput + ): TaskPushNotificationConfig { + // Cast to access both formats + const c = input as TaskPushNotificationConfig & RestTaskPushNotificationConfig; + const taskId = c.taskId ?? c.task_id; + if (!taskId) { + throw A2AError.invalidParams('taskId is required'); + } + const pnConfig = c.pushNotificationConfig ?? c.push_notification_config; + if (!pnConfig) { + throw A2AError.invalidParams('pushNotificationConfig is required'); + } + + return { + pushNotificationConfig: pnConfig, + taskId, + }; + } +} diff --git a/src/server/transports/rest/rest_types.ts b/src/server/transports/rest/rest_types.ts new file mode 100644 index 00000000..9e876c50 --- /dev/null +++ b/src/server/transports/rest/rest_types.ts @@ -0,0 +1,120 @@ +/** + * REST API Types (snake_case format) + * + * These types mirror the internal types but use snake_case naming + * to support TCK and clients that send snake_case payloads. + */ + +import { + Part, + Message, + MessageSendParams, + TaskPushNotificationConfig, + FileWithBytes, + FileWithUri, +} from '../../../types.js'; + +// ============================================================================ +// REST Types (snake_case format) +// ============================================================================ + +/** + * REST file with bytes (snake_case mime_type). + */ +export interface RestFileWithBytes { + bytes: string; + mime_type?: string; + name?: string; +} + +/** + * REST file with URI (snake_case mime_type). + */ +export interface RestFileWithUri { + uri: string; + mime_type?: string; + name?: string; +} + +/** + * REST file union. + */ +export type RestFile = RestFileWithBytes | RestFileWithUri; + +/** + * File input - accepts both camelCase and snake_case. + */ +export type FileInput = FileWithBytes | FileWithUri | RestFileWithBytes | RestFileWithUri; + +/** + * REST Part with snake_case file fields. + */ +export type RestPart = + | { kind: 'text'; text: string; metadata?: Record } + | { kind: 'file'; file: RestFile; metadata?: Record } + | { kind: 'data'; data: Record; metadata?: Record }; + +/** + * REST Message with snake_case fields. + */ +export interface RestMessage { + kind: 'message'; + role: 'agent' | 'user'; + parts: RestPart[]; + message_id: string; + context_id?: string; + task_id?: string; + reference_task_ids?: string[]; + extensions?: string[]; + metadata?: Record; +} + +/** + * REST PushNotificationConfig (same as internal, no snake_case fields). + */ +export interface RestPushNotificationConfig { + id: string; + url: string; + authentication?: { + schemes: string[]; + credentials?: string; + }; +} + +/** + * REST MessageSendConfiguration with snake_case fields. + */ +export interface RestMessageSendConfiguration { + blocking?: boolean; + accepted_output_modes?: string[]; + history_length?: number; + push_notification_config?: RestPushNotificationConfig; +} + +/** + * REST MessageSendParams with snake_case configuration. + */ +export interface RestMessageSendParams { + message: RestMessage; + configuration?: RestMessageSendConfiguration; + metadata?: Record; +} + +/** + * REST TaskPushNotificationConfig with snake_case fields. + */ +export interface RestTaskPushNotificationConfig { + task_id: string; + push_notification_config: RestPushNotificationConfig; +} + +// ============================================================================ +// Input Types - Accept both camelCase and snake_case +// ============================================================================ + +export type PartInput = Part | RestPart; +export type MessageInput = Message | RestMessage; +export type MessageSendParamsInput = MessageSendParams | RestMessageSendParams; +export type TaskPushNotificationConfigInput = + | TaskPushNotificationConfig + | RestTaskPushNotificationConfig; diff --git a/src/sse_utils.ts b/src/sse_utils.ts new file mode 100644 index 00000000..8f127b9a --- /dev/null +++ b/src/sse_utils.ts @@ -0,0 +1,61 @@ +/** + * Shared Server-Sent Events (SSE) utilities for both JSON-RPC and REST transports. + * This module provides common SSE formatting functions and headers. + */ + +// ============================================================================ +// SSE Headers +// ============================================================================ + +/** + * Standard HTTP headers for Server-Sent Events (SSE) streaming responses. + * These headers ensure proper SSE behavior across different proxies and clients. + */ +export const SSE_HEADERS = { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'X-Accel-Buffering': 'no', // Disable buffering in nginx +} as const; + +// ============================================================================ +// SSE Event Formatting +// ============================================================================ + +/** + * Formats a data event for Server-Sent Events (SSE) protocol. + * Creates a standard SSE event with an ID and JSON-stringified data. + * + * @param event - The event data to send (will be JSON stringified) + * @returns Formatted SSE event string following the SSE specification + * + * @example + * ```typescript + * formatSSEEvent({ kind: 'message', text: 'Hello' }) + * // Returns: "data: {\"kind\":\"message\",\"text\":\"Hello\"}\n\n" + * + * formatSSEEvent({ result: 'success' }, 'custom-id') + * // Returns: "data: {\"result\":\"success\"}\n\n" + * ``` + */ +export function formatSSEEvent(event: unknown): string { + return `data: ${JSON.stringify(event)}\n\n`; +} + +/** + * Formats an error event for Server-Sent Events (SSE) protocol. + * Error events use the "error" event type to distinguish them from data events, + * allowing clients to handle errors differently. + * + * @param error - The error object (will be JSON stringified) + * @returns Formatted SSE error event string with custom event type + * + * @example + * ```typescript + * formatSSEErrorEvent({ code: -32603, message: 'Internal error' }) + * // Returns: "event: error\ndata: {\"code\":-32603,\"message\":\"Internal error\"}\n\n" + * ``` + */ +export function formatSSEErrorEvent(error: unknown): string { + return `event: error\ndata: ${JSON.stringify(error)}\n\n`; +} diff --git a/tck/agent/index.ts b/tck/agent/index.ts index f0abb195..26f51a48 100644 --- a/tck/agent/index.ts +++ b/tck/agent/index.ts @@ -10,7 +10,12 @@ import { ExecutionEventBus, DefaultRequestHandler, } from '../../src/server/index.js'; -import { A2AExpressApp } from '../../src/server/express/index.js'; +import { + jsonRpcHandler, + agentCardHandler, + httpRestHandler, + UserBuilder, +} from '../../src/server/express/index.js'; /** * SUTAgentExecutor implements the agent's core logic. @@ -24,7 +29,7 @@ class SUTAgentExecutor implements AgentExecutor { const cancelledUpdate: TaskStatusUpdateEvent = { kind: 'status-update', taskId: taskId, - contextId: this.lastContextId, + contextId: this.lastContextId ?? uuidv4(), status: { state: 'canceled', timestamp: new Date().toISOString(), @@ -146,8 +151,8 @@ class SUTAgentExecutor implements AgentExecutor { const SUTAgentCard: AgentCard = { name: 'SUT Agent', description: 'A sample agent to be used as SUT against tck tests.', - // Adjust the base URL and port as needed. /a2a is the default base in A2AExpressApp - url: 'http://localhost:41241/', + // Main URL points to JSON-RPC endpoint (preferred transport) + url: 'http://localhost:41241/a2a/jsonrpc', provider: { organization: 'A2A Samples', url: 'https://example.com/a2a-samples', // Added provider URL @@ -174,7 +179,11 @@ const SUTAgentCard: AgentCard = { ], supportsAuthenticatedExtendedCard: false, preferredTransport: 'JSONRPC', - additionalInterfaces: [{ url: 'http://localhost:41241', transport: 'JSONRPC' }], + // All supported transports (including preferred) + additionalInterfaces: [ + { url: 'http://localhost:41241/a2a/jsonrpc', transport: 'JSONRPC' }, + { url: 'http://localhost:41241/a2a/rest', transport: 'HTTP+JSON' }, + ], }; async function main() { @@ -187,9 +196,26 @@ async function main() { // 3. Create DefaultRequestHandler const requestHandler = new DefaultRequestHandler(SUTAgentCard, taskStore, agentExecutor); - // 4. Create and setup A2AExpressApp - const appBuilder = new A2AExpressApp(requestHandler); - const expressApp = appBuilder.setupRoutes(express()); + // 4. Setup Express app with modular handlers + const expressApp = express(); + + // Register agent card handler at well-known location (shared by all transports) + expressApp.use( + '/.well-known/agent-card.json', + agentCardHandler({ agentCardProvider: requestHandler }) + ); + + // Register JSON-RPC handler (preferred transport, backward compatible) + expressApp.use( + '/a2a/jsonrpc', + jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }) + ); + + // Register HTTP+REST handler (new feature - additional transport) + expressApp.use( + '/a2a/rest', + httpRestHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }) + ); // 5. Start the server const PORT = process.env.PORT || 41241; diff --git a/test/server/a2a_express_app.spec.ts b/test/server/a2a_express_app.spec.ts index 37ae1f6c..8f2cdd1b 100644 --- a/test/server/a2a_express_app.spec.ts +++ b/test/server/a2a_express_app.spec.ts @@ -6,7 +6,7 @@ import request from 'supertest'; import { A2AExpressApp } from '../../src/server/express/a2a_express_app.js'; import { A2ARequestHandler } from '../../src/server/request_handler/a2a_request_handler.js'; -import { JsonRpcTransportHandler } from '../../src/server/transports/jsonrpc_transport_handler.js'; +import { JsonRpcTransportHandler } from '../../src/server/transports/jsonrpc/jsonrpc_transport_handler.js'; import { AgentCard, JSONRPCSuccessResponse, JSONRPCErrorResponse } from '../../src/index.js'; import { AGENT_CARD_PATH, HTTP_EXTENSION_HEADER } from '../../src/constants.js'; import { A2AError } from '../../src/server/error.js'; diff --git a/test/server/http_rest_handler.spec.ts b/test/server/http_rest_handler.spec.ts new file mode 100644 index 00000000..e0548815 --- /dev/null +++ b/test/server/http_rest_handler.spec.ts @@ -0,0 +1,595 @@ +import 'mocha'; +import { assert } from 'chai'; +import sinon, { SinonStub } from 'sinon'; +import express, { Express } from 'express'; +import request from 'supertest'; + +import { httpRestHandler, UserBuilder } from '../../src/server/express/index.js'; +import { A2ARequestHandler } from '../../src/server/request_handler/a2a_request_handler.js'; +import { AgentCard, Task, Message } from '../../src/types.js'; +import { A2AError } from '../../src/server/error.js'; + +/** + * Test suite for httpRestHandler - HTTP+REST transport implementation + * + * This suite tests the REST API endpoints following the A2A specification: + * - GET /v1/card - Agent card retrieval + * - POST /v1/message:send - Send message (non-streaming) + * - POST /v1/message:stream - Send message with SSE streaming + * - GET /v1/tasks/:taskId - Get task status + * - POST /v1/tasks/:taskId:cancel - Cancel task + * - POST /v1/tasks/:taskId:subscribe - Resubscribe to task updates + * - Push notification config CRUD operations + */ +describe('httpRestHandler', () => { + let mockRequestHandler: A2ARequestHandler; + let app: Express; + + const testAgentCard: AgentCard = { + protocolVersion: '0.3.0', + name: 'Test Agent', + description: 'An agent for testing purposes', + url: 'http://localhost:8080', + preferredTransport: 'HTTP+JSON', + version: '1.0.0', + capabilities: { + streaming: true, + pushNotifications: true, + }, + defaultInputModes: ['text/plain'], + defaultOutputModes: ['text/plain'], + skills: [], + }; + + // camelCase format (internal type) + const testMessage: Message = { + messageId: 'msg-1', + role: 'user' as const, + parts: [{ kind: 'text' as const, text: 'Hello' }], + kind: 'message' as const, + }; + + // snake_case format (REST/TCK style input) + const snakeCaseMessage = { + message_id: 'msg-1', + role: 'user' as const, + parts: [{ kind: 'text' as const, text: 'Hello' }], + kind: 'message' as const, + }; + + const testTask: Task = { + id: 'task-1', + kind: 'task' as const, + status: { state: 'completed' as const }, + contextId: 'ctx-1', + history: [], + }; + + beforeEach(() => { + mockRequestHandler = { + getAgentCard: sinon.stub().resolves(testAgentCard), + getAuthenticatedExtendedAgentCard: sinon.stub().resolves(testAgentCard), + sendMessage: sinon.stub(), + sendMessageStream: sinon.stub(), + getTask: sinon.stub(), + cancelTask: sinon.stub(), + setTaskPushNotificationConfig: sinon.stub(), + getTaskPushNotificationConfig: sinon.stub(), + listTaskPushNotificationConfigs: sinon.stub(), + deleteTaskPushNotificationConfig: sinon.stub(), + resubscribe: sinon.stub(), + }; + + app = express(); + app.use( + httpRestHandler({ + requestHandler: mockRequestHandler, + userBuilder: UserBuilder.noAuthentication, + }) + ); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('GET /v1/card', () => { + it('should return the agent card with 200 OK', async () => { + const response = await request(app).get('/v1/card').expect(200); + + // REST API returns data (format checked by handler) + assert.isTrue((mockRequestHandler.getAuthenticatedExtendedAgentCard as SinonStub).calledOnce); + assert.deepEqual(response.body.name, testAgentCard.name); + }); + + it('should return 500 if getAuthenticatedExtendedAgentCard fails', async () => { + (mockRequestHandler.getAuthenticatedExtendedAgentCard as SinonStub).rejects( + A2AError.internalError('Card fetch failed') + ); + + const response = await request(app).get('/v1/card').expect(500); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + }); + }); + + describe('POST /v1/message:send', () => { + it('should accept camelCase message and return 201 with Task', async () => { + (mockRequestHandler.sendMessage as SinonStub).resolves(testTask); + + const response = await request(app) + .post('/v1/message:send') + .send({ message: testMessage }) + .expect(201); + + assert.deepEqual(response.body.id, testTask.id); + assert.deepEqual(response.body.kind, 'task'); + }); + + it('should accept snake_case message and return 201 with Task', async () => { + (mockRequestHandler.sendMessage as SinonStub).resolves(testTask); + + const response = await request(app) + .post('/v1/message:send') + .send({ message: snakeCaseMessage }) + .expect(201); + + assert.deepEqual(response.body.id, testTask.id); + assert.deepEqual(response.body.kind, 'task'); + }); + + it('should return camelCase response regardless of input format', async () => { + (mockRequestHandler.sendMessage as SinonStub).resolves(testTask); + + const response = await request(app) + .post('/v1/message:send') + .send({ message: snakeCaseMessage }) + .expect(201); + + // Response must be camelCase only + assert.property(response.body, 'contextId'); + assert.notProperty(response.body, 'context_id'); + }); + + it('should return 400 when message is invalid', async () => { + (mockRequestHandler.sendMessage as SinonStub).rejects( + A2AError.invalidParams('Message is required') + ); + + await request(app).post('/v1/message:send').send({ message: null }).expect(400); + }); + }); + + describe('POST /v1/message:stream', () => { + it('should accept camelCase message and stream via SSE', async () => { + async function* mockStream() { + yield testMessage; + yield testTask; + } + (mockRequestHandler.sendMessageStream as SinonStub).resolves(mockStream()); + + const response = await request(app) + .post('/v1/message:stream') + .send({ message: testMessage }) + .expect(200); + + assert.equal(response.headers['content-type'], 'text/event-stream'); + }); + + it('should accept snake_case message and stream via SSE', async () => { + async function* mockStream() { + yield testMessage; + } + (mockRequestHandler.sendMessageStream as SinonStub).resolves(mockStream()); + + const response = await request(app) + .post('/v1/message:stream') + .send({ message: snakeCaseMessage }) + .expect(200); + + assert.equal(response.headers['content-type'], 'text/event-stream'); + }); + + it('should return 400 if streaming is not supported', async () => { + const noStreamRequestHandler = { + ...mockRequestHandler, + getAgentCard: sinon.stub().resolves({ + ...testAgentCard, + capabilities: { streaming: false, pushNotifications: false }, + }), + }; + const noStreamApp = express(); + noStreamApp.use( + httpRestHandler({ + requestHandler: noStreamRequestHandler as any, + userBuilder: UserBuilder.noAuthentication, + }) + ); + + await request(noStreamApp) + .post('/v1/message:stream') + .send({ message: testMessage }) + .expect(400); + }); + }); + + describe('GET /v1/tasks/:taskId', () => { + it('should return task with 200 OK', async () => { + (mockRequestHandler.getTask as SinonStub).resolves(testTask); + + const response = await request(app).get('/v1/tasks/task-1').expect(200); + + assert.deepEqual(response.body.id, testTask.id); + assert.deepEqual(response.body.kind, 'task'); + assert.isTrue((mockRequestHandler.getTask as SinonStub).calledWith({ id: 'task-1' })); + }); + + it('should support historyLength query parameter', async () => { + (mockRequestHandler.getTask as SinonStub).resolves(testTask); + + await request(app).get('/v1/tasks/task-1?historyLength=10').expect(200); + + assert.isTrue( + (mockRequestHandler.getTask as SinonStub).calledWith({ + id: 'task-1', + historyLength: 10, + }) + ); + }); + + it('should return 400 if historyLength is invalid', async () => { + await request(app).get('/v1/tasks/task-1?historyLength=invalid').expect(400); + }); + + it('should return 404 if task is not found', async () => { + (mockRequestHandler.getTask as SinonStub).rejects(A2AError.taskNotFound('task-1')); + + const response = await request(app).get('/v1/tasks/task-1').expect(404); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + }); + }); + + describe('POST /v1/tasks/:taskId:cancel', () => { + it('should cancel task and return 202 Accepted', async () => { + const cancelledTask = { ...testTask, status: { state: 'canceled' as const } }; + (mockRequestHandler.cancelTask as SinonStub).resolves(cancelledTask); + + const response = await request(app).post('/v1/tasks/task-1:cancel').expect(202); + + assert.deepEqual(response.body.id, cancelledTask.id); + assert.deepEqual(response.body.status.state, 'canceled'); + assert.isTrue((mockRequestHandler.cancelTask as SinonStub).calledWith({ id: 'task-1' })); + }); + + it('should return 404 if task is not found', async () => { + (mockRequestHandler.cancelTask as SinonStub).rejects(A2AError.taskNotFound('task-1')); + + const response = await request(app).post('/v1/tasks/task-1:cancel').expect(404); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + }); + + it('should return 409 if task is not cancelable', async () => { + (mockRequestHandler.cancelTask as SinonStub).rejects(A2AError.taskNotCancelable('task-1')); + + const response = await request(app).post('/v1/tasks/task-1:cancel').expect(409); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + }); + }); + + describe('POST /v1/tasks/:taskId:subscribe', () => { + it('should resubscribe to task updates via SSE', async () => { + async function* mockStream() { + yield testTask; + } + + (mockRequestHandler.resubscribe as SinonStub).resolves(mockStream()); + + const response = await request(app).post('/v1/tasks/task-1:subscribe').expect(200); + + assert.equal(response.headers['content-type'], 'text/event-stream'); + assert.isTrue((mockRequestHandler.resubscribe as SinonStub).calledWith({ id: 'task-1' })); + }); + + it('should return 400 if streaming is not supported', async () => { + // Create new app with handler that has capabilities without streaming + const noStreamRequestHandler = { + ...mockRequestHandler, + getAgentCard: sinon.stub().resolves({ + ...testAgentCard, + capabilities: { streaming: false, pushNotifications: false }, + }), + }; + const noStreamApp = express(); + noStreamApp.use( + httpRestHandler({ + requestHandler: noStreamRequestHandler as any, + userBuilder: UserBuilder.noAuthentication, + }) + ); + + const response = await request(noStreamApp).post('/v1/tasks/task-1:subscribe').expect(400); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + }); + }); + + describe('Push Notification Config Endpoints', () => { + const mockConfig = { + taskId: 'task-1', + pushNotificationConfig: { + id: 'config-1', + url: 'https://example.com/webhook', + }, + }; + + describe('POST /v1/tasks/:taskId/pushNotificationConfigs', () => { + it('should accept camelCase pushNotificationConfig and return 201', async () => { + (mockRequestHandler.setTaskPushNotificationConfig as SinonStub).resolves(mockConfig); + + const response = await request(app) + .post('/v1/tasks/task-1/pushNotificationConfigs') + .send({ + pushNotificationConfig: { + id: 'config-1', + url: 'https://example.com/webhook', + }, + }) + .expect(201); + + assert.deepEqual(response.body.taskId, mockConfig.taskId); + }); + + it('should accept snake_case push_notification_config and return 201', async () => { + (mockRequestHandler.setTaskPushNotificationConfig as SinonStub).resolves(mockConfig); + + const response = await request(app) + .post('/v1/tasks/task-1/pushNotificationConfigs') + .send({ + push_notification_config: { + id: 'config-1', + url: 'https://example.com/webhook', + }, + }) + .expect(201); + + assert.deepEqual(response.body.taskId, mockConfig.taskId); + }); + + it('should return camelCase response regardless of input format', async () => { + (mockRequestHandler.setTaskPushNotificationConfig as SinonStub).resolves(mockConfig); + + const response = await request(app) + .post('/v1/tasks/task-1/pushNotificationConfigs') + .send({ + push_notification_config: { id: 'config-1', url: 'https://example.com/webhook' }, + }) + .expect(201); + + // Response must be camelCase only + assert.property(response.body, 'taskId'); + assert.property(response.body, 'pushNotificationConfig'); + assert.notProperty(response.body, 'task_id'); + assert.notProperty(response.body, 'push_notification_config'); + }); + + it('should return 400 if push notifications not supported', async () => { + const noPNRequestHandler = { + ...mockRequestHandler, + getAgentCard: sinon.stub().resolves({ + ...testAgentCard, + capabilities: { streaming: false, pushNotifications: false }, + }), + }; + const noPNApp = express(); + noPNApp.use( + httpRestHandler({ + requestHandler: noPNRequestHandler as any, + userBuilder: UserBuilder.noAuthentication, + }) + ); + + await request(noPNApp) + .post('/v1/tasks/task-1/pushNotificationConfigs') + .send({ pushNotificationConfig: { id: 'config-1', url: 'https://example.com/webhook' } }) + .expect(400); + }); + }); + + describe('GET /v1/tasks/:taskId/pushNotificationConfigs', () => { + it('should list push notification configs and return 200', async () => { + const configs = [mockConfig]; + (mockRequestHandler.listTaskPushNotificationConfigs as SinonStub).resolves(configs); + + const response = await request(app) + .get('/v1/tasks/task-1/pushNotificationConfigs') + .expect(200); + + assert.isArray(response.body); + assert.lengthOf(response.body, configs.length); + }); + }); + + describe('GET /v1/tasks/:taskId/pushNotificationConfigs/:configId', () => { + it('should get specific push notification config and return 200', async () => { + (mockRequestHandler.getTaskPushNotificationConfig as SinonStub).resolves(mockConfig); + + const response = await request(app) + .get('/v1/tasks/task-1/pushNotificationConfigs/config-1') + .expect(200); + + // REST API returns camelCase + assert.deepEqual(response.body.taskId, mockConfig.taskId); + assert.isTrue( + (mockRequestHandler.getTaskPushNotificationConfig as SinonStub).calledWith({ + id: 'task-1', + pushNotificationConfigId: 'config-1', + }) + ); + }); + + it('should return 404 if config not found', async () => { + (mockRequestHandler.getTaskPushNotificationConfig as SinonStub).rejects( + A2AError.taskNotFound('task-1') + ); + + const response = await request(app) + .get('/v1/tasks/task-1/pushNotificationConfigs/config-1') + .expect(404); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + }); + }); + + describe('DELETE /v1/tasks/:taskId/pushNotificationConfigs/:configId', () => { + it('should delete push notification config and return 204', async () => { + (mockRequestHandler.deleteTaskPushNotificationConfig as SinonStub).resolves(); + + await request(app).delete('/v1/tasks/task-1/pushNotificationConfigs/config-1').expect(204); + + assert.isTrue( + (mockRequestHandler.deleteTaskPushNotificationConfig as SinonStub).calledWith({ + id: 'task-1', + pushNotificationConfigId: 'config-1', + }) + ); + }); + + it('should return 404 if config not found', async () => { + (mockRequestHandler.deleteTaskPushNotificationConfig as SinonStub).rejects( + A2AError.taskNotFound('task-1') + ); + + const response = await request(app) + .delete('/v1/tasks/task-1/pushNotificationConfigs/config-1') + .expect(404); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + }); + }); + }); + + /** + * File Parts Format Tests + */ + describe('File parts format acceptance', () => { + it('should accept camelCase mimeType in file parts', async () => { + (mockRequestHandler.sendMessage as SinonStub).resolves(testTask); + + await request(app) + .post('/v1/message:send') + .send({ + message: { + messageId: 'msg-file', + role: 'user', + kind: 'message', + parts: [ + { + kind: 'file', + file: { + uri: 'https://example.com/file.pdf', + mimeType: 'application/pdf', + name: 'document.pdf', + }, + }, + ], + }, + }) + .expect(201); + }); + + it('should accept snake_case mime_type in file parts', async () => { + (mockRequestHandler.sendMessage as SinonStub).resolves(testTask); + + await request(app) + .post('/v1/message:send') + .send({ + message: { + message_id: 'msg-file', + role: 'user', + kind: 'message', + parts: [ + { + kind: 'file', + file: { + uri: 'https://example.com/file.pdf', + mime_type: 'application/pdf', + name: 'document.pdf', + }, + }, + ], + }, + }) + .expect(201); + }); + }); + + /** + * Configuration Format Tests + */ + describe('Configuration format acceptance', () => { + it('should accept camelCase configuration fields', async () => { + (mockRequestHandler.sendMessage as SinonStub).resolves(testTask); + + await request(app) + .post('/v1/message:send') + .send({ + message: testMessage, + configuration: { + acceptedOutputModes: ['text/plain'], + historyLength: 5, + }, + }) + .expect(201); + }); + + it('should accept snake_case configuration fields', async () => { + (mockRequestHandler.sendMessage as SinonStub).resolves(testTask); + + await request(app) + .post('/v1/message:send') + .send({ + message: snakeCaseMessage, + configuration: { + accepted_output_modes: ['text/plain'], + history_length: 5, + }, + }) + .expect(201); + }); + }); + + describe('Error Handling', () => { + it('should return 404 for unknown message action (route not matched)', async () => { + // Unknown actions don't match the route pattern, so Express returns default 404 + await request(app).post('/v1/message:unknown').send({ message: testMessage }).expect(404); + }); + + it('should return 404 for unknown task action (route not matched)', async () => { + // Unknown actions don't match the route pattern, so Express returns default 404 + await request(app).post('/v1/tasks/task-1:unknown').expect(404); + }); + + it('should handle internal server errors gracefully', async () => { + (mockRequestHandler.sendMessage as SinonStub).rejects(new Error('Unexpected internal error')); + + const response = await request(app) + .post('/v1/message:send') + .send({ message: snakeCaseMessage }) + .expect(500); + + assert.property(response.body, 'code'); + assert.property(response.body, 'message'); + assert.deepEqual(response.body.code, -32603); // Internal error code + }); + }); +}); diff --git a/test/server/jsonrpc_transport_handler.spec.ts b/test/server/jsonrpc_transport_handler.spec.ts index 738fed30..04a81096 100644 --- a/test/server/jsonrpc_transport_handler.spec.ts +++ b/test/server/jsonrpc_transport_handler.spec.ts @@ -2,7 +2,7 @@ import 'mocha'; import { expect } from 'chai'; import sinon from 'sinon'; -import { JsonRpcTransportHandler } from '../../src/server/transports/jsonrpc_transport_handler.js'; +import { JsonRpcTransportHandler } from '../../src/server/transports/jsonrpc/jsonrpc_transport_handler.js'; import { A2ARequestHandler } from '../../src/server/request_handler/a2a_request_handler.js'; import { JSONRPCErrorResponse, JSONRPCRequest } from '../../src/index.js';