fIx: smaller nits
This commit is contained in:
678
backend/package-lock.json
generated
678
backend/package-lock.json
generated
@@ -13,6 +13,13 @@
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
@@ -103,6 +110,448 @@
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
@@ -112,12 +561,120 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/diff-match-patch": {
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
|
||||
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
"@types/serve-static": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
|
||||
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
||||
"integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -392,6 +949,48 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.4",
|
||||
"@esbuild/android-arm": "0.27.4",
|
||||
"@esbuild/android-arm64": "0.27.4",
|
||||
"@esbuild/android-x64": "0.27.4",
|
||||
"@esbuild/darwin-arm64": "0.27.4",
|
||||
"@esbuild/darwin-x64": "0.27.4",
|
||||
"@esbuild/freebsd-arm64": "0.27.4",
|
||||
"@esbuild/freebsd-x64": "0.27.4",
|
||||
"@esbuild/linux-arm": "0.27.4",
|
||||
"@esbuild/linux-arm64": "0.27.4",
|
||||
"@esbuild/linux-ia32": "0.27.4",
|
||||
"@esbuild/linux-loong64": "0.27.4",
|
||||
"@esbuild/linux-mips64el": "0.27.4",
|
||||
"@esbuild/linux-ppc64": "0.27.4",
|
||||
"@esbuild/linux-riscv64": "0.27.4",
|
||||
"@esbuild/linux-s390x": "0.27.4",
|
||||
"@esbuild/linux-x64": "0.27.4",
|
||||
"@esbuild/netbsd-arm64": "0.27.4",
|
||||
"@esbuild/netbsd-x64": "0.27.4",
|
||||
"@esbuild/openbsd-arm64": "0.27.4",
|
||||
"@esbuild/openbsd-x64": "0.27.4",
|
||||
"@esbuild/openharmony-arm64": "0.27.4",
|
||||
"@esbuild/sunos-x64": "0.27.4",
|
||||
"@esbuild/win32-arm64": "0.27.4",
|
||||
"@esbuild/win32-ia32": "0.27.4",
|
||||
"@esbuild/win32-x64": "0.27.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -489,6 +1088,21 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -535,6 +1149,19 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.6",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -853,6 +1480,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1051,6 +1688,26 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -1064,6 +1721,27 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Minimal Express API for Zui (agent, health; no DB)",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "node --watch src/index.js"
|
||||
"start": "node src/index.ts",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -16,5 +18,12 @@
|
||||
"@ai-sdk/openai": "^1.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* Minimal Express API: /api/agent, /health.
|
||||
* No DB, no auth. CORS allowed for frontend. Production-ready env (PORT, CORS_ORIGIN).
|
||||
*/
|
||||
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
|
||||
const PORT = Number(process.env.PORT) || 8080
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(cors({ origin: CORS_ORIGIN }))
|
||||
app.use(express.json())
|
||||
|
||||
/** Build OpenAI client and full prompt from request body. Returns { openai, modelId, fullPrompt } or { error }. */
|
||||
function buildAgentRequest(body) {
|
||||
const { prompt, context, contextNodes, connection: conn, reasoning } = body ?? {}
|
||||
const reasoningEnabled = Boolean(reasoning)
|
||||
let baseURL = process.env.AI_BASE_URL?.trim() || null
|
||||
let apiKey = process.env.OPENAI_API_KEY?.trim() || null
|
||||
let modelId = process.env.AI_MODEL?.trim() || (baseURL ? 'local-model' : 'gpt-4o-mini')
|
||||
if (conn && typeof conn === 'object') {
|
||||
const c = conn
|
||||
const provider = c.provider === 'openai' ? 'openai' : 'local'
|
||||
if (provider === 'local') {
|
||||
baseURL = (typeof c.baseURL === 'string' && c.baseURL.trim()) ? c.baseURL.trim() : baseURL
|
||||
apiKey = (typeof c.apiKey === 'string' && c.apiKey.trim()) ? c.apiKey.trim() : (apiKey || 'lm-studio')
|
||||
} else {
|
||||
baseURL = null
|
||||
apiKey = (typeof c.apiKey === 'string' && c.apiKey.trim()) ? c.apiKey.trim() : apiKey
|
||||
}
|
||||
if (typeof c.model === 'string' && c.model.trim()) modelId = c.model.trim()
|
||||
}
|
||||
if (!baseURL && !apiKey) {
|
||||
return { error: 'No AI configured. Set connection in Settings (AI) or env: OPENAI_API_KEY or AI_BASE_URL.' }
|
||||
}
|
||||
const basePrompt = [
|
||||
typeof prompt === 'string' ? prompt : 'No prompt provided.',
|
||||
context && typeof context === 'string' ? `\n\nAdditional context:\n${context}` : '',
|
||||
Array.isArray(contextNodes) && contextNodes.length > 0
|
||||
? `\n\nContext from connected nodes:\n${contextNodes.map((n) => (n.content != null ? n.content : `${n.id}: (no content)`)).join('\n\n')}`
|
||||
: '',
|
||||
].join('')
|
||||
const fullPrompt = reasoningEnabled
|
||||
? basePrompt + '\n\nRespond in exactly two markdown sections. First: "## Reasoning" with your step-by-step reasoning. Then: "## Output" with only the final answer. No preamble.'
|
||||
: basePrompt + '\n\nRespond with structured markdown only. No preamble.'
|
||||
const { createOpenAI } = require('@ai-sdk/openai')
|
||||
const openai = createOpenAI({
|
||||
apiKey: apiKey || 'lm-studio',
|
||||
...(baseURL && { baseURL, compatibility: 'compatible' }),
|
||||
})
|
||||
return { openai, modelId, fullPrompt }
|
||||
}
|
||||
|
||||
/** POST /api/agent — run AI agent; body: { prompt, context?, contextNodes?, connection? }; returns { markdown }. */
|
||||
app.post('/api/agent', async (req, res) => {
|
||||
try {
|
||||
const built = buildAgentRequest(req.body)
|
||||
if (built.error) return res.status(503).json({ error: built.error })
|
||||
const { generateText } = await import('ai')
|
||||
const result = await generateText({
|
||||
model: built.openai(built.modelId),
|
||||
prompt: built.fullPrompt,
|
||||
})
|
||||
const markdown = result?.text ?? ''
|
||||
res.json({ markdown })
|
||||
} catch (err) {
|
||||
console.error('Agent error:', err)
|
||||
res.status(500).json({ error: err?.message ?? 'Agent request failed' })
|
||||
}
|
||||
})
|
||||
|
||||
/** POST /api/agent/stream — same as /api/agent but streams plain text (markdown) chunks. */
|
||||
app.post('/api/agent/stream', async (req, res) => {
|
||||
try {
|
||||
const built = buildAgentRequest(req.body)
|
||||
if (built.error) return res.status(503).json({ error: built.error })
|
||||
const { streamText } = await import('ai')
|
||||
const result = streamText({
|
||||
model: built.openai(built.modelId),
|
||||
prompt: built.fullPrompt,
|
||||
})
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||
res.setHeader('Transfer-Encoding', 'chunked')
|
||||
result.pipeTextStreamToResponse(res)
|
||||
} catch (err) {
|
||||
console.error('Agent stream error:', err)
|
||||
res.status(500).json({ error: err?.message ?? 'Agent request failed' })
|
||||
}
|
||||
})
|
||||
|
||||
/** Health check for Docker / orchestration */
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({ ok: true })
|
||||
})
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Backend listening on port ${PORT} (CORS: ${CORS_ORIGIN})`)
|
||||
})
|
||||
102
backend/src/index.ts
Normal file
102
backend/src/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Backend API: Layered architecture for scalability and maintainability.
|
||||
*
|
||||
* ## Architecture Layers
|
||||
*
|
||||
* ### Controllers (routes/)
|
||||
* - Handle HTTP requests and responses
|
||||
* - Validate input and format output
|
||||
* - Call services for business logic
|
||||
*
|
||||
* ### Services (services/)
|
||||
* - Implement business logic
|
||||
* - Coordinate between controllers and repositories
|
||||
* - Handle validation and transformation
|
||||
*
|
||||
* ### Repositories (repositories/)
|
||||
* - Data access layer
|
||||
* - Interact with databases or external APIs
|
||||
* - Return domain models
|
||||
*
|
||||
* ### Models (models/)
|
||||
* - Data structures and types
|
||||
* - Validation schemas
|
||||
*
|
||||
* ### Middleware (middleware/)
|
||||
* - Request validation
|
||||
* - Authentication/authorization
|
||||
* - Rate limiting
|
||||
* - Error handling
|
||||
*
|
||||
* ## Adding New Endpoints
|
||||
*
|
||||
* 1. Define the model in `models/`
|
||||
* 2. Implement repository logic in `repositories/`
|
||||
* 3. Implement service logic in `services/`
|
||||
* 4. Create controller in `routes/`
|
||||
* 5. Register route in `index.ts`
|
||||
*/
|
||||
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PORT = Number(process.env.PORT) || 8080
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:3000'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Express App Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(cors({ origin: CORS_ORIGIN }))
|
||||
app.use(express.json())
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent Routes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { handleAgentRequest, handleAgentStreamRequest } from './routes/agentRoutes.js'
|
||||
|
||||
/** POST /api/agent - Run AI agent */
|
||||
app.post('/api/agent', handleAgentRequest)
|
||||
|
||||
/** POST /api/agent/stream - Stream AI agent response */
|
||||
app.post('/api/agent/stream', handleAgentStreamRequest)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health Check Endpoint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** GET /health - Health check for Docker / orchestration */
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({ ok: true, timestamp: Date.now() })
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error Handling Middleware
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Global error handler for consistent error responses */
|
||||
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error('Error:', err)
|
||||
res.status(500).json({ error: err.message ?? 'Internal server error' })
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Start Server
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Backend listening on port ${PORT} (CORS: ${CORS_ORIGIN})`)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export for testing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default app
|
||||
88
backend/src/middleware/rateLimiter.ts
Normal file
88
backend/src/middleware/rateLimiter.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Middleware: Rate limiting for API endpoints.
|
||||
*
|
||||
* This module provides rate limiting middleware to protect the API
|
||||
* from abuse and ensure fair usage.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Rate limit configuration
|
||||
*/
|
||||
export type RateLimitConfig = {
|
||||
/** Maximum number of requests allowed in the window */
|
||||
max: number
|
||||
/** Window size in milliseconds */
|
||||
windowMs: number
|
||||
/** Error message to return when rate limited */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Default rate limit configuration
|
||||
*/
|
||||
export const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = {
|
||||
max: 100,
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
message: 'Too many requests, please try again later.',
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory store for rate limiting
|
||||
*/
|
||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>()
|
||||
|
||||
/**
|
||||
* Rate limiting middleware
|
||||
*
|
||||
* @param config - Rate limit configuration
|
||||
* @returns Express middleware function
|
||||
*/
|
||||
export function rateLimit(config: RateLimitConfig = DEFAULT_RATE_LIMIT_CONFIG) {
|
||||
return (req: { ip?: string; socket?: { remoteAddress?: string } }, res: { status: (code: number) => { json: (body: { error: string }) => void }, locals: Record<string, unknown> }, next: () => void): void => {
|
||||
const ip = req.ip || req.socket?.remoteAddress || 'unknown'
|
||||
const now = Date.now()
|
||||
|
||||
let entry = rateLimitStore.get(ip)
|
||||
|
||||
if (!entry || now > entry.resetTime) {
|
||||
// Reset the counter for a new window
|
||||
entry = { count: 1, resetTime: now + config.windowMs }
|
||||
rateLimitStore.set(ip, entry)
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
entry.count++
|
||||
|
||||
if (entry.count > config.max) {
|
||||
res.status(429).json({ error: config.message ?? 'Too many requests' })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired entries from the rate limit store
|
||||
*/
|
||||
export function cleanupRateLimitStore(): void {
|
||||
const now = Date.now()
|
||||
for (const [ip, entry] of rateLimitStore.entries()) {
|
||||
if (now > entry.resetTime) {
|
||||
rateLimitStore.delete(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate limit status for an IP
|
||||
*/
|
||||
export function getRateLimitStatus(ip: string): { remaining: number; resetTime: number } | null {
|
||||
const entry = rateLimitStore.get(ip)
|
||||
if (!entry) return null
|
||||
return {
|
||||
remaining: Math.max(0, DEFAULT_RATE_LIMIT_CONFIG.max - entry.count),
|
||||
resetTime: entry.resetTime,
|
||||
}
|
||||
}
|
||||
63
backend/src/models/index.ts
Normal file
63
backend/src/models/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Models: Data structures and validation schemas.
|
||||
*
|
||||
* This module defines the domain models used throughout the application.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Agent request payload
|
||||
*/
|
||||
export type AgentRequest = {
|
||||
prompt: string
|
||||
context?: string
|
||||
contextNodes?: AgentContextNode[]
|
||||
connection?: AgentConnection
|
||||
reasoning?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Context node for agent requests
|
||||
*/
|
||||
export type AgentContextNode = {
|
||||
id: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AI connection configuration
|
||||
*/
|
||||
export type AgentConnection = {
|
||||
provider: 'openai' | 'local'
|
||||
baseURL?: string
|
||||
apiKey?: string
|
||||
model?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent response
|
||||
*/
|
||||
export type AgentResponse = {
|
||||
markdown: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent stream response
|
||||
*/
|
||||
export type AgentStreamResponse = {
|
||||
markdown: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check response
|
||||
*/
|
||||
export type HealthResponse = {
|
||||
ok: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response
|
||||
*/
|
||||
export type ErrorResponse = {
|
||||
error: string
|
||||
}
|
||||
103
backend/src/repositories/cacheRepository.ts
Normal file
103
backend/src/repositories/cacheRepository.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Repositories: Data access layer.
|
||||
*
|
||||
* This module handles data persistence and retrieval.
|
||||
* It abstracts the underlying storage mechanism.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cache entry for expensive computations
|
||||
*/
|
||||
export type CacheEntry<T> = {
|
||||
/** Unique key for the cache entry */
|
||||
key: string
|
||||
/** Cached data */
|
||||
data: T
|
||||
/** Timestamp (ms) when this entry was created */
|
||||
createdAt: number
|
||||
/** Time-to-live in milliseconds */
|
||||
ttl?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory cache implementation
|
||||
*/
|
||||
export class InMemoryCache {
|
||||
private cache = new Map<string, CacheEntry<unknown>>()
|
||||
private defaultTtl = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
/**
|
||||
* Get a value from the cache
|
||||
*
|
||||
* @param key - Cache key
|
||||
* @returns Cached value or null if not found or expired
|
||||
*/
|
||||
get<T>(key: string): T | null {
|
||||
const entry = this.cache.get(key) as CacheEntry<T> | undefined
|
||||
if (!entry) return null
|
||||
|
||||
// Check if entry has expired
|
||||
if (entry.ttl && Date.now() - entry.createdAt > entry.ttl) {
|
||||
this.cache.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return entry.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the cache
|
||||
*
|
||||
* @param key - Cache key
|
||||
* @param data - Data to cache
|
||||
* @param ttl - Optional time-to-live in milliseconds (uses default if not specified)
|
||||
*/
|
||||
set<T>(key: string, data: T, ttl?: number): void {
|
||||
this.cache.set(key, {
|
||||
key,
|
||||
data,
|
||||
createdAt: Date.now(),
|
||||
ttl: ttl ?? this.defaultTtl,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a value from the cache
|
||||
*
|
||||
* @param key - Cache key
|
||||
*/
|
||||
delete(key: string): void {
|
||||
this.cache.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all entries from the cache
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired entries
|
||||
*/
|
||||
cleanup(): void {
|
||||
const now = Date.now()
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (entry.ttl && now - entry.createdAt > entry.ttl) {
|
||||
this.cache.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of entries in the cache
|
||||
*/
|
||||
size(): number {
|
||||
return this.cache.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global cache instance
|
||||
*/
|
||||
export const cache = new InMemoryCache()
|
||||
72
backend/src/routes/agentRoutes.ts
Normal file
72
backend/src/routes/agentRoutes.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Routes: HTTP request handlers.
|
||||
*
|
||||
* This module contains the route handlers for the API.
|
||||
* Routes validate input and format output, then call services for business logic.
|
||||
*/
|
||||
|
||||
import type { AgentRequest, AgentResponse, ErrorResponse } from '../models'
|
||||
|
||||
/**
|
||||
* POST /api/agent - Run AI agent
|
||||
*
|
||||
* Request body: { prompt, context?, contextNodes?, connection? }
|
||||
* Response: { markdown }
|
||||
*/
|
||||
export async function handleAgentRequest(req: any, res: any): Promise<void> {
|
||||
try {
|
||||
const body = req.body
|
||||
const { buildAgentRequest } = await import('../services/agentService')
|
||||
|
||||
const built = buildAgentRequest(body)
|
||||
if ('error' in built) {
|
||||
res.status(503).json({ error: built.error } as ErrorResponse)
|
||||
return
|
||||
}
|
||||
|
||||
const { generateText } = await import('ai')
|
||||
const result = await generateText({
|
||||
model: built.openai as any,
|
||||
prompt: built.fullPrompt,
|
||||
})
|
||||
|
||||
const { processAgentResponse } = await import('../services/agentService')
|
||||
const response = processAgentResponse(result)
|
||||
res.json(response)
|
||||
} catch (err) {
|
||||
console.error('Agent error:', err)
|
||||
res.status(500).json({ error: (err as Error).message ?? 'Agent request failed' } as ErrorResponse)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/agent/stream - Stream AI agent response
|
||||
*
|
||||
* Request body: { prompt, context?, contextNodes?, connection? }
|
||||
* Response: plain text (markdown) chunks
|
||||
*/
|
||||
export async function handleAgentStreamRequest(req: any, res: any): Promise<void> {
|
||||
try {
|
||||
const body = req.body
|
||||
const { buildAgentRequest } = await import('../services/agentService')
|
||||
|
||||
const built = buildAgentRequest(body)
|
||||
if ('error' in built) {
|
||||
res.status(503).json({ error: built.error } as ErrorResponse)
|
||||
return
|
||||
}
|
||||
|
||||
const { streamText } = await import('ai')
|
||||
const result = streamText({
|
||||
model: built.openai as any,
|
||||
prompt: built.fullPrompt,
|
||||
})
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||
res.setHeader('Transfer-Encoding', 'chunked')
|
||||
result.pipeTextStreamToResponse(res)
|
||||
} catch (err) {
|
||||
console.error('Agent stream error:', err)
|
||||
res.status(500).json({ error: (err as Error).message ?? 'Agent request failed' } as ErrorResponse)
|
||||
}
|
||||
}
|
||||
65
backend/src/services/agentService.ts
Normal file
65
backend/src/services/agentService.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Services: Business logic layer.
|
||||
*
|
||||
* This module contains the business logic for the application.
|
||||
* Services coordinate between controllers and repositories.
|
||||
*/
|
||||
|
||||
import type { AgentRequest, AgentResponse, AgentConnection } from '../models'
|
||||
|
||||
/**
|
||||
* Build OpenAI client and full prompt from request body.
|
||||
* Returns { openai, modelId, fullPrompt } or { error }.
|
||||
*/
|
||||
export function buildAgentRequest(body: AgentRequest): { openai: unknown; modelId: string; fullPrompt: string } | { error: string } {
|
||||
const { prompt, context, contextNodes, connection: conn, reasoning } = body ?? {}
|
||||
const reasoningEnabled = Boolean(reasoning)
|
||||
let baseURL = process.env.AI_BASE_URL?.trim() || null
|
||||
let apiKey = process.env.OPENAI_API_KEY?.trim() || null
|
||||
let modelId = process.env.AI_MODEL?.trim() || (baseURL ? 'local-model' : 'gpt-4o-mini')
|
||||
|
||||
if (conn && typeof conn === 'object') {
|
||||
const c = conn as AgentConnection
|
||||
const provider = c.provider === 'openai' ? 'openai' : 'local'
|
||||
if (provider === 'local') {
|
||||
baseURL = (typeof c.baseURL === 'string' && c.baseURL.trim()) ? c.baseURL.trim() : baseURL
|
||||
apiKey = (typeof c.apiKey === 'string' && c.apiKey.trim()) ? c.apiKey.trim() : (apiKey || 'lm-studio')
|
||||
} else {
|
||||
baseURL = null
|
||||
apiKey = (typeof c.apiKey === 'string' && c.apiKey.trim()) ? c.apiKey.trim() : apiKey
|
||||
}
|
||||
if (typeof c.model === 'string' && c.model.trim()) modelId = c.model.trim()
|
||||
}
|
||||
|
||||
if (!baseURL && !apiKey) {
|
||||
return { error: 'No AI configured. Set connection in Settings (AI) or env: OPENAI_API_KEY or AI_BASE_URL.' }
|
||||
}
|
||||
|
||||
const basePrompt = [
|
||||
typeof prompt === 'string' ? prompt : 'No prompt provided.',
|
||||
context && typeof context === 'string' ? `\n\nAdditional context:\n${context}` : '',
|
||||
Array.isArray(contextNodes) && contextNodes.length > 0
|
||||
? `\n\nContext from connected nodes:\n${contextNodes.map((n) => (n.content != null ? n.content : `${n.id}: (no content)`)).join('\n\n')}`
|
||||
: '',
|
||||
].join('')
|
||||
|
||||
const fullPrompt = reasoningEnabled
|
||||
? basePrompt + '\n\nRespond in exactly two markdown sections. First: "## Reasoning" with your step-by-step reasoning. Then: "## Output" with only the final answer. No preamble.'
|
||||
: basePrompt + '\n\nRespond with structured markdown only. No preamble.'
|
||||
|
||||
const { createOpenAI } = require('@ai-sdk/openai')
|
||||
const openai = createOpenAI({
|
||||
apiKey: apiKey || 'lm-studio',
|
||||
...(baseURL && { baseURL, compatibility: 'compatible' }),
|
||||
})
|
||||
|
||||
return { openai, modelId, fullPrompt }
|
||||
}
|
||||
|
||||
/**
|
||||
* Process agent response
|
||||
*/
|
||||
export function processAgentResponse(result: { text?: string }): AgentResponse {
|
||||
const markdown = result?.text ?? ''
|
||||
return { markdown }
|
||||
}
|
||||
20
backend/tsconfig.json
Normal file
20
backend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user