commit 8bcd7eacd8bd18b89aacc2b93444654c13f84b99 Author: I Luk Kim Date: Thu Mar 19 21:46:44 2026 -0700 feat: initial commit with UX improvements Add full project including 12 UX improvements: wider gesture edge zone (50px), prefers-reduced-motion support, additional shortcut keys (^A ^E ^R ^W ^U) and PageDown, Escape to close sidebar, haptic feedback, key repeat for arrow keys, sidebar loading/empty state, login loading state, reconnect status indicator, terminal dim on disconnect, QR modal focus management, and desktop keyboard toggle. Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88c7961 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.claude/ +*.env diff --git a/bin/cmux-remote.js b/bin/cmux-remote.js new file mode 100755 index 0000000..924fff1 --- /dev/null +++ b/bin/cmux-remote.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import '../dist/index.js'; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ec1cdcb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3590 @@ +{ + "name": "cmux-remote", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cmux-remote", + "version": "0.1.0", + "dependencies": { + "bcrypt": "^5.1.0", + "commander": "^13.0.0", + "express": "^5.0.0", + "jsonwebtoken": "^9.0.0", + "nanoid": "^5.0.0", + "qrcode": "^1.5.4", + "qrcode-terminal": "^0.12.0", + "ws": "^8.18.0" + }, + "bin": { + "cmux-remote": "bin/cmux-remote.js" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.0", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.0", + "@types/node": "^22.0.0", + "@types/qrcode": "^1.5.6", + "@types/ws": "^8.5.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", + "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..02b9e83 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "cmux-remote", + "version": "0.1.0", + "description": "Browser-based remote terminal control for cmux", + "type": "module", + "bin": { + "cmux-remote": "./bin/cmux-remote.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "dev:local": "tsx src/index.ts --no-tunnel", + "start": "node dist/index.js", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "bcrypt": "^5.1.0", + "commander": "^13.0.0", + "express": "^5.0.0", + "jsonwebtoken": "^9.0.0", + "nanoid": "^5.0.0", + "qrcode": "^1.5.4", + "qrcode-terminal": "^0.12.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.0", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.0", + "@types/node": "^22.0.0", + "@types/qrcode": "^1.5.6", + "@types/ws": "^8.5.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0" + } +} diff --git a/public/css/keyboard.css b/public/css/keyboard.css new file mode 100644 index 0000000..e93cac7 --- /dev/null +++ b/public/css/keyboard.css @@ -0,0 +1,204 @@ +.virtual-keyboard { + flex-shrink: 0; + background: var(--bg-secondary); + backdrop-filter: blur(var(--blur-amount)); + -webkit-backdrop-filter: blur(var(--blur-amount)); + border-top: 1px solid var(--border-color); + padding: 8px 8px; + padding-bottom: max(8px, env(safe-area-inset-bottom)); +} + +.key-row { + display: flex; + gap: 5px; + margin-bottom: 5px; + align-items: center; +} + +.key-row:last-child { + margin-bottom: 0; +} + +/* ── Physical key style ──────────────────────────── */ +.key { + flex: 0 0 auto; + min-width: 40px; + height: 36px; + border: 1px solid var(--key-border); + border-bottom: 2px solid var(--key-depth); + border-radius: var(--radius-xs); + background: var(--key-bg); + background-image: linear-gradient(180deg, var(--key-bg-highlight) 0%, transparent 60%); + color: var(--text-primary); + font-size: 0.77rem; + font-weight: 500; + font-family: inherit; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0 7px; + -webkit-user-select: none; + user-select: none; + transition: background var(--transition-fast), transform var(--transition-fast), border-color var(--transition-fast); +} + +.key:active { + background: var(--key-active-bg); + background-image: none; + border-bottom-width: 1px; + transform: translateY(1px); +} + +.key.modifier { + font-weight: 600; + font-size: 0.7rem; + letter-spacing: 0.03em; + text-transform: uppercase; + color: var(--text-secondary); +} + +.key.modifier.active { + background: var(--accent); + background-image: var(--accent-gradient); + color: #fff; + border-color: transparent; + border-bottom-color: var(--accent-dim); + box-shadow: 0 0 14px var(--accent-glow); +} + +.key.modifier.locked { + background: var(--accent); + background-image: var(--accent-gradient); + color: #fff; + border-color: transparent; + border-bottom-color: var(--accent-dim); + box-shadow: 0 0 0 2px var(--accent-hover), 0 0 20px var(--accent-glow); +} + +.key.shortcut { + font-family: 'SF Mono', 'JetBrains Mono', 'Menlo', monospace; + font-size: 0.7rem; + font-weight: 700; + color: var(--text-secondary); + min-width: 36px; +} + +.key-spacer { + flex: 1; +} + +/* ── Input row — pill style ──────────────────────── */ +.input-row { + gap: 6px; +} + +.text-input { + flex: 1; + height: 38px; + border: 1px solid var(--border-color); + border-radius: var(--radius-pill); + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.9rem; + padding: 0 16px; + outline: none; + font-family: 'SF Mono', 'JetBrains Mono', 'Menlo', monospace; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.text-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.text-input::placeholder { + color: var(--text-dimmed); +} + +.send-key { + width: 38px; + height: 38px; + min-width: unset; + padding: 0; + flex-shrink: 0; + border-radius: var(--radius-pill); + background: var(--accent); + background-image: var(--accent-gradient); + color: #fff; + border: none; + border-bottom: none; + font-size: 1.05rem; + font-weight: 700; + box-shadow: 0 2px 10px var(--accent-glow); +} + +.send-key:active { + background-image: none; + background: var(--accent-dim); + transform: scale(0.92); + box-shadow: none; +} + +/* ── Landscape ───────────────────────────────────── */ +@media (max-height: 500px) { + .key { + height: 30px; + min-width: 34px; + font-size: 0.68rem; + } + .text-input, .send-key { + height: 30px; + } + .virtual-keyboard { + padding: 4px; + } + .key-row { + margin-bottom: 3px; + } +} + +/* ── Desktop ─────────────────────────────────────── */ +@media (min-width: 1024px) { + .virtual-keyboard { + padding: 10px 20px; + padding-bottom: max(10px, env(safe-area-inset-bottom)); + } + + .key { + min-width: 46px; + height: 34px; + } + + .key:hover { + background-image: none; + background: var(--key-active-bg); + border-color: var(--accent); + } + + .text-input { + max-width: 640px; + } + + .send-key:hover { + filter: brightness(1.12); + box-shadow: 0 4px 16px var(--accent-glow); + } +} + +.key-row.shortcut-keys { flex-wrap: wrap; } +.key-row.shortcut-keys .key.shortcut { min-width: 32px; padding: 0 5px; } + +.keyboard-toggle { display: none; } +@media (min-width: 1024px) { + .keyboard-toggle { + display: flex; align-items: center; justify-content: center; gap: 6px; + width: 100%; height: 28px; + background: var(--bg-secondary); border: none; + border-top: 1px solid var(--border-color); + color: var(--text-dimmed); font-size: 0.72rem; + cursor: pointer; flex-shrink: 0; + } + .keyboard-toggle:hover { color: var(--text-secondary); background: var(--bg-tertiary); } + .virtual-keyboard.collapsed { display: none; } +} diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000..e804076 --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,322 @@ +* { + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +html, body { + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; + background: var(--bg-primary); + color: var(--text-primary); + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Inter', system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.app-container { + display: flex; + flex-direction: column; + height: 100%; + height: 100dvh; + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); +} + +/* ── Top bar ─────────────────────────────────────── */ +.top-bar { + display: flex; + align-items: center; + height: 48px; + padding: 0 10px; + background: var(--bg-secondary); + backdrop-filter: blur(var(--blur-amount)); + -webkit-backdrop-filter: blur(var(--blur-amount)); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; + gap: 8px; + z-index: 10; + position: relative; +} + +/* Accent line at top */ +.top-bar::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: var(--accent-gradient); + opacity: 0.5; +} + +.menu-btn { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.15rem; + width: 34px; + height: 34px; + padding: 0; + cursor: pointer; + border-radius: var(--radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.menu-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.app-title { + font-weight: 700; + font-size: 0.85rem; + letter-spacing: -0.02em; + color: var(--text-primary); + white-space: nowrap; +} + +.top-bar-right { + margin-left: auto; + display: flex; + align-items: center; + gap: 10px; +} + +/* Connection status pill */ +.connection-status { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 8px 3px 6px; + border-radius: var(--radius-pill); + background: rgba(248, 113, 113, 0.08); + border: 1px solid rgba(248, 113, 113, 0.15); + font-size: 0.7rem; + font-weight: 600; + color: var(--red); + transition: all var(--transition-normal); + white-space: nowrap; +} + +.connection-status::before { + content: ''; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--red); + flex-shrink: 0; + box-shadow: 0 0 5px var(--red); +} + +.connection-status.connected { + background: rgba(52, 211, 153, 0.08); + border-color: rgba(52, 211, 153, 0.15); + color: var(--green); +} + +.connection-status.connected::before { + background: var(--green); + box-shadow: 0 0 6px var(--green); +} + +@keyframes status-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} + +.connection-status:not(.connected) { + animation: status-pulse 2.5s ease-in-out infinite; +} + +.theme-toggle { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1rem; + padding: 0; + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); + flex-shrink: 0; +} + +.theme-toggle:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +/* ── Terminal container ──────────────────────────── */ +.terminal-container { + flex: 1; + overflow-y: auto; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + background: var(--terminal-bg); +} + +.terminal-container::-webkit-scrollbar { + width: 4px; +} +.terminal-container::-webkit-scrollbar-track { + background: transparent; +} +.terminal-container::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 2px; +} + +/* ── QR button ───────────────────────────────────── */ +.qr-btn { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.1rem; + padding: 0; + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); + flex-shrink: 0; +} + +.qr-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +/* ── QR modal ────────────────────────────────────── */ +.qr-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 200; + display: flex; + align-items: center; + justify-content: center; +} + +.qr-modal-overlay[hidden] { + display: none; +} + +.qr-modal { + position: relative; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg, 12px); + padding: 24px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + max-width: min(360px, 90vw); + width: 100%; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5); +} + +.qr-modal-close { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.4rem; + line-height: 1; + cursor: pointer; + padding: 4px 8px; + border-radius: var(--radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); +} + +.qr-modal-close:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.qr-modal-image { + width: min(260px, 80vw); + height: min(260px, 80vw); + display: block; + border-radius: 8px; + background: #fff; + padding: 8px; +} + +.qr-modal-url { + font-size: 0.72rem; + color: var(--text-secondary); + word-break: break-all; + text-align: center; + max-width: 100%; + font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace; +} + +/* ── Desktop layout ──────────────────────────────── */ +@media (min-width: 1024px) { + body { + display: flex; + flex-direction: row; + } + + .sidebar-overlay { + display: none; + } + + .app-container { + flex: 1; + min-width: 0; + } + + .top-bar { + height: 50px; + padding: 0 18px; + } + + .menu-btn { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .connection-status:not(.connected) { animation: none; } +} + +.connection-status.reconnecting { + background: rgba(251, 191, 36, 0.08); + border-color: rgba(251, 191, 36, 0.15); + color: var(--yellow); +} +.connection-status.reconnecting::before { + background: var(--yellow); + box-shadow: 0 0 5px var(--yellow); +} + +.terminal-container.stale { position: relative; } +.terminal-container.stale::after { + content: ''; + position: absolute; + inset: 0; + background: var(--bg-primary); + opacity: 0.4; + pointer-events: none; + z-index: 1; +} diff --git a/public/css/sidebar.css b/public/css/sidebar.css new file mode 100644 index 0000000..827e2a3 --- /dev/null +++ b/public/css/sidebar.css @@ -0,0 +1,227 @@ +.sidebar { + position: fixed; + top: 0; + left: 0; + width: 272px; + height: 100%; + background: var(--sidebar-bg); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-right: 1px solid var(--sidebar-border); + z-index: 100; + transform: translateX(-100%); + transition: transform var(--transition-smooth); + display: flex; + flex-direction: column; + padding-top: env(safe-area-inset-top); + box-shadow: var(--shadow-lg); +} + +.sidebar.open { + transform: translateX(0); +} + +.sidebar-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + z-index: 99; + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-smooth); +} + +.sidebar-overlay.visible { + opacity: 1; + pointer-events: auto; +} + +/* ── Header ──────────────────────────────────────── */ +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 14px 12px; + border-bottom: 1px solid var(--sidebar-border); +} + +.sidebar-header h2 { + margin: 0; + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-dimmed); +} + +.sidebar-close { + background: none; + border: none; + color: var(--text-dimmed); + font-size: 1.25rem; + cursor: pointer; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); + line-height: 1; + padding: 0; +} + +.sidebar-close:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +/* ── Content ─────────────────────────────────────── */ +.sidebar-content { + flex: 1; + overflow-y: auto; + padding: 10px 0 16px; +} + +.sidebar-content::-webkit-scrollbar { + width: 3px; +} +.sidebar-content::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 2px; +} + +/* ── Group label ─────────────────────────────────── */ +.ws-group-label { + padding: 14px 16px 5px; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-dimmed); + font-weight: 700; +} + +/* ── Workspace item ──────────────────────────────── */ +.ws-item { + padding: 9px 14px; + cursor: pointer; + display: flex; + align-items: center; + gap: 9px; + font-size: 0.86rem; + font-weight: 500; + color: var(--text-secondary); + transition: background var(--transition-fast), color var(--transition-fast); + margin: 1px 8px; + border-radius: var(--radius-sm); +} + +.ws-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.ws-item.active { + background: var(--accent-soft); + color: var(--accent-hover); +} + +.ws-item .ws-icon { + color: var(--text-dimmed); + font-size: 0.8rem; + width: 18px; + text-align: center; + flex-shrink: 0; +} + +.ws-item .ws-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Surface item ────────────────────────────────── */ +.surface-item { + padding: 8px 12px; + cursor: pointer; + font-size: 0.83rem; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 8px; + transition: background var(--transition-fast), color var(--transition-fast); + margin: 1px 8px; + border-radius: var(--radius-sm); + position: relative; +} + +.surface-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.surface-item.active { + background: linear-gradient(90deg, var(--accent-soft) 0%, transparent 100%); + color: var(--accent-hover); + font-weight: 600; +} + +/* Left accent bar for active item */ +.surface-item.active::before { + content: ''; + position: absolute; + left: -8px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + background: var(--accent-gradient); + border-radius: 0 var(--radius-pill) var(--radius-pill) 0; +} + +.surface-item .surface-icon { + font-size: 0.75rem; + width: 16px; + text-align: center; + flex-shrink: 0; + opacity: 0.6; +} + +.surface-item.active .surface-icon { + opacity: 1; +} + +/* ── Desktop ─────────────────────────────────────── */ +@media (min-width: 1024px) { + .sidebar { + position: relative; + transform: none; + width: 252px; + flex-shrink: 0; + box-shadow: none; + height: 100vh; + height: 100dvh; + } + + .sidebar.open { + transform: none; + } + + .sidebar-overlay { + display: none; + } + + .sidebar-close { + display: none; + } +} + +.sidebar-empty { + padding: 24px 16px; + text-align: center; + color: var(--text-dimmed); + font-size: 0.8rem; +} diff --git a/public/css/terminal.css b/public/css/terminal.css new file mode 100644 index 0000000..c920fc3 --- /dev/null +++ b/public/css/terminal.css @@ -0,0 +1,101 @@ +@font-face { + font-family: 'SymbolsNerdFont'; + src: url('/fonts/SymbolsNerdFontMono-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-display: block; + unicode-range: U+23FB-23FE, U+2665, U+26A1, U+2B58, U+E000-E00A, U+E0A0-E0A2, U+E0A3, U+E0B0-E0C8, U+E0CA, U+E0CC-E0D7, U+E200-E2A9, U+E300-E3E3, U+E5FA-E6B7, U+E700-E7C5, U+EA60-EBEB, U+F000-F2FF, U+F400-F533, U+F0001-F1AF0; +} + +.terminal-output { + margin: 0; + padding: 12px 16px; + font-family: 'SF Mono', 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'Menlo', 'Monaco', 'Consolas', 'SymbolsNerdFont', 'Symbols Nerd Font Mono', monospace; + font-size: 13px; + line-height: 1.0; + white-space: pre; + word-wrap: normal; + color: var(--text-primary); + contain: content; + min-height: 100%; + -webkit-user-select: text; + user-select: text; + font-feature-settings: "liga" 0, "calt" 0; +} + +.terminal-output::selection, +.terminal-output *::selection { + background: var(--accent-glow); + color: inherit; +} + +/* Claude Code pattern highlighting */ + +.hl-tool { + color: var(--blue); +} + +.hl-thinking { + color: var(--purple); +} + +.hl-output { + color: var(--gray); +} + +.hl-prompt { + color: var(--green); + font-weight: 600; +} + +.hl-success { + color: var(--green); +} + +.hl-error { + color: var(--red); +} + +.hl-path { + color: var(--cyan); +} + +.hl-box { + color: var(--text-dimmed); +} + +.hl-dim { + color: var(--text-dimmed); +} + +.hl-header { + color: var(--yellow); + font-weight: 600; +} + +.hl-linenum { + color: var(--text-dimmed); +} + +.hl-added { + color: var(--green); +} + +.hl-removed { + color: var(--red); +} + + +/* QR code lines — font-size set dynamically in terminal-view.js to fit width */ +.qr-line { + display: block; + line-height: 1.0; + letter-spacing: -0.03em; +} + +@media (min-width: 1024px) { + .terminal-output { + padding: 16px 24px; + font-size: 13.5px; + } +} diff --git a/public/css/themes.css b/public/css/themes.css new file mode 100644 index 0000000..8f71ff8 --- /dev/null +++ b/public/css/themes.css @@ -0,0 +1,153 @@ +:root, +[data-theme="dark"] { + --bg-primary: #0b0b13; + --bg-secondary: rgba(14, 14, 24, 0.9); + --bg-tertiary: rgba(255, 255, 255, 0.04); + --bg-elevated: rgba(20, 20, 36, 0.95); + --bg-glass: rgba(14, 14, 24, 0.75); + --terminal-bg: #080810; + --text-primary: #e2e2ec; + --text-secondary: #7878a0; + --text-dimmed: #3e3e58; + --border-color: rgba(255, 255, 255, 0.055); + --border-subtle: rgba(255, 255, 255, 0.025); + --border-strong: rgba(255, 255, 255, 0.1); + --accent: #6366f1; + --accent-hover: #818cf8; + --accent-dim: #4f52cc; + --accent-glow: rgba(99, 102, 241, 0.18); + --accent-soft: rgba(99, 102, 241, 0.08); + --accent-gradient: linear-gradient(135deg, #6366f1, #a78bfa); + --green: #34d399; + --green-glow: rgba(52, 211, 153, 0.2); + --red: #f87171; + --red-glow: rgba(248, 113, 113, 0.15); + --blue: #60a5fa; + --purple: #a78bfa; + --cyan: #22d3ee; + --yellow: #fbbf24; + --orange: #fb923c; + --gray: #6b7280; + --key-bg: rgba(26, 26, 46, 0.9); + --key-bg-highlight: rgba(255, 255, 255, 0.035); + --key-border: rgba(255, 255, 255, 0.07); + --key-depth: rgba(0, 0, 0, 0.5); + --key-active-bg: rgba(99, 102, 241, 0.15); + --sidebar-bg: #09090f; + --sidebar-border: rgba(255, 255, 255, 0.04); + --scrollbar-thumb: rgba(255, 255, 255, 0.07); + --scrollbar-track: transparent; + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.6); + --blur-amount: 20px; + --transition-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 220ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-smooth: 320ms cubic-bezier(0.16, 1, 0.3, 1); + --radius-xs: 5px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 18px; + --radius-pill: 100px; +} + +[data-theme="light"] { + --bg-primary: #f4f4f8; + --bg-secondary: rgba(255, 255, 255, 0.9); + --bg-tertiary: rgba(0, 0, 0, 0.035); + --bg-elevated: rgba(255, 255, 255, 0.95); + --bg-glass: rgba(255, 255, 255, 0.75); + --terminal-bg: #fafafa; + --text-primary: #0f0f1a; + --text-secondary: #60607a; + --text-dimmed: #aaaabb; + --border-color: rgba(0, 0, 0, 0.07); + --border-subtle: rgba(0, 0, 0, 0.04); + --border-strong: rgba(0, 0, 0, 0.12); + --accent: #5558eb; + --accent-hover: #4347d4; + --accent-dim: #7b7ef5; + --accent-glow: rgba(85, 88, 235, 0.12); + --accent-soft: rgba(85, 88, 235, 0.07); + --accent-gradient: linear-gradient(135deg, #5558eb, #9b59f5); + --green: #059669; + --green-glow: rgba(5, 150, 105, 0.12); + --red: #dc2626; + --red-glow: rgba(220, 38, 38, 0.1); + --blue: #2563eb; + --purple: #7c3aed; + --cyan: #0891b2; + --yellow: #d97706; + --orange: #ea580c; + --gray: #9ca3af; + --key-bg: rgba(255, 255, 255, 0.95); + --key-bg-highlight: rgba(255, 255, 255, 0.6); + --key-border: rgba(0, 0, 0, 0.09); + --key-depth: rgba(0, 0, 0, 0.12); + --key-active-bg: rgba(85, 88, 235, 0.08); + --sidebar-bg: #eeeef4; + --sidebar-border: rgba(0, 0, 0, 0.05); + --scrollbar-thumb: rgba(0, 0, 0, 0.1); + --scrollbar-track: transparent; + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.07); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.1); + --blur-amount: 20px; + --transition-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 220ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-smooth: 320ms cubic-bezier(0.16, 1, 0.3, 1); + --radius-xs: 5px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 18px; + --radius-pill: 100px; +} + +@media (prefers-color-scheme: light) { + :root:not([data-theme="dark"]) { + --bg-primary: #f4f4f8; + --bg-secondary: rgba(255, 255, 255, 0.9); + --bg-tertiary: rgba(0, 0, 0, 0.035); + --bg-elevated: rgba(255, 255, 255, 0.95); + --bg-glass: rgba(255, 255, 255, 0.75); + --terminal-bg: #fafafa; + --text-primary: #0f0f1a; + --text-secondary: #60607a; + --text-dimmed: #aaaabb; + --border-color: rgba(0, 0, 0, 0.07); + --border-subtle: rgba(0, 0, 0, 0.04); + --border-strong: rgba(0, 0, 0, 0.12); + --accent: #5558eb; + --accent-hover: #4347d4; + --accent-dim: #7b7ef5; + --accent-glow: rgba(85, 88, 235, 0.12); + --accent-soft: rgba(85, 88, 235, 0.07); + --accent-gradient: linear-gradient(135deg, #5558eb, #9b59f5); + --green: #059669; + --green-glow: rgba(5, 150, 105, 0.12); + --red: #dc2626; + --red-glow: rgba(220, 38, 38, 0.1); + --blue: #2563eb; + --purple: #7c3aed; + --cyan: #0891b2; + --yellow: #d97706; + --orange: #ea580c; + --gray: #9ca3af; + --key-bg: rgba(255, 255, 255, 0.95); + --key-bg-highlight: rgba(255, 255, 255, 0.6); + --key-border: rgba(0, 0, 0, 0.09); + --key-depth: rgba(0, 0, 0, 0.12); + --key-active-bg: rgba(85, 88, 235, 0.08); + --sidebar-bg: #eeeef4; + --sidebar-border: rgba(0, 0, 0, 0.05); + --scrollbar-thumb: rgba(0, 0, 0, 0.1); + --scrollbar-track: transparent; + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.07); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.1); + --blur-amount: 20px; + } +} diff --git a/public/fonts/SymbolsNerdFont-Regular.ttf b/public/fonts/SymbolsNerdFont-Regular.ttf new file mode 100644 index 0000000..38c24fb Binary files /dev/null and b/public/fonts/SymbolsNerdFont-Regular.ttf differ diff --git a/public/fonts/SymbolsNerdFontMono-Regular.ttf b/public/fonts/SymbolsNerdFontMono-Regular.ttf new file mode 100644 index 0000000..d898eea Binary files /dev/null and b/public/fonts/SymbolsNerdFontMono-Regular.ttf differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..ca35da7 --- /dev/null +++ b/public/index.html @@ -0,0 +1,107 @@ + + + + + + cmux-remote + + + + + + + + + + + + + + + + +
+ +
+ + cmux-remote +
+ Offline + + +
+
+ + +
+

+    
+ + + +
+
+ + + + + + + + +
+
+ + + + + + + + + + + + + +
+
+ + +
+
+
+ + + + + + + + + + + + + + diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..62c9435 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,197 @@ +// Main application orchestrator +(async function () { + // Check if auth is required + const authStatus = await fetch('/api/auth/status').then((r) => r.json()).catch(() => ({ authRequired: true })); + if (authStatus.authRequired) { + const token = localStorage.getItem('cmux-remote-token'); + if (!token) { + window.location.href = '/login.html'; + return; + } + } + + // State + let currentWorkspace = null; + let currentSurface = null; + + // Components + const ws = new WebSocketClient(); + const terminal = new TerminalView(document.getElementById('terminal-output')); + const statusDot = document.getElementById('connection-status'); + const terminalContainer = document.getElementById('terminal-container'); + const sidebar = new Sidebar((wsRef, surfaceRef) => { + switchSurface(wsRef, surfaceRef); + }); + + const gestures = new GestureHandler(sidebar); + const theme = new ThemeSwitcher(); + + // QR modal + const qrOverlay = document.getElementById('qr-modal-overlay'); + const qrBtn = document.getElementById('qr-btn'); + const qrCloseBtn = document.getElementById('qr-modal-close'); + const qrUrlEl = document.getElementById('qr-modal-url'); + + function _qrKeyHandler(e) { + if (e.key === 'Escape') { + closeQrModal(); + } else if (e.key === 'Tab') { + e.preventDefault(); + qrCloseBtn.focus(); + } + } + + async function openQrModal() { + try { + const { url } = await fetch('/api/access-url').then(r => r.json()); + qrUrlEl.textContent = url; + } catch { + qrUrlEl.textContent = window.location.origin; + } + // Reload QR image so it's always fresh + document.getElementById('qr-modal-image').src = '/api/qr.svg?' + Date.now(); + qrOverlay.hidden = false; + qrCloseBtn.focus(); + document.addEventListener('keydown', _qrKeyHandler); + } + + function closeQrModal() { + qrOverlay.hidden = true; + document.removeEventListener('keydown', _qrKeyHandler); + qrBtn.focus(); + } + + qrBtn.addEventListener('click', openQrModal); + qrCloseBtn.addEventListener('click', closeQrModal); + qrOverlay.addEventListener('click', (e) => { + if (e.target === qrOverlay) closeQrModal(); + }); + + const keyboard = new VirtualKeyboard( + (text) => { + if (currentWorkspace && currentSurface) { + ws.sendText(currentWorkspace, currentSurface, text); + } + }, + (key) => { + if (currentWorkspace && currentSurface) { + ws.sendKey(currentWorkspace, currentSurface, key); + } + } + ); + + // Keyboard toggle (desktop) + const keyboardToggle = document.getElementById('keyboard-toggle'); + const virtualKeyboardEl = document.getElementById('virtual-keyboard'); + + if (window.innerWidth >= 1024) { + virtualKeyboardEl.classList.add('collapsed'); + keyboardToggle.hidden = false; + } + + keyboardToggle.addEventListener('click', () => { + virtualKeyboardEl.classList.toggle('collapsed'); + }); + + window.addEventListener('resize', () => { + if (window.innerWidth < 1024) { + keyboardToggle.hidden = true; + virtualKeyboardEl.classList.remove('collapsed'); + } else { + keyboardToggle.hidden = false; + } + }); + + // Connection events + ws.on('connected', () => { + statusDot.classList.add('connected'); + statusDot.classList.remove('reconnecting'); + statusDot.textContent = 'Connected'; + terminalContainer.classList.remove('stale'); + }); + + ws.on('disconnected', () => { + statusDot.classList.remove('connected'); + statusDot.classList.add('reconnecting'); + statusDot.textContent = 'Reconnecting...'; + terminalContainer.classList.add('stale'); + }); + + ws.on('reconnecting', ({ attempt }) => { + statusDot.textContent = `Reconnecting (${attempt})...`; + }); + + ws.on('auth-ok', () => { + // Request workspace list after auth + ws.listWorkspaces(); + }); + + ws.on('workspaces', (msg) => { + sidebar.setWorkspaces(msg.workspaces); + + // Auto-subscribe to first terminal surface if none selected + if (!currentSurface && msg.workspaces.length > 0) { + const firstWs = msg.workspaces[0]; + for (const pane of firstWs.panes) { + for (const surface of pane.surfaces) { + if (surface.type === 'terminal') { + switchSurface(firstWs.ref, surface.ref); + return; + } + } + } + // Fallback: first surface of any type + if (firstWs.panes.length > 0 && firstWs.panes[0].surfaces.length > 0) { + const s = firstWs.panes[0].surfaces[0]; + switchSurface(firstWs.ref, s.ref); + } + } + }); + + ws.on('screen', (msg) => { + if (msg.surface === currentSurface) { + terminal.setContent(msg.lines); + } + }); + + ws.on('screen-diff', (msg) => { + if (msg.surface === currentSurface) { + terminal.applyDiff(msg.patches); + } + }); + + function switchSurface(wsRef, surfaceRef) { + // Unsubscribe from current + if (currentSurface) { + ws.unsubscribe(currentSurface); + } + + currentWorkspace = wsRef; + currentSurface = surfaceRef; + + terminal.clear(); + ws.subscribe(wsRef, surfaceRef); + sidebar.setActiveSurface(surfaceRef); + } + + // Visibility API: pause polling when hidden + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + if (currentSurface) ws.unsubscribe(currentSurface); + } else { + if (currentWorkspace && currentSurface) { + ws.subscribe(currentWorkspace, currentSurface); + } + } + }); + + // Periodic workspace refresh + setInterval(() => { + if (ws.isConnected()) { + ws.listWorkspaces(); + } + }, 10000); + + // Start + ws.connect(authStatus.authRequired); +})(); diff --git a/public/js/auth.js b/public/js/auth.js new file mode 100644 index 0000000..d3344a8 --- /dev/null +++ b/public/js/auth.js @@ -0,0 +1,58 @@ +// Login page handler +(function () { + const form = document.getElementById('login-form'); + const passwordInput = document.getElementById('password'); + const errorEl = document.getElementById('error'); + + // If already authenticated, redirect + const token = localStorage.getItem('cmux-remote-token'); + if (token) { + fetch('/api/auth/verify', { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((r) => r.json()) + .then((data) => { + if (data.valid) window.location.href = '/'; + }) + .catch(() => {}); + } + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + errorEl.classList.remove('visible'); + + const password = passwordInput.value.trim(); + if (!password) return; + + const submitBtn = form.querySelector('button[type="submit"]'); + submitBtn.disabled = true; + submitBtn.textContent = 'Connecting...'; + + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + + const data = await res.json(); + + if (res.ok && data.token) { + localStorage.setItem('cmux-remote-token', data.token); + window.location.href = '/'; + } else { + errorEl.textContent = data.error || 'Login failed'; + errorEl.classList.add('visible'); + passwordInput.value = ''; + passwordInput.focus(); + submitBtn.disabled = false; + submitBtn.textContent = 'Connect'; + } + } catch (err) { + errorEl.textContent = 'Connection failed'; + errorEl.classList.add('visible'); + submitBtn.disabled = false; + submitBtn.textContent = 'Connect'; + } + }); +})(); diff --git a/public/js/gestures.js b/public/js/gestures.js new file mode 100644 index 0000000..18acf13 --- /dev/null +++ b/public/js/gestures.js @@ -0,0 +1,63 @@ +class GestureHandler { + constructor(sidebar) { + this.sidebar = sidebar; + this.startX = 0; + this.startY = 0; + this.tracking = false; + + document.addEventListener('touchstart', (e) => this.onTouchStart(e), { passive: true }); + document.addEventListener('touchmove', (e) => this.onTouchMove(e), { passive: false }); + document.addEventListener('touchend', (e) => this.onTouchEnd(e), { passive: true }); + } + + onTouchStart(e) { + const touch = e.touches[0]; + this.startX = touch.clientX; + this.startY = touch.clientY; + this.tracking = true; + } + + onTouchMove(e) { + if (!this.tracking) return; + + const touch = e.touches[0]; + const dx = touch.clientX - this.startX; + const dy = touch.clientY - this.startY; + + // Only track horizontal swipes from edge + if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10) { + // Swipe right from left edge to open sidebar + if (dx > 0 && this.startX < 50 && !this.sidebar.isOpen()) { + e.preventDefault(); + } + // Swipe left to close sidebar + if (dx < 0 && this.sidebar.isOpen()) { + e.preventDefault(); + } + } + } + + onTouchEnd(e) { + if (!this.tracking) return; + this.tracking = false; + + const touch = e.changedTouches[0]; + const dx = touch.clientX - this.startX; + const dy = touch.clientY - this.startY; + + // Minimum swipe distance + if (Math.abs(dx) < 50 || Math.abs(dy) > Math.abs(dx)) return; + + // Swipe right from left edge = open sidebar + if (dx > 0 && this.startX < 50) { + this.sidebar.open(); + } + + // Swipe left = close sidebar + if (dx < 0 && this.sidebar.isOpen()) { + this.sidebar.close(); + } + } +} + +window.GestureHandler = GestureHandler; diff --git a/public/js/sidebar.js b/public/js/sidebar.js new file mode 100644 index 0000000..daa5b66 --- /dev/null +++ b/public/js/sidebar.js @@ -0,0 +1,114 @@ +class Sidebar { + constructor(onSelectSurface) { + this.onSelectSurface = onSelectSurface; + this.sidebar = document.getElementById('sidebar'); + this.overlay = document.getElementById('sidebar-overlay'); + this.tree = document.getElementById('sidebar-tree'); + this.menuBtn = document.getElementById('menu-btn'); + this.closeBtn = document.getElementById('sidebar-close'); + + this.workspaces = []; + this.activeSurface = null; + this._loaded = false; + + this.menuBtn.addEventListener('click', () => this.open()); + this.closeBtn.addEventListener('click', () => this.close()); + this.overlay.addEventListener('click', () => this.close()); + + // On desktop, sidebar is always visible via CSS (position: relative) + // Check on resize to close overlay if switching to desktop + this._onResize = () => { + if (window.innerWidth >= 1024) { + this.overlay.classList.remove('visible'); + } + }; + window.addEventListener('resize', this._onResize); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.isOpen() && !this.isDesktop()) this.close(); + }); + + this.render(); + } + + open() { + this.sidebar.classList.add('open'); + this.overlay.classList.add('visible'); + } + + close() { + this.sidebar.classList.remove('open'); + this.overlay.classList.remove('visible'); + } + + isDesktop() { + return window.innerWidth >= 1024; + } + + isOpen() { + return this.sidebar.classList.contains('open'); + } + + setWorkspaces(workspaces) { + this.workspaces = workspaces; + this._loaded = true; + this.render(); + } + + setActiveSurface(surfaceRef) { + this.activeSurface = surfaceRef; + this.render(); + } + + render() { + this.tree.innerHTML = ''; + + if (this.workspaces.length === 0) { + const empty = document.createElement('div'); + empty.className = 'sidebar-empty'; + empty.textContent = this._loaded ? 'No workspaces' : 'Loading...'; + this.tree.appendChild(empty); + return; + } + + for (const ws of this.workspaces) { + // Workspace group label + const groupLabel = document.createElement('div'); + groupLabel.className = 'ws-group-label'; + groupLabel.textContent = ws.name || ws.ref; + this.tree.appendChild(groupLabel); + + // Surfaces within workspace + for (const pane of ws.panes) { + for (const surface of pane.surfaces) { + const item = document.createElement('div'); + item.className = 'surface-item'; + if (surface.ref === this.activeSurface) { + item.classList.add('active'); + } + + const icon = document.createElement('span'); + icon.className = 'surface-icon'; + icon.textContent = surface.type === 'browser' ? '\u{1F310}' : '\u{1F4BB}'; + + const label = document.createElement('span'); + label.className = 'ws-label'; + label.textContent = surface.title || surface.ref; + + item.appendChild(icon); + item.appendChild(label); + + item.addEventListener('click', () => { + this.onSelectSurface(ws.ref, surface.ref); + this.setActiveSurface(surface.ref); + if (!this.isDesktop()) this.close(); + }); + + this.tree.appendChild(item); + } + } + } + } +} + +window.Sidebar = Sidebar; diff --git a/public/js/terminal-view.js b/public/js/terminal-view.js new file mode 100644 index 0000000..4071807 --- /dev/null +++ b/public/js/terminal-view.js @@ -0,0 +1,170 @@ +class TerminalView { + constructor(element) { + this.el = element; + this.lines = []; + this.renderPending = false; + this.autoScroll = true; + + // Track scroll position to determine auto-scroll + const container = this.el.parentElement; + container.addEventListener('scroll', () => { + const atBottom = + container.scrollHeight - container.scrollTop - container.clientHeight < 30; + this.autoScroll = atBottom; + }); + } + + setContent(lines) { + this.lines = lines; + this.scheduleRender(); + } + + applyDiff(patches) { + for (const patch of patches) { + this.lines.splice(patch.startLine, patch.deleteCount, ...patch.insertLines); + } + this.scheduleRender(); + } + + scheduleRender() { + if (this.renderPending) return; + this.renderPending = true; + requestAnimationFrame(() => { + this.render(); + this.renderPending = false; + }); + } + + isQrLine(line) { + // Detect QR code lines: mostly Unicode block characters (▀▄█) + const blockChars = (line.match(/[\u2580-\u2588]/g) || []).length; + const nonSpace = line.replace(/\s/g, '').length; + return nonSpace > 4 && blockChars / nonSpace > 0.3; + } + + render() { + const output = []; + let i = 0; + while (i < this.lines.length) { + if (this.isQrLine(this.lines[i])) { + // Collect all consecutive QR lines + const qrBlock = []; + while (i < this.lines.length && this.isQrLine(this.lines[i])) { + qrBlock.push(this.lines[i]); + i++; + } + // Calculate font-size so QR block fits container width without clipping + const maxLen = Math.max(...qrBlock.map(l => l.length)); + const containerWidth = (this.el.parentElement?.clientWidth || 375) - 32; + // monospace char width ≈ 0.62 × font-size + const fontSize = Math.max(3, Math.floor(containerWidth / (maxLen * 0.62))); + for (const qline of qrBlock) { + const esc = qline + .replace(/&/g, '&') + .replace(//g, '>'); + output.push(`${esc}`); + } + } else { + output.push(this.highlightLine(this.lines[i])); + i++; + } + } + const html = output.join('\n'); + this.el.innerHTML = html; + + if (this.autoScroll) { + const container = this.el.parentElement; + container.scrollTop = container.scrollHeight; + } + } + + highlightLine(line) { + // Escape HTML entities first + let escaped = line + .replace(/&/g, '&') + .replace(//g, '>'); + + // Apply highlighting patterns (order matters — more specific first) + + // Diff: added lines (starts with +) + if (/^\+[^+]/.test(line)) { + return `${escaped}`; + } + + // Diff: removed lines (starts with -) + if (/^-[^-]/.test(line)) { + return `${escaped}`; + } + + // Tool use markers (blue circle) + escaped = escaped.replace( + /(\u23FA|\u25CF|\u2B24)/g, + '$1' + ); + + // Thinking markers + escaped = escaped.replace( + /(\u273B)/g, + '$1' + ); + + // Output indent marker + escaped = escaped.replace( + /(\u23BF)/g, + '$1' + ); + + // Prompt marker + escaped = escaped.replace( + /(\u276F)/g, + '$1' + ); + + // Success markers + escaped = escaped.replace( + /(\u2713|\u2714|passed|PASS)/g, + '$1' + ); + + // Error markers + escaped = escaped.replace( + /(\u2717|\u2718|error|ERROR|FAIL|failed)/g, + '$1' + ); + + // Box drawing characters + escaped = escaped.replace( + /([\u2500-\u257F\u250C\u2510\u2514\u2518\u251C\u2524\u252C\u2534\u253C]+)/g, + '$1' + ); + + // File paths (simplified: word containing / with file extension) + escaped = escaped.replace( + /(?$1' + ); + + // Line numbers in context (e.g., " 123 |" or ":42:") + escaped = escaped.replace( + /^(\s*\d+\s*[|:])/, + '$1' + ); + + return escaped; + } + + scrollToBottom() { + const container = this.el.parentElement; + container.scrollTop = container.scrollHeight; + this.autoScroll = true; + } + + clear() { + this.lines = []; + this.el.innerHTML = ''; + } +} + +window.TerminalView = TerminalView; diff --git a/public/js/theme.js b/public/js/theme.js new file mode 100644 index 0000000..e94cbb7 --- /dev/null +++ b/public/js/theme.js @@ -0,0 +1,38 @@ +class ThemeSwitcher { + constructor() { + this.btn = document.getElementById('theme-toggle'); + this.current = localStorage.getItem('cmux-remote-theme') || 'auto'; + + this.apply(); + this.btn.addEventListener('click', () => this.toggle()); + } + + toggle() { + const resolved = this.getResolved(); + // Toggle to opposite of current effective theme + this.current = resolved === 'dark' ? 'light' : 'dark'; + localStorage.setItem('cmux-remote-theme', this.current); + this.apply(); + } + + getResolved() { + if (this.current === 'auto') { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + return this.current; + } + + apply() { + const theme = this.getResolved(); + document.documentElement.setAttribute('data-theme', theme); + this.btn.textContent = theme === 'dark' ? '\u{2600}' : '\u{1F319}'; + + // Update theme-color meta + const meta = document.querySelector('meta[name="theme-color"]'); + if (meta) { + meta.content = theme === 'dark' ? '#0c0c14' : '#f8f9fc'; + } + } +} + +window.ThemeSwitcher = ThemeSwitcher; diff --git a/public/js/virtual-keyboard.js b/public/js/virtual-keyboard.js new file mode 100644 index 0000000..12a993b --- /dev/null +++ b/public/js/virtual-keyboard.js @@ -0,0 +1,165 @@ +class VirtualKeyboard { + constructor(onSendText, onSendKey) { + this.onSendText = onSendText; + this.onSendKey = onSendKey; + this.ctrlActive = false; + this.ctrlLocked = false; + this.altActive = false; + this.altLocked = false; + this._repeatTimer = null; + this._repeatInterval = null; + + this.textInput = document.getElementById('text-input'); + this.sendBtn = document.getElementById('send-btn'); + this.ctrlKey = document.getElementById('ctrl-key'); + this.altKey = document.getElementById('alt-key'); + + this.init(); + } + + static get REPEAT_KEYS() { + return new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'PageUp', 'PageDown']); + } + + static get REPEAT_INITIAL_DELAY() { return 400; } + static get REPEAT_INTERVAL() { return 80; } + + init() { + // Text input + send + this.sendBtn.addEventListener('click', () => this.submitText()); + this.textInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.submitText(); + } + }); + + // Ctrl modifier + this.ctrlKey.addEventListener('click', () => this.toggleModifier('ctrl')); + this.ctrlKey.addEventListener('dblclick', () => this.lockModifier('ctrl')); + + // Alt modifier + this.altKey.addEventListener('click', () => this.toggleModifier('alt')); + this.altKey.addEventListener('dblclick', () => this.lockModifier('alt')); + + // Special keys + document.querySelectorAll('.key[data-key]').forEach((btn) => { + if (btn.classList.contains('modifier')) return; + const key = btn.dataset.key; + if (VirtualKeyboard.REPEAT_KEYS.has(key)) { + const startRepeat = () => { + this._clearRepeat(); + this.handleKeyPress(key); + this._repeatTimer = setTimeout(() => { + this._repeatInterval = setInterval(() => { + this.handleKeyPress(key); + }, VirtualKeyboard.REPEAT_INTERVAL); + }, VirtualKeyboard.REPEAT_INITIAL_DELAY); + }; + btn.addEventListener('mousedown', startRepeat); + btn.addEventListener('touchstart', startRepeat, { passive: true }); + btn.addEventListener('mouseup', () => this._clearRepeat()); + btn.addEventListener('mouseleave', () => this._clearRepeat()); + btn.addEventListener('touchend', () => this._clearRepeat()); + btn.addEventListener('touchcancel', () => this._clearRepeat()); + } else { + btn.addEventListener('click', () => this.handleKeyPress(key)); + } + }); + } + + _haptic() { + if (navigator.vibrate) navigator.vibrate(8); + } + + _clearRepeat() { + clearTimeout(this._repeatTimer); + clearInterval(this._repeatInterval); + this._repeatTimer = null; + this._repeatInterval = null; + } + + submitText() { + this._haptic(); + const text = this.textInput.value; + if (text) { + this.onSendText(text + '\n'); + this.textInput.value = ''; + } else { + // Empty submit = Enter key + this.onSendKey('Enter'); + } + this.textInput.focus(); + } + + handleKeyPress(key) { + this._haptic(); + // Check for shortcut keys like Ctrl-c + if (key.startsWith('Ctrl-')) { + this.onSendKey(key); + return; + } + + // Apply active modifiers + let finalKey = key; + if (this.ctrlActive) { + finalKey = `Ctrl-${key}`; + if (!this.ctrlLocked) this.deactivateModifier('ctrl'); + } + if (this.altActive) { + finalKey = `Alt-${key}`; + if (!this.altLocked) this.deactivateModifier('alt'); + } + + this.onSendKey(finalKey); + } + + toggleModifier(mod) { + this._haptic(); + if (mod === 'ctrl') { + if (this.ctrlLocked) { + this.deactivateModifier('ctrl'); + return; + } + this.ctrlActive = !this.ctrlActive; + this.ctrlKey.classList.toggle('active', this.ctrlActive); + } else if (mod === 'alt') { + if (this.altLocked) { + this.deactivateModifier('alt'); + return; + } + this.altActive = !this.altActive; + this.altKey.classList.toggle('active', this.altActive); + } + } + + lockModifier(mod) { + if (mod === 'ctrl') { + this.ctrlActive = true; + this.ctrlLocked = true; + this.ctrlKey.classList.add('active', 'locked'); + } else if (mod === 'alt') { + this.altActive = true; + this.altLocked = true; + this.altKey.classList.add('active', 'locked'); + } + } + + deactivateModifier(mod) { + if (mod === 'ctrl') { + this.ctrlActive = false; + this.ctrlLocked = false; + this.ctrlKey.classList.remove('active', 'locked'); + } else if (mod === 'alt') { + this.altActive = false; + this.altLocked = false; + this.altKey.classList.remove('active', 'locked'); + } + } + + focus() { + this.textInput.focus(); + } +} + +window.VirtualKeyboard = VirtualKeyboard; diff --git a/public/js/websocket-client.js b/public/js/websocket-client.js new file mode 100644 index 0000000..3338767 --- /dev/null +++ b/public/js/websocket-client.js @@ -0,0 +1,133 @@ +class WebSocketClient { + constructor() { + this.ws = null; + this.handlers = new Map(); + this.reconnectDelay = 1000; + this.maxReconnectDelay = 30000; + this.currentDelay = this.reconnectDelay; + this.shouldReconnect = true; + this.connected = false; + this.reconnectAttempt = 0; + } + + connect(authRequired) { + this.authRequired = authRequired !== false; + const token = localStorage.getItem('cmux-remote-token'); + if (this.authRequired && !token) { + window.location.href = '/login.html'; + return; + } + + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = `${proto}//${location.host}`; + + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + this.connected = true; + this.reconnectAttempt = 0; + this.currentDelay = this.reconnectDelay; + this.emit('connected'); + + // Authenticate (send token or empty for no-auth mode) + this.send({ type: 'auth', token: token || '' }); + }; + + this.ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + this.emit(msg.type, msg); + + // Redirect to login on auth failure (only if auth is required) + if (msg.type === 'auth-fail' && this.authRequired) { + localStorage.removeItem('cmux-remote-token'); + window.location.href = '/login.html'; + } + } catch (err) { + console.error('[ws] Parse error:', err); + } + }; + + this.ws.onclose = () => { + this.connected = false; + this.emit('disconnected'); + + if (this.shouldReconnect) { + const attempt = ++this.reconnectAttempt; + const delay = this.currentDelay; + this.emit('reconnecting', { attempt, delay }); + setTimeout(() => this.connect(this.authRequired), delay); + this.currentDelay = Math.min(this.currentDelay * 1.5, this.maxReconnectDelay); + } + }; + + this.ws.onerror = (err) => { + console.error('[ws] Error:', err); + }; + } + + send(msg) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(msg)); + } + } + + on(type, handler) { + if (!this.handlers.has(type)) { + this.handlers.set(type, []); + } + this.handlers.get(type).push(handler); + } + + off(type, handler) { + const list = this.handlers.get(type); + if (list) { + const idx = list.indexOf(handler); + if (idx >= 0) list.splice(idx, 1); + } + } + + emit(type, data) { + const list = this.handlers.get(type); + if (list) { + for (const handler of list) { + handler(data); + } + } + } + + subscribe(workspace, surface) { + this.send({ type: 'subscribe', workspace, surface }); + } + + unsubscribe(surface) { + this.send({ type: 'unsubscribe', surface }); + } + + sendText(workspace, surface, text) { + this.send({ type: 'send-text', workspace, surface, text }); + } + + sendKey(workspace, surface, key) { + this.send({ type: 'send-key', workspace, surface, key }); + } + + listWorkspaces() { + this.send({ type: 'list-workspaces' }); + } + + requestScroll(workspace, surface, lines) { + this.send({ type: 'scroll-request', workspace, surface, lines }); + } + + disconnect() { + this.shouldReconnect = false; + if (this.ws) this.ws.close(); + } + + isConnected() { + return this.connected; + } +} + +window.WebSocketClient = WebSocketClient; diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..1dfca50 --- /dev/null +++ b/public/login.html @@ -0,0 +1,276 @@ + + + + + + cmux-remote + + + + + + + + +
+
+
+
+
+ + + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..7e8387a --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "cmux-remote", + "short_name": "cmux", + "description": "Remote terminal control for cmux", + "start_url": "/", + "display": "standalone", + "orientation": "any", + "theme_color": "#0c0c14", + "background_color": "#0c0c14", + "icons": [ + { + "src": "data:image/svg+xml,", + "sizes": "any", + "type": "image/svg+xml" + } + ] +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..31cc178 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,53 @@ +const CACHE_NAME = 'cmux-remote-v3'; +const STATIC_ASSETS = [ + '/', + '/index.html', + '/login.html', + '/css/main.css', + '/css/terminal.css', + '/css/keyboard.css', + '/css/sidebar.css', + '/css/themes.css', + '/js/app.js', + '/js/auth.js', + '/js/terminal-view.js', + '/js/websocket-client.js', + '/js/virtual-keyboard.js', + '/js/sidebar.js', + '/js/gestures.js', + '/js/theme.js', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', (event) => { + // Skip API and WebSocket requests + const url = new URL(event.request.url); + if (url.pathname.startsWith('/api/') || event.request.headers.get('upgrade') === 'websocket') { + return; + } + + event.respondWith( + fetch(event.request) + .then((response) => { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + return response; + }) + .catch(() => caches.match(event.request)) + ); +}); diff --git a/src/bridge/cmux-client.ts b/src/bridge/cmux-client.ts new file mode 100644 index 0000000..8fa9b73 --- /dev/null +++ b/src/bridge/cmux-client.ts @@ -0,0 +1,207 @@ +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import type { WorkspaceInfo, PaneInfo, SurfaceInfo } from '../protocol/messages.js'; + +const execFileAsync = promisify(execFile); + +const MAX_CONCURRENT = 5; +const CACHE_TTL_MS = 2000; + +interface CacheEntry { + data: T; + timestamp: number; +} + +export class CmuxClient { + private cmuxPath: string; + private running = 0; + private queue: Array<() => void> = []; + private cache = new Map>(); + private shuttingDown = false; + + constructor(cmuxPath = 'cmux') { + this.cmuxPath = cmuxPath; + } + + shutdown(): void { + this.shuttingDown = true; + } + + private async throttle(): Promise { + if (this.running < MAX_CONCURRENT) { + this.running++; + return; + } + return new Promise((resolve) => { + this.queue.push(() => { + this.running++; + resolve(); + }); + }); + } + + private release(): void { + this.running--; + const next = this.queue.shift(); + if (next) next(); + } + + private getCached(key: string): T | null { + const entry = this.cache.get(key); + if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) { + return entry.data as T; + } + this.cache.delete(key); + return null; + } + + private setCache(key: string, data: T): void { + this.cache.set(key, { data, timestamp: Date.now() }); + } + + async exec(args: string[]): Promise { + if (this.shuttingDown) return ''; + await this.throttle(); + try { + const { stdout } = await execFileAsync(this.cmuxPath, args, { + timeout: 10000, + maxBuffer: 1024 * 1024, + }); + return stdout; + } catch (err) { + if (this.shuttingDown) return ''; + throw err; + } finally { + this.release(); + } + } + + async listWorkspaces(): Promise { + const cached = this.getCached('workspaces'); + if (cached) return cached; + + const output = await this.exec(['tree', '--all']); + const workspaces = this.parseTree(output); + this.setCache('workspaces', workspaces); + return workspaces; + } + + parseTree(output: string): WorkspaceInfo[] { + const workspaces: WorkspaceInfo[] = []; + let currentWs: WorkspaceInfo | null = null; + let currentPane: PaneInfo | null = null; + + for (const line of output.split('\n')) { + // Strip tree-drawing characters and leading whitespace + const stripped = line.replace(/^[\s│├└─┬┤┼┌┐┘┴]+/g, '').trim(); + if (!stripped) continue; + + // Match workspace lines: "workspace workspace:1 "name" [flags]" + const wsMatch = stripped.match(/^workspace\s+(workspace:\d+)\s*(.*)/); + if (wsMatch) { + // Extract quoted name if present + const nameMatch = wsMatch[2].match(/"([^"]+)"/); + currentWs = { + id: wsMatch[1], + ref: wsMatch[1], + name: nameMatch?.[1] || undefined, + panes: [], + }; + workspaces.push(currentWs); + currentPane = null; + continue; + } + + // Match pane lines: "pane pane:2 [flags]" + const paneMatch = stripped.match(/^pane\s+(pane:\d+)/); + if (paneMatch && currentWs) { + currentPane = { + id: paneMatch[1], + ref: paneMatch[1], + surfaces: [], + }; + currentWs.panes.push(currentPane); + continue; + } + + // Match surface lines: "surface surface:2 [terminal] "title" [flags]" + const surfaceMatch = stripped.match(/^surface\s+(surface:\d+)\s*(?:\[(\w+)\])?\s*(.*)/); + if (surfaceMatch && currentPane) { + const titleMatch = surfaceMatch[3].match(/"([^"]+)"/); + currentPane.surfaces.push({ + id: surfaceMatch[1], + ref: surfaceMatch[1], + type: surfaceMatch[2] || 'terminal', + title: titleMatch?.[1] || undefined, + }); + } + } + + return workspaces; + } + + async listPaneSurfaces(workspace: string, pane?: string): Promise { + const cacheKey = `surfaces:${workspace}:${pane || 'all'}`; + const cached = this.getCached(cacheKey); + if (cached) return cached; + + const args = ['list-pane-surfaces', '--workspace', workspace]; + if (pane) args.push('--pane', pane); + + const output = await this.exec(args); + const surfaces = this.parseSurfaces(output); + this.setCache(cacheKey, surfaces); + return surfaces; + } + + parseSurfaces(output: string): SurfaceInfo[] { + const surfaces: SurfaceInfo[] = []; + for (const line of output.split('\n')) { + const trimmed = line.trim(); + if (!trimmed) continue; + const match = trimmed.match(/^(surface:\d+)\s*(?:\[(\w+)\])?\s*(.*)/); + if (match) { + surfaces.push({ + id: match[1], + ref: match[1], + type: match[2] || 'terminal', + title: match[3]?.trim() || undefined, + }); + } + } + return surfaces; + } + + async readScreen( + workspace: string, + surface: string, + opts?: { scrollback?: boolean; lines?: number } + ): Promise { + const args = ['read-screen', '--workspace', workspace, '--surface', surface]; + if (opts?.scrollback) args.push('--scrollback'); + if (opts?.lines) args.push('--lines', String(opts.lines)); + return this.exec(args); + } + + async sendText(workspace: string, surface: string, text: string): Promise { + await this.exec(['send', '--workspace', workspace, '--surface', surface, text]); + } + + async sendKey(workspace: string, surface: string, key: string): Promise { + await this.exec(['send-key', '--workspace', workspace, '--surface', surface, key]); + } + + async capturePaneScrollback( + workspace: string, + surface: string, + lines?: number + ): Promise { + const args = ['capture-pane', '--workspace', workspace, '--surface', surface, '--scrollback']; + if (lines) args.push('--lines', String(lines)); + return this.exec(args); + } + + clearCache(): void { + this.cache.clear(); + } +} diff --git a/src/bridge/input-handler.ts b/src/bridge/input-handler.ts new file mode 100644 index 0000000..01fab5f --- /dev/null +++ b/src/bridge/input-handler.ts @@ -0,0 +1,53 @@ +import type { CmuxClient } from './cmux-client.js'; + +// Map browser key names to cmux send-key names +const KEY_MAP: Record = { + Enter: 'enter', + Escape: 'escape', + Tab: 'tab', + Backspace: 'backspace', + Delete: 'delete', + ArrowUp: 'arrow_up', + ArrowDown: 'arrow_down', + ArrowLeft: 'arrow_left', + ArrowRight: 'arrow_right', + Home: 'home', + End: 'end', + PageUp: 'page_up', + PageDown: 'page_down', + Space: 'space', +}; + +export class InputHandler { + private client: CmuxClient; + + constructor(client: CmuxClient) { + this.client = client; + } + + async handleText(workspace: string, surface: string, text: string): Promise { + await this.client.sendText(workspace, surface, text); + } + + async handleKey(workspace: string, surface: string, key: string): Promise { + // Handle Ctrl+key combos (from frontend: "Ctrl-c", "Ctrl-d", etc.) + const ctrlMatch = key.match(/^(?:Ctrl|Control)[+-](.+)$/i); + if (ctrlMatch) { + const letter = ctrlMatch[1].toLowerCase(); + await this.client.sendKey(workspace, surface, `ctrl+${letter}`); + return; + } + + // Handle Alt+key combos + const altMatch = key.match(/^Alt[+-](.+)$/i); + if (altMatch) { + const letter = altMatch[1].toLowerCase(); + await this.client.sendKey(workspace, surface, `alt+${letter}`); + return; + } + + // Map known keys + const mapped = KEY_MAP[key] || key; + await this.client.sendKey(workspace, surface, mapped); + } +} diff --git a/src/bridge/screen-poller.ts b/src/bridge/screen-poller.ts new file mode 100644 index 0000000..35f69d1 --- /dev/null +++ b/src/bridge/screen-poller.ts @@ -0,0 +1,166 @@ +import { createHash } from 'node:crypto'; +import { EventEmitter } from 'node:events'; +import type { CmuxClient } from './cmux-client.js'; +import { computeDiff } from '../utils/text-differ.js'; +import type { DiffPatch } from '../protocol/messages.js'; + +interface PollTarget { + workspace: string; + surface: string; + subscribers: Set; + lastHash: string; + lastLines: string[]; + idleCount: number; + timer: ReturnType | null; +} + +interface ScreenUpdate { + surface: string; + workspace: string; + full: boolean; + lines: string[]; + patches?: DiffPatch[]; +} + +export class ScreenPoller extends EventEmitter { + private client: CmuxClient; + private targets = new Map(); + private basePollRate: number; + private fullSyncInterval = 30000; + private lastFullSync = new Map(); + + static readonly IDLE_THRESHOLD = 3; + static readonly DEEP_IDLE_MS = 10000; + + constructor(client: CmuxClient, pollRate = 200) { + super(); + this.client = client; + this.basePollRate = pollRate; + } + + subscribe(clientId: string, workspace: string, surface: string): void { + const key = `${workspace}:${surface}`; + let target = this.targets.get(key); + + if (!target) { + target = { + workspace, + surface, + subscribers: new Set(), + lastHash: '', + lastLines: [], + idleCount: 0, + timer: null, + }; + this.targets.set(key, target); + } + + target.subscribers.add(clientId); + + if (!target.timer) { + this.schedulePoll(key, target, 0); + } + } + + unsubscribe(clientId: string, surface?: string): void { + for (const [key, target] of this.targets) { + if (surface && !key.endsWith(`:${surface}`)) continue; + + target.subscribers.delete(clientId); + + if (target.subscribers.size === 0) { + if (target.timer) clearTimeout(target.timer); + target.timer = null; + this.targets.delete(key); + this.lastFullSync.delete(key); + } + } + } + + resetToFast(surface: string): void { + for (const [key, target] of this.targets) { + if (key.endsWith(`:${surface}`)) { + target.idleCount = 0; + // Reschedule with fast rate + if (target.timer) clearTimeout(target.timer); + this.schedulePoll(key, target, this.basePollRate); + } + } + } + + private getInterval(target: PollTarget): number { + if (target.idleCount >= ScreenPoller.IDLE_THRESHOLD) { + const idleTimeMs = target.idleCount * this.basePollRate; + if (idleTimeMs > ScreenPoller.DEEP_IDLE_MS) return 2000; + return 1000; + } + return this.basePollRate; + } + + private schedulePoll(key: string, target: PollTarget, delay: number): void { + target.timer = setTimeout(() => this.poll(key, target), delay); + } + + private async poll(key: string, target: PollTarget): Promise { + if (target.subscribers.size === 0) return; + + try { + const content = await this.client.readScreen(target.workspace, target.surface); + const hash = createHash('sha256').update(content).digest('hex'); + + const now = Date.now(); + const needsFullSync = + !this.lastFullSync.has(key) || + now - this.lastFullSync.get(key)! > this.fullSyncInterval; + + if (hash !== target.lastHash) { + target.idleCount = 0; + const newLines = content.split('\n'); + + const update: ScreenUpdate = { + surface: target.surface, + workspace: target.workspace, + full: target.lastLines.length === 0 || needsFullSync, + lines: newLines, + }; + + if (!update.full && target.lastLines.length > 0) { + update.patches = computeDiff(target.lastLines, newLines); + } + + if (needsFullSync) { + this.lastFullSync.set(key, now); + } + + target.lastHash = hash; + target.lastLines = newLines; + + this.emit('update', update); + } else { + target.idleCount++; + } + } catch (err) { + this.emit('error', { surface: target.surface, error: err }); + } + + // Schedule next poll + if (target.subscribers.size > 0) { + this.schedulePoll(key, target, this.getInterval(target)); + } + } + + getSubscriberCount(surface: string): number { + for (const [key, target] of this.targets) { + if (key.endsWith(`:${surface}`)) return target.subscribers.size; + } + return 0; + } + + destroy(): void { + for (const target of this.targets.values()) { + if (target.timer) clearTimeout(target.timer); + } + this.targets.clear(); + this.removeAllListeners(); + } +} diff --git a/src/bridge/session-manager.ts b/src/bridge/session-manager.ts new file mode 100644 index 0000000..d73924f --- /dev/null +++ b/src/bridge/session-manager.ts @@ -0,0 +1,88 @@ +import type { ScreenPoller } from './screen-poller.js'; + +interface ClientSession { + id: string; + authenticated: boolean; + subscriptions: Set; // "workspace:surface" keys +} + +export class SessionManager { + private sessions = new Map(); + private poller: ScreenPoller; + + constructor(poller: ScreenPoller) { + this.poller = poller; + } + + addClient(clientId: string): void { + this.sessions.set(clientId, { + id: clientId, + authenticated: false, + subscriptions: new Set(), + }); + } + + authenticateClient(clientId: string): void { + const session = this.sessions.get(clientId); + if (session) session.authenticated = true; + } + + isAuthenticated(clientId: string): boolean { + return this.sessions.get(clientId)?.authenticated ?? false; + } + + subscribe(clientId: string, workspace: string, surface: string): void { + const session = this.sessions.get(clientId); + if (!session) return; + + const key = `${workspace}:${surface}`; + session.subscriptions.add(key); + this.poller.subscribe(clientId, workspace, surface); + } + + unsubscribe(clientId: string, surface: string): void { + const session = this.sessions.get(clientId); + if (!session) return; + + for (const key of session.subscriptions) { + if (key.endsWith(`:${surface}`)) { + session.subscriptions.delete(key); + } + } + this.poller.unsubscribe(clientId, surface); + } + + removeClient(clientId: string): void { + const session = this.sessions.get(clientId); + if (!session) return; + + // Unsubscribe from all surfaces + this.poller.unsubscribe(clientId); + this.sessions.delete(clientId); + } + + getSubscribedClients(surface: string): string[] { + const clients: string[] = []; + for (const [id, session] of this.sessions) { + for (const key of session.subscriptions) { + if (key.endsWith(`:${surface}`)) { + clients.push(id); + break; + } + } + } + return clients; + } + + getClientCount(): number { + return this.sessions.size; + } + + getAuthenticatedCount(): number { + let count = 0; + for (const session of this.sessions.values()) { + if (session.authenticated) count++; + } + return count; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..653ac37 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,123 @@ +import { createServer } from 'node:http'; +import { Command } from 'commander'; +import qrcode from 'qrcode-terminal'; +import { createApp } from './server/app.js'; +import { AuthManager, generatePassphrase } from './server/auth.js'; +import { WsServer } from './server/websocket.js'; +import { CloudflareTunnel } from './server/tunnel.js'; +import { CmuxClient } from './bridge/cmux-client.js'; +import { ScreenPoller } from './bridge/screen-poller.js'; +import { SessionManager } from './bridge/session-manager.js'; +import { InputHandler } from './bridge/input-handler.js'; +import type { Config } from './utils/config.js'; +import { DEFAULT_CONFIG } from './utils/config.js'; + +const program = new Command(); + +program + .name('cmux-remote') + .description('Browser-based remote terminal control for cmux') + .version('0.1.0') + .option('-p, --port ', 'Local port', String(DEFAULT_CONFIG.port)) + .option('-P, --password ', 'Access password (or CMUX_REMOTE_PASSWORD env)') + .option('--no-tunnel', 'Disable Cloudflare Tunnel') + .option('--poll-rate ', 'Poll interval in ms', String(DEFAULT_CONFIG.pollRate)) + .option('--theme ', 'Default theme (dark|light)', DEFAULT_CONFIG.theme) + .option('-v, --verbose', 'Verbose logging', false) + .action(async (opts) => { + const config: Config = { + port: parseInt(opts.port, 10), + password: opts.password || process.env.CMUX_REMOTE_PASSWORD || null, + tunnel: opts.tunnel !== false, + pollRate: parseInt(opts.pollRate, 10), + theme: opts.theme as 'dark' | 'light', + verbose: opts.verbose, + cmuxPath: process.env.CMUX_PATH || 'cmux', + }; + + // Initialize components + const auth = new AuthManager(); + if (config.password) { + await auth.setPassword(config.password); + } else { + auth.disableAuth(); + } + + const cmux = new CmuxClient(config.cmuxPath); + const poller = new ScreenPoller(cmux, config.pollRate); + const sessions = new SessionManager(poller); + const input = new InputHandler(cmux); + + // Create HTTP server; qrUrlRef is updated after tunnel starts + const qrUrlRef = { url: `http://localhost:${config.port}` }; + const app = createApp(auth, qrUrlRef); + const server = createServer(app); + + // WebSocket server + const wsServer = new WsServer({ + server, + auth, + sessions, + poller, + input, + cmux, + verbose: config.verbose, + }); + + // Start listening + server.listen(config.port, () => { + console.log(''); + console.log(' cmux-remote v0.1.0'); + console.log(' =================='); + console.log(''); + console.log(` Local: http://localhost:${config.port}`); + if (config.password) { + console.log(` Password: ${config.password}`); + } else { + console.log(` Auth: disabled (use -P to set password)`); + } + console.log(''); + }); + + // Start tunnel + let tunnel: CloudflareTunnel | null = null; + if (config.tunnel) { + tunnel = new CloudflareTunnel(); + try { + const tunnelUrl = await tunnel.start(config.port); + qrUrlRef.url = tunnelUrl; + console.log(` Tunnel: ${tunnelUrl}`); + } catch (err) { + console.warn(` Tunnel: Failed - ${(err as Error).message}`); + } + } + + // Print QR code to terminal + console.log(''); + qrcode.generate(qrUrlRef.url, { small: true }, (code: string) => { + for (const line of code.split('\n')) { + console.log(` ${line}`); + } + console.log(''); + }); + + // Graceful shutdown + let shuttingDown = false; + const shutdown = () => { + if (shuttingDown) return; + shuttingDown = true; + console.log('\nShutting down...'); + cmux.shutdown(); + poller.destroy(); + wsServer.close(); + tunnel?.stop(); + server.close(() => process.exit(0)); + // Force exit after 3s if connections don't drain + setTimeout(() => process.exit(0), 3000).unref(); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + }); + +program.parse(); diff --git a/src/protocol/messages.ts b/src/protocol/messages.ts new file mode 100644 index 0000000..ed5fe57 --- /dev/null +++ b/src/protocol/messages.ts @@ -0,0 +1,119 @@ +// Server -> Client messages +export interface ScreenMessage { + type: 'screen'; + surface: string; + content: string; + lines: string[]; + cursor?: { row: number; col: number }; +} + +export interface ScreenDiffMessage { + type: 'screen-diff'; + surface: string; + patches: DiffPatch[]; +} + +export interface DiffPatch { + startLine: number; + deleteCount: number; + insertLines: string[]; +} + +export interface WorkspacesMessage { + type: 'workspaces'; + workspaces: WorkspaceInfo[]; +} + +export interface WorkspaceInfo { + id: string; + ref: string; + name?: string; + panes: PaneInfo[]; +} + +export interface PaneInfo { + id: string; + ref: string; + surfaces: SurfaceInfo[]; +} + +export interface SurfaceInfo { + id: string; + ref: string; + type: string; + title?: string; +} + +export interface AuthResultMessage { + type: 'auth-ok' | 'auth-fail'; + message?: string; +} + +export interface ErrorMessage { + type: 'error'; + message: string; +} + +export type ServerMessage = + | ScreenMessage + | ScreenDiffMessage + | WorkspacesMessage + | AuthResultMessage + | ErrorMessage; + +// Client -> Server messages +export interface AuthMessage { + type: 'auth'; + token: string; +} + +export interface SubscribeMessage { + type: 'subscribe'; + workspace: string; + surface: string; +} + +export interface UnsubscribeMessage { + type: 'unsubscribe'; + surface: string; +} + +export interface SendTextMessage { + type: 'send-text'; + workspace: string; + surface: string; + text: string; +} + +export interface SendKeyMessage { + type: 'send-key'; + workspace: string; + surface: string; + key: string; +} + +export interface ListWorkspacesMessage { + type: 'list-workspaces'; +} + +export interface ListSurfacesMessage { + type: 'list-surfaces'; + workspace: string; +} + +export interface ScrollRequestMessage { + type: 'scroll-request'; + workspace: string; + surface: string; + lines: number; +} + +export type ClientMessage = + | AuthMessage + | SubscribeMessage + | UnsubscribeMessage + | SendTextMessage + | SendKeyMessage + | ListWorkspacesMessage + | ListSurfacesMessage + | ScrollRequestMessage; diff --git a/src/server/app.ts b/src/server/app.ts new file mode 100644 index 0000000..d247ee5 --- /dev/null +++ b/src/server/app.ts @@ -0,0 +1,73 @@ +import express from 'express'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import QRCode from 'qrcode'; +import type { AuthManager } from './auth.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export function createApp(auth: AuthManager, qrUrlRef: { url: string }): express.Express { + const app = express(); + + // Security headers + app.use(auth.cspMiddleware()); + + // Parse JSON bodies + app.use(express.json()); + + // QR code endpoint — generates SVG for the current access URL + app.get('/api/qr.svg', async (_req, res) => { + try { + const svg = await QRCode.toString(qrUrlRef.url, { type: 'svg' }); + res.setHeader('Content-Type', 'image/svg+xml'); + res.send(svg); + } catch { + res.status(500).send('QR generation failed'); + } + }); + + // Access URL endpoint — returns the current access URL (tunnel or local) + app.get('/api/access-url', (_req, res) => { + res.json({ url: qrUrlRef.url }); + }); + + // Auth status endpoint + app.get('/api/auth/status', (_req, res) => { + res.json({ authRequired: !auth.isAuthDisabled() }); + }); + + // Auth endpoint + app.post('/api/auth/login', auth.loginHandler()); + + // Token verification endpoint + app.get('/api/auth/verify', (req, res) => { + if (auth.isAuthDisabled()) { + res.json({ valid: true }); + return; + } + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + res.status(401).json({ valid: false }); + return; + } + const token = authHeader.slice(7); + const valid = auth.verifyToken(token); + res.json({ valid }); + }); + + // Static files — no-cache so changes are always picked up + const publicDir = join(__dirname, '..', '..', 'public'); + app.use(express.static(publicDir, { + setHeaders: (res) => { + res.setHeader('Cache-Control', 'no-store'); + }, + })); + + // SPA fallback — serve index.html for non-API, non-file routes + app.get('/{*splat}', (_req, res) => { + res.sendFile(join(publicDir, 'index.html')); + }); + + return app; +} diff --git a/src/server/auth.ts b/src/server/auth.ts new file mode 100644 index 0000000..13ab99a --- /dev/null +++ b/src/server/auth.ts @@ -0,0 +1,140 @@ +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { randomBytes } from 'node:crypto'; +import type { Request, Response, NextFunction } from 'express'; + +const SALT_ROUNDS = 10; +const TOKEN_EXPIRY = '24h'; +const MAX_ATTEMPTS = 5; +const RATE_WINDOW_MS = 60000; + +interface AttemptRecord { + count: number; + firstAttempt: number; +} + +export class AuthManager { + private passwordHash: string | null = null; + private jwtSecret: string; + private attempts = new Map(); + private authDisabled = false; + + constructor() { + this.jwtSecret = randomBytes(32).toString('hex'); + } + + disableAuth(): void { + this.authDisabled = true; + } + + isAuthDisabled(): boolean { + return this.authDisabled; + } + + async setPassword(password: string): Promise { + this.passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + } + + async verifyPassword(password: string): Promise { + if (!this.passwordHash) return false; + return bcrypt.compare(password, this.passwordHash); + } + + generateToken(): string { + return jwt.sign({ ts: Date.now() }, this.jwtSecret, { expiresIn: TOKEN_EXPIRY }); + } + + verifyToken(token: string): boolean { + if (this.authDisabled) return true; + try { + jwt.verify(token, this.jwtSecret); + return true; + } catch { + return false; + } + } + + isRateLimited(ip: string): boolean { + const record = this.attempts.get(ip); + if (!record) return false; + + // Reset if window expired + if (Date.now() - record.firstAttempt > RATE_WINDOW_MS) { + this.attempts.delete(ip); + return false; + } + + return record.count >= MAX_ATTEMPTS; + } + + recordAttempt(ip: string): void { + const record = this.attempts.get(ip); + if (!record || Date.now() - record.firstAttempt > RATE_WINDOW_MS) { + this.attempts.set(ip, { count: 1, firstAttempt: Date.now() }); + } else { + record.count++; + } + } + + resetAttempts(ip: string): void { + this.attempts.delete(ip); + } + + loginHandler() { + return async (req: Request, res: Response): Promise => { + const ip = req.ip || req.socket.remoteAddress || 'unknown'; + + if (this.isRateLimited(ip)) { + res.status(429).json({ error: 'Too many attempts. Try again later.' }); + return; + } + + const { password } = req.body as { password?: string }; + if (!password) { + res.status(400).json({ error: 'Password required' }); + return; + } + + this.recordAttempt(ip); + + const valid = await this.verifyPassword(password); + if (!valid) { + console.log(`[auth] Failed login from ${ip}`); + res.status(401).json({ error: 'Invalid password' }); + return; + } + + this.resetAttempts(ip); + const token = this.generateToken(); + console.log(`[auth] Successful login from ${ip}`); + res.json({ token }); + }; + } + + cspMiddleware() { + return (_req: Request, res: Response, next: NextFunction): void => { + res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' wss: ws:; img-src 'self' data:;" + ); + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('Referrer-Policy', 'no-referrer'); + next(); + }; + } +} + +export function generatePassphrase(): string { + const words = [ + 'alpha', 'bravo', 'cedar', 'delta', 'ember', 'frost', 'grape', 'hatch', + 'ivory', 'junco', 'knack', 'lunar', 'maple', 'north', 'orbit', 'pearl', + 'quilt', 'ridge', 'solar', 'thorn', 'ultra', 'vivid', 'watch', 'xenon', + ]; + const picked: string[] = []; + const bytes = randomBytes(4); + for (let i = 0; i < 4; i++) { + picked.push(words[bytes[i] % words.length]); + } + return picked.join('-'); +} diff --git a/src/server/tunnel.ts b/src/server/tunnel.ts new file mode 100644 index 0000000..f2480d9 --- /dev/null +++ b/src/server/tunnel.ts @@ -0,0 +1,65 @@ +import { spawn, type ChildProcess } from 'node:child_process'; + +export class CloudflareTunnel { + private process: ChildProcess | null = null; + private url: string | null = null; + + async start(port: number): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Tunnel startup timed out after 30s')); + }, 30000); + + this.process = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], { + stdio: ['ignore', 'pipe', 'pipe'], + }); + + const handleOutput = (data: Buffer) => { + const text = data.toString(); + // cloudflared prints the URL to stderr + const urlMatch = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/); + if (urlMatch && !this.url) { + this.url = urlMatch[0]; + clearTimeout(timeout); + resolve(this.url); + } + }; + + this.process.stdout?.on('data', handleOutput); + this.process.stderr?.on('data', handleOutput); + + this.process.on('error', (err) => { + clearTimeout(timeout); + if (err.message.includes('ENOENT')) { + reject( + new Error( + 'cloudflared not found. Install with: brew install cloudflared\n' + + 'Or disable tunnel with: --no-tunnel' + ) + ); + } else { + reject(err); + } + }); + + this.process.on('exit', (code) => { + if (!this.url) { + clearTimeout(timeout); + reject(new Error(`cloudflared exited with code ${code}`)); + } + }); + }); + } + + getUrl(): string | null { + return this.url; + } + + stop(): void { + if (this.process) { + this.process.kill('SIGTERM'); + this.process = null; + this.url = null; + } + } +} diff --git a/src/server/websocket.ts b/src/server/websocket.ts new file mode 100644 index 0000000..2c524cc --- /dev/null +++ b/src/server/websocket.ts @@ -0,0 +1,197 @@ +import { WebSocketServer, WebSocket } from 'ws'; +import type { Server } from 'node:http'; +import { nanoid } from 'nanoid'; +import type { AuthManager } from './auth.js'; +import type { SessionManager } from '../bridge/session-manager.js'; +import type { ScreenPoller } from '../bridge/screen-poller.js'; +import type { InputHandler } from '../bridge/input-handler.js'; +import type { CmuxClient } from '../bridge/cmux-client.js'; +import type { ClientMessage, ServerMessage } from '../protocol/messages.js'; + +interface ClientInfo { + id: string; + ws: WebSocket; +} + +export class WsServer { + private wss: WebSocketServer; + private clients = new Map(); + private auth: AuthManager; + private sessions: SessionManager; + private poller: ScreenPoller; + private input: InputHandler; + private cmux: CmuxClient; + private verbose: boolean; + + constructor(opts: { + server: Server; + auth: AuthManager; + sessions: SessionManager; + poller: ScreenPoller; + input: InputHandler; + cmux: CmuxClient; + verbose?: boolean; + }) { + this.auth = opts.auth; + this.sessions = opts.sessions; + this.poller = opts.poller; + this.input = opts.input; + this.cmux = opts.cmux; + this.verbose = opts.verbose ?? false; + + this.wss = new WebSocketServer({ + server: opts.server, + perMessageDeflate: true, + }); + + this.wss.on('connection', (ws) => this.handleConnection(ws)); + + // Listen for screen updates from poller + this.poller.on('update', (update) => this.broadcastUpdate(update)); + } + + private handleConnection(ws: WebSocket): void { + const clientId = nanoid(12); + this.clients.set(clientId, { id: clientId, ws }); + this.sessions.addClient(clientId); + + if (this.verbose) console.log(`[ws] Client connected: ${clientId}`); + + ws.on('message', (data) => { + try { + const msg = JSON.parse(data.toString()) as ClientMessage; + this.handleMessage(clientId, msg); + } catch (err) { + this.sendTo(clientId, { type: 'error', message: 'Invalid message format' }); + } + }); + + ws.on('close', () => { + if (this.verbose) console.log(`[ws] Client disconnected: ${clientId}`); + this.sessions.removeClient(clientId); + this.clients.delete(clientId); + }); + + ws.on('error', (err) => { + console.error(`[ws] Client error ${clientId}:`, err.message); + }); + } + + private async handleMessage(clientId: string, msg: ClientMessage): Promise { + // Auth messages don't require prior authentication + if (msg.type === 'auth') { + const valid = this.auth.verifyToken(msg.token); + if (valid) { + this.sessions.authenticateClient(clientId); + this.sendTo(clientId, { type: 'auth-ok' }); + } else { + this.sendTo(clientId, { type: 'auth-fail', message: 'Invalid token' }); + } + return; + } + + // All other messages require authentication + if (!this.sessions.isAuthenticated(clientId)) { + this.sendTo(clientId, { type: 'auth-fail', message: 'Not authenticated' }); + return; + } + + switch (msg.type) { + case 'subscribe': + this.sessions.subscribe(clientId, msg.workspace, msg.surface); + if (this.verbose) console.log(`[ws] ${clientId} subscribed to ${msg.surface}`); + break; + + case 'unsubscribe': + this.sessions.unsubscribe(clientId, msg.surface); + break; + + case 'send-text': + if (this.verbose) console.log(`[ws] ${clientId} send-text to ${msg.surface}`); + this.poller.resetToFast(msg.surface); + await this.input.handleText(msg.workspace, msg.surface, msg.text); + break; + + case 'send-key': + if (this.verbose) console.log(`[ws] ${clientId} send-key ${msg.key} to ${msg.surface}`); + this.poller.resetToFast(msg.surface); + await this.input.handleKey(msg.workspace, msg.surface, msg.key); + break; + + case 'list-workspaces': { + const workspaces = await this.cmux.listWorkspaces(); + this.sendTo(clientId, { type: 'workspaces', workspaces }); + break; + } + + case 'list-surfaces': { + const surfaces = await this.cmux.listPaneSurfaces(msg.workspace); + // Wrap in workspace info format + const workspaces = await this.cmux.listWorkspaces(); + this.sendTo(clientId, { type: 'workspaces', workspaces }); + break; + } + + case 'scroll-request': { + const content = await this.cmux.capturePaneScrollback( + msg.workspace, + msg.surface, + msg.lines + ); + const lines = content.split('\n'); + this.sendTo(clientId, { + type: 'screen', + surface: msg.surface, + content, + lines, + }); + break; + } + + default: + this.sendTo(clientId, { type: 'error', message: `Unknown message type` }); + } + } + + private broadcastUpdate(update: { + surface: string; + workspace: string; + full: boolean; + lines: string[]; + patches?: Array<{ startLine: number; deleteCount: number; insertLines: string[] }>; + }): void { + const subscribedClients = this.sessions.getSubscribedClients(update.surface); + + for (const clientId of subscribedClients) { + if (update.full || !update.patches) { + this.sendTo(clientId, { + type: 'screen', + surface: update.surface, + content: update.lines.join('\n'), + lines: update.lines, + }); + } else { + this.sendTo(clientId, { + type: 'screen-diff', + surface: update.surface, + patches: update.patches, + }); + } + } + } + + private sendTo(clientId: string, msg: ServerMessage): void { + const client = this.clients.get(clientId); + if (client && client.ws.readyState === WebSocket.OPEN) { + client.ws.send(JSON.stringify(msg)); + } + } + + getClientCount(): number { + return this.clients.size; + } + + close(): void { + this.wss.close(); + } +} diff --git a/src/types/qrcode-terminal.d.ts b/src/types/qrcode-terminal.d.ts new file mode 100644 index 0000000..c20fbe8 --- /dev/null +++ b/src/types/qrcode-terminal.d.ts @@ -0,0 +1,8 @@ +declare module 'qrcode-terminal' { + interface Options { + small?: boolean; + } + function generate(text: string, opts?: Options, callback?: (code: string) => void): void; + export { generate }; + export default { generate }; +} diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000..91837d7 --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,19 @@ +export interface Config { + port: number; + password: string | null; + tunnel: boolean; + pollRate: number; + theme: 'dark' | 'light'; + verbose: boolean; + cmuxPath: string; +} + +export const DEFAULT_CONFIG: Config = { + port: 9870, + password: null, + tunnel: true, + pollRate: 200, + theme: 'dark', + verbose: false, + cmuxPath: 'cmux', +}; diff --git a/src/utils/text-differ.ts b/src/utils/text-differ.ts new file mode 100644 index 0000000..f6294c1 --- /dev/null +++ b/src/utils/text-differ.ts @@ -0,0 +1,47 @@ +import type { DiffPatch } from '../protocol/messages.js'; + +export function computeDiff(oldLines: string[], newLines: string[]): DiffPatch[] { + const patches: DiffPatch[] = []; + const maxLen = Math.max(oldLines.length, newLines.length); + + let i = 0; + while (i < maxLen) { + // Skip identical lines + if (i < oldLines.length && i < newLines.length && oldLines[i] === newLines[i]) { + i++; + continue; + } + + // Found a difference — collect consecutive changed lines + const start = i; + + // Advance through differing lines + while ( + i < maxLen && + !(i < oldLines.length && i < newLines.length && oldLines[i] === newLines[i]) + ) { + i++; + } + + patches.push({ + startLine: start, + deleteCount: Math.min(i, oldLines.length) - start, + insertLines: newLines.slice(start, Math.min(i, newLines.length)), + }); + } + + return patches; +} + +export function applyDiff(lines: string[], patches: DiffPatch[]): string[] { + const result = [...lines]; + let offset = 0; + + for (const patch of patches) { + const adjustedStart = patch.startLine + offset; + result.splice(adjustedStart, patch.deleteCount, ...patch.insertLines); + offset += patch.insertLines.length - patch.deleteCount; + } + + return result; +} diff --git a/test/bridge/cmux-client.test.ts b/test/bridge/cmux-client.test.ts new file mode 100644 index 0000000..cd727e4 --- /dev/null +++ b/test/bridge/cmux-client.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from 'vitest'; +import { CmuxClient } from '../../src/bridge/cmux-client.js'; + +describe('CmuxClient', () => { + describe('parseTree', () => { + it('parses real cmux tree output with tree-drawing chars', () => { + const client = new CmuxClient(); + const output = `window window:1 [current] +├── workspace workspace:1 "My Project" [selected] +│ ├── pane pane:2 +│ │ ├── surface surface:2 [terminal] "zsh" +│ │ └── surface surface:13 [terminal] "node" +│ └── pane pane:3 [focused] +│ └── surface surface:4 [browser] "localhost" +└── workspace workspace:2 "Notes" + └── pane pane:6 [focused] + └── surface surface:6 [terminal] "vim" [selected]`; + + const result = client.parseTree(output); + + expect(result).toHaveLength(2); + expect(result[0].ref).toBe('workspace:1'); + expect(result[0].name).toBe('My Project'); + expect(result[0].panes).toHaveLength(2); + expect(result[0].panes[0].surfaces).toHaveLength(2); + expect(result[0].panes[0].surfaces[0].type).toBe('terminal'); + expect(result[0].panes[0].surfaces[0].title).toBe('zsh'); + expect(result[0].panes[1].surfaces[0].type).toBe('browser'); + expect(result[0].panes[1].surfaces[0].title).toBe('localhost'); + expect(result[1].panes[0].surfaces[0].title).toBe('vim'); + }); + + it('handles empty output', () => { + const client = new CmuxClient(); + expect(client.parseTree('')).toEqual([]); + }); + + it('handles workspace without quoted name', () => { + const client = new CmuxClient(); + const output = `window window:1 +├── workspace workspace:1 [selected] +│ └── pane pane:1 +│ └── surface surface:1 [terminal] [selected]`; + + const result = client.parseTree(output); + expect(result[0].name).toBeUndefined(); + }); + + it('extracts titles with special characters', () => { + const client = new CmuxClient(); + const output = `window window:1 +└── workspace workspace:5 "✳ cmux-remote-plugin" + └── pane pane:9 [focused] + └── surface surface:14 [terminal] "⠂ cmux-remote-plugin" [selected]`; + + const result = client.parseTree(output); + expect(result[0].name).toBe('✳ cmux-remote-plugin'); + expect(result[0].panes[0].surfaces[0].title).toBe('⠂ cmux-remote-plugin'); + }); + }); + + describe('parseSurfaces', () => { + it('parses surface list', () => { + const client = new CmuxClient(); + const output = `surface:1 [terminal] zsh +surface:2 [browser] localhost`; + + const result = client.parseSurfaces(output); + expect(result).toHaveLength(2); + expect(result[0].ref).toBe('surface:1'); + expect(result[0].type).toBe('terminal'); + expect(result[1].type).toBe('browser'); + }); + }); +}); diff --git a/test/bridge/screen-poller.test.ts b/test/bridge/screen-poller.test.ts new file mode 100644 index 0000000..8b27188 --- /dev/null +++ b/test/bridge/screen-poller.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ScreenPoller } from '../../src/bridge/screen-poller.js'; + +// Mock CmuxClient +function createMockClient(screenContent = 'line1\nline2\nline3') { + return { + readScreen: vi.fn().mockResolvedValue(screenContent), + sendText: vi.fn(), + sendKey: vi.fn(), + } as any; +} + +describe('ScreenPoller', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + it('emits update on first poll', async () => { + const client = createMockClient(); + const poller = new ScreenPoller(client, 200); + const updates: any[] = []; + + poller.on('update', (u: any) => updates.push(u)); + poller.subscribe('client1', 'workspace:1', 'surface:1'); + + // Fast-forward past first poll + await vi.advanceTimersByTimeAsync(50); + + expect(updates.length).toBe(1); + expect(updates[0].full).toBe(true); + expect(updates[0].lines).toEqual(['line1', 'line2', 'line3']); + + poller.destroy(); + }); + + it('does not emit when content is unchanged', async () => { + const client = createMockClient(); + const poller = new ScreenPoller(client, 200); + const updates: any[] = []; + + poller.on('update', (u: any) => updates.push(u)); + poller.subscribe('client1', 'workspace:1', 'surface:1'); + + // First poll + await vi.advanceTimersByTimeAsync(50); + expect(updates.length).toBe(1); + + // Second poll — same content + await vi.advanceTimersByTimeAsync(250); + expect(updates.length).toBe(1); + + poller.destroy(); + }); + + it('sends diff patches when content changes', async () => { + const client = createMockClient('line1\nline2\nline3'); + const poller = new ScreenPoller(client, 200); + const updates: any[] = []; + + poller.on('update', (u: any) => updates.push(u)); + poller.subscribe('client1', 'workspace:1', 'surface:1'); + + // First poll + await vi.advanceTimersByTimeAsync(50); + expect(updates.length).toBe(1); + + // Change content + client.readScreen.mockResolvedValue('line1\nline2-changed\nline3'); + + // Second poll + await vi.advanceTimersByTimeAsync(250); + expect(updates.length).toBe(2); + expect(updates[1].full).toBe(false); + expect(updates[1].patches).toBeDefined(); + + poller.destroy(); + }); + + it('stops polling when all subscribers unsubscribe', async () => { + const client = createMockClient(); + const poller = new ScreenPoller(client, 200); + + poller.subscribe('client1', 'workspace:1', 'surface:1'); + await vi.advanceTimersByTimeAsync(50); + + poller.unsubscribe('client1', 'surface:1'); + + const callCount = client.readScreen.mock.calls.length; + await vi.advanceTimersByTimeAsync(500); + + // Should not have polled again + expect(client.readScreen.mock.calls.length).toBe(callCount); + + poller.destroy(); + }); + + it('resets to fast polling on resetToFast', async () => { + const client = createMockClient(); + const poller = new ScreenPoller(client, 200); + + poller.subscribe('client1', 'workspace:1', 'surface:1'); + await vi.advanceTimersByTimeAsync(50); + + // Simulate idle + for (let i = 0; i < 5; i++) { + await vi.advanceTimersByTimeAsync(250); + } + + poller.resetToFast('surface:1'); + + // Should have reset idle count internally + poller.destroy(); + }); +}); diff --git a/test/server/auth.test.ts b/test/server/auth.test.ts new file mode 100644 index 0000000..725bdfd --- /dev/null +++ b/test/server/auth.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { AuthManager, generatePassphrase } from '../../src/server/auth.js'; + +describe('AuthManager', () => { + let auth: AuthManager; + + beforeEach(async () => { + auth = new AuthManager(); + await auth.setPassword('test-password'); + }); + + describe('password verification', () => { + it('accepts correct password', async () => { + expect(await auth.verifyPassword('test-password')).toBe(true); + }); + + it('rejects wrong password', async () => { + expect(await auth.verifyPassword('wrong')).toBe(false); + }); + }); + + describe('JWT tokens', () => { + it('generates and verifies a valid token', () => { + const token = auth.generateToken(); + expect(auth.verifyToken(token)).toBe(true); + }); + + it('rejects invalid tokens', () => { + expect(auth.verifyToken('not-a-real-token')).toBe(false); + }); + + it('rejects tokens from different instance', async () => { + const token = auth.generateToken(); + const other = new AuthManager(); + await other.setPassword('x'); + expect(other.verifyToken(token)).toBe(false); + }); + }); + + describe('rate limiting', () => { + it('allows initial attempts', () => { + expect(auth.isRateLimited('1.2.3.4')).toBe(false); + }); + + it('blocks after max attempts', () => { + for (let i = 0; i < 5; i++) { + auth.recordAttempt('1.2.3.4'); + } + expect(auth.isRateLimited('1.2.3.4')).toBe(true); + }); + + it('does not affect other IPs', () => { + for (let i = 0; i < 5; i++) { + auth.recordAttempt('1.2.3.4'); + } + expect(auth.isRateLimited('5.6.7.8')).toBe(false); + }); + + it('resets after successful auth', () => { + for (let i = 0; i < 3; i++) { + auth.recordAttempt('1.2.3.4'); + } + auth.resetAttempts('1.2.3.4'); + expect(auth.isRateLimited('1.2.3.4')).toBe(false); + }); + }); +}); + +describe('generatePassphrase', () => { + it('generates a 4-word passphrase', () => { + const phrase = generatePassphrase(); + const words = phrase.split('-'); + expect(words.length).toBe(4); + for (const word of words) { + expect(word.length).toBeGreaterThan(0); + } + }); + + it('generates different passphrases', () => { + const a = generatePassphrase(); + const b = generatePassphrase(); + // Extremely unlikely to be equal + expect(a).not.toBe(b); + }); +}); diff --git a/test/utils/text-differ.test.ts b/test/utils/text-differ.test.ts new file mode 100644 index 0000000..311b48a --- /dev/null +++ b/test/utils/text-differ.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { computeDiff, applyDiff } from '../../src/utils/text-differ.js'; + +describe('text-differ', () => { + describe('computeDiff', () => { + it('returns empty patches for identical content', () => { + const lines = ['a', 'b', 'c']; + expect(computeDiff(lines, lines)).toEqual([]); + }); + + it('detects a single line change', () => { + const oldLines = ['a', 'b', 'c']; + const newLines = ['a', 'B', 'c']; + const patches = computeDiff(oldLines, newLines); + + expect(patches.length).toBe(1); + expect(patches[0].startLine).toBe(1); + expect(patches[0].deleteCount).toBe(1); + expect(patches[0].insertLines).toEqual(['B']); + }); + + it('detects added lines', () => { + const oldLines = ['a', 'b']; + const newLines = ['a', 'b', 'c', 'd']; + const patches = computeDiff(oldLines, newLines); + + expect(patches.length).toBeGreaterThan(0); + const result = applyDiff(oldLines, patches); + expect(result).toEqual(newLines); + }); + + it('detects removed lines', () => { + const oldLines = ['a', 'b', 'c', 'd']; + const newLines = ['a', 'd']; + const patches = computeDiff(oldLines, newLines); + + const result = applyDiff(oldLines, patches); + expect(result).toEqual(newLines); + }); + }); + + describe('applyDiff', () => { + it('applies patches to reconstruct new content', () => { + const old = ['line1', 'line2', 'line3', 'line4', 'line5']; + const now = ['line1', 'CHANGED', 'line3', 'ADDED', 'line4', 'line5']; + + const patches = computeDiff(old, now); + const result = applyDiff(old, patches); + expect(result).toEqual(now); + }); + + it('handles empty old lines', () => { + const patches = computeDiff([], ['a', 'b']); + const result = applyDiff([], patches); + expect(result).toEqual(['a', 'b']); + }); + + it('handles empty new lines', () => { + const patches = computeDiff(['a', 'b'], []); + const result = applyDiff(['a', 'b'], patches); + expect(result).toEqual([]); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5bef216 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +}