diff --git a/services/backend/package.json b/services/backend/package.json index 64654d4..f04f270 100644 --- a/services/backend/package.json +++ b/services/backend/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@discordjs/voice": "^0.19.2", + "@trpc/server": "^11.18.0", "axios": "^1.16.1", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", @@ -31,10 +32,10 @@ "@biomejs/biome": "latest", "@types/express": "^5.0.6", "@types/node": "^25.9.0", + "@types/pg": "^8.20.0", "@types/ws": "^8.18.1", "tsx": "^4.22.2", "typescript": "^5.9.3", - "@types/pg": "^8.20.0", "vitest": "latest" } } diff --git a/services/backend/pnpm-lock.yaml b/services/backend/pnpm-lock.yaml new file mode 100644 index 0000000..2586174 --- /dev/null +++ b/services/backend/pnpm-lock.yaml @@ -0,0 +1,2572 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@discordjs/voice': + specifier: ^0.19.2 + version: 0.19.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@trpc/server': + specifier: ^11.18.0 + version: 11.18.0(typescript@5.9.3) + axios: + specifier: ^1.16.1 + version: 1.19.0(debug@4.4.3) + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.22.0) + express: + specifier: ^5.2.1 + version: 5.2.1 + helmet: + specifier: ^8.1.0 + version: 8.3.0 + ioredis: + specifier: ^5.11.0 + version: 5.11.1 + pg: + specifier: ^8.21.0 + version: 8.22.0 + pino: + specifier: ^9.6.0 + version: 9.14.0 + prom-client: + specifier: ^15.1.3 + version: 15.1.3 + ws: + specifier: ^8.20.1 + version: 8.21.1 + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@biomejs/biome': + specifier: latest + version: 2.5.8 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ^25.9.0 + version: 25.9.5 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + tsx: + specifier: ^4.22.2 + version: 4.23.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: latest + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(vite@8.2.0(@types/node@25.9.5)(esbuild@0.28.1)(tsx@4.23.1)) + +packages: + + '@biomejs/biome@2.5.8': + resolution: {integrity: sha512-aeAeeJB9fSDc7Gq+2GqpQxA0qBj6gj1k2R6L1cYqGePKP/baIq1WX8y6B+D+nRsO5ViQL22K/8IwbqERW0q1nw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.8': + resolution: {integrity: sha512-mk1QON9PHllvvLN5gU3f4rMxeh4syK5p9OvKyWH6/W8ueh04uaC8TUXXByhGufWf/y5mQc03ZLM45zU+cmqMjA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.8': + resolution: {integrity: sha512-bsGwFMBNyHPyiLSsQcZJxdoRrg1V4JL+d7wEsvUBczlP9U9lwM+7mzQHxI4o1mhBsTmdOBbAb6fHU3Z3snN45w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.8': + resolution: {integrity: sha512-VcJNbstduTHx83NGAdhp78/JOcP45BZHXL7yNsfI1uGzdUgegAz2s+mSoT7wK6PBNzLoqG0zDOXaz/RQYVtSiw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.8': + resolution: {integrity: sha512-XmFiA0WPYFC+uiUDC8WRFzAIH9bo7vwQLav38Uoq4ETC+T/+uBi0TsYGJECkugY3r8USl3jc+Ae2/irAF6F2lQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.8': + resolution: {integrity: sha512-kKmiyokeISRGq2FLwvr+TzsgBusfxaZ0FZNLcOYOpCK/78tRrEjeEBLvq3xLZMpqbANgJdRPI7vZX8ZL37u9/w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.8': + resolution: {integrity: sha512-S5wcm9OBDvLHodD4PUaN488hCpco9QD/9ZxuYJiw4euWtr/oQvLR72z2ixItH8Wd5BCm6FZaeb+YNvOoM1xHtQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.8': + resolution: {integrity: sha512-nILH0mzm3Hi3iEdd7o7GpB8kBR/mSQwfQG/tyBqyNrY2GFtcgwfV9nV8xLmbtUpMNY/Oi0Ml1XgfR4flOdq+AA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.8': + resolution: {integrity: sha512-I2czzXTY61f3nFJxXoMDq80t7MivxDEnCjE+8sDKoFfcKMaoQdkqhIFQ3KyY0XLzeSpUBYeNAXgD+iOV/BU0VA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@discordjs/voice@0.19.2': + resolution: {integrity: sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA==} + engines: {node: '>=22.12.0'} + + '@emnapi/core@2.0.0-alpha.3': + resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + + '@emnapi/runtime@2.0.0-alpha.3': + resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + + '@emnapi/wasi-threads@2.0.1': + resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@ioredis/commands@1.10.0': + resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.2.1': + resolution: {integrity: sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@rolldown/binding-android-arm64@1.2.1': + resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.1': + resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.1': + resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.1': + resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.1': + resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.1': + resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.1': + resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.1': + resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.1': + resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.1': + resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.2.1': + resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + + '@rolldown/binding-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@snazzah/davey-android-arm-eabi@0.1.12': + resolution: {integrity: sha512-6VC/an+Sx5dI5skb+90rYcIB1jhm48Rl0nDaw0UNT4bz1rMjpVfmmZqeocYXMq96IdbBMlE6OTKGcBm2C3gkQg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@snazzah/davey-android-arm64@0.1.12': + resolution: {integrity: sha512-0Bwd03/JsTFhlPhF4q/LW0RxSzntFpQdhz+TBdFljYSg8IEyA38saPJeTNjpIgDfhAumPzvhCdfS6O5qT8yXDw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@snazzah/davey-darwin-arm64@0.1.12': + resolution: {integrity: sha512-lKMV6ITi9BQLt0fx/pAT7M8xcojVK7bryVJGdaW3bq8gABFslS3ti/KzrWabQvhpEV71FZe5mV0UcKHFVaTsZw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@snazzah/davey-darwin-x64@0.1.12': + resolution: {integrity: sha512-vXXc/eW/e3TQeb7VsdtrPqs3/22j0aSqiP1ZXmZtDjQRBwgSxwItWYa6sh5MELP2EHB2igNlGzB6Hc0XlbGi4g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@snazzah/davey-freebsd-x64@0.1.12': + resolution: {integrity: sha512-G1gas5HrC4Xp3mRY0+OeAqXS6fG2tRgBEc8gQh69Hw4YK9RV9mzQKcmoKMkBM72U1+2C+2u57dolnKKzwozByQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@snazzah/davey-linux-arm-gnueabihf@0.1.12': + resolution: {integrity: sha512-97Fujh82r2Ll7dPZeNuoZ3yKfsqycf3c93OWXOo/ThNL/18Onl2Ht4SIvpX6VHhfeS9bDpHJ1lHCaz1b/i5ocw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@snazzah/davey-linux-arm64-gnu@0.1.12': + resolution: {integrity: sha512-FWyAOv52cHKDM4BOsmImKKogHFvqNFoXmZcicNJbX3XpVl8Mas88ZoXQ+IA5V+qc9pNtAl1MbWwEZ9JrqAQtbg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@snazzah/davey-linux-arm64-musl@0.1.12': + resolution: {integrity: sha512-ptRbLSQxtV6EjXppS5z7qaPDI0NRKhrkJYsTlAjEghmOvlAObozSCYYnMO6nbkt6Ab3+lWqyahClzcRNcD2ouw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@snazzah/davey-linux-x64-gnu@0.1.12': + resolution: {integrity: sha512-w86fZvhJn0ErOoAQHt2UbQ95V/cgwvfvQ4GlTPQLCzt58nn+rLlXLgPn90qYlSQrZxFW38rKXwqVOMbm9p+pwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@snazzah/davey-linux-x64-musl@0.1.12': + resolution: {integrity: sha512-LLNnO+hfG41ymeI+O1YHo5/0h3aKaetUNLdkBwpdJsjoyKMXZaeCnB+aHNkkupJCMPmWS0g6iPMCHUOqZSBcTg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@snazzah/davey-wasm32-wasi@0.1.12': + resolution: {integrity: sha512-MPKFuqYVkDFXheR7qmtEY4FWxQ/ADfgsCojQWHi13sibUqCTR9q2F1LqNn2i9IVh3sh1sxeg87fdFMCH63pl7g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@snazzah/davey-win32-arm64-msvc@0.1.12': + resolution: {integrity: sha512-Uf4OYHyfbXpzyaOqIV8/h6kv166Qni5+Bxmc1E/ov4uhhKO8qXbOky8zbVOzu0U4cv5ll3s4IQ8jDAJkx/K75Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@snazzah/davey-win32-ia32-msvc@0.1.12': + resolution: {integrity: sha512-nRVbKTsb2ldcPI8D4BDA7P/UeiMEMvR+wYuUMp7H1pRD/3dF2hKo+MzUU+Pn78EhuYA3CWHItiAcS/GsPld67A==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@snazzah/davey-win32-x64-msvc@0.1.12': + resolution: {integrity: sha512-AgUA3itPDVkxQq7RIkgE1thCiWePwWjyfOZefBBxGIlMWpRUOSwF4vY9kNLnrScyJcFULdR+Zm8PwiwV8/RKnw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@snazzah/davey@0.1.12': + resolution: {integrity: sha512-V+NlX5931RwVamZhhEfZekMdcvXDKdMAmHW1AuGaykVQsNyBOq3bpmGpoKRBDCYgFWKIufJ0Dcg3m4cYhvUy6g==} + engines: {node: '>= 10'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@trpc/server@11.18.0': + resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==} + hasBin: true + peerDependencies: + typescript: '>=5.7.2' + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@5.1.2': + resolution: {integrity: sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + axios@1.19.0: + resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} + + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + discord-api-types@0.38.52: + resolution: {integrity: sha512-uwe9EKfbjsmgWc2fdFjvDbj+dQqx3lp7wqDCmIha0jInuU+xeQjkCK9tMMn+p7RXfdVQORCInq4cD3U2ymDmyg==} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + helmet@8.3.0: + resolution: {integrity: sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==} + engines: {node: '>=18.0.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@5.11.1: + resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} + engines: {node: '>=12.22.0'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prism-media@1.3.5: + resolution: {integrity: sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==} + peerDependencies: + '@discordjs/opus': '>=0.8.0 <1.0.0' + ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0 + node-opus: ^0.3.3 + opusscript: ^0.0.8 + peerDependenciesMeta: + '@discordjs/opus': + optional: true + ffmpeg-static: + optional: true + node-opus: + optional: true + opusscript: + optional: true + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + + thread-stream@3.2.0: + resolution: {integrity: sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + 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 + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@biomejs/biome@2.5.8': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.8 + '@biomejs/cli-darwin-x64': 2.5.8 + '@biomejs/cli-linux-arm64': 2.5.8 + '@biomejs/cli-linux-arm64-musl': 2.5.8 + '@biomejs/cli-linux-x64': 2.5.8 + '@biomejs/cli-linux-x64-musl': 2.5.8 + '@biomejs/cli-win32-arm64': 2.5.8 + '@biomejs/cli-win32-x64': 2.5.8 + + '@biomejs/cli-darwin-arm64@2.5.8': + optional: true + + '@biomejs/cli-darwin-x64@2.5.8': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.8': + optional: true + + '@biomejs/cli-linux-arm64@2.5.8': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.8': + optional: true + + '@biomejs/cli-linux-x64@2.5.8': + optional: true + + '@biomejs/cli-win32-arm64@2.5.8': + optional: true + + '@biomejs/cli-win32-x64@2.5.8': + optional: true + + '@discordjs/voice@0.19.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + dependencies: + '@snazzah/davey': 0.1.12(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@types/ws': 8.18.1 + discord-api-types: 0.38.52 + prism-media: 1.3.5 + tslib: 2.8.1 + ws: 8.21.1 + transitivePeerDependencies: + - '@discordjs/opus' + - '@emnapi/core' + - '@emnapi/runtime' + - bufferutil + - ffmpeg-static + - node-opus + - opusscript + - utf-8-validate + + '@emnapi/core@2.0.0-alpha.3': + dependencies: + '@emnapi/wasi-threads': 2.0.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@2.0.0-alpha.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@2.0.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@ioredis/commands@1.10.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + dependencies: + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@opentelemetry/api@1.9.1': {} + + '@oxc-project/types@0.142.0': {} + + '@pinojs/redact@0.4.0': {} + + '@rolldown/binding-android-arm64@1.2.1': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.1': + optional: true + + '@rolldown/binding-darwin-x64@1.2.1': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.1': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.1': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.1': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.1': + optional: true + + '@rolldown/binding-wasm32-wasi@1.2.1': + dependencies: + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.1': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.1': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@snazzah/davey-android-arm-eabi@0.1.12': + optional: true + + '@snazzah/davey-android-arm64@0.1.12': + optional: true + + '@snazzah/davey-darwin-arm64@0.1.12': + optional: true + + '@snazzah/davey-darwin-x64@0.1.12': + optional: true + + '@snazzah/davey-freebsd-x64@0.1.12': + optional: true + + '@snazzah/davey-linux-arm-gnueabihf@0.1.12': + optional: true + + '@snazzah/davey-linux-arm64-gnu@0.1.12': + optional: true + + '@snazzah/davey-linux-arm64-musl@0.1.12': + optional: true + + '@snazzah/davey-linux-x64-gnu@0.1.12': + optional: true + + '@snazzah/davey-linux-x64-musl@0.1.12': + optional: true + + '@snazzah/davey-wasm32-wasi@0.1.12(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + dependencies: + '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@snazzah/davey-win32-arm64-msvc@0.1.12': + optional: true + + '@snazzah/davey-win32-ia32-msvc@0.1.12': + optional: true + + '@snazzah/davey-win32-x64-msvc@0.1.12': + optional: true + + '@snazzah/davey@0.1.12(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + optionalDependencies: + '@snazzah/davey-android-arm-eabi': 0.1.12 + '@snazzah/davey-android-arm64': 0.1.12 + '@snazzah/davey-darwin-arm64': 0.1.12 + '@snazzah/davey-darwin-x64': 0.1.12 + '@snazzah/davey-freebsd-x64': 0.1.12 + '@snazzah/davey-linux-arm-gnueabihf': 0.1.12 + '@snazzah/davey-linux-arm64-gnu': 0.1.12 + '@snazzah/davey-linux-arm64-musl': 0.1.12 + '@snazzah/davey-linux-x64-gnu': 0.1.12 + '@snazzah/davey-linux-x64-musl': 0.1.12 + '@snazzah/davey-wasm32-wasi': 0.1.12(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@snazzah/davey-win32-arm64-msvc': 0.1.12 + '@snazzah/davey-win32-ia32-msvc': 0.1.12 + '@snazzah/davey-win32-x64-msvc': 0.1.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + '@standard-schema/spec@1.1.0': {} + + '@trpc/server@11.18.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.9.5 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.9.5 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@5.1.2': + dependencies: + '@types/node': 25.9.5 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.2 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 25.9.5 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 25.9.5 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.9.5 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.5 + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@25.9.5)(esbuild@0.28.1)(tsx@4.23.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.0(@types/node@25.9.5)(esbuild@0.28.1)(tsx@4.23.1) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + axios@1.19.0(debug@4.4.3): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3) + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + bintrees@1.0.2: {} + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chai@6.2.2: {} + + cluster-key-slot@1.1.1: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + discord-api-types@0.38.52: {} + + dotenv@17.4.2: {} + + drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.22.0): + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/pg': 8.20.0 + pg: 8.22.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.1: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escape-html@1.0.3: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + etag@1.8.1: {} + + expect-type@1.4.0: {} + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + helmet@8.3.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ioredis@5.11.1: + dependencies: + '@ioredis/commands': 1.10.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + negotiator@1.0.0: {} + + object-inspect@1.13.4: {} + + obug@2.1.4: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.2.0 + + postcss@8.5.25: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prism-media@1.3.5: {} + + process-warning@5.1.0: {} + + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.1 + tdigest: 0.1.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + quick-format-unescaped@4.0.4: {} + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + real-require@0.2.0: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + rolldown@1.2.1: + dependencies: + '@oxc-project/types': 0.142.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.1 + '@rolldown/binding-darwin-arm64': 1.2.1 + '@rolldown/binding-darwin-x64': 1.2.1 + '@rolldown/binding-freebsd-x64': 1.2.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 + '@rolldown/binding-linux-arm64-gnu': 1.2.1 + '@rolldown/binding-linux-arm64-musl': 1.2.1 + '@rolldown/binding-linux-ppc64-gnu': 1.2.1 + '@rolldown/binding-linux-s390x-gnu': 1.2.1 + '@rolldown/binding-linux-x64-gnu': 1.2.1 + '@rolldown/binding-linux-x64-musl': 1.2.1 + '@rolldown/binding-openharmony-arm64': 1.2.1 + '@rolldown/binding-wasm32-wasi': 1.2.1 + '@rolldown/binding-win32-arm64-msvc': 1.2.1 + '@rolldown/binding-win32-x64-msvc': 1.2.1 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + send@1.2.1: + 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.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + split2@4.2.0: {} + + stackback@0.0.2: {} + + standard-as-callback@2.1.0: {} + + statuses@2.0.2: {} + + std-env@4.2.0: {} + + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + + thread-stream@3.2.0: + dependencies: + real-require: 0.2.0 + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.1: {} + + toidentifier@1.0.1: {} + + tslib@2.8.1: {} + + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + undici-types@7.24.6: {} + + unpipe@1.0.0: {} + + vary@1.1.2: {} + + vite@8.2.0(@types/node@25.9.5)(esbuild@0.28.1)(tsx@4.23.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.5 + esbuild: 0.28.1 + fsevents: 2.3.3 + tsx: 4.23.1 + + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(vite@8.2.0(@types/node@25.9.5)(esbuild@0.28.1)(tsx@4.23.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@25.9.5)(esbuild@0.28.1)(tsx@4.23.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.0(@types/node@25.9.5)(esbuild@0.28.1)(tsx@4.23.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 25.9.5 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrappy@1.0.2: {} + + ws@8.21.1: {} + + xtend@4.0.2: {} + + zod@4.4.3: {} diff --git a/services/backend/src/http/app.ts b/services/backend/src/http/app.ts index 03d1999..122d859 100644 --- a/services/backend/src/http/app.ts +++ b/services/backend/src/http/app.ts @@ -1,3 +1,4 @@ +import { nodeHTTPRequestHandler } from "@trpc/server/adapters/node-http"; import express, { type Express, type NextFunction, @@ -6,20 +7,17 @@ import express, { } from "express"; import helmet from "helmet"; import { createChildLogger } from "@/shared/logger/index"; -import { createAnalysisRouter } from "../modules/analysis/index.js"; -import { createChatbotRouter } from "../modules/chatbot/index.js"; -import { createConfigRouter } from "../modules/config/index.js"; -import { createDashboardRouter } from "../modules/dashboard/index.js"; import { createHealthRouter } from "../modules/health/index.js"; -import { createMediaRouter } from "../modules/media/index.js"; -import { createMessagesRouter } from "../modules/messages/index.js"; -import { createModerationRouter } from "../modules/moderation/index.js"; -import { createRecordingsRouter } from "../modules/recordings/index.js"; -import { createUiStateRouter } from "../modules/ui-state/index.js"; -import { createVoiceRouter } from "../modules/voice/index.js"; import { errorHandler } from "../shared/middlewares/index.js"; +import { appRouter } from "../trpc/routers"; -// Auth removed — dashboard is public +// Auth removed — dashboard is public. +// All data APIs (dashboard, messages, moderation, media, voice, recordings, +// analysis, chatbot, config, ui-state) now flow over tRPC, served on TWO +// transports sharing the /trpc path: +// - WebSocket (browser live RPCs) — see trpc/ws.ts +// - HTTP POST (server-side / RSC fetch) — handled below +// Only infra endpoints (health, prometheus metrics) remain plain HTTP. const logger = createChildLogger("http.app"); @@ -33,7 +31,7 @@ export function createHttpApp(): Express { }), ); - // Body parsing + // Body parsing (still needed for any JSON POST; tRPC is WS-based) app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -59,24 +57,40 @@ export function createHttpApp(): Express { next(); }); - // All routes are public + // Infra-only HTTP endpoints app.use("/api", createHealthRouter()); - app.use("/api", createConfigRouter()); - app.use("/api", createDashboardRouter()); - app.use("/api", createMessagesRouter()); - app.use("/api", createAnalysisRouter()); - app.use("/api", createChatbotRouter()); - app.use("/api", createRecordingsRouter()); - app.use("/api", createUiStateRouter()); - app.use("/api", createMediaRouter()); - app.use("/api", createVoiceRouter()); - app.use("/api", createModerationRouter()); + + // tRPC over HTTP (server-side / RSC fetch). The context has no WebSocket + // here (that's the WS transport's job); procedures don't read ctx.conn, so + // a null conn is safe. + // NOTE: Express 5 (path-to-regexp v8) rejects the `"/trpc/*"` wildcard route, + // and `nodeHTTPRequestHandler` uses `opts.path` as the literal procedure + // path (it does NOT derive it from `req.url`). So we mount a plain + // middleware and compute the procedure path from the URL ourselves. + app.use((req: Request, res: Response, next: NextFunction) => { + if (!req.path.startsWith("/trpc")) { + next(); + return; + } + const procPath = req.url.replace(/^\/trpc\/?/, "").split("?")[0] || "/"; + nodeHTTPRequestHandler({ + router: appRouter, + createContext: () => ({ conn: null }), + req, + res, + path: procPath, + }).catch((err: unknown) => { + logger.error({ err }, "tRPC HTTP handler failed"); + if (!res.headersSent) res.status(500).json({ error: "INTERNAL" }); + }); + }); // 404 handler app.use((_req: Request, res: Response) => { res.status(404).json({ error: "NOT_FOUND", - message: "Endpoint not found", + message: + "Endpoint not found — data APIs are served over /trpc (WebSocket/HTTP)", }); }); diff --git a/services/backend/src/http/server.ts b/services/backend/src/http/server.ts index 554bd5f..c54d3fb 100644 --- a/services/backend/src/http/server.ts +++ b/services/backend/src/http/server.ts @@ -2,6 +2,7 @@ import { createServer, type Server } from "node:http"; import { createChildLogger } from "@/shared/logger/index"; import { config } from "../shared/config/index.js"; import { initializeDatabase } from "../shared/database/index.js"; +import { createTRPCWebSocketServer } from "../trpc/ws.js"; import { startRedisBridge } from "../ws/redis-bridge.js"; import { createWebSocketServer } from "../ws/server.js"; import { createHttpApp } from "./app.js"; @@ -16,8 +17,9 @@ export async function startHttpServer(): Promise { const server = createServer(app); - // Attach WebSocket server to the same HTTP server - createWebSocketServer(server); + // Attach WebSocket servers to the same HTTP server + createWebSocketServer(server); // /ws — voice PCM + gateway events + createTRPCWebSocketServer(server); // /trpc — structured data RPCs // Start Redis pub/sub bridge to forward discord-gateway events to WS clients await startRedisBridge(); diff --git a/services/backend/src/modules/analysis/analysis.routes.ts b/services/backend/src/modules/analysis/analysis.routes.ts deleted file mode 100644 index 3092248..0000000 --- a/services/backend/src/modules/analysis/analysis.routes.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { analysisService } from "./analysis.service.js"; - -const logger = createChildLogger("analysis.routes"); - -export function createAnalysisRouter(): Router { - const router = express.Router(); - - // GET /api/analysis/search - router.get( - "/analysis/search", - asyncHandler(async (req: Request, res: Response) => { - const q = (req.query.q as string) || ""; - const channelId = (req.query.channelId as string) || undefined; - const limit = Number(req.query.limit) || 20; - - logger.debug({ q, channelId, limit }, "Analysis search requested"); - const result = await analysisService.search({ q, channelId, limit }); - res.json(result); - }), - ); - - return router; -} diff --git a/services/backend/src/modules/analysis/index.ts b/services/backend/src/modules/analysis/index.ts deleted file mode 100644 index f223bb3..0000000 --- a/services/backend/src/modules/analysis/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createAnalysisRouter } from "./analysis.routes.js"; diff --git a/services/backend/src/modules/chatbot/chatbot.controller.ts b/services/backend/src/modules/chatbot/chatbot.controller.ts deleted file mode 100644 index cac9845..0000000 --- a/services/backend/src/modules/chatbot/chatbot.controller.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Request, Response } from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { chatbotService } from "./chatbot.service.js"; - -const logger = createChildLogger("chatbot.controller"); - -interface AuthenticatedRequest extends Request { - userId?: string; -} - -/** - * Resolve the actor id for a request. Frontend (no-login) sends a per-device - * UUID via X-User-Id so chat history stays isolated per visitor; a registered - * auth middleware userId takes precedence when present. - */ -function resolveUserId(req: Request): string { - const authId = (req as AuthenticatedRequest).userId; - if (authId) return authId; - const header = (req.headers["x-user-id"] as string | undefined)?.trim(); - return header || "anonymous"; -} - -export const handleChatbotChat = asyncHandler( - async (req: Request, res: Response) => { - const { message, context } = req.body as { - message: string; - context?: Record; - }; - - // Validate required fields - if (!message || typeof message !== "string") { - return res.status(400).json({ - error: "INVALID_INPUT", - message: "Message is required and must be a string", - }); - } - - // Get user ID from X-User-Id header (no-login device uuid) or auth - const userId = resolveUserId(req); - - logger.debug( - { userId, messageLength: message.length, context }, - "Received chatbot chat message", - ); - - // Process message & generate response - const response = await chatbotService.processMessage( - message, - context, - userId, - ); - - // Save conversation to database - await chatbotService.saveConversation({ - userId, - userMessage: message, - botResponse: response, - context, - timestamp: new Date(), - }); - - logger.info({ userId }, "Chatbot chat processed successfully"); - - res.status(200).json({ - response, - timestamp: new Date().toISOString(), - }); - }, -); - -export const getChatbotHistory = asyncHandler( - async (req: Request, res: Response) => { - const userId = resolveUserId(req); - const limit = Math.min(parseInt(req.query.limit as string, 10) || 50, 100); - - const history = await chatbotService.getChatHistory(userId, limit); - - res.status(200).json({ - history, - total: history.length, - }); - }, -); - -export const clearChatbotHistory = asyncHandler( - async (req: Request, res: Response) => { - const userId = resolveUserId(req); - - await chatbotService.clearChatHistory(userId); - - res.status(200).json({ - message: "Chat history cleared successfully", - }); - }, -); diff --git a/services/backend/src/modules/chatbot/chatbot.routes.ts b/services/backend/src/modules/chatbot/chatbot.routes.ts deleted file mode 100644 index 78790e4..0000000 --- a/services/backend/src/modules/chatbot/chatbot.routes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import express, { type Router } from "express"; -import { validateBody } from "../../shared/middlewares/index.js"; -import { - clearChatbotHistory, - getChatbotHistory, - handleChatbotChat, -} from "./chatbot.controller.js"; -import { chatRequestSchema } from "./chatbot.schema.js"; - -export function createChatbotRouter(): Router { - const router = express.Router(); - - router.post("/chat", validateBody(chatRequestSchema), handleChatbotChat); - router.get("/chat/history", getChatbotHistory); - router.delete("/chat/history", clearChatbotHistory); - - return router; -} diff --git a/services/backend/src/modules/chatbot/index.ts b/services/backend/src/modules/chatbot/index.ts deleted file mode 100644 index 6b73e50..0000000 --- a/services/backend/src/modules/chatbot/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createChatbotRouter } from "./chatbot.routes.js"; diff --git a/services/backend/src/modules/config/config.routes.ts b/services/backend/src/modules/config/config.routes.ts deleted file mode 100644 index 5e835bb..0000000 --- a/services/backend/src/modules/config/config.routes.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Router } from "express"; -import express from "express"; -import { config } from "../../shared/config/index.js"; - -export function createConfigRouter(): Router { - const router = express.Router(); - - // GET /api/config - router.get("/config", (_req, res) => { - res.json({ - monitorGuildId: config.MONITOR_GUILD_ID || null, - webserverPort: config.WEBSERVER_PORT, - nodeEnv: config.NODE_ENV, - backlogSyncHours: config.BACKLOG_SYNC_HOURS, - backlogSyncBatchSize: config.BACKLOG_SYNC_BATCH_SIZE, - retentionMessagesDays: config.RETENTION_MESSAGES_DAYS, - retentionAttachmentsDays: config.RETENTION_ATTACHMENTS_DAYS, - retentionVoiceDays: config.RETENTION_VOICE_DAYS, - autoDeleteFlaggedEnabled: config.AUTO_DELETE_FLAGGED_ENABLED, - aiAnalysisEnabled: config.AI_ANALYSIS_ENABLED, - voiceGuildId: config.VOICE_GUILD_ID || null, - voiceChannelId: config.VOICE_CHANNEL_ID || null, - logLevel: config.LOG_LEVEL, - }); - }); - - return router; -} diff --git a/services/backend/src/modules/config/index.ts b/services/backend/src/modules/config/index.ts deleted file mode 100644 index 94b2c79..0000000 --- a/services/backend/src/modules/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createConfigRouter } from "./config.routes.js"; diff --git a/services/backend/src/modules/dashboard/dashboard.routes.ts b/services/backend/src/modules/dashboard/dashboard.routes.ts deleted file mode 100644 index 7d7c01d..0000000 --- a/services/backend/src/modules/dashboard/dashboard.routes.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { dashboardService } from "./dashboard.service.js"; - -const logger = createChildLogger("dashboard.routes"); - -export function createDashboardRouter(): Router { - const router = express.Router(); - - // GET /api/dashboard/stats — aggregated server statistics - router.get( - "/dashboard/stats", - asyncHandler(async (_req: Request, res: Response) => { - logger.debug("Fetching dashboard stats"); - const stats = await dashboardService.getStats(); - res.json(stats); - }), - ); - - // GET /api/dashboard/activity?days=14 — message volume over time - router.get( - "/dashboard/activity", - asyncHandler(async (req: Request, res: Response) => { - const days = Math.min(Math.max(Number(req.query.days) || 14, 1), 90); - const activity = await dashboardService.getActivity(days); - res.json(activity); - }), - ); - - // GET /api/dashboard/users — paginated user list with profiles - router.get( - "/dashboard/users", - asyncHandler(async (req: Request, res: Response) => { - const limit = Number(req.query.limit) || 20; - const cursor = - typeof req.query.cursor === "string" ? req.query.cursor : undefined; - const search = - typeof req.query.search === "string" ? req.query.search : undefined; - - const result = await dashboardService.listUsers({ - limit, - cursor, - search, - }); - res.json(result); - }), - ); - - // GET /api/dashboard/users/:userId — single user detail - router.get( - "/dashboard/users/:userId", - asyncHandler(async (req: Request, res: Response) => { - const userId = String(req.params.userId); - const detail = await dashboardService.getUserDetail(userId); - res.json(detail); - }), - ); - - // GET /api/dashboard/channels — paginated channel list with culture summaries - router.get( - "/dashboard/channels", - asyncHandler(async (req: Request, res: Response) => { - const limit = Number(req.query.limit) || 20; - const search = - typeof req.query.search === "string" ? req.query.search : undefined; - const guildId = - typeof req.query.guild_id === "string" ? req.query.guild_id : undefined; - - const result = await dashboardService.listChannels({ - limit, - search, - guildId, - }); - res.json(result); - }), - ); - - // GET /api/dashboard/channels/:channelId — single channel detail - router.get( - "/dashboard/channels/:channelId", - asyncHandler(async (req: Request, res: Response) => { - const channelId = String(req.params.channelId); - const detail = await dashboardService.getChannelDetail(channelId); - res.json(detail); - }), - ); - - // GET /api/dashboard/reactions — top reacted messages - router.get( - "/dashboard/reactions", - asyncHandler(async (req: Request, res: Response) => { - const limit = Number(req.query.limit) || 20; - const reactions = await dashboardService.getTopReactions(limit); - res.json(reactions); - }), - ); - - // GET /api/dashboard/reactors — top users by reactions given - router.get( - "/dashboard/reactors", - asyncHandler(async (req: Request, res: Response) => { - const limit = Number(req.query.limit) || 20; - const reactors = await dashboardService.getTopReactors(limit); - res.json(reactors); - }), - ); - - return router; -} diff --git a/services/backend/src/modules/dashboard/index.ts b/services/backend/src/modules/dashboard/index.ts deleted file mode 100644 index 95a56db..0000000 --- a/services/backend/src/modules/dashboard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createDashboardRouter } from "./dashboard.routes.js"; diff --git a/services/backend/src/modules/media/index.ts b/services/backend/src/modules/media/index.ts deleted file mode 100644 index 641bca4..0000000 --- a/services/backend/src/modules/media/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createMediaRouter } from "./media.routes.js"; diff --git a/services/backend/src/modules/media/media.routes.ts b/services/backend/src/modules/media/media.routes.ts deleted file mode 100644 index 3ab6283..0000000 --- a/services/backend/src/modules/media/media.routes.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler, validateBody } from "../../shared/middlewares/index.js"; -import { mediaLoopSchema, mediaQueueSchema } from "./media.schema.js"; -import { getStatus, queue, setLoop, skip, stop } from "./media.service.js"; - -const logger = createChildLogger("media.routes"); - -export function createMediaRouter(): Router { - const router = express.Router(); - - // GET /api/media/status - router.get( - "/media/status", - asyncHandler(async (_req: Request, res: Response) => { - logger.debug("Media status requested"); - const status = await getStatus(); - res.json(status); - }), - ); - - // POST /api/media/queue - router.post( - "/media/queue", - validateBody(mediaQueueSchema), - asyncHandler(async (req: Request, res: Response) => { - const { source, mode } = req.body as { - source: string; - mode: "music" | "screen"; - }; - logger.debug({ source, mode }, "Media queue requested"); - const state = await queue(source, mode); - res.json(state); - }), - ); - - // POST /api/media/skip - router.post( - "/media/skip", - asyncHandler(async (_req: Request, res: Response) => { - logger.debug("Media skip requested"); - const state = await skip(); - res.json(state); - }), - ); - - // POST /api/media/stop - router.post( - "/media/stop", - asyncHandler(async (_req: Request, res: Response) => { - logger.debug("Media stop requested"); - const state = await stop(); - res.json(state); - }), - ); - - // POST /api/media/loop - router.post( - "/media/loop", - validateBody(mediaLoopSchema), - asyncHandler(async (req: Request, res: Response) => { - const { loop } = req.body as { loop: boolean }; - logger.debug({ loop }, "Media loop requested"); - const state = await setLoop(loop); - res.json(state); - }), - ); - - return router; -} diff --git a/services/backend/src/modules/messages/index.ts b/services/backend/src/modules/messages/index.ts deleted file mode 100644 index 9fe7a6f..0000000 --- a/services/backend/src/modules/messages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createMessagesRouter } from "./messages.routes.js"; diff --git a/services/backend/src/modules/messages/messages.controller.ts b/services/backend/src/modules/messages/messages.controller.ts deleted file mode 100644 index e83eb62..0000000 --- a/services/backend/src/modules/messages/messages.controller.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { Request, Response } from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { messageQuerySchema } from "./messages.schema.js"; -import { messagesService } from "./messages.service.js"; - -const logger = createChildLogger("messages.controller"); - -export const handleListMessages = asyncHandler( - async (req: Request, res: Response) => { - const query = messageQuerySchema.parse(req.query); - logger.debug({ query }, "Handling list messages request"); - const result = await messagesService.listMessages(query); - res.json(result); - }, -); - -export const handleGetMessagesByChannel = asyncHandler( - async (req: Request, res: Response) => { - if (!req.params.channelId) { - res.status(400).json({ error: "Missing route parameter: channelId" }); - return; - } - const channelId = req.params.channelId as string; - const query = messageQuerySchema.parse(req.query); - logger.debug({ channelId, query }, "Handling get messages by channel"); - const result = await messagesService.getMessagesByChannel(channelId, query); - res.json(result); - }, -); - -export const handleGetMessageById = asyncHandler( - async (req: Request, res: Response) => { - if (!req.params.id) { - res.status(400).json({ error: "Missing route parameter: id" }); - return; - } - const id = req.params.id as string; - logger.debug({ id }, "Handling get message by ID"); - const result = await messagesService.getMessageById(id); - res.json(result); - }, -); - -export const handleGetImageMessages = asyncHandler( - async (req: Request, res: Response) => { - const guildId = req.query.guildId as string | undefined; - if (!guildId) { - res.status(400).json({ error: "Missing query parameter: guildId" }); - return; - } - const limit = Number(req.query.limit) || 50; - logger.debug({ guildId, limit }, "Handling get image messages"); - const result = await messagesService.getImageMessages(guildId, limit); - res.json(result); - }, -); - -export const handleGetAttachmentsByChannel = asyncHandler( - async (req: Request, res: Response) => { - if (!req.params.channelId) { - res.status(400).json({ error: "Missing route parameter: channelId" }); - return; - } - const channelId = req.params.channelId as string; - const query = messageQuerySchema.parse(req.query); - logger.debug({ channelId, query }, "Handling get attachments by channel"); - const result = await messagesService.getAttachmentsByChannel( - channelId, - query, - ); - res.json(result); - }, -); diff --git a/services/backend/src/modules/messages/messages.routes.ts b/services/backend/src/modules/messages/messages.routes.ts deleted file mode 100644 index 65445f7..0000000 --- a/services/backend/src/modules/messages/messages.routes.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { - handleGetAttachmentsByChannel, - handleGetImageMessages, - handleGetMessageById, - handleGetMessagesByChannel, - handleListMessages, -} from "./messages.controller.js"; -import { messagesService } from "./messages.service.js"; - -const logger = createChildLogger("messages.routes"); - -export function createMessagesRouter(): Router { - const router = express.Router(); - - // GET /api/messages/images - Get messages with image attachments - // MUST be registered BEFORE /messages/:channelId so "images" is not - // captured as a channelId param. - router.get("/messages/images", handleGetImageMessages); - - // GET /api/messages - List messages - router.get("/messages", handleListMessages); - - // GET /api/messages/:channelId - Get messages by channel - router.get("/messages/:channelId", handleGetMessagesByChannel); - - // GET /api/messages/:channelId/attachments - Get attachments by channel - router.get("/messages/:channelId/attachments", handleGetAttachmentsByChannel); - - // GET /api/messages/detail/:id - Get single message by ID - // (uses /detail/ prefix to avoid collision with :channelId route above) - router.get("/messages/detail/:id", handleGetMessageById); - - // GET /api/review - Get flagged/warned messages for review - router.get( - "/review", - asyncHandler(async (req: Request, res: Response) => { - const limit = Number(req.query.limit) || 20; - const channelId = (req.query.channelId as string) || undefined; - - const rows = await messagesService.getReviewMessages(channelId, limit); - logger.debug({ limit, channelId }, "Review query executed"); - res.json({ results: rows, limit, cursor: null }); - }), - ); - - return router; -} diff --git a/services/backend/src/modules/moderation/index.ts b/services/backend/src/modules/moderation/index.ts deleted file mode 100644 index aa97530..0000000 --- a/services/backend/src/modules/moderation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createModerationRouter } from "./moderation.routes.js"; diff --git a/services/backend/src/modules/moderation/moderation.routes.ts b/services/backend/src/modules/moderation/moderation.routes.ts deleted file mode 100644 index afeb835..0000000 --- a/services/backend/src/modules/moderation/moderation.routes.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "../../shared/logger/index.js"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { moderationService } from "./moderation.service.js"; - -const logger = createChildLogger("moderation.routes"); - -export function createModerationRouter(): Router { - const router = express.Router(); - - // GET /api/moderation/stats — moderation action summary - router.get( - "/moderation/stats", - asyncHandler(async (_req: Request, res: Response) => { - const stats = await moderationService.getStats(); - res.json(stats); - }), - ); - - // GET /api/moderation/actions — paginated moderation action log - router.get( - "/moderation/actions", - asyncHandler(async (req: Request, res: Response) => { - const limit = Number(req.query.limit) || 50; - const status = req.query.status as string | undefined; - const actionType = req.query.actionType as string | undefined; - const cursor = req.query.cursor as string | undefined; - - const result = await moderationService.listActions({ - limit, - status, - actionType, - cursor: cursor ? Number(cursor) : undefined, - }); - - logger.debug({ count: result.data.length }, "Moderation actions listed"); - res.json(result); - }), - ); - - return router; -} diff --git a/services/backend/src/modules/recordings/index.ts b/services/backend/src/modules/recordings/index.ts deleted file mode 100644 index c3df629..0000000 --- a/services/backend/src/modules/recordings/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createRecordingsRouter } from "./recordings.routes.js"; diff --git a/services/backend/src/modules/recordings/recordings.routes.ts b/services/backend/src/modules/recordings/recordings.routes.ts deleted file mode 100644 index 41a2add..0000000 --- a/services/backend/src/modules/recordings/recordings.routes.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { recordingsService } from "./recordings.service.js"; - -const logger = createChildLogger("recordings.routes"); - -export function createRecordingsRouter(): Router { - const router = express.Router(); - - // GET /api/recordings - router.get( - "/recordings", - asyncHandler(async (req: Request, res: Response) => { - const limit = Number(req.query.limit) || 50; - const channelId = req.query.channelId as string | undefined; - const userId = req.query.userId as string | undefined; - const cursor = req.query.cursor as string | undefined; - logger.debug({ limit, channelId, userId, cursor }, "Fetching recordings"); - const result = await recordingsService.getRecent(limit, { - channelId, - userId, - cursor, - }); - res.json(result); - }), - ); - - // DELETE /api/recordings/:id - router.delete( - "/recordings/:id", - asyncHandler(async (req: Request, res: Response) => { - const id = req.params.id as string; - await recordingsService.deleteById(id); - res.json({ ok: true }); - }), - ); - - return router; -} diff --git a/services/backend/src/modules/ui-state/index.ts b/services/backend/src/modules/ui-state/index.ts deleted file mode 100644 index 63360a7..0000000 --- a/services/backend/src/modules/ui-state/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createUiStateRouter } from "./ui-state.routes.js"; diff --git a/services/backend/src/modules/ui-state/ui-state.routes.ts b/services/backend/src/modules/ui-state/ui-state.routes.ts deleted file mode 100644 index 1b67152..0000000 --- a/services/backend/src/modules/ui-state/ui-state.routes.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { uiStateService } from "./ui-state.service.js"; - -const logger = createChildLogger("ui-state.routes"); - -export function createUiStateRouter(): Router { - const router = express.Router(); - - // GET /api/ui-state - router.get( - "/ui-state", - asyncHandler(async (_req: Request, res: Response) => { - logger.debug("Fetching UI state"); - const state = await uiStateService.getState(); - res.json(state); - }), - ); - - // POST /api/ui-state - router.post( - "/ui-state", - asyncHandler(async (req: Request, res: Response) => { - const updates = req.body as Record; - logger.debug({ keys: Object.keys(updates) }, "Updating UI state"); - const result = await uiStateService.updateState(updates); - res.json(result); - }), - ); - - return router; -} diff --git a/services/backend/src/modules/voice/index.ts b/services/backend/src/modules/voice/index.ts deleted file mode 100644 index 99349a9..0000000 --- a/services/backend/src/modules/voice/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createVoiceRouter } from "./voice.routes.js"; diff --git a/services/backend/src/modules/voice/voice.controller.ts b/services/backend/src/modules/voice/voice.controller.ts deleted file mode 100644 index b76ff07..0000000 --- a/services/backend/src/modules/voice/voice.controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Request, Response } from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler } from "../../shared/middlewares/index.js"; -import { publishCommandNoReply } from "../../shared/redis/index.js"; -import type { ConnectVoiceInput, VoiceCommandInput } from "./voice.schema.js"; -import { - connectVoice, - disconnectVoice, - getVoiceStatus, -} from "./voice.service.js"; - -const logger = createChildLogger("voice.controller"); - -export const handleGetVoiceStatus = asyncHandler( - async (_req: Request, res: Response) => { - const status = await getVoiceStatus(); - res.json(status); - }, -); - -export const handleConnectVoice = asyncHandler( - async (req: Request, res: Response) => { - const { guildId, channelId } = req.body as ConnectVoiceInput; - logger.debug({ guildId, channelId }, "Connecting to voice channel"); - const status = await connectVoice(guildId, channelId); - res.json(status); - }, -); - -export const handleDisconnectVoice = asyncHandler( - async (_req: Request, res: Response) => { - logger.debug("Disconnecting from voice"); - const status = await disconnectVoice(); - res.json(status); - }, -); - -export const handleVoiceCommand = asyncHandler( - async (req: Request, res: Response) => { - const { command } = req.body as VoiceCommandInput; - logger.debug({ command }, "Publishing voice command"); - await publishCommandNoReply(command); - res.json({ success: true, command }); - }, -); diff --git a/services/backend/src/modules/voice/voice.routes.ts b/services/backend/src/modules/voice/voice.routes.ts deleted file mode 100644 index 74d9ed8..0000000 --- a/services/backend/src/modules/voice/voice.routes.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { Request, Response, Router } from "express"; -import express from "express"; -import { createChildLogger } from "@/shared/logger/index"; -import { asyncHandler, validateBody } from "../../shared/middlewares/index.js"; -import { - handleConnectVoice, - handleDisconnectVoice, - handleGetVoiceStatus, - handleVoiceCommand, -} from "./voice.controller.js"; -import { connectVoiceSchema, voiceCommandSchema } from "./voice.schema.js"; -import { - getGuilds, - getTextChannels, - getVoiceChannels, -} from "./voice.service.js"; - -const logger = createChildLogger("voice.routes"); - -export function createVoiceRouter(): Router { - const router = express.Router(); - - // ── Guilds ────────────────────────────────────────────────────────────── - - // GET /api/guilds - router.get( - "/guilds", - asyncHandler(async (_req: Request, res: Response) => { - logger.debug("Fetching guilds"); - const guilds = await getGuilds(); - res.json(guilds); - }), - ); - - // GET /api/guilds/:guildId/channels - router.get( - "/guilds/:guildId/channels", - asyncHandler(async (req: Request, res: Response) => { - const guildId = req.params.guildId as string; - logger.debug({ guildId }, "Fetching text channels"); - const channels = await getTextChannels(guildId); - res.json(channels); - }), - ); - - // GET /api/guilds/:guildId/voice-channels - router.get( - "/guilds/:guildId/voice-channels", - asyncHandler(async (req: Request, res: Response) => { - const guildId = req.params.guildId as string; - logger.debug({ guildId }, "Fetching voice channels"); - const channels = await getVoiceChannels(guildId); - res.json(channels); - }), - ); - - // ── Voice connection ──────────────────────────────────────────────────── - - // GET /api/voice/status - router.get("/voice/status", handleGetVoiceStatus); - - // POST /api/voice/connect - router.post( - "/voice/connect", - validateBody(connectVoiceSchema), - handleConnectVoice, - ); - - // POST /api/voice/disconnect - router.post("/voice/disconnect", handleDisconnectVoice); - - // POST /api/voice/command — send arbitrary voice command (transmit start/stop) - router.post( - "/voice/command", - validateBody(voiceCommandSchema), - handleVoiceCommand, - ); - - return router; -} diff --git a/services/backend/src/trpc/routers.ts b/services/backend/src/trpc/routers.ts new file mode 100644 index 0000000..ec7d8fa --- /dev/null +++ b/services/backend/src/trpc/routers.ts @@ -0,0 +1,347 @@ +import { z } from "zod"; +import { analysisService } from "../modules/analysis/analysis.service"; +import { chatRequestSchema } from "../modules/chatbot/chatbot.schema"; +import { chatbotService } from "../modules/chatbot/chatbot.service"; +// ── Service imports ────────────────────────────────────────────── +import { dashboardService } from "../modules/dashboard/dashboard.service"; +import { + mediaLoopSchema, + mediaQueueSchema, +} from "../modules/media/media.schema"; +import { + getStatus, + queue, + setLoop, + skip, + stop, +} from "../modules/media/media.service"; +import { messageQuerySchema } from "../modules/messages/messages.schema"; +import { messagesService } from "../modules/messages/messages.service"; +import { moderationService } from "../modules/moderation/moderation.service"; +import { recordingsService } from "../modules/recordings/recordings.service"; +import { uiStateService } from "../modules/ui-state/ui-state.service"; +import { + connectVoice, + disconnectVoice, + getGuilds, + getTextChannels, + getVoiceChannels, + getVoiceStatus, +} from "../modules/voice/voice.service"; +import { config } from "../shared/config/index"; +import { publishCommandNoReply } from "../shared/redis/index"; +import { logger, publicProcedure, router } from "./trpc"; + +// ── Dashboard ──────────────────────────────────────────────────── +const dashboardRouter = router({ + stats: publicProcedure.query(() => dashboardService.getStats()), + activity: publicProcedure + .input( + z.object({ days: z.coerce.number().int().min(1).max(90).default(14) }), + ) + .query(({ input }) => dashboardService.getActivity(input.days)), + users: publicProcedure + .input( + z.object({ + limit: z.coerce.number().int().positive().default(20), + cursor: z.string().optional(), + search: z.string().optional(), + }), + ) + .query(({ input }) => + dashboardService.listUsers({ + limit: input.limit, + cursor: input.cursor, + search: input.search, + }), + ), + userDetail: publicProcedure + .input(z.object({ userId: z.string() })) + .query(({ input }) => dashboardService.getUserDetail(input.userId)), + channels: publicProcedure + .input( + z.object({ + limit: z.coerce.number().int().positive().default(20), + search: z.string().optional(), + guildId: z.string().optional(), + }), + ) + .query(({ input }) => + dashboardService.listChannels({ + limit: input.limit, + search: input.search, + guildId: input.guildId, + }), + ), + channelDetail: publicProcedure + .input(z.object({ channelId: z.string() })) + .query(({ input }) => dashboardService.getChannelDetail(input.channelId)), + reactions: publicProcedure + .input(z.object({ limit: z.coerce.number().int().positive().default(20) })) + .query(({ input }) => dashboardService.getTopReactions(input.limit)), + reactors: publicProcedure + .input(z.object({ limit: z.coerce.number().int().positive().default(20) })) + .query(({ input }) => dashboardService.getTopReactors(input.limit)), +}); + +// ── Messages ───────────────────────────────────────────────────── +const messagesRouter = router({ + list: publicProcedure + .input(messageQuerySchema) + .query(({ input }) => messagesService.listMessages(input)), + byChannel: publicProcedure + .input( + z.object({ + channelId: z.string(), + query: messageQuerySchema, + }), + ) + .query(({ input }) => + messagesService.getMessagesByChannel(input.channelId, input.query), + ), + detail: publicProcedure + .input(z.object({ id: z.string() })) + .query(({ input }) => messagesService.getMessageById(input.id)), + images: publicProcedure + .input( + z.object({ + guildId: z.string(), + limit: z.coerce.number().int().positive().default(50), + }), + ) + .query(({ input }) => + messagesService.getImageMessages(input.guildId, input.limit), + ), + attachmentsByChannel: publicProcedure + .input( + z.object({ + channelId: z.string(), + query: messageQuerySchema, + }), + ) + .query(({ input }) => + messagesService.getAttachmentsByChannel(input.channelId, input.query), + ), + review: publicProcedure + .input( + z.object({ + limit: z.coerce.number().int().positive().default(20), + channelId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + const rows = await messagesService.getReviewMessages( + input.channelId, + input.limit, + ); + return { results: rows, limit: input.limit, cursor: null }; + }), +}); + +// ── Moderation ─────────────────────────────────────────────────── +const moderationRouter = router({ + stats: publicProcedure.query(() => moderationService.getStats()), + actions: publicProcedure + .input( + z.object({ + limit: z.coerce.number().int().positive().default(50), + status: z.string().optional(), + actionType: z.string().optional(), + cursor: z.coerce.number().int().optional(), + }), + ) + .query(({ input }) => + moderationService.listActions({ + limit: input.limit, + status: input.status, + actionType: input.actionType, + cursor: input.cursor, + }), + ), +}); + +// ── Media ──────────────────────────────────────────────────────── +const mediaRouter = router({ + status: publicProcedure.query(() => getStatus()), + queue: publicProcedure.input(mediaQueueSchema).mutation(async ({ input }) => { + await queue(input.source, input.mode); + return getStatus(); + }), + skip: publicProcedure.mutation(async () => { + await skip(); + return getStatus(); + }), + stop: publicProcedure.mutation(async () => { + await stop(); + return getStatus(); + }), + loop: publicProcedure.input(mediaLoopSchema).mutation(async ({ input }) => { + await setLoop(input.loop); + return getStatus(); + }), +}); + +// ── Voice ───────────────────────────────────────────────────────── +const voiceRouter = router({ + guilds: publicProcedure.query(() => getGuilds()), + textChannels: publicProcedure + .input(z.object({ guildId: z.string() })) + .query(({ input }) => getTextChannels(input.guildId)), + voiceChannels: publicProcedure + .input(z.object({ guildId: z.string() })) + .query(({ input }) => getVoiceChannels(input.guildId)), + status: publicProcedure.query(() => getVoiceStatus()), + connect: publicProcedure + .input(z.object({ guildId: z.string(), channelId: z.string() })) + .mutation(async ({ input }) => { + await connectVoice(input.guildId, input.channelId); + return getVoiceStatus(); + }), + disconnect: publicProcedure.mutation(async () => { + await disconnectVoice(); + return getVoiceStatus(); + }), + command: publicProcedure + .input(z.object({ command: z.string().min(1) })) + .mutation(async ({ input }) => { + await publishCommandNoReply(input.command); + return { success: true, command: input.command }; + }), +}); + +// ── Recordings ─────────────────────────────────────────────────── +const recordingsRouter = router({ + list: publicProcedure + .input( + z.object({ + limit: z.coerce.number().int().positive().default(50), + channelId: z.string().optional(), + userId: z.string().optional(), + cursor: z.string().optional(), + }), + ) + .query(({ input }) => + recordingsService.getRecent(input.limit, { + channelId: input.channelId, + userId: input.userId, + cursor: input.cursor, + }), + ), + delete: publicProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input }) => { + await recordingsService.deleteById(input.id); + return { ok: true }; + }), +}); + +// ── Analysis (search) ────────────────────────────────────────────── +const analysisRouter = router({ + search: publicProcedure + .input( + z.object({ + q: z.string().default(""), + channelId: z.string().optional(), + limit: z.coerce.number().int().positive().default(20), + }), + ) + .query(({ input }) => + analysisService.search({ + q: input.q, + channelId: input.channelId, + limit: input.limit, + }), + ), +}); + +// ── Chatbot ─────────────────────────────────────────────────────── +const chatbotRouter = router({ + chat: publicProcedure + .input( + chatRequestSchema.extend({ + // Per-device actor id; the old REST layer used an X-User-Id header. + // Anonymous sessions use a stable "anonymous" id. + userId: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + const userId = input.userId ?? "anonymous"; + const response = await chatbotService.processMessage( + input.message, + input.context, + userId, + ); + await chatbotService.saveConversation({ + userId, + userMessage: input.message, + botResponse: response, + context: input.context, + timestamp: new Date(), + }); + return { response, timestamp: new Date().toISOString() }; + }), + history: publicProcedure + .input( + z.object({ + limit: z.coerce.number().int().positive().max(100).default(50), + userId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + const userId = input.userId ?? "anonymous"; + const history = await chatbotService.getChatHistory(userId, input.limit); + return { history, total: history.length }; + }), + clearHistory: publicProcedure + .input(z.object({ userId: z.string().optional() })) + .mutation(async ({ input }) => { + const userId = input.userId ?? "anonymous"; + await chatbotService.clearChatHistory(userId); + return { ok: true }; + }), +}); + +// ── Config (public dashboard config snapshot) ────────────────────── +const configRouter = router({ + get: publicProcedure.query(() => ({ + monitorGuildId: config.MONITOR_GUILD_ID || null, + webserverPort: config.WEBSERVER_PORT, + nodeEnv: config.NODE_ENV, + backlogSyncHours: config.BACKLOG_SYNC_HOURS, + backlogSyncBatchSize: config.BACKLOG_SYNC_BATCH_SIZE, + retentionMessagesDays: config.RETENTION_MESSAGES_DAYS, + retentionAttachmentsDays: config.RETENTION_ATTACHMENTS_DAYS, + retentionVoiceDays: config.RETENTION_VOICE_DAYS, + autoDeleteFlaggedEnabled: config.AUTO_DELETE_FLAGGED_ENABLED, + aiAnalysisEnabled: config.AI_ANALYSIS_ENABLED, + voiceGuildId: config.VOICE_GUILD_ID || null, + voiceChannelId: config.VOICE_CHANNEL_ID || null, + logLevel: config.LOG_LEVEL, + })), +}); + +// ── UI State ────────────────────────────────────────────────────── +const uiStateRouter = router({ + get: publicProcedure.query(() => uiStateService.getState()), + update: publicProcedure + .input(z.record(z.string(), z.unknown())) + .mutation(({ input }) => uiStateService.updateState(input)), +}); + +// ── Root router ─────────────────────────────────────────────────── +export const appRouter = router({ + dashboard: dashboardRouter, + messages: messagesRouter, + moderation: moderationRouter, + media: mediaRouter, + voice: voiceRouter, + recordings: recordingsRouter, + analysis: analysisRouter, + chatbot: chatbotRouter, + config: configRouter, + uiState: uiStateRouter, +}); + +export type AppRouter = typeof appRouter; + +logger.info("tRPC appRouter constructed"); diff --git a/services/backend/src/trpc/trpc.ts b/services/backend/src/trpc/trpc.ts new file mode 100644 index 0000000..ab2092d --- /dev/null +++ b/services/backend/src/trpc/trpc.ts @@ -0,0 +1,35 @@ +import { initTRPC } from "@trpc/server"; +import type { WebSocket } from "ws"; +import { createChildLogger } from "@/shared/logger/index"; + +const logger = createChildLogger("trpc"); + +/** + * tRPC context. The WebSocket transport enriches each request with the raw + * socket so procedures can, if needed, inspect connection metadata. The + * dashboard is public (no auth), mirroring the previous REST layer. + */ +export interface TRPCContext { + conn: WebSocket | null; +} + +const t = initTRPC.context().create({ + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + // Surface a stable code + message for client-side handling. + code: error.code, + stack: undefined, + }, + }; + }, +}); + +export const router = t.router; +export const publicProcedure = t.procedure; + +// Re-export so routers can import z from one place if desired. +export { z } from "zod"; +export { logger }; diff --git a/services/backend/src/trpc/ws.ts b/services/backend/src/trpc/ws.ts new file mode 100644 index 0000000..8ab0b79 --- /dev/null +++ b/services/backend/src/trpc/ws.ts @@ -0,0 +1,47 @@ +import type { IncomingMessage, Server } from "node:http"; +import type { Duplex } from "node:stream"; +import { applyWSSHandler } from "@trpc/server/adapters/ws"; +import { WebSocketServer } from "ws"; +import { createChildLogger } from "@/shared/logger/index"; +import { appRouter } from "./routers"; + +const logger = createChildLogger("trpc.ws"); + +/** + * Attach the tRPC WebSocket handler to the shared HTTP server, on a path + * SEPARATE from the voice/binary WebSocket (`/ws`). All structured data RPCs + * (dashboard, messages, moderation, media, voice control, recordings, + * analysis, chatbot, config, ui-state) flow over this `/trpc` socket; the + * `/ws` socket is left untouched for Discord PCM audio + gateway events. + * + * We use `noServer` + a manual `upgrade` router (instead of + * `new WebSocketServer({ server, path: "/trpc" })`) because two `ws` servers + * mounted with the `server` option on the SAME http.Server both register + * `upgrade` listeners, and `ws`'s path-guarded listener can reject (400) the + * other server's path. Routing the upgrade ourselves by URL keeps `/trpc` + * and `/ws` fully isolated. + */ +export function createTRPCWebSocketServer(server: Server): WebSocketServer { + const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false }); + + applyWSSHandler({ + wss, + prefix: "/trpc", + router: appRouter, + createContext: (opts) => ({ conn: opts.res }), + keepAlive: { enabled: true, pingMs: 30_000, pongWaitMs: 10_000 }, + onError: (err) => { + logger.error({ err }, "tRPC WS error"); + }, + }); + + server.on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => { + if (!req.url?.startsWith("/trpc")) return; // let the /ws server handle it + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + }); + + logger.info({ path: "/trpc" }, "tRPC WebSocket server attached"); + return wss; +} diff --git a/services/backend/src/ws/server.ts b/services/backend/src/ws/server.ts index 6e45023..bfe4567 100644 --- a/services/backend/src/ws/server.ts +++ b/services/backend/src/ws/server.ts @@ -1,4 +1,5 @@ -import type { Server } from "node:http"; +import type { IncomingMessage, Server } from "node:http"; +import type { Duplex } from "node:stream"; import { WebSocket, WebSocketServer } from "ws"; import { config } from "../shared/config/index.js"; import { BACKEND_COMMAND, BACKEND_VOICE_TRANSMIT } from "../shared/index.js"; @@ -96,9 +97,20 @@ export function createWebSocketServer(server: Server): WebSocketServer { const frontendClients = new Set(); const gatewayClients = new Set(); - const wss = new WebSocketServer({ server, path: "/ws" }); + const wss = new WebSocketServer({ noServer: true, perMessageDeflate: true }); _wss = wss; + // Manual upgrade routing: without this, two `ws` servers bound to the same + // http.Server via the `server` option both register `upgrade` listeners and + // the path-guarded one destructively rejects the other's path (400). We own + // the upgrade event and dispatch by URL instead. + server.on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => { + if (!req.url?.startsWith("/ws")) return; + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + }); + // Map-based dispatcher for JSON WebSocket message types const jsonHandlers = new Map(); diff --git a/services/frontend/package.json b/services/frontend/package.json index 59a86eb..a50901c 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -10,6 +10,7 @@ "format": "biome format --write" }, "dependencies": { + "@trpc/client": "^11.18.0", "clsx": "^2.1.1", "lucide-react": "^1.27.0", "motion": "^12.0.0", diff --git a/services/frontend/pnpm-lock.yaml b/services/frontend/pnpm-lock.yaml index 40f0bd3..89f83fd 100644 --- a/services/frontend/pnpm-lock.yaml +++ b/services/frontend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@trpc/client': + specifier: ^11.18.0 + version: 11.18.0(@trpc/server@11.18.0(typescript@5.9.3))(typescript@5.9.3) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -538,6 +541,19 @@ packages: '@tailwindcss/postcss@4.3.3': resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + '@trpc/client@11.18.0': + resolution: {integrity: sha512-wOqeg3Fvl25V1ZisQhUD3K8G60ZJDlSGJNSyeXrLH24xAo5w6GSR2Kzb1cSNY9Y+IQ2YZvYGZstBU+V/ulo/ow==} + hasBin: true + peerDependencies: + '@trpc/server': 11.18.0 + typescript: '>=5.7.2' + + '@trpc/server@11.18.0': + resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==} + hasBin: true + peerDependencies: + typescript: '>=5.7.2' + '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} @@ -1376,6 +1392,15 @@ snapshots: postcss: 8.5.25 tailwindcss: 4.3.3 + '@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@trpc/server': 11.18.0(typescript@5.9.3) + typescript: 5.9.3 + + '@trpc/server@11.18.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@tweenjs/tween.js@23.1.3': {} '@types/node@20.19.43': diff --git a/services/frontend/src/lib/api/chatbot.ts b/services/frontend/src/lib/api/chatbot.ts index 014c683..35e8b8c 100644 --- a/services/frontend/src/lib/api/chatbot.ts +++ b/services/frontend/src/lib/api/chatbot.ts @@ -1,27 +1,22 @@ +import { trpc } from "@/lib/trpc/client"; import type { ChatbotHistoryRow, ChatbotResponse } from "@/lib/types"; -import { api } from "./client"; - -function userHeader(userId?: string): Record { - return userId && userId !== "anonymous" ? { "X-User-Id": userId } : {}; -} export const chatbotApi = { send: (message: string, guildId?: string, userId?: string) => - api.post( - "/api/chat", - { - message, - context: guildId ? { guildId } : undefined, - }, - userHeader(userId), - ), + trpc.chatbot.chat.mutate({ + message, + context: guildId ? { guildId } : undefined, + userId, + }) as unknown as Promise, getHistory: (userId?: string) => - api.get<{ history: ChatbotHistoryRow[]; total: number }>( - "/api/chat/history", - userHeader(userId), - ), + trpc.chatbot.history.query({ + limit: 50, + userId, + }) as unknown as Promise<{ history: ChatbotHistoryRow[]; total: number }>, clearHistory: (userId?: string) => - api.delete<{ ok: boolean }>("/api/chat/history", userHeader(userId)), + trpc.chatbot.clearHistory.mutate({ + userId, + }) as unknown as Promise<{ ok: boolean }>, }; diff --git a/services/frontend/src/lib/api/client.ts b/services/frontend/src/lib/api/client.ts deleted file mode 100644 index 23c0d12..0000000 --- a/services/frontend/src/lib/api/client.ts +++ /dev/null @@ -1,69 +0,0 @@ -export class ApiError extends Error { - statusCode: number; - - constructor(message: string, statusCode: number) { - super(message); - this.name = "ApiError"; - this.statusCode = statusCode; - } -} - -/** - * API base URL resolution. - * - * Default: same-origin — the production nginx (gmw-proxy) proxies /api/* to - * the backend, so no cross-origin config is needed. For local dev against a - * remote deployment, set NEXT_PUBLIC_API_URL (e.g. https://imphnen.asepharyana.my.id). - */ -function getBaseUrl(): string { - const override = - typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL : ""; - if (override) return override.replace(/\/+$/, ""); - - if (typeof window === "undefined") return ""; - - const protocol = window.location.protocol.replace(":", ""); - const port = window.location.port; - return `${protocol}://${window.location.hostname}${port ? `:${port}` : ""}`; -} - -export async function apiRequest( - method: string, - path: string, - body?: unknown, - headers?: Record, -): Promise { - const url = `${getBaseUrl()}${path}`; - - const finalHeaders: Record = { ...(headers ?? {}) }; - if (body !== undefined) { - finalHeaders["Content-Type"] ??= "application/json"; - } - - const response = await fetch(url, { - method, - headers: finalHeaders, - body: body !== undefined ? JSON.stringify(body) : undefined, - }); - - if (response.status >= 400) { - const text = await response.text().catch(() => ""); - throw new ApiError(text || `HTTP ${response.status}`, response.status); - } - - // Handle 204 No Content (e.g., DELETE) - if (response.status === 204) { - return undefined as T; - } - - return response.json() as Promise; -} - -export const api = { - get: (path: string, headers?: Record) => - apiRequest("GET", path, undefined, headers), - post: (path: string, body?: unknown, headers?: Record) => - apiRequest("POST", path, body, headers), - delete: (path: string, headers?: Record) => - apiRequest("DELETE", path, undefined, headers), -}; diff --git a/services/frontend/src/lib/api/config.ts b/services/frontend/src/lib/api/config.ts index dff3d06..e25ae86 100644 --- a/services/frontend/src/lib/api/config.ts +++ b/services/frontend/src/lib/api/config.ts @@ -1,6 +1,6 @@ +import { trpc } from "@/lib/trpc/client"; import type { AppConfig } from "@/lib/types"; -import { api } from "./client"; export const configApi = { - get: () => api.get("/api/config"), + get: () => trpc.config.get.query() as unknown as Promise, }; diff --git a/services/frontend/src/lib/api/dashboard.ts b/services/frontend/src/lib/api/dashboard.ts index d3a01da..480fe99 100644 --- a/services/frontend/src/lib/api/dashboard.ts +++ b/services/frontend/src/lib/api/dashboard.ts @@ -1,3 +1,4 @@ +import { trpc } from "@/lib/trpc/client"; import type { DashboardActivity, DashboardChannelDetail, @@ -8,44 +9,47 @@ import type { TopReactedMessage, TopReactor, } from "@/lib/types"; -import { api } from "./client"; export const dashboardApi = { - getStats: () => api.get("/api/dashboard/stats"), + getStats: () => + trpc.dashboard.stats.query() as unknown as Promise, getActivity: (days = 14) => - api.get(`/api/dashboard/activity?days=${days}`), + trpc.dashboard.activity.query({ + days, + }) as unknown as Promise, - listUsers: (limit?: number, cursor?: string, search?: string) => { - const params = new URLSearchParams(); - if (limit) params.set("limit", String(limit)); - if (cursor) params.set("cursor", cursor); - if (search) params.set("search", search); - const qs = params.toString(); - return api.get(`/api/dashboard/users${qs ? `?${qs}` : ""}`); - }, + listUsers: (limit?: number, cursor?: string, search?: string) => + trpc.dashboard.users.query({ + limit, + cursor, + search, + }) as unknown as Promise, getUserDetail: (userId: string) => - api.get(`/api/dashboard/users/${userId}`), + trpc.dashboard.userDetail.query({ + userId, + }) as unknown as Promise, - listChannels: (limit?: number, search?: string, guildId?: string) => { - const params = new URLSearchParams(); - if (limit) params.set("limit", String(limit)); - if (search) params.set("search", search); - // Backend reads req.query.guild_id (snake_case) — see createDashboardRouter in dashboard.routes.ts - if (guildId) params.set("guild_id", guildId); - const qs = params.toString(); - return api.get( - `/api/dashboard/channels${qs ? `?${qs}` : ""}`, - ); - }, + listChannels: (limit?: number, search?: string, guildId?: string) => + trpc.dashboard.channels.query({ + limit, + search, + guildId, + }) as unknown as Promise, getChannelDetail: (channelId: string) => - api.get(`/api/dashboard/channels/${channelId}`), + trpc.dashboard.channelDetail.query({ + channelId, + }) as unknown as Promise, getTopReactions: (limit = 20) => - api.get(`/api/dashboard/reactions?limit=${limit}`), + trpc.dashboard.reactions.query({ limit }) as unknown as Promise< + TopReactedMessage[] + >, getTopReactors: (limit = 20) => - api.get(`/api/dashboard/reactors?limit=${limit}`), + trpc.dashboard.reactors.query({ limit }) as unknown as Promise< + TopReactor[] + >, }; diff --git a/services/frontend/src/lib/api/index.ts b/services/frontend/src/lib/api/index.ts index 7bce938..ae8b1e5 100644 --- a/services/frontend/src/lib/api/index.ts +++ b/services/frontend/src/lib/api/index.ts @@ -1,5 +1,8 @@ +// tRPC client is browser-only (wsLink). Re-export it for convenience / for any +// code that wants to call tRPC directly instead of going through the api/* +// wrappers. Server-side RSC data lives in ./server (httpLink). +export { trpc } from "../trpc/client"; export { chatbotApi } from "./chatbot"; -export { ApiError, api, apiRequest } from "./client"; export { configApi } from "./config"; export { dashboardApi } from "./dashboard"; export { mediaApi } from "./media"; diff --git a/services/frontend/src/lib/api/media.ts b/services/frontend/src/lib/api/media.ts index 470e827..e3e3fec 100644 --- a/services/frontend/src/lib/api/media.ts +++ b/services/frontend/src/lib/api/media.ts @@ -1,11 +1,12 @@ +import { trpc } from "@/lib/trpc/client"; import type { MediaState } from "@/lib/types"; -import { api } from "./client"; export const mediaApi = { - getStatus: () => api.get("/api/media/status"), + getStatus: () => trpc.media.status.query() as unknown as Promise, queue: (source: string, mode: string) => - api.post("/api/media/queue", { source, mode }), - skip: () => api.post("/api/media/skip", {}), - stop: () => api.post("/api/media/stop", {}), - loop: (loop: boolean) => api.post("/api/media/loop", { loop }), + trpc.media.queue.mutate({ source, mode }) as unknown as Promise, + skip: () => trpc.media.skip.mutate() as unknown as Promise, + stop: () => trpc.media.stop.mutate() as unknown as Promise, + loop: (loop: boolean) => + trpc.media.loop.mutate({ loop }) as unknown as Promise, }; diff --git a/services/frontend/src/lib/api/messages.ts b/services/frontend/src/lib/api/messages.ts index b1f42b1..4f71a0e 100644 --- a/services/frontend/src/lib/api/messages.ts +++ b/services/frontend/src/lib/api/messages.ts @@ -1,5 +1,5 @@ +import { trpc } from "@/lib/trpc/client"; import type { AttachmentRecord, MessageRecord } from "@/lib/types"; -import { api } from "./client"; export const messagesApi = { list: ( @@ -7,69 +7,59 @@ export const messagesApi = { limit?: number, channelId?: string, cursor?: string, - ) => { - // Backend messageQuerySchema expects camelCase guildId (see messages.schema.ts) - const params = new URLSearchParams({ guildId }); - if (limit) params.set("limit", String(limit)); - if (channelId) params.set("channelId", channelId); - if (cursor) params.set("cursor", cursor); - return api.get<{ data: MessageRecord[]; nextCursor: string | null }>( - `/api/messages?${params}`, - ); - }, + ) => + trpc.messages.list.query({ + guildId, + limit, + channelId, + cursor, + }) as unknown as Promise<{ + data: MessageRecord[]; + nextCursor: string | null; + }>, - getByChannel: (channelId: string, limit?: number, cursor?: string) => { - const params = new URLSearchParams(); - if (limit) params.set("limit", String(limit)); - if (cursor) params.set("cursor", cursor); - const qs = params.toString(); - return api.get<{ data: MessageRecord[]; nextCursor: string | null }>( - `/api/messages/${channelId}${qs ? `?${qs}` : ""}`, - ); - }, + getByChannel: (channelId: string, limit?: number, cursor?: string) => + trpc.messages.byChannel.query({ + channelId, + query: { channelId, limit, cursor }, + }) as unknown as Promise<{ + data: MessageRecord[]; + nextCursor: string | null; + }>, getDetail: (id: string) => - api.get(`/api/messages/detail/${id}`), + trpc.messages.detail.query({ id }) as unknown as Promise, - getImages: (guildId: string, limit?: number) => { - // Backend reads req.query.guildId (camelCase) — see handleGetImageMessages in messages.controller.ts - const params = new URLSearchParams({ guildId }); - if (limit) params.set("limit", String(limit)); - return api.get<{ data: MessageRecord[]; nextCursor: string | null }>( - `/api/messages/images?${params}`, - ); - }, + getImages: (guildId: string, limit?: number) => + trpc.messages.images.query({ guildId, limit }) as unknown as Promise<{ + data: MessageRecord[]; + nextCursor: string | null; + }>, getAttachments: ( channelId: string, limit?: number, cursor?: string, messageId?: string, - ) => { - const params = new URLSearchParams(); - if (limit) params.set("limit", String(limit)); - if (cursor) params.set("cursor", cursor); - if (messageId) params.set("messageId", messageId); - const qs = params.toString(); - return api.get<{ data: AttachmentRecord[]; nextCursor: string | null }>( - `/api/messages/${channelId}/attachments${qs ? `?${qs}` : ""}`, - ); - }, + ) => + trpc.messages.attachmentsByChannel.query({ + channelId, + query: { channelId, limit, cursor, messageId }, + }) as unknown as Promise<{ + data: AttachmentRecord[]; + nextCursor: string | null; + }>, - getReview: (limit?: number, channelId?: string) => { - const params = new URLSearchParams(); - if (limit) params.set("limit", String(limit)); - if (channelId) params.set("channelId", channelId); - return api.get<{ results: MessageRecord[]; limit: number; cursor: null }>( - `/api/review?${params}`, - ); - }, + getReview: (limit?: number, channelId?: string) => + trpc.messages.review.query({ limit, channelId }) as unknown as Promise<{ + results: MessageRecord[]; + limit: number; + cursor: null; + }>, - search: (query: string, limit?: number) => { - const params = new URLSearchParams({ q: query }); - if (limit) params.set("limit", String(limit)); - return api.get<{ results: MessageRecord[] }>( - `/api/analysis/search?${params}`, - ); - }, + // Analysis search (formerly /api/analysis/search → tRPC analysis.search) + search: (q: string, limit?: number) => + trpc.analysis.search.query({ q, limit }) as unknown as Promise<{ + results: MessageRecord[]; + }>, }; diff --git a/services/frontend/src/lib/api/moderation.ts b/services/frontend/src/lib/api/moderation.ts index d69b67b..cab2037 100644 --- a/services/frontend/src/lib/api/moderation.ts +++ b/services/frontend/src/lib/api/moderation.ts @@ -1,23 +1,20 @@ +import { trpc } from "@/lib/trpc/client"; import type { ModerationStats, PaginatedModerationActions } from "@/lib/types"; -import { api } from "./client"; export const moderationApi = { - getStats: () => api.get("/api/moderation/stats"), + getStats: () => + trpc.moderation.stats.query() as unknown as Promise, listActions: ( limit?: number, status?: string, actionType?: string, cursor?: string, - ) => { - const params = new URLSearchParams(); - if (limit) params.set("limit", String(limit)); - if (status) params.set("status", status); - if (actionType) params.set("actionType", actionType); - if (cursor) params.set("cursor", cursor); - const qs = params.toString(); - return api.get( - `/api/moderation/actions${qs ? `?${qs}` : ""}`, - ); - }, + ) => + trpc.moderation.actions.query({ + limit, + status, + actionType, + cursor, + }) as unknown as Promise, }; diff --git a/services/frontend/src/lib/api/recordings.ts b/services/frontend/src/lib/api/recordings.ts index 0ae8ed5..69dfa59 100644 --- a/services/frontend/src/lib/api/recordings.ts +++ b/services/frontend/src/lib/api/recordings.ts @@ -1,5 +1,5 @@ +import { trpc } from "@/lib/trpc/client"; import type { PaginatedRecordings } from "@/lib/types"; -import { api } from "./client"; export const recordingsApi = { list: ( @@ -7,15 +7,16 @@ export const recordingsApi = { channelId?: string, userId?: string, cursor?: string, - ) => { - const params = new URLSearchParams(); - if (limit) params.set("limit", String(limit)); - if (channelId) params.set("channelId", channelId); - if (userId) params.set("userId", userId); - if (cursor) params.set("cursor", cursor); - const qs = params.toString(); - return api.get(`/api/recordings${qs ? `?${qs}` : ""}`); - }, + ) => + trpc.recordings.list.query({ + limit, + channelId, + userId, + cursor, + }) as unknown as Promise, - delete: (id: string) => api.delete<{ ok: boolean }>(`/api/recordings/${id}`), + delete: (id: string) => + trpc.recordings.delete.mutate({ id }) as unknown as Promise<{ + ok: boolean; + }>, }; diff --git a/services/frontend/src/lib/api/server.ts b/services/frontend/src/lib/api/server.ts index fc3e695..79d7083 100644 --- a/services/frontend/src/lib/api/server.ts +++ b/services/frontend/src/lib/api/server.ts @@ -1,30 +1,51 @@ /** - * Server-only data layer. + * Server-only data layer (React Server Components / route handlers). * - * These fetchers run exclusively on the Next.js server (React Server - * Components / route handlers). They call the backend over HTTP directly - * (`GMW_BACKEND_URL`), so the browser never needs a client round-trip for the - * initial page data — the first paint is server-rendered. + * The dashboard is fully tRPC-native: this module talks to the backend's + * tRPC HTTP endpoint (/trpc) via an httpLink client — the same appRouter the + * browser reaches over WebSocket. No legacy REST `/api/*` is used. * - * Never import this module from a client component. Browser code should keep - * using `@/lib/api/client` (same-origin via the reverse proxy) for live ops. + * The client is loosely typed (see ./types → TRPCClient); results are asserted + * to the frontend's local types at each call site. + * + * Never import this module from a client component. Browser code uses + * `@/lib/trpc/client` (wsLink) via the `@/lib/api/*` wrappers. */ +import { createTRPCClient, httpBatchLink } from "@trpc/client"; import type { AppConfig, DashboardActivity, DashboardStats, Guild, MediaState, - ModerationAction, ModerationStats, + PaginatedModerationActions, PaginatedRecordings, VoiceStatus, } from "@/lib/types"; +import type { TRPCClient } from "../trpc/types"; const BACKEND_URL = process.env.GMW_BACKEND_URL?.replace(/\/+$/, "") || "http://127.0.0.1:4001"; +let _client: TRPCClient | null = null; +function serverTrpc(): TRPCClient { + if (!_client) { + _client = createTRPCClient({ + links: [ + httpBatchLink({ + url: `${BACKEND_URL}/trpc`, + fetch(url, init) { + return fetch(url, { ...init, cache: "no-store" }); + }, + }), + ], + }) as unknown as TRPCClient; + } + return _client; +} + export class ApiServerError extends Error { statusCode: number; constructor(message: string, statusCode: number) { @@ -34,102 +55,48 @@ export class ApiServerError extends Error { } } -async function serverFetch( - path: string, - init?: { timeoutMs?: number }, -): Promise { - const url = `${BACKEND_URL}${path}`; - const controller = new AbortController(); - const timeout = setTimeout( - () => controller.abort(), - init?.timeoutMs ?? 8_000, - ); - - let res: Response; - try { - res = await fetch(url, { - headers: { Accept: "application/json" }, - cache: "no-store", - signal: controller.signal, - }); - } finally { - clearTimeout(timeout); - } - - if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new ApiServerError(text || `HTTP ${res.status}`, res.status); - } - return res.json() as Promise; -} - // ---- Dashboard ---- - export async function getDashboardStats(): Promise { - return serverFetch("/api/dashboard/stats"); + return serverTrpc().dashboard.stats.query() as unknown as Promise; } - export async function getActivity(days = 14): Promise { - return serverFetch(`/api/dashboard/activity?days=${days}`); + return serverTrpc().dashboard.activity.query({ + days, + }) as unknown as Promise; } // ---- Media ---- - export async function getMediaStatus(): Promise { - return serverFetch("/api/media/status"); + return serverTrpc().media.status.query() as unknown as Promise; } // ---- Config ---- - export async function getConfig(): Promise { - return serverFetch("/api/config"); + return serverTrpc().config.get.query() as unknown as Promise; } // ---- Moderation ---- - export async function getModerationStats(): Promise { - return serverFetch("/api/moderation/stats"); + return serverTrpc().moderation.stats.query() as unknown as Promise; } - -export async function getModerationActions( - limit = 100, -): Promise { - const res = await serverFetch<{ data: ModerationAction[] }>( - `/api/moderation/actions?limit=${limit}`, - ); +export async function getModerationActions(limit = 100) { + const res = (await serverTrpc().moderation.actions.query({ + limit, + })) as unknown as PaginatedModerationActions; return res.data; } // ---- Voice ---- - export async function getGuilds(): Promise { - return serverFetch("/api/guilds"); + return serverTrpc().voice.guilds.query() as unknown as Promise; } - export async function getVoiceStatus(): Promise { - return serverFetch("/api/voice/status"); + return serverTrpc().voice.status.query() as unknown as Promise; } // ---- Recordings ---- - export async function getRecordings(limit = 50): Promise { - return serverFetch(`/api/recordings?limit=${limit}`); -} - -// ---- Messages ---- - -export interface MessagePageResult { - data: import("@/lib/types").MessageRecord[]; - nextCursor: string | null; -} - -export async function getMessages( - guildId: string, - channelId?: string, - cursor?: string, -): Promise { - const params = new URLSearchParams({ guildId }); - if (channelId) params.set("channelId", channelId); - if (cursor) params.set("cursor", cursor); - return serverFetch(`/api/messages?${params.toString()}`); + return serverTrpc().recordings.list.query({ + limit, + }) as unknown as Promise; } diff --git a/services/frontend/src/lib/api/ui-state.ts b/services/frontend/src/lib/api/ui-state.ts index 9d13f75..f74718d 100644 --- a/services/frontend/src/lib/api/ui-state.ts +++ b/services/frontend/src/lib/api/ui-state.ts @@ -1,8 +1,9 @@ +import { trpc } from "@/lib/trpc/client"; import type { UiState } from "@/lib/types"; -import { api } from "./client"; export const uiStateApi = { - get: () => api.get("/api/ui-state"), + get: () => trpc.uiState.get.query() as unknown as Promise, - save: (state: UiState) => api.post<{ ok: boolean }>("/api/ui-state", state), + save: (state: UiState) => + trpc.uiState.update.mutate(state) as unknown as Promise<{ ok: boolean }>, }; diff --git a/services/frontend/src/lib/api/voice.ts b/services/frontend/src/lib/api/voice.ts index 373a1b0..718faaa 100644 --- a/services/frontend/src/lib/api/voice.ts +++ b/services/frontend/src/lib/api/voice.ts @@ -1,21 +1,25 @@ +import { trpc } from "@/lib/trpc/client"; import type { Channel, Guild, VoiceStatus } from "@/lib/types"; -import { api } from "./client"; export const voiceApi = { // Guilds - getGuilds: () => api.get("/api/guilds"), + getGuilds: () => trpc.voice.guilds.query() as unknown as Promise, getTextChannels: (guildId: string) => - api.get(`/api/guilds/${guildId}/channels`), + trpc.voice.textChannels.query({ guildId }) as unknown as Promise, getVoiceChannels: (guildId: string) => - api.get(`/api/guilds/${guildId}/voice-channels`), + trpc.voice.voiceChannels.query({ + guildId, + }) as unknown as Promise, // Voice connection - getStatus: () => api.get("/api/voice/status"), + getStatus: () => trpc.voice.status.query() as unknown as Promise, connect: (guildId: string, channelId: string) => - api.post("/api/voice/connect", { guildId, channelId }), - disconnect: () => api.post("/api/voice/disconnect", {}), + trpc.voice.connect.mutate({ + guildId, + channelId, + }) as unknown as Promise, + disconnect: () => + trpc.voice.disconnect.mutate() as unknown as Promise, sendCommand: (command: string) => - api.post<{ success: boolean; command: string }>("/api/voice/command", { - command, - }), + trpc.voice.command.mutate({ command }) as unknown as Promise, }; diff --git a/services/frontend/src/lib/trpc/client.ts b/services/frontend/src/lib/trpc/client.ts new file mode 100644 index 0000000..bf63583 --- /dev/null +++ b/services/frontend/src/lib/trpc/client.ts @@ -0,0 +1,33 @@ +"use client"; + +import { createTRPCClient, createWSClient, wsLink } from "@trpc/client"; +import type { TRPCClient } from "./types"; + +/** + * WebSocket URL for the tRPC data RPC endpoint (/trpc), served by the backend + * on the same host as the page (nginx proxies it). Upgrades http→ws and + * derives wss:// when the page is served over https. + */ +function resolveWsUrl(): string { + if (typeof window === "undefined") return "ws://localhost/trpc"; + const proto = window.location.protocol === "https:" ? "wss" : "ws"; + return `${proto}://${window.location.host}/trpc`; +} + +/** + * tRPC client over WebSocket (browser only). `createTRPCClient` (no router + * generic) is an untyped client; we assert it into the loose `TRPCClient` + * shape so `.dashboard.stats.query(...)` etc. typecheck. See ./types for why + * the client shape is intentionally untyped. + */ +const wsClient = + typeof window === "undefined" + ? null + : createWSClient({ url: resolveWsUrl() }); + +export const trpc: TRPCClient = wsClient + ? (createTRPCClient({ + links: [wsLink({ client: wsClient })], + }) as unknown as TRPCClient) + : // SSR fallback — api/* callers are client components, never run server-side. + (undefined as unknown as TRPCClient); diff --git a/services/frontend/src/lib/trpc/types.ts b/services/frontend/src/lib/trpc/types.ts new file mode 100644 index 0000000..be757ae --- /dev/null +++ b/services/frontend/src/lib/trpc/types.ts @@ -0,0 +1,15 @@ +/** + * Loosely-typed tRPC client shape shared by the browser (wsLink) and + * server-side (httpLink) clients. The real router lives on the backend; we do + * NOT import its type here (coupling the FE typecheck to the BE source tree + * and its `@/` alias layout is fragile). Instead we describe the minimal shape + * the api/* wrappers rely on: any path segment yields an object with `query` + * and `mutate`, and arbitrary further nesting is allowed. Leaf results are + * asserted to the frontend's local types inside the api/* wrappers. + */ +export type TRPCClient = { + [k: string]: TRPCClient; +} & { + query(input?: unknown): Promise; + mutate(input?: unknown): Promise; +};