diff --git a/package-lock.json b/package-lock.json index c48e1de..42aeb9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2223,74 +2223,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.11", "cpu": [ @@ -2306,363 +2238,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "license": "MIT", @@ -3525,27 +3100,6 @@ "@parcel/watcher-win32-x64": "2.5.1" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.1", "cpu": [ @@ -3565,237 +3119,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", diff --git a/server/index.ts b/server/index.ts index 6606feb..6a49638 100644 --- a/server/index.ts +++ b/server/index.ts @@ -438,6 +438,42 @@ app.post('/api/endpoint/:endpointId/hangup', async (req: Request, res: Response) } }); +// GET /bxml/hangup - Returns BXML that hangs up the call +app.get('/bxml/hangup', (req: Request, res: Response) => { + res.type('application/xml').send(` + + +`); +}); + +// POST /api/endpoint/:endpointId/bxml-hangup - Redirect active PSTN call to BXML Hangup +app.post('/api/endpoint/:endpointId/bxml-hangup', async (req: Request, res: Response) => { + const endpointId = req.params.endpointId; + const callStatus = endpointCallStatusMap.get(endpointId); + if (!callStatus) { + return res.status(404).json({ error: 'No active call for this endpoint' }); + } + + try { + const token = await getAuthToken(); + const configuration = new Configuration({ accessToken: token }); + if (VOICE_URL !== PROD_VOICE_URL) { + configuration.basePath = VOICE_URL; + } + + const callsApi = new CallsApi(configuration); + await callsApi.updateCall(ACCOUNT_ID, callStatus.callId, { + state: 'active', + redirectUrl: `${CALLBACK_BASE_URL}/bxml/hangup`, + }); + console.log(`Redirected call ${callStatus.callId} to BXML hangup for endpoint ${endpointId}`); + res.sendStatus(200); + } catch (error: any) { + console.error(`Error sending BXML hangup for endpoint ${endpointId}:`, error.message); + res.status(500).json({ error: error.message }); + } +}); + // DELETE /api/endpoints - Delete all endpoints on the account app.delete('/api/endpoints', async (req: Request, res: Response) => { try { @@ -497,7 +533,9 @@ app.listen(PORT, '0.0.0.0', () => { console.log(` GET /token - Create endpoint and get JWT`); console.log(` DELETE /api/endpoint/:endpointId - Delete endpoint`); console.log(` GET /api/endpoint/:endpointId/call-status - Get PSTN call status`); - console.log(` POST /api/endpoint/:endpointId/hangup - Hang up PSTN leg`); + console.log(` POST /api/endpoint/:endpointId/hangup - Hang up PSTN leg (Voice API)`); + console.log(` POST /api/endpoint/:endpointId/bxml-hangup - Hang up via BXML redirect`); + console.log(` GET /bxml/hangup - BXML Hangup verb`); console.log(` POST /callbacks/bandwidth - BRTC events + incoming PSTN calls`); console.log(` POST /callbacks/bandwidth/status - Voice API status (disconnect)`); console.log(` POST /calls/answer - Outbound call answer BXML callback`); diff --git a/src/App.tsx b/src/App.tsx index 5461f54..41299b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import './css/App.scss'; import Navbar from "./components/Navbar"; import EndpointHandler from "./components/EndpointHandler"; @@ -13,6 +13,17 @@ function App() { const [brtcClientReady, setBrtcClientReady] = useState(false); const [readyMetadata, setReadyMetadata] = useState(null); const [inCall, setInCall] = useState(false); + const [incomingCallId, setIncomingCallId] = useState(null); + const [inboundStream, setInboundStream] = useState(null); + const [autoAccept, setAutoAccept] = useState(true); + const autoAcceptRef = useRef(autoAccept); + useEffect(() => { autoAcceptRef.current = autoAccept; }, [autoAccept]); + // true once the WS streamAvailable notification arrives; gates in-call state + const callExpectedRef = useRef(false); + // holds the subscribe-peer MediaStream from WebRTC ontrack, which fires once + // at connection time before any call arrives + const subscribeStreamRef = useRef(null); + const [endpointId, setEndpointId] = useState(null); const prepBrtcClient= async (reset: boolean) => { console.log("Prepping Bandwidth RTC Client") @@ -38,11 +49,86 @@ function App() { prepBrtcClient(false) }, []); + useEffect(() => { + if (!inCall) { + setInboundStream(null); + } + }, [inCall]); + + useEffect(() => { + if (!brtcClient) return; + brtcClient.onStreamAvailable((s) => { + console.log("Stream available:", s); + if (s.callId && s.mediaStream) { + // New SDK: combined event — callId and mediaStream arrive together. + if (s.autoAccepted === true || autoAcceptRef.current) { + if (s.autoAccepted !== true) { + brtcClient.acceptStream(s.callId); + } + setInCall(true); + setInboundStream(s.mediaStream); + } else { + subscribeStreamRef.current = s.mediaStream; + setIncomingCallId(s.callId); + } + } else if (s.callId && !s.mediaStream) { + // Old SDK two-phase: WS notification arrives before ontrack. + callExpectedRef.current = true; + if (s.autoAccepted === true) { + setInCall(true); + if (subscribeStreamRef.current) { + setInboundStream(subscribeStreamRef.current); + } + } else if (autoAcceptRef.current) { + brtcClient.acceptStream(s.callId); + setInCall(true); + if (subscribeStreamRef.current) { + setInboundStream(subscribeStreamRef.current); + } + } else { + setIncomingCallId(s.callId); + } + } else if (s.mediaStream) { + // Old SDK: ontrack fires with stream (no callId). + subscribeStreamRef.current = s.mediaStream; + if (callExpectedRef.current) { + setInCall(true); + setInboundStream(s.mediaStream); + } + } + }); + brtcClient.onStreamUnavailable((s) => { + console.log("Stream unavailable:", s); + callExpectedRef.current = false; + // subscribeStreamRef is intentionally kept: the subscribe peer's + // MediaStream is permanent for the lifetime of the WS connection; + // ontrack only fires once so we need it for consecutive calls. + setInCall(false); + setIncomingCallId(null); + setInboundStream(null); + }); + }, [brtcClient]); + + const handleAccept = async () => { + if (!brtcClient) return; + await brtcClient.acceptStream(incomingCallId ?? undefined); + setInCall(true); + setIncomingCallId(null); + if (subscribeStreamRef.current) { + setInboundStream(subscribeStreamRef.current); + } + }; + + const handleDecline = async () => { + if (!brtcClient) return; + await brtcClient.declineStream(incomingCallId ?? undefined); + setIncomingCallId(null); + }; + const resetClient = async () => { await prepBrtcClient(true) } - // Fetch the WebSocket URL from env const gatewayUrl = process.env.REACT_APP_WSS_URL; return ( @@ -50,18 +136,48 @@ function App() { {brtcClient && ( <> - +
{brtcClientReady} + {incomingCallId && ( +
+ Incoming call +
+ + +
+
+ )} {readyMetadata && ( <>

Bandwidth RTC Agent Sample

- +
- +
)} diff --git a/src/components/CallController.tsx b/src/components/CallController.tsx index 0d62757..3f03e49 100644 --- a/src/components/CallController.tsx +++ b/src/components/CallController.tsx @@ -70,7 +70,7 @@ function isDestinationValid(destination: string): boolean { return true } -function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: {bandwidthRtcClient: BandwidthRtc, readyMetadata: ReadyMetadata, inCall: boolean, setInCall: (inCall: boolean) => void} ) { +function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall, endpointId}: {bandwidthRtcClient: BandwidthRtc, readyMetadata: ReadyMetadata, inCall: boolean, setInCall: (inCall: boolean) => void, endpointId?: string | null} ) { if (!bandwidthRtcClient) { throw new Error("webrtcClient is required"); } @@ -112,12 +112,17 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}: } const handleHangUp = async () => { setCallStatus('Hanging Up...'); - // Ensure E.164 format: clean digits and add '+' const e164Number = '+' + destNumber.replace(/[^\d]/g, ''); - let result = await bandwidthRtcClient.hangupConnection(e164Number, EndpointType.PHONE_NUMBER) - setCallStatus(result.result) + await bandwidthRtcClient.hangupConnection(e164Number, EndpointType.PHONE_NUMBER); + setInCall(false); + setCallStatus('Call ended'); } + const handleBxmlHangup = async () => { + if (!endpointId) return; + await fetch(`/api/endpoint/${endpointId}/bxml-hangup`, { method: 'POST' }); + }; + const handleDigitClick = (value: string) => { if (inCall) { console.log(`Sending DTMF: ${value}, duration: ${dtmfDuration}ms`); @@ -237,6 +242,13 @@ function CallController({bandwidthRtcClient, readyMetadata, inCall, setInCall}:
{!inCall ? : }
+ {inCall && ( +
+ +
+ )} ); diff --git a/src/components/EndpointHandler.tsx b/src/components/EndpointHandler.tsx index bdfd09c..0b906a4 100644 --- a/src/components/EndpointHandler.tsx +++ b/src/components/EndpointHandler.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from "react"; import BandwidthRtc from "bandwidth-rtc"; import {Endpoint} from "../../server/types" -function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwidthRtcClient: BandwidthRtc, resetClient: () => void, gatewayUrl?: string }) { +function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl, onEndpointChange, autoAccept, setAutoAccept}: {bandwidthRtcClient: BandwidthRtc, resetClient: () => void, gatewayUrl?: string, onEndpointChange?: (endpointId: string | null) => void, autoAccept?: boolean, setAutoAccept?: (v: boolean) => void }) { const [endpoint, setEndpoint] = useState(null); const [banner, setBanner] = useState<{ message: string; isError: boolean } | null>(null); @@ -19,10 +19,12 @@ function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwid throw new Error("Failed to create endpoint") } setEndpoint(endpointData) + onEndpointChange?.(endpointData.endpointId) await bandwidthRtcClient.connect({ endpointToken: endpointData.token }, { - websocketUrl: gatewayUrl + websocketUrl: gatewayUrl, + autoAccept }).then(() => { console.log("WebRTC Client Connected"); }).catch((error) => { @@ -42,6 +44,7 @@ function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwid bandwidthRtcClient.disconnect(); resetClient(); setEndpoint(null); + onEndpointChange?.(null); setBanner({ message: 'Endpoints all deleted', isError: false }); setTimeout(() => setBanner(prev => prev && !prev.isError ? null : prev), 4000); } catch (err: any) { @@ -53,13 +56,8 @@ function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwid if (endpoint) { bandwidthRtcClient.disconnect(); resetClient(); // Reset for now until reconnection logic is fixed (init rebinding in bandwidthRtcClient.connect(...)) - let endpointData = await fetch(`/api/endpoint/${endpoint.endpointId}`, { method: "DELETE" }) - .then(res => res.json()) - .then(data => data as Endpoint) - .catch((err) => { - console.error(err); - return null; - }) as Endpoint | null; + onEndpointChange?.(null); + await fetch(`/api/endpoint/${endpoint.endpointId}`, { method: "DELETE" }).catch(console.error); setEndpoint(null) } } @@ -82,6 +80,16 @@ function EndpointHandler({bandwidthRtcClient, resetClient, gatewayUrl}: {bandwid )} + diff --git a/src/components/MediaPlayer.tsx b/src/components/MediaPlayer.tsx index 9e2afae..a9ea170 100644 --- a/src/components/MediaPlayer.tsx +++ b/src/components/MediaPlayer.tsx @@ -1,90 +1,70 @@ -import React, {useEffect, useRef, useState} from "react"; -import BandwidthRtc, {RtcStream} from "bandwidth-rtc"; - -function MediaPlayer({bandwidthRtcClient, inCall, setInCall}: {bandwidthRtcClient: BandwidthRtc, inCall: boolean, setInCall: (inCall: boolean) => void} ) { - if (!bandwidthRtcClient) { - throw new Error("webrtcClient is required"); - } - if (!setInCall) { - throw new Error("setInCall is required"); - } - - // Register inbound media handler - bandwidthRtcClient.onStreamAvailable(async (s) => { - console.log("Stream available:", s) - setInCall(true); - await handleMediaSubscribe(s) - }); - bandwidthRtcClient.onStreamUnavailable(async (s) => { - console.log("Stream unavailable:", s) - setInCall(false); - await handleMediaUnsubscribe(s) - }) +import React, { useEffect, useRef, useState } from "react"; +function MediaPlayer({ inboundStream }: { inboundStream: MediaStream | null }) { const audioRef = useRef(null); const canvasRef = useRef(null); - const [audioSource, setAudioSource] = useState(null); - const [audioSourceNode, setAudioSourceNode] = useState(null); - const [audioContext, setAudioContext] = useState(null); - const [analyser, setAnalyser] = useState(null); - const [isPlaying, setIsPlaying] = useState(false); - const [isSubscribed, setIsSubscribed] = useState(false); - const [dataArray, setDataArray] = useState(new Uint8Array(512)); - const [fftSize, setFftSize] = useState(512); - const [directMediaStream, setDirectMediaStream] = useState(null); - const [localOutputAudioNode, setLocalOutputAudioNode] = useState(undefined); - const [outputStreamSourceNode, setOutputStreamSourceNode] = useState(undefined); + const contextRef = useRef(null); + const analyserRef = useRef(null); + const sourceRef = useRef(null); + const dataArrayRef = useRef(new Uint8Array(512)); + const speakersConnectedRef = useRef(false); + const [isPlaying, setIsPlaying] = useState(false); useEffect(() => { - // Initialize Web Audio API - const context = new window.AudioContext(); - const analyserNode = context.createAnalyser(); - analyserNode.fftSize = fftSize; - setAudioContext(context); - setAnalyser(analyserNode); + const context = new AudioContext(); + const analyser = context.createAnalyser(); + analyser.fftSize = 512; + contextRef.current = context; + analyserRef.current = analyser; }, []); - const drawFFT = () => { - if (!analyser || !canvasRef.current) return; - let bgColor = "rgb(200 200 200)"; - let lineColor = "rgb(0 0 0)"; - let drawFft = false; - for (let i = 0; i < dataArray.length; i++) { - if (dataArray[i] != 128) { - drawFft = true; - break; + useEffect(() => { + const context = contextRef.current; + const analyser = analyserRef.current; + if (!inboundStream || !context || !analyser || !audioRef.current) { + if (audioRef.current) { + audioRef.current.srcObject = null; } + if (sourceRef.current) { + sourceRef.current.disconnect(); + sourceRef.current = null; + } + speakersConnectedRef.current = false; + setIsPlaying(false); + return; } - if (!drawFft) { - // Clear the FFT - bgColor = "rgb(200 200 200)"; - lineColor = "rgb(200 200 200)"; - } + const source = context.createMediaStreamSource(inboundStream); + source.connect(analyser); + audioRef.current.srcObject = inboundStream; + sourceRef.current = source; + context.resume(); + drawFFT(); + }, [inboundStream]); + const drawFFT = () => { + const analyser = analyserRef.current; const canvas = canvasRef.current; + if (!analyser || !canvas) { return; } const ctx = canvas.getContext("2d"); - if (!ctx) return - analyser.getByteTimeDomainData(dataArray); - - ctx.fillStyle = bgColor; + if (!ctx) { return; } + const data = dataArrayRef.current; + analyser.getByteTimeDomainData(data); + const active = data.some(v => v !== 128); + ctx.fillStyle = "rgb(200 200 200)"; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - ctx.lineWidth = 2; - ctx.strokeStyle = lineColor; + ctx.strokeStyle = active ? "rgb(0 0 0)" : "rgb(200 200 200)"; ctx.beginPath(); - - const sliceWidth = (ctx.canvas.width * 1.0) / dataArray.length; + const sliceWidth = ctx.canvas.width / data.length; let x = 0; - for (let i = 0; i < dataArray.length; i++) { - const v = dataArray[i] / 128.0; + for (let i = 0; i < data.length; i++) { + const v = data[i] / 128.0; const y = (v * ctx.canvas.height) / 2; - if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } - x += sliceWidth; } ctx.lineTo(ctx.canvas.width, ctx.canvas.height / 2); @@ -92,88 +72,28 @@ function MediaPlayer({bandwidthRtcClient, inCall, setInCall}: {bandwidthRtcClien requestAnimationFrame(drawFFT); }; - const handleMediaSubscribe = async (rtcStream: RtcStream): Promise => { - if (!audioContext || !analyser) { - throw new Error("Audio context or analyser is not initialized"); - } - if (!audioRef.current) { - throw new Error("Audio element is not initialized"); - } - if (!rtcStream.mediaStream) { - throw new Error("RTC stream has no media stream"); - } - if (!isSubscribed) { - let sourceNode = audioContext.createMediaStreamSource(rtcStream.mediaStream); - let destination = audioContext.createMediaStreamDestination(); - - sourceNode.connect(analyser); - analyser.connect(destination); - let stream = destination.stream; - drawFFT(); - audioRef.current.srcObject = rtcStream.mediaStream; - setAudioSourceNode(sourceNode); - setIsSubscribed(true); - await audioContext.resume() - setDirectMediaStream(stream) - return stream; - } else { - throw new Error("Already subscribed to media"); - } - } - - const handleMediaUnsubscribe = async (s: RtcStream) => { - console.log("Unsubscribing from stream:", s.mediaStream?.id); - // Stop playing if we are - if (isPlaying) { - await handlePlay() - } - if (isSubscribed) { - setIsSubscribed(false); - setAudioSourceNode(null); - setDirectMediaStream(null); - } - } - const handlePlay = async () => { - console.log("handlePlay") - let sourceNode = audioSourceNode; - if (!sourceNode) { - console.log("sourceNode is null") - return - } - if (!audioContext) { - throw new Error("Audio context is not initialized"); - return - } - if (!directMediaStream) { - console.log("directMediaStream is null") - return - } - if (!sourceNode) { - sourceNode = audioContext.createMediaStreamSource(directMediaStream) - // audioRef.current.srcObject = mediaStream; - setOutputStreamSourceNode(sourceNode) - } - if (!localOutputAudioNode) { - setLocalOutputAudioNode(sourceNode.connect(audioContext.destination)) - setIsPlaying(true) + const context = contextRef.current; + const source = sourceRef.current; + if (!context || !source) { return; } + await context.resume(); + if (speakersConnectedRef.current) { + source.disconnect(context.destination); + speakersConnectedRef.current = false; + setIsPlaying(false); } else { - if (localOutputAudioNode) { - sourceNode.disconnect(audioContext.destination); - setLocalOutputAudioNode(undefined) - setIsPlaying(false) - } + source.connect(context.destination); + speakersConnectedRef.current = true; + setIsPlaying(true); } }; return (
+
);